Skip to content

feat(engines): paragraph layout and line breaking for the layout engine - #834

Merged
KnifeLemon merged 7 commits into
builtin-enginesfrom
engine/paragraph-layout
Oct 30, 2026
Merged

KnifeLemon merged 7 commits into
builtin-enginesfrom
engine/paragraph-layout

Conversation

@KnifeLemon

@KnifeLemon KnifeLemon commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

What does this change?

Implements the Project 4 item "Layout engine: paragraph layout and line breaking": a paragraph goes in, positioned and shaped lines come out. Everything is in src/Filee.Engines/Typesetting. No converter uses it yet; the render quality harness now measures it against rhwp (DocumentRenderers.Candidate is set).

  • Line breaking (LineBreaker.cs): UAX Add e-book conversion, a built-in HTML reader and a calibre package #14 at Unicode 16.0, rules LB1–LB31.
    • It passes all 16,672 cases of Unicode's LineBreakTest.txt. I checked that locally and didn't commit the file; LineBreakerTests covers each rule family instead.
    • Tailorings:
      • 한글 어절 (breakNonLatinWord KEEP_WORD) keeps Hangul words whole.
      • 글자 (BREAK_WORD) breaks between syllables.
      • Latin words can break anywhere (breakLatinWord BREAK_WORD).
      • A document's own characters forbidden at the start or end of a line (Word w:noLineBreaksBefore / After) are honoured.
  • Paragraph text (ParagraphText.cs): the inlines become one string: tabs, line breaks as U+2028, links, bookmarks, and a list marker in front.
    • Each character gets its style from its 한글 language slot (font, 장평, 자간, relative size, 글자 위치) or from Word's run font, scale, spacing and raise.
  • Layout (ParagraphLayout.cs, output in LineBox.cs):
    • Pipeline: items of one style, font, script and direction are shaped; lines are filled greedily and break at the last opportunity. Each line's parts are shaped again with their context.
    • Line fitting:
      • Spaces at a line end hang past it.
      • A word wider than the line breaks between clusters.
      • 최소 공백 (condense) lets the spaces shrink to fit.
      • lineWrap KEEP and SQUEEZE hold the paragraph on one line.
    • Alignment: left, right and centre; justify widens the spaces (or the clusters on a line with no spaces), and distribute (배분) widens the last line too. First-line and hanging indents.
    • Tabs: left, right, centre and decimal stops with leaders, measured from the left indent as 한글 does, with default stops every 40 pt. A hanging indent works as a stop, so a list marker hangs.
    • Line height:
      • 한글's percent, fixed, at-least and between-lines rules measure from the font size, with the baseline at 85% of the text height.
      • Word's multiple and 한글 "글꼴에 어울리는 줄 높이" use the fonts' OS/2 win metrics.
    • Characters: ratio, letter spacing, raise, super / subscript, colour, shade, underline, strike-through, links and bookmarks. Synthetic bold and italic are drawn through new GlyphRun.Embolden / SkewX.
    • Direction: a simplified bidi pass sets levels, and parts are reordered by rule L2. Runs stay in logical order in the page, so the PDF's text reads as written. Full UBA stays with the bidi item.
  • Model: HCharLayout gains FontSpace and Kerning, read from HWPX useFontSpace / useKerning.
    • With "글꼴에 어울리는 빈칸" off (as in our template), a space is half the font size wide.
    • With useKerning="0", the font's kerning is off.
    • These two settings made the line breaks match rhwp (below).
  • PDF writer fix: subset cmaps now keep only the characters the text has. CJK fonts map 文 and the Kangxi radical ⽂ to one glyph, and the ToUnicode table took the lower code point, so Japanese and Chinese text copied out as radicals.
  • Interim page flow (FileeRenderer.cs): sections' pages, paragraphs stacked with a new page when full, and headings as the PDF outline. Table cells are stacked as paragraphs for now. The page composer item replaces this flow.
  • Docs: a new section "Filee renderer: line breaking and paragraph layout" in ENGINES.md; RENDER-QUALITY.md updated.

Against 한글 itself

HangulLineBreakTests (a private-corpus test, run by build/render-compare.ps1) compares where 한글 broke lines with where Filee breaks them.

  • Input: HWPX files that 한글 saved. They store the start and width of every line (hp:linesegarray).
  • Method: each plain-text paragraph is laid out at the width 한글 used, with the file's shapes, and its line starts are compared with 한글's.

