From 4a2a6cd9148ff7703ae0dbc056cda2b9a122ce9d Mon Sep 17 00:00:00 2001 From: baseballyama Date: Fri, 31 Jul 2026 18:30:14 +0900 Subject: [PATCH] fix(preview): stop shrinking text in shapes without normAutofit PowerPoint never shrinks text in shapes that lack : noAutofit (or no autofit element) overflows the box, and spAutoFit grows the box instead of the text. The heuristic shrink-to-fit estimator rendered template placeholders (size inherited from layout/master, box authored tightly around sample text) at down to 0.4x of their PowerPoint size. Remove the estimator; only authored normAutofit shrinks text. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016az3UDqDkcNbCmWe3LoGnm --- .changeset/heavy-buses-tell.md | 15 +++++++ packages/preview/src/render-slide.ts | 59 +++++----------------------- 2 files changed, 24 insertions(+), 50 deletions(-) create mode 100644 .changeset/heavy-buses-tell.md diff --git a/.changeset/heavy-buses-tell.md b/.changeset/heavy-buses-tell.md new file mode 100644 index 00000000..af4846f4 --- /dev/null +++ b/.changeset/heavy-buses-tell.md @@ -0,0 +1,15 @@ +--- +'@office-kit/pptx-preview': patch +--- + +fix(preview): stop shrinking text in shapes without `` + +PowerPoint never shrinks text in shapes that lack ``: +`` (or no autofit element) simply overflows the box, and +`` grows the box to fit the text. The preview applied a +heuristic shrink-to-fit to such shapes, which rendered template +placeholders (font size inherited from the layout/master, box authored +tightly around the sample text) at down to 0.4× of their PowerPoint +size. The heuristic estimator is removed; only an authored +`` (with or without a baked `fontScale`) shrinks text, +matching PowerPoint. diff --git a/packages/preview/src/render-slide.ts b/packages/preview/src/render-slide.ts index 03dd7abb..17bfdc39 100644 --- a/packages/preview/src/render-slide.ts +++ b/packages/preview/src/render-slide.ts @@ -2147,13 +2147,6 @@ const renderRun = ( // `line-height` declaration in renderRun. const LINE_HEIGHT = 1.05; -// Rough mean glyph width as a fraction of font-size in a typical -// sans-serif. 0.55 is what PowerPoint's auto-fit estimator uses for -// Calibri at body sizes. Used to estimate when text wraps so the -// renderer can shrink the font to keep wrapped placeholders from -// overflowing. -const AVG_GLYPH_W_RATIO = 0.55; - // `` shrink-to-fit search bounds. PowerPoint reduces the font // in discrete steps until the body fits its box; we sweep from 1.0 down to the // floor in fixed decrements. The floor stops a pathologically small box from @@ -2754,51 +2747,17 @@ export const resolveTextBodyModel = ( // computed one (``). That's the same // multiplier PowerPoint applies on-screen, so honouring it is what // brings the rendered size into 1:1 agreement with the deck. + // + // No estimation happens for shapes WITHOUT ``: PowerPoint + // never shrinks those — `` (and no autofit at all) simply + // overflows the box, and `` grows the box instead of the text. + // An earlier heuristic shrank such shapes to fit their authored box, which + // rendered template placeholders (size inherited from layout/master, box + // sized by the template author) at up to 0.4× of their PowerPoint size. const authoredAutofit = getShapeTextAutoFitParams(shape); let autoFitScale = authoredAutofit?.fontScale ?? 1; - let lineHeightScale = 1 - (authoredAutofit?.lnSpcReduction ?? 0); - - // Only fall back to the heuristic estimator when no authored - // autofit ran. CJK glyphs are ~1em wide vs the ~0.55em Latin - // average, so detect a leading CJK character per paragraph and - // widen the per-line estimate accordingly — keeps Japanese titles - // from over-shrinking on placeholders that already fit. - if (!authoredAutofit) { - const innerWPx = innerW / EMU_PER_PX; - const innerHPx = innerH / EMU_PER_PX; - let totalH = 0; - for (const para of paraData) { - let maxSize = defaultPt; - let totalChars = 0; - let cjkChars = 0; - for (const run of para.runs) { - if (run.sizePt > maxSize) maxSize = run.sizePt; - totalChars += run.text.length; - for (let i = 0; i < run.text.length; i++) { - const c = run.text.charCodeAt(i); - // CJK Unified Ideographs, Hiragana, Katakana, Hangul. - if ( - (c >= 0x3040 && c <= 0x309f) || - (c >= 0x30a0 && c <= 0x30ff) || - (c >= 0x4e00 && c <= 0x9fff) || - (c >= 0xac00 && c <= 0xd7af) - ) - cjkChars++; - } - } - if (totalChars === 0) totalChars = 1; - const cjkRatio = cjkChars / totalChars; - // Weighted average: CJK glyphs ≈ 1.0em wide, Latin ≈ 0.55em. - const glyphRatio = cjkRatio * 1.0 + (1 - cjkRatio) * AVG_GLYPH_W_RATIO; - const sizePx = maxSize * PX_PER_PT; - const charsPerLine = Math.max(1, Math.floor(innerWPx / Math.max(1, sizePx * glyphRatio))); - const lineCount = Math.max(1, Math.ceil(totalChars / charsPerLine)); - totalH += sizePx * LINE_HEIGHT * lineCount; - } - if (totalH > innerHPx) { - autoFitScale = Math.max(0.4, innerHPx / totalH); - } - } + const lineHeightScale = 1 - (authoredAutofit?.lnSpcReduction ?? 0); + // Apply line-height reduction by tightening per-line spacing. We // pass it through to renderRun via a closed-over factor. const effectiveLineHeight = LINE_HEIGHT * lineHeightScale;