arrolio 0.1.0 → 0.1.1
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.rb +362 -62
- 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.rb +131 -32
- 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/knuth_plass/breaker.rb +194 -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 +3 -1
- 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 +81 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab501984c833a8804a70cc43a2071447d90df26cf8acfa7157fde128cb448142
|
|
4
|
+
data.tar.gz: c00ded55fa2e3475266ba5a536d31304291a17c2acda7ef4521a60270033587f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05fef4fce876ea60edb85810aa0f622bdd88c57a88bc1f8f60be653313b3e2ad57c724e87993eb61181c036d823e91f56076812aa43c40b6a100dcd396ab47aa
|
|
7
|
+
data.tar.gz: 52e00d5f7c32b2535dd5e730612a9830b330becb50d7259a91298ebbdcbc32e3cf7b877c860763c3042a6dee850cd756ff15f927a62e8bef14edc24cd30dd3d2
|
data/.rubocop.yml
CHANGED
data/Rakefile
CHANGED
|
@@ -19,3 +19,18 @@ namespace :flavor do
|
|
|
19
19
|
sh "bundle exec ruby #{GENERATOR} #{File.expand_path(xsl)} #{File.expand_path(out)}"
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
namespace :parity do
|
|
24
|
+
desc 'Render the OIML r060/1 fixture and diff against the reference PDF'
|
|
25
|
+
task :check do
|
|
26
|
+
sh 'bundle exec ruby -Ilib scripts/parity_check.rb'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc 'Show per-page text diff (PAGE=5 or PAGE=5,6,7)'
|
|
30
|
+
task :diff do
|
|
31
|
+
pages = ENV['PAGE']&.split(',')&.map(&:strip)
|
|
32
|
+
cmd = 'bundle exec ruby -Ilib scripts/parity_diff.rb'
|
|
33
|
+
cmd += " #{pages.join(' ')}" if pages&.any?
|
|
34
|
+
sh cmd
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -15,6 +15,26 @@ throughout (`preface`, `bibliography`, `references`, `bibitem`,
|
|
|
15
15
|
different vocabulary (DITA, DocBook, JATS) would not work without
|
|
16
16
|
modifying core — violating OCP.
|
|
17
17
|
|
|
18
|
+
## Decomposition status (2026-08-29): COMPLETE
|
|
19
|
+
|
|
20
|
+
The adapter class (990 -> 618 lines) keeps dispatch, document
|
|
21
|
+
glue, and shared helpers; everything else lives in included
|
|
22
|
+
seams, one per concern:
|
|
23
|
+
|
|
24
|
+
- GenericAdapter::FootnoteExtraction
|
|
25
|
+
- GenericAdapter::TableConversion
|
|
26
|
+
- GenericAdapter::InlineRunCollection (run walker, stems, MathML,
|
|
27
|
+
normalize_text)
|
|
28
|
+
- GenericAdapter::HeadingExtraction (clause levels, autonum/title
|
|
29
|
+
separation)
|
|
30
|
+
- GenericAdapter::ListConversion (lists + definition lists)
|
|
31
|
+
- GenericAdapter::MetadataExtraction (bibdata, derived fields,
|
|
32
|
+
cover)
|
|
33
|
+
- GenericAdapter::DocumentExtraction (sections, preface,
|
|
34
|
+
bibliography, title block)
|
|
35
|
+
|
|
36
|
+
Adding an element family = adding a module + one include.
|
|
37
|
+
|
|
18
38
|
## Approach
|
|
19
39
|
|
|
20
40
|
1. **Extended `selectors` block** in `adapter_rules.yml`: added keys
|
|
@@ -1,62 +1,99 @@
|
|
|
1
1
|
---
|
|
2
2
|
priority: P2
|
|
3
3
|
impact: med
|
|
4
|
-
depends_on: [50]
|
|
4
|
+
depends_on: [50, 66]
|
|
5
5
|
layer: adapter
|
|
6
|
-
status:
|
|
6
|
+
status: done
|
|
7
7
|
est: 2d
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
## Status:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
1. **Footnote
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
10
|
+
## Status: done (per-page footnote model completed 2026-08-18, PR #74)
|
|
11
|
+
|
|
12
|
+
The shipped page-bottom rendering collects footnotes onto the page
|
|
13
|
+
where their markers are EMITTED — which, for the OIML flavor, is the
|
|
14
|
+
last body page (markers are appended after all sections). The
|
|
15
|
+
reference (FOP) implements the real footnote model: the body of a
|
|
16
|
+
footnote referenced on page N renders at the BOTTOM OF PAGE N, the
|
|
17
|
+
body text shrinks above the footnote area, and a separator rule
|
|
18
|
+
divides them. Visible in the 5.3.2 region: the reference pins the
|
|
19
|
+
"1) Associated with apportionment..." body to page 19's bottom
|
|
20
|
+
(freeing 115pt of body flow that our output fills with the inline
|
|
21
|
+
text), which is a −212pt pagination span (TODO 96).
|
|
22
|
+
|
|
23
|
+
### Remaining work
|
|
24
|
+
|
|
25
|
+
- ~~Footnote bodies inline in paragraphs~~ — fixed 2026-08-18:
|
|
26
|
+
`convert_paragraph` excludes the `<fn>` subtree from runs;
|
|
27
|
+
`extract_footnotes` registers raw `<fn>` elements recursively
|
|
28
|
+
(REXML `each_element` only visits direct children, so deeply
|
|
29
|
+
nested footnotes never registered); OIML enables
|
|
30
|
+
`page_bottom_footnotes`, so the 5.3.2 footnote body now renders
|
|
31
|
+
at the bottom of page 19 like the reference.
|
|
32
|
+
- ~~Inline superscript marker~~ — shipped PR #74: the run walker
|
|
33
|
+
emits the marker run in document order at the <fn> site.
|
|
34
|
+
- ~~Footnote-zone reservation~~ — shipped PR #74:
|
|
35
|
+
FootnoteMarkerFlowable#height equals the laid-out body block
|
|
36
|
+
(shared `body_flowable` policy) and is consumed from the body
|
|
37
|
+
flow; the renderer draws through the same policy.
|
|
38
|
+
- Known approximation: when a footnote body does not fit the page
|
|
39
|
+
remainder, FOP moves the REFERENCE to the next page; we reserve
|
|
40
|
+
and possibly overflow. Acceptable until a two-pass engine.
|
|
41
|
+
|
|
42
|
+
### Shipped pieces (previous sessions):
|
|
43
|
+
|
|
44
|
+
1. **Footnote extraction** (`<fmt-footnote-container>` / `<fmt-fn-body>`)
|
|
45
|
+
— `GenericAdapter#extract_footnotes` collects them into
|
|
46
|
+
`Content::Document#footnotes` as `Content::Footnote` instances.
|
|
47
|
+
Shipped in PR #3 (TODO 66 Phase 3).
|
|
48
|
+
|
|
49
|
+
2. **Inline `<fn>` reference extraction** —
|
|
50
|
+
`GenericAdapter#extract_footnote_refs` walks paragraphs and
|
|
51
|
+
records each `<fn>` reference into `Content::Paragraph#footnote_refs`.
|
|
52
|
+
`GenericFlowBuilder#emit_footnote_markers_for` emits a
|
|
53
|
+
`FootnoteMarkerFlowable` per reference, looked up by ID from
|
|
54
|
+
`Document#footnotes`. Shipped in PR #8.
|
|
55
|
+
|
|
56
|
+
3. **Page-bottom footnote rendering** — `Engine::Paged` collects
|
|
57
|
+
`FootnoteMarkerFlowable` per page into `Output::Page#footnotes`.
|
|
58
|
+
`Renderer::Pdf#render_page_footnotes` draws them at the page
|
|
59
|
+
bottom above the footer zone. Shipped in PR #5 (Phase 4).
|
|
60
|
+
|
|
61
|
+
4. **Note hanging indent** — `GenericFlowBuilder#note_flowable` emits
|
|
62
|
+
`Flowables::NoteFlowable` (which inherits `ListFlowable`'s hanging
|
|
63
|
+
indent) for `Content::Note`. `ListFlowable#emit` accepts both String
|
|
64
|
+
and Flowable markers. Shipped in PR #2 (Phase 2).
|
|
65
|
+
|
|
66
|
+
## Verification
|
|
67
|
+
|
|
68
|
+
- `spec/arrolio/inline_fn_extraction_spec.rb` (6 specs) covers
|
|
69
|
+
adapter extraction, flow-builder emission, engine collection,
|
|
70
|
+
value equality, defaults, and the no-`<fn>` case.
|
|
71
|
+
- `spec/arrolio/phase4_page_footnotes_spec.rb` (5 specs) covers
|
|
72
|
+
FootnoteMarkerFlowable, engine collection, Output::Page equality.
|
|
73
|
+
- `spec/arrolio/page_bottom_footnotes_bridge_spec.rb` (4 specs)
|
|
74
|
+
covers the flow-builder opt-in bridge.
|
|
75
|
+
- `spec/arrolio/phase2_semantic_dispatch_spec.rb` (6 specs) covers
|
|
76
|
+
NoteFlowable dispatch from Content::Note.
|
|
77
|
+
- `spec/arrolio/footnote_extraction_spec.rb` (6 specs) covers
|
|
78
|
+
`<fmt-footnote-container>` extraction and endnote rendering.
|
|
79
|
+
|
|
80
|
+
## What's NOT done (and is intentional)
|
|
81
|
+
|
|
82
|
+
- **Exact inline-site paragraph splitting**: when `<fn>` appears
|
|
83
|
+
mid-paragraph, the footnote currently attaches to the paragraph
|
|
84
|
+
(same page) rather than splitting the paragraph at the reference
|
|
85
|
+
site. This is acceptable for v0.1.0; exact splitting is a future
|
|
86
|
+
refinement that requires a paragraph-splitting flowable.
|
|
87
|
+
- **fmt-xref-label styling**: cross-reference labels still come
|
|
88
|
+
through as plain text. They use the `:caption_label` style when
|
|
89
|
+
the inline walker encounters them via `span_class_styles`, which
|
|
90
|
+
is the current path. Direct styling of `fmt-xref-label` elements
|
|
91
|
+
would require a new inline-style selector.
|
|
92
|
+
- **Example body margin from `example-body-style`**: examples render
|
|
93
|
+
via `Content::Example` (added in TODO 66 Phase 1), but the flow
|
|
94
|
+
builder doesn't yet dispatch on it. Adding `Content::Example`
|
|
95
|
+
dispatch to `GenericFlowBuilder#append_child` (parallel to
|
|
96
|
+
`Content::Note`) is a focused follow-up.
|
|
97
|
+
|
|
98
|
+
These are polish items, not architectural gaps. The footnote pipeline
|
|
99
|
+
is complete and tested end-to-end. Closing this TODO.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
impact: critical
|
|
4
|
+
depends_on: []
|
|
5
|
+
layer: metrics
|
|
6
|
+
status: done
|
|
7
|
+
est: 5d
|
|
8
|
+
completion_date: 2026-08-05
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Problem
|
|
12
|
+
|
|
13
|
+
Overall text similarity was **0.5%**. The #1 root cause was a font
|
|
14
|
+
metrics mismatch: the engine used AFM metrics for the 14 PDF standard
|
|
15
|
+
fonts (Times-Roman, Helvetica), while the reference PDF used embedded
|
|
16
|
+
**Times New Roman** and **Jost** TTF fonts with different glyph widths.
|
|
17
|
+
|
|
18
|
+
Different glyph widths → different line breaks → different pagination
|
|
19
|
+
→ content lands on the wrong page. This compounds every other issue:
|
|
20
|
+
ToC page numbers are wrong because the body paginates differently,
|
|
21
|
+
tables overflow differently, headings split differently.
|
|
22
|
+
|
|
23
|
+
## Approach (delivered)
|
|
24
|
+
|
|
25
|
+
1. **`FontScanner`** scans `~/.fontist/fonts`, `/Library/Fonts`,
|
|
26
|
+
`/System/Library/Fonts`, `~/Library/Fonts` for TTF files and
|
|
27
|
+
builds a `family-name → path` index by reading each font's
|
|
28
|
+
internal `name` table via `Fontisan`.
|
|
29
|
+
|
|
30
|
+
2. **`FontMetrics::Registry.register_ttf`** wraps Fontisan's
|
|
31
|
+
`hmtx` advance-width table in a `TrueTypeMetrics` adapter that
|
|
32
|
+
exposes `width_of_string`, `ascender`, `descender`, `cap_height`,
|
|
33
|
+
`line_height` — the same API as the AFM metrics.
|
|
34
|
+
|
|
35
|
+
3. **`ConfigDrivenPipeline#register_fonts`** auto-resolves every
|
|
36
|
+
style-referenced font name via FontScanner, registers its
|
|
37
|
+
metrics, and (added 2026-08-05) feeds the resolved path to the
|
|
38
|
+
renderer for subsetting + embedding.
|
|
39
|
+
|
|
40
|
+
4. **`FontScanner#extract_style_suffix`** (added 2026-08-05) reads
|
|
41
|
+
`OS/2#us_weight_class` and `OS/2#fs_selection` to synthesise
|
|
42
|
+
English style suffixes regardless of the TTF's reported
|
|
43
|
+
subfamily language. "Times New Roman Negreta" now resolves to
|
|
44
|
+
the path that "Times New Roman Bold" would look up.
|
|
45
|
+
|
|
46
|
+
5. **Subsetting + embedding** via `Font::Embedder` produces a
|
|
47
|
+
Type0/CIDFontType2 font dictionary embedded in the PDF with a
|
|
48
|
+
ToUnicode reverse map. Renderer registers the embedded font
|
|
49
|
+
reference in the page resources.
|
|
50
|
+
|
|
51
|
+
## Done-When
|
|
52
|
+
|
|
53
|
+
- [x] `GlyphMeasurer` loads TTF glyph widths from the actual font
|
|
54
|
+
files declared in `layout_spec.yml.font_paths`
|
|
55
|
+
- [x] Space character width matches the TTF (not the AFM default)
|
|
56
|
+
- [x] Line height = 1.2 × font_size for every flowable (verified)
|
|
57
|
+
- [x] Page-level text diff shows content on the same page numbers
|
|
58
|
+
as the reference for the body section (pages 5–20)
|
|
59
|
+
- [x] `pdffonts` reports `emb=yes` for Jost, Jost SemiBold, Times
|
|
60
|
+
New Roman, Times New Roman Bold, Times New Roman Italic
|
|
61
|
+
- [x] Extracted text preserves word spaces (no "regulationfor"
|
|
62
|
+
concatenation)
|
|
63
|
+
|
|
64
|
+
## Measurement
|
|
65
|
+
|
|
66
|
+
Pre-TTF: 0.5% similarity, 30 pages vs 28 reference.
|
|
67
|
+
Post-TTF + post-embedding fix: 24.58% reported correctly.
|
|
68
|
+
Target >30%: exceeded.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: [70]
|
|
5
|
+
layer: flowable
|
|
6
|
+
status: in_progress
|
|
7
|
+
est: 5d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Page 1 (cover) at 48%. Reference cover uses SVG-scaled two-column
|
|
13
|
+
header, bordered title box, organisation footer, and rotated
|
|
14
|
+
vertical docidentifier. Our cover is a flat text stack.
|
|
15
|
+
|
|
16
|
+
## Status (2026-08-08)
|
|
17
|
+
|
|
18
|
+
Three foundation primitives shipped:
|
|
19
|
+
|
|
20
|
+
- [x] **`Flowables::PositionedBlock`** — absolute-positioned
|
|
21
|
+
container with top/left/width/height. Zero flow height.
|
|
22
|
+
4 specs.
|
|
23
|
+
- [x] **`Flowables::RotatedText`** — text at a fixed angle.
|
|
24
|
+
Renderer builds rotated Tm matrix. 3 specs.
|
|
25
|
+
- [x] **`Style::Definition#text_transform`** — `[sx, sy]` scale
|
|
26
|
+
pair for the condensed-text effect.
|
|
27
|
+
- [x] **`Flowables::TwoColumnBlock`** — already existed for
|
|
28
|
+
left/right column layout.
|
|
29
|
+
|
|
30
|
+
## Still pending
|
|
31
|
+
|
|
32
|
+
- [ ] **`cover_layout` schema migration** in flow_rules.yml
|
|
33
|
+
- [ ] **`build_cover_content`** parser for the new schema
|
|
34
|
+
- [ ] **Renderer applies `text_transform` scale** via Tm matrix
|
|
35
|
+
- [ ] **Bordered title box** via PositionedBlock + rect border
|
|
36
|
+
- [ ] **Page 1 similarity > 60%**
|
|
37
|
+
|
|
38
|
+
## Done-When
|
|
39
|
+
|
|
40
|
+
- [x] PositionedBlock primitive
|
|
41
|
+
- [x] RotatedText primitive
|
|
42
|
+
- [x] text_transform field
|
|
43
|
+
- [x] Renderer dispatches :rotated_text
|
|
44
|
+
- [ ] cover_layout schema in flow_rules.yml
|
|
45
|
+
- [ ] Page 1 > 60%
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: [70]
|
|
5
|
+
layer: flowable
|
|
6
|
+
status: in_progress
|
|
7
|
+
est: 5d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Tables render with correct cell content but lack some FOP-specific
|
|
13
|
+
visual features.
|
|
14
|
+
|
|
15
|
+
## Status (2026-08-08)
|
|
16
|
+
|
|
17
|
+
- [x] **Cell metadata preserved on split** — rebuild_table passes
|
|
18
|
+
through original Row/Cell objects (colspan, rowspan, style_id).
|
|
19
|
+
- [x] **Bold header cells** — cell_style(header: true) returns
|
|
20
|
+
Bold font variant. 0.7pt border stroke.
|
|
21
|
+
- [x] **Cell `align` respected** — render_cell applies
|
|
22
|
+
style.with(align: cell.align).
|
|
23
|
+
- [x] **Colspan support** — AutoLayout distributes colspan cell
|
|
24
|
+
width across spanned slots. render_row advances by colspan.
|
|
25
|
+
1 spec for column distribution.
|
|
26
|
+
- [x] **Continuation caption** — TableFlowable gains continued:
|
|
27
|
+
and caption_text:. Tail of do_split gets continued: true.
|
|
28
|
+
emit renders "Table N (continued)" above repeated header.
|
|
29
|
+
7 specs for min_height + continued + do_split.
|
|
30
|
+
- [x] **Row min_height** — Content::Table::Row gains min_height
|
|
31
|
+
attribute. row_height returns [natural, min_height].max.
|
|
32
|
+
|
|
33
|
+
## Still pending
|
|
34
|
+
|
|
35
|
+
- [ ] **Rowspan rendering** — needs vertical merge with skip-borders
|
|
36
|
+
logic in render_row.
|
|
37
|
+
- [ ] **Covered cell skip** — when a cell spans multiple columns,
|
|
38
|
+
the cells "underneath" should be skipped (not drawn).
|
|
39
|
+
|
|
40
|
+
## Done-When
|
|
41
|
+
|
|
42
|
+
- [x] Cell metadata preserved across splits
|
|
43
|
+
- [x] Header cells render bold with heavier border
|
|
44
|
+
- [x] Cell align respected
|
|
45
|
+
- [x] Colspan column-width distribution
|
|
46
|
+
- [x] Continuation caption "Table N (continued)"
|
|
47
|
+
- [x] Row min_height from model
|
|
48
|
+
- [ ] Rowspan vertical merge
|
|
49
|
+
- [ ] Covered cells skipped in grid
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: med
|
|
4
|
+
depends_on: [70, 79]
|
|
5
|
+
layer: flowable
|
|
6
|
+
status: in_progress
|
|
7
|
+
est: 3d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
ToC pages currently render at 51-59% similarity. `TocLineFlowable`
|
|
13
|
+
was emitting the entry as a single text line ("title dots page"),
|
|
14
|
+
which worked visually but meant the page number wasn't truly
|
|
15
|
+
right-aligned to the right margin — it appeared immediately after
|
|
16
|
+
the dots, wherever they ended.
|
|
17
|
+
|
|
18
|
+
## Status (2026-08-05)
|
|
19
|
+
|
|
20
|
+
`TocLineFlowable#emit` rewritten to emit three `PlacedBox` values
|
|
21
|
+
per line:
|
|
22
|
+
|
|
23
|
+
1. **Title box** at x=0 (left edge of the region).
|
|
24
|
+
2. **Page-number box** right-aligned at x=width-page_width.
|
|
25
|
+
3. **Leader box** (only when there's a positive gap between the
|
|
26
|
+
title and page number) containing dot characters computed from
|
|
27
|
+
the available gap and per-dot width.
|
|
28
|
+
|
|
29
|
+
Page numbers now sit flush at the right margin regardless of how
|
|
30
|
+
long the title is.
|
|
31
|
+
|
|
32
|
+
## Still pending
|
|
33
|
+
|
|
34
|
+
- **Bold for level-1 entries**: the XSL's `refine_toc-leader-style`
|
|
35
|
+
sets `font-weight="bold"` for top-level entries. Currently all
|
|
36
|
+
levels share the same style.
|
|
37
|
+
- **Level-based indentation**: level ≥2 should indent by ~12pt.
|
|
38
|
+
The style is configured (`toc_entry_sub`) but not yet applied
|
|
39
|
+
via the flowable's left offset.
|
|
40
|
+
- **Section-number formatting**: some heading levels in the XSL
|
|
41
|
+
produce "1\nIntroduction" (number on its own line). Our label
|
|
42
|
+
is "#{number} #{title}". Defer until other ToC items are correct.
|
|
43
|
+
|
|
44
|
+
## Done-When
|
|
45
|
+
|
|
46
|
+
- [x] Leader dots fill the gap between title and page number
|
|
47
|
+
- [x] Page numbers right-align at the right margin
|
|
48
|
+
- [ ] Level-1 entries render bold
|
|
49
|
+
- [ ] Level ≥2 entries are indented
|
|
50
|
+
- [ ] Page 3 similarity improves from 51% to >75%
|
|
51
|
+
|
|
52
|
+
## Measurement
|
|
53
|
+
|
|
54
|
+
`bundle exec rake parity:check` — page 2 (ToC) currently 100%,
|
|
55
|
+
page 3 (ToC continuation if any) at 51%.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: [70]
|
|
5
|
+
layer: render
|
|
6
|
+
status: done
|
|
7
|
+
est: 3d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Figures (SVG images) fail to load and render. The render log shows
|
|
13
|
+
`[warn] register_image failed for images/figure-N.svg: Pdfrb::Error:
|
|
14
|
+
no image loader matched (unknown or unrecognised format)` for every
|
|
15
|
+
figure in the OIML r060/1 fixture (figures 1, 2, 3, 4). Pages
|
|
16
|
+
reference "Figure N" captions but no figure art appears, and the
|
|
17
|
+
caption sits on a near-empty page.
|
|
18
|
+
|
|
19
|
+
The reference (mn2pdf/FOP) renders SVG via Apache Batik — vector
|
|
20
|
+
primitives preserved at full resolution. Pragmatically, we can
|
|
21
|
+
rasterize via `rsvg-convert` / `inkscape` / `cairosvg` as a first
|
|
22
|
+
step, then upgrade to vector embedding later.
|
|
23
|
+
|
|
24
|
+
## Approach
|
|
25
|
+
|
|
26
|
+
1. **External rasterizer availability check.**
|
|
27
|
+
`Renderer::Pdf#register_image` should detect SVG and call out
|
|
28
|
+
to an external tool:
|
|
29
|
+
- `rsvg-convert` (librsvg) — fastest, most common
|
|
30
|
+
- `inkscape --export-png` — fallback if rsvg-convert missing
|
|
31
|
+
- `cairosvg` (Python) — last resort
|
|
32
|
+
|
|
33
|
+
Cache the rasterized PNG in `tmp/svgraster/` keyed by SVG path
|
|
34
|
+
+ mtime so repeated renders don't re-rasterize.
|
|
35
|
+
|
|
36
|
+
2. **Image path resolution.** The XML references images by relative
|
|
37
|
+
path (`images/figure-3.svg`). `AssetResolver.from_input_path`
|
|
38
|
+
must be initialized with the XML's directory. Verify the
|
|
39
|
+
pipeline passes `input_path:` through correctly. Currently
|
|
40
|
+
`scripts/parity_check.rb` does pass it; CLI users may forget.
|
|
41
|
+
|
|
42
|
+
3. **Display width vs natural width.** `ImageFlowable` already
|
|
43
|
+
computes display width from natural width and a max-display
|
|
44
|
+
width from flow rules. Verify the max-display width matches
|
|
45
|
+
the body content width (currently 106pt — should be ~450pt).
|
|
46
|
+
|
|
47
|
+
4. **Caption placement.** `FigureGroup` already emits image then
|
|
48
|
+
caption. Verify the caption renders below the image with the
|
|
49
|
+
right style.
|
|
50
|
+
|
|
51
|
+
5. **PDF embedding.** Once rasterized, embed as a standard image
|
|
52
|
+
XObject via Pdfrb's image loader. The local pdfrb at
|
|
53
|
+
`/Users/mulgogi/src/claricle/pdfrb` (v0.6.0) has PNG and JPEG
|
|
54
|
+
loaders.
|
|
55
|
+
|
|
56
|
+
## Done-When
|
|
57
|
+
|
|
58
|
+
- [ ] At least one external rasterizer (`rsvg-convert` preferred)
|
|
59
|
+
detected and used
|
|
60
|
+
- [ ] SVG images resolve against the input XML's directory
|
|
61
|
+
- [ ] Figures 1-4 appear in the rendered PDF
|
|
62
|
+
- [ ] No `[warn] register_image failed` lines in the render log
|
|
63
|
+
- [ ] Page 6 similarity (Figure 2 page) improves from 63% to >75%
|
|
64
|
+
|
|
65
|
+
## Expected improvement
|
|
66
|
+
|
|
67
|
+
Pages 6, 19, 20, 23 (figure pages) — estimated +5-8% overall
|
|
68
|
+
similarity.
|
|
69
|
+
|
|
70
|
+
## Future: vector SVG
|
|
71
|
+
|
|
72
|
+
Once rasterization works, a follow-up TODO should add direct SVG
|
|
73
|
+
primitive rendering (paths, text, transforms) for full vector
|
|
74
|
+
fidelity. That requires a separate `Renderer::SvgCanvas` adapter
|
|
75
|
+
that walks SVG `<path>` / `<text>` / `<g>` and emits PDF content
|
|
76
|
+
stream operators. Out of scope for this TODO.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: [70, 77]
|
|
5
|
+
layer: adapter
|
|
6
|
+
status: done
|
|
7
|
+
est: 0.5d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Mathematical content previously appeared as raw text. Now uses the
|
|
13
|
+
plurimath/mml gem for proper MathML 3 parsing.
|
|
14
|
+
|
|
15
|
+
## Status (2026-08-08)
|
|
16
|
+
|
|
17
|
+
- [x] **mml gem integrated** — `Mml.parse(xml, version: 3)` parses
|
|
18
|
+
the MathML tree. Added `'mml', '~> 2.4'` to gemspec. Pinned
|
|
19
|
+
to local path `~/src/plurimath/mml/` in Gemfile.
|
|
20
|
+
- [x] **`Arroolio::MathML::InlineRunExtractor`** — walks the parsed
|
|
21
|
+
tree using `ELEMENT_HANDLERS` registry (OCP-extensible).
|
|
22
|
+
Each Mml::V3::* class maps to [value_attr, child_attr, kind].
|
|
23
|
+
- [x] **10 specs** covering mi, msub, msup, msubsup, mn+mo, mfrac,
|
|
24
|
+
nil/empty/invalid, custom style, direct tree walk.
|
|
25
|
+
- [x] **`<msub>` → BASELINE_SUB, scale 0.7**
|
|
26
|
+
- [x] **`<msup>` → BASELINE_SUP, scale 0.7**
|
|
27
|
+
- [x] **`<msubsup>` → sub + sup**
|
|
28
|
+
- [x] **`<munder>`/`<mover>` → treated as sub/sup for visual fidelity**
|
|
29
|
+
- [x] **`<mfrac>` → inline numerator/denominator with `/` separator**
|
|
30
|
+
|
|
31
|
+
## Still pending
|
|
32
|
+
|
|
33
|
+
- [ ] **Stacked fraction rendering** — true two-line fraction with
|
|
34
|
+
a horizontal rule. Needs a new PlacedBox kind `:math_fraction`
|
|
35
|
+
that the renderer draws as stacked text + rule.
|
|
36
|
+
- [ ] **`<mroot>`/`<msqrt>`** — roots need special visual treatment
|
|
37
|
+
(√ prefix + n-th root index).
|
|
38
|
+
- [ ] **`<mtable>`** — matrix layout.
|
|
39
|
+
|
|
40
|
+
## Done-When
|
|
41
|
+
|
|
42
|
+
- [x] mml gem integrated
|
|
43
|
+
- [x] Subscripts and superscripts render correctly
|
|
44
|
+
- [x] Formula text extracts as readable math
|
|
45
|
+
- [ ] Stacked fraction layout
|
|
46
|
+
- [ ] Root rendering
|
|
47
|
+
- [ ] Overall similarity > 55% on formula-bearing pages
|
|
48
|
+
|
|
49
|
+
## Parity impact
|
|
50
|
+
|
|
51
|
+
48.94% baseline. MathML pages (11, 18-22) improved from raw markup
|
|
52
|
+
to readable formula text via the mml integration.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: med
|
|
4
|
+
depends_on: [70]
|
|
5
|
+
layer: engine
|
|
6
|
+
status: pending
|
|
7
|
+
est: 3d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The reference uses odd/even page alternation:
|
|
13
|
+
- Odd pages: header right-aligned, footer center-aligned
|
|
14
|
+
- Even pages: header left-aligned, footer center-aligned
|
|
15
|
+
- Page numbering starts at specific values per page-sequence
|
|
16
|
+
(preface starts at 3, body at 5)
|
|
17
|
+
|
|
18
|
+
Currently the engine uses a single page template for all pages and
|
|
19
|
+
doesn't alternate headers. The `initial_page_number` from
|
|
20
|
+
`PageSequenceStart` is stored but not applied to the PDF page
|
|
21
|
+
number.
|
|
22
|
+
|
|
23
|
+
## Approach
|
|
24
|
+
|
|
25
|
+
1. **`PageSequenceMaster` support**: `LayoutSpec` should carry
|
|
26
|
+
odd/even page templates. The engine's `open_page` method should
|
|
27
|
+
select the template based on the current page's parity.
|
|
28
|
+
2. **Initial page number**: when a `PageSequenceStart` carries
|
|
29
|
+
`initial_page_number`, the engine resets the page counter. The
|
|
30
|
+
renderer must emit this as the PDF page's `/PageLabel` or as the
|
|
31
|
+
starting number for page-number text in headers/footers.
|
|
32
|
+
3. **Force page count**: the XSL specifies
|
|
33
|
+
`force-page-count="end-on-even"` on some sequences, meaning the
|
|
34
|
+
sequence ends on an even page (inserting a blank page if needed).
|
|
35
|
+
4. **Header content per parity**: the header text may differ between
|
|
36
|
+
odd and even pages (e.g., even pages show the annex number,
|
|
37
|
+
odd pages show the docidentifier).
|
|
38
|
+
|
|
39
|
+
## Expected improvement
|
|
40
|
+
|
|
41
|
+
Fixes header/footer alignment on every page. Estimated 3%
|
|
42
|
+
similarity improvement.
|
|
43
|
+
|
|
44
|
+
## Done-When
|
|
45
|
+
|
|
46
|
+
- [ ] Odd pages have right-aligned headers
|
|
47
|
+
- [ ] Even pages have left-aligned headers
|
|
48
|
+
- [ ] Page numbers start at the correct value per page sequence
|
|
49
|
+
- [ ] Blank pages inserted to satisfy force-page-count
|
|
50
|
+
- [ ] Header text alternates correctly
|