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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions