介绍一款自动化图文生成项目

介绍一款自动化图文生成项目

  在这个视觉化的时代,一张好的图片配上恰当的文字往往能够更好地传达情感和信息。今天,我要向大家介绍小艺术家工作室™最新研发的一个项目——一个能够自动化地为图片配上文字的集成系统。

项目概述

  这个项目的核心功能是将一系列图片与相应的文字描述相结合,生成具有视觉冲击力的图文内容。它可以广泛应用于心灵禅语、励志名言、社交媒体配图等多种场景。用户可以轻松地为数百张不同的图片配上数百句独特的文字,每一张图片都是独立的艺术品。

项目结构

  本项目由多个Python脚本文件构成,它们协同工作,完成从图片处理到图文生成的全过程。

目录结构

  • **sources/images/{主题名}**:存放用户上传的图片。
  • **sources/text_data/{主题名}.txt**:包含与图片对应的文字描述。
  • **sources/fonts/{字体名}.ttf**:包含与图片对应的文字描述。

关键步骤

  1. 创建主题目录:通过运行create_topic_directories.py脚本,在sources/images/下创建一个新的主题文件夹。
  2. 放置图片:在创建好的主题文件夹中添加图片。
  3. 创建文本数据:在sources/text_data/下创建一个与主题名称一致的文本文件,输入相应的文字描述。
  4. 裁剪图片:使用cut_images_new.py脚本裁剪图片到指定尺寸。
  5. 加深图片颜色:通过darken_images.py脚本对图片进行颜色加深处理。
  6. 生成图文:运行main.py脚本,根据文本文件中的文字和图片生成图文结合的输出。

脚本功能详解

**create_topic_dirs.py**:初始化新的图文主题目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os
from helper import create_new_topic_dirs

def main():
topic = "test"

# 获取当前工作目录作为项目目录
project_dir = os.getcwd()

# 创建新主题目录
create_new_topic_dirs(topic, project_dir)
print(f"Directories for topic '{topic}' have been created.")

if __name__ == "__main__":
main()

**cut_images_new.py**:将图片裁剪到适合图文展示的尺寸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import os
from helper import cut_images

def main():
# 获取当前工作目录
current_dir = os.getcwd()

# 定义图片的输入和输出文件夹路径
images_folder = os.path.join(current_dir, "sources/images/test")
images_folder_cropped = os.path.join(current_dir, "sources/images/test/cropped")

# 确保输出文件夹存在
if not os.path.exists(images_folder_cropped):
os.makedirs(images_folder_cropped)

# 裁剪图片
cut_images(images_folder, images_folder_cropped)
print("Images have been cropped.")

if __name__ == "__main__":
main()

**darken_images.py**:调整图片颜色,使其更适合文字的展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
from helper import darken_images

def main():
# 获取当前工作目录
current_dir = os.getcwd()

# 定义裁剪后的图片文件夹路径
images_folder_cropped = os.path.join(current_dir, "sources/images/test/cropped")

# 定义加深颜色后的图片输出文件夹路径
images_folder_cropped_darken = os.path.join(current_dir, "sources/images/test/cropped/darken")

# 确保输出文件夹存在
if not os.path.exists(images_folder_cropped_darken):
os.makedirs(images_folder_cropped_darken)

# 加深图片颜色
darken_images(images_folder_cropped, images_folder_cropped_darken)
print("Images have been darkened.")

if __name__ == "__main__":
main()

**process_images.py**:将create_topic_dirs.pycut_images_new.pydarken_images.py合并为一步,避免繁琐。

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
32
import os
from helper import create_new_topic_dirs, cut_images, darken_images

def create_topic_dirs():
topic = "test"
project_dir = os.getcwd()
create_new_topic_dirs(topic, project_dir)
print(f"Directories for topic '{topic}' have been created.")

def cut_images_new():
images_folder = os.path.join(os.getcwd(), "sources/images/test")
images_folder_cropped = os.path.join(os.getcwd(), "sources/images/test/cropped")
if not os.path.exists(images_folder_cropped):
os.makedirs(images_folder_cropped)
cut_images(images_folder, images_folder_cropped)
print("Images have been cropped.")

def darken_images_new():
images_folder_cropped = os.path.join(os.getcwd(), "sources/images/test/cropped")
images_folder_cropped_darken = os.path.join(os.getcwd(), "sources/images/test/cropped/darken")
if not os.path.exists(images_folder_cropped_darken):
os.makedirs(images_folder_cropped_darken)
darken_images(images_folder_cropped, images_folder_cropped_darken)
print("Images have been darkened.")

def main():
create_topic_dirs()
cut_images_new()
darken_images_new()

if __name__ == "__main__":
main()

**main.py**:项目的主执行脚本,负责调用其他脚本并生成最终的图文输出。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import os
import post_handler
import helper


TOPIC = "test" # Available topics: christian, fitness
SHOW_AUTHOR = False # Shows the author of the quote under the quote (if available)
CUSTOMER_NAME = "20241002"
NUM_OF_POSTS = 35 # Disable limit: -1 (will create images according to the amount of quotes in the .txt file)

''' To create a new topic, please follow these steps:
1. Create a {topic}.txt file inside /sources/text_data
2. Run "helper.create_new_topic_dirs(TOPIC, project_dir)" to auto create all the directories needed
3. Add images to /sources/images/{topic}
4. Run "helper.cut_images_new(images_folder, images_folder_cropped)" to crop the images to 1080 X 1350
(you can change the dimension inside the function)
5. Run "helper.darken_images(images_folder_cropped, images_folder_cropped_darken)" if you want to make the images darker
(it makes the text look better)
ANS THAT'S IT! :)
Feel free to create a Pull Request if you want to help others as well!
'''

# Define the paths and values to everything
project_dir = os.getcwd().replace("\\", "/")
images_folder = f"{project_dir}/sources/images/{TOPIC}"
images_folder_cropped = f"{images_folder}/cropped"
images_folder_cropped_darken = f"{images_folder_cropped}/darken"
text_file = f"{project_dir}/sources/text_data/{TOPIC}.txt"
quote_font = f"{project_dir}/sources/fonts/FZMiFXSJW.TTF" # Bible
# quote_font = f"{project_dir}/sources/fonts/Bebas-KM7y.ttf" # Fitness
author_font = f"{project_dir}/sources/fonts/MangabeyRegular-rgqVO.otf"
output_folder = f"{project_dir}/generated/{TOPIC}"
logo_file = f"{project_dir}/sources/logo.png"


if __name__ == "__main__":
# helper.create_new_topic_dirs(TOPIC, project_dir)
# helper.fix_text_syntax(quote_font, text_file) # Goes through the .txt file and fixes chars for some fonts
# helper.cut_images_new(images_folder, images_folder_cropped)
# helper.darken_images(images_folder_cropped, images_folder_cropped_darken)

# LOGO
post_handler.create_posts(images_folder=images_folder_cropped_darken, text_file=text_file,
quote_font=quote_font, author_font=author_font, output_folder=output_folder,
logo_file=logo_file, customer_name=CUSTOMER_NAME, number_of_posts=NUM_OF_POSTS, show_author=SHOW_AUTHOR)

# NO LOGO
# post_handler.create_posts(images_folder=images_folder_cropped_darken, text_file=text_file,
# quote_font=quote_font, author_font=author_font, output_folder=output_folder,
# customer_name=CUSTOMER_NAME, number_of_posts=NUM_OF_POSTS, show_author=SHOW_AUTHOR)

**post_handler.py**:处理图文生成的具体逻辑。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import os
import random
import re
import textwrap
import time
from string import ascii_letters

from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance

import helper
import json_handler


def create_dirs(output_folder, customer_name):
# create a folder for this customer if it doesn't exist
output_path = f"{output_folder}/{customer_name}"
if not os.path.exists(output_path):
os.makedirs(output_path)
# Create folder inside for images
# if not os.path.exists(f"{output_path}/verse_images"):
# os.makedirs(f"{output_path}/verse_images")
return output_path


def create_posts(images_folder, text_file, quote_font, author_font, output_folder, customer_name, number_of_posts, logo_file: str = None, show_author : bool = False):
# json_data = json_handler.get_data(json_file)
# verses: str = json_data[0]
# refs: str = json_data[1]

# Read the text file into a list of lines
with open(text_file, 'r', encoding='utf-8') as file:
quotes = file.readlines()

# If number_of_posts is set to -1, it will do it for the amount of quotes there is in the data file
if number_of_posts == -1:
number_of_posts = len(quotes) - 1

run_time_average = 0
if number_of_posts > 1:
start_time_total = time.time()

# Get list of photos in the specified folder and shuffle it
image_num = list()
image_files = [f"{images_folder}/{file}" for file in os.listdir(images_folder) if file.endswith(".jpg") or file.endswith(".png") or file.endswith(".jpeg")]
random_for_image = random.randint(0, len(image_files) - 1)
for i in range(number_of_posts):
image_num.append((random_for_image + i) % len(image_files))
random.shuffle(image_num)

# Data for spreadsheet
spreadsheet_col1 = list()
spreadsheet_col2 = list()
spreadsheet_col3 = list()

# Creating folder for customer
output_path = create_dirs(output_folder, customer_name)

for i in range(number_of_posts):
start_time = time.time()
print(f"Creating Post #{i}")

author_text = ""
quote_text = ""
text = quotes[i]
quote = text.split(":::")
quote_text = quote[0]
if show_author and quote_text != text:
author_text = quote[1]
if author_text.rstrip(" ") == "":
author_text = None

# Choose a random image file from the list
random_image_num = image_num[0]
del image_num[0]
image_file = image_files[random_image_num]

image_license = image_file.split('/')
image_license = image_license[len(image_license)-1]
image_license = image_license.split('-')
image_license = image_license[len(image_license)-1].rstrip(".jpg")

file_name = f"/{i}-{image_license}.jpg"
create_post(image_file=image_file, quote_text=quote_text,
quote_font=quote_font, author_font=author_font, output_path=output_path, file_name=file_name,
logo_file=logo_file, customer_name=customer_name, author_text=author_text)

# Add to spreadsheet
spreadsheet_col1.append(file_name.strip("/"))
spreadsheet_col2.append(author_text)
spreadsheet_col3.append(quote_text)

end_time = time.time()
run_time = end_time - start_time
run_time_average += run_time
print(f"\033[0;34m DONE #{i}, Run time:", round(run_time, 2), "seconds! \033[0m", output_path)

helper.add_sheets(file_names=spreadsheet_col1, output_path=output_path, customer_name=customer_name,
authors=spreadsheet_col2, quotes=spreadsheet_col3)

if number_of_posts > 1:
run_time_average /= number_of_posts
end_time_total = time.time()
run_time_total = end_time_total - start_time_total
print(f"\n\033[0;32mDone making {number_of_posts} posts for {customer_name}!"
f"\nTotal run time:", round(run_time_total, 2), "seconds = ", round(run_time_total / 60, 2), " minutes!",
f"\nAverage run time:", round(run_time_average, 2), "seconds!\033[0m")


def create_post(image_file, quote_text, quote_font, author_font, output_path, file_name, logo_file, customer_name, author_text: str = None):
# Open specific image
img = Image.open(image_file)

# Load selected font
quote_font = ImageFont.truetype(font=f'{quote_font}', size=75)

# Create DrawText object
draw = ImageDraw.Draw(im=img)

# Define our text:
# Calculate the average length of a single character of our font.
# Note: this takes into account the specific font and font size.
# avg_char_width = sum(font.getsize(char)[0] for char in ascii_letters) / len(ascii_letters)

# Translate this average length into a character count
# max_char_count = int(img.size[0] / avg_char_width)
max_char_count = 12
# Create a wrapped text object using scaled character count
new_text = textwrap.fill(text=quote_text, width=max_char_count)
# FIX FONT WITH SPACES
new_text = new_text.replace(" ", " ")
# new_text = helper_images.split_string(text, max_char_count)
# Define the positions of logo and text
x_logo = 0
y_logo = 1100
x_text = img.size[0] / 2
y_text = img.size[1] / 2
position = (x_text, y_text)

# Draw the shadow text
shadow_color = (0, 0, 0, 128)
shadow_position = (x_text+5, y_text+5)
draw.text(shadow_position, new_text, font=quote_font, fill=shadow_color, anchor='mm', align='center')

# Add main text to the image
draw.text(position, text=new_text, font=quote_font, fill=(255, 255, 255, 255), anchor='mm',
align='center')

if author_text is not None:
# Add author text
# Count '\n' in the text to see how many lines there are
author_font = ImageFont.truetype(font=f'{author_font}', size=45)
num_of_lines = new_text.count("\n") + 1
line_height = 55 # TODO CHECK REAL HEIGHT
text_height = line_height * num_of_lines + 40
# TODO CHANGE AUTHORS FONT
author_position = (position[0], position[1] + text_height)
draw.text(author_position, text=author_text, font=author_font, fill=(255, 255, 255, 255), anchor='mm', align='center')

if logo_file is not None:
# Open logo file
img_logo = Image.open(logo_file)

# Reduce the alpha of the overlay image by 30%
alpha = 1
enhancer = ImageEnhance.Brightness(img_logo)
img_logo_darken = enhancer.enhance(alpha)

# Create a new image object with the same size as the background image
img_with_logo = Image.new('RGBA', img.size, (0, 0, 0, 0))

# Draw the background image onto the new image
img_with_logo.paste(img, (0, 0))

# Draw the overlay image onto the new image
img_with_logo.paste(img_logo_darken, (int(x_logo), int(y_logo)), mask=img_logo_darken)

# Convert from RGBA to RGB
img_with_logo_rgb = img_with_logo.convert("RGB")

# file_name

# Save the image
img_with_logo_rgb.save(f"{output_path}/{file_name}")
# combined.show()
return f"{output_path}/{file_name}"

# If logo was off
# Save the image
img.save(f"{output_path}/{file_name}")
# combined.show()
return f"{output_path}/{file_name}"

**helper.py**:提供辅助功能,如字符串分割、图片裁剪、颜色加深等。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import csv

from PIL import Image, ImageEnhance
import os


def split_string(string, max_chars_per_line):
words = string.split()
lines = []
current_line = ""
for word in words:
if len(current_line + " " + word) > max_chars_per_line:
lines.append(current_line.strip())
current_line = ""
current_line += " " + word
if current_line:
lines.append(current_line.strip())

# Re-combine lines to achieve even distribution of words
num_lines = len(lines)
if num_lines > 1:
total_words = len(words)
ideal_words_per_line = (total_words + num_lines - 1) // num_lines
excess_words = total_words - ideal_words_per_line * (num_lines - 1)

even_lines = []
i = 0
while i < num_lines - 1:
line_words = words[:ideal_words_per_line]
if excess_words > 0:
line_words.append(words[ideal_words_per_line])
excess_words -= 1
words.pop(ideal_words_per_line)
even_lines.append(" ".join(line_words))
words = words[ideal_words_per_line:]
i += 1
even_lines.append(" ".join(words))
return "\n".join(even_lines)
else:
return lines[0]


def darken_images(images_folder, output_folder):
# Set desired darkness
dark = 0.5

# Loop through all the images in the directory
for filename in os.listdir(images_folder):
if filename.endswith(".jpg") or filename.endswith(".png"):
# Open the image
filepath = os.path.join(images_folder, filename)
img = Image.open(filepath)

# Create an enhancer object for the image
enhancer = ImageEnhance.Brightness(img)

# Reduce the brightness by a factor of 'dark'
dark_img = enhancer.enhance(dark)

# Save the cropped image
dark_img.save(f"{output_folder}/{filename}")


def cut_images_old(images_folder, output_folder):
# Set the target size
target_size = (1080, 1350)
# Loop through all the images in the directory
for filename in os.listdir(images_folder):
if filename.endswith(".jpg") or filename.endswith(".png"):
# Open the image
filepath = os.path.join(images_folder, filename)
img = Image.open(filepath)
resize_ration = min(target_size[0] / img.width, target_size[1] / img.height)

# Get the size of the image
width, height = img.size

# Calculate the coordinates for cropping
left = (width - target_size[0]) // 2
top = (height - target_size[1]) // 2
right = left + target_size[0]
bottom = top + target_size[1]

# Crop the image
img = img.crop((left, top, right, bottom))

# Save the cropped image
img.save(f"{output_folder}/{filename}")


def cut_images(images_folder, output_folder):
# Set desired ratio
desired_ratio = 1080 / 1350

# Loop through all files in input folder
for filename in os.listdir(images_folder):
if filename.endswith('.jpg') or filename.endswith('.jpeg') or filename.endswith('.png'):
# Open the image
img = Image.open(os.path.join(images_folder, filename))

# Get image dimensions
width, height = img.size
ratio = width / height

# Calculate new dimensions
if ratio > desired_ratio:
# Image is wider than desired ratio, crop width
new_width = round(height * desired_ratio)
new_height = height
else:
# Image is taller than desired ratio, crop height
new_width = width
new_height = round(width / desired_ratio)

# Crop the image in the center
left = (width - new_width) / 2
top = (height - new_height) / 2
right = left + new_width
bottom = top + new_height
img = img.crop((left, top, right, bottom))

# Resize the image if necessary
if img.size != (1080, 1350):
img = img.resize((1080, 1350))

# Save the image to output folder
img.save(os.path.join(output_folder, filename))


def create_new_topic_dirs(topic, project_dir):
# /customers/___
if not os.path.exists(f"{project_dir}/customers/{topic}"):
os.makedirs(f"{project_dir}/customers/{topic}")
# /sources/images/___
if not os.path.exists(f"{project_dir}/sources/images/{topic}"):
os.makedirs(f"{project_dir}/sources/images/{topic}")
os.makedirs(f"{project_dir}/sources/images/{topic}/cropped")
os.makedirs(f"{project_dir}/sources/images/{topic}/cropped/darken")


def fix_text_syntax(font: str, text_file):
with open(text_file, 'r', encoding="utf-8") as file:
lines = file.readlines()

# Bebas can't display ’ -> replace with '
if font.__contains__("Bebas"):
# open file in read mode
file = open(text_file, "r")
replaced_content = ""

# looping through the file
for line in file:
# stripping line break
line = line.strip()

# replacing the texts
new_line = line.replace("’", "'").replace("’", "'")

# concatenate the new string and add an end-line break
replaced_content = replaced_content + new_line + "\n"

# close the file
file.close()

# Open file in write mode
write_file = open(text_file, "w")

# overwriting the old file contents with the new/replaced content
write_file.write(replaced_content)

# close the file
write_file.close()

def add_sheets(file_names: str, output_path: str, customer_name: str, authors: str, quotes: str):
with open(f'{output_path}/{customer_name}.csv', 'w', newline='') as file:
writer = csv.writer(file)
writer.writerow(["File Name", "Reference", "Verse"])
for i in range(len(file_names)):
writer.writerow([file_names[i], authors[i], quotes[i]])

注意事项

  • 新建主题:create_topic_directories.py里的topic =后面的文字为主题名称。
  • 主题和客户名称:在main.py中,用户需要根据需要修改TOPICCUSTOMER_NAMEquote_font
  • 字体选择:确保使用于quote_font的字体支持中文,以避免输出错误。
  • 字符数调整:在post_handler.py中,根据文字长度调整max_char_count
  • 必要时修改logo图标,但必须使用给定的模板,否则会出现ValueError: image has wrong mode的错误。

输出结果

  最终生成的图文内容将保存在generated/{主题名}/{客户名称}/目录下,用户可以轻松地管理和使用这些内容。

结语

  这个项目不仅仅是一个工具,它是一个创意的催化剂,帮助用户将思想和情感转化为视觉艺术。我期待看到用户如何使用这个工具来创造美丽的图文作品。

  未经许可,禁止转载!

  我是小冰,下期再会


介绍一款自动化图文生成项目
https://ice-water.store/2024/10/02/2024-10-02-tuwen/
Author
小冰
Posted on
October 2, 2024
Licensed under