Skip to content

"Implausible compression ratio" excludes some valid fonts #184

@jasonthorsness

Description

@jasonthorsness

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

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions