fix: match note image template fonts to their visual styles

This commit is contained in:
tianma-if committed 2026-10-18 20:07:03 +08:00
1 parent eb034b3c8b
commit 51973cf16e
3 files changed
+65 -17

No files matched your search

+19 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import {
NOTE_IMAGE_THEME_OPTIONS, NOTE_IMAGE_THEMES, NOTE_IMAGE_BACKGROUND_COLORS, NOTE_IMAGE_FONT_FAMILIES,
NOTE_IMAGE_BUNDLED_FONTS, NOTE_IMAGE_TEMPLATE_TYPOGRAPHY, NOTE_IMAGE_THEME_OPTIONS, NOTE_IMAGE_THEMES, NOTE_IMAGE_BACKGROUND_COLORS, NOTE_IMAGE_FONT_FAMILIES,
resolveTheme, resolveNoteImageTypography, buildImageExportBasename, buildNoteImageCardMarkup,
estimateReadingStats, generateCardCss,
} from "./note-image-card";
@@ -8,8 +8,26 @@ import { zhCN } from "./i18n/zh-CN";
import { enUS } from "./i18n/en-US";
import { ja } from "./i18n/ja";
import { pl } from "./i18n/pl";
import { NOTE_IMAGE_FONT_SUBSETS } from "./note-image-font-manifest";
describe("note image templates", () => {
test("each template references the font family registered by its bundled assets", () => {
for (const theme of NOTE_IMAGE_THEME_OPTIONS) {
const preset = NOTE_IMAGE_TEMPLATE_TYPOGRAPHY[theme];
for (const role of ["body", "heading"]) {
const familyStack = resolveNoteImageTypography(theme)[`${role}Family`];
for (const font of preset[role]) {
const subsets = NOTE_IMAGE_FONT_SUBSETS[font];
expect(subsets.length).toBeGreaterThan(0);
for (const { family } of subsets) {
expect(family).toBe(NOTE_IMAGE_BUNDLED_FONTS[font].family);
expect(familyStack).toContain(`"${family}"`);
}
}
}
}
});
test("every selectable template is localized and has renderable colors", () => {
for (const theme of NOTE_IMAGE_THEME_OPTIONS) {
expect(NOTE_IMAGE_THEMES[theme]).toBeDefined();
+40 -9
View File
@@ -106,19 +106,50 @@ export const NOTE_IMAGE_FONT_FAMILIES: Record<Exclude<NoteImageFontStyle, "auto"
...CATALOG_FONT_STACKS,
};
export const NOTE_IMAGE_TEMPLATE_FONT_FAMILIES: Record<NoteImageActiveTheme, string> = {
gradient: "Alibaba-PuHuiTi-Regular", glass: "Alibaba-PuHuiTi-Regular", quote: "Huiwen_mingchao",
book: "SourceHanSerifCN_SemiBold", memo: "MiSans-Regular", bento: "Alibaba-PuHuiTi-Regular",
"black-sun": "Alibaba-PuHuiTi-Regular", frame: "Alibaba-PuHuiTi-Regular", handwritten: "Long Cang", code: "Geist Mono Variable",
/** Keep CSS families and asset selection together so defaults cannot silently fall back. */
export const NOTE_IMAGE_BUNDLED_FONTS = {
puhui: { family: "Alibaba-PuHuiTi-Regular", fallback: "sans" },
misans: { family: "MiSans", fallback: "sans" },
mincho: { family: "Huiwen_mingchao", fallback: "serif" },
song: { family: "SourceHanSerifCN_SemiBold", fallback: "serif" },
longcang: { family: "Long Cang", fallback: "kai" },
geist: { family: "Geist Mono Variable", fallback: "mono" },
} as const;
type BundledNoteImageFont = keyof typeof NOTE_IMAGE_BUNDLED_FONTS;
type TemplateTypography = {
body: readonly BundledNoteImageFont[];
heading: readonly BundledNoteImageFont[];
headingWeight: number;
};
export const NOTE_IMAGE_TEMPLATE_TYPOGRAPHY = {
gradient: { body: ["misans"], heading: ["misans"], headingWeight: 700 },
glass: { body: ["misans"], heading: ["mincho"], headingWeight: 400 },
quote: { body: ["mincho"], heading: ["mincho"], headingWeight: 700 },
book: { body: ["song"], heading: ["song"], headingWeight: 700 },
memo: { body: ["misans"], heading: ["misans"], headingWeight: 700 },
bento: { body: ["misans"], heading: ["song"], headingWeight: 600 },
"black-sun": { body: ["geist", "misans"], heading: ["geist", "misans"], headingWeight: 700 },
frame: { body: ["song"], heading: ["song"], headingWeight: 600 },
handwritten: { body: ["longcang"], heading: ["longcang"], headingWeight: 400 },
code: { body: ["geist"], heading: ["geist"], headingWeight: 700 },
} as const satisfies Record<NoteImageActiveTheme, TemplateTypography>;
const templateFontStack = (fonts: readonly BundledNoteImageFont[]) => [
...fonts.map(font => `"${NOTE_IMAGE_BUNDLED_FONTS[font].family}"`),
NOTE_IMAGE_FONT_FAMILIES[NOTE_IMAGE_BUNDLED_FONTS[fonts[0]].fallback],
].join(", ");
export const resolveNoteImageTypography = (theme: NoteImageTheme, fontStyle: NoteImageFontStyle = "auto") => {
const active = resolveTheme(undefined, theme);
const style = fontStyle !== "auto" ? fontStyle : active === "book" || active === "quote" ? "serif" : active === "handwritten" ? "kai" : active === "code" ? "mono" : "sans";
const family = fontStyle === "auto"
? `"${NOTE_IMAGE_TEMPLATE_FONT_FAMILIES[active]}", ${NOTE_IMAGE_FONT_FAMILIES[style]}`
: NOTE_IMAGE_FONT_FAMILIES[style];
return { bodyFamily: family, headingFamily: family, headingWeight: active === "handwritten" ? 400 : 700, lineHeightAdjustment: 0 };
const preset = NOTE_IMAGE_TEMPLATE_TYPOGRAPHY[active];
return {
bodyFamily: fontStyle === "auto" ? templateFontStack(preset.body) : NOTE_IMAGE_FONT_FAMILIES[fontStyle],
headingFamily: fontStyle === "auto" ? templateFontStack(preset.heading) : NOTE_IMAGE_FONT_FAMILIES[fontStyle],
headingWeight: fontStyle === "auto" ? preset.headingWeight : active === "handwritten" ? 400 : 700,
lineHeightAdjustment: 0,
};
};
export const NOTE_IMAGE_FONT_SIZES = {
+6 -7
View File
@@ -1,15 +1,10 @@
import { NOTE_IMAGE_FONT_NOTICES } from "./note-image-font-notices";
import { NOTE_IMAGE_FONT_SUBSETS } from "./note-image-font-manifest";
import { resolveTheme, type NoteImageTheme, type NoteImageFontStyle } from "./note-image-card";
import { NOTE_IMAGE_TEMPLATE_TYPOGRAPHY, resolveTheme, type NoteImageTheme, type NoteImageFontStyle } from "./note-image-card";
import { isRemoteNoteImageFont } from "./font-catalog";
import { fontSubsetContains, loadCatalogFont } from "./font-loader";
export { fontSubsetContains, createRemoteFontResourceLoader, loadRemoteFontResource } from "./font-loader";
export const NOTE_IMAGE_THEME_FONTS = {
gradient: "puhui", glass: "puhui", quote: "mincho", book: "song", memo: "misans",
bento: "puhui", "black-sun": "puhui", frame: "puhui", handwritten: "longcang", code: "geist",
} as const;
type AssetUrl = string | number | { uri: string };
const resources = new Map<string, Promise<string>>();
const loadedFaces = new Map<string, Promise<FontFace>>();
@@ -55,7 +50,11 @@ export const prepareNoteImageAssets = async ({ theme, fontStyle = "auto", text,
};
const points = new Set(Array.from(text, char => char.codePointAt(0)!));
const remote = isRemoteNoteImageFont(fontStyle);
const catalog = !remote && fontStyle === "auto" ? NOTE_IMAGE_FONT_SUBSETS[NOTE_IMAGE_THEME_FONTS[active]] : [];
const preset = NOTE_IMAGE_TEMPLATE_TYPOGRAPHY[active];
const fontIds = [...new Set([...preset.body, ...preset.heading])];
const catalog = !remote && fontStyle === "auto"
? fontIds.flatMap(id => [...NOTE_IMAGE_FONT_SUBSETS[id]])
: [];
const blocks = catalog.filter(block => fontSubsetContains(block.range, points));
const fontFacesCss = (await Promise.all(blocks.map(async block => {
let loaded = loadedFaces.get(block.asset);