From d3d30781704fb810bfc101b81adec60bf737713e Mon Sep 17 00:00:00 2001 From: Sergei Nikolaev Date: Fri, 12 Apr 2024 19:27:29 +0400 Subject: [PATCH] feat: add SVG table support (#672) add SVG table parsing, writing and drawing --- docs/font-inspector.html | 34 ++-- docs/glyph-inspector.html | 15 ++ docs/index.html | 43 ++++- src/font.js | 8 +- src/glyph.js | 26 +++ src/opentype.js | 5 + src/path.js | 16 +- src/svgimages.js | 230 ++++++++++++++++++++++++ src/tables/sfnt.js | 2 + src/tables/svg.js | 110 ++++++++++++ src/util.js | 48 ++++- test/fonts/TestSVGgradientTransform.otf | Bin 0 -> 48292 bytes test/fonts/TestSVGgzip.otf | Bin 0 -> 3024 bytes test/fonts/TestSVGmultiGlyphs.otf | Bin 0 -> 10876 bytes test/tables/svg.js | 48 +++++ 15 files changed, 560 insertions(+), 25 deletions(-) create mode 100644 src/svgimages.js create mode 100644 src/tables/svg.js create mode 100644 test/fonts/TestSVGgradientTransform.otf create mode 100644 test/fonts/TestSVGgzip.otf create mode 100644 test/fonts/TestSVGmultiGlyphs.otf create mode 100644 test/tables/svg.js diff --git a/docs/font-inspector.html b/docs/font-inspector.html index b515dd5e..e5ac87ff 100644 --- a/docs/font-inspector.html +++ b/docs/font-inspector.html @@ -103,6 +103,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 @@ -134,18 +145,7 @@

Free Software

var previewCanvas = document.getElementById('preview'); var previewCtx = previewCanvas.getContext("2d"); previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height); - var options = { - 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'] } - ] - }; - options = Object.assign({}, font.defaultRenderOptions, options); + var options = Object.assign({}, font.defaultRenderOptions, drawOptions); font.draw(previewCtx, textToRender, 0, 32, fontSize, options); } @@ -261,9 +261,19 @@

Free Software

} function onFontLoaded(font) { + if (window.font) { + window.font.onGlyphUpdated = null; + } window.font = font; renderText(font); displayFontData(font); + font.onGlyphUpdated = (glyphId) => { + const options = Object.assign({}, font.defaultRenderOptions, drawOptions); + const glyphIds = font.stringToGlyphIndexes(textToRender, options); + if (glyphIds.includes(glyphId)) { + renderText(font); + } + }; } document.getElementById('update').addEventListener('click', () => displayFontData(window.font)); diff --git a/docs/glyph-inspector.html b/docs/glyph-inspector.html index c44680fc..d020c471 100644 --- a/docs/glyph-inspector.html +++ b/docs/glyph-inspector.html @@ -227,6 +227,11 @@

Free Software

} return; } + const image = path._image; + if ( image ) { + ctx.drawImage(image.image, image.x, image.y, image.width, image.height); + return; + } var i, cmd, x1, y1, x2, y2; var arrows = []; ctx.beginPath(); @@ -490,6 +495,9 @@

Free Software

}; function onFontLoaded(font) { + if (window.font) { + window.font.onGlyphUpdated = null + } window.font = font; options = Object.assign({}, window.font.defaultRenderOptions); window.fontOptions = options; @@ -525,6 +533,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 2cadad0f..54a08a75 100755 --- a/docs/index.html +++ b/docs/index.html @@ -116,15 +116,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 - } - }; - options = Object.assign({}, font.defaultRenderOptions, options); + var options = Object.assign({}, font.defaultRenderOptions, getDrawOptions()); previewCtx.clearRect(0, 0, 940, 300); const fontSize = +form.fontsize.value; font.draw(previewCtx, textToRender, 0, 200, fontSize, options); @@ -142,6 +134,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); @@ -184,6 +187,9 @@

Free Software

} function onFontLoaded(font) { + if (window.font) { + window.font.onGlyphUpdated = null + } window.font = font; // Show the first 100 glyphs. @@ -194,9 +200,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, {}, font); glyph.drawPoints(ctx, x, y, fontSize); glyph.drawMetrics(ctx, x, y, fontSize); @@ -214,6 +222,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, {}, font); + glyph.drawPoints(ctx, x, y, fontSize); + glyph.drawMetrics(ctx, x, y, fontSize); + } + + const textToRender = form.textField.value; + const options = Object.assign({}, font.defaultRenderOptions, getDrawOptions()); + const glyphIds = font.stringToGlyphIndexes(textToRender, options); + 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/font.js b/src/font.js index e0963a5d..13466de2 100644 --- a/src/font.js +++ b/src/font.js @@ -8,6 +8,7 @@ import Position from './position.js'; import Substitution from './substitution.js'; import { PaletteManager } from './palettes.js'; import { LayerManager } from './layers.js'; +import { SVGImageManager } from './svgimages.js'; import { isBrowser, checkArgument } from './util.js'; import HintingTrueType from './hintingtt.js'; import Bidi from './bidi.js'; @@ -142,6 +143,7 @@ function Font(options) { this.tables = this.tables || {}; this.palettes = new PaletteManager(this); this.layers = new LayerManager(this); + this.svgImages = new SVGImageManager(this); // needed for low memory mode only. this._push = null; @@ -332,7 +334,8 @@ Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) { * See https://www.microsoft.com/typography/otspec/featuretags.htm * @property {boolean} [hinting=false] - whether to apply font hinting to the outlines * @property {integer} [usePalette=0] For COLR/CPAL fonts, the zero-based index of the color palette to use. (Use `Font.palettes.get()` to get the available palettes) - * @property {integer} [drawLayers=true] For COLR/CPAL fonts, this can be turned to false in order to draw the fallback glyphs instead + * @property {boolean} [drawLayers=true] For COLR/CPAL fonts, this can be turned to false in order to draw the fallback glyphs instead + * @property {boolean} [drawSVG=true] For SVG fonts, this can be turned to false in order to draw the fallback glyphs instead */ Font.prototype.defaultRenderOptions = { kerning: true, @@ -348,6 +351,7 @@ Font.prototype.defaultRenderOptions = { hinting: false, usePalette: 0, drawLayers: true, + drawSVG: true, }; /** @@ -417,7 +421,7 @@ Font.prototype.getPath = function(text, x, y, fontSize, options) { } this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); - if ( options.drawLayers ) { + if ( options.drawSVG || options.drawLayers ) { const layers = glyphPath._layers; if ( layers && layers.length ) { for(let l = 0; l < layers.length; l++) { diff --git a/src/glyph.js b/src/glyph.js index 4fb96030..e0ff16f4 100644 --- a/src/glyph.js +++ b/src/glyph.js @@ -170,6 +170,21 @@ Glyph.prototype.getPath = function(x, y, fontSize, options, font) { } const p = new Path(); + if ( options.drawSVG ) { + const svgImage = this.getSvgImage(font); + if ( svgImage ) { + const layer = new Path(); + layer._image = { + image: svgImage.image, + x: x + svgImage.leftSideBearing * scale, + y: y - svgImage.baseline * scale, + width: svgImage.image.width * scale, + height: svgImage.image.height * scale, + }; + p._layers = [layer]; + return p; + } + } if ( options.drawLayers ) { const layers = this.getLayers(font); if ( layers && layers.length ) { @@ -226,6 +241,17 @@ Glyph.prototype.getLayers = function(font) { return font.layers.get(this.index); }; +/** + * @param {opentype.Font} font + * @returns {import('./svgimages.js').SVGImage | undefined} + */ +Glyph.prototype.getSvgImage = function(font) { + if(!font) { + throw Error('The font object is required to read the svg table in order to get the image.'); + } + return font.svgImages.get(this.index); +}; + /** * Split the glyph into contours. * This function is here for backwards compatibility, and to diff --git a/src/opentype.js b/src/opentype.js index e088f0da..d827fa14 100644 --- a/src/opentype.js +++ b/src/opentype.js @@ -36,6 +36,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'; import { PaletteManager } from './palettes.js'; /** * The opentype library. @@ -396,6 +397,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 b29c3d0d..c6fb022d 100644 --- a/src/path.js +++ b/src/path.js @@ -512,7 +512,13 @@ Path.prototype.draw = function(ctx) { } return; } - + + const image = this._image; + if ( image ) { + ctx.drawImage(image.image, image.x, image.y, image.width, image.height); + return; + } + ctx.beginPath(); for (let i = 0; i < this.commands.length; i += 1) { const cmd = this.commands[i]; @@ -640,6 +646,14 @@ Path.prototype.toSVG = function(options, pathData) { */ console.warn('toSVG() does not support colr font layers yet'); } + if (this._image) { + /** + * @TODO: implement SVG output for SVG glyphs + * We can't simply output the whole SVG document as it is, we should first sanitize it + * to make sure it doesn't contain any malicious scripts or features not supported in SVG fonts. + */ + console.warn('toSVG() does not support SVG glyphs yet'); + } if (!pathData) { pathData = this.toPathData(options); } diff --git a/src/svgimages.js b/src/svgimages.js new file mode 100644 index 00000000..660369fa --- /dev/null +++ b/src/svgimages.js @@ -0,0 +1,230 @@ +import { isGzip, unGzip } from './util.js'; + +/** + * @typedef {object} SVGDocCacheEntry + * @prop {Promise} template + * @prop {Map} images + */ + +/** + * @typedef {string | [string, string, string, string, string, string, string]} SVGTemplate + */ + +/** + * @typedef {object} SVGImageCacheEntry + * @prop {Promise} promise + * @prop {SVGImage | undefined} image + */ + +/** + * @typedef {Object} SVGImage + * @prop {number} leftSideBearing + * @prop {number} baseline + * @prop {HTMLImageElement} image + */ + +export class SVGImageManager { + /** + * @param {opentype.Font} font + */ + constructor(font) { + /** @type {opentype.Font} */ + this.font = font; + /** @type {WeakMap} */ + this.cache = new WeakMap(); + } + + /** + * @param {number} glyphIndex + * @returns {SvgImage | undefined} + */ + get(glyphIndex) { + const svgImageCacheEntry = this.getOrCreateSvgImageCacheEntry(glyphIndex); + return svgImageCacheEntry && svgImageCacheEntry.image; + } + + /** + * @param {number} glyphIndex + * @returns {Promise | undefined} + */ + getAsync(glyphIndex) { + const svgImageCacheEntry = this.getOrCreateSvgImageCacheEntry(glyphIndex); + return svgImageCacheEntry && svgImageCacheEntry.promise; + } + + /** + * @param {number} glyphIndex + * @returns {SVGImageCacheEntry | undefined} + */ + getOrCreateSvgImageCacheEntry(glyphIndex) { + const svg = this.font.tables.svg; + if (svg === undefined) return; + + const svgBuf = svg.get(glyphIndex); + if (svgBuf === undefined) return; + + let svgDocCacheEntry = this.cache.get(svgBuf); + if (svgDocCacheEntry === undefined) { + svgDocCacheEntry = createSvgDocCacheEntry(svgBuf); + this.cache.set(svgBuf, svgDocCacheEntry); + } + + let svgImageCacheEntry = svgDocCacheEntry.images.get(glyphIndex); + if (svgImageCacheEntry === undefined) { + svgImageCacheEntry = createSvgImageCacheEntry(this.font, svgDocCacheEntry.template, glyphIndex); + svgImageCacheEntry.promise.then((svgImage) => { + svgImageCacheEntry.image = svgImage; + if (typeof this.font.onGlyphUpdated === 'function') { + try { + this.font.onGlyphUpdated(glyphIndex); + } catch (error) { + console.error('font.onGlyphUpdated', glyphIndex, error); + } + } + }); + svgDocCacheEntry.images.set(glyphIndex, svgImageCacheEntry); + } + return svgImageCacheEntry; + } +} + +/** + * @param {Uint8Array} svgBuf + * @returns {SVGDocCacheEntry} + */ +function createSvgDocCacheEntry(svgBuf) { + return { + template: decodeSvgDocument(svgBuf).then(makeSvgTemplate), + images: new Map(), + }; +} + +/** + * @param {opentype.Font} font + * @param {Promise} svgTemplatePromise + * @param {number} glyphIndex + * @returns {SVGImageCacheEntry} + */ +function createSvgImageCacheEntry(font, svgTemplatePromise, glyphIndex) { + return { + promise: svgTemplatePromise.then((svgTemplate) => { + let svgText; + if (typeof svgTemplate === 'string') { + svgText = svgTemplate; + } else { + svgTemplate[4] = glyphIndex; + svgText = svgTemplate.join(''); + } + const svgImage = makeSvgImage(svgText, font.unitsPerEm); + return svgImage.image.decode().then(() => svgImage); + }), + image: undefined, + }; +} + +/** +* @param {Uint8Array} buf +* @returns {Promise} +*/ +export const decodeSvgDocument = globalThis.DecompressionStream + ? decodeSvgDocumentWithDecompressionStream + : decodeSvgDocumentWithTinyInflate; + +/** + * @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); + } +} + +/** + * https://learn.microsoft.com/en-us/typography/opentype/spec/svg#glyph-identifiers + * @param {string} text + * @returns {SVGTemplate} + */ +export function makeSvgTemplate(text) { + const documentStart = text.indexOf('', documentStart + 4) + 1; + + if (/ id=['"]glyph\d+['"]/.test(text.substring(documentStart, contentStart))) { + return text; + } + + const contentEnd = text.lastIndexOf(''); + return [ + text.substring(0, contentStart), + '', + text.substring(contentStart, contentEnd), + '', + text.substring(contentEnd), + ]; +} + +/** +* @param {string} text +* @param {number} unitsPerEm +* @returns {SVGImage} +*/ +export function makeSvgImage(text, unitsPerEm) { + 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; + } else if (unitsPerEm) { + xScale = unitsPerEm / viewBoxVal.width; + yScale = unitsPerEm / viewBoxVal.height; + } + } + + 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 }; +} diff --git a/src/tables/sfnt.js b/src/tables/sfnt.js index 6083e2ba..4f00a7da 100644 --- a/src/tables/sfnt.js +++ b/src/tables/sfnt.js @@ -27,6 +27,7 @@ import avar from './avar.js'; import cvar from './cvar.js'; import gvar from './gvar.js'; import gasp from './gasp.js'; +import svg from './svg.js'; function log2(v) { return Math.log(v) / Math.log(2) | 0; @@ -364,6 +365,7 @@ function fontToSfntTable(font) { fvar, gvar, gasp, + svg, }; const optionalTableArgs = { diff --git a/src/tables/svg.js b/src/tables/svg.js new file mode 100644 index 00000000..7bf389e1 --- /dev/null +++ b/src/tables/svg.js @@ -0,0 +1,110 @@ +import { Parser } from '../parse.js'; +import table from '../table.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); +} + +export default { + make: makeSvgTable, + parse: parseSvgTable, +}; diff --git a/src/util.js b/src/util.js index 87e1e2bb..2ddcc717 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' || @@ -90,4 +92,48 @@ function binarySearchInsert(array, key, value) { return low; } -export { isBrowser, isNode, checkArgument, arraysEqual, objectsEqual, binarySearch, binarySearchIndex, binarySearchInsert }; +/** + * [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, binarySearch, binarySearchIndex, binarySearchInsert, isGzip, unGzip }; diff --git a/test/fonts/TestSVGgradientTransform.otf b/test/fonts/TestSVGgradientTransform.otf new file mode 100644 index 0000000000000000000000000000000000000000..59cfc5a1027a25b754065fab8daf6239c405401f GIT binary patch literal 48292 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 literal 0 HcmV?d00001 diff --git a/test/fonts/TestSVGgzip.otf b/test/fonts/TestSVGgzip.otf new file mode 100644 index 0000000000000000000000000000000000000000..69d976f153080b2edd32f217d329142f49e00651 GIT binary patch literal 3024 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 V#xL@%O`qj&-|oMCdtmnd{{Vvly*vN_ literal 0 HcmV?d00001 diff --git a/test/tables/svg.js b/test/tables/svg.js new file mode 100644 index 00000000..7e83fe0c --- /dev/null +++ b/test/tables/svg.js @@ -0,0 +1,48 @@ +import assert from 'assert'; +import { loadSync } from '../../src/opentype.js'; +import svg from '../../src/tables/svg.js'; +import { decodeSvgDocument } from '../../src/svgimages.js'; + +/** @typedef {import('../src/tables/svg.js').SVGTable} SVGTable */ + +describe('tables/svg.js', () => { + /** + * @type {Array} + */ + const svgTables = [ + './test/fonts/TestSVGgradientTransform.otf', + './test/fonts/TestSVGgzip.otf', + './test/fonts/TestSVGmultiGlyphs.otf', + ].map((fontPath) => loadSync(fontPath).tables.svg); + + it('can parse SVG table', async () => { + for (const svgTable of svgTables) { + /** @type {Map>} */ + const documentGlyphs = new Map(); + for (const [glyphId, svgDocBytes] of svgTable) { + let glyphIds = documentGlyphs.get(svgDocBytes); + if (glyphIds === undefined) { + glyphIds = []; + documentGlyphs.set(svgDocBytes, glyphIds); + } + glyphIds.push(glyphId); + } + for (const [svgDocBytes, glyphIds] of documentGlyphs) { + const svgDocText = await decodeSvgDocument(svgDocBytes); + assert(svgDocText.startsWith('')); + } + } + }); + + it('can make SVG table', () => { + for (const svgTable of svgTables) { + const bytes = new Uint8Array(svg.make(svgTable).encode()); + const data = new DataView(bytes.buffer); + assert.deepStrictEqual(svg.parse(data, bytes.byteOffset), svgTable); + } + }); +});