일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 우분투
- 서보모터
- 메타
- OpenAI
- ControlNet
- 가상환경
- 뉴럴링크
- javascript
- ChatGPT
- PYTHON
- 이미지 편집
- AI 기술
- 생성형 AI
- AI
- 확산 모델
- 오블완
- 티스토리챌린지
- 트랜스포머
- 시간적 일관성
- 오픈AI
- LORA
- 인공지능
- TRANSFORMER
- ubuntu
- 일론 머스크
- 멀티모달
- 아두이노
- 딥마인드
- LLM
- tts
- Today
- Total
AI 탐구노트
나만의 썸네일 메이커 만들기 - 1편 본문
블로그 글을 써보니 아무런 이미지 없이 글만 덩그러니 있으면 영 없어 보입니다.
사실 Thumbnail 이미지는 대부분 비슷합니다.
파워포인트 표지 슬라이드처럼 말이죠.
하지만, 이런 것들을 잘 사용하면 블로그나 카페의 글이 훨씬 더 풍부함을 얻을 수 있습니다.
그래서, 최근엔 Thumbnail Maker를 사용해서 이미지를 생성하고 이걸 블로그 글머리에 넣었었습니다.
아쉬운 점
이 솔루션이 심플하고 편하고 이쁘고...
다른 건 다 편하고 좋은데 한 가지 아쉬운 부분이 있었습니다.
이미지 URL에 제가 자주 이용하는 Unsplash의 이미지 URL을 입력하면
보이는 화면 상에는 반영된 이미지가 나오는데
'완료 및 이미지화' 버튼을 눌러 이미지 파일로 열어보면
배경이 비어있는 상태의 이미지만 나온다는 것입니다.
그래서, 별도로 나만의 툴을 만들어 보기로 했습니다.
요구사항
제가 필요한 기능은 다음과 같았습니다.
- 제목, 세부제목, 분류를 텍스트로 입력하면 바로 반영되어야 한다.
- 제목, 세부제목, 분류는 각각 다른 색상을 입힐 수 있어야 한다.
- 배경 이미지를 변경할 수 있어야 한다. 다만, url 대신 파일 업로드나 클립보드를 사용하면 좋겠다.
- 배경 이미지가 없을 경우, 그라디언트 색상 변화를 적용한다. 단, 세로 방향만 하진 않았으면 좋겠다.
서버/클라이언트 방식이 아닌 단독 실행 방식의 도구면 좋겠다.
대략 이 정도였던 것 같습니다.
위 내용 가운데 취소선이 적용된 것은 아래에 언급하겠지만 시행착오 끝에 폐기된 것입니다. -_-;
시행착오
이걸 만들기 위해 진행했던 시행착오는 다음과 같습니다.
시행착오-1.어플리케이션 방식 선정
Python GUI 이용 -> Electron 어플 -> Python (FastAPI) + HTML + CSS + Javascript
원래 단독으로 돌아가는 프로그램을 원했고 시도해 본 것은 위의 순서대로 였습니다.
- Python 자체 GUI를 만들 때 Thinker를 이용했는데 너무 멋짐이 없었습니다.
- Electron 방식은 UI는 괜찮은데, 보안 규정 때문인지 제약이 많았습니다.
결국 최종적으로는 Python(FastAPI)는 서버로만 사용하고
HTML/CSS/Javascript로 화면과 기능을 구현하는 것으로 바뀌었습니다.
시행착오-2.캔버스에 글자 중복 찍힘
아래 화면처럼 글자가 여러번 중복해서 찍히는 현상이 발생했습니다.
캔버스의 내용이 초기화되지 않은 상태로 그대로 덧붙여져 발생한 것이었습니다.
그 뒤 비슷한 현상이 한번 더 있었는데 이는 그라디언트와 이미지, 텍스트 그리기의 순서 때문에 발생했었습니다.
시행착오-3.클립보드 이미지 붙여넣기
저는 이미지를 복사해서 배경으로 붙여 넣는 기능으로 구현하고 싶었습니다.
ChatGPT가 구현된 코드로 되어야 할 것 같은데 안 되고 계속해서 막히더군요.
결국은 클립보드를 다루는 대신 이미지 파일을 Drag & Drop 하는 방식으로 변경했습니다.
Drag & Drop 시, 새탭에서 열리는 문제가 발생했는데
브라우저의 기본 동작을 제한하는 방식으로 접근했었습니다.
그런데 거기서도 막힘... 뭐지 싶었는데...
혹시나 하는 마음에 브라우저의 개발자 도구 창을 닫았더니 되더군요. -_-;
일단 여기까지 온 마당에 다시 이전으로 돌아갈 수도 없고 해서...
그대로 Drag & Drop 방식으로 진행했습니다.
코드
python의 FastAPI를 서버로 만들고, html, css, javascript로 화면을 생성하는 구성입니다.
전체 파일 구조는 다음과 같습니다.
코드 : main.py
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
app = FastAPI()
# 정적 파일 제공 (HTML, CSS, JS)
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
def read_root():
return FileResponse("static/index.html")
코드 : index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thumbnail Maker</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<h1>Thumbnail Maker</h1>
<div class="thumbnail-preview">
<canvas id="thumbnailCanvas" width="800" height="400"></canvas>
</div>
<div class="controls">
<!-- 제목 관련 입력 필드 -->
<div class="input-group">
<label for="title">제목:</label>
<input type="text" id="title" placeholder="메일 제목">
<input type="color" id="titleColor" value="#FFFFFF"> <!-- 색상 선택기 -->
</div>
<!-- 세부제목 관련 입력 필드 -->
<div class="input-group">
<label for="subtitle">세부제목:</label>
<input type="text" id="subtitle" placeholder="세부 제목">
<input type="color" id="subtitleColor" value="#FFFFFF"> <!-- 색상 선택기 -->
</div>
<!-- 분류 관련 입력 필드 -->
<div class="input-group">
<label for="category">분류:</label>
<input type="text" id="category" placeholder="분류">
<input type="color" id="categoryColor" value="#FFFFFF"> <!-- 색상 선택기 -->
</div>
<button id="generate">랜덤 그라디언트 생성</button>
<button id="save">이미지 저장</button>
</div>
<div class="drag-drop-area" id="dragDropArea">
<p>여기에 이미지를 끌어다 놓으세요</p>
</div>
</div>
<script src="/static/app.js"></script>
</body>
</html>
코드 : app.js
변경사항
- 약간 코드를 수정해서 제목 아래 줄을 넣었습니다.
- Input 박스나 색상 선택 시 Canvas 내의 내용을 즉시 반영하도록 이벤트 리스너를 조정했습니다.
document.addEventListener("DOMContentLoaded", () => {
const canvas = document.getElementById("thumbnailCanvas");
const ctx = canvas.getContext("2d");
const titleInput = document.getElementById("title");
const subtitleInput = document.getElementById("subtitle");
const categoryInput = document.getElementById("category");
const titleColorPicker = document.getElementById("titleColor");
const subtitleColorPicker = document.getElementById("subtitleColor");
const categoryColorPicker = document.getElementById("categoryColor");
const generateBtn = document.getElementById("generate");
const saveBtn = document.getElementById("save");
const dragDropArea = document.getElementById("dragDropArea");
let backgroundImage = null; // 현재 배경 이미지 저장용 변수
let backgroundGradient = null; // 현재 배경 그라디언트 저장용 변수
const textStyles = {
title: { fontSize: 50, yPosPercent: 50 },
subtitle: { fontSize: 28, yPosPercent: 35 },
category: { fontSize: 20, yPosPercent: 10 }
};
// 랜덤 그라디언트 배경 생성
function generateRandomGradient() {
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255})`);
gradient.addColorStop(1, `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255})`);
backgroundGradient = gradient;
drawText(); // 그라디언트를 그리고 텍스트를 그리기
}
// 텍스트 그리기
function drawText() {
const title = titleInput.value;
const subtitle = subtitleInput.value;
const category = categoryInput.value;
const titleColor = titleColorPicker.value;
const subtitleColor = subtitleColorPicker.value;
const categoryColor = categoryColorPicker.value;
// 캔버스 지우기
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 그라디언트 배경 그리기
if (backgroundGradient) {
ctx.fillStyle = backgroundGradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// 제목 텍스트
// ctx.fillStyle = titleColor;
// ctx.textAlign = "center";
// ctx.font = `bold ${textStyles.title.fontSize}px Arial`;
// ctx.fillText(title, canvas.width / 2, canvas.height - (canvas.height * textStyles.title.yPosPercent) / 100);
ctx.fillStyle = titleColor;
ctx.textAlign = 'center';
ctx.font = `bold ${textStyles.title.fontSize}px Arial`;
const titleYPos = canvas.height - (canvas.height * textStyles.title.yPosPercent) / 100;
ctx.fillText(title, canvas.width / 2, titleYPos);
// 제목 아래 줄 그리기
if (title) {
const textWidth = ctx.measureText(title).width;
const lineYPos = titleYPos + 10; // 제목 글자와의 간격 10px
ctx.strokeStyle = titleColor;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo((canvas.width - textWidth) / 2, lineYPos);
ctx.lineTo((canvas.width + textWidth) / 2, lineYPos);
ctx.stroke();
}
// 세부제목 텍스트
ctx.fillStyle = subtitleColor;
ctx.font = `bold ${textStyles.subtitle.fontSize}px Arial`;
ctx.fillText(subtitle, canvas.width / 2, canvas.height - (canvas.height * textStyles.subtitle.yPosPercent) / 100);
// 분류 텍스트
ctx.fillStyle = categoryColor;
ctx.font = `bold ${textStyles.category.fontSize}px Arial`;
ctx.fillText(category, canvas.width / 2, canvas.height - (canvas.height * textStyles.category.yPosPercent) / 100);
}
// 모든 텍스트 입력 시 즉시 반영되도록 이벤트 리스너 추가
titleInput.addEventListener('input', drawText);
subtitleInput.addEventListener('input', drawText);
categoryInput.addEventListener('input', drawText);
titleColorPicker.addEventListener('input', drawText);
subtitleColorPicker.addEventListener('input', drawText);
categoryColorPicker.addEventListener('input', drawText);
// 이미지 파일을 드래그 앤 드롭하여 캔버스에 배경으로 설정
dragDropArea.addEventListener('dragover', (event) => {
event.preventDefault(); // 기본 동작 방지
dragDropArea.classList.add('dragging');
});
dragDropArea.addEventListener('dragleave', () => {
dragDropArea.classList.remove('dragging');
});
dragDropArea.addEventListener('drop', (event) => {
event.preventDefault(); // 기본 동작 방지
event.stopPropagation(); // 브라우저의 기본 동작을 차단하여 탭에서 열리지 않도록 함
dragDropArea.classList.remove('dragging');
const file = event.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
const img = new Image();
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
img.onload = () => {
backgroundImage = img; // 이미지 배경 저장
backgroundGradient = null; // 그라디언트를 제거
drawText(); // 배경 이미지를 그리고 텍스트 다시 그리기
};
};
reader.readAsDataURL(file);
}
});
generateBtn.addEventListener("click", generateRandomGradient);
saveBtn.addEventListener("click", () => {
const link = document.createElement("a");
link.download = "thumbnail.png";
link.href = canvas.toDataURL();
link.click();
});
// 페이지 로드 시 기본 그라디언트 배경 생성
generateRandomGradient();
// 초기 텍스트 그리기
drawText();
});
코드 : style.css
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
margin-bottom: 20px;
}
.thumbnail-preview {
margin-bottom: 20px;
}
canvas {
border: 2px solid #ccc;
}
.controls {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 20px;
width: 100%; /* 전체 너비 */
max-width: 600px; /* 최대 너비 제한 */
align-items: flex-start; /* 텍스트 박스와 버튼을 왼쪽 정렬 */
}
.input-group {
display: flex;
align-items: center;
gap: 10px; /* 각 요소 간의 간격 추가 */
width: 100%; /* 한 줄에 꽉 차도록 설정 */
}
label {
font-size: 16px;
font-weight: bold;
width: 100px; /* 레이블의 고정 너비 설정 */
}
input[type="text"] {
padding: 10px;
font-size: 16px;
width: 100%; /* 입력 필드가 가능한 넓게 확장되도록 설정 */
flex: 1;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="color"] {
width: 60px;
height: 40px;
border: none;
cursor: pointer;
flex-shrink: 0; /* 색상 선택기의 크기를 고정 */
}
button {
padding: 10px 20px;
font-size: 16px;
border: none;
background-color: #007bff;
color: white;
border-radius: 5px;
cursor: pointer;
width: 100%;
max-width: 320px;
text-align: center;
}
button:hover {
background-color: #0056b3;
}
.drag-drop-area {
margin-top: 20px;
padding: 20px;
border: 2px dashed #007bff;
width: 100%;
max-width: 600px;
text-align: center;
color: #007bff;
background-color: #f9f9f9;
border-radius: 10px;
}
.drag-drop-area p {
margin: 0;
font-size: 16px;
}
.drag-drop-area.dragging {
background-color: #cce7ff;
border-color: #0056b3;
}
결과 확인
최종적으로 만들어진 화면은 아래와 같습니다.
생각보다 깔끔하게 잘 나왔습니다.
사실 이 정도 나오기까지 많은 시행착오가 있었습니다. -_-;
제공되는 기능은 다음과 같습니다.
- 제목, 세부제목, 분류 텍스트 별 색상을 별도로 설정할 수 있습니다.
- 랜덤 그라디언트는 세로, 가로 가리지 않고 막~ 만듭니다... -_-;
- 배경 이미지는 끌어다 놓으면 반영됩니다.
Unsplash에서 이미지를 다운받아 배치해 보면 다음과 같이 됩니다.
이번 글은 Thumbnail Maker 만들어보기를 해 봤습니다.
앞으로 블로그 글 쓸 때 이것을 많이 활용해 볼 생각입니다. ^^
'DIY 테스트' 카테고리의 다른 글
썸네일 메이커 Electron 어플리케이션으로 전환하기 (1) | 2024.10.09 |
---|---|
AudioCraft를 이용한 효과음 만들어 보기 (5) | 2024.10.09 |
Flux.1 설치 및 테스트 (2) | 2024.10.06 |
복수의 인원이 포함된 영상에서의 안면 비식별화 (0) | 2024.10.04 |
퀴즈 : 국기보고 나라 이름 맞추기 게임 만들기 (9) | 2024.10.02 |