Skip to content
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

fixed the woff2 parsing? #116

Merged
merged 4 commits into from
May 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Pomax marked this conversation as resolved.
Show resolved Hide resolved
},
"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);
Comment on lines -18 to +19
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how the hell did this work before? O_o

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

¯_(ツ)_/¯


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);
});
}
Comment on lines +7 to +11
Copy link
Owner Author

@Pomax Pomax Apr 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a dynamic import, but for a native module, so while in theory this might kick in "after everything else": in reality it gets executed immediately.


/**
* 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}`
);
});
});
});
});
}