Merge pull request #3119 from fonttools/glyph-data-format-option
fontBuilder: add glyphDataFormat=0; error with accidentally cubic outlines
This commit is contained in:
commit
77a35fe16d
@ -131,6 +131,7 @@ fb.save("test.otf")
|
|||||||
|
|
||||||
from .ttLib import TTFont, newTable
|
from .ttLib import TTFont, newTable
|
||||||
from .ttLib.tables._c_m_a_p import cmap_classes
|
from .ttLib.tables._c_m_a_p import cmap_classes
|
||||||
|
from .ttLib.tables._g_l_y_f import flagCubic
|
||||||
from .misc.timeTools import timestampNow
|
from .misc.timeTools import timestampNow
|
||||||
import struct
|
import struct
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
@ -319,7 +320,7 @@ _OS2Defaults = dict(
|
|||||||
|
|
||||||
|
|
||||||
class FontBuilder(object):
|
class FontBuilder(object):
|
||||||
def __init__(self, unitsPerEm=None, font=None, isTTF=True):
|
def __init__(self, unitsPerEm=None, font=None, isTTF=True, glyphDataFormat=0):
|
||||||
"""Initialize a FontBuilder instance.
|
"""Initialize a FontBuilder instance.
|
||||||
|
|
||||||
If the `font` argument is not given, a new `TTFont` will be
|
If the `font` argument is not given, a new `TTFont` will be
|
||||||
@ -327,15 +328,31 @@ class FontBuilder(object):
|
|||||||
the font will be a glyf-based TTF; if `isTTF` is False it will be
|
the font will be a glyf-based TTF; if `isTTF` is False it will be
|
||||||
a CFF-based OTF.
|
a CFF-based OTF.
|
||||||
|
|
||||||
|
The `glyphDataFormat` argument corresponds to the `head` table field
|
||||||
|
that defines the format of the TrueType `glyf` table (default=0).
|
||||||
|
TrueType glyphs historically can only contain quadratic splines and static
|
||||||
|
components, but there's a proposal to add support for cubic Bezier curves as well
|
||||||
|
as variable composites/components at
|
||||||
|
https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md
|
||||||
|
You can experiment with the new features by setting `glyphDataFormat` to 1.
|
||||||
|
A ValueError is raised if `glyphDataFormat` is left at 0 but glyphs are added
|
||||||
|
that contain cubic splines or varcomposites. This is to prevent accidentally
|
||||||
|
creating fonts that are incompatible with existing TrueType implementations.
|
||||||
|
|
||||||
If `font` is given, it must be a `TTFont` instance and `unitsPerEm`
|
If `font` is given, it must be a `TTFont` instance and `unitsPerEm`
|
||||||
must _not_ be given. The `isTTF` argument will be ignored.
|
must _not_ be given. The `isTTF` and `glyphDataFormat` arguments will be ignored.
|
||||||
"""
|
"""
|
||||||
if font is None:
|
if font is None:
|
||||||
self.font = TTFont(recalcTimestamp=False)
|
self.font = TTFont(recalcTimestamp=False)
|
||||||
self.isTTF = isTTF
|
self.isTTF = isTTF
|
||||||
now = timestampNow()
|
now = timestampNow()
|
||||||
assert unitsPerEm is not None
|
assert unitsPerEm is not None
|
||||||
self.setupHead(unitsPerEm=unitsPerEm, created=now, modified=now)
|
self.setupHead(
|
||||||
|
unitsPerEm=unitsPerEm,
|
||||||
|
create=now,
|
||||||
|
modified=now,
|
||||||
|
glyphDataFormat=glyphDataFormat,
|
||||||
|
)
|
||||||
self.setupMaxp()
|
self.setupMaxp()
|
||||||
else:
|
else:
|
||||||
assert unitsPerEm is None
|
assert unitsPerEm is None
|
||||||
@ -631,7 +648,7 @@ class FontBuilder(object):
|
|||||||
for fontDict in topDict.FDArray:
|
for fontDict in topDict.FDArray:
|
||||||
fontDict.Private.vstore = vstore
|
fontDict.Private.vstore = vstore
|
||||||
|
|
||||||
def setupGlyf(self, glyphs, calcGlyphBounds=True):
|
def setupGlyf(self, glyphs, calcGlyphBounds=True, validateGlyphFormat=True):
|
||||||
"""Create the `glyf` table from a dict, that maps glyph names
|
"""Create the `glyf` table from a dict, that maps glyph names
|
||||||
to `fontTools.ttLib.tables._g_l_y_f.Glyph` objects, for example
|
to `fontTools.ttLib.tables._g_l_y_f.Glyph` objects, for example
|
||||||
as made by `fontTools.pens.ttGlyphPen.TTGlyphPen`.
|
as made by `fontTools.pens.ttGlyphPen.TTGlyphPen`.
|
||||||
@ -639,8 +656,26 @@ class FontBuilder(object):
|
|||||||
If `calcGlyphBounds` is True, the bounds of all glyphs will be
|
If `calcGlyphBounds` is True, the bounds of all glyphs will be
|
||||||
calculated. Only pass False if your glyph objects already have
|
calculated. Only pass False if your glyph objects already have
|
||||||
their bounding box values set.
|
their bounding box values set.
|
||||||
|
|
||||||
|
If `validateGlyphFormat` is True, raise ValueError if any of the glyphs contains
|
||||||
|
cubic curves or is a variable composite but head.glyphDataFormat=0.
|
||||||
|
Set it to False to skip the check if you know in advance all the glyphs are
|
||||||
|
compatible with the specified glyphDataFormat.
|
||||||
"""
|
"""
|
||||||
assert self.isTTF
|
assert self.isTTF
|
||||||
|
|
||||||
|
if validateGlyphFormat and self.font["head"].glyphDataFormat == 0:
|
||||||
|
for name, g in glyphs.items():
|
||||||
|
if g.isVarComposite():
|
||||||
|
raise ValueError(
|
||||||
|
f"Glyph {name!r} is a variable composite, but glyphDataFormat=0"
|
||||||
|
)
|
||||||
|
elif g.numberOfContours > 0 and any(f & flagCubic for f in g.flags):
|
||||||
|
raise ValueError(
|
||||||
|
f"Glyph {name!r} has cubic Bezier outlines, but glyphDataFormat=0; "
|
||||||
|
"either convert to quadratics with cu2qu or set glyphDataFormat=1."
|
||||||
|
)
|
||||||
|
|
||||||
self.font["loca"] = newTable("loca")
|
self.font["loca"] = newTable("loca")
|
||||||
self.font["glyf"] = newTable("glyf")
|
self.font["glyf"] = newTable("glyf")
|
||||||
self.font["glyf"].glyphs = glyphs
|
self.font["glyf"].glyphs = glyphs
|
||||||
|
@ -137,6 +137,29 @@ def test_build_ttf(tmpdir):
|
|||||||
_verifyOutput(outPath)
|
_verifyOutput(outPath)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_cubic_ttf(tmp_path):
|
||||||
|
pen = TTGlyphPen(None)
|
||||||
|
pen.moveTo((100, 100))
|
||||||
|
pen.curveTo((200, 200), (300, 300), (400, 400))
|
||||||
|
pen.closePath()
|
||||||
|
glyph = pen.glyph()
|
||||||
|
glyphs = {"A": glyph}
|
||||||
|
|
||||||
|
# cubic outlines are not allowed in glyf table format 0
|
||||||
|
fb = FontBuilder(1000, isTTF=True, glyphDataFormat=0)
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError, match="Glyph 'A' has cubic Bezier outlines, but glyphDataFormat=0"
|
||||||
|
):
|
||||||
|
fb.setupGlyf(glyphs)
|
||||||
|
# can skip check if feeling adventurous
|
||||||
|
fb.setupGlyf(glyphs, validateGlyphFormat=False)
|
||||||
|
|
||||||
|
# cubics are (will be) allowed in glyf table format 1
|
||||||
|
fb = FontBuilder(1000, isTTF=True, glyphDataFormat=1)
|
||||||
|
fb.setupGlyf(glyphs)
|
||||||
|
assert "A" in fb.font["glyf"].glyphs
|
||||||
|
|
||||||
|
|
||||||
def test_build_otf(tmpdir):
|
def test_build_otf(tmpdir):
|
||||||
outPath = os.path.join(str(tmpdir), "test.otf")
|
outPath = os.path.join(str(tmpdir), "test.otf")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user