-
-
Notifications
You must be signed in to change notification settings - Fork 72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Flaticon.woff2 is not parsing correctly. #120
Comments
Looking at the function buildWoff2LazyLookups(woff2, decoded, createTable) {
woff2.tables = {};
woff2.directory.forEach((entry) => {
lazy(woff2.tables, entry.tag.trim(), () => {
const start = entry.offset;
const end =
start +
(entry.transformLength ? entry.transformLength : entry.origLength);
const data = decoded.slice(start, end);
return createTable(
woff2.tables,
{ tag: entry.tag, offset: 0, length: entry.origLength },
new DataView(data.buffer)
);
});
});
} |
Updated code to be more explicit about what's happening: function buildWoff2LazyLookups(woff2, decoded, createTable) {
woff2.tables = {};
woff2.directory.forEach((entry) => {
lazy(woff2.tables, entry.tag.trim(), () => {
const start = entry.offset;
const end =
start +
(entry.transformLength ? entry.transformLength : entry.origLength);
console.log(`packing data`);
let data = decoded.slice(start, end);
try {
data = new DataView(data.buffer);
} catch (e) {
console.error(e);
}
console.log(`packed data:`, data);
console.log(`creating table...`);
try {
return createTable(
woff2.tables,
{ tag: entry.tag, offset: 0, length: entry.origLength },
data
);
} catch (e) {
console.error(e);
}
});
});
} This reveals the following error:
|
Investigating class name extends SimpleTable {
constructor(dict, dataview) {
const { p } = super(dict, dataview);
console.log(`CONSTRUCTING NAME TABLE`);
this.format = p.uint16;
this.count = p.uint16;
this.stringOffset = p.Offset16; // relative to start of table
console.log(`parsing name records`);
// name records
this.nameRecords = [...new Array(this.count)].map(
(_) => new NameRecord(p, this)
);
// lang-tag records, if applicable
if (this.format === 1) {
this.langTagCount = p.uint16;
this.langTagRecords = [...new Array(this.langTagCount)].map(
(_) => new LangTagRecord(p.uint16, p.Offset16)
);
}
console.log(`caching global string start offset`);
// cache these values for use in `.get(nameID)`
this.stringStart = this.tableStart + this.stringOffset;
} This shows things go wrong during record parsing (we never get to the global string start offset) |
Adding debug prints to the NameRecord constructor: class NameRecord {
constructor(p, nameTable) {
this.platformID = p.uint16;
this.encodingID = p.uint16;
this.languageID = p.uint16;
this.nameID = p.uint16;
this.length = p.uint16;
this.offset = p.Offset16;
console.log(this.platformID, this.encodingID, this.languageID, this.nameID, this.length, this.offset);
lazy(this, `string`, () => {
p.currentPosition = nameTable.stringStart + this.offset;
return decodeString(p, this);
});
}
} Shows initially correct, but predominantly wrong, data. The fourth record is already quite clearly wrong, suggesting we've started parsing way too late in the name record section of the table (although miraculously, aligned to an actual record start!):
|
<name>
<namerecord nameID="0" platformID="1" platEncID="0" langID="0x0" unicode="True">
</namerecord>
<namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True">
Flaticon
</namerecord>
<namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True">
Regular
</namerecord>
<namerecord nameID="3" platformID="1" platEncID="0" langID="0x0" unicode="True">
FontForge 2.0 : Flaticon : 21-12-2019
</namerecord>
<namerecord nameID="4" platformID="1" platEncID="0" langID="0x0" unicode="True">
Flaticon
</namerecord>
<namerecord nameID="5" platformID="1" platEncID="0" langID="0x0" unicode="True">
Version 001.000
</namerecord>
<namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True">
Flaticon
</namerecord>
<namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
</namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
Flaticon
</namerecord>
<namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
Regular
</namerecord>
<namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
FontForge 2.0 : Flaticon : 21-12-2019
</namerecord>
<namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
Flaticon
</namerecord>
<namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
Version 001.000
</namerecord>
<namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
Flaticon
</namerecord>
</name> |
Data inspection: let decoded;
let buffer = dataview.buffer.slice(dictOffset);
if (brotliDecode) {
decoded = brotliDecode(new Uint8Array(buffer));
} else if (nativeBrotliDecode) {
decoded = new Uint8Array(nativeBrotliDecode(buffer));
} else {
const msg = `no brotli decoder available to decode WOFF2 font`;
if (font.onerror) font.onerror(msg);
throw new Error(msg);
}
const asText = Array.from(decoded).map(v => String.fromCharCode(v)).join(``);
const lines = asText.slice(17760, 17760 + 447).match(/([\w\W]{1,16})/g).map(line => {
return line.split(``).map(v => `${v.charCodeAt(0).toString(16).toUpperCase()}`.padStart(2, `0`)).join(` | `);
});
console.log(lines);
buildWoff2LazyLookups(this, decoded, createTable); Yields: [
'00 | 03 | 00 | 4A | 00 | 36 | 00 | 03 | 00 | 01 | 04 | 09 | 00 | 04 | 00 | 10',
'00 | A8 | 00 | 03 | 00 | 01 | 04 | 09 | 00 | 05 | 00 | 20 | 00 | C3 | 00 | 03',
'00 | 01 | 04 | 09 | 00 | 06 | 00 | 10 | 00 | F6 | 00 | 00 | 00 | 00 | 46 | 00',
'6C | 00 | 61 | 00 | 74 | 00 | 69 | 00 | 63 | 00 | 6F | 00 | 6E | 00 | 00 | 46',
'6C | 61 | 74 | 69 | 63 | 6F | 6E | 00 | 00 | 52 | 00 | 65 | 00 | 67 | 00 | 75',
'00 | 6C | 00 | 61 | 00 | 72 | 00 | 00 | 52 | 65 | 67 | 75 | 6C | 61 | 72 | 00',
'00 | 46 | 00 | 6F | 00 | 6E | 00 | 74 | 00 | 46 | 00 | 6F | 00 | 72 | 00 | 67',
'00 | 65 | 00 | 20 | 00 | 32 | 00 | 2E | 00 | 30 | 00 | 20 | 00 | 3A | 00 | 20',
'00 | 46 | 00 | 6C | 00 | 61 | 00 | 74 | 00 | 69 | 00 | 63 | 00 | 6F | 00 | 6E',
'00 | 20 | 00 | 3A | 00 | 20 | 00 | 32 | 00 | 31 | 00 | 2D | 00 | 31 | 00 | 32',
'00 | 2D | 00 | 32 | 00 | 30 | 00 | 31 | 00 | 39 | 00 | 00 | 46 | 6F | 6E | 74',
'46 | 6F | 72 | 67 | 65 | 20 | 32 | 2E | 30 | 20 | 3A | 20 | 46 | 6C | 61 | 74',
'69 | 63 | 6F | 6E | 20 | 3A | 20 | 32 | 31 | 2D | 31 | 32 | 2D | 32 | 30 | 31',
'39 | 00 | 00 | 46 | 00 | 6C | 00 | 61 | 00 | 74 | 00 | 69 | 00 | 63 | 00 | 6F',
'00 | 6E | 00 | 00 | 46 | 6C | 61 | 74 | 69 | 63 | 6F | 6E | 00 | 00 | 56 | 00',
'65 | 00 | 72 | 00 | 73 | 00 | 69 | 00 | 6F | 00 | 6E | 00 | 20 | 00 | 30 | 00',
'30 | 00 | 31 | 00 | 2E | 00 | 30 | 00 | 30 | 00 | 30 | 00 | 20 | 00 | 00 | 56',
'65 | 72 | 73 | 69 | 6F | 6E | 20 | 30 | 30 | 31 | 2E | 30 | 30 | 30 | 20 | 00',
'00 | 46 | 00 | 6C | 00 | 61 | 00 | 74 | 00 | 69 | 00 | 63 | 00 | 6F | 00 | 6E',
'00 | 00 | 46 | 6C | 61 | 74 | 69 | 63 | 6F | 6E | 00 | 00 | 02 | 00 | 00 | 00',
'00 | 00 | 00 | FF | C0 | 00 | 19 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00',
'00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 41 | 00 | 00 | 00',
'01 | 00 | 02 | 00 | 03 | 01 | 02 | 01 | 03 | 01 | 04 | 01 | 05 | 01 | 06 | 01',
'07 | 01 | 08 | 01 | 09 | 01 | 0A | 01 | 0B | 01 | 0C | 01 | 0D | 01 | 0E | 01',
'0F | 01 | 10 | 01 | 11 | 01 | 12 | 01 | 13 | 01 | 14 | 01 | 15 | 01 | 16 | 01',
'17 | 01 | 18 | 01 | 19 | 01 | 1A | 01 | 1B | 01 | 1C | 01 | 1D | 01 | 1E | 01',
'1F | 01 | 20 | 01 | 21 | 01 | 22 | 01 | 23 | 01 | 24 | 01 | 25 | 01 | 26 | 01',
'27 | 01 | 28 | 01 | 29 | 01 | 2A | 01 | 2B | 01 | 2C | 01 | 2D | 01 | 2E'
] Which cannot be the name table: that has to start with either |
Let's do some byte hunting... Skipping back 16 rows and highlighting the 3/1/1033 records: Working back through the 1/0/0 records: Working back through the header... storage offset (0xAE = 174): count (0x0E = 14): version (0x00 = 0): So we know the table starts 132 bytes earlier than its dictionary entry says it starts. Where does that 132 come from. |
It comes from this record:
loca is one of the three tables that may be additionally transformed, so looking at its
Further quoting:
|
Looking at our code for offset computation, we see: // parse the dictionary
this.directory = [...new Array(this.numTables)].map(
(_) => new Woff2TableDirectoryEntry(p)
);
let dictOffset = p.currentPosition; // = start of CompressedFontData block
// compute table byte offsets in the decompressed data
this.directory[0].offset = 0;
this.directory.forEach((e, i) => {
let next = this.directory[i + 1];
if (next) {
next.offset =
e.offset + (e.transformLength ? e.transformLength : e.origLength);
}
}); and there's our bug: The fix is (of course) trivial: this.directory.forEach((e, i) => {
let next = this.directory[i + 1];
if (next) {
next.offset =
e.offset + (e.transformLength !== undefined ? e.transformLength : e.origLength);
}
}); That is, we use |
With this update, running the following code: function testFont(font) {
const { directory, tables } = font.opentype;
console.log(directory);
const { name } = tables;
console.log(name);
} Yields: name {
format: 0,
count: 14,
stringOffset: 174,
nameRecords: [
NameRecord {
platformID: 1,
encodingID: 0,
languageID: 0,
nameID: 0,
length: 0,
offset: 2
},
NameRecord {
platformID: 1,
encodingID: 0,
languageID: 0,
nameID: 1,
length: 8,
offset: 21
},
NameRecord {
platformID: 1,
encodingID: 0,
languageID: 0,
nameID: 2,
length: 7,
offset: 46
},
NameRecord {
platformID: 1,
encodingID: 0,
languageID: 0,
nameID: 3,
length: 37,
offset: 130
},
NameRecord {
platformID: 1,
encodingID: 0,
languageID: 0,
nameID: 4,
length: 8,
offset: 186
},
NameRecord {
platformID: 1,
encodingID: 0,
languageID: 0,
nameID: 5,
length: 16,
offset: 229
},
NameRecord {
platformID: 1,
encodingID: 0,
languageID: 0,
nameID: 6,
length: 8,
offset: 264
},
NameRecord {
platformID: 3,
encodingID: 1,
languageID: 1033,
nameID: 0,
length: 0,
offset: 0
},
NameRecord {
platformID: 3,
encodingID: 1,
languageID: 1033,
nameID: 1,
length: 16,
offset: 3
},
NameRecord {
platformID: 3,
encodingID: 1,
languageID: 1033,
nameID: 2,
length: 14,
offset: 30
},
NameRecord {
platformID: 3,
encodingID: 1,
languageID: 1033,
nameID: 3,
length: 74,
offset: 54
},
NameRecord {
platformID: 3,
encodingID: 1,
languageID: 1033,
nameID: 4,
length: 16,
offset: 168
},
NameRecord {
platformID: 3,
encodingID: 1,
languageID: 1033,
nameID: 5,
length: 32,
offset: 195
},
NameRecord {
platformID: 3,
encodingID: 1,
languageID: 1033,
nameID: 6,
length: 16,
offset: 246
}
],
stringStart: 174
} |
Testing string extraction reveals one more parsing error: function testFont(font) {
const { directory, tables } = font.opentype;
console.log(directory);
const { name } = tables;
name.nameRecords.forEach((record) => {
try {
const str = record.string;
console.log(str);
} catch (e) {
console.error(e);
}
});
} Yields:
|
Looking at function decodeString(p, record) {
const { nameID, platformID, encodingID, length } = record;
console.log(nameID, platformID, encodingID, length);
// We decode strings for the Unicode/Microsoft platforms as UTF-16
if (platformID === 0 || platformID === 3) {
const str = [];
for (let i = 0, e = length / 2; i < e; i++)
str[i] = String.fromCharCode(p.uint16);
return str.join(``);
}
// Everything else, we treat as plain bytes.
const bytes = p.readBytes(length);
const str = [];
bytes.forEach(function (b, i) {
str[i] = String.fromCharCode(b);
});
return str.join(``);
// TODO: if someone wants to finesse this/implement all the other string encodings, have at it!
} Shows:
Looking at that first record's encoded values, we see a length of zero bytes, which means we should return an empty string immediately, instead of actually running the code we're currently running. Again, a trivial fix: function decodeString(p, record) {
const { platformID, length } = record;
if (length === 0) return ``;
...rest of function here...
} |
With these two fixes: function testFont(font) {
const { name } = font.opentype.tables;
name.nameRecords.forEach((record) => {
try {
const str = record.string;
console.log(str);
} catch (e) {
console.error(e);
}
});
} yields:
And finally we have a correct result. PR incoming. |
#121 filed (including tests) and merged in. |
v2.3.0 published to npm |
Font
https://www.panoramicinfotech.com/wp-content/themes/engitech/fonts/Flaticon.woff2
Problem
table entries seem to resolve incorrectly.
Code
Results
Directory:
Directory entry for
name
table:Error when destructuring the
name
table itself:The text was updated successfully, but these errors were encountered: