opentypejs / opentype.js

Read and write OpenType fonts using JavaScript.
https://opentype.js.org/
MIT License
4.49k stars 479 forks source link

Subsetted font: Failed to decode downloaded font #155

Open raphaelokon opened 9 years ago

raphaelokon commented 9 years ago

Hi guys. I am creating a subsetted TTF from a TTF like:

OT.load(fontURL, function (err, font) {

let glyphs = font.stringToGlyphs(uniqueChars);
let subsetFont = new OT.Font({
                        familyName: font.familyName,
                        styleName: font.styleName,
                        unitsPerEm: font.unitsPerEm,
                        glyphs: glyphs
                    });
});
    @font-face {
        font-family: MyFont;
        src: url(fonts/subset.ttf) format('truetype');
        font-weight: normal;
        font-style: normal;
    }

When I try to use this font with a @font-face I get a Failed to decode downloaded font message in browser. Any clues? Any help really appreciated.

fdb commented 7 years ago

Hi @raphaelokon, I know this is old, but do you still have this issue? Could you provide me with the font so I can test it?

tybenz commented 6 years ago

I'm able to reproduce this one.

Here's my source font file: https://www.dropbox.com/s/hxukwekcetivirc/FiraCode-Bold.otf?dl=0

Here's the subsetted version (created with opentype.js in node): https://www.dropbox.com/s/x6shux7arexvp9o/basic-latin2.otf?dl=0

tybenz commented 6 years ago

@fdb could you take a look at why the subsetted version might yield the browser error: "Failed to decode downloaded font"?

Jolg42 commented 6 years ago

Thanks! I'm just uploading these to GitHub to avoid future 404 links :)

FiraCode-Bold.otf.zip basic-latin2.otf.zip

fdb commented 6 years ago

This happens because both on Firefox and Chrome, fonts get "sanitized" using ots.

Trying this with the basic-latin2.otf file, I get

$ ./ots-sanitize basic-latin2.otf
ERROR: CFF : Failed to parse table
Failed to sanitize file!

So this is a bug in the CFF table but we don't know where. Since OTS is open-source I downloaded it in and uncommented #undef OTS_DEBUG in config.h to get more information about the error message. This gives me the following information:

ERROR at src/cff.cc:150 (ParseNameData)
ERROR at src/cff.cc:952 (Parse)
ERROR: CFF : Failed to parse table
Failed to sanitize file!

Looking at ParseNameData on line 150, I find:

// non-ASCII characters are not recommended (except the first character).
if (name[j] < 33 || name[j] > 126) {
  return OTS_FAILURE();
}
// [, ], ... are not allowed.
if (std::strchr("[](){}<>/% ", name[j])) {
  return OTS_FAILURE();
}

So I've printed out the font name. The name OpenType.js generates is Fira CodeBold (note the space). The original name is FiraCode-Bold. This is what OTS is complaining about: spaces are not allowed in the CFF font name.

If we comment out that code from OTS, the font validates successfully.

So we need to fix this in OpenType.js: whenever we write out the font name in the CFF table, we should make sure it doesn't contain any spaces.

fdb commented 6 years ago

Weirdly enough, there is already an explicit fix for this. Unless you specify the postScriptName yourself, OpenType.js should never generate a name with spaces.

Here's the code I use to generate a subset font that passes through ots:

const opentype = require('../dist/opentype.js');

const font = opentype.loadSync('../fonts/FiraSansMedium.woff');
const uniqueChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890 ';

let glyphs = font.stringToGlyphs(uniqueChars);
glyphs.unshift(font.glyphs.get(0)); // .notdef glyph
let subsetFont = new opentype.Font({
                        familyName: font.names.fontFamily.en,
                        styleName: font.names.fontSubfamily.en,
                        unitsPerEm: font.unitsPerEm,
                        ascender: font.ascender,
                        descender: font.descender,
                        glyphs: glyphs
                    });

console.log(subsetFont.names.postScriptName.en);
subsetFont.download('FiraSansSubset.otf');
Jolg42 commented 6 years ago

@fdb This fix is actually recent and not pushed on npm

fdb commented 6 years ago

This seems related to #339. I propose hammering a bit more on checking the user-given and auto-generated names so they don't throw any errors in OTS.

The regex by @fpirsch (.replace(/[^!-~[\](){}<>/%]/g, '')) seems suitable.

Also, I notice we do this in two places: in font.js and in sfnt.js when writing out the font. I propose just doing it in sfnt.js.

fdb commented 6 years ago

Also: should we throw an error if a user specifies a PostScript name that is not valid?

Note that the replace is just one part of the validation. The length of the name is also important.

Jolg42 commented 6 years ago

@fdb the safest option so far is to do smething like this

var postScriptName = 'mySuper Font Bold'
// Remove white-space and everything that is not a-z A-Z 0-9 or a dash
.replace(/[^a-zA-Z0-9-]/g, '')
// Remove dash from beginning of name
.replace(/^[-]*/g, '');

With a max length of 29 chars. Also only 1 dash should be present to avoid any issue...