mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
fix(pdf): register CJK fallback at primary font weights so bold rende… (#3080)
The CJK fallback (Noto Sans SC / Noto Serif SC) was only registered at weight 400. When react-pdf rendered CJK characters with font-weight 700 (e.g. <strong> from a rich-text section, or templates' bold style), it walked the font-family stack [primary, cjkFallback], failed on the primary (no CJK glyphs), then fell back to the only registered fallback variant (400) — and react-pdf does not synthesize bold. The bold style was silently dropped for CJK runs in both the live preview and the exported PDF, while still working for Latin runs. Register the CJK fallback at the same weight range as the primary font (lowest + highest, both styles). When body and heading share the same fallback (the common case where both are sans or both are serif), merge their weight ranges so each weight is registered exactly once. webfontlist.json already ships all weights for the default CJK fallbacks, so no font-list changes are required. Closes #3079 Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
@@ -71,6 +71,43 @@ describe("registerFonts", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("registers a bold CJK fallback variant so <strong> renders bold for CJK glyphs", async () => {
|
||||
const registerSpy = vi.spyOn(Font, "register").mockImplementation(() => {});
|
||||
vi.spyOn(Font, "registerHyphenationCallback").mockImplementation(() => {});
|
||||
const { registerFonts } = await import("./use-register-fonts");
|
||||
|
||||
// body has highest weight 700, heading has 600 — both should be registered
|
||||
// for the CJK fallback so textkit can substitute bold CJK glyphs.
|
||||
const boldTypography = {
|
||||
body: { ...typography.body, fontWeights: ["400", "700"] },
|
||||
heading: { ...typography.heading, fontWeights: ["400", "600"] },
|
||||
} satisfies Typography;
|
||||
|
||||
registerFonts(boldTypography, "zh-CN");
|
||||
|
||||
expect(registerSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
family: "Noto Serif SC",
|
||||
fontWeight: 700,
|
||||
fontStyle: "normal",
|
||||
}),
|
||||
);
|
||||
expect(registerSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
family: "Noto Serif SC",
|
||||
fontWeight: 700,
|
||||
fontStyle: "italic",
|
||||
}),
|
||||
);
|
||||
expect(registerSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
family: "Noto Serif SC",
|
||||
fontWeight: 600,
|
||||
fontStyle: "normal",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the full CJK font source for synthetic italic variants when the CJK font is primary", async () => {
|
||||
const registerSpy = vi.spyOn(Font, "register").mockImplementation(() => {});
|
||||
vi.spyOn(Font, "registerHyphenationCallback").mockImplementation(() => {});
|
||||
|
||||
@@ -133,19 +133,38 @@ export const registerFonts = (typography: Typography, locale: Locale, hasCjkCont
|
||||
}
|
||||
|
||||
// Register a CJK fallback so textkit can substitute per-codepoint for
|
||||
// characters the primary font lacks (#2986). One weight per style is
|
||||
// enough — substitution is per-codepoint, not per-weight.
|
||||
// characters the primary font lacks (#2986). We register both the
|
||||
// regular and bold weights so that <strong>/font-weight: 700 styles
|
||||
// are honored for CJK glyphs — without the bold variant, textkit
|
||||
// would only find a 400-weight match and synthesize an unbolded run.
|
||||
const bodyCjkFallback = needsCjkTextSupport ? getPdfCjkFallbackFontFamily(bodyFontFamily) : null;
|
||||
const headingCjkFallback = needsCjkTextSupport ? getPdfCjkFallbackFontFamily(headingFontFamily) : null;
|
||||
|
||||
if (bodyCjkFallback) {
|
||||
registerFont(bodyCjkFallback, 400, false);
|
||||
registerFont(bodyCjkFallback, 400, true);
|
||||
}
|
||||
const registerCjkFallback = (family: string, ranges: FontWeightRange[]) => {
|
||||
const weights = new Set<number>();
|
||||
for (const range of ranges) {
|
||||
weights.add(range.lowest);
|
||||
weights.add(range.highest);
|
||||
}
|
||||
for (const italic of [false, true]) {
|
||||
for (const weight of weights) {
|
||||
registerFont(family, weight, italic);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (headingCjkFallback && headingCjkFallback !== bodyCjkFallback) {
|
||||
registerFont(headingCjkFallback, 400, false);
|
||||
registerFont(headingCjkFallback, 400, true);
|
||||
if (bodyCjkFallback && bodyCjkFallback === headingCjkFallback) {
|
||||
// Same fallback for body and heading: merge weight ranges so that
|
||||
// bold styles applied to either typography level have a matching
|
||||
// CJK glyph variant.
|
||||
registerCjkFallback(bodyCjkFallback, [bodyRange, headingRange]);
|
||||
} else {
|
||||
if (bodyCjkFallback) {
|
||||
registerCjkFallback(bodyCjkFallback, [bodyRange]);
|
||||
}
|
||||
if (headingCjkFallback) {
|
||||
registerCjkFallback(headingCjkFallback, [headingRange]);
|
||||
}
|
||||
}
|
||||
|
||||
// Latin-only path: no fallback registered, return as-is.
|
||||
|
||||
Reference in New Issue
Block a user