diff --git a/package-lock.json b/package-lock.json index 1cc0680..cdc298e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,14 @@ "express": "^5.2.1", "express-rate-limit": "^8.6.1", "file-type": "^22.0.1", + "fonteditor-core": "^2.6.3", + "fontkit": "^2.0.4", "heic-convert": "^2.1.0", "icojs": "^1.0.0", "mammoth": "^1.12.0", "markdown-it": "^15.0.0", "multer": "^2.2.0", + "opentype.js": "^2.0.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^6.2.108", "pm2": "^7.0.3", @@ -1531,6 +1534,21 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@swc/helpers/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", @@ -2128,6 +2146,15 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -2447,6 +2474,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2799,6 +2835,12 @@ "wrappy": "1" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/dingbat-to-unicode": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", @@ -3242,6 +3284,12 @@ "node": ">=10.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-json-patch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", @@ -3344,6 +3392,31 @@ } } }, + "node_modules/fonteditor-core": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/fonteditor-core/-/fonteditor-core-2.6.3.tgz", + "integrity": "sha512-YUryIKjkenjZ41E7JvM3V+02Ak4mTHDDTwBWgs9KBzypzHqLZHuua1UDRevZNTKawmnq1dbBAa70Jddl2+F4FQ==", + "dependencies": { + "@xmldom/xmldom": "^0.8.3" + } + }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -4775,6 +4848,15 @@ "wrappy": "1" } }, + "node_modules/opentype.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-2.0.0.tgz", + "integrity": "sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A==", + "license": "MIT", + "bin": { + "ot": "bin/ot" + } + }, "node_modules/option": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", @@ -5465,6 +5547,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -5999,6 +6087,12 @@ "node": ">= 6" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -6225,6 +6319,32 @@ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index e25996b..9cf8aa6 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,14 @@ "express": "^5.2.1", "express-rate-limit": "^8.6.1", "file-type": "^22.0.1", + "fonteditor-core": "^2.6.3", + "fontkit": "^2.0.4", "heic-convert": "^2.1.0", "icojs": "^1.0.0", "mammoth": "^1.12.0", "markdown-it": "^15.0.0", "multer": "^2.2.0", + "opentype.js": "^2.0.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^6.2.108", "pm2": "^7.0.3", diff --git a/test/converters/font.test.js b/test/converters/font.test.js new file mode 100644 index 0000000..184b6fb --- /dev/null +++ b/test/converters/font.test.js @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import opentype from 'opentype.js'; +import * as fontkit from 'fontkit'; + +async function parseWithOpentype(fixtureName) { + const buf = await fs.readFile(path.join(import.meta.dirname, '..', 'fixtures', fixtureName)); + const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + return opentype.parse(ab); +} + +describe('font fixtures', () => { + it('sample.otf parses as a CFF-flavored OpenType font', async () => { + const font = await parseWithOpentype('sample.otf'); + expect(font.outlinesFormat).toBe('cff'); + expect(font.glyphs.length).toBe(1568); + }); + + it('sample.ttf parses as a TrueType font with the same glyph count as sample.otf', async () => { + const font = await parseWithOpentype('sample.ttf'); + expect(font.outlinesFormat).toBe('truetype'); + expect(font.glyphs.length).toBe(1568); + }); + + it('sample.dfont is a valid dfont container with one embedded sfnt font', async () => { + const buffer = await fs.readFile(path.join(import.meta.dirname, '..', 'fixtures', 'sample.dfont')); + const dfont = fontkit.create(buffer); + const sfntType = dfont.header.map.typeList.types.find((t) => t.name === 'sfnt'); + expect(sfntType.refList).toHaveLength(1); + }); +}); diff --git a/test/fixtures/FONT_ATTRIBUTION.md b/test/fixtures/FONT_ATTRIBUTION.md new file mode 100644 index 0000000..f32e177 --- /dev/null +++ b/test/fixtures/FONT_ATTRIBUTION.md @@ -0,0 +1,15 @@ +# Font fixture attribution + +`sample.otf` and `sample.ttf` are the Regular weight of +[Source Code Pro](https://github.com/adobe-fonts/source-code-pro) by Adobe, +licensed under the SIL Open Font License 1.1 (see that repo's +`LICENSE.md`). Both are the same font family deliberately, so tests can +assert glyph-count fidelity (1568 glyphs) across otf/ttf/woff conversions +of "the same font" rather than comparing unrelated files. + +`sample.dfont` is `Tests/ttx/data/TestDFONT.dfont` from +[fonttools/fonttools](https://github.com/fonttools/fonttools), MIT +licensed. It is a small (3.5KB) fixture containing one real embedded +`sfnt` TrueType resource (7 glyphs) — used to exercise this project's +dfont-container extraction code without needing a large real-world +font suitcase. diff --git a/test/fixtures/sample.dfont b/test/fixtures/sample.dfont new file mode 100644 index 0000000..a6ea700 Binary files /dev/null and b/test/fixtures/sample.dfont differ diff --git a/test/fixtures/sample.otf b/test/fixtures/sample.otf new file mode 100644 index 0000000..16c7b0b Binary files /dev/null and b/test/fixtures/sample.otf differ diff --git a/test/fixtures/sample.ttf b/test/fixtures/sample.ttf new file mode 100644 index 0000000..10c73d7 Binary files /dev/null and b/test/fixtures/sample.ttf differ