Chỉnh sửa chữ trên ảnh

Tạo vào: 13 tháng 1, 2025

Trả lời bằng OpenAI 01 bởi OpenAI01

Câu hỏi

Ảnh màn hình 2025-01-14 lúc 00.37.16.pngẢnh màn hình 2025-01-14 lúc 00.37.16.png
Chỉnh sửa đoạn code dưới đây, bỏ đi phần nền của chữ, chuyền thành chữ vàng in đậm, viền đen như trong hình ảnh được cung cấp, căn giữa ảnh và nếu chữ vượt quá độ dài của hình thì xuống dòng nhưng luôn đảm bảo cách viền dưới ảnh một khoảng cố định. code cũ: import csv
import os
import platform
import requests
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO

# Đường dẫn logo cần chèn
LOGO_PATH = "/Users/ngophungkhanh/Downloads/vf68-logo.png"

def get_vietnamese_font(size=30):
"""
Trả về font Arial (có hỗ trợ tiếng Việt) trên macOS.
Nếu không tìm thấy, fallback sang font mặc định của Pillow.
"""
font_path_macos = "/System/Library/Fonts/Supplemental/Arial.ttf"
# Nếu đang trên macOS và file Arial.ttf tồn tại:
if platform.system() == "Darwin" and os.path.exists(font_path_macos):
return ImageFont.truetype(font_path_macos, size=size)
else:
return ImageFont.load_default()

def download_image(url):
"""
Tải ảnh từ URL, trả về đối tượng Pillow Image nếu thành công,
hoặc None nếu thất bại (lỗi mạng, URL không đúng, v.v.).
"""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
img = Image.open(BytesIO(response.content))
return img
except:
return None

def resize_image(img, fixed_width=800):
"""
Resize ảnh về chiều rộng fixed_width, giữ nguyên tỉ lệ.
"""
width, height = img.size
if width != fixed_width:
ratio = fixed_width / float(width)
new_height = int(height * ratio)
# Image.LANCZOS cho chất lượng tốt (Pillow >= 9)
img = img.resize((fixed_width, new_height), Image.LANCZOS)
return img

def add_title_to_image(img, title):
"""
Chèn text (title) vào cuối ảnh:
- Font chữ size=30 (nếu có Arial) hoặc fallback.
- Text trắng, viền đen.
- Nền tím tràn hết chiều ngang, padding trên dưới ~7px.
- Cách mép dưới ảnh 20px.
"""
# Đảm bảo ảnh là RGB hoặc RGBA trước khi vẽ (tránh lỗi palette)
if img.mode not in ("RGB", "RGBA"):
img = img.convert("RGB")

draw = ImageDraw.Draw(img)
font = get_vietnamese_font(size=30)

# Tính kích thước text (dùng textbbox)
bbox = draw.textbbox((0, 0), title, font=font, stroke_width=1)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]

# Tính vị trí để text ở giữa, cách mép dưới 20px
x = (img.width - text_w) // 2
y = img.height - text_h - 20

# Vẽ nền tím tràn hết chiều ngang
padding_top_bottom = 7
rect_x1 = 0
rect_y1 = y - padding_top_bottom
rect_x2 = img.width
rect_y2 = y + text_h + padding_top_bottom
draw.rectangle(
[rect_x1, rect_y1, rect_x2, rect_y2],
fill=(12, 192, 223) # Màu tím
)

# Vẽ text (stroke_width=1)
draw.text(
(x, y),
title,
fill=(255, 255, 255),
font=font,
stroke_width=1,
stroke_fill=(0, 0, 0),
)

return img

def add_logo(img, logo_path):
"""
Chèn logo vào góc trái trên của ảnh, chiếm ~1/6 chiều rộng.
Logo có thể có alpha, do đó ta chuyển cả logo và ảnh nền sang RGBA khi cần.
"""
if not os.path.exists(logo_path):
print("Không tìm thấy file logo:", logo_path)
return img

try:
# Mở logo, chuyển sang RGBA
logo = Image.open(logo_path).convert("RGBA")
except Exception as e:
print("Không thể mở hoặc convert logo:", logo_path, e)
return img

# Ảnh chính nếu không phải RGBA, chuyển sang RGBA để paste logo
if img.mode != "RGBA":
img = img.convert("RGBA")

