Files
edentext/tests/embedded-fonts.test.ts
Steffen Becker a8ecd8ce06 feat: a saved file carries the fonts it was opened with
Both importers already read a package's embedded faces and register them, but
nothing wrote them back: a document set in a font nobody has installed lost its
look on the next machine. ODF gets plain TTFs under Fonts/ with a manifest entry
each, merged into the font-face declaration the name already has; Word gets a
per-part GUID, an obfuscated .odttf and its w:embed... in the family's w:font.
The picker lists the families too, and a slow store read no longer replaces the
faces of a file opened while it was still running.
2026-09-12 00:36:03 +02:00

55 lines
2.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { unzipSync, strFromU8 } from 'fflate';
import { buildOdt } from '../src/lib/export/odt';
import { buildDocx } from '../src/lib/export/docx';
import { importOdt } from '../src/lib/import/odt';
import { importDocx } from '../src/lib/import/docx';
import type { EmbeddedFont } from '../src/lib/fonts/embeddedFonts';
const ttf = (name: string) => new Uint8Array(readFileSync(`src/assets/fonts/${name}`));
const fonts = (): EmbeddedFont[] => [
{ family: 'Probe & Co', weight: 'normal', style: 'normal', data: ttf('LiberationSans-Regular.ttf') },
{ family: 'Probe & Co', weight: 'bold', style: 'italic', data: ttf('LiberationSans-BoldItalic.ttf') },
{ family: 'Zweite', weight: 'normal', style: 'italic', data: ttf('LiberationSerif-Italic.ttf') },
];
const doc = { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hallo' }] }] } as any;
// The fonts ride the last positional argument, after the 18 the two builders share.
const args = (list: EmbeddedFont[]) => [...new Array(18).fill(undefined), list] as any;
const same = (got: EmbeddedFont[], want: EmbeddedFont[]) => {
expect(got.map((f) => `${f.family}|${f.weight}|${f.style}|${f.data.length}`))
.toEqual(want.map((f) => `${f.family}|${f.weight}|${f.style}|${f.data.length}`));
for (let i = 0; i < want.length; i++) expect(Array.from(got[i].data.slice(0, 64))).toEqual(Array.from(want[i].data.slice(0, 64)));
};
describe('embedded fonts survive a save', () => {
it('round-trips through .odt', async () => {
const want = fonts();
const bytes = await buildOdt(doc, ...args(fonts()));
same(importOdt(bytes).fonts, want);
// Our own importer reads the package by path; LibreOffice needs the manifest entry.
const manifest = strFromU8(unzipSync(bytes)['META-INF/manifest.xml']);
for (let i = 1; i <= want.length; i++) expect(manifest).toContain(`manifest:full-path="Fonts/font${i}.ttf"`);
});
it('round-trips through .docx', async () => {
const want = fonts();
const bytes = await buildDocx(doc, ...args(fonts()));
same(importDocx(bytes).fonts, want);
const files = unzipSync(bytes);
// Word obfuscates a font part, so the stored bytes are not the font's own header.
expect(Array.from(files['word/fonts/font1.odttf'].slice(0, 4))).not.toEqual(Array.from(want[0].data.slice(0, 4)));
expect(strFromU8(files['[Content_Types].xml'])).toContain('Extension="odttf"');
});
it('carries a document opened as .docx into an .odt', async () => {
const want = fonts();
const read = importDocx(await buildDocx(doc, ...args(fonts())));
same(importOdt(await buildOdt(read.content as any, ...args(read.fonts))).fonts, want);
});
});