Skip to content

BL-16627: improve BloomBridge process-book fix-up: auto-fit origami splits (both directions, nested stacks) and shrink oversized images - #8129

Closed
hatton wants to merge 5 commits into
masterfrom
BL-16627-shrink-image-pane
Closed

BL-16627: improve BloomBridge process-book fix-up: auto-fit origami splits (both directions, nested stacks) and shrink oversized images#8129
hatton wants to merge 5 commits into
masterfrom
BL-16627-shrink-image-pane

Conversation

@hatton

@hatton hatton commented Jul 30, 2026

Copy link
Copy Markdown
Member

Auto-fit of image/text origami splits gets two related improvements. Both are about the same
governing rules: never leave text overflowing, never waste space to no one's benefit, and always
measure the real browser layout rather than estimating font sizes.

1. Let auto-fit shrink the image pane, not just grow it (7bea4c7)

fitImageOverTextSplitOnPage() computed the right split and then discarded it whenever applying it
would make the image pane smaller. On a page whose illustration is wider than 4:3 the image pane is
bigger than the picture needs, the surplus is dead space, and the text below gets clipped — even
though the page has room to spare. Replaced the one-way ratchet with a small deadband so the divider
moves whenever it is worth moving in either direction.

2. Handle nested text/image/text stacks (e455270)

The function bailed on any nested split (// nested split: too complex, skip), which excluded one of
the most common page shapes there is: text above, an illustration, text below.

Nothing ever chose those dividers on purpose. An importer emits the shape as a right-nested chain of
horizontal splits, and with no explicit percentages Bloom's 50/50-per-split CSS default cascades into
an effective 50/25/25 — so the top text block gets half the page for its one line, the
illustration is squeezed into a quarter, and the last (usually longest) block is clipped.

What makes a top/bottom stack tractable is that every pane in it has the same width, so a text
block's required height doesn't depend on where any divider sits. Each text pane can therefore be
measured on its own — give it a size, ask OverflowChecker whether it overflows, binary-search the
boundary — and the answers add up. The illustration takes what the text doesn't need, capped at the
size where it already fills the width; the surplus past that goes back to the text panes in
proportion to what each needed.

Left/right stacks are deliberately excluded: there the panes have different widths, so they aren't
independent and the one-at-a-time measurement doesn't hold.

Still declined, as before: a split inside a first pane, mixed axes, a second pane holding a split
alongside other content, a pane that isn't cleanly just-a-picture or just-text, an illustration
carrying overlays, more than one illustration, and genuinely over-full pages (text that overflows
even with nearly the whole page, or blocks that together need more than the page has).

Measured result

FUV_Taale_Taale_Fulbe (Fulfulde, Nigeria; A5Portrait, marginBox 469x703), where 9 of the 11
content pages
are this shape. Every one sat at 351.5/175.75/175.75 px, and 9 pages had text
overflowing. Afterwards none do.

page before after
5 352 / 176 / 176 (clipped) 103 / 334 / 266
8 352 / 176 / 176 (clipped) 154 / 325 / 224
9 352 (clipped) / 176 / 176 490 / 152 / 62
11 352 / 176 / 176 (clipped) 238 / 101 / 364
14 352 / 176 / 176 (clipped) 87 / 397 / 219

Page 15 is text/text with no illustration, so it is out of scope and correctly left untouched (it is
genuinely over-full).

Testing

autoFitImageOverTextSplitsSpec.ts covers shape recognition and the nested percentage arithmetic —
including that unset splits read as 50/25/25 rather than thirds, which is the bug captured as an
assertion.

The measuring itself needs real geometry, which jsdom does not provide (OverflowSpec is skipped
there for the same reason). That half was verified by loading the real saved book, with its real
Bloom CSS, in Chromium and running fitImageOverTextSplits() against it — that is where the numbers
above come from.

Ref: https://issues.bloomlibrary.org/youtrack/issue/BL-16627

🤖 Generated with Claude Code

Devin review


This change is Reviewable

hatton and others added 3 commits July 29, 2026 11:27
fitImageOverTextSplitOnPage() computed the right split and then discarded it
whenever applying it would make the image pane smaller. On a page whose
illustration is wider than 4:3, the image pane is bigger than the picture needs,
the surplus is dead space, and the text below it gets clipped -- even though the
page has room to spare.

Measured case (A5Portrait, marginBox 469x703): the binary search found the text
needs 54%, the image-fit cap found the image needs only 31% (so the text could
safely have 69%), finalTextPercent landed on 69 -- and the only-grow guard threw
it away because 69 >= the authored 50. The text overflowed its box by 26px (~1.1
lines) while 135px of the image pane stood empty.

Replace that guard with a small deadband so the divider moves whenever it is
worth moving in either direction. Growing the text pane up to
textFloorForImageFit costs the illustration nothing -- past that point the image
is width-limited and renders at the same size either way. Deliberately no cap at
the authored split: when the text needs more than the image's fill point, the
image really does get smaller, because clipped text is a failure and a smaller
picture is a trade. The existing early-out still leaves a genuinely over-full
page (text overflowing even at 90%) untouched.

Checked before trusting a two-directional fit: computeImageFitFirstPanePercent
is stable (spread 0.0 points across pane sizes from 10% to 90% on every
qualifying page of a real book), so removing the one-way ratchet can't cause
oscillation. Over that book, the old version moved 0 pages and left one
overflowing; the new version moved 8, left alone the one page whose image
genuinely wants 49.8%, and no page overflows afterwards.

Also document the trap that made the image-fit cap useless on import:
getImageAspectRatio() reads the .bloom-backgroundImage box, which on a fresh
import is whatever the importer wrote (BloomBridge writes the full pane rect),
so we were reading the pane's aspect rather than the picture's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…o-pane pages

fitImageOverTextSplitOnPage() bailed on any nested split ("too complex, skip"),
which excluded one of the most common page shapes there is: text above, an
illustration, text below. Nothing ever chose those dividers on purpose. An
importer emits the shape as a right-nested chain of horizontal splits, and with
no explicit percentages Bloom's 50/50-per-split CSS default cascades into an
effective 50/25/25 -- so the top text block gets half the page for its one line,
the illustration is squeezed into a quarter, and the last (usually longest)
block is clipped. Auto-fit is precisely the code that should fix that, and it
declined to look.

Measured on FUV_Taale_Taale_Fulbe (Fulfulde, Nigeria, A5Portrait, marginBox
469x703), where 9 of the 11 content pages are this shape: every one sat at
351.5/175.75/175.75 px and 9 pages had text overflowing its box. After this
change 0 of them do. Page 8 goes from 352/176/176 (bottom text clipped
mid-line) to 154/325/224 -- the illustration nearly doubles and all the text
fits, which is about the proportion the printed source page uses.

Recognizing the shape: collectSplitStack() walks splits of ONE axis chained
down their second panes, each contributing its first pane as a slot and the
innermost contributing its second. Anything else nested is still declined -- a
split inside a FIRST pane, mixed axes, a second pane holding a split alongside
other content, a pane we can't classify as just-a-picture or just-text, an
illustration carrying overlays, or more than one illustration.

Fitting it: what makes a top/bottom stack tractable is that every pane in it is
the same WIDTH, so a text block's required height does not depend on where any
divider sits. Each text pane can therefore be measured on its own -- give it a
size, ask OverflowChecker whether it overflows, binary-search the boundary --
and the answers simply add up. Still measured, never estimated, same as the
two-pane path. The illustration takes what the text doesn't need, capped at the
size where it already fills the width; past that a taller pane is pure
whitespace, so the surplus goes back to the text panes in proportion to what
each needed. Left/right stacks are deliberately excluded: there the panes have
different widths, so the panes aren't independent and the one-at-a-time
measurement doesn't hold.

Governing rules are unchanged. A page whose text overflows even with nearly the
whole page to itself, or whose blocks together need more than the page has, is
over-full: there is no arrangement of these dividers that works, so we leave it
exactly as authored rather than producing something both clipped AND ugly.

Two details worth knowing:
- The illustration's fill-point cap is measured up front, while the stack is
  still laid out as it was saved. The probing deliberately squeezes whichever
  panes it isn't testing, including that one, so measuring later would read the
  picture's chrome out of a pane collapsed to its minimum.
- A page we end up declining is restored from its saved style attributes rather
  than by re-deriving percentages, so a pane that had no style keeps having
  none. Re-deriving would stamp an explicit "50%" onto pages we chose not to
  touch and put them in the saved HTML's diff for nothing.

Also de-duplicates the divider-writing and text-overflow helpers, which the
two-pane path now shares (no behavior change there).

The new spec covers shape recognition and the nested percentage arithmetic --
including that unset splits read as 50/25/25 rather than thirds, which is the
bug in one assertion. The measuring itself needs real geometry, which jsdom
does not provide (OverflowSpec is skipped there for the same reason), so it was
verified against this real book in Chromium instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hatton and others added 2 commits July 29, 2026 18:47
Devin flagged (informational) an asymmetry the nested-stack work introduced: the
new stack path restores a declined page from its saved style attributes, while
the two-pane path still wrote `setTextPercent(originalTextPercent)`. Those look
equivalent but aren't. Measuring lays the split out repeatedly, so by the time
either path decides a page is out of reach the styles have been rewritten; on a
split with no explicit percentage -- the common case, defaulting to 50% -- the
write-back leaves an explicit "50%" behind on a page we deliberately chose not
to touch, putting it in the saved HTML's diff for nothing.

Generalize the capture/restore helpers to take SplitConfig[] and use them in
both of the two-pane decline paths (over-full page, and move-not-worth-making).
No layout change: the restored geometry is identical either way, and process-book
just stops rewriting pages it left alone.

Also record, at computeImageFitPaneSizePx, why only the chrome term is
scale-divided (Devin's second informational note): it matches
computeImageFitFirstPanePercent and the reference getImagePercent() in
lib/split-pane/split-pane.ts, so changing it here alone would put this out of
step with the re-fit that runs afterward. Scale is 1 in the off-screen
processor, and at any other scale it can only skew the whitespace cap, never the
no-overflow guarantee, which comes from measuring real text.

Re-verified against FUV_Taale_Taale_Fulbe in Chromium: same fitted geometry as
before, 9 pages overflowing before the feature and 0 after (the one remaining is
a text/text page with no illustration, out of scope and genuinely over-full).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… does

Three comment-only corrections, no behavior change. Two of them were saying
things that are no longer true now that stacks are handled, and a reader who
trusted them would be misled about the feature's scope:

- captureContentForExternalProcessing (bloomEditing.ts) and ProcessBook's
  <summary> (BookProcessor.cs) both still described the feature as two-pane
  only, image-in-the-first-pane, and grow-only. Both now describe what it
  actually does: two-pane image-above-text and image-left-of-text, PLUS
  top-to-bottom stacks of three or more panes with one illustration and text in
  the rest, with each divider moving in either direction.

- getImageAspectRatio's caveat asserted that BloomBridge "emits the background
  element at the full PANE rect", which measurement contradicts: on this book it
  writes the pane's height by the picture's proportional width, giving an aspect
  of 0.7869 against the picture's own 975x1239 = 0.7869 exactly. So the caveat
  named the wrong culprit and implied the image-fit cap is useless on import
  when it isn't. Kept the warning -- an importer that DOES size the box to the
  pane would cost us whitespace, and that is worth knowing -- but stated it
  generically and recorded what BloomBridge was actually measured to do, so
  nobody plans around a false specific.

Also note in classifySlot why the overlay check is not redundant with the
`!textGroup` test that precedes it: that test already excludes text bubbles, so
the overlay check is what excludes image/video overlays. Devin flagged the pair
as apparently-redundant, which is exactly the misreading that would lead someone
to delete the check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hatton

hatton commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 5 from Hatton's machine during preflight]

Consulted Devin on 2026-07-29, up to commit 8833fb4f6b2ba1c6d3d3d823476d3bb4f866264d.

Devin ran on each of the three pushed commits and I waited for the job matching that exact sha
each time. The final pass is clean: 0 Bugs, 0 Investigate flags. Informational items only
throughout (3, then 6, then 7). Those aren't mirrored as review threads, but I read all of them,
and two were worth acting on:

  • "Two-pane decline path writes an explicit percent onto previously-unset splits" — fixed in
    496767bf2b. Measuring rewrites the split styles repeatedly, so a page we then decline had
    already been mutated; the old write-back stamped an explicit 50% onto splits that had no style
    at all. Both paths now restore the captured style attributes, so a page we choose not to touch
    stays out of the saved HTML's diff.
  • "Overlay-on-image rejection is masked by the translationGroup check" — the check isn't dead, so
    no code change; but Devin's reading is exactly the misreading that would lead someone to delete
    it, so 8833fb4f6b records why it's needed (the earlier test excludes text bubbles; this one
    excludes image/video overlays).

The rest were confirmations rather than concerns — notably that the leftover-redistribution and
over-full guards preserve the no-overflow invariant, and that the unit-mixing in the image-fit cap
matches the reference getImagePercent() in split-pane.ts and only ever skews the whitespace
cap, never the no-overflow guarantee.

Also green at this commit: CI (pr-automation, track / set-waiting), and the full BloomBrowserUI
test suite (586 passing, 5 skipped). CodeRabbit is configured but switched off in this repo
(.coderabbit.yml has auto_review.enabled: false), so it is not a pending reviewer.

Full preflight report, including the before/after page geometry and a few decisions for the
developer: https://raw.githack.com/BloomBooks/dev-process-artifacts/main/deciders/BloomDesktop-BL-16627-shrink-image-pane.html

@hatton hatton changed the title BL-16627: auto-fit origami splits in both directions, and for nested text/image/text stacks BL-16627: improve BloomBridge process-book fix-up: auto-fit origami splits (both directions, nested stacks) and shrink oversized images Aug 1, 2026
@hatton

hatton commented Aug 1, 2026

Copy link
Copy Markdown
Member Author

[Claude Fable 5 following a prompt from Hatton] Superseded by #8135: the branch was renamed from BL-16627-shrink-image-pane to BL-16627-process-book to match the card's broadened scope (the work now covers more of the process-book fix-up than the image-pane split), and a branch rename closes the PR that pointed at the old name. Same commits plus the new image-shrink step continue there.

@hatton hatton closed this Aug 1, 2026
@hatton
hatton deleted the BL-16627-shrink-image-pane branch August 1, 2026 16:33
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