feat: add source-only dfont font converter
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user