Skip to content

Commit

Permalink
fixed the woff2 parsing? (#116)
Browse files Browse the repository at this point in the history
* fixed the woff2 parsing
  • Loading branch information
Pomax authored May 20, 2021
1 parent 0980351 commit 0481241
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 41 deletions.
6 changes: 3 additions & 3 deletions lib-font.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@ class Font extends EventManager {
async parseBasicData(type) {
return loadTableClasses().then(createTable => {
if (type === `SFNT`) {
this.opentype = new SFNT(this.fontData, createTable);
this.opentype = new SFNT(this, this.fontData, createTable);
}
if (type === `WOFF`) {
this.opentype = new WOFF(this.fontData, createTable);
this.opentype = new WOFF(this, this.fontData, createTable);
}
if (type === `WOFF2`) {
this.opentype = new WOFF2(this.fontData, createTable);
this.opentype = new WOFF2(this, this.fontData, createTable);
}
return this.opentype;
});
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"test:browser": "run-p test:server test:puppeteer",
"test:server": "node ./testing/browser/server.js",
"test:puppeteer": "node ./testing/browser/puppeteer.js",
"test:jest": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --verbose=false ./testing/node/"
"test:jest": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --verbose=false ./testing/node/",
"test:manual": "http-server -o testing/manual/index.html"
},
"repository": {
"type": "git",
Expand All @@ -39,6 +40,7 @@
"devDependencies": {
"cross-env": "^7.0.2",
"express": "^4.17.1",
"http-server": "^0.12.3",
"jest": "^26.6.3",
"npm-run-all": "^4.1.5",
"open-cli": "^6.0.1",
Expand Down
2 changes: 1 addition & 1 deletion src/opentype/sfnt.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import lazy from "../lazy.js";
* See https://docs.microsoft.com/en-us/typography/opentype/spec/overview for more information
*/
class SFNT extends SimpleTable {
constructor(dataview, createTable) {
constructor(font, dataview, createTable) {
const { p } = super({ offset: 0, length: 12 }, dataview, `sfnt`);

this.version = p.uint32;
Expand Down
2 changes: 1 addition & 1 deletion src/opentype/tables/advanced/GPOS.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CommonLayoutTable } from "../common-layout-table.js";

class GPOS extends CommonLayoutTable {
constructor(dict, dataview) {
super(dict, dataview);
super(dict, dataview, `GPOS`);
}
getLookup(lookupIndex) {
return super.getLookup(lookupIndex, `GPOS`);
Expand Down
2 changes: 1 addition & 1 deletion src/opentype/tables/advanced/GSUB.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CommonLayoutTable } from "../common-layout-table.js";

class GSUB extends CommonLayoutTable {
constructor(dict, dataview) {
super(dict, dataview);
super(dict, dataview, `GSUB`);
}

getLookup(lookupIndex) {
Expand Down
4 changes: 2 additions & 2 deletions src/opentype/tables/common-layout-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import lazy from "../../lazy.js";
* See https://docs.microsoft.com/en-us/typography/opentype/spec/GPOS
*/
class CommonLayoutTable extends SimpleTable {
constructor(name, dict, dataview) {
const { p, tableStart } = super(name, dict, dataview);
constructor(dict, dataview, name) {
const { p, tableStart } = super(dict, dataview, name);

this.majorVersion = p.uint16;
this.minorVersion = p.uint16;
Expand Down
2 changes: 1 addition & 1 deletion src/opentype/woff.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const gzipDecode = globalThis.pako ? globalThis.pako.inflate : undefined;
* See https://docs.microsoft.com/en-us/typography/opentype/spec/overview for font information
*/
class WOFF extends SimpleTable {
constructor(dataview, createTable) {
constructor(font, dataview, createTable) {
const { p } = super({ offset: 0, length: 44 }, dataview, `woff`);

this.signature = p.tag;
Expand Down
84 changes: 53 additions & 31 deletions src/opentype/woff2.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import { SimpleTable } from "./tables/simple-table.js";
import lazy from "../lazy.js";

const brotliDecode = globalThis.unbrotli;
let nativeBrotliDecode = undefined;

if (!brotliDecode) {
import("zlib").then((zlib) => {
nativeBrotliDecode = (buffer) => zlib.brotliDecompressSync(buffer);
});
}

/**
* The WOFF2 header
Expand All @@ -10,7 +17,7 @@ const brotliDecode = globalThis.unbrotli;
* See https://docs.microsoft.com/en-us/typography/opentype/spec/overview for font information
*/
class WOFF2 extends SimpleTable {
constructor(dataview, createTable) {
constructor(font, dataview, createTable) {
const { p } = super({ offset: 0, length: 48 }, dataview, `woff2`);
this.signature = p.tag;
this.flavor = p.uint32;
Expand All @@ -33,24 +40,31 @@ class WOFF2 extends SimpleTable {
this.directory = [...new Array(this.numTables)].map(
(_) => new Woff2TableDirectoryEntry(p)
);
let dictOffset = p.currentPosition;
let dictOffset = p.currentPosition; // = start of CompressedFontData block

// compute table byte offsets in the decompressed data
this.directory[0].origOffset = 0;
this.directory[0].offset = 0;
this.directory.forEach((e, i) => {
let t = this.directory[i + 1];
if (t) {
const useTransform = typeof e.transformLength !== "undefined";
t.origOffset =
e.origOffset + (useTransform ? e.transformLength : e.origLength);
let next = this.directory[i + 1];
if (next) {
next.offset =
e.offset + (e.transformLength ? e.transformLength : e.origLength);
}
});

// then decompress the original data and lazy-bind
let decoded = brotliDecode(
new Uint8Array(dataview.buffer.slice(dictOffset))
);
buildWoff2LazyLookups(this, decoded, createTable);
let buffer = dataview.buffer.slice(dictOffset);

if (brotliDecode) {
const decoded = brotliDecode(new Uint8Array(buffer));
buildWoff2LazyLookups(this, decoded, createTable);
} else if (nativeBrotliDecode) {
const decoded = new Uint8Array(nativeBrotliDecode(buffer));
buildWoff2LazyLookups(this, decoded, createTable);
} else {
const msg = `no brotli decoder available to decode WOFF2 font`;
if (font.onerror) font.onerror(msg);
}
}
}

Expand All @@ -68,38 +82,46 @@ class Woff2TableDirectoryEntry {
this.tag = getWOFF2Tag(tagNumber);
}

/*
"Bits 6 and 7 indicate the preprocessing transformation version number (0-3)
that was applied to each table. For all tables in a font, except for 'glyf'
and 'loca' tables, transformation version 0 indicates the null transform
where the original table data is passed directly to the Brotli compressor
for inclusion in the compressed data stream. For 'glyf' and 'loca' tables,
transformation version 3 indicates the null transform"
*/
const transformVersion = (this.transformVersion = (this.flags & 192) >> 6);
let hasTransforms = transformVersion !== 0;
if (this.tag === `glyf` || this.tag === `loca`) {
hasTransforms = this.transformVersion !== 3;
}

this.origLength = p.uint128;
const pptVersion = (this.pptVersion = this.flags >> 6);
if (
pptVersion !== 0 ||
((this.tag === "glyf" || this.tag === "loca") && pptVersion !== 3)
) {
if (hasTransforms) {
this.transformLength = p.uint128;
}
this.length = p.offset; // FIXME: we can probably calculat this without asking the parser
}
}

/**
* Build late-evaluating properties for each table in a
* woff/woff2 font, so that accessing a table via the
* woff.tables.tableName or woff2.tables.tableName
* property kicks off a table parse on first access.
* woff2 font, so that accessing a table via the
* font.opentype.tables.tableName property kicks off
* a table parse on first access.
*
* @param {*} woff the woff or woff2 font object
* @param {DataView} dataview passed when dealing with woff
* @param {buffer} decoded passed when dealing with woff2
* @param {*} woff2 the woff2 font object
* @param {decoded} the original (decompressed) SFNT data
* @param {createTable} the opentype table builder function
*/
function buildWoff2LazyLookups(woff2, decoded, createTable) {
woff2.tables = {};
woff2.directory.forEach((entry) => {
lazy(woff2.tables, entry.tag.trim(), () => {
const useTransform = typeof entry.transformLength !== "undefined";
const data = decoded.slice(
entry.origOffset,
entry.origOffset +
(useTransform ? entry.transformLength : entry.origLength)
);
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 },
Expand Down Expand Up @@ -179,7 +201,7 @@ function getWOFF2Tag(flag) {
`Gloc`,
`Feat`,
`Sill`,
][flag];
][flag & 63];
}

export { WOFF2 };
14 changes: 14 additions & 0 deletions testing/manual/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>LibFont unit tests</title>
<script src="/lib/inflate.js" defer></script>
<script src="/lib/unbrotli.js" defer></script>
<script src="/lib-font.js" type="module" defer></script>
<script src="./index.js" type="module" defer></script>
</head>
<body>
<h1>Please open dev tools for now</h1>
</body>
</html>
43 changes: 43 additions & 0 deletions testing/manual/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const font = new Font("woff2 testing");
font.onerror = (evt) => console.error(evt);
font.onload = (evt) => {
let font = evt.detail.font;

const { GSUB } = font.opentype.tables;
processGSUB(GSUB);
};

const fonts = [
`/fonts/broken/BrushPosterGrotesk.woff2`,
`/fonts/broken/135abd30-1390-4f9c-b6a2-d843157c3468.woff2`, // No GSUB table?
`/fonts/broken/64017d81-9430-4cba-8219-8f5cc28b923e.woff2`, // No GSUB table?
`/fonts/broken/proximanova-regular-webfont.woff2`,
];

font.src = fonts[0];

function processGSUB(GSUB) {
let scripts = GSUB.getSupportedScripts();

scripts.forEach((script) => {
let langsys = GSUB.getSupportedLangSys(script);

langsys.forEach((lang) => {
let langSysTable = GSUB.getLangSysTable(script, lang);
let features = GSUB.getFeatures(langSysTable);

features.forEach((feature) => {
const lookupIDs = feature.lookupListIndices;

lookupIDs.forEach((id) => {
const lookup = GSUB.getLookup(id);
const cnt = lookup.subTableCount;
const s = cnt !== 1 ? "s" : "";
console.log(
`lookup type ${lookup.lookupType} in ${lang}, lookup ${id}, ${cnt} subtable${s}`
);
});
});
});
});
}

0 comments on commit 0481241

Please sign in to comment.