From 2cde4ec0359140ab7b0a44a0471239de39cba806 Mon Sep 17 00:00:00 2001 From: Anthony GAEREMYNCK <1@anthony.sh> Date: Fri, 31 Jul 2026 12:56:13 +0200 Subject: [PATCH] feat: add source-only dfont font converter Co-Authored-By: Claude Sonnet 5 --- src/converters/dfont.js | 52 +++++++++++++++++++++ test/converters/dfont.test.js | 87 +++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 src/converters/dfont.js create mode 100644 test/converters/dfont.test.js diff --git a/src/converters/dfont.js b/src/converters/dfont.js new file mode 100644 index 0000000..1afc785 --- /dev/null +++ b/src/converters/dfont.js @@ -0,0 +1,52 @@ +import fs from 'node:fs/promises'; +import * as fontkit from 'fontkit'; +import { Font } from 'fonteditor-core'; +import { register } from './registry.js'; +import { convertBufferToOtf } from './font.js'; + +export function probeDfont(buffer) { + try { + const dfont = fontkit.create(buffer); + const types = dfont.header?.map?.typeList?.types ?? []; + return types.some((t) => t.name === 'sfnt'); + } catch { + return false; + } +} + +export function extractSfntBuffer(buffer) { + const dfont = fontkit.create(buffer); + const sfntType = dfont.header.map.typeList.types.find((t) => t.name === 'sfnt'); + const ref = sfntType.refList[0]; + const lenPos = dfont.header.dataOffset + ref.dataOffset; + const len = buffer.readUInt32BE(lenPos); + const start = lenPos + 4; + return buffer.subarray(start, start + len); +} + +function sniffSfntType(buffer) { + return buffer.slice(0, 4).toString('ascii') === 'OTTO' ? 'otf' : 'ttf'; +} + +export function registerDfontConverter() { + for (const targetFormat of ['ttf', 'otf', 'woff']) { + register({ + family: 'font', + sourceFormat: 'dfont', + targetFormat, + convert: async (inputPath, outputPath) => { + const dfontBuffer = await fs.readFile(inputPath); + const sfntBuffer = extractSfntBuffer(dfontBuffer); + + if (targetFormat === 'otf') { + await convertBufferToOtf(sfntBuffer, outputPath); + } else { + const sourceFormat = sniffSfntType(sfntBuffer); + const font = Font.create(sfntBuffer, { type: sourceFormat }); + const out = font.write({ type: targetFormat }); + await fs.writeFile(outputPath, Buffer.from(out)); + } + }, + }); + } +} diff --git a/test/converters/dfont.test.js b/test/converters/dfont.test.js new file mode 100644 index 0000000..6207fe1 --- /dev/null +++ b/test/converters/dfont.test.js @@ -0,0 +1,87 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import os from 'node:os'; +import opentype from 'opentype.js'; +import { probeDfont, extractSfntBuffer, registerDfontConverter } from '../../src/converters/dfont.js'; +import { resolve, listTargetFormats } from '../../src/converters/registry.js'; +import { detectInputMime } from '../../src/mime.js'; + +let tmpDir; + +beforeAll(async () => { + registerDfontConverter(); + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'converter-dfont-')); +}); + +afterAll(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }); +}); + +describe('probeDfont', () => { + it('returns true for a real dfont fixture', async () => { + const buffer = await fs.readFile(path.join(import.meta.dirname, '..', 'fixtures', 'sample.dfont')); + expect(probeDfont(buffer)).toBe(true); + }); + + it('returns false for content starting with the ICO-colliding magic bytes but no real dfont structure', () => { + const buffer = Buffer.from([0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x10, 0x10]); + expect(probeDfont(buffer)).toBe(false); + }); +}); + +describe('extractSfntBuffer', () => { + it('extracts a valid, byte-exact standalone sfnt font from the dfont fixture', async () => { + const buffer = await fs.readFile(path.join(import.meta.dirname, '..', 'fixtures', 'sample.dfont')); + const sfnt = extractSfntBuffer(buffer); + + const ab = sfnt.buffer.slice(sfnt.byteOffset, sfnt.byteOffset + sfnt.byteLength); + const font = opentype.parse(ab); + expect(font.outlinesFormat).toBe('truetype'); + expect(font.glyphs.length).toBe(7); + }); +}); + +describe('dfont converter registration', () => { + it('registers dfont as a source for ttf, otf, and woff', () => { + expect(listTargetFormats('dfont')).toEqual(expect.arrayContaining(['ttf', 'otf', 'woff'])); + }); + + it('never registers dfont as a target format', () => { + expect(listTargetFormats('ttf')).not.toContain('dfont'); + expect(listTargetFormats('otf')).not.toContain('dfont'); + expect(listTargetFormats('woff')).not.toContain('dfont'); + }); +}); + +describe('dfont -> font conversion', () => { + it('converts the dfont fixture to ttf', async () => { + const inputPath = path.join(import.meta.dirname, '..', 'fixtures', 'sample.dfont'); + const outputPath = path.join(tmpDir, 'from-dfont.ttf'); + + await resolve('dfont', 'ttf').convert(inputPath, outputPath); + + const detected = await detectInputMime(outputPath); + expect(detected.mime).toBe('font/ttf'); + }); + + it('converts the dfont fixture to woff', async () => { + const inputPath = path.join(import.meta.dirname, '..', 'fixtures', 'sample.dfont'); + const outputPath = path.join(tmpDir, 'from-dfont.woff'); + + await resolve('dfont', 'woff').convert(inputPath, outputPath); + + const detected = await detectInputMime(outputPath); + expect(detected.mime).toBe('font/woff'); + }); + + it('converts the dfont fixture to otf', async () => { + const inputPath = path.join(import.meta.dirname, '..', 'fixtures', 'sample.dfont'); + const outputPath = path.join(tmpDir, 'from-dfont.otf'); + + await resolve('dfont', 'otf').convert(inputPath, outputPath); + + const detected = await detectInputMime(outputPath); + expect(detected.mime).toBe('font/otf'); + }); +});