Result on the four HWPX files attached to Filee's issues (#49, #51, #65), kept out of the repository:

Matching 한글
Paragraphs 150 of 152 (98.7%)
Line starts 161 of 163 (98.8%)

The two that differ:

  • one uses a font that isn't installed (210 이솝우화);
  • one is a 75-character run of - wider than the line, which 한글 keeps on one line.

자간 is a share of each character's own width (장평 applied), not of the font size. A space or a narrowed character closes up less. Before this change, two lines with negative 자간 took one or two characters more than 한글's; now they break where 한글 broke them.

Reader fix: 한글 writes breakNonLatinWord the other way round from its names. BREAK_WORD is 어절 and KEEP_WORD is 글자. Two pieces of evidence:

  • In the binary HWP of the same documents, bit 7 of the paragraph shape (한글 단위; 1 = 글자) is set exactly where the HWPX says KEEP_WORD.
  • In plain KEEP_WORD paragraphs, 한글's stored lines break inside words 6 times in 9 ("외국인등록|번호", "공동이|용을").

HwpxReader now maps it that way; breakLatinWord agrees with its names.

Against rhwp (feature matrix, Windows fonts)

Feature Same lines Note
script-zh, script-ja, lists, tabs 100% same baselines too
line-spacing-* (5 alignments) 87.5% rhwp splits a word in the distributed paragraph
script-ko 60% rhwp splits some words even in 어절 paragraphs ("않습니|다.")
ar, he, hi, gu, th allowed rhwp draws these unshaped or at fixed widths; Filee's text now round-trips 100%
tables, headers-footers, notes, columns low left to the page composer

The thresholds in render-thresholds.json are today's values, each with its reason.

Also: the main sync brought Italian and Indonesian without error.document_too_complex (a builtin-engines key). Both now have it; that is what failed the first CI run.

Tests

  • LineBreakerTests: 34 cases.
  • ParagraphLayoutTests: 38 cases, with the pinned Noto fonts of build/fetch-fonts.ps1. Expected widths come from the shaper, so no magic numbers.
  • A FontSubsetterTests case for the cmap fix.
  • Engines suite locally: 2299 passed, 0 failed.

Not in this PR

These have their own board items.

🤖 Generated with Claude Code

KnifeLemon and others added 3 commits October 30, 2026 22:07
…절 and Latin tailorings

Passes all 16,672 cases of Unicode's LineBreakTest.txt (not kept in the repository).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t copy out as the radical ⽂

CJK fonts map both to one glyph and Skia's ToUnicode took the lower code point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…render harness

Itemising, shaping, greedy line breaking (UAX #14 with 한글 어절 / 글자), alignment with justify and distribute, indents,
tab stops with leaders, 한글 and Word line height rules, character spacing, ratio, raise, decorations and links, and a
simplified bidi. 한글's useFontSpace and useKerning are read into HCharLayout. An interim page flow
(Typesetting/FileeRenderer.cs) registers the renderer as DocumentRenderers.Candidate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KnifeLemon KnifeLemon added the built-in engines Filee: built-in engines (Project 4), on the builtin-engines branch, not released yet label Oct 30, 2026
@github-actions github-actions Bot added the feature New features label Oct 30, 2026
KnifeLemon and others added 4 commits October 30, 2026 23:10
…languages lacked on builtin-engines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re line breaks with 한글's own

한글 writes the setting the other way round from its names: the binary HWP of the same document has the 글자 bit set
where the HWPX says KEEP_WORD, and 한글's stored line layout breaks KEEP_WORD paragraphs inside words.
HangulLineBreakTests lays out the plain-text paragraphs of a private corpus at the width 한글 used and compares
their line starts with the ones 한글 stored (hp:linesegarray).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…font size

A space or a character narrowed by 장평 closes up less. With it, the two lines of the issue documents that took one or
two characters more than 한글's now break where 한글 broke them (150 of 152 plain paragraphs; the other two are a
font that isn't installed and an unbreakable run wider than the line).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and HWPX code now refers to

The document asks for the sentence in the product's interface, manuals, help and source: THIRD-PARTY-NOTICES.md (shown
on the About page), README, ENGINES.md and the files that use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KnifeLemon
KnifeLemon merged commit 6af44f0 into builtin-engines Oct 30, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

built-in engines Filee: built-in engines (Project 4), on the builtin-engines branch, not released yet feature New features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant