Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Drop the FT2Font intermediate buffer.#30059

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
anntzer wants to merge2 commits intomatplotlib:text-overhaul
base:text-overhaul
Choose a base branch
Loading
fromanntzer:ft-direct-render

Conversation

@anntzer
Copy link
Contributor

@anntzeranntzer commentedMay 16, 2025
edited
Loading

Directly render FT glyphs to the Agg buffer. In particular, this naturally provides, with no extra work, subpixel positioning of glyphs (closing#29551) (this could also have been implemented in the old framework, but would have required careful tracking of subpixel offets).

Note that rendering of mathtext boxes (e.g., fraction bars) is currently missing, but should be "easy" (the main question being whether/how to ensure proper hinting/alignment to pixel edges in the horizontal or vertical output cases). Likewise, all baseline images should be regenerated. Finally, the new APIs added to FT2Font are likely up to bikeshedding (but they are all private).

In practice this should get included (if we agree to go this way) into the FreeType update (#29816). This would also supersede the patch advertised at#29816 (comment).

test modified from#29551 to also include mathtext:

importmatplotlib.pyplotaspltimportmatplotlib.animationasmanimfig=plt.figure(figsize=(2,2))ts= [fig.text(0,0,'$ABC123$'),fig.text(0,0.2,'ABC123'),]change=0.001defupdate(*args,**kwargs):globalchangefortints:x,y=t.get_position()ifnot (0<=x<=1and0<=y<=1):change*=-1t.set_x(x+change)t.set_y(y+change)returntsani=manim.FuncAnimation(fig,update,interval=20,frames=int(2/change),cache_frame_data=False)ani.save('text.gif')

PR summary

PR checklist

@story645
Copy link
Member

story645 commentedMay 16, 2025
edited
Loading

Would you be open to using a tracking issue or project board for the font work (sample categories: draft, waiting on other PRs, ready for review, please review first)?

I think breaking everything up into small PRs is fantastic but I'm finding it a bit overwhelming to keep track of the order for reviewing things (and figure I'm not alone here) and how these PRs relate to each other.

@anntzer
Copy link
ContributorAuthor

anntzer commentedMay 16, 2025
edited
Loading

Sure, I'll do that. I'm sorry for the mess, but this is basically me trying to remember all the patches or even just ideas I have accumulated over the years that went nowhere because they would thrash all the baseline images, and that I now try to present given that I see an opportunity with the FreeType upgrade. Also, some of them are notfully complete patches but things that I believe can be made complete with relatively little effort (for some value of "little effort"...) simply because I do not have the time now to finalize them but want to put them up for discussion as the underlying idea should be clear.

Edit: seethe wiki; attn@QuLogic as well too.

story645 reacted with heart emoji

@story645
Copy link
Member

story645 commentedMay 16, 2025
edited
Loading

I'm sorry for the mess, but

No apologies needed, I commend you for taking advantage of the freetype upgrade. And thanks for the wiki, though some reason can't link cleanly☹️

@anntzer
Copy link
ContributorAuthor

link fixed

story645 reacted with thumbs up emoji

@anntzeranntzerforce-pushed theft-direct-render branch 3 times, most recently fromd44360f to66fdae0CompareMay 17, 2025 09:55
@anntzer
Copy link
ContributorAuthor

Edit: fraction bar rendering has been added too.

@QuLogic
Copy link
Member

A few warnings from compiling locally:

[2/4] Compiling C++ object src/ft2font.cpython-313-x86_64-linux-gnu.so.p/ft2font_wrapper.cpp.o../../src/ft2font_wrapper.cpp: In lambda function:../../src/ft2font_wrapper.cpp:1785:22: warning: unused variable ‘error’ [-Wunused-variable] 1785 |             if (auto error = FT_Load_Glyph(face, idx, static_cast<FT_Int32>(flags))) {      |                      ^~~~~../../src/ft2font_wrapper.cpp:1788:22: warning: unused variable ‘error’ [-Wunused-variable] 1788 |             if (auto error = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL)) {      |                      ^~~~~../../src/ft2font_wrapper.cpp: In lambda function:../../src/ft2font_wrapper.cpp:1804:26: warning: unused variable ‘error’ [-Wunused-variable] 1804 |                 if (auto error = FT_Glyph_To_Bitmap(&g, FT_RENDER_MODE_NORMAL, &origin, 1)) {      |                          ^~~~~

@anntzer
Copy link
ContributorAuthor

This is semi-intentional: if these checks fail (error > 0) then the error should really be raised using throw_ft_error, not with a plain raise as I currently do, but throw_ft_error is not visible from ft2font_wrapper.cpp so some code needs to be moved around, e.g. make throw_ft_error an inline function defined in ft2font.h, or move the new implementations into real C++ FT2Font methods in ft2font.cpp and add wrapper lambdas in ft2font_wrapper.cpp, or (would be my preferred choice) switch error checking to the FT_CHECK macro used in mplcairo (see mplcairo's macros.h).
I left the warning as is just to keep this as a side discussion point, but it can trivially be suppressed by not capturing the error value too.

@QuLogic
Copy link
Member

On a side note, I'm in favour of this in general as removing the intermediate buffer will make emoji/colour glyphs easier since they won't need to go through it.

anntzer reacted with thumbs up emoji

@QuLogic
Copy link
Member

On the call, we were discussing whether it made sense to target all these font-related (and requiring full image rebuild) PRs to a separate feature branch, which would contain the final test image updates. Do you have any thoughts on retargetting this PR (instead of folding it in to#29816)?

@anntzer
Copy link
ContributorAuthor

anntzer commentedMay 30, 2025
edited
Loading

On the call, we were discussing whether it made sense to target all these font-related (and requiring full image rebuild) PRs to a separate feature branch, which would contain the final test image updates. Do you have any thoughts on retargetting this PR (instead of folding it in to#29816)?

Sure, I don't really mind either way? Should the branch live on this repo and be ultimately pruned out, or on yours?

@anntzer
Copy link
ContributorAuthor

Fixed#30059 (comment) by using FT_CHECK (thus also needs#30123).

@QuLogicQuLogic changed the base branch frommain totext-overhaulJune 5, 2025 01:47
@QuLogicQuLogic added this to thev3.11.0 milestoneJun 5, 2025
Copy link
Member

@QuLogicQuLogic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

So I've experimented a little bit to try and see where the changes arise; I've added a note below of one that I think I see.

Using the following small script:

importitertoolsimportstringimportsysimportmatplotlib.pyplotaspltfig=plt.figure()fori,fontsizeinenumerate([None,20,32]):forj,textinenumerate(itertools.batched(string.ascii_lowercase,3)):fig.text(0.1+0.2*i,0.1+0.1*j,''.join(text),fontsize=fontsize)fig.savefig(sys.argv[1]iflen(sys.argv)>1else'bar.png')

inRendererAgg.draw_text, thex/y are almost always integers (or 3e-14 close to one) so fractional positioning shouldn't matter much. But when I compare results with thetext-overhaul branch, almost every string has a difference inonly the first character vs the rest. Do you have any idea why it might only affect that one advance?


self._renderer.draw_text_image(font,x,y+1,angle,gc)
forbitmapinfont._render_glyphs(
x,self.height-y,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

If this were trimmed to integers (int(x), int(self.height - y)) then forboxarrow_test_image, all but the first letter in each string remain the same as the old images. I guess keeping the fractional part (i.e., as its written now) gives us the proper sub-pixel positioning?

Copy link
ContributorAuthor

@anntzeranntzerOct 29, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

That's the intent, at least. Looks like something similar to#30059 (comment) was happening as well?

@QuLogic
Copy link
Member

QuLogic commentedOct 28, 2025
edited
Loading

test modified from#29551 to also include mathtext:

If I extend this test to include\frac{abc123}{def456}, then the fraction bar is jumpy as well, because in theangle=0 case, we draw the bars with a NumPy array at a rounded position.

text

This could be fixed by always using the non-0-angle implementation which draws aPath instead, so long as we also callgc1.set_snap(False). I'm not sure whether we want to do that? It of course causes the bars to instead be smeared across two pixels periodically instead, so I don't know if we want to optimize for that over better static images. Or perhaps we should just let the user decide by settingsnap on theText object (which may or may not need additional threading through as I don't know if it cared about snapping before)?

@timhoffm
Copy link
Member

How do fonts handle dashes and underscores? Would that be a reasonable precedent behavior?

@anntzer
Copy link
ContributorAuthor

anntzer commentedOct 28, 2025
edited
Loading

Controlling based on Artist.get_snap() sounds like a good idea (as you mention, manually inserting gc.set_snap(False) works); I pushed that (and checked that it works). (However, from a quick look, maybe the exact way rounding works in snapping is different? The fraction bar looks shifted down?)

Edit: Likely the proper way to rely on path snapping requires changing the fraction bar from being a filled rectangle (with zero-linewidth) to being a single line with fixed linewidth; still todo, and maybe a problem for the rare case of drawing vertical bars.

Fonts likely control that based on hinting (https://en.wikipedia.org/wiki/Font_hinting), which could also be the switch, but I thought having separate controls for fraction bars and for the rest of hinting would be better.

@anntzer
Copy link
ContributorAuthor

So I've experimented a little bit to try and see where the changes arise; I've added a note below of one that I think I see.

Using the following small script:

importitertoolsimportstringimportsysimportmatplotlib.pyplotaspltfig=plt.figure()fori,fontsizeinenumerate([None,20,32]):forj,textinenumerate(itertools.batched(string.ascii_lowercase,3)):fig.text(0.1+0.2*i,0.1+0.1*j,''.join(text),fontsize=fontsize)fig.savefig(sys.argv[1]iflen(sys.argv)>1else'bar.png')

inRendererAgg.draw_text, thex/y are almost always integers (or 3e-14 close to one) so fractional positioning shouldn't matter much. But when I compare results with thetext-overhaul branch, almost every string has a difference inonly the first character vs the rest. Do you have any idea why it might only affect that one advance?

I figured that out. There's a bug in draw_glyphs_to_bitmap, which is fixed by

diff --git i/src/ft2font.cpp w/src/ft2font.cppindex 8838f68ee5..96ccafe1b0 100644--- i/src/ft2font.cpp+++ w/src/ft2font.cpp@@ -711,8 +711,8 @@ void FT2Font::draw_glyphs_to_bitmap(bool antialiased)         // now, draw to our target surface (convert position)         // bitmap left and top in pixel, string bbox in subpixel-        FT_Int x = (FT_Int)(bitmap->left - (bbox.xMin * (1. / 64.)));-        FT_Int y = (FT_Int)((bbox.yMax * (1. / 64.)) - bitmap->top + 1);+        FT_Int x = (FT_Int)std::floor(bitmap->left - (bbox.xMin * (1. / 64.)));+        FT_Int y = (FT_Int)std::floor((bbox.yMax * (1. / 64.)) - bitmap->top + 1);         draw_bitmap(image, &bitmap->bitmap, x, y);     }

The problem being that for the very first glyph only,bitmap->left - (bbox.xMin * (1. / 64.)) is negative and thus the cast to FT_Int rounds up, whereas for the others, the cast rounds down, so the first glyph ends up shifted one pixel to the right relative to the others. (I didn't actually check for y but a similar issue seems likely.)

@QuLogic
Copy link
Member

I figured that out. There's a bug in draw_glyphs_to_bitmap, which is fixed by

OK, so then it's an additional fix here; it did seem better in several cases (e.g., forThis, theTh did seem to hit each other before).

@QuLogic
Copy link
Member

How do fonts handle dashes and underscores? Would that be a reasonable precedent behavior?

This depends on the internal hinting and FreeType settings. Depending on the hinter, itcan align vertical and horizontal lines to the pixel grid (which is why scaling up or down text isn't always correct), but it could just as easily not do that if the font doesn't implement it.

@anntzer
Copy link
ContributorAuthor

I tested the positioning of the fraction bar a bit more:

importsysimportmatplotlib.pyplotaspltfig=plt.figure(figsize=(10,2))fig.suptitle(sys.argv[1])foriinrange(100):fig.text(0.01*i,.1+.001*i,r'$\frac{A}{B}$',snap=False),fig.text(0.01*i,.2+.001*i,r'$\frac{A}{B}$',snap=True),plt.show()
imageimageimage

Both text-overhaul and direct-render are clearly better than main. direct-render looks blurry when snapping is disabled, but that's intentional. However, it looks like direct-render also draws the fraction bar slightly too low, whereas text-overhaul draws it slightly too long to the right?

@QuLogic
Copy link
Member

QuLogic commentedOct 30, 2025
edited
Loading

Comparingmain withtext-overhaul, the difference looks like the glyphs only; the bars stay in the same spot and are the same width. Comparingtext-overhaul withdirect-render, it looks like the bar is a bit narrower, but based on the not-snapped line, it's actually a partial pixel on the right side, which seems to have snapped to a whole one those lines. WRT the line vertical position, there seems to be some finagling with +/- 1 in the old code:

height=max(int(y2-y1)-1,0)
ifheight==0:
center= (y2+y1)/2
y=int(center- (height+1)/2)
else:
y=int(y1)
x1=math.floor(x1)
x2=math.ceil(x2)
image[y:y+height+1,x1:x2+1]=0xff

I'm not sure how relevant that would be, but also note thefloor/ceil on the x positions which is likely related to the width changing.

@QuLogic
Copy link
Member

I forget, was there more to be done here, or just reviewing the images changes?

@anntzer
Copy link
ContributorAuthor

We still need to agree that text.get_snap() is the correct API to control fraction bar blurring (that seems reasonable enough to me), and maybe try to figure out whether there's any off-by-1 issue with the fraction bar positioning. Some help on that would be welcome.

@tacaswell
Copy link
Member

I am 👍🏻 on using snapping for this.

@tacaswell
Copy link
Member

#22852 is also related to this.

so

Throwing latex into this as well, I think it looks like our bar is a bit too low.

@story645story645 moved this fromIn Progress toReady for Review inFont and text overhaulDec 4, 2025
anntzerand others added2 commitsDecember 18, 2025 14:48
Directly render FT glyphs to the Agg buffer.  In particular, thisnaturally provides, with no extra work, subpixel positioning of glyphs(which could also have been implemented in the old framework, but wouldhave required careful tracking of subpixel offets).Note that all baseline images should be regenerated.  The new APIs addedto FT2Font are also up to bikeshedding (but they are all private).
@QuLogic
Copy link
Member

I've rebased again and added the test images. This only includes the images that have changed here and not in thetext-overhaul branch already. This can be a bit misleading, as the image diffs will include all the changes up to now. To compare thetext-overhaul branch's figure to this PR, please use this link:QuLogic/matplotlib@text-overhaul-figures...anntzer:matplotlib:ft-direct-render

@greglucas
Copy link
Contributor

Looking atlib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_41.png in the comparisons it looks like the new "epsilon" looks squished on the top curve to me. It looks like almost everything was compressed somehow in that image. I'm not sure if that is expected or not, just thought I'd comment on that glyph since it stood out to me.

@QuLogic
Copy link
Member

Ah sorry, I just realized that GitHub gave me the "base" comparison link, so it actually shows the full diff betweenmain and this PR. Thecorrect diff link (with a difference of one.) ishttps://github.com/QuLogic/matplotlib/compare/text-overhaul-figures..anntzer:matplotlib:ft-direct-render

If I'm not mistaken, t looks like the epsilon was changed in an earlier PR.

@greglucas
Copy link
Contributor

That looks like a more minimal change in the epsilon, so I agree with you that it was updated in a different PR. 👍 Still has my approval.

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@QuLogicQuLogicQuLogic left review comments

@greglucasgreglucasgreglucas approved these changes

Assignees

No one assigned

Projects

Status: Ready for Review

Milestone

v3.11.0

Development

Successfully merging this pull request may close these issues.

6 participants

@anntzer@story645@QuLogic@timhoffm@tacaswell@greglucas

[8]ページ先頭

©2009-2025 Movatter.jp