Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

font.getsize() changes after installing libraqm #4483

Closed
seasker opened this issue Mar 23, 2020 · 5 comments
Closed

font.getsize() changes after installing libraqm #4483

seasker opened this issue Mar 23, 2020 · 5 comments

Comments

@seasker
Copy link

seasker commented Mar 23, 2020

I am using PIL to render text on images with position annotation. The text size changes after installing libraqm.

from PIL import Image, ImageFont, ImageDraw
font=ImageFont.truetype('Microsoft/msyhl.ttc',size=14,layout_engine=ImageFont.LAYOUT_RAQM)
print(font.getsize('a'))  
print(font.getsize('b'))  
print(font.getsize('c'))  
print(font.getsize('abc'))  

Result without libraqm

(7, 16)
(8, 16)
(7, 16)
(22, 16)

The sum of character's width 7+8+7=22 is correct.
Result after installing libraqm

(8, 16)
(9, 16)
(7, 16)
(23, 16)

The sum of character's width 8+9+7=24 doesn't equal to 23.
Also I find that the text size changes differently on different font after installing libraqm.
Why are these happening after installing libraqm?

@radarhere
Copy link
Member

Well, libraqm is a library for complex text layout. So it doesn't seem unreasonable that it would want to layout text slightly differently.

If you don't want to use raqm, instead of

font=ImageFont.truetype('Microsoft/msyhl.ttc',size=14,layout_engine=ImageFont.LAYOUT_RAQM)

you can use ImageFont.LAYOUT_BASIC,

font=ImageFont.truetype('Microsoft/msyhl.ttc',size=14,layout_engine=ImageFont.LAYOUT_RAQM)

Regarding the fact that 8+9+7 doesn't equal to 23, it is certainly possible in other languages that the width of a combination of two characters is not the same as the width of the two characters separately. It's also conceivable if the English font were slanted - one letter might easily stray into the space of the previous one. I imagine that this is less obvious in your font.

If you'd like to follow this further, it would be helpful to provide a copy of a font that we could use to reproduce this. However, I don't think it's necessarily true that 'a', 'b' and 'c' should combine to make the width of 'abc'.

@nulano
Copy link
Contributor

nulano commented Apr 21, 2020

The font msyhl.ttc in the original post appears to be a component of Windows Vista or newer (but I don't see the light version on my Windows 7 Professional, only on my Windows 10 Home): https://docs.microsoft.com/en-us/typography/font-list/microsoft-yahei


I believe this is caused by rounding. Looking at the source code in a debugger, it looks like basic text layout is using FreeType's hinting to round advance values (glyph widths) to the nearest integer, while Raqm layout is returning unrounded values, the sum of which is then rounded up in _imagingft.getsize.

For example, the unrounded advance width of the glyph 'a' is 7.46875px, which is rounded to 7px for basic layout. If rendering the string 'a' with Raqm, the sum of 7.46875px is rounded up to 8px at the end of getsize.

from PIL import ImageFont
basic = ImageFont.truetype("msyhl.ttc", 14, layout_engine=ImageFont.LAYOUT_BASIC)
print(basic.getsize("a"))       # (7, 16)
print(basic.getsize("a" * 10))  # (70, 16)
raqm = ImageFont.truetype("msyhl.ttc", 14, layout_engine=ImageFont.LAYOUT_RAQM)
print(raqm.getsize("a"))        # (8, 16)
print(raqm.getsize("a" * 10))   # (75, 16)

Let's look at the 10 character string 'aaaaaaaaaa'. With basic layout, the width of each 'a' is rounded to 7px before computing the size, so the sum will be 10×7px=70px. With raqm, the sum will be 74.6875px, which gets rounded up to 75px.


Adding FT_LOAD_NO_HINTING to the following line makes basic layout match raqm layout:

load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP;

This even passes all tests, except for those using the oldest versions of FreeType (the failing test runners don't support raqm either): https://github.com/nulano/Pillow/runs/605844576

I'm not sure what the pros and cons of this change are regarding the appearance of text, but I don't think it's worth changing for Pillow backwards compatibility reasons. The Raqm values cannot be rounded before rendering, because this could misalign complex glyphs.

Note also, that this difference can make it harder (confusing) to write tests for ImageFont, as tests in test_imagefont.py are run with both layouts, and the rendered images can differ due to this rounding difference.

@radarhere
Copy link
Member

Thanks @nulano for your analysis. Sounds to me like raqm made a decision to do things differently, and this is the consequence. I agree that we shouldn't make changes to fight against the complex rendering of Raqm.

Am I correct in thinking that your intention with #4959 is that the new method

print(font.getlength('a'))  
print(font.getlength('b'))  
print(font.getlength('c'))  
print(font.getlength('abc'))

will mean that 'a + b + c' will equal 'abc' regardless of using Raqm or not, because the increased precision will mean that rounding is no longer a problem?

@nulano
Copy link
Contributor

nulano commented Jan 2, 2021

Am I correct in thinking that your intention with #4959 is that the new method ... will mean that 'a + b + c' will equal 'abc' regardless of using Raqm or not, because the increased precision will mean that rounding is no longer a problem?

That is correct.

Note that this is not the only advantage, font.getlength is also significantly faster.

@radarhere
Copy link
Member

Cool. Closing then, as it sounds like users could use that moving forward, and breaking backwards compatibility to fix this more directly is likely to be more trouble than it's worth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants