arrolio 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -2
- data/Rakefile +15 -0
- data/TODO.complete/56-generic-adapter-mece-decomposition.md +20 -0
- data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +92 -55
- data/TODO.complete/70-ttf-font-metrics-parity.md +68 -0
- data/TODO.complete/71-cover-layout-parity.md +45 -0
- data/TODO.complete/72-table-layout-parity.md +49 -0
- data/TODO.complete/73-toc-leaders-parity.md +55 -0
- data/TODO.complete/74-svg-image-rendering-parity.md +76 -0
- data/TODO.complete/75-mathml-formula-rendering-parity.md +52 -0
- data/TODO.complete/76-odd-even-page-templates-parity.md +50 -0
- data/TODO.complete/77-text-formatting-parity.md +55 -0
- data/TODO.complete/78-bibliography-formatting-parity.md +62 -0
- data/TODO.complete/79-line-breaking-parity.md +80 -0
- data/TODO.complete/80-parity-diff-harness-in-ci.md +68 -0
- data/TODO.complete/81-conditional-style-refinements.md +70 -0
- data/TODO.complete/82-semantic-presentation-dedup.md +65 -0
- data/TODO.complete/83-nested-list-support.md +74 -0
- data/TODO.complete/84-figure-svg-rendering.md +70 -0
- data/TODO.complete/85-locality-reference-formatting.md +48 -0
- data/TODO.complete/86-list-item-text-overflow.md +80 -0
- data/TODO.complete/87-odd-even-page-templates.md +55 -0
- data/TODO.complete/88-hyphenation-support.md +52 -0
- data/TODO.complete/89-table-rowspan.md +49 -0
- data/TODO.complete/90-mathml-stacked-layout.md +65 -0
- data/TODO.complete/91-parity-diff-diagnostics.md +50 -0
- data/TODO.complete/92-table-cell-height-parity.md +66 -0
- data/TODO.complete/93-figure-text-searchability.md +49 -0
- data/TODO.complete/94-fo-space-resolution.md +63 -0
- data/TODO.complete/95-title-header-placement.md +55 -0
- data/TODO.complete/96-pagination-drift-parity.md +517 -0
- data/TODO.complete/README.md +58 -38
- data/lib/arrolio/config_driven_pipeline.rb +45 -1
- data/lib/arrolio/content/document.rb +4 -3
- data/lib/arrolio/content/section.rb +3 -0
- data/lib/arrolio/content/table.rb +25 -14
- data/lib/arrolio/embedded_block_extractor.rb +54 -0
- data/lib/arrolio/engine/paged.rb +109 -28
- data/lib/arrolio/flavor/manifest.rb +0 -1
- data/lib/arrolio/flavor/registry.rb +1 -1
- data/lib/arrolio/flowable.rb +21 -0
- data/lib/arrolio/flowables/figure_flowable.rb +17 -0
- data/lib/arrolio/flowables/footnote_marker_flowable.rb +32 -13
- data/lib/arrolio/flowables/group_flowable.rb +57 -0
- data/lib/arrolio/flowables/list_flowable.rb +46 -18
- data/lib/arrolio/flowables/note_flowable.rb +84 -9
- data/lib/arrolio/flowables/page_sequence_start.rb +8 -4
- data/lib/arrolio/flowables/positioned_block.rb +55 -0
- data/lib/arrolio/flowables/rotated_text.rb +51 -0
- data/lib/arrolio/flowables/table_flowable.rb +303 -71
- data/lib/arrolio/flowables/text_flowable.rb +67 -11
- data/lib/arrolio/flowables/toc_line_flowable.rb +70 -7
- data/lib/arrolio/flowables.rb +4 -0
- data/lib/arrolio/font/embedder.rb +22 -3
- data/lib/arrolio/font_scanner.rb +133 -0
- data/lib/arrolio/generic_adapter/document_extraction.rb +101 -0
- data/lib/arrolio/generic_adapter/footnote_extraction.rb +81 -0
- data/lib/arrolio/generic_adapter/heading_extraction.rb +92 -0
- data/lib/arrolio/generic_adapter/inline_run_collection.rb +136 -0
- data/lib/arrolio/generic_adapter/list_conversion.rb +70 -0
- data/lib/arrolio/generic_adapter/metadata_extraction.rb +46 -0
- data/lib/arrolio/generic_adapter/table_conversion.rb +105 -0
- data/lib/arrolio/generic_adapter.rb +263 -317
- data/lib/arrolio/generic_flow_builder/bibliography.rb +47 -0
- data/lib/arrolio/generic_flow_builder/figures.rb +90 -0
- data/lib/arrolio/generic_flow_builder/lists.rb +66 -0
- data/lib/arrolio/generic_flow_builder/notes.rb +61 -0
- data/lib/arrolio/generic_flow_builder/sequences.rb +102 -0
- data/lib/arrolio/generic_flow_builder/tables.rb +28 -0
- data/lib/arrolio/generic_flow_builder/terms.rb +89 -0
- data/lib/arrolio/generic_flow_builder.rb +92 -187
- data/lib/arrolio/glyph_measurer.rb +16 -5
- data/lib/arrolio/inline_run.rb +2 -1
- data/lib/arrolio/layout_spec/page_template.rb +14 -8
- data/lib/arrolio/math_ml/inline_run_extractor.rb +272 -0
- data/lib/arrolio/math_ml.rb +7 -0
- data/lib/arrolio/output/page.rb +5 -4
- data/lib/arrolio/output/placed_box.rb +5 -2
- data/lib/arrolio/renderer/pdf/assets.rb +94 -0
- data/lib/arrolio/renderer/pdf/font_embedding.rb +77 -0
- data/lib/arrolio/renderer/pdf/metadata.rb +52 -0
- data/lib/arrolio/renderer/pdf.rb +140 -215
- data/lib/arrolio/style/definition.rb +7 -3
- data/lib/arrolio/style/registry.rb +6 -1
- data/lib/arrolio/table/auto_layout.rb +19 -10
- data/lib/arrolio/table/grid.rb +105 -0
- data/lib/arrolio/table.rb +1 -0
- data/lib/arrolio/text_layout/greedy.rb +15 -1
- data/lib/arrolio/text_layout/knuth_plass/breaker.rb +216 -92
- data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +31 -17
- data/lib/arrolio/text_layout/knuth_plass.rb +3 -0
- data/lib/arrolio/text_layout/line.rb +10 -2
- data/lib/arrolio/version.rb +1 -1
- data/lib/arrolio.rb +8 -4
- data/scripts/parity_check.rb +52 -0
- data/scripts/parity_diff.rb +89 -0
- data/scripts/xsl_profiles/standoc.yml +8 -2
- data/scripts/xsl_to_config.rb +28 -1
- metadata +91 -5
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
impact: medium
|
|
4
|
+
depends_on: [76]
|
|
5
|
+
layer: render
|
|
6
|
+
status: pending
|
|
7
|
+
est: 1d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The OIML reference PDF uses distinct page templates for odd vs even
|
|
13
|
+
pages:
|
|
14
|
+
- Odd pages: header right-aligned
|
|
15
|
+
- Even pages: header left-aligned (or centered when title_complementary
|
|
16
|
+
is present)
|
|
17
|
+
|
|
18
|
+
Our engine produces correct page-count parity but doesn't apply
|
|
19
|
+
distinct templates based on page number. The header alignment shifts
|
|
20
|
+
but the underlying template (margins, region extents) is the same.
|
|
21
|
+
|
|
22
|
+
## Current state
|
|
23
|
+
|
|
24
|
+
- `header_align_for(page_number)` in `Engine::Paged` returns `:left`
|
|
25
|
+
for even, `:right` for odd — SHIPPED and now REACHABLE
|
|
26
|
+
(2026-08-25): a nil `header_align` flows through
|
|
27
|
+
PageSequenceStart/Output::Page as "no flavor opinion"; the builder
|
|
28
|
+
no longer defaults to :right, and the OIML flavor dropped its
|
|
29
|
+
explicit `header_align: right` pins. Explicit values still win.
|
|
30
|
+
- Page templates: single `body` template, no odd/even variants.
|
|
31
|
+
|
|
32
|
+
## Approach
|
|
33
|
+
|
|
34
|
+
1. **Add odd/even template variants to LayoutSpec.** Each
|
|
35
|
+
`page_template` entry can specify `odd` and `even` sub-templates
|
|
36
|
+
with different margins, headers, footers.
|
|
37
|
+
|
|
38
|
+
2. **Wire PageSequenceMaster.** The XSL uses
|
|
39
|
+
`<xsl:conditional-page-master-reference>` with
|
|
40
|
+
`odd-or-even` condition. Map this to the engine's page-open logic.
|
|
41
|
+
|
|
42
|
+
3. **Apply template per page.** `Engine::Paged#open_page` should
|
|
43
|
+
select the template based on `page_number.odd?` / `page_number.even?`.
|
|
44
|
+
|
|
45
|
+
## Done-When
|
|
46
|
+
|
|
47
|
+
- [ ] Odd and even pages use different templates when configured
|
|
48
|
+
- [ ] Header alignment matches reference on all pages
|
|
49
|
+
- [ ] Blank pages inserted for even-page chapter starts
|
|
50
|
+
- [ ] Specs cover odd/even template selection
|
|
51
|
+
|
|
52
|
+
## Measurement
|
|
53
|
+
|
|
54
|
+
Minor visual impact; mostly affects header position consistency.
|
|
55
|
+
Last measured: 2026-08-08.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
impact: medium
|
|
4
|
+
depends_on: [70, 79]
|
|
5
|
+
layer: text
|
|
6
|
+
status: pending
|
|
7
|
+
est: 3d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Long words at line boundaries get pushed to the next line, leaving the
|
|
13
|
+
previous line underfull. FOP uses TeX hyphenation patterns to break
|
|
14
|
+
words at valid hyphenation points. Our KP breaker has no hyphenation.
|
|
15
|
+
|
|
16
|
+
## Impact
|
|
17
|
+
|
|
18
|
+
Visible on paragraphs with long technical terms:
|
|
19
|
+
- "characteristics" → pushes to next line
|
|
20
|
+
- "classification" → underfull line before
|
|
21
|
+
- "metrological" → gap at line end
|
|
22
|
+
|
|
23
|
+
## Approach
|
|
24
|
+
|
|
25
|
+
1. **Choose a hyphenation gem.** Options:
|
|
26
|
+
- `text-hyphen` — pure Ruby, TeX patgen patterns
|
|
27
|
+
- `ruby-hyphen` — C extension, faster
|
|
28
|
+
- Inline TeX pattern data file (no dependency)
|
|
29
|
+
|
|
30
|
+
2. **Integrate into ItemBuilder.** When a word exceeds remaining line
|
|
31
|
+
width, emit `Box + Penalty(flagged=true)` at each valid hyphenation
|
|
32
|
+
point. The KP breaker treats flagged penalties as valid break
|
|
33
|
+
opportunities with a demerit.
|
|
34
|
+
|
|
35
|
+
3. **Language-aware patterns.** Use the document's `language` metadata
|
|
36
|
+
to select the correct pattern file (en, fr, de, etc.).
|
|
37
|
+
|
|
38
|
+
4. **Minimum word length.** Don't hyphenate words shorter than 5 chars
|
|
39
|
+
(TeX default).
|
|
40
|
+
|
|
41
|
+
## Done-When
|
|
42
|
+
|
|
43
|
+
- [ ] Long words break at valid hyphenation points
|
|
44
|
+
- [ ] Hyphen character (`-`) rendered at break point
|
|
45
|
+
- [ ] Language detection from document metadata
|
|
46
|
+
- [ ] No overfull lines from hyphenation
|
|
47
|
+
- [ ] Specs cover hyphenation edge cases
|
|
48
|
+
|
|
49
|
+
## Measurement
|
|
50
|
+
|
|
51
|
+
Affects ~20-30 body paragraphs with long technical terms.
|
|
52
|
+
Last measured: 2026-08-08.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: [72]
|
|
5
|
+
layer: flowable
|
|
6
|
+
status: done
|
|
7
|
+
est: 2d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Table cells with `rowspan` (cells spanning multiple rows) were not
|
|
13
|
+
supported: covered cells shifted into the wrong columns (Table 5's
|
|
14
|
+
"Influence factor" column rendered under the p_LC column) and tall
|
|
15
|
+
table-cell text was crammed into one overfull line.
|
|
16
|
+
|
|
17
|
+
## Solution (2026-08-17)
|
|
18
|
+
|
|
19
|
+
- `Table::Grid` — occupancy grid over a Content::Table honoring
|
|
20
|
+
colspan AND rowspan. Answers cell placement (which column each
|
|
21
|
+
cell really occupies) and which rows are welded together by a
|
|
22
|
+
vertical span. Shared by AutoLayout (natural widths), TableFlowable
|
|
23
|
+
(row heights, placement, splitting).
|
|
24
|
+
- `TableFlowable` places every cell at its grid column; a rowspan
|
|
25
|
+
cell is drawn once in its start row with the combined spanned-row
|
|
26
|
+
height, `valign: middle` centers its content (Table 5's "1.0").
|
|
27
|
+
- Row heights: single-row cells drive their row; a spanning cell that
|
|
28
|
+
does not fit its span distributes the deficit equally over the
|
|
29
|
+
spanned rows.
|
|
30
|
+
- `do_split` splits only at welded-group boundaries — a rowspan cell
|
|
31
|
+
is never cut by a page break. When the first group does not fit
|
|
32
|
+
the remainder, no head is returned and the whole table moves to
|
|
33
|
+
the next page (FOP semantics).
|
|
34
|
+
- `Engine::Paged#place` propagates the page state after a
|
|
35
|
+
whole-flowable move (previously the caller kept filling the old
|
|
36
|
+
page and content fell out of document order).
|
|
37
|
+
|
|
38
|
+
## Done-When
|
|
39
|
+
|
|
40
|
+
- [x] `<td rowspan="2">` renders as a cell spanning 2 rows
|
|
41
|
+
- [x] Subsequent rows skip cells covered by rowspan
|
|
42
|
+
- [x] Cell content vertically centered in spanned area
|
|
43
|
+
- [x] Specs cover rowspan + colspan combinations
|
|
44
|
+
- [x] OIML Table 4 (rowspan header + colspan) and Table 5 (rowspan 9)
|
|
45
|
+
render correctly
|
|
46
|
+
|
|
47
|
+
## Measurement
|
|
48
|
+
|
|
49
|
+
Table 5's page similarity 38% → 86% (2026-08-17).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
impact: medium
|
|
4
|
+
depends_on: [75]
|
|
5
|
+
layer: flowable
|
|
6
|
+
status: in progress
|
|
7
|
+
est: 1d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
MathML stacked layout (mfrac, msqrt) renders flat.
|
|
13
|
+
|
|
14
|
+
## Measured nuance (2026-08-19)
|
|
15
|
+
|
|
16
|
+
Do NOT expand line height for every line carrying sub/superscript
|
|
17
|
+
runs: the reference keeps ordinary symbol sub/sups compact (the
|
|
18
|
+
terminology region's E_min/D_max/v_min lines are single-height and
|
|
19
|
+
were aligned). Only the parenthesized display-style expressions
|
|
20
|
+
(e.g. 5.3.2's ": (0.3 <= pLC <= 0.8)," after a colon) lay out stacked
|
|
21
|
+
- whole expression ~10pt below the text baseline, parens at the
|
|
22
|
+
text level, box two lines tall. A global 1.9x on math lines was
|
|
23
|
+
tested and overshoots the document by 2 pages (29 vs 28). The
|
|
24
|
+
discriminator between compact sub/sup and stacked expressions is in
|
|
25
|
+
the base XSL's stem handling (not the OIML extension) - find it
|
|
26
|
+
before implementing.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
MathML fractions (`<mfrac>`) render as "numerator denominator" on one
|
|
30
|
+
line instead of a stacked fraction. Other stacked elements (mroot,
|
|
31
|
+
msqrt) also need vertical layout.
|
|
32
|
+
|
|
33
|
+
## Current state
|
|
34
|
+
|
|
35
|
+
- MathML integration: SHIPPED (plurimath/mml)
|
|
36
|
+
- `ELEMENT_HANDLERS` registry: mi, mn, mo, msub, msup, msubsup, mfrac
|
|
37
|
+
- 10 specs covering basic elements
|
|
38
|
+
|
|
39
|
+
## Approach
|
|
40
|
+
|
|
41
|
+
1. **Stacked fraction flowable.** Create a `MathFractionFlowable` that
|
|
42
|
+
renders numerator centered above denominator with a horizontal rule.
|
|
43
|
+
|
|
44
|
+
2. **Vertical layout in renderer.** The renderer needs to handle
|
|
45
|
+
`Output::PlacedBox` with `data[:type] == :math_fraction`:
|
|
46
|
+
- Draw numerator at top
|
|
47
|
+
- Draw horizontal rule at middle
|
|
48
|
+
- Draw denominator at bottom
|
|
49
|
+
|
|
50
|
+
3. **Mroot/msqrt.** Render radical sign with content.
|
|
51
|
+
|
|
52
|
+
4. **Mtable.** Render simple matrices.
|
|
53
|
+
|
|
54
|
+
## Done-When
|
|
55
|
+
|
|
56
|
+
- [ ] `<mfrac>` renders as stacked fraction with dividing line
|
|
57
|
+
- [ ] `<msqrt>` renders with radical sign
|
|
58
|
+
- [ ] `<mroot>` renders with index
|
|
59
|
+
- [ ] Math content vertically centered in line
|
|
60
|
+
- [ ] Specs cover fraction, sqrt, root
|
|
61
|
+
|
|
62
|
+
## Measurement
|
|
63
|
+
|
|
64
|
+
Affects ~5 formula references in OIML body text.
|
|
65
|
+
Last measured: 2026-08-08.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: [80]
|
|
5
|
+
layer: harness
|
|
6
|
+
status: in_progress
|
|
7
|
+
est: 1d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The parity check reports overall similarity but doesn't show
|
|
13
|
+
per-element breakdowns. When a page has low similarity, we can't tell
|
|
14
|
+
if the issue is spacing, text content, image absence, or table layout.
|
|
15
|
+
|
|
16
|
+
## Current state
|
|
17
|
+
|
|
18
|
+
- `rake parity:check` renders and diffs — SHIPPED
|
|
19
|
+
- Per-page similarity: SHIPPED
|
|
20
|
+
- Per-element diff: NOT implemented
|
|
21
|
+
|
|
22
|
+
## Approach
|
|
23
|
+
|
|
24
|
+
1. **Visual diff overlay.** Generate a side-by-side PNG comparison
|
|
25
|
+
of each page (reference vs ours) with differences highlighted.
|
|
26
|
+
|
|
27
|
+
2. **Text diff per page.** Extract text from both PDFs per page and
|
|
28
|
+
show unified diff (added/removed lines).
|
|
29
|
+
|
|
30
|
+
3. **Element classification.** Tag each difference:
|
|
31
|
+
- `spacing` — same text, different position
|
|
32
|
+
- `content` — different text
|
|
33
|
+
- `missing` — element in reference but not in ours
|
|
34
|
+
- `extra` — element in ours but not in reference
|
|
35
|
+
|
|
36
|
+
4. **Parity leaderboard.** Track parity % per commit so we can see
|
|
37
|
+
which changes helped or hurt.
|
|
38
|
+
|
|
39
|
+
## Done-When
|
|
40
|
+
|
|
41
|
+
- [ ] `rake parity:diff PAGE=5` shows detailed diff for page 5
|
|
42
|
+
- [ ] Visual overlay PNGs generated to `tmp/parity_overlays/`
|
|
43
|
+
- [ ] Text diff shows exact added/removed words
|
|
44
|
+
- [ ] Element classification helps identify root cause
|
|
45
|
+
- [ ] CI reports parity delta on each PR
|
|
46
|
+
|
|
47
|
+
## Measurement
|
|
48
|
+
|
|
49
|
+
Critical for iterating on parity. Without this, fixes are guesswork.
|
|
50
|
+
Last measured: 2026-08-08.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: [89]
|
|
5
|
+
layer: flowable
|
|
6
|
+
status: done
|
|
7
|
+
est: 2d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Tables rendered with incorrect cell heights and padding (rows 16pt vs
|
|
13
|
+
the reference's 25.45pt pitch), table-cell footnotes inlined into the
|
|
14
|
+
header cell (inflating the header and the column widths), SVG figures
|
|
15
|
+
a third taller than the reference, and captions that could be
|
|
16
|
+
orphaned at a page bottom. Tables spanned the wrong pages and shifted
|
|
17
|
+
content on pages 19-27.
|
|
18
|
+
|
|
19
|
+
## Root causes (measured against mn2pdf v2.55, 2026-08-17)
|
|
20
|
+
|
|
21
|
+
1. The flavor XSL `table-row-style` sets `min-height: 8.3mm` — with
|
|
22
|
+
border/padding allowance the reference row pitch is 25.45pt. Our
|
|
23
|
+
rows had no floor (content + 4pt).
|
|
24
|
+
2. `<fn>` inside a table cell is a TABLE FOOTNOTE in the FOP model:
|
|
25
|
+
only a superscript marker stays in the cell, the body renders
|
|
26
|
+
below the table. The adapter inlined the whole body into the cell.
|
|
27
|
+
3. FOP sizes viewport-less SVGs (viewBox only) at CSS pixels → PDF
|
|
28
|
+
points (×72/96). We treated viewBox units as points.
|
|
29
|
+
4. `do_split` ignored the caption and repeated header rows in the
|
|
30
|
+
head budget, over-filling the page.
|
|
31
|
+
5. Table captions were separate flowables and could be orphaned.
|
|
32
|
+
|
|
33
|
+
## Solution
|
|
34
|
+
|
|
35
|
+
- Flavor-configurable table geometry in `flow_rules.yml`:
|
|
36
|
+
`table: { min_row_height, cell_padding, footnote_font_size }` —
|
|
37
|
+
OIML calibrated to 25.45 / 3.3 / 9.0 from the XSL + reference.
|
|
38
|
+
- Adapter: table-cell `<fn>` elements contribute a superscript marker
|
|
39
|
+
run to the cell (selector `footnote_marker`) and their bodies to
|
|
40
|
+
`Content::Table#footnotes`; the inline run collector takes an
|
|
41
|
+
`exclude:` selector so footnote text never leaks into cells.
|
|
42
|
+
- `TableFlowable` renders table footnotes below the last row (marker
|
|
43
|
+
+ body, 9pt, 10pt gap) and carries them across splits to the part
|
|
44
|
+
holding the final rows.
|
|
45
|
+
- Caption emitted inside `TableFlowable` (full caption on the head,
|
|
46
|
+
"Table N (continued)" on continuations) — never orphaned.
|
|
47
|
+
- SVG user-unit dimensions converted px → pt (×0.75) in both the
|
|
48
|
+
adapter (inline SVG viewBox) and the flow builder (external SVG).
|
|
49
|
+
- `Content::Table::Cell` gained `valign`; `align`/`valign` parsed
|
|
50
|
+
from the XML cell attributes.
|
|
51
|
+
|
|
52
|
+
## Done-When
|
|
53
|
+
|
|
54
|
+
- [x] Row pitch matches the reference (25.45pt floor from config)
|
|
55
|
+
- [x] Table footnotes render below the table with an in-cell
|
|
56
|
+
superscript marker
|
|
57
|
+
- [x] Figures render at FOP's px→pt scale
|
|
58
|
+
- [x] Caption + header + first row group budgeted in do_split
|
|
59
|
+
- [x] Continuation caption appears when a table spans pages
|
|
60
|
+
- [x] Specs cover footnote extraction, geometry, viewBox scaling
|
|
61
|
+
|
|
62
|
+
## Measurement
|
|
63
|
+
|
|
64
|
+
Overall similarity 62.7% → 64.07% (2026-08-17). Table region:
|
|
65
|
+
p19 34→61, p21 38→68, p22 42→61, p23 38→86.
|
|
66
|
+
Remaining drift documented in TODO 96.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
impact: medium
|
|
4
|
+
depends_on: [84]
|
|
5
|
+
layer: render
|
|
6
|
+
status: pending
|
|
7
|
+
est: 3d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
SVG figures rendered via rsvg-convert are rasterized to PNG. Text
|
|
13
|
+
inside the figures (labels, annotations) becomes pixels and is not
|
|
14
|
+
searchable in the PDF. The reference mn2pdf/FOP renders SVG as vector
|
|
15
|
+
content, preserving text searchability.
|
|
16
|
+
|
|
17
|
+
This accounts for ~1,500 characters of the remaining text content gap
|
|
18
|
+
(53,472 vs 55,534 reference). Figure labels like "Peripheral devices",
|
|
19
|
+
"Keyboard to operate", "Maximum capacity Emax", etc. are visible in
|
|
20
|
+
the reference's pdftotext output but absent from ours.
|
|
21
|
+
|
|
22
|
+
## Approach
|
|
23
|
+
|
|
24
|
+
1. **Vector SVG embedding.** Instead of rasterizing via rsvg-convert,
|
|
25
|
+
embed the SVG as a PDF XObject (Form XObject). This preserves text
|
|
26
|
+
as actual PDF text operators.
|
|
27
|
+
|
|
28
|
+
2. **Text overlay.** Alternatively, rasterize the SVG for visual
|
|
29
|
+
fidelity but add an invisible text layer with the SVG's text
|
|
30
|
+
content. The text layer is marked as invisible but searchable.
|
|
31
|
+
|
|
32
|
+
3. **SVG text extraction.** Parse the SVG XML to extract all `<text>`
|
|
33
|
+
elements with their positions. Use these to create searchable text
|
|
34
|
+
in the PDF at the corresponding coordinates.
|
|
35
|
+
|
|
36
|
+
Option 3 is the most pragmatic: it doesn't require vector rendering
|
|
37
|
+
support but makes figure text searchable.
|
|
38
|
+
|
|
39
|
+
## Done-When
|
|
40
|
+
|
|
41
|
+
- [ ] Figure text content appears in pdftotext output
|
|
42
|
+
- [ ] Text positions match visual figure positions
|
|
43
|
+
- [ ] No visual regression in figure rendering
|
|
44
|
+
- [ ] Text content gap reduced from ~2,000 to <500 chars
|
|
45
|
+
|
|
46
|
+
## Measurement
|
|
47
|
+
|
|
48
|
+
`pdftotext` char count comparison. Currently 53,472 vs 55,534.
|
|
49
|
+
Last measured: 2026-08-09.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
impact: critical
|
|
4
|
+
depends_on: [81]
|
|
5
|
+
layer: engine
|
|
6
|
+
status: done
|
|
7
|
+
est: 2d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Our engine ADDS consecutive flowable margins (space_after of previous
|
|
13
|
+
+ space_before of current). FOP/FO uses space RESOLUTION: takes the
|
|
14
|
+
MAX of the two, not the sum. This causes every spacing change to
|
|
15
|
+
cascade incorrectly.
|
|
16
|
+
|
|
17
|
+
## Evidence
|
|
18
|
+
|
|
19
|
+
The reference PDF has ~8pt paragraph spacing between consecutive body
|
|
20
|
+
paragraphs (measured from bbox: consecutive paragraph lines are 21.2pt
|
|
21
|
+
apart vs 13.2pt normal line height = 8pt extra).
|
|
22
|
+
|
|
23
|
+
Adding margin_bottom: 8 to the body style caused page explosion (27 →
|
|
24
|
+
33 pages) because the margin cascaded to ALL child styles and ALL
|
|
25
|
+
paragraphs (including those inside notes, lists, terms).
|
|
26
|
+
|
|
27
|
+
## Approach
|
|
28
|
+
|
|
29
|
+
1. **Implement FO space resolution in Engine::Paged.** When placing
|
|
30
|
+
flowable B after flowable A, the gap should be:
|
|
31
|
+
```
|
|
32
|
+
gap = max(A.space_after, B.space_before)
|
|
33
|
+
```
|
|
34
|
+
Not `A.space_after + B.space_before`.
|
|
35
|
+
|
|
36
|
+
2. **Track previous flowable's space_after.** The engine needs to know
|
|
37
|
+
the previous flowable's margin_bottom to resolve the space.
|
|
38
|
+
|
|
39
|
+
3. **Apply to section-level flowables only.** Flowables inside
|
|
40
|
+
containers (NoteFlowable, ListFlowable) handle their own internal
|
|
41
|
+
spacing. Space resolution applies between sibling flowables in the
|
|
42
|
+
main flow.
|
|
43
|
+
|
|
44
|
+
## Impact
|
|
45
|
+
|
|
46
|
+
With FO space resolution + body margin_bottom: 8:
|
|
47
|
+
- Standalone body paragraphs get 8pt gap (correct)
|
|
48
|
+
- Note body paragraphs still get 0 gap (correct — notes have their
|
|
49
|
+
own margin_bottom on the container)
|
|
50
|
+
- Heading-to-body gap: max(heading.space_after, body.space_before) =
|
|
51
|
+
max(12, 0) = 12pt (correct, no double-counting)
|
|
52
|
+
|
|
53
|
+
## Done-When
|
|
54
|
+
|
|
55
|
+
- [ ] Engine resolves consecutive flowable spaces via max()
|
|
56
|
+
- [ ] body margin_bottom: 8 produces correct inter-paragraph spacing
|
|
57
|
+
- [ ] No cascade to note/list/term internal paragraphs
|
|
58
|
+
- [ ] Overall parity > 58%
|
|
59
|
+
|
|
60
|
+
## Measurement
|
|
61
|
+
|
|
62
|
+
`bundle exec rake parity:check` — currently 53.99%.
|
|
63
|
+
Last measured: 2026-08-10.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: [71]
|
|
5
|
+
layer: engine
|
|
6
|
+
status: done
|
|
7
|
+
est: 2d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The document title block ("Part 1 - Metrological and technical
|
|
13
|
+
requirements") is rendered as BODY flow content, consuming ~58pt of
|
|
14
|
+
body region space on page 5. The reference (mn2pdf/FOP) renders it
|
|
15
|
+
in the HEADER area above the body region, getting it "for free".
|
|
16
|
+
|
|
17
|
+
## Evidence
|
|
18
|
+
|
|
19
|
+
Reference page 5:
|
|
20
|
+
- Title at y=71-95 (ABOVE body region)
|
|
21
|
+
- "1 Introduction" at y=130 (body region start)
|
|
22
|
+
|
|
23
|
+
Our page 5:
|
|
24
|
+
- Title at y=126-144 (INSIDE body region)
|
|
25
|
+
- "1 Introduction" at y=183 (pushed down by title)
|
|
26
|
+
|
|
27
|
+
Net effect: our page 5 has ~58pt less content than reference. Over
|
|
28
|
+
the document, this accumulates to ~1 page of offset.
|
|
29
|
+
|
|
30
|
+
## Approach
|
|
31
|
+
|
|
32
|
+
1. **Static content per page sequence.** The PageSequenceStart
|
|
33
|
+
flowable should support a `title_template` that renders in the
|
|
34
|
+
header region on the first page of the sequence.
|
|
35
|
+
|
|
36
|
+
2. **Renderer support.** The renderer draws the title at a fixed
|
|
37
|
+
position in the header area (y = margin_top to body_top) when the
|
|
38
|
+
page is the first of a sequence with a title template.
|
|
39
|
+
|
|
40
|
+
3. **Flow builder change.** `append_title_block` should NOT emit a
|
|
41
|
+
body TextFlowable. Instead, pass the title to the
|
|
42
|
+
PageSequenceStart flowable.
|
|
43
|
+
|
|
44
|
+
## Done-When
|
|
45
|
+
|
|
46
|
+
- [ ] Title renders above body region (y < 126 on A4 with 26.5mm/18mm)
|
|
47
|
+
- [ ] Body content starts at same y as reference (y=130 for heading)
|
|
48
|
+
- [ ] Page 5 contains Introduction + section 2 content (matching ref)
|
|
49
|
+
- [ ] Overall parity > 65%
|
|
50
|
+
|
|
51
|
+
## Measurement
|
|
52
|
+
|
|
53
|
+
`bundle exec rake parity:check` — currently 62.7%. Page 5: 99.6%,
|
|
54
|
+
page 6: 66.7%, page 7: 50.0% (mostly empty due to figure push).
|
|
55
|
+
Last measured: 2026-08-16.
|