Skip to content

Implement plugins to support (subset) fonts in standalone CFF1 and Type1 format #704

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

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/plugins/pdfjs
5 changes: 5 additions & 0 deletions docs/font-inspector.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ <h1>Free Software</h1>

<script type="module">
import * as opentype from "/dist/opentype.module.js";
import cff1filePlugin from '/dist/plugins/opentypejs.plugin.cff1file.module.js';
import type1Plugin from '/dist/plugins/opentypejs.plugin.type1.module.js';

opentype.plugins.push(cff1filePlugin);
opentype.plugins.push(type1Plugin);

var font = null;
const fontSize = 32;
Expand Down
14 changes: 11 additions & 3 deletions docs/glyph-inspector.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ <h1>Free Software</h1>

<script type="module">
import * as opentype from "/dist/opentype.module.js";
import cff1filePlugin from '/dist/plugins/opentypejs.plugin.cff1file.module.js';
import type1Plugin from '/dist/plugins/opentypejs.plugin.type1.module.js';

opentype.plugins.push(cff1filePlugin);
opentype.plugins.push(type1Plugin);

window.opentype = opentype;

Expand Down Expand Up @@ -162,7 +167,8 @@ <h1>Free Software</h1>
}
html += '</dl>';
if (glyph.numberOfContours > 0) {
var contours = glyph.getContours(window.font.variation.getTransform(glyph, window.fontOptions.variation).points);
const points = window.font.variation &&window.font.variation.getTransform(glyph, window.fontOptions.variation).points || glyph.points;
var contours = glyph.getContours(points);
html += 'contours:<div id="glyph-contours">' + contours.map(contourToString).join('\n') + '</div>';
} else if (glyph.isComposite) {
html += '<br>This composite glyph is a combination of :<ul><li>' +
Expand Down Expand Up @@ -387,8 +393,8 @@ <h1>Free Software</h1>
hline('yMin', font.tables.head.yMin);
hline('Ascender', font.tables.hhea.ascender);
hline('Descender', font.tables.hhea.descender);
hline('Typo Ascender', font.tables.os2.sTypoAscender);
hline('Typo Descender', font.tables.os2.sTypoDescender);
hline('Typo Ascender', font.tables?.os2?.sTypoAscender);
hline('Typo Descender', font.tables?.os2?.sTypoDescender);
}

window.redraw = function(options = { withColors: true, withVariations: true }) {
Expand Down Expand Up @@ -581,6 +587,8 @@ <h1>Free Software</h1>
if ( fontData ) {
const arrayBuffer = base64ToArrayBuffer(fontData);
onFontLoaded(opentype.parse(arrayBuffer));
// redraw with correct widths
window.redraw();
} else {
const fontFileName = 'fonts/FiraSansMedium.woff';
display(await fetch(fontFileName), fontFileName);
Expand Down
5 changes: 5 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ <h1>Free Software</h1>

<script type="module">
import * as opentype from "/dist/opentype.module.js";
import cff1filePlugin from '/dist/plugins/opentypejs.plugin.cff1file.module.js';
import type1Plugin from '/dist/plugins/opentypejs.plugin.type1.module.js';

opentype.plugins.push(cff1filePlugin);
opentype.plugins.push(type1Plugin);

const form = document.forms.demo;
form.oninput = renderText;
Expand Down
20 changes: 13 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,22 @@
"browser": "./dist/opentype.js",
"module": "./dist/opentype.module.js",
"scripts": {
"build": "npm run b:umd && npm run b:esm",
"dist": " npm run d:umd && npm run d:esm",
"build": "npm run b:umd && npm run b:esm && npm run build:plugins",
"dist": " npm run d:umd && npm run d:esm && npm run dist:plugins",
"build:plugins": "npm run b:plugins:umd && npm run b:plugins:esm",
"dist:plugins": "npm run d:plugins:umd && npm run d:plugins:esm",
"test": "npm run build && npm run dist && mocha --require reify --recursive && npm run lint",
"lint": "eslint src",
"lint-fix": "eslint src --fix",
"start": "esbuild --bundle src/opentype.js --outdir=dist --external:fs --external:http --external:https --target=es2018 --format=esm --out-extension:.js=.module.js --global-name=opentype --define:DEBUG=false --footer:js=\"(function (root, factory) { if (typeof define === 'function' && define.amd)define(factory); else if (typeof module === 'object' && module.exports)module.exports = factory(); else root.opentype = factory(); }(typeof self !== 'undefined' ? self : this, () => ({...opentype,'default':opentype})));\" --watch --servedir=. --footer:js=\"new EventSource('/esbuild').addEventListener('change', () => location.reload())\"",
"b:umd": "esbuild --bundle src/opentype.js --outdir=dist --external:fs --external:http --external:https --target=es2018 --format=iife --out-extension:.js=.js --define:DEBUG=false --global-name=opentype --footer:js=\"(function (root, factory) { if (typeof define === 'function' && define.amd)define(factory); else if (typeof module === 'object' && module.exports)module.exports = factory(); else root.opentype = factory(); }(typeof self !== 'undefined' ? self : this, () => ({...opentype,'default':opentype})));\"",
"d:umd": "esbuild --bundle src/opentype.js --outdir=dist --external:fs --external:http --external:https --target=es2018 --format=iife --out-extension:.js=.min.js --define:DEBUG=false --global-name=opentype --footer:js=\"(function (root, factory) { if (typeof define === 'function' && define.amd)define(factory); else if (typeof module === 'object' && module.exports)module.exports = factory(); else root.opentype = factory(); }(typeof self !== 'undefined' ? self : this, () => ({...opentype,'default':opentype})));\" --minify --sourcemap",
"b:esm": "esbuild --bundle src/opentype.js --outdir=dist --external:fs --external:http --external:https --target=es2018 --format=esm --out-extension:.js=.module.js --define:DEBUG=false",
"d:esm": "esbuild --bundle src/opentype.js --outdir=dist --external:fs --external:http --external:https --target=es2018 --format=esm --out-extension:.js=.module.min.js --define:DEBUG=false --minify --sourcemap"
"start": "esbuild --bundle src/opentype.js --bundle src/plugins/*.js --outdir=dist --external:fs --external:http --external:https --target=esnext --format=esm --out-extension:.js=.module.js --global-name=opentype --define:DEBUG=false --footer:js=\"(function (root, factory) { if (typeof define === 'function' && define.amd)define(factory); else if (typeof module === 'object' && module.exports)module.exports = factory(); else root.opentype = factory(); }(typeof self !== 'undefined' ? self : this, () => ({...opentype,'default':opentype})));\" --watch --servedir=. --footer:js=\"new EventSource('/esbuild').addEventListener('change', () => location.reload())\"",
"b:umd": "esbuild --bundle src/opentype.js --outdir=dist --external:fs --external:http --external:https --target=es2018 --format=iife --out-extension:.js=.js --define:DEBUG=false --global-name=opentype --footer:js=\"(function (root, factory) { if (typeof define === 'function' && define.amd)define(factory); else if (typeof module === 'object' && module.exports)module.exports = factory(); else root.opentype = factory(); }(typeof self !== 'undefined' ? self : this, () => ({...opentype,'default':opentype})));\" --legal-comments=inline",
"d:umd": "esbuild --bundle src/opentype.js --outdir=dist --external:fs --external:http --external:https --target=es2018 --format=iife --out-extension:.js=.min.js --define:DEBUG=false --global-name=opentype --footer:js=\"(function (root, factory) { if (typeof define === 'function' && define.amd)define(factory); else if (typeof module === 'object' && module.exports)module.exports = factory(); else root.opentype = factory(); }(typeof self !== 'undefined' ? self : this, () => ({...opentype,'default':opentype})));\" --minify --sourcemap --legal-comments=inline",
"b:esm": "esbuild --bundle src/opentype.js --outdir=dist --external:fs --external:http --external:https --target=es2018 --format=esm --out-extension:.js=.module.js --define:DEBUG=false --legal-comments=inline",
"d:esm": "esbuild --bundle src/opentype.js --outdir=dist --external:fs --external:http --external:https --target=es2018 --format=esm --out-extension:.js=.module.min.js --define:DEBUG=false --minify --sourcemap --legal-comments=inline",
"b:plugins:umd": "esbuild --bundle src/plugins/*.js --outdir=dist/plugins --external:fs --external:http --external:https --target=es2018 --format=iife --out-extension:.js=.js --define:DEBUG=false --tree-shaking=true --legal-comments=inline",
"d:plugins:umd": "esbuild --bundle src/plugins/*.js --outdir=dist/plugins --external:fs --external:http --external:https --target=es2018 --format=iife --out-extension:.js=.min.js --define:DEBUG=false --minify --sourcemap --legal-comments=inline",
"b:plugins:esm": "esbuild --bundle src/plugins/*.js --outdir=dist/plugins --external:fs --external:http --external:https --target=es2018 --format=esm --out-extension:.js=.mjs --define:DEBUG=false --tree-shaking=true --legal-comments=inline",
"d:plugins:esm": "esbuild --bundle src/plugins/*.js --outdir=dist/plugins --external:fs --external:http --external:https --target=es2018 --format=esm --out-extension:.js=.min.mjs --define:DEBUG=false --minify --sourcemap --legal-comments=inline"
},
"devDependencies": {
"mocha": "^8.4.0",
Expand Down
2 changes: 1 addition & 1 deletion src/encoding.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ function addGlyphNamesAll(font) {
const c = charCodes[i];
const glyphIndex = glyphIndexMap[c];
glyph = font.glyphs.get(glyphIndex);
glyph.addUnicode(parseInt(c));
glyph && glyph.addUnicode(parseInt(c));
Copy link
Contributor

@yne yne May 8, 2024

Choose a reason for hiding this comment

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

you already used the optional chaining .? syntax early on,
so keep it unified (I have no preference, so it's up to you to chose one :) )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, did I use optional chaining? I thought that would throw an error with the current reify setup... I'll take a look at it!

Copy link
Contributor

Choose a reason for hiding this comment

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

}

for (let i = 0; i < font.glyphs.length; i += 1) {
Expand Down
4 changes: 2 additions & 2 deletions src/font.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import HintingTrueType from './hintingtt.js';
import Bidi from './bidi.js';
import { applyPaintType } from './tables/cff.js';

function createDefaultNamesInfo(options) {
export function createDefaultNamesInfo(options) {
return {
fontFamily: {en: options.familyName || ' '},
fontSubfamily: {en: options.styleName || ' '},
fullName: {en: options.fullName || options.familyName + ' ' + options.styleName},
// postScriptName may not contain any whitespace
postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).replace(/\s/g, '')},
postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).toString().replace(/\s/g, '')},
designer: {en: options.designer || ' '},
designerURL: {en: options.designerURL || ' '},
manufacturer: {en: options.manufacturer || ' '},
Expand Down
3 changes: 1 addition & 2 deletions src/glyphset.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ if(typeof Symbol !== 'undefined' && Symbol.iterator) {
*/
GlyphSet.prototype.get = function(index) {
// this.glyphs[index] is 'undefined' when low memory mode is on. glyph is pushed on request only.
if (this.glyphs[index] === undefined) {
if (index != null && this.glyphs[index] === undefined && index < (this.font.numGlyphs || this.font.nGlyphs)) {
this.font._push(index);
if (typeof this.glyphs[index] === 'function') {
this.glyphs[index] = this.glyphs[index]();
Expand Down Expand Up @@ -153,7 +153,6 @@ function ttfGlyphLoader(font, index, parseGlyph, data, position, buildPath) {
* @alias opentype.cffGlyphLoader
* @param {opentype.Font} font
* @param {number} index
* @param {Function} parseCFFCharstring
* @param {string} charstring
* @return {opentype.Glyph}
*/
Expand Down
76 changes: 63 additions & 13 deletions src/opentype.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// opentype.js
// https://github.com/opentypejs/opentype.js
// (c) 2015 Frederik De Bleser
// opentype.js may be freely distributed under the MIT license.
/*! opentype.js
* https://github.com/opentypejs/opentype.js
* (c) 2015-present Frederik De Bleser and contributors
* opentype.js may be freely distributed under the MIT license.
*/

import { tinf_uncompress as inflate } from './[email protected]'; // from code4fukui/tiny-inflate-es
import { isNode } from './util.js';
import Font from './font.js';
import Font, { createDefaultNamesInfo } from './font.js';
import Glyph from './glyph.js';
import glyphset from './glyphset.js';
import { CmapEncoding, GlyphNames, addGlyphNames } from './encoding.js';
import parse from './parse.js';
import BoundingBox from './bbox.js';
Expand Down Expand Up @@ -39,6 +41,9 @@ import meta from './tables/meta.js';
import gasp from './tables/gasp.js';
import svg from './tables/svg.js';
import { PaletteManager } from './palettes.js';
import { sizeOf } from './types.js';
import { plugins, applyPlugins } from './plugins.mjs';

/**
* The opentype library.
* @namespace opentype
Expand Down Expand Up @@ -261,6 +266,20 @@ function parseBuffer(buffer, opt={}) {
} else if (signature === 'wOF2') {
var issue = 'https://github.com/opentypejs/opentype.js/issues/183#issuecomment-1147228025';
throw new Error('WOFF2 require an external decompressor library, see examples at: ' + issue);
} else if(
applyPlugins(
'parseBuffer_signature',
{ font, opt, cff, CmapEncoding, Glyph, GlyphNames, glyphset, signature, data, createDefaultNamesInfo, parse, sizeOf, tableEntries }
)
) {
numTables = tableEntries.length;
} else if (
signature.substring(0,2) === '%!' ||
(parse.getByte(data, 0) === 0x80 && parse.getByte(data, 1) === 0x01)
) {
throw new Error('PostScript/PS1/T1/Adobe Type 1 fonts are not supported directly, but you can use the plugin "opentypejs.plugin.type1"');
} else if (data.buffer.byteLength > (3 * sizeOf.Card8() + sizeOf.OffSize()) && parse.getByte(data, 0) === 0x01) {
throw new Error('Standalone CFF1 files are not supported directly, but you can use the plugin "opentypejs.plugin.cff1file"');
} else {
throw new Error('Unsupported OpenType signature ' + signature);
}
Expand Down Expand Up @@ -412,9 +431,19 @@ function parseBuffer(buffer, opt={}) {
}
}

const nameTable = uncompressTable(data, nameTableEntry);
font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable);
font.names = font.tables.name;
applyPlugins('parseBuffer_processed', {opentype: this, font, opt, data});

if (nameTableEntry) {
const nameTable = uncompressTable(data, nameTableEntry);
font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable);
font.names = font.tables.name;
} else if(!font.names) {
console.error('Font is missing the required table "name"');
font.names = {};
font.names.unicode = createDefaultNamesInfo({});
font.names.macintosh = createDefaultNamesInfo({});
font.names.windows = createDefaultNamesInfo({});
}

if (glyfTableEntry && locaTableEntry) {
const shortVersion = indexToLocFormat === 0;
Expand All @@ -428,13 +457,27 @@ function parseBuffer(buffer, opt={}) {
} else if (cff2TableEntry) {
const cffTable2 = uncompressTable(data, cff2TableEntry);
cff.parse(cffTable2.data, cffTable2.offset, font, opt);
} else {
} else if (!font.nGLyphs && !font.numGlyphs) {
throw new Error('Font doesn\'t contain TrueType, CFF or CFF2 outlines.');
}

const hmtxTable = uncompressTable(data, hmtxTableEntry);
hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt);
addGlyphNames(font, opt);

if(hmtxTableEntry) {
const hmtxTable = uncompressTable(data, hmtxTableEntry);
hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt);
} else if(!font._hmtxTableData) {
console.error('Font is missing the required table "hmtx"');
}

applyPlugins('parseBuffer_before_addGlyphNames', { opentype: this, font, opt, data, createDefaultNamesInfo});

if(font.tables.cmap) {
addGlyphNames(font, opt);
} else {
font._IndexToUnicodeMap = font._IndexToUnicodeMap || {};
font.glyphNames = font.glyphNames || new GlyphNames({});
console.warn('This font has no "cmap" table');
}

if (kernTableEntry) {
const kernTable = uncompressTable(data, kernTableEntry);
Expand Down Expand Up @@ -523,6 +566,8 @@ function parseBuffer(buffer, opt={}) {

font.palettes = new PaletteManager(font);

applyPlugins('parseBuffer_parsed', { opentype: this, font, opt, data, tableEntries});

return font;
}

Expand Down Expand Up @@ -580,13 +625,18 @@ function loadSync(url, opt) {
return parseBuffer(require('fs').readFileSync(url), opt);
}

const { GlyphSet } = glyphset;

export {
Font,
GlyphSet,
Glyph,
GlyphNames,
Path,
BoundingBox,
parse as _parse,
parseBuffer as parse,
load,
loadSync
loadSync,
plugins
};
16 changes: 15 additions & 1 deletion src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -1079,4 +1079,18 @@ export default {
Parser,
};

export { Parser };
export {
getByte,
getByte as getCard8,
getUShort,
getUShort as getCard16,
getShort,
getUInt24,
getULong,
getFixed,
getTag,
getOffset,
getBytes,
bytesToString,
Parser
};
33 changes: 33 additions & 0 deletions src/plugins.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export const plugins = [];
Copy link
Contributor

@yne yne May 8, 2024

Choose a reason for hiding this comment

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

would it be cleaner to abstract/hide this plugins array with a registerPlugins
so we could do all the registration check (typeof == function etc...) at registration time

(just a suggestion)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about that as well, but then we'd also need methods to unregister a plugin and change the execution order in case two plugins modify the same entry point data. I think everyone should be familiar with modifying an array, so we should be fine leaving this without abstraction.


/**
* Plugins are registered by pushing them to the opentype.plugins array.
* They need to export any supported entry point names as functions, which will be passed a returnData object
* and different params depending on the entry point, which should be documented.
*
* The returnData object is shared by all plugins handling the same entry point and can be used to return
* data which can be handled by the code following an entry point. Data can also be mutated on any references
* to arrays or objects passed via the params.
* What returnData is expected/supported by each entry point should be documented as well.
* An entry point function returning a falsy value for returnData signals that it has not handled that entry point.
*/

/**
* checks if any registered plugin covers the given entry point and data
* @param {string} entryPoint - name of an entry point from where the plugin is called
* @param {Object} params - object of parameters to pass on to the plugin entry point
* @returns {Object|boolean} returnData object if the entry point was handled successfully by at least one plugin, false if not
*/
export function applyPlugins(entryPoint, params) {
let handled = false;
let returnData = {};
for(const plugin of plugins) {
if(typeof plugin[entryPoint] !== 'function') continue;
const pluginReturnData = plugin[entryPoint](returnData, params);
if(typeof pluginReturnData === 'object') {
returnData = Object.assign({}, returnData, pluginReturnData);
}
if(!!pluginReturnData) handled = true;
}
return handled ? returnData : false;
}
Loading
Loading