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

Implement a subset of FontTools Pens for write-fonts, leaning on kurbo for things like transform #237

Merged
merged 6 commits into from
Mar 1, 2023

Conversation

rsheeter
Copy link
Collaborator

@rsheeter rsheeter commented Feb 16, 2023

Having this makes porting algorithms implemented against pens in FontTools and nanoemoji easier.

My immediate need is to match https://github.com/googlefonts/ufo2ft/blob/dd738cdcddf61cce2a744d1cafab5c9b33e92dd4/Lib/ufo2ft/util.py#L205 for googlefonts/fontc#123 so I need transform and reverse. I will presumably need a glyf pen too but I haven't done that yet.

@rsheeter rsheeter requested a review from dfrg February 16, 2023 02:48
@rsheeter rsheeter changed the title [WIP] Implement FontTools Pens for write-fonts [WIP] Implement a subset of FontTools Pens for write-fonts, leaning on kurbo for things like transform Feb 16, 2023
Copy link
Member

@dfrg dfrg left a comment

Choose a reason for hiding this comment

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

Looks good with some suggestions for more idiomatic code.

write-fonts/src/pens.rs Outdated Show resolved Hide resolved
write-fonts/src/pens.rs Outdated Show resolved Hide resolved
@rsheeter rsheeter marked this pull request as ready for review February 16, 2023 03:41
@rsheeter rsheeter changed the title [WIP] Implement a subset of FontTools Pens for write-fonts, leaning on kurbo for things like transform Implement a subset of FontTools Pens for write-fonts, leaning on kurbo for things like transform Feb 17, 2023
write-fonts/src/pens.rs Outdated Show resolved Hide resolved
write-fonts/src/pens.rs Outdated Show resolved Hide resolved
});
}

// a closing line to start is a Z
Copy link
Member

Choose a reason for hiding this comment

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

what you're doing here (replacing the last LineTo of the reversed path with a ClosePath when it overlaps the starting point) is problematic because you may inadvertently drop a duplicate lineTo point that immediately followed moveTo pre-reversal and which deliberately has the same (x,y) coordinates, thus potentially making VF sources incompatible after reversing.

In fontTools ReverseContourPen (as well as in the PointToSegmentPen that converts between points and segment-oriented pens) we have an option outputImpliedClosingLine that when True it always output the implied closing line regardless of whether the last point overlaps moveTo or not. Setting this option is preferable when working with VF masters.
eg. see this font where some contour points collaps onto one another but only in some masters googlefonts/fontmake#572

But even when outputImpliedClosingLine=False, fonttools ReverseContourPen will not omit the last lineTo (which was the first one, before reversing) if it overlaps with the starting point, otherwise the number of points and segments would change after reversing
See googlefonts/cu2qu#51 (fixed by fonttools/fonttools#1080)

see this test case:
https://github.com/fonttools/fonttools/blob/bf265ce49e0cae6f032420a4c80c31d8e16285b8/Tests/pens/reverseContourPen_test.py#L227-L250

and also this one: https://github.com/fonttools/fonttools/blob/bf265ce49e0cae6f032420a4c80c31d8e16285b8/Tests/pens/reverseContourPen_test.py#L38-L53

actually all the tests in reverseContourPen_test.py would be nice to port over...

Copy link
Member

Choose a reason for hiding this comment

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

what you're doing here (replacing the last LineTo of the reversed path with a ClosePath when it overlaps the starting point)

I read the code again and I think I got confused.. from my testing, it looks like you are already emitting reversed paths which look like the ones from fonttools ReverseContourPen with outputImpliedClosingLine=True
I'll take a break and come back to this

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call re porting tests, will do.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm dubious about the FontTools practice of eliminating the line from a close during reversal. Imagine you have several versions of move/line/line/close and in some of them the last line ends at the start ... if you eliminate the "unnecessary" line from close as suggested by https://github.com/fonttools/fonttools/blob/bf265ce49e0cae6f032420a4c80c31d8e16285b8/Tests/pens/reverseContourPen_test.py#L23 - note that an input MLLLC turns into an output MLLC - then these shapes will no longer interpolate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I suppose ^ is to say I like the outputImpliedClosingLine=True behavior and do not currently implement the FontTools default mode of dropping the closing lines.

write-fonts/src/pens.rs Outdated Show resolved Hide resolved
pen.curve_to(150.0, 150.0, 50.0, 150.0, 25.0, 300.0);
pen.quad_to(0.0, 150.0, 75.0, 100.0);
pen.line_to(100.0, 50.0);
pen.close();
Copy link
Member

Choose a reason for hiding this comment

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

I think I found an issue. All your closed test shapes end with a line_to so this does not show up in your tests.
Basically, whenever the last pen command before the close is not a line_to (e.g. a curve_to or quad_to or move_to even), then after reversing you end up with an additional line segment that overlaps the move.
E.g. given this path (notice it ends with a curve_to before the closing 'Z'):

"M100,100 L150,200 L50,200 C75,200 75,100 100,100 Z"

If I reverse it using your current code, I get this (I added commas for legibility):

"M100,100 L100,100 C75,100 75,200 50,200 L150,200 Z"

Note that first 'L' segment following the 'M' is a duplicate and was not in the original closed path.

Compare this with the output from fontTools ReverseContourPen:

from fontTools.pens.recordingPen import RecordingPen
from fontTools.pens.reverseContourPen import ReverseContourPen
rec = RecordingPen()
rev = ReverseContourPen(rec)
rev.moveTo((100.0, 100.0))
rev.lineTo((150.0, 200.0))
rev.lineTo((50.0, 200.0))
rev.curveTo((75.0, 200.0), (75.0, 100.0), (100.0, 100.0))
rev.closePath()
print(rec.value)
"""
[('moveTo', ((100.0, 100.0),)),
 ('curveTo', ((75.0, 100.0), (75.0, 200.0), (50.0, 200.0))),
 ('lineTo', ((150.0, 200.0),)),
 ('closePath', ())]
"""

Copy link
Member

Choose a reason for hiding this comment

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

All this talk about implied closing line lead me to discover what I believe is a bug in fonttools' implementation as well LOL

fonttools/fonttools#2995

The ReverseContourPen's outputImpliedClosingLine parameter is a very recent addition by behdad, we haven't released it yet (a parameter with that name previously existed only in the PointToSegmentPen). It turns out it has a very similar bug whereby a duplicate lineTo, overlapping moveTo, gets inserted after reversing a closed path.
The bug is not present in the default case, when outputImpliedClosingLine=False.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added commas for legibility

@dfrg are we allowed to change the to_svg format for Kurbo? - it is quite illegible right now

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Forgive my density but I don't quite follow.

E.g. given this path (notice it ends with a curve_to before the closing 'Z'):

"M100,100 L150,200 L50,200 C75,200 75,100 100,100 Z" => MLLCZ

If I reverse it using your current code, I get this (I added commas for legibility):

"M100,100 L100,100 C75,100 75,200 50,200 L150,200 Z" => MLCLZ

So before and after we have 1 M, 2 L's, 1 C, and a Z. In the original the Z is pointless - we already ended the C at 100,100 - so when reversed the first command is pointless. I feel like I'm missing something.

Copy link
Member

Choose a reason for hiding this comment

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

I don’t think kurbo makes any particular guarantees about the svg output. I assume you’d prefer a format like the comment above?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I assume you’d prefer a format like the comment above?

Yes. The , helps spot which values are a point and the space before the letter makes it far easier to spot the commands.

write-fonts/src/pens.rs Outdated Show resolved Hide resolved
font-types/src/pen.rs Outdated Show resolved Hide resolved
write-fonts/src/pens.rs Outdated Show resolved Hide resolved
@rsheeter rsheeter merged commit f9b38ec into main Mar 1, 2023
@rsheeter rsheeter deleted the glyf branch March 1, 2023 03:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants