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

[varLib.mutator] Improve CharString rounding #3481

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion Lib/fontTools/pens/t2CharStringPen.py
Expand Up @@ -47,7 +47,7 @@ def _closePath(self):
def _endPath(self):
pass

def getCharString(self, private=None, globalSubrs=None, optimize=True):
def getProgram(self, optimize=True):
commands = self._commands
if optimize:
maxstack = 48 if not self._CFF2 else 513
Expand All @@ -62,6 +62,10 @@ def getCharString(self, private=None, globalSubrs=None, optimize=True):
program.insert(0, otRound(self._width))
if not self._CFF2:
program.append("endchar")
return program

def getCharString(self, private=None, globalSubrs=None, optimize=True):
program = self.getProgram(optimize=optimize)
charString = T2CharString(
program=program, private=private, globalSubrs=globalSubrs
)
Expand Down
10 changes: 9 additions & 1 deletion Lib/fontTools/varLib/mutator.py
Expand Up @@ -69,6 +69,8 @@ def interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas):


def interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder):
from fontTools.pens.t2CharStringPen import T2CharStringPen

charstrings = topDict.CharStrings
for gname in glyphOrder:
# Interpolate charstring
Expand Down Expand Up @@ -100,7 +102,7 @@ def interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder):
next_ti = tuplei + num_regions
deltas = charstring.program[tuplei:next_ti]
delta = interpolateFromDeltas(vsindex, deltas)
charstring.program[argi] += otRound(delta)
charstring.program[argi] += delta
tuplei = next_ti
argi += 1
new_program.extend(charstring.program[last_i:end_args])
Expand All @@ -109,6 +111,12 @@ def interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder):
new_program.extend(charstring.program[last_i:])
charstring.program = new_program

# Rebuild charstring program by passing it through T2CharStringPen which
# will have the side effect of rounding coordinates.
t2Pen = T2CharStringPen(width=None, glyphSet=None)
charstring.draw(t2Pen)
charstring.program = t2Pen.getProgram(optimize=True)


def interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc):
"""Unlike TrueType glyphs, neither advance width nor bounding box
Expand Down
147 changes: 147 additions & 0 deletions Tests/varLib/data/TestCFF2VFRounding.ttx
@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont sfntVersion="OTTO" ttLibVersion="4.51">

<GlyphOrder>
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
<GlyphID id="0" name=".notdef"/>
<GlyphID id="1" name="glyph00001"/>
</GlyphOrder>

<head>
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="5.0"/>
<checkSumAdjustment value="0xbcbbec73"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000000"/>
<unitsPerEm value="4000"/>
<created value="Tue Apr 9 18:59:32 2024"/>
<modified value="Tue Apr 9 18:59:32 2024"/>
<xMin value="-4000"/>
<yMin value="-4000"/>
<xMax value="4000"/>
<yMax value="4000"/>
<macStyle value="00000000 00000010"/>
<lowestRecPPEM value="6"/>
<fontDirectionHint value="2"/>
<indexToLocFormat value="0"/>
<glyphDataFormat value="0"/>
</head>

<maxp>
<tableVersion value="0x5000"/>
<numGlyphs value="2"/>
</maxp>

<hmtx>
<mtx name=".notdef" width="4000" lsb="0"/>
<mtx name="glyph00001" width="4000" lsb="-228"/>
</hmtx>

<CFF2>
<major value="2"/>
<minor value="0"/>
<CFFFont name="CFF2Font">
<FontMatrix value="0.00025 0 0 0.00025 0 0"/>
<FDArray>
<FontDict index="0">
<Private>
<BlueScale value="0.039625"/>
<BlueShift value="7"/>
<BlueFuzz value="1"/>
<LanguageGroup value="0"/>
<ExpansionFactor value="0.06"/>
</Private>
</FontDict>
</FDArray>
<CharStrings>
<CharString name=".notdef">
</CharString>
<CharString name="glyph00001">
-216 -4 1 blend
-12 rmoveto
64 240 12 92 120 188 12 1 blend
-12 64 hhcurveto
24 12 60 8 1 blend
16 12 -4 12 -12 hvcurveto
-200 16 -48 8 68 vvcurveto
52 44 80 -8 1 blend
80 144 vhcurveto
908 1580 -40 -36 2 blend
8 0 80 -1048 4 -20 20 -8 3 blend
-68 rlineto
-500 8 156 -476 640 -140 20 148 156 -100 5 blend
hhcurveto
928 380 976 668 560 -268 336 -544 -472 -352 -276 -408 -212 24 24 -8 -20 8 -36 20 -24 60 40 84 40 -40 13 blend
hvcurveto
-12 hlineto
-16 204 -12 208 -12 220 4 -60 4 -48 4 -20 6 blend
rrcurveto
20 4 1 blend
-8 8 -16 -16 1 blend
-16 -100 -48 1 blend
-4 -8 -76 -4 -4 2 blend
vhcurveto
-20 -4 1 blend
-4 -8 -8 -8 -16 -372 -640 -360 -640 -380 -636 4 -4 -4 192 336 -160 -260 -28 -68 9 blend
-232 -388 -4 1 blend
-48 -8 8 1 blend
-212 -20 rrcurveto
-16 -16 -40 -8 1 blend
-36 -12 4 -12 8 hvcurveto
2508 80 -8 8 2 blend
</CharString>
</CharStrings>
<VarStore Format="1">
<Format value="1"/>
<VarRegionList>
<!-- RegionAxisCount=1 -->
<!-- RegionCount=1 -->
<Region index="0">
<VarRegionAxis index="0">
<StartCoord value="0.0"/>
<PeakCoord value="1.0"/>
<EndCoord value="1.0"/>
</VarRegionAxis>
</Region>
</VarRegionList>
<!-- VarDataCount=1 -->
<VarData index="0">
<!-- ItemCount=0 -->
<NumShorts value="0"/>
<!-- VarRegionCount=1 -->
<VarRegionIndex index="0" value="0"/>
</VarData>
</VarStore>
</CFFFont>

<GlobalSubrs>
<!-- The 'index' attribute is only for humans; it is ignored when parsed. -->
</GlobalSubrs>
</CFF2>

<avar>
<version major="1" minor="0"/>
<segment axis="wght">
<mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/>
<mapping from="0.3333" to="0.3"/>
<mapping from="0.6667" to="0.63336"/>
<mapping from="1.0" to="1.0"/>
</segment>
</avar>

<fvar>

<!-- Weight -->
<Axis>
<AxisTag>wght</AxisTag>
<Flags>0x0</Flags>
<MinValue>400.0</MinValue>
<DefaultValue>400.0</DefaultValue>
<MaxValue>700.0</MaxValue>
<AxisNameID>256</AxisNameID>
</Axis>
</fvar>

</ttFont>
21 changes: 11 additions & 10 deletions Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx
Expand Up @@ -34,35 +34,33 @@
84 hmoveto
432 660 -432 hlineto
48 -628 rmoveto
102 176 64 106 rlineto
4 hlineto
62 -106 100 -176 rlineto
102 176 64 106 4 0 62 -106 100 -176 rlineto
-342 42 rmoveto
536 vlineto
154 -270 rlineto
22 26 rmoveto
-56 92 -94 168 rlineto
302 hlineto
-94 -168 -54 -92 rlineto
-56 92 -94 168 302 0 -94 -168 -54 -92 rlineto
22 -26 rmoveto
152 270 rlineto
-536 vlineto
endchar
</CharString>
<CharString name="A">
50 hmoveto
32 hlineto
140 396 28 80 24 68 24 82 rlinecurve
4 hlineto
24 -82 24 -68 28 -80 138 -396 rcurveline
34 hlineto
-236 660 rlineto
24 -82 24 -68 28 -80 rrcurveto
138 -396 34 0 -236 660 rlineto
-28 hlineto
-134 -424 rmoveto
293 28 -293 hlineto
endchar
</CharString>
<CharString name="T">
284 hmoveto
32 632 234 28 -500 -28 234 hlineto
endchar
</CharString>
<CharString name="dollar">
311 34 rmoveto
Expand All @@ -74,14 +72,16 @@
-99 -78 -54 -88 hvcurveto
-166 338 28 -156 vvcurveto
-70 -56 -50 -103 -85 -66 38 34 -40 vhcurveto
-18 -22 45 -38 73 -40 91 0 rlinecurve
-18 -22 rlineto
-38 45 73 -40 91 hhcurveto
-18 566 rmoveto
30 hlineto
50 0 50 50 vvcurveto
-30 hlineto
-50 0 -50 -50 vvcurveto
-562 vmoveto
-148 30 148 vlineto
endchar
</CharString>
<CharString name="glyph00003">
311 34 rmoveto
Expand All @@ -97,6 +97,7 @@
-38 45 73 -40 91 hhcurveto
-70 -146 rmoveto
158 860 -30 4 -158 -860 rlineto
endchar
</CharString>
</CharStrings>
</CFFFont>
Expand Down
18 changes: 18 additions & 0 deletions Tests/varLib/mutator_test.py
Expand Up @@ -179,6 +179,24 @@ def test_varlib_mutator_CFF2(self):
expected_ttx_path = self.get_test_output(expected_ttx_name + ".ttx")
self.expect_ttx(new_font, expected_ttx_path, tables)

def test_varlib_mutator_CFF2_rounding(self):
from fontTools.pens.recordingPen import RecordingPen

varfont = TTFont(recalcBBoxes=False, recalcTimestamp=False)
varfont.importXML(self.get_test_input("TestCFF2VFRounding.ttx"))
font = make_instance(varfont, {"wght": float(600)})

charString = font["CFF2"].cff[0].CharStrings["glyph00001"]
pen = RecordingPen()
charString.draw(pen)

# Accumulated rounding errors can cause the last point of the contour to
# not to match first point, leading to rendering artifacts.
assert pen.value[0][0] == "moveTo"
assert pen.value[0][1][0] == (-219, -12)
assert pen.value[-2][0] == "curveTo"
assert pen.value[-2][1][2] == pen.value[0][1][0]


if __name__ == "__main__":
sys.exit(unittest.main())