-
Notifications
You must be signed in to change notification settings - Fork 222
Description
I have constructed a font in order to cover planes 0, 1, and 15 with blank glyphs. I am using cmap format 12 for this because format 13 is less well supported. To keep the cmap size low, I create a large number N of identical empty glyphs, so that I can cover N codepoints with every group in format 12.
I have found N can't exceed about 5500 glyphs or else Chrome fails to load the font. The woff2_decompress tool also fails at exactly the same N with the error "Implausible compression ratio" from
Line 1371 in 0f4d304
| fprintf(stderr, "Implausible compression ratio %.01f\n", compression_ratio); |
My font loads in other tools and as far as I understand is valid per the specification. The compression ratio check seems wrong.
It's a bit moot because even with N = ~5500 I can get the woff2 size to ~560 bytes and that compression level is accepted (it's just under 100). But, if this library accepted my font, I could use N = 65534 and get the size to ~340 bytes. At this most extreme level the error is "Implausible compression ratio 1952.7".
Here is a font that reproduces the error. If my font fails to load you will see the fallback font render "failed".
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
@font-face {
font-family: "EmbeddedFont";
src: url(data:font/woff2;base64,d09GMgABAAAAAAIQAAoAAAAB2CwAAAHBCWcGJQAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgzQKhe4Agv0WATYCJAPdZAvdZAAEIAUGByAbLfAIHoPj7mZevjSiUKIEijze4nn4cf/92k86A6MCimKk8zjDX22QwFC+0IbpWzcA8p+TTYcnHEeB5FwWpJZhYAkneCAf4JWufLcxpVYjcDrartFCTc3tnFdL7cSa93Fho4p6c9UReLTXjah88tc/NTX6wcrpC6Z7RkV9//fTcDcv1ktFRc2g2tPopxpRrQktWiDvAQYAXbcjdN3BmOeEMS/MWYowZxlrnhLWPLN3LGHvOKevIpy+xlsF4a12Hh4nPDzBJz8gfPKjr9chfL1e8iEi+Ui6TyXSfZqMu4XIuNsyry+Ref2y7hyRdeezryKyn5zdTuTsjrz9nMjbL/J4KZHHy/LZUyKfPZNvxxL5dlwqiIJS/ZSOd5TqZ0rPsUTpOa5MuIooE64pCwqiLGhXNhxHlA0nyoEfiHLgR+X8OqKcX1/e/ZAo735Unk4lytNp5YtbRPnidvm+L1G+71cqCHTdCWNe5ckXOT6YJgBQ9SMQQcoDQFUj0wQggD4QAJwLjdvqBt87mf7ryqTfGmobwFcfvtwCb1b9twGAOlUg/BSY19y2p2aSPQ33btxME4AT9oiV6LpTFaEcrlOtq7yZDQAA);
}
body {
font-family: "EmbeddedFont";
}
</style>
</head>
<body>
<span style="color:red">failed</span>
</body>
</html>Here is a font prepared the same way with slightly fewer glyphs that works because the compression ratio is small enough:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
@font-face {
font-family: "EmbeddedFont";
src: url(data:font/woff2;base64,d09GMgABAAAAAAJEAAoAAAABimAAAAH0CWcGJQAAAAAAAAAAAAAAAAAAAAAAAAAABmAAhAgKhPEAgr1aATYCJAPOFAvOFAAEIAUGByAb9cgInoWpyLIe3b4Xh1wky5EY9GdFkQzTWbd2LoCYNQBNQEsAagC+eaABTjpABjQFcCviQQYsPQAggHHV7LodwGQgMgkNdEPim+k2v1ic/zo84TgKJOeyILUMA0s4wQP5AM905LuNKbUagdPR9uwWVsQ7giFWseoovguvN5aoXwqbwUPwifP/j37+Fos5Bk/9I/z5jP9lVf+MbewJPuTR//9ZMZf0wTHLEMtswQ47oD8AZ4CjTsJRlxMlCCfKLoYJFyMujQiXxp4tE56tuLUl3Nq5OybcnXjrS3jr5/6CcH/pWyyEb3F4eA7h4QUecyE85uHvLYS/ClKphUiljsy/Q2T+g9zaIHJrL0oGUZSccgeJcodq1CJq1K5jmqhjpjErojHrNreJNnc6d4Xo3I2enBI9OSNFKESKMPp9H6LfD+h/xMDIlrH9kZGtMHq2xOjZjc0xMTYnE+BLTIAfk3xBTPLllMdCTHkc0/ycmOYXM5RLzFDezN8SM6+Y41pijuvm+jsx1z/mXRvEvGufTzKI+SRn/kfgqBsnqslTOfEH2QMAlpoAEgQtAAgbyR4AAsCYBgCcC5ulUovPr+565ip5fZVL5MD3+x+LwY/Mv6kArLEEhP+E85UdFmIvFvJ7N26yB8ArFmIljrqXCOXwGst0/73EBgAAAA==);
}
body {
font-family: "EmbeddedFont";
}
</style>
</head>
<body>
<span style="color:red">failed</span>
</body>
</html>Here is a python script that produces the fonts above. It uses an input woff2 to get the metrics to use for the empty glyphs. woff2_decompress will fail with the implausible compression ratio error unless N is below the threshold which is near ~5500.
MAX_UNICODE = 0x10FFFF # maximum Unicode code point
def make_empty_glyph():
g = Glyph()
g.numberOfContours = 1
g.endPtsOfContours = [1]
g.flags = [1, 1]
g.coordinates = GlyphCoordinates([(0, 0), (0, 0)])
g.program = Program()
return g
def optimize(input_path, output_path, planes):
font = TTFont(input_path)
metrics = font['hmtx'].metrics[font['cmap'].getBestCmap()[0x30]] # 0 to match CSS 1ch
glyf = font['glyf']
hmtx = font['hmtx']
MAX_GLYPHS = 6000 # change to 5000 and it will work
stub_names = []
for i in range(MAX_GLYPHS):
name = f"fontflayer_stub_{i:04X}"
stub_names.append(name)
g = make_empty_glyph()
glyf[name] = g
hmtx.metrics[name] = metrics
cmap = {}
new_order = [".notdef"]
new_order.extend(stub_names)
font.setGlyphOrder(new_order)
for plane in planes:
plane_start = plane * 0x10000
plane_end = plane_start + 0xFFFF
for cp in range(plane_start, min(plane_end + 1, MAX_UNICODE + 1)):
cmap[cp] = stub_names[cp % MAX_GLYPHS]
new_sub = CmapSubtable.newSubtable(12)
new_sub.platformID = 3
new_sub.platEncID = 10
new_sub.language = 0
new_sub.cmap = cmap
font['cmap'].tables = [new_sub]
# subset
opts = Options()
opts.hinting = False
opts.glyph_names = False
opts.legacy_cmap = False
opts.symbol_cmap = False
opts.name_IDs = []
opts.name_legacy = False
opts.name_languages = []
opts.drop_tables = [
'DSIG', 'gasp', 'kern', 'GPOS', 'GSUB',
'GDEF', 'BASE', 'JSTF', 'cvt ', 'fpgm',
'prep', 'VDMX', 'gvar', 'gsub', 'FVAR',
'DSIG','gasp','kern','GPOS','GSUB',
'GDEF','BASE','JSTF','cvt ','fpgm',
'prep','VDMX','gvar','avar','cvar','fvar','STAT','HVAR','MVAR','hdmx','LTSH'
]
subsetter = Subsetter(options=opts)
subsetter.populate(glyphs=font.getGlyphOrder())
subsetter.subset(font)
font.save(output_path)
default_planes = [0, 1, 15]
if __name__ == "__main__":
if len(sys.argv) < 3 or len(sys.argv) > 4:
print(f"Usage: python {sys.argv[0]} input.woff2 output.woff2 [planes]")
print("planes: comma-delimited list of plane numbers (default: 0,1,15)")
else:
planes = default_planes
if len(sys.argv) == 4:
planes = [int(p.strip()) for p in sys.argv[3].split(',')]
optimize(sys.argv[1], sys.argv[2], planes)