-
-
Notifications
You must be signed in to change notification settings - Fork 2
feat: custom fonts via in-browser upload #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
0474f70
402b9ef
771896a
b53ac7f
a40365d
e9e743a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| /** | ||
| * Generates a minimal, tiny sfnt (TrueType) font in memory for tests. | ||
| * | ||
| * The font is not meant to be rendered (the Typst compiler is mocked in tests); | ||
| * it only carries a valid `name` table so that the add-in's family/subfamily | ||
| * detection and the Custom fonts UI can be exercised against a real font file | ||
| * without committing a large binary fixture. | ||
| */ | ||
|
|
||
| /** Encodes a string as big-endian UTF-16 (the encoding used by Windows name records). */ | ||
| function utf16be(value: string): Buffer { | ||
| const buffer = Buffer.alloc(value.length * 2); | ||
| for (let i = 0; i < value.length; i++) { | ||
| buffer.writeUInt16BE(value.charCodeAt(i), i * 2); | ||
| } | ||
| return buffer; | ||
| } | ||
|
|
||
| /** | ||
| * Builds a minimal font whose `name` table reports the given family and | ||
| * subfamily (style) names. | ||
| */ | ||
| export function makeTestFont(family: string, subfamily: string): Buffer { | ||
| const records = [ | ||
| { nameId: 1, value: utf16be(family) }, // Font Family | ||
| { nameId: 2, value: utf16be(subfamily) }, // Font Subfamily | ||
| ]; | ||
|
|
||
| // --- name table --- | ||
| const nameHeaderSize = 6; | ||
| const stringStorageOffset = nameHeaderSize + records.length * 12; | ||
| const strings = Buffer.concat(records.map(record => record.value)); | ||
| const nameTable = Buffer.alloc(stringStorageOffset + strings.length); | ||
|
|
||
| nameTable.writeUInt16BE(0, 0); // format 0 | ||
| nameTable.writeUInt16BE(records.length, 2); // count | ||
| nameTable.writeUInt16BE(stringStorageOffset, 4); // string storage offset | ||
|
|
||
| let recordOffset = nameHeaderSize; | ||
| let stringOffset = 0; | ||
| for (const record of records) { | ||
| nameTable.writeUInt16BE(3, recordOffset); // platformID: Windows | ||
| nameTable.writeUInt16BE(1, recordOffset + 2); // encodingID: Unicode BMP | ||
| nameTable.writeUInt16BE(0x0409, recordOffset + 4); // languageID: English (US) | ||
| nameTable.writeUInt16BE(record.nameId, recordOffset + 6); | ||
| nameTable.writeUInt16BE(record.value.length, recordOffset + 8); // length | ||
| nameTable.writeUInt16BE(stringOffset, recordOffset + 10); // offset in storage | ||
| recordOffset += 12; | ||
| stringOffset += record.value.length; | ||
| } | ||
| strings.copy(nameTable, stringStorageOffset); | ||
|
|
||
| // --- sfnt wrapper: offset table + one table record pointing at `name` --- | ||
| const offsetTableSize = 12; | ||
| const tableRecordSize = 16; | ||
| const nameTableFileOffset = offsetTableSize + tableRecordSize; | ||
| const font = Buffer.alloc(nameTableFileOffset + nameTable.length); | ||
|
|
||
| font.writeUInt32BE(0x00010000, 0); // sfntVersion: TrueType | ||
| font.writeUInt16BE(1, 4); // numTables | ||
| // searchRange/entrySelector/rangeShift are not read by the parser; leave 0. | ||
|
|
||
| font.write("name", offsetTableSize, "ascii"); // tag | ||
| font.writeUInt32BE(0, offsetTableSize + 4); // checksum (unused by the parser) | ||
| font.writeUInt32BE(nameTableFileOffset, offsetTableSize + 8); // offset | ||
| font.writeUInt32BE(nameTable.length, offsetTableSize + 12); // length | ||
| nameTable.copy(font, nameTableFileOffset); | ||
|
|
||
| return font; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { expect } from "@playwright/test"; | ||
| import { test } from "./_support/fixtures"; | ||
| import { makeTestFont } from "./_support/font-fixture"; | ||
|
|
||
| const FAMILY = "PPTypst Test"; | ||
|
|
||
| test("adds a custom font from an uploaded file", async ({ powerPointPage }) => { | ||
| await powerPointPage.openFontsPanel(); | ||
| await powerPointPage.addFontFiles([ | ||
| { name: "PPTypstTest-Regular.ttf", buffer: makeTestFont(FAMILY, "Regular") }, | ||
| ]); | ||
|
|
||
| await expect(powerPointPage.fontItems()).toHaveCount(1); | ||
| await powerPointPage.expectFontFamilyCount(FAMILY, 1); | ||
| await powerPointPage.expectFontStyleListed("Regular"); | ||
| await powerPointPage.expectStatus(`Added: ${FAMILY} Regular`); | ||
| }); | ||
|
|
||
| test("keeps multiple weights of the same family as separate faces", async ({ powerPointPage }) => { | ||
| await powerPointPage.openFontsPanel(); | ||
| await powerPointPage.addFontFiles([ | ||
| { name: "PPTypstTest-Regular.ttf", buffer: makeTestFont(FAMILY, "Regular") }, | ||
| { name: "PPTypstTest-Bold.ttf", buffer: makeTestFont(FAMILY, "Bold") }, | ||
| ]); | ||
|
|
||
| // Both faces coexist (keyed per family+style) instead of overwriting each other. | ||
| await expect(powerPointPage.fontItems()).toHaveCount(2); | ||
| await powerPointPage.expectFontFamilyCount(FAMILY, 2); | ||
| await powerPointPage.expectFontStyleListed("Regular"); | ||
| await powerPointPage.expectFontStyleListed("Bold"); | ||
| }); | ||
|
|
||
| test("removes a custom font", async ({ powerPointPage }) => { | ||
| await powerPointPage.openFontsPanel(); | ||
| await powerPointPage.addFontFiles([ | ||
| { name: "PPTypstTest-Regular.ttf", buffer: makeTestFont(FAMILY, "Regular") }, | ||
| ]); | ||
| await expect(powerPointPage.fontItems()).toHaveCount(1); | ||
|
|
||
| await powerPointPage.removeFirstFont(); | ||
|
|
||
| await expect(powerPointPage.fontItems()).toHaveCount(0); | ||
| await powerPointPage.expectNoCustomFonts(); | ||
| }); | ||
|
|
||
| test("persists custom fonts across a reload", async ({ powerPointPage }) => { | ||
| await powerPointPage.openFontsPanel(); | ||
| await powerPointPage.addFontFiles([ | ||
| { name: "PPTypstTest-Regular.ttf", buffer: makeTestFont(FAMILY, "Regular") }, | ||
| ]); | ||
| await expect(powerPointPage.fontItems()).toHaveCount(1); | ||
|
|
||
| await powerPointPage.reload(); | ||
| await powerPointPage.openFontsPanel(); | ||
|
|
||
| // The font was restored from IndexedDB, not re-uploaded. | ||
| await expect(powerPointPage.fontItems()).toHaveCount(1); | ||
| await powerPointPage.expectFontFamilyCount(FAMILY, 1); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -106,6 +106,17 @@ | |
| ></textarea> | ||
| </details> | ||
|
|
||
| <details id="fontsDetails" class="fonts-panel"> | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fonts panel should have a small vertical spacing to the |
||
| <summary class="fonts-summary" title="Upload font files to use them in your Typst code via #set text(font: ...)"> | ||
| Custom fonts | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should add a small section explaining briefly (via a hint icon tooltip) how this works, e.g. that we store the actual font in a database in the local browser (that the plugin runs in). |
||
| </summary> | ||
| <label class="fonts-upload"> | ||
| <span class="fonts-upload-text">+ Add font file</span> | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should indicate which font formats are actually supported and where it is limited. Maybe also with a link to the Typst documentation. |
||
| <input id="fontsInput" type="file" class="fonts-input" accept=".ttf,.otf,.ttc" multiple> | ||
| </label> | ||
| <ul id="fontsList" class="fonts-list"></ul> | ||
| </details> | ||
|
|
||
| <!-- Status Bar --> | ||
| <div id="statusBar" class="status-bar"> | ||
| <div id="status" class="status-text"></div> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.