Skip to content

Commit 2600974

Browse files
authored
Merge pull request #15 from parishwolfe/Fix-image_util
fixes #14
2 parents e35f549 + 59e9b97 commit 2600974

File tree

6 files changed

+212
-16
lines changed

6 files changed

+212
-16
lines changed

img/test_1.png

-1.05 KB
Loading

img/test_2.png

115 Bytes
Loading

img/test_3.png

-192 Bytes
Loading

img/test_4.png

-355 Bytes
Loading

util/image_util.py

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
from PIL import Image, ImageDraw, ImageFont
2-
3-
def create_text_image(text: str, height: int, width: int, file_name: str, color: str="#000000"):
2+
import os
3+
4+
5+
def get_text_width(font, text):
6+
if hasattr(font, 'getlength'):
7+
# For newer versions of Pillow
8+
return font.getlength(text)
9+
elif hasattr(font, 'getsize'):
10+
# For older versions of Pillow
11+
return font.getsize(text)[0]
12+
elif hasattr(font, 'getbbox'):
13+
# Alternative method
14+
bbox = font.getbbox(text)
15+
return bbox[2] - bbox[0]
16+
else:
17+
raise AttributeError(
18+
"Font object has no method to calculate text width.")
19+
20+
21+
def create_text_image(text: str, height: int, width: int, file_name: str, color: str = "#000000"):
422
"""
523
Creates an image with the specified text centered within the given dimensions and saves it as a PNG file.
624
Args:
@@ -27,14 +45,48 @@ def create_text_image(text: str, height: int, width: int, file_name: str, color:
2745
best_wrapped_text = None
2846
best_total_height = None
2947

30-
# Load the font once to avoid repetitive loading
31-
font_path = "arial.ttf" # Ensure this font is available on your system
48+
# Function to find a font that exists on the system
49+
def find_font():
50+
possible_fonts = [
51+
# Common fonts on Mac
52+
"/Library/Fonts/Arial.ttf",
53+
"/System/Library/Fonts/Supplemental/Arial.ttf",
54+
"/Library/Fonts/Helvetica.ttf",
55+
"/System/Library/Fonts/Supplemental/Helvetica.ttf",
56+
"/Library/Fonts/Times New Roman.ttf",
57+
# Common fonts on Windows
58+
"C:\\Windows\\Fonts\\Arial.ttf",
59+
"C:\\Windows\\Fonts\\times.ttf",
60+
"C:\\Windows\\Fonts\\verdana.ttf",
61+
# Common fonts on Linux
62+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
63+
# PIL default font
64+
"arial.ttf",
65+
"DejaVuSans.ttf", # Comes with matplotlib
66+
]
67+
for font_path in possible_fonts:
68+
if os.path.exists(font_path):
69+
return font_path
70+
# If none of the above fonts exist, use PIL's default font
71+
return None
72+
73+
font_path = find_font()
74+
if font_path is None:
75+
# Use PIL's default font
76+
font = ImageFont.load_default()
77+
else:
78+
font = None # We'll set the font in the loop
3279

3380
while min_font_size <= max_font_size:
3481
font_size = (min_font_size + max_font_size) // 2
35-
font = ImageFont.truetype(font_path, size=font_size)
82+
if font_path is None:
83+
# Use default font
84+
font = ImageFont.load_default()
85+
else:
86+
font = ImageFont.truetype(font_path, size=font_size)
3687

37-
fits, wrapped_text, total_height = does_text_fit(draw, text, font, width, height)
88+
fits, wrapped_text, total_height = does_text_fit(
89+
draw, text, font, width, height)
3890
if fits:
3991
# This font size fits, try a bigger one
4092
best_font_size = font_size
@@ -47,10 +99,15 @@ def create_text_image(text: str, height: int, width: int, file_name: str, color:
4799

48100
if best_wrapped_text is None:
49101
# Text does not fit even at the minimum font size
50-
raise ValueError("Text cannot fit into the image at the minimum font size.")
102+
raise ValueError(
103+
"Text cannot fit into the image at the minimum font size.")
51104

52105
# Use the best font size
53-
font = ImageFont.truetype(font_path, size=best_font_size)
106+
if font_path is None:
107+
# Use default font
108+
font = ImageFont.load_default()
109+
else:
110+
font = ImageFont.truetype(font_path, size=best_font_size)
54111
ascent, descent = font.getmetrics()
55112
line_spacing = int(best_font_size * 0.2) # 20% of font size
56113

@@ -62,7 +119,8 @@ def create_text_image(text: str, height: int, width: int, file_name: str, color:
62119

63120
# Draw the text on the image
64121
for line in best_wrapped_text:
65-
text_width = font.getlength(line)
122+
# Get text width
123+
text_width = get_text_width(font, line)
66124
x = (width - text_width) / 2 # Center horizontally
67125
draw.text((x, y_offset), line, fill=color, font=font)
68126
y_offset += ascent + descent + line_spacing # Move down for the next line
@@ -71,6 +129,7 @@ def create_text_image(text: str, height: int, width: int, file_name: str, color:
71129
image.save(file_name, "PNG")
72130
return file_name
73131

132+
74133
def does_text_fit(draw, text, font, width, height):
75134
ascent, descent = font.getmetrics()
76135
line_spacing = int(font.size * 0.2) # 20% of font size
@@ -83,7 +142,8 @@ def does_text_fit(draw, text, font, width, height):
83142
current_line = ""
84143
for word in words:
85144
test_line = f"{current_line} {word}".strip()
86-
text_width = font.getlength(test_line)
145+
# Get text width
146+
text_width = get_text_width(font, test_line)
87147
if text_width <= width:
88148
current_line = test_line
89149
else:
@@ -97,31 +157,40 @@ def does_text_fit(draw, text, font, width, height):
97157

98158
# Calculate total height for the wrapped text
99159
num_lines = len(wrapped_text)
100-
total_height = (ascent + descent) * num_lines + line_spacing * (num_lines - 1)
160+
total_height = (ascent + descent) * num_lines + \
161+
line_spacing * (num_lines - 1)
101162

102163
# Check if any line exceeds width
103-
any_line_too_wide = any(font.getlength(line) > width for line in wrapped_text)
164+
any_line_too_wide = any(get_text_width(font, line)
165+
> width for line in wrapped_text)
104166

105167
# Return whether text fits, the wrapped_text, and total_height
106168
fits = (total_height <= height) and not any_line_too_wide
107169
return fits, wrapped_text, total_height
108170

109171

110172
if __name__ == "__main__":
173+
# Create output directory if it doesn't exist
174+
output_dir = "../img"
175+
if not os.path.exists(output_dir):
176+
os.makedirs(output_dir)
177+
111178
# Example usage:
112179
create_text_image(
113180
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
114181
800,
115182
1200,
116-
"img/test_1.png",
183+
os.path.join(output_dir, "test_1.png"),
117184
"#000000"
118185
)
119186
create_text_image(
120187
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
121188
800,
122189
400,
123-
"img/test_2.png",
190+
os.path.join(output_dir, "test_2.png"),
124191
"#FF5733"
125192
)
126-
create_text_image("Lorem ipsum dolor sit amet", 800, 1200, "img/test_3.png")
127-
create_text_image("Lorem ipsum dolor sit amet", 800, 400, "img/test_4.png", "#FFFFFF")
193+
create_text_image("Lorem ipsum dolor sit amet", 800, 1200,
194+
os.path.join(output_dir, "test_3.png"))
195+
create_text_image("Lorem ipsum dolor sit amet", 800, 400,
196+
os.path.join(output_dir, "test_4.png"), "#FFFFFF")

util/image_util_win.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from PIL import Image, ImageDraw, ImageFont
2+
3+
def create_text_image(text: str, height: int, width: int, file_name: str, color: str="#000000"):
4+
"""
5+
Creates an image with the specified text centered within the given dimensions and saves it as a PNG file.
6+
Args:
7+
text (str): The text to be displayed on the image.
8+
height (int): The height of the image in pixels.
9+
width (int): The width of the image in pixels.
10+
file_name (str): The name of the file to save the image as, including the file extension (e.g., 'image.png').
11+
color (str, optional): The color of the text in hexadecimal format (default is black, "#000000").
12+
Raises:
13+
ValueError: If the text cannot fit into the image at the minimum font size.
14+
Returns:
15+
None
16+
"""
17+
18+
# Create a blank transparent image
19+
image = Image.new("RGBA", (width, height), (255, 255, 255, 0))
20+
draw = ImageDraw.Draw(image)
21+
22+
# Font parameters
23+
min_font_size = 1
24+
max_font_size = 500 # Set an upper limit for font size
25+
26+
best_font_size = min_font_size
27+
best_wrapped_text = None
28+
best_total_height = None
29+
30+
# Load the font once to avoid repetitive loading
31+
font_path = "arial.ttf" # Ensure this font is available on your system
32+
33+
while min_font_size <= max_font_size:
34+
font_size = (min_font_size + max_font_size) // 2
35+
font = ImageFont.truetype(font_path, size=font_size)
36+
37+
fits, wrapped_text, total_height = does_text_fit(draw, text, font, width, height)
38+
if fits:
39+
# This font size fits, try a bigger one
40+
best_font_size = font_size
41+
best_wrapped_text = wrapped_text
42+
best_total_height = total_height
43+
min_font_size = font_size + 1
44+
else:
45+
# Font size too big, try a smaller one
46+
max_font_size = font_size - 1
47+
48+
if best_wrapped_text is None:
49+
# Text does not fit even at the minimum font size
50+
raise ValueError("Text cannot fit into the image at the minimum font size.")
51+
52+
# Use the best font size
53+
font = ImageFont.truetype(font_path, size=best_font_size)
54+
ascent, descent = font.getmetrics()
55+
line_spacing = int(best_font_size * 0.2) # 20% of font size
56+
57+
# Adjust total height to include the descent of the last line
58+
best_total_height += descent
59+
60+
# Center position for the text
61+
y_offset = (height - best_total_height) / 2
62+
63+
# Draw the text on the image
64+
for line in best_wrapped_text:
65+
text_width = font.getlength(line)
66+
x = (width - text_width) / 2 # Center horizontally
67+
draw.text((x, y_offset), line, fill=color, font=font)
68+
y_offset += ascent + descent + line_spacing # Move down for the next line
69+
70+
# Save the image
71+
image.save(file_name, "PNG")
72+
return file_name
73+
74+
def does_text_fit(draw, text, font, width, height):
75+
ascent, descent = font.getmetrics()
76+
line_spacing = int(font.size * 0.2) # 20% of font size
77+
78+
# Wrap the text
79+
wrapped_text = []
80+
lines = text.splitlines()
81+
for line in lines:
82+
words = line.split()
83+
current_line = ""
84+
for word in words:
85+
test_line = f"{current_line} {word}".strip()
86+
text_width = font.getlength(test_line)
87+
if text_width <= width:
88+
current_line = test_line
89+
else:
90+
if not current_line:
91+
# Single word longer than width, cannot fit
92+
return False, None, None
93+
wrapped_text.append(current_line)
94+
current_line = word
95+
if current_line:
96+
wrapped_text.append(current_line)
97+
98+
# Calculate total height for the wrapped text
99+
num_lines = len(wrapped_text)
100+
total_height = (ascent + descent) * num_lines + line_spacing * (num_lines - 1)
101+
102+
# Check if any line exceeds width
103+
any_line_too_wide = any(font.getlength(line) > width for line in wrapped_text)
104+
105+
# Return whether text fits, the wrapped_text, and total_height
106+
fits = (total_height <= height) and not any_line_too_wide
107+
return fits, wrapped_text, total_height
108+
109+
110+
if __name__ == "__main__":
111+
# Example usage:
112+
create_text_image(
113+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
114+
800,
115+
1200,
116+
"img/test_1.png",
117+
"#000000"
118+
)
119+
create_text_image(
120+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
121+
800,
122+
400,
123+
"img/test_2.png",
124+
"#FF5733"
125+
)
126+
create_text_image("Lorem ipsum dolor sit amet", 800, 1200, "img/test_3.png")
127+
create_text_image("Lorem ipsum dolor sit amet", 800, 400, "img/test_4.png", "#FFFFFF")

0 commit comments

Comments
 (0)