diff --git a/docs/font-inspector.html b/docs/font-inspector.html
index 52e1ea14..109965b2 100644
--- a/docs/font-inspector.html
+++ b/docs/font-inspector.html
@@ -96,6 +96,17 @@
Free Software
var font = null;
const fontSize = 32;
const textToRender = 'Grumpy wizards make toxic brew for the evil Queen and Jack.';
+const drawOptions = {
+ kerning: true,
+ features: [
+ /**
+ * these 4 features are required to render Arabic text properly
+ * and shouldn't be turned off when rendering arabic text.
+ */
+ { script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] },
+ { script: 'latn', tags: ['liga', 'rlig'] }
+ ]
+};
function escapeHtml(unsafe) {
return unsafe
@@ -127,17 +138,7 @@ Free Software
var previewCanvas = document.getElementById('preview');
var previewCtx = previewCanvas.getContext("2d");
previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height);
- font.draw(previewCtx, textToRender, 0, 32, fontSize, {
- kerning: true,
- features: [
- /**
- * these 4 features are required to render Arabic text properly
- * and shouldn't be turned off when rendering arabic text.
- */
- { script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] },
- { script: 'latn', tags: ['liga', 'rlig'] }
- ]
- });
+ font.draw(previewCtx, textToRender, 0, 32, fontSize, drawOptions);
}
function showErrorMessage(message) {
@@ -234,9 +235,18 @@ Free Software
}
function onFontLoaded(font) {
+ if (window.font) {
+ window.font.onGlyphUpdated = null;
+ }
window.font = font;
renderText(font);
displayFontData(font);
+ font.onGlyphUpdated = (glyphId) => {
+ const glyphIds = font.stringToGlyphIndexes(textToRender, drawOptions);
+ if (glyphIds.includes(glyphId)) {
+ renderText(font);
+ }
+ };
}
var tableHeaders = document.getElementById('font-data').getElementsByTagName('h3');
diff --git a/docs/glyph-inspector.html b/docs/glyph-inspector.html
index 60732306..a6048fde 100644
--- a/docs/glyph-inspector.html
+++ b/docs/glyph-inspector.html
@@ -340,6 +340,9 @@ Free Software
}
function onFontLoaded(font) {
+ if (window.font) {
+ window.font.onGlyphUpdated = null
+ }
window.font = font;
var w = cellWidth - cellMarginLeftRight * 2,
@@ -371,6 +374,13 @@ Free Software
displayGlyphPage(0);
displayGlyph(-1);
displayGlyphData(-1);
+
+ font.onGlyphUpdated = (glyphId) => {
+ const firstGlyph = pageSelected * cellCount;
+ if (firstGlyph <= glyphId && glyphId < firstGlyph + cellCount) {
+ renderGlyphItem(document.getElementById('g'+(glyphId - firstGlyph)), glyphId);
+ }
+ };
}
function cellSelect(event) {
diff --git a/docs/index.html b/docs/index.html
index ed92e27e..6ad6f692 100755
--- a/docs/index.html
+++ b/docs/index.html
@@ -109,14 +109,7 @@ Free Software
if (!font) return;
const textToRender = form.textField.value;
var previewCtx = document.getElementById('preview').getContext('2d');
- var options = {
- kerning: form.kerning.checked,
- hinting: form.hinting.checked,
- features: {
- liga: form.ligatures.checked,
- rlig: form.ligatures.checked
- }
- };
+ var options = getDrawOptions();
previewCtx.clearRect(0, 0, 940, 300);
const fontSize = +form.fontsize.value;
font.draw(previewCtx, textToRender, 0, 200, fontSize, options);
@@ -134,6 +127,17 @@ Free Software
snapPath.draw(snapCtx);
}
+function getDrawOptions() {
+ return {
+ kerning: form.kerning.checked,
+ hinting: form.hinting.checked,
+ features: {
+ liga: form.ligatures.checked,
+ rlig: form.ligatures.checked
+ }
+ };
+}
+
function enableHighDPICanvas(canvas) {
if (typeof canvas === 'string') {
canvas = document.getElementById(canvas);
@@ -176,6 +180,9 @@ Free Software
}
function onFontLoaded(font) {
+ if (window.font) {
+ window.font.onGlyphUpdated = null
+ }
window.font = font;
// Show the first 100 glyphs.
@@ -186,9 +193,11 @@ Free Software
const x = 50;
const y = 120;
const fontSize = 72;
+ const ctxs = new Array(amount);
for (let i = 0; i < amount; i++) {
const glyph = font.glyphs.get(i);
const ctx = createGlyphCanvas(glyph, 150);
+ ctxs[i] = ctx;
glyph.draw(ctx, x, y, fontSize);
glyph.drawPoints(ctx, x, y, fontSize);
glyph.drawMetrics(ctx, x, y, fontSize);
@@ -206,6 +215,23 @@ Free Software
}
renderText();
+
+ font.onGlyphUpdated = (glyphId) => {
+ if (0 <= glyphId && glyphId < amount) {
+ const glyph = font.glyphs.get(glyphId);
+ const ctx = ctxs[glyphId];
+ glyph.draw(ctx, x, y, fontSize);
+ glyph.drawPoints(ctx, x, y, fontSize);
+ glyph.drawMetrics(ctx, x, y, fontSize);
+ }
+
+ const textToRender = form.textField.value;
+ const drawOptions = getDrawOptions();
+ const glyphIds = font.stringToGlyphIndexes(textToRender, drawOptions);
+ if (glyphIds.includes(glyphId)) {
+ renderText();
+ }
+ };
}
const loadScript = (src) => new Promise((onload) => document.head.append(Object.assign(document.createElement('script'), {src, onload})));
async function display(file, name) {
diff --git a/src/glyph.js b/src/glyph.js
index ff38f6b3..d16089ee 100644
--- a/src/glyph.js
+++ b/src/glyph.js
@@ -36,6 +36,7 @@ function getPathDefinition(glyph, path) {
* @property {number} [yMax]
* @property {number} [advanceWidth]
* @property {number} [leftSideBearing]
+ * @property {import('./tables/svg.js').SVGImage | Promise | Error} [svgImage]
*/
// A Glyph is an individual mark that often corresponds to a character.
@@ -147,6 +148,19 @@ Glyph.prototype.getPath = function(x, y, fontSize, options, font) {
let yScale = options.yScale;
const scale = 1 / (this.path.unitsPerEm || 1000) * fontSize;
+ const svgImage = this.svgImage;
+ if (svgImage && svgImage.image) {
+ const path = new Path();
+ path.drawImage(
+ svgImage.image,
+ x + svgImage.leftSideBearing * scale,
+ y - svgImage.baseline * scale,
+ svgImage.image.width * scale,
+ svgImage.image.height * scale,
+ );
+ return path;
+ }
+
if (options.hinting && font && font.hinting) {
// in case of hinting, the hinting engine takes care
// of scaling the points (not the path) before hinting.
diff --git a/src/glyphset.js b/src/glyphset.js
index f4acc393..02d9c095 100644
--- a/src/glyphset.js
+++ b/src/glyphset.js
@@ -1,6 +1,7 @@
// The GlyphSet object
import Glyph from './glyph.js';
+import svg from './tables/svg.js';
// Define a property on the glyph that depends on the path being loaded.
function defineDependentProperty(glyph, externalName, internalName) {
@@ -103,6 +104,39 @@ GlyphSet.prototype.push = function(index, loader) {
this.length++;
};
+function defineSvgImageLoader(glyph, font) {
+ glyph._svgImage = function () {
+ return font.tables.svg
+ ? svg.imageLoader(font.tables.svg, glyph.index)
+ : Promise.resolve();
+ };
+
+ Object.defineProperty(glyph, 'svgImage', {
+ get: function() {
+ if ( typeof glyph._svgImage === 'function' ) {
+ glyph._svgImage = glyph._svgImage()
+ .then(svgImage => {
+ glyph._svgImage = svgImage;
+ if (svgImage && font.onGlyphUpdated) {
+ font.onGlyphUpdated(glyph.index);
+ }
+ return svgImage;
+ })
+ .catch(error => {
+ glyph._svgImage = error;
+ throw error;
+ });
+ }
+ return glyph._svgImage;
+ },
+ set: function(newValue) {
+ glyph._svgImage = newValue;
+ },
+ enumerable: true,
+ configurable: true
+ });
+}
+
/**
* @alias opentype.glyphLoader
* @param {opentype.Font} font
@@ -142,6 +176,8 @@ function ttfGlyphLoader(font, index, parseGlyph, data, position, buildPath) {
defineDependentProperty(glyph, 'yMin', '_yMin');
defineDependentProperty(glyph, 'yMax', '_yMax');
+ defineSvgImageLoader(glyph, font);
+
return glyph;
};
}
@@ -163,6 +199,8 @@ function cffGlyphLoader(font, index, parseCFFCharstring, charstring, version) {
return path;
};
+ defineSvgImageLoader(glyph, font);
+
return glyph;
};
}
diff --git a/src/opentype.js b/src/opentype.js
index 0fd00854..4c3cade2 100644
--- a/src/opentype.js
+++ b/src/opentype.js
@@ -35,6 +35,7 @@ import os2 from './tables/os2.js';
import post from './tables/post.js';
import meta from './tables/meta.js';
import gasp from './tables/gasp.js';
+import svg from './tables/svg.js';
/**
* The opentype library.
* @namespace opentype
@@ -390,6 +391,10 @@ function parseBuffer(buffer, opt={}) {
table = uncompressTable(data, tableEntry);
font.tables.gasp = gasp.parse(table.data, table.offset);
break;
+ case 'SVG ':
+ table = uncompressTable(data, tableEntry);
+ font.tables.svg = svg.parse(table.data, table.offset);
+ break;
}
}
diff --git a/src/path.js b/src/path.js
index 02f9bdff..e12167f8 100644
--- a/src/path.js
+++ b/src/path.js
@@ -413,6 +413,21 @@ Path.prototype.quadTo = Path.prototype.quadraticCurveTo = function(x1, y1, x, y)
});
};
+/**
+ * @function drawImage
+ * @memberof opentype.Path.prototype
+ */
+Path.prototype.drawImage = function(image, x, y, width, height) {
+ this.commands.push({
+ type: 'I',
+ image,
+ x,
+ y,
+ width,
+ height
+ });
+};
+
/**
* Closes the path
* @function closePath
@@ -488,6 +503,14 @@ Path.prototype.getBoundingBox = function() {
prevX = startX;
prevY = startY;
break;
+ case 'I':
+ box.addPoint(cmd.x, cmd.y);
+ box.addPoint(cmd.x + cmd.width, cmd.y);
+ box.addPoint(cmd.x + cmd.width, cmd.y + cmd.height);
+ box.addPoint(cmd.x, cmd.y + cmd.height);
+ startX = prevX = cmd.x;
+ startY = prevY = cmd.y;
+ break;
default:
throw new Error('Unexpected path command ' + cmd.type);
}
@@ -516,6 +539,8 @@ Path.prototype.draw = function(ctx) {
ctx.quadraticCurveTo(cmd.x1, cmd.y1, cmd.x, cmd.y);
} else if (cmd.type === 'Z') {
ctx.closePath();
+ } else if (cmd.type === 'I') {
+ ctx.drawImage(cmd.image, cmd.x, cmd.y, cmd.width, cmd.height);
}
}
diff --git a/src/tables/sfnt.js b/src/tables/sfnt.js
index 8abdb844..2a10fc69 100644
--- a/src/tables/sfnt.js
+++ b/src/tables/sfnt.js
@@ -25,6 +25,7 @@ import fvar from './fvar.js';
import stat from './stat.js';
import avar from './avar.js';
import gasp from './gasp.js';
+import svg from './svg.js';
function log2(v) {
return Math.log(v) / Math.log(2) | 0;
@@ -361,7 +362,8 @@ function fontToSfntTable(font) {
cpal,
colr,
stat,
- avar
+ avar,
+ svg,
};
const optionalTableArgs = {
diff --git a/src/tables/svg.js b/src/tables/svg.js
new file mode 100644
index 00000000..33341dfd
--- /dev/null
+++ b/src/tables/svg.js
@@ -0,0 +1,254 @@
+import { Parser } from '../parse.js';
+import table from '../table.js';
+import { isGzip, unGzip } from '../util.js';
+
+// https://learn.microsoft.com/en-us/typography/opentype/spec/svg
+
+/**
+ * @typedef {Map} SVGTable
+ */
+
+/**
+ * @param {DataView} data
+ * @param {number} offset
+ * @returns {SVGTable}
+ */
+function parseSvgTable(data, offset) {
+ const svgTable = new Map();
+ const buf = data.buffer;
+ const p = new Parser(data, offset);
+ const version = p.parseUShort();
+ if (version !== 0) return svgTable;
+
+ p.relativeOffset = p.parseOffset32();
+ const svgDocumentListOffset = data.byteOffset + offset + p.relativeOffset;
+ const numEntries = p.parseUShort();
+ const svgDocMap = new Map();
+ for (let i = 0; i < numEntries; i++) {
+ const startGlyphID = p.parseUShort();
+ const endGlyphID = p.parseUShort();
+ const svgDocOffset = svgDocumentListOffset + p.parseOffset32();
+ const svgDocLength = p.parseULong();
+ let svgDoc = svgDocMap.get(svgDocOffset);
+ if (svgDoc === undefined) {
+ svgDoc = new Uint8Array(buf, svgDocOffset, svgDocLength);
+ svgDocMap.set(svgDocOffset, svgDoc);
+ }
+ for (let i = startGlyphID; i <= endGlyphID; i++) {
+ svgTable.set(i, svgDoc);
+ }
+ }
+ return svgTable;
+}
+
+/**
+ * @param {SVGTable} svgTable
+ * @returns {opentype.Table}
+ */
+function makeSvgTable(svgTable) {
+ const glyphIds = Array.from(svgTable.keys()).sort();
+ const documentRecords = [];
+ const documentBuffers = [];
+ const documentOffsets = new Map();
+ let nextSvgDocOffset = 0;
+ let record = { endGlyphID: null };
+ for (let i = 0, l = glyphIds.length; i < l; i++) {
+ const glyphId = glyphIds[i];
+ const svgDoc = svgTable.get(glyphId);
+ let svgDocOffset = documentOffsets.get(svgDoc);
+ if (svgDocOffset === undefined) {
+ svgDocOffset = nextSvgDocOffset;
+ documentBuffers.push(svgDoc);
+ documentOffsets.set(svgDoc, svgDocOffset);
+ nextSvgDocOffset += svgDoc.byteLength;
+ }
+ if (glyphId - 1 === record.endGlyphID && svgDocOffset === record.svgDocOffset) {
+ record.endGlyphID = glyphId;
+ } else {
+ record = {
+ startGlyphID: glyphId,
+ endGlyphID: glyphId,
+ svgDocOffset,
+ svgDocLength: svgDoc.byteLength,
+ };
+ documentRecords.push(record);
+ }
+ }
+
+ const numEntries = documentRecords.length;
+ const numDocuments = documentBuffers.length;
+ const documentsOffset = 2 + numEntries * (2 + 2 + 4 + 4);
+ const fields = new Array(3 + 1 + numEntries * 4 + numDocuments);
+ let fieldIndex = 0;
+
+ // SVG Table Header
+ fields[fieldIndex++] = { name: 'version', type: 'USHORT', value: 0 };
+ fields[fieldIndex++] = { name: 'svgDocumentListOffset', type: 'ULONG', value: 2 + 4 + 4 };
+ fields[fieldIndex++] = { name: 'reserved', type: 'ULONG', value: 0 };
+
+ // SVG Document List
+ fields[fieldIndex++] = { name: 'numEntries', type: 'USHORT', value: numEntries };
+ for (let i = 0; i < numEntries; i++) {
+ const namePrefix = 'documentRecord_' + i;
+ const { startGlyphID, endGlyphID, svgDocOffset, svgDocLength } = documentRecords[i];
+ fields[fieldIndex++] = { name: namePrefix + '_startGlyphID', type: 'USHORT', value: startGlyphID };
+ fields[fieldIndex++] = { name: namePrefix + '_endGlyphID', type: 'USHORT', value: endGlyphID };
+ fields[fieldIndex++] = { name: namePrefix + '_svgDocOffset', type: 'ULONG', value: documentsOffset + svgDocOffset };
+ fields[fieldIndex++] = { name: namePrefix + '_svgDocLength', type: 'ULONG', value: svgDocLength };
+ }
+
+ // SVG Documents
+ for (let i = 0; i < numDocuments; i++) {
+ fields[fieldIndex++] = { name: 'svgDoc_' + i, type: 'LITERAL', value: documentBuffers[i] };
+ }
+
+ return new table.Table('SVG ', fields);
+}
+
+/**
+ * @param {Uint8Array} buf
+ * @returns {Promise}
+ */
+function decodeSvgDocumentWithTinyInflate(buf) {
+ try {
+ return Promise.resolve(new TextDecoder().decode(isGzip(buf) ? unGzip(buf) : buf));
+ } catch (error) {
+ return Promise.reject(error);
+ }
+}
+/**
+ * @param {Uint8Array} buf
+ * @returns {Promise}
+ */
+function decodeSvgDocumentWithDecompressionStream(buf) {
+ if (isGzip(buf)) {
+ return new Response(new Response(buf).body.pipeThrough(new DecompressionStream('gzip'))).text();
+ }
+ try {
+ return Promise.resolve(new TextDecoder().decode(buf));
+ } catch (error) {
+ return Promise.reject(error);
+ }
+}
+/**
+ * @param {Uint8Array} buf
+ * @returns {Promise}
+ */
+const decodeSvgDocument = globalThis.DecompressionStream
+ ? decodeSvgDocumentWithDecompressionStream
+ : decodeSvgDocumentWithTinyInflate;
+
+/**
+ * @typedef {string | [string, string, string, string, string, string, string]} SVGTemplate
+ */
+
+/**
+ * @typedef {Object} SVGImage
+ * @prop {number} leftSideBearing
+ * @prop {number} baseline
+ * @prop {HTMLImageElement} image
+ */
+
+/** @type {WeakMap>} */
+const svgTemplateCache = new WeakMap();
+
+/**
+ * @param {SVGTable} svgTable
+ * @param {number} glyphId
+ * @returns {Promise | undefined}
+ */
+function svgImageLoader(svgTable, glyphId) {
+ const svgBuf = svgTable.get(glyphId);
+ if (svgBuf === undefined) return Promise.resolve();
+ let svgTemplatePromise = svgTemplateCache.get(svgBuf);
+ if (svgTemplatePromise === undefined) {
+ svgTemplatePromise = decodeSvgDocument(svgBuf).then(makeSvgTemplate);
+ svgTemplateCache.set(svgBuf, svgTemplatePromise);
+ }
+ return svgTemplatePromise.then((svgTemplate) => {
+ let svgText;
+ if (typeof svgTemplate === 'string') {
+ svgText = svgTemplate;
+ } else {
+ svgTemplate[4] = glyphId;
+ svgText = svgTemplate.join('');
+ }
+ const svgImage = makeSvgImage(svgText);
+ return svgImage.image.decode().then(() => svgImage);
+ });
+}
+
+/**
+ * https://learn.microsoft.com/en-us/typography/opentype/spec/svg#glyph-identifiers
+ * @param {string} text
+ * @returns {SVGTemplate}
+ */
+function makeSvgTemplate(text) {
+ const documentStart = text.indexOf('');
+ return [
+ text.substring(0, contentStart),
+ '',
+ text.substring(contentStart, contentEnd),
+ '',
+ text.substring(contentEnd),
+ ];
+}
+
+/**
+ * @param {string} text
+ * @returns {SVGImage}
+ */
+function makeSvgImage(text) {
+ const svgDocument = new DOMParser().parseFromString(text, 'image/svg+xml');
+ /** @type {SVGSVGElement} */
+ const svg = svgDocument.documentElement;
+ const viewBoxVal = svg.viewBox.baseVal;
+ const widthVal = svg.width.baseVal;
+ const heightVal = svg.height.baseVal;
+ let xScale = 1;
+ let yScale = 1;
+ if (viewBoxVal.width > 0 && viewBoxVal.height > 0) {
+ if (widthVal.unitType === 1) {
+ xScale = widthVal.valueInSpecifiedUnits / viewBoxVal.width;
+ yScale = heightVal.unitType === 1 ? heightVal.valueInSpecifiedUnits / viewBoxVal.height : xScale;
+ } else if (heightVal.unitType === 1) {
+ yScale = heightVal.valueInSpecifiedUnits / viewBoxVal.height;
+ xScale = yScale;
+ }
+ }
+
+ const div = document.createElement('div');
+ div.style.position = 'fixed';
+ div.style.visibility = 'hidden';
+ div.appendChild(svg);
+ document.body.appendChild(div);
+ const bbox = svg.getBBox();
+ document.body.removeChild(div);
+
+ const leftSideBearing = (bbox.x - viewBoxVal.x) * xScale;
+ const baseline = (viewBoxVal.y - bbox.y) * yScale;
+ const width = bbox.width * xScale;
+ const height = bbox.height * yScale;
+
+ svg.setAttribute('viewBox', [bbox.x, bbox.y, bbox.width, bbox.height].join(' '));
+ if (xScale !== 1) svg.setAttribute('width', width);
+ if (yScale !== 1) svg.setAttribute('height', height);
+
+ const image = new Image(width, height);
+ image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg.outerHTML);
+ return { leftSideBearing, baseline, image };
+}
+
+export default {
+ make: makeSvgTable,
+ parse: parseSvgTable,
+ decodeDocument: decodeSvgDocument,
+ imageLoader: svgImageLoader,
+};
diff --git a/src/util.js b/src/util.js
index 0757513f..ae5666c3 100644
--- a/src/util.js
+++ b/src/util.js
@@ -1,3 +1,5 @@
+import { tinf_uncompress as inflate } from './tiny-inflate@1.0.3.esm.js';
+
function isBrowser() {
return (
typeof window !== 'undefined' ||
@@ -39,4 +41,48 @@ function objectsEqual(obj1, obj2) {
return arraysEqual(val1, val2) && arraysEqual(keys1, keys2);
}
-export { isBrowser, isNode, checkArgument, arraysEqual, objectsEqual };
+/**
+ * [GZIP File Format Specification](https://datatracker.ietf.org/doc/html/rfc1952#section-2.3)
+ * @param {Uint8Array} buf
+ * @returns {boolean}
+ */
+function isGzip(buf) {
+ return buf[0] === 31 && buf[1] === 139 && buf[2] === 8;
+}
+
+/**
+ * [GZIP File Format Specification](https://datatracker.ietf.org/doc/html/rfc1952#section-2.3)
+ * @param {Uint8Array} gzip
+ * @returns {Uint8Array}
+ */
+function unGzip(gzip) {
+ const data = new DataView(gzip.buffer, gzip.byteOffset, gzip.byteLength);
+
+ let start = 10;
+ const end = gzip.byteLength - 8;
+ const flg = data.getInt8(3);
+ // FEXTRA
+ if (flg & 0b00000100) {
+ start += 2 + data.getUint16(start, true);
+ }
+ // FNAME
+ if (flg & 0b00001000) {
+ while (start < end) if (gzip[start++] === 0) break;
+ }
+ // FCOMMENT
+ if (flg & 0b00010000) {
+ while (start < end) if (gzip[start++] === 0) break;
+ }
+ // FHCRC
+ if (flg & 0b00000010) {
+ start += 2;
+ }
+
+ if (start >= end) throw new Error('Can\'t find compressed blocks');
+
+ const isize = data.getUint32(data.byteLength - 4, true);
+
+ return inflate(gzip.subarray(start, end), new Uint8Array(isize));
+}
+
+export { isBrowser, isNode, checkArgument, arraysEqual, objectsEqual, isGzip, unGzip };
diff --git a/test/fonts/Colortube-lWAd.otf b/test/fonts/Colortube-lWAd.otf
new file mode 100644
index 00000000..175fc646
Binary files /dev/null and b/test/fonts/Colortube-lWAd.otf differ
diff --git a/test/svg.js b/test/svg.js
new file mode 100644
index 00000000..0462796e
--- /dev/null
+++ b/test/svg.js
@@ -0,0 +1,52 @@
+import assert from 'assert';
+import { loadSync } from '../src/opentype.js';
+import svg from '../src/tables/svg.js';
+
+/** @typedef {import('../src/tables/svg.js').SVGTable} SVGTable */
+
+describe('tables/svg.js', () => {
+ /**
+ * https://www.fontspace.com/colortube-font-f28146
+ * @type {SVGTable}
+ */
+ const svgTable = loadSync('./test/fonts/Colortube-lWAd.otf').tables.svg;
+ const glyphIds = Array.from(svgTable.keys()).sort();
+
+ /**
+ * @param {SVGTable} svgTable
+ * @param {number} recordIndex
+ */
+ async function checkDocument(svgTable, glyphId) {
+ const svgDocBytes = svgTable.get(glyphId);
+ const svgDocText = await svg.decodeDocument(svgDocBytes);
+ assert(svgDocText.startsWith('