Chỉnh sửa chữ trên ảnh
Creato il: 13 gennaio 2025
Creato il: 13 gennaio 2025
Thought about text styling in images cho 33 giây
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) để:
Bạn chỉ cần thay thế hai hàm get_vietnamese_font
và add_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:
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()
.draw.rectangle(...)
vẽ nền.fill=(255, 215, 0)
(vàng đậm).stroke_width=2
và stroke_fill=(0, 0, 0)
.max_text_width = img.width - 2 * margin_side
để chừa lề hai bên.textlength
để đo bề rộng thực tế. Nếu vượt max_text_width
thì xuống dòng.img.height - total_text_height - margin_bottom
.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!
Thought about complete code modification cho 17 giây
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)
Thought about text length calculation error cho 34 giây
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 ý:
textbbox
(không kèm stroke_width
) thay vì textlength(..., stroke_width=...)
.stroke_width=2
và stroke_fill=(0,0,0)
để tạo viền đen.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).