From 5bef819f8a4e0c5175abcdb644a2470c9ba57e3d Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Tue, 9 Jun 2026 01:22:35 +0100 Subject: [PATCH] perf(engine): binary-search auto-size font fitting; deprecate unused adjustFontSizeToFit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-size paragraphs (.autoSize(...)) resolved the font size by scanning every step from max down to min, re-measuring the line at each candidate (up to ~50 measures). Line width is linear in font size so the fit is monotonic; binary-search the grid for the same boundary in ~log2(n) measures instead of n. Byte-identical: returns the same grid size the linear scan did (covered by the existing auto-size integration and snapshot tests). Also deprecate Font.adjustFontSizeToFit (+ the PdfFont/WordFont implementations): unused and incorrect — the PdfFont impl re-measured with the unchanged style, so it always returned the minimum size. Kept for binary compatibility; canonical auto-size is the layout compiler path above. Dropped a dead local while there. Finding 12. --- CHANGELOG.md | 18 +++++++++++++ .../document/layout/TextFlowSupport.java | 27 ++++++++++++++----- .../com/demcha/compose/engine/font/Font.java | 15 ++++++++++- .../compose/engine/render/pdf/PdfFont.java | 14 ++++++++-- .../compose/engine/render/word/WordFont.java | 11 +++++--- 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da2d5e260..7c73db801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,24 @@ Open cycle — bug-fix / housekeeping. Entries land here as they merge. large table this removes the dominant per-cell layout allocation. No public API or behaviour change. +- **Auto-size font fitting binary-searches the size grid.** A paragraph with + `autoSize(...)` resolved its font size by scanning every step from max down to + min, re-measuring the line at each candidate (up to ~50 measurements). Line width + is linear in font size, so the fit is monotonic — the search now binary-searches + the grid for the same boundary in ~log2(n) measurements instead of n. **Output is + byte-identical** — it returns the same grid size the linear scan did (covered by + the existing auto-size integration and snapshot tests). No public API or behaviour + change. + +### Deprecations + +- **`Font.adjustFontSizeToFit(...)` is deprecated.** The engine-internal + `Font#adjustFontSizeToFit` (and its `PdfFont` / `WordFont` implementations) is + unused and incorrect — the only real implementation re-measured with the + unchanged style, so it always returned the minimum size. Canonical auto-size is + resolved by the layout compiler. The method is kept for binary compatibility and + scheduled for removal in the next major. + ### Tests / tooling - **Benchmark regression gate and measurement probe (benchmarks module, not part diff --git a/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java b/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java index 0da49991a..7155f88f7 100644 --- a/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java +++ b/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java @@ -585,15 +585,30 @@ private static DocumentTextStyle resolveAutoSizeTextStyle(ParagraphNode node, double minSize = autoSize.minSize(); double step = Math.max(0.1, autoSize.step()); - // Single-line text: shrink the font size until the longest logical line - // measures inside the available inner width, otherwise fall back to the - // smallest configured size. - for (double size = maxSize; size >= minSize - 1e-6; size -= step) { - DocumentTextStyle candidate = baseStyle.withSize(size); + // Single-line text: pick the largest grid size (maxSize, maxSize-step, …, + // down to >= minSize) whose longest logical line measures inside the + // available inner width, otherwise fall back to the smallest configured + // size. The fit predicate is monotonic in size (line width is linear in + // size), so binary-search the grid for the boundary instead of measuring + // at every step — the same size the linear scan returned, in ~log2(n) + // measurements rather than n. + int maxStepCount = (int) Math.floor((maxSize - minSize + 1e-6) / step); + int lo = 0; + int hi = maxStepCount; + int fitStep = -1; + while (lo <= hi) { + int mid = (lo + hi) >>> 1; + DocumentTextStyle candidate = baseStyle.withSize(maxSize - mid * step); if (paragraphFitsSingleLine(node, candidate, innerWidth, measurement)) { - return candidate; + fitStep = mid; // fits — try a larger size (fewer steps down) + hi = mid - 1; + } else { + lo = mid + 1; // too wide — need a smaller size (more steps) } } + if (fitStep >= 0) { + return baseStyle.withSize(maxSize - fitStep * step); + } return baseStyle.withSize(minSize); } diff --git a/src/main/java/com/demcha/compose/engine/font/Font.java b/src/main/java/com/demcha/compose/engine/font/Font.java index d6e87b8d3..a64a214df 100644 --- a/src/main/java/com/demcha/compose/engine/font/Font.java +++ b/src/main/java/com/demcha/compose/engine/font/Font.java @@ -46,7 +46,20 @@ default T fontType(TextDecoration textDecoration) { double scale(double size); - public TextStyle adjustFontSizeToFit(String text, TextStyle style, double availableWidth); + /** + * @param text the text to fit + * @param style the starting style + * @param availableWidth the width to fit within + * @return a re-sized style + * @deprecated Unused and incorrect: the only real implementation re-measures + * with the unchanged {@code style}, so the loop never converges and the + * result is always the minimum size. Canonical auto-size is resolved by the + * layout compiler ({@code TextFlowSupport.resolveAutoSizeTextStyle}); this + * method has no callers and is kept only for binary compatibility, + * scheduled for removal in the next major. + */ + @Deprecated + TextStyle adjustFontSizeToFit(String text, TextStyle style, double availableWidth); ContentSize getTightBounds(String text, TextStyle style); } diff --git a/src/main/java/com/demcha/compose/engine/render/pdf/PdfFont.java b/src/main/java/com/demcha/compose/engine/render/pdf/PdfFont.java index f84eb24b3..09386ce4d 100644 --- a/src/main/java/com/demcha/compose/engine/render/pdf/PdfFont.java +++ b/src/main/java/com/demcha/compose/engine/render/pdf/PdfFont.java @@ -29,7 +29,18 @@ public PdfFont(PDFont defaultFont, PDFont bold, PDFont italic, PDFont boldItalic } - // Adjust font size automatically based on the width of the text and available space + /** + * @param text the text to fit + * @param style the starting style + * @param availableWidth the width to fit within + * @return a re-sized style + * @deprecated Unused and incorrect — it re-measures with the unchanged + * {@code style}, so {@code textWidth} never shrinks and the loop runs to the + * minimum size regardless of fit. Canonical auto-size is handled by the + * layout compiler; kept only for binary compatibility. + */ + @Deprecated + @Override public TextStyle adjustFontSizeToFit(String text, TextStyle style, double availableWidth) { double textWidth = getTextWidth(style, text); @@ -39,7 +50,6 @@ public TextStyle adjustFontSizeToFit(String text, TextStyle style, double availa newSize--; // Reduce size textWidth = getTextWidth(style, text); // Recalculate text width } - PDFont pdFont = fontType(style.decoration()); return new TextStyle(style.fontName(), newSize, style.decoration(), style.color()); } diff --git a/src/main/java/com/demcha/compose/engine/render/word/WordFont.java b/src/main/java/com/demcha/compose/engine/render/word/WordFont.java index db35a7b9f..2e3e93941 100644 --- a/src/main/java/com/demcha/compose/engine/render/word/WordFont.java +++ b/src/main/java/com/demcha/compose/engine/render/word/WordFont.java @@ -83,11 +83,14 @@ public double scale(double size) { } /** - * @param text - * @param style - * @param availableWidth - * @return + * @param text the text to fit + * @param style the starting style + * @param availableWidth the width to fit within + * @return {@code null} — never implemented + * @deprecated Unused; never implemented (returns {@code null}). Kept only for + * binary compatibility. See {@code Font#adjustFontSizeToFit}. */ + @Deprecated @Override public TextStyle adjustFontSizeToFit(String text, TextStyle style, double availableWidth) { return null;