Skip to content

Geometry-accurate per-line text layout becomes auto mode - #243

Merged
Gnathonic merged 15 commits into
developfrom
feat/original-mode-line-coords
Jul 5, 2026
Merged

Geometry-accurate per-line text layout becomes auto mode#243
Gnathonic merged 15 commits into
developfrom
feat/original-mode-line-coords

Conversation

@Gnathonic

Copy link
Copy Markdown
Owner

Summary

mokuro's block-level font_size is the mean detected line-quad width — for vertical Japanese that includes furigana and mask slack, overstating the true character size by 20% at the median and 2× at p95 (measured across 120 volumes / 160k blocks). Rendering it faithfully overflows balloons; the old auto mode's measure-and-shrink workaround discarded line breaks and per-line positions.

The per-line quads (lines_coords) mokuro already emits are accurate, and they're stored verbatim in IndexedDB — no re-import needed. This PR renders each OCR line at its own quad with a geometry-derived font size, and makes that the auto mode (app default). original mode reverts to faithful raw rendering; blocks without lines_coords fall back to the legacy hover-fit auto.

Root-cause analysis with annotated evidence: https://claude.ai/code/artifact/611aa1cc-41ea-4186-ae7a-83542e5c4032

Layout algorithm (src/lib/reader/line-coords-layout.ts)

  • Per-line fit: min(quad_cross, quad_length ÷ measured text advance) (canvas measurer, heuristic fallback)
  • Uniform block size: ink-area-weighted median of clean lines — print keeps one size per balloon; ruby fragments can't outvote the base line
  • Merged base+furigana "lines" wrap into columns inside their quad (benefit-gated); whole-balloon single-line blocks wrap at their geometry optimum
  • Standalone furigana / deliberately-small lines keep their own size
  • Overlapping re-captures: text-subsumed duplicates collapse losslessly; diverged hallucination clusters partition their union bbox into reading-order bands so all OCR text stays readable
  • Block-level duplicate detections (same text, IoU > 0.5) dropped in all font modes
  • Hallucinated lines (text ≫ quad) stay contained instead of overflowing up to 90×

Verification

  • 829 tests pass (24 layout-module tests using real problem blocks from Dr Stone, Killing Bites, Saki the Succubus, JJK, Pokémon, FMA as fixtures)
  • Playwright end-to-end before/after screenshots on real pages
  • Corpus regression diff: 57k blocks re-laid-out against the pre-overlap-handling baseline — 99.06% bit-identical, all changes confined to blocks with the targeted overlap pathology

🤖 Generated with Claude Code

Gnathonic and others added 15 commits July 4, 2026 23:23
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mokuro's block font_size is the detected line-quad width (furigana
included), median 1.2x the true character size, so block-level original
rendering overflows. Per-line quads are accurate: place each line at its
quad and fit the font size to quad_length / text_advance, capped by quad
width. Falls back to the legacy path when lines_coords is missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quads are often wider than the glyph column (ruby, mask slack, empty
margin) with the base glyphs near the middle; left-edge anchoring shoved
wide-quad columns into their neighbors (Dr Stone 01 p27 本物から).
Center each line on the quad's cross axis instead.

The detector also emits ~19 duplicate blocks per 1000 pages: the same
balloon as a properly segmented block plus a single whole-box synthetic
line with a huge font_size (Dr Stone 01 p29). Dedupe at render time by
identical joined text + box IoU > 0.5, keeping the better-segmented
block; applies to all font modes and already-imported volumes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A quad much wider than the block's typical line size whose text only
fits far smaller is multiple print columns captured as one OCR line —
usually base text plus its furigana reading (Dr Stone 01 p32
空は私ならだいじょうぶ: 112px quad squeezed to 14.7px). Such lines now
wrap inside their full quad bbox at (near) the block's median line size.
Clean lines keep their own fitted size: print mixes sizes within a
balloon (emphasis words like 大丈夫), so uniformity is not forced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Print keeps one size per balloon; per-quad fitted sizes vary only via
quad slack, and mixed sizes look sloppy (user feedback: Dr Stone p32
大丈夫 is 1:1 with the base line, its tall quad is just loose). All
lines now render at the block's median clean-line size, tolerating
1.15x length / 1.2x cross quad slack. Escapes: deliberately-small lines
(standalone furigana) keep their own size, and wrapped merged-column
lines pull the whole block down with them so sizes stay uniform.
wrapFitSize now searches column counts exactly instead of iterating to
a suboptimal fixpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fixed 1.6x-reference quad-width wrap gate missed Dr Stone 01 p53
必要なことはう by 0.7px, leaving a merged line at a tiny 19px when two
columns at 31.5px fit. Gate on what wrapFitSize actually achieves
(>= 1.25x the single-line size) instead. And when >= 2 clean lines
agree within 1.25x, their median fixes the block size — a wrapped
line's lower fit no longer drags well-attested blocks down (it still
does for 0-1 clean-line blocks, which is what makes p32 land on the
true size).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…al size

A block whose only line is one quad covering the whole balloon (Dr
Stone 01 p87: 9 chars in 356x390) could never wrap: the reference size
derives from that same line, so the shrink gate never fired, leaving a
small single column in a huge box. When a block has no clean lines,
suspect lines now wrap at their geometry-optimal size (~sqrt(main x
cross / advance)) — p87 renders 3 columns at ~119px matching the print.
Still gated on the 1.25x benefit so genuine one-liners in loose quads
stay single. Hallucinated lines now wrap into dense contained columns
instead of rendering sub-pixel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Killing Bites 01 p42: 「百獣王」 with its katakana gloss split into two
small ruby lines around it — a plain per-line median is ruby-dominated
and dragged the 76px base text down to 31px. Lines now vote for the
block reference size proportionally to their quad ink area (the base
line holds 80% of the block's ink), so ruby fragments register as small
outliers and keep their own sizes while the base renders at its true
size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reconstruction supersedes the old measure-and-shrink auto mode and
is not a faithful raster either, so auto (the app default) is its
honest home. Blocks without lines_coords fall back to the legacy
hover-fit auto path; original mode reverts to raw font_size rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Saki the Succubus 02 p129: the detector re-captures the same ink region
as several overlapping lines — a column alone AND a bigger quad
spanning it plus its neighbors (あれは inside あれはキスではないですよ),
or nested hallucination blobs — which rendered stacked text. When one
line bbox covers >= 70% of a smaller one: if the smaller's text is
contained in the bigger's, hide the smaller (the bigger wraps the full
region, no text lost); otherwise hide the enclosing blob and keep the
precise small captures. Hidden lines don't vote on the block size and
don't render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hiding the enclosing blob of a hallucination cluster (Saki 02 p129)
filtered out most of the OCR text — unreadable in a new way. The user
requirement is that OCR text stays readable even when it is wrong:
diverged overlapping captures now merge into a cluster whose union bbox
is partitioned into reading-order bands weighted by text length; each
line wraps inside its own band. All text renders, ordered, nothing
stacks. Text-subsumed duplicates (あれは) still collapse losslessly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Killing Bites 01 p211: the detector assigned the middle column of one
balloon to the neighboring block (its quad slots exactly between the
victim block's columns). Per-line placement renders it at the correct
print position, but the overlapping block's whole-box white background
painted over it, hiding the text. In per-line mode the opaque backing
now lives on each line span — it still masks the printed glyphs under
the rendered text, and overlapping boxes can no longer blank out each
other's lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-line white strips only mask print they cover — on stylized, angled
or misaligned text (Saki 02 p129) the original ink bleeds around every
strip. Restore the whole-box white panel and fix the actual p211
problem, which was paint order: text boxes no longer create stacking
contexts (z-index removed; tree order still stacks smaller boxes over
bigger), and positioned line spans get z-index 1, lifting every block's
text above every block's panel. Overlapping blocks mask robustly and
can no longer blank out each other's lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mokuro-reader Building Building Preview, Comment Jul 5, 2026 5:40pm

Request Review

@Gnathonic
Gnathonic merged commit d869f6a into develop Jul 5, 2026
8 of 9 checks passed
adrian-tompkins pushed a commit to adrian-tompkins/mokuro-reader that referenced this pull request Aug 2, 2026
fix: Prevent scrollbars in reader by hiding document overflow
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