# Tính toán kích thước logo (chiếm 1/6 chiều rộng ảnh)
desired_logo_width = img.width // 6
logo_ratio = logo.height / float(logo.width)
new_logo_height = int(desired_logo_width * logo_ratio)
logo_resized = logo.resize((desired_logo_width, new_logo_height), Image.LANCZOS)

# Dán logo (logo_resized) lên ảnh (img), góc trái trên, margin 10px
margin = 10
pos_x = margin
pos_y = margin
img.paste(logo_resized, (pos_x, pos_y), logo_resized)

# Sau khi xong, chuyển ảnh về RGB để lưu JPEG
if img.mode == "RGBA":
img = img.convert("RGB")

return img

def main(csv_file_path):
"""
CSV gồm 4 cột: [Title, Slug, imageUrl, Folder].
- Title: chèn vào ảnh
- Slug: đặt tên file ảnh
- imageUrl: URL tải ảnh
- Folder: thư mục lưu ảnh
"""
with open(csv_file_path, mode='r', encoding='utf-8-sig') as f:
reader = csv.reader(f)
next(reader, None) # Bỏ qua dòng tiêu đề (header) nếu có

for row in reader:
if len(row) < 4:
print("Dòng CSV không đủ 4 cột, bỏ qua:", row)
continue

title, slug, image_url, folder_name = row[0], row[1], row[2], row[3]

# Tải ảnh
img = download_image(image_url)
if not img:
print(f"Lỗi khi tải ảnh từ URL: {image_url}. Bỏ qua.")
continue

# Để chắc chắn, nếu ảnh là P (hoặc khác), chuyển sang RGB
if img.mode not in ("RGB", "RGBA"):
img = img.convert("RGB")

# Resize ảnh
img = resize_image(img, 800)

# Chèn chữ (Title)
img = add_title_to_image(img, title)

# Chèn logo vào góc trái trên
img = add_logo(img, LOGO_PATH)

# Tạo folder nếu chưa có
if not os.path.exists(folder_name):
os.makedirs(folder_name)

# Dùng slug để đặt tên file ảnh (loại bỏ ký tự đặc biệt)
safe_slug = "".join(c for c in slug if c.isalnum() or c in (" ", "-", "_")).rstrip()
filename = f"{safe_slug}.jpg"
save_path = os.path.join(folder_name, filename)

# Lưu ảnh
img.save(save_path)
print(f"Đã xử lý và lưu ảnh: {save_path}")

if __name__ == "__main__":
# Thay đường dẫn CSV cho phù hợp
csv_file_path = "/Users/ngophungkhanh/Downloads/data.csv"
main(csv_file_path)

Suy nghĩ

Thought about text styling in images cho 33 giây

Trả lời

Dưới đây là ví dụ cách chỉnh sửa hàm add_title_to_image (và bổ sung thêm một hàm lấy font chữ đậm) để:

  1. Bỏ nền (không vẽ hình chữ nhật phía sau).
  2. Chữ vàng đậm, có viền đen.
  3. Canh giữa (center) cho dòng (hoặc nhiều dòng) văn bản.
  4. Xuống dòng nếu vượt quá chiều ngang ảnh.
  5. Giữ khoảng cách cố định với viền dưới của ảnh.

Bạn chỉ cần thay thế hai hàm get_vietnamese_fontadd_title_to_image cũ bằng đoạn mã dưới đây (các hàm còn lại trong code giữ nguyên):

import textwrap def get_vietnamese_bold_font(size=30): """ Trả về font Arial Bold (có hỗ trợ tiếng Việt) trên macOS. Nếu không tìm thấy, fallback sang font mặc định của Pillow. """ font_path_macos_bold = "/System/Library/Fonts/Supplemental/Arial Bold.ttf" if platform.system() == "Darwin" and os.path.exists(font_path_macos_bold): return ImageFont.truetype(font_path_macos_bold, size=size) else: return ImageFont.load_default() def add_title_to_image(img, title): """ Chèn text vào cuối ảnh, chữ vàng đậm, viền đen, canh giữa, tự xuống dòng nếu quá dài, và giữ khoảng cách cố định so với đáy ảnh. """ # Đảm bảo ảnh ở mode RGB/RGBA if img.mode not in ("RGB", "RGBA"): img = img.convert("RGB") draw = ImageDraw.Draw(img) # Lấy font đậm font = get_vietnamese_bold_font(size=30) # Quy định khoảng cách lề hai bên để tránh bị cắt chữ margin_side = 40 # Quy định khoảng cách từ đáy ảnh margin_bottom = 50 # Xác định độ rộng tối đa cho một dòng chữ max_text_width = img.width - 2 * margin_side # Tách câu thành các dòng nhỏ nếu quá rộng # Ta sẽ dùng vòng lặp thủ công hoặc `textwrap.wrap` lines = [] # Sử dụng textwrap cho đơn giản (cần import textwrap ở đầu file) wrapped_text = textwrap.wrap(title, width=1000) # width lớn, sau đó ta tự cắt theo pixel # Với mỗi "dòng" do textwrap cắt, ta kiểm tra lại độ dài pixel thực tế for candidate_line in wrapped_text: # Nếu dòng này còn dài quá so với max_text_width thì ta cắt nhỏ thêm words = candidate_line.split() current_line = words[0] for w in words[1:]: # Thử gộp thêm w vào dòng hiện tại if draw.textlength(current_line + " " + w, font=font, stroke_width=2) <= max_text_width: current_line += " " + w else: lines.append(current_line) current_line = w lines.append(current_line) # Tính tổng chiều cao text khi đã xuống dòng # Ta đo chiều cao của 1 dòng mẫu rồi nhân với số dòng line_height = draw.textbbox((0, 0), "Hg", font=font, stroke_width=2)[3] total_text_height = line_height * len(lines) # Vị trí dòng đầu tiên (theo chiều dọc) - cách đáy ảnh một khoảng margin_bottom y = img.height - total_text_height - margin_bottom # Vẽ lần lượt từng dòng, canh giữa for line in lines: line_width = draw.textlength(line, font=font, stroke_width=2) x = (img.width - line_width) / 2 draw.text( (x, y), line, fill=(255, 215, 0), # Màu vàng đậm font=font, stroke_width=2, # Độ dày viền stroke_fill=(0, 0, 0) # Màu viền đen ) y += line_height # chuyển xuống dòng kế tiếp return img

Giải thích chính:

  • Font đậm: Tạo hàm get_vietnamese_bold_font để ưu tiên dùng Arial Bold.ttf trên macOS (nếu có), nếu không thì dùng ImageFont.load_default().
  • Bỏ nền: Không còn phần draw.rectangle(...) vẽ nền.
  • Màu chữ: Đặt fill=(255, 215, 0) (vàng đậm).
  • Viền đen: Đặt stroke_width=2stroke_fill=(0, 0, 0).
  • Canh giữa và xuống dòng:
    • Tính max_text_width = img.width - 2 * margin_side để chừa lề hai bên.
    • Dùng textlength để đo bề rộng thực tế. Nếu vượt max_text_width thì xuống dòng.
    • Tính tổng chiều cao các dòng và bắt đầu vẽ từ img.height - total_text_height - margin_bottom.
    • Mỗi dòng được canh giữa bằng cách x = (img.width - line_width) / 2.

Bạn chỉ cần thay các hàm này vào code gốc, những phần khác (download ảnh, resize, lưu,...) giữ nguyên như cũ. Sau đó chạy lại là sẽ có kết quả đúng yêu cầu. Chúc bạn thành công!

Câu hỏi

Viết hoàn chỉnh toàn bộ code, bỏ đi các chú thích, đừng lười biếng.

Suy nghĩ

Thought about complete code modification cho 17 giây

Trả lời

import csv import os import platform import requests from PIL import Image, ImageDraw, ImageFont from io import BytesIO import textwrap LOGO_PATH = "/Users/ngophungkhanh/Downloads/vf68-logo.png" def get_vietnamese_bold_font(size=30): font_path_macos_bold = "/System/Library/Fonts/Supplemental/Arial Bold.ttf" if platform.system() == "Darwin" and os.path.exists(font_path_macos_bold): return ImageFont.truetype(font_path_macos_bold, size=size) else: return ImageFont.load_default() def download_image(url): try: response = requests.get(url, timeout=10) response.raise_for_status() return Image.open(BytesIO(response.content)) except: return None def resize_image(img, fixed_width=800): width, height = img.size if width != fixed_width: ratio = fixed_width / float(width) new_height = int(height * ratio) img = img.resize((fixed_width, new_height), Image.LANCZOS) return img def add_title_to_image(img, title): if img.mode not in ("RGB", "RGBA"): img = img.convert("RGB") draw = ImageDraw.Draw(img) font = get_vietnamese_bold_font(size=30) margin_side = 40 margin_bottom = 50 max_text_width = img.width - 2 * margin_side lines = [] wrapped_text = textwrap.wrap(title, width=1000) for candidate_line in wrapped_text: words = candidate_line.split() current_line = words[0] for w in words[1:]: if draw.textlength(current_line + " " + w, font=font, stroke_width=2) <= max_text_width: current_line += " " + w else: lines.append(current_line) current_line = w lines.append(current_line) line_height = draw.textbbox((0, 0), "Hg", font=font, stroke_width=2)[3] total_text_height = line_height * len(lines) y = img.height - total_text_height - margin_bottom for line in lines: line_width = draw.textlength(line, font=font, stroke_width=2) x = (img.width - line_width) / 2 draw.text((x, y), line, fill=(255, 215, 0), font=font, stroke_width=2, stroke_fill=(0, 0, 0)) y += line_height return img def add_logo(img, logo_path): if not os.path.exists(logo_path): return img try: logo = Image.open(logo_path).convert("RGBA") except: return img if img.mode != "RGBA": img = img.convert("RGBA") desired_logo_width = img.width // 6 logo_ratio = logo.height / float(logo.width) new_logo_height = int(desired_logo_width * logo_ratio) logo_resized = logo.resize((desired_logo_width, new_logo_height), Image.LANCZOS) margin = 10 pos_x = margin pos_y = margin img.paste(logo_resized, (pos_x, pos_y), logo_resized) if img.mode == "RGBA": img = img.convert("RGB") return img def main(csv_file_path): with open(csv_file_path, mode='r', encoding='utf-8-sig') as f: reader = csv.reader(f) next(reader, None) for row in reader: if len(row) < 4: continue title, slug, image_url, folder_name = row[0], row[1], row[2], row[3] img = download_image(image_url) if not img: continue if img.mode not in ("RGB", "RGBA"): img = img.convert("RGB") img = resize_image(img, 800) img = add_title_to_image(img, title) img = add_logo(img, LOGO_PATH) if not os.path.exists(folder_name): os.makedirs(folder_name) safe_slug = "".join(c for c in slug if c.isalnum() or c in (" ", "-", "_")).rstrip() filename = f"{safe_slug}.jpg" save_path = os.path.join(folder_name, filename) img.save(save_path) if __name__ == "__main__": csv_file_path = "/Users/ngophungkhanh/Downloads/data.csv" main(csv_file_path)

Câu hỏi

Lỗi: Traceback (most recent call last):
File "/Users/ngophungkhanh/Downloads/Test/cabin.py", line 110, in <module>
main(csv_file_path)
File "/Users/ngophungkhanh/Downloads/Test/cabin.py", line 99, in main
img = add_title_to_image(img, title)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/ngophungkhanh/Downloads/Test/cabin.py", line 48, in add_title_to_image
if draw.textlength(current_line + " " + w, font=font, stroke_width=2) <= max_text_width:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: ImageDraw.textlength() got an unexpected keyword argument 'stroke_width'

Suy nghĩ

Thought about text length calculation error cho 34 giây

Trả lời

Dưới đây là phiên bản code đã được chỉnh sửa để tránh lỗi TypeError: ImageDraw.textlength() got an unexpected keyword argument 'stroke_width'. Thay vì dùng textlength(...) kèm stroke_width, ta sẽ dùng textbbox(...) (không kèm stroke_width) để đo chiều rộng dòng. Phần chữ vẫn giữ được viền đen nhờ tham số stroke_width trong lúc vẽ:

import csv import os import platform import requests from PIL import Image, ImageDraw, ImageFont from io import BytesIO import textwrap LOGO_PATH = "/Users/ngophungkhanh/Downloads/vf68-logo.png" def get_vietnamese_bold_font(size=30): font_path_macos_bold = "/System/Library/Fonts/Supplemental/Arial Bold.ttf" if platform.system() == "Darwin" and os.path.exists(font_path_macos_bold): return ImageFont.truetype(font_path_macos_bold, size=size) else: return ImageFont.load_default() def download_image(url): try: response = requests.get(url, timeout=10) response.raise_for_status() return Image.open(BytesIO(response.content)) except: return None def resize_image(img, fixed_width=800): width, height = img.size if width != fixed_width: ratio = fixed_width / float(width) new_height = int(height * ratio) img = img.resize((fixed_width, new_height), Image.LANCZOS) return img def add_title_to_image(img, title): if img.mode not in ("RGB", "RGBA"): img = img.convert("RGB") draw = ImageDraw.Draw(img) font = get_vietnamese_bold_font(size=30) margin_side = 40 margin_bottom = 50 max_text_width = img.width - 2 * margin_side lines = [] wrapped_text = textwrap.wrap(title, width=1000) # Cắt từng dòng sao cho không vượt quá max_text_width for candidate_line in wrapped_text: words = candidate_line.split() current_line = words[0] for w in words[1:]: test_line = current_line + " " + w bbox_test = draw.textbbox((0, 0), test_line, font=font) # đo khung (x1, y1, x2, y2) line_width = bbox_test[2] - bbox_test[0] if line_width <= max_text_width: current_line = test_line else: lines.append(current_line) current_line = w lines.append(current_line) # Tính chiều cao cho mỗi dòng (dùng textbbox không stroke_width để đo) bbox_line_height = draw.textbbox((0, 0), "Hg", font=font) line_height = bbox_line_height[3] - bbox_line_height[1] total_text_height = line_height * len(lines) y = img.height - total_text_height - margin_bottom # Vẽ từng dòng, canh giữa, chữ vàng, viền đen stroke_width=2 for line in lines: bbox_line = draw.textbbox((0, 0), line, font=font) line_width = bbox_line[2] - bbox_line[0] x = (img.width - line_width) / 2 draw.text( (x, y), line, fill=(255, 215, 0), font=font, stroke_width=2, stroke_fill=(0, 0, 0) ) y += line_height return img def add_logo(img, logo_path): if not os.path.exists(logo_path): return img try: logo = Image.open(logo_path).convert("RGBA") except: return img if img.mode != "RGBA": img = img.convert("RGBA") desired_logo_width = img.width // 6 logo_ratio = logo.height / float(logo.width) new_logo_height = int(desired_logo_width * logo_ratio) logo_resized = logo.resize((desired_logo_width, new_logo_height), Image.LANCZOS) margin = 10 pos_x = margin pos_y = margin img.paste(logo_resized, (pos_x, pos_y), logo_resized) if img.mode == "RGBA": img = img.convert("RGB") return img def main(csv_file_path): with open(csv_file_path, mode='r', encoding='utf-8-sig') as f: reader = csv.reader(f) next(reader, None) for row in reader: if len(row) < 4: continue title, slug, image_url, folder_name = row[0], row[1], row[2], row[3] img = download_image(image_url) if not img: continue if img.mode not in ("RGB", "RGBA"): img = img.convert("RGB") img = resize_image(img, 800) img = add_title_to_image(img, title) img = add_logo(img, LOGO_PATH) if not os.path.exists(folder_name): os.makedirs(folder_name) safe_slug = "".join(c for c in slug if c.isalnum() or c in (" ", "-", "_")).rstrip() filename = f"{safe_slug}.jpg" save_path = os.path.join(folder_name, filename) img.save(save_path) if __name__ == "__main__": csv_file_path = "/Users/ngophungkhanh/Downloads/data.csv" main(csv_file_path)

Điểm chính cần lưu ý:

  • Để đo độ dài dòng, ta dùng textbbox (không kèm stroke_width) thay vì textlength(..., stroke_width=...).
  • Khi vẽ chữ, vẫn có thể dùng stroke_width=2stroke_fill=(0,0,0) để tạo viền đen.
  • Nếu bạn tiếp tục gặp lỗi với textbbox(..., stroke_width=2), hãy bỏ stroke_width trong lúc đo chiều cao dòng (hoặc cập nhật Pillow lên phiên bản mới hơn).

Chia sẻ Q&A này