Repository navigation
Description
What did you do?
Rendering (multi line) text in left(horizontal) center(vertical) of bounding box.
What did you expect to happen?
Textbox located at left(horizontal), center(vertical)
What actually happened?
Textbox height is wrong. I don't understand line_spacing = self._multiline_spacing(font, spacing, stroke_width) in ImageDraw.multiline_textbbox()
In def multiline_textbbox, self.multiline_spacingfunction
def _multiline_spacing(self, font, spacing, stroke_width):
return (
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3]
+ stroke_width
+ spacing
)self.textbbox((0, 0), "A", font, stroke_width=stroke_width) is (x1,y1,x2,y2), y1 is not started from zero(In my case, 2), but using (y2+stroke_width+spacing) is not general line_spacing I know.(even "A" is not the highest character!! ex."[", "}" ). line spacing can be different line by line. I suggest
def _multiline_spacing(self, font, spacing, stroke_width):
return (
self.textbbox((0, 0), "[", font, stroke_width=stroke_width)[3]
- self.textbbox((0, 0), "[", font, stroke_width=stroke_width)[1]
+ spacing
)What are your OS, Python and Pillow versions?
- OS: MAC
- Python: 3.9.5
- Pillow:
--------------------------------------------------------------------
Pillow 10.3.0
Python 3.9.5 (default, Nov 23 2021, 15:27:38)
[GCC 9.3.0]
--------------------------------------------------------------------
Python executable is /volume1/ml_image_tr_server/bin/python3
Environment Python files loaded from /volume1/ml_image_tr_server
System Python files loaded from /usr
--------------------------------------------------------------------
Python Pillow modules loaded from /volume1/ml_image_tr_server/lib/python3.9/site-packages/PIL
Binary Pillow modules loaded from /volume1/ml_image_tr_server/lib/python3.9/site-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 10.3.0
*** TKINTER support not installed
--- FREETYPE2 support ok, loaded 2.13.2
--- LITTLECMS2 support ok, loaded 2.16
--- WEBP support ok, loaded 1.3.2
--- WEBP Transparency support ok
--- WEBPMUX support ok
--- WEBP Animation support ok
--- JPEG support ok, compiled for libjpeg-turbo 3.0.2
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.2
--- ZLIB (PNG/ZIP) support ok, loaded 1.2.11
--- LIBTIFF support ok, loaded 4.6.0
--- RAQM (Bidirectional Text) support ok, loaded 0.10.1, fribidi 1.0.8, harfbuzz 8.4.0
*** LIBIMAGEQUANT (Quantization method) support not installed
--- XCB (X protocol) support ok
--------------------------------------------------------------------
I ignore line spacing, and calculate line height line by line instead of max_line_h(maybe pillow does), and using anchor="lt" (not "la") and adding first y adding stroke_width
from PIL import Image, ImageDraw, ImageFont
import numpy as np
bbox_x1, bbox_y1, bbox_x2, bbox_y2 = 0,0,300,100
bbox_h = bbox_y2 - bbox_y1
text = "Tag[name]\n is \nenglish."
font_size = 20
lang = "english"
writing_direction = "ltr"
text_alignment = "left"
text_border_width=3
canvas_hw=(500, 1000)
text_color=(255, 255, 255)
text_border_color=(125, 125, 125)
font_path = "/volume1/ml_image_tr_server/fonts/Pretendard-Regular.otf" ## your font_path
canvas = Image.new(mode="RGB", size=(canvas_hw[1], canvas_hw[0]))
font = ImageFont.truetype(font=font_path, size=max(1, round(font_size)))
draw = ImageDraw.Draw(canvas)
tx1,ty1,tx2,ty2 = draw.textbbox(
xy=(0, 0),
text=text,
font=font,
anchor=None,
language=lang,
stroke_width=text_border_width,
font_size=font_size
)
lines = text.split('\n')
l_coords = [font.getbbox(
text = line,
language=lang,
stroke_width=text_border_width,
direction = writing_direction
) for line in lines]
total_height = sum([t[3] - t[1] for t in l_coords])
print(f"multi_line tbox y = {ty2-ty1}, total_line_height={total_height}")
start_y = bbox_y1+(bbox_h-total_height)/2 + text_border_width
xy = (bbox_x1+text_border_width, start_y)
anchor = 'lt'
lbox_coords = [] # line box coords
for line_idx, line_text in enumerate(text.split("\n")):
draw.text(
xy = xy,
text = line_text,
fill = text_color,
font = font,
anchor = anchor,
align = text_alignment,
direction = writing_direction,
language=lang,
stroke_width=text_border_width,
stroke_fill = text_border_color
)
lbox_x1, lbox_y1, lbox_x2, lbox_y2 = draw.textbbox(
xy = xy,
text = line_text,
font = font,
anchor = anchor,
align=text_alignment,
direction = writing_direction,
language=lang,
stroke_width=text_border_width,
font_size=font_size
)
# calculate next line xy
xy = (xy[0], xy[1]+(lbox_y2-lbox_y1))
# draw line bbox
draw.rectangle((lbox_x1, lbox_y1, lbox_x2, lbox_y2), outline=(255, 255, 0))
lbox_coords.append([lbox_x1, lbox_y1, lbox_x2, lbox_y2])
lbox_coords = np.array(lbox_coords)
tbox_x1, tbox_y1 = lbox_coords[:, :2].min(axis=0)
tbox_x2, tbox_y2 = lbox_coords[:, 2:4].max(axis=0)
tbox = (tbox_x1, tbox_y1, tbox_x2, tbox_y2)
bbox = (0,0,300,100)
def move_box_a_to_center_of_box_b(A, B):
# A와 B의 좌표 (l, t, r, b)
lA, tA, rA, bA = A
lB, tB, rB, bB = B
# 박스 A의 너비와 높이
width_A = rA - lA
height_A = bA - tA
# 박스 B의 중심 좌표
center_x_B = (lB + rB) / 2
center_y_B = (tB + bB) / 2
# 박스 A의 새로운 좌표 (중심을 B의 중심으로 이동)
new_lA = center_x_B - width_A / 2
new_tA = center_y_B - height_A / 2
new_rA = center_x_B + width_A / 2
new_bA = center_y_B + height_A / 2
# 새로운 A 박스의 좌표 반환
return (new_lA, new_tA, new_rA, new_bA)
ctbox_x1, ctbox_y1, ctbox_x2, ctbox_y2 = move_box_a_to_center_of_box_b(tbox,bbox)
# draw text box and text bbox(bbox center align)
draw.rectangle((ctbox_x1, ctbox_y1, ctbox_x2, ctbox_y2), outline=(255, 0, 255))
draw.rectangle((tbox_x1, tbox_y1, tbox_x2, tbox_y2), outline=(255, 0, 0))
draw.rectangle(bbox, outline=(0,0,255))
canvas.save("result.png")
Activity
[-]rendering multi line text in bounding box, text bbox coordinates issue[/-][+]Rendering multi line text in bounding box, text bbox coordinates issue[/+]radarhere commentedon Jun 1, 2024
I think the unfortunate reality is that each font can set the tallest character to be whatever it wants. Here's a font where 'A' is taller.
It's also feasible that a font doesn't have the '[' character. 'A' was chosen in #1574
If I understand correctly, you're not saying that the co-ordinates returned by
draw.textbbox()for multiline text don't match the co-ordinates of multiline text drawn bydraw.text(). Rather,draw.textbbox()anddraw.text()are both incorrect for multiline text because you're saying the line height is incorrect?So your suggestion is to remove the vertical offset that the font chooses to apply to the text. I'm not sold on the idea that we should be ignoring the font author's idea about how much vertical gap should be above characters. If you'd like to do so in your code, that is fine.
nulano commentedon Jun 1, 2024
We already ignore the font author's idea about line spacing, see #1646 / #6469 (comment).
My suggestion from #6469 (comment) is:
I just don't know how we could make this change without breaking backwards compatibility for a lot of people.
HERIUN commentedon Jun 3, 2024
@radarhere
I understand "A" can be taller than "[".
Sorry for confusing. I think draw.textbbox() and draw.text() are both incorrect for multiline text. because
def _multiline_spacing()add stroke_width 3 times (rather 2). ie.(self.textbbox(2) + stroke_width(1))radarhere commentedon Jun 3, 2024
+ stroke_widthwas added #7080 for the sake of backwards compatibility. See #7059 (comment) for further info.HERIUN commentedon Jun 3, 2024
@nulano
I don't understand "backwards compatibility"
I suggest 2 method. (but only
font.font.heightis always tallest character's height)I think spacing must be out of
def _multiline_spacing, rather addedfor line in linesloopWhy i don't want use
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3],self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[0]can be minus value width stroke_width...radarhere commentedon Jun 3, 2024
Pillow would like to continue working as expected when users upgrade to newer versions. If text changes position, this would break expectations.
Alternatively, we could
Neither of these are very elegant though.
nulano commentedon Jun 3, 2024
No,
textbboxreturnsfont.getbboxwith an offset ((0,0)in this case):Pillow/src/PIL/ImageDraw.py
Lines 753 to 756 in 95a69ec
And
font.getbboxonly adds stroke_width once:Pillow/src/PIL/ImageFont.py
Lines 402 to 404 in 95a69ec
Removing all except the last return value, we are left with:
Inline the variables:
Rearrange:
Simplify:
No,
font.font.heightis the font designer's intended line spacing, it has nothing to do with the height of the glyphs.Yeah, that is what I've been leaning towards, using the version from #8092 (comment) (but perhaps as a font object member function, since we'll need a different API if we ever want to finish #6926), but I suspect the API might get quite ugly for this.
HERIUN commentedon Jun 4, 2024
Appreciate for explaination. I understand "backwards compatibility", "font.font.height is not height of the glyphs", "font.getbbox() add stroke_width once"
But, if stroke_width > offset[1]. top coords of font.getbbox() can be minus value. so, draw.textbbox() left,top can be minus value. and finally def _multiline_spacing() is not what we
== offset[1] + size[1] + 2*stroke_width + spacing
It doesn't mean line_spacing to me,
I expected,
def _multiline_spacing()output isor consider
font.font.heightnulano commentedon Jun 4, 2024
Have a look at this diagram: https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html#quick-reference
We currently calculate the line spacing as the sum of:
It is not a good idea to use the
maxfunction as it is not predictable here, instead I think you meant the following:This would be similar to what we currently use, but instead of measuring the letter 'A' from the ascender line, it would be measuring it from the top line (specific to the letter 'A'), so even less predictable than currently.
The value
font.font.heightis equal to the sum of:I believe this would be better than what we use since:
However, as stated above, we can't easily make this change without breaking backwards compatibility.
radarhere commentedon Jun 8, 2024
Shall I close this and let the problem be followed in #1646 then?