Skip to content

feat: render project fonts in editor#3348

Draft
nighca wants to merge 2 commits into
goplus:devfrom
nighca:issue-3342
Draft

feat: render project fonts in editor#3348
nighca wants to merge 2 commits into
goplus:devfrom
nighca:issue-3342

Conversation

@nighca

@nighca nighca commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Depends on #3362.

Summary

  • provide project fonts and the fontPreferences array to the editor SVG rendering context;
  • apply project font semantics only to custom SVG font family names;
  • embed only fonts referenced by each SVG image;
  • update image-preview consumers to use the font-aware rendering path.

Status

This PR remains draft. Large fonts currently make SVG previews slow because they are embedded as data URIs; the editor rendering strategy needs further optimization before this layer is ready to merge.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for project-specific font preferences and custom font families, specifically adding basic Chinese font support and integrating it with SVG rendering. Feedback on the changes focuses on performance optimizations and code simplification: it is recommended to avoid redundant SVG render cache invalidation by checking for actual changes in font preferences, cache the network fetch promise for the basic Chinese font to prevent repeated network requests, simplify the font initialization logic in the project creation modal, and refactor manual DOM traversal in SVG font injection using the closest() API.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread spx-gui/src/utils/img-rendering.ts Outdated
Comment thread spx-gui/src/models/spx/font.ts Outdated
Comment thread spx-gui/src/components/project/ProjectCreateModal.vue Outdated
Comment thread spx-gui/src/utils/scratch-svg-font.ts Outdated

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

临时使用 NotoSansCJKsc-Regular.otf 占位,PR 合入前换成 spx 当前使用的中文字体

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

临时使用 NotoSans-Medium.ttf 占位,PR 合入前换成 spx 当前使用的 Latin 字体

@nighca
nighca marked this pull request as ready for review July 21, 2026 08:53

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: support project font collection

Solid, well-tested feature. Fonts are carried through file serialization (so they survive duplication/remix), the SVG-injection escaping via JSON.stringify is safe against CSS/markup breakout, the WeakMap-backed derived-SVG cache with catch-driven eviction is clean, and arrayBufferToDataUri chunks base64 to avoid stack overflow. No security issues found.

A few concrete points below (inline). Summary of the non-line-specific ones:

Performance — same font re-encoded per SVG. svg-font.ts:arrayBufferToDataUri base64-encodes the whole font file on every injectFontsToSvgText call, and SvgFontContext.cache is keyed on the SVG File, not the font. So for N SVGs sharing one font family, the font is base64-encoded N times (and each derived blob carries its own ~1.33× copy of the font). With the multi-MB NotoSansCJKsc-Regular.otf, an editor rendering many text-bearing sprites/backdrops does O(N × fontSize) redundant CPU, and a single fontPreferences change discards the cache and re-encodes everything. Consider memoizing the data-URI (or the whole @font-face rule string) per font File in a module-level WeakMap<File, string> so it survives context rebuilds. See inline note at svg-font.ts:30.

default family is render-only and never persisted (asymmetry worth a comment). ProjectEditor.vue injects createDefaultFontFile() under defaultFontFamilyName into the render-time map, and the constructor defaults fontPreferences = ['default'], but 'default' is never added to project.fonts and so is not in the exported assets/fonts collection. This looks intentional (the runtime supplies default), but fontPreferences referencing a family absent from the exported collection is non-obvious — a one-line comment near ProjectEditor.vue or in font.ts would prevent a future reader from reading it as a missing-export bug.

Placeholders before merge. The two unresolved threads still apply: NotoSansCJKsc-Regular.otf and NotoSans-Medium.ttf are placeholders to swap for spx's actual fonts. Note default/README.md labels its font "a temporary Latin placeholder" but basic-chinese/README.md has no such note — if that OTF is likewise temporary, add the same wording for parity.

Comment thread spx-gui/src/models/spx/project.ts Outdated
}

export function parseFontPreferences(fontPreferences: string) {
return fontPreferences.split(',').map(stripFontPreferenceQuotes)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseFontPreferences doesn't drop empty entries. An empty or misconfigured VITE_DEFAULT_FONT_PREFERENCES, or a trailing/double comma in a project's stored fontPreferences, yields entries like ['default', ''] (or ['']). These empties get stored, re-serialized on export (round-trips as "default, "), and flow into svg-font.ts producing an invalid CSS token font-family: default, ""; plus usedFontFamilies.add(""). Suggest .map(stripFontPreferenceQuotes).filter((f) => f !== '').

return trimmed.slice(1, -1)
}
return trimmed
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stripQuotes here is byte-for-byte identical to stripFontPreferenceQuotes in project.ts, and both parse a comma-separated font-family list. Worth extracting one shared helper — otherwise the two can drift (e.g. one later handling escaped quotes/whitespace differently) and silently break round-tripping between what's embedded in the SVG and what's stored in fontPreferences.

attrValue
.split(',')
.map(stripQuotes)
.forEach((family) => used.add(family))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getUsedFontFamilies doesn't skip empty/whitespace values, so font-family="" (or " , ") adds "" to the used set. It's harmless today only because fontFiles.get("") is null and filtered later, but it's fragile — add .filter((family) => family !== '') after .map(stripQuotes) to make the intent explicit.

for (let i = 0; i < bytes.byteLength; i += 8192) {
binary += String.fromCharCode(...bytes.subarray(i, i + 8192))
}
return `data:${file.type || 'font/opentype'};base64,${btoa(binary)}`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This base64-encodes the entire font on every call, and the derived-SVG cache in img-rendering.ts is keyed on the SVG file, not the font — so the same font is re-encoded once per SVG that uses it, and each cached blob holds its own ~1.33× copy. For the multi-MB CJK OTF across many text-bearing SVGs this is significant CPU + memory. Consider memoizing the data-URI (or the @font-face rule) per font File in a module-level WeakMap<File, string> so it's reused across SVGs and survives SvgFontContext rebuilds.

const fonts = await FontFamily.loadAll(files)
// Projects created before project-font support have no font config, but may rely on Builder's
// previous Chinese SVG fallback. Materialize the equivalent project font on their next save.
if (fontPreferences == null && fonts.length === 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the migration guard couples fontPreferences == null with fonts.length === 0. A legacy project with fontPreferences == null but a non-empty assets/fonts dir would fall into the else branch and get ['default'], loading those font families into this.fonts without any preference referencing them. Unlikely in practice, but a one-line comment on why fonts.length === 0 is part of the guard would help.

@nighca
nighca force-pushed the issue-3342 branch 2 times, most recently from 677be1f to c1730e5 Compare July 21, 2026 10:24
@nighca nighca changed the title feat: support project font collection feat: render project fonts in editor Jul 21, 2026
@nighca
nighca marked this pull request as draft July 21, 2026 10:25
@nighca
nighca force-pushed the issue-3342 branch 2 times, most recently from dee72f7 to 061134a Compare July 22, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant