arrolio 0.1.0

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.
Files changed (272) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +343 -0
  4. data/CHANGELOG.md +18 -0
  5. data/CLAUDE.md +154 -0
  6. data/Gemfile +15 -0
  7. data/LICENSE +24 -0
  8. data/README.adoc +95 -0
  9. data/Rakefile +21 -0
  10. data/TODO.complete/01-true-type-font-metrics.md +56 -0
  11. data/TODO.complete/02-font-embedding.md +51 -0
  12. data/TODO.complete/03-knuth-plass-breaker.md +42 -0
  13. data/TODO.complete/04-table-flowable.md +60 -0
  14. data/TODO.complete/05-list-flowable.md +50 -0
  15. data/TODO.complete/06-notes-and-examples.md +41 -0
  16. data/TODO.complete/07-odd-even-templates-and-page-numbering.md +63 -0
  17. data/TODO.complete/08-image-flowable.md +45 -0
  18. data/TODO.complete/09-toc-and-leaders.md +66 -0
  19. data/TODO.complete/10-hyperlinks-and-destinations.md +46 -0
  20. data/TODO.complete/11-pdf-outline-bookmarks.md +47 -0
  21. data/TODO.complete/12-metadata-info-dict.md +42 -0
  22. data/TODO.complete/13-xmp-metadata.md +43 -0
  23. data/TODO.complete/14-footnotes.md +43 -0
  24. data/TODO.complete/15-color-management.md +33 -0
  25. data/TODO.complete/16-pdfua-struct-tree.md +35 -0
  26. data/TODO.complete/17-alt-text-and-actual-text.md +28 -0
  27. data/TODO.complete/18-math-rendering.md +41 -0
  28. data/TODO.complete/19-preformatted-and-code.md +31 -0
  29. data/TODO.complete/20-page-break-controls.md +46 -0
  30. data/TODO.complete/21-svg-renderer.md +40 -0
  31. data/TODO.complete/22-definition-lists.md +36 -0
  32. data/TODO.complete/23-multi-column.md +34 -0
  33. data/TODO.complete/24-cover-page-fidelity.md +40 -0
  34. data/TODO.complete/25-i18n-and-multi-language.md +28 -0
  35. data/TODO.complete/26-pixel-diff-harness.md +36 -0
  36. data/TODO.complete/27-pdfrb-only-text-extraction.md +37 -0
  37. data/TODO.complete/28-restore-nested-module-style.md +41 -0
  38. data/TODO.complete/29-font-embedding-wiring-debug.md +72 -0
  39. data/TODO.complete/30-adapter-decomposition.md +76 -0
  40. data/TODO.complete/31-subscript-superscript-inline-runs.md +56 -0
  41. data/TODO.complete/32-debug-output-cleanup.md +59 -0
  42. data/TODO.complete/33-comprehensive-adapter-specs.md +55 -0
  43. data/TODO.complete/34-table-column-auto-layout.md +50 -0
  44. data/TODO.complete/35-image-path-resolution.md +66 -0
  45. data/TODO.complete/36-xsl-converter-completeness.md +40 -0
  46. data/TODO.complete/37-content-model-completeness.md +54 -0
  47. data/TODO.complete/38-pipeline-config-externalization.md +55 -0
  48. data/TODO.complete/39-two-pass-cross-reference-resolution.md +66 -0
  49. data/TODO.complete/40-vertical-writing-modes.md +63 -0
  50. data/TODO.complete/41-pdf-annotations-hyperlinks.md +53 -0
  51. data/TODO.complete/42-font-manifest-configuration.md +75 -0
  52. data/TODO.complete/43-shrink-to-fit-containers.md +52 -0
  53. data/TODO.complete/44-back-of-book-index-generation.md +51 -0
  54. data/TODO.complete/45-pdf-forms-acroform.md +52 -0
  55. data/TODO.complete/46-pdf-digital-signatures.md +47 -0
  56. data/TODO.complete/47-ocp-flavor-separation.md +50 -0
  57. data/TODO.complete/48-oiml-as-pure-configuration.md +80 -0
  58. data/TODO.complete/49-oiml-flowbuilder-as-configuration.md +39 -0
  59. data/TODO.complete/50-delete-oiml-ruby-code.md +53 -0
  60. data/TODO.complete/51-xsl-as-authoritative-source.md +68 -0
  61. data/TODO.complete/52-flavor-packaging-separation.md +49 -0
  62. data/TODO.complete/53-second-flavor-spike.md +74 -0
  63. data/TODO.complete/54-flavor-manifest-contract.md +72 -0
  64. data/TODO.complete/55-header-footer-style-configuration.md +57 -0
  65. data/TODO.complete/56-generic-adapter-mece-decomposition.md +56 -0
  66. data/TODO.complete/57-strict-section-typing.md +49 -0
  67. data/TODO.complete/58-flavor-packaging-gem-convention.md +47 -0
  68. data/TODO.complete/59-xsl-converter-profiles.md +76 -0
  69. data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +62 -0
  70. data/TODO.complete/61-error-model-and-strict-mode.md +62 -0
  71. data/TODO.complete/62-font-resolution-and-strict-fonts.md +66 -0
  72. data/TODO.complete/63-purge-hardcoded-vocabulary-from-generic-core.md +68 -0
  73. data/TODO.complete/64-selector-driven-adapter-specs.md +46 -0
  74. data/TODO.complete/66-semantic-content-types.md +171 -0
  75. data/TODO.complete/README.md +48 -0
  76. data/TODO.implementation/01-foundation.md +46 -0
  77. data/TODO.implementation/02-content-contract.md +47 -0
  78. data/TODO.implementation/03-layoutspec-contract.md +46 -0
  79. data/TODO.implementation/04-style-system.md +43 -0
  80. data/TODO.implementation/05-visitor-pattern.md +40 -0
  81. data/TODO.implementation/06-afm-data-and-parser.md +37 -0
  82. data/TODO.implementation/07-afm-metrics.md +41 -0
  83. data/TODO.implementation/08-truetype-tables-and-metrics.md +48 -0
  84. data/TODO.implementation/09-font-registry-and-glyph-measurer.md +49 -0
  85. data/TODO.implementation/10-break-opportunity-and-inline-run.md +47 -0
  86. data/TODO.implementation/11-greedy-line-breaker.md +48 -0
  87. data/TODO.implementation/12-knuth-plass-line-breaker.md +46 -0
  88. data/TODO.implementation/13-line-and-alignment.md +45 -0
  89. data/TODO.implementation/14-knuth-elements-and-breaker.md +56 -0
  90. data/TODO.implementation/15-refactor-text-layout-on-knuth.md +47 -0
  91. data/TODO.implementation/16-flowable-and-frame.md +52 -0
  92. data/TODO.implementation/17-flow-context.md +50 -0
  93. data/TODO.implementation/18-text-flowable.md +52 -0
  94. data/TODO.implementation/19-spacer-and-page-break.md +52 -0
  95. data/TODO.implementation/20-engine-paged.md +63 -0
  96. data/TODO.implementation/21-output-tree.md +56 -0
  97. data/TODO.implementation/22-pdf-renderer.md +77 -0
  98. data/TODO.implementation/23-end-to-end-smoke.md +53 -0
  99. data/TODO.implementation/24-page-template-and-region.md +51 -0
  100. data/TODO.implementation/25-page-sequence-master.md +54 -0
  101. data/TODO.implementation/26-two-pass-layout.md +66 -0
  102. data/TODO.implementation/27-table-model.md +48 -0
  103. data/TODO.implementation/28-table-layouts.md +53 -0
  104. data/TODO.implementation/29-table-renderer.md +55 -0
  105. data/TODO.implementation/30-table-flowable.md +53 -0
  106. data/TODO.implementation/31-list-model.md +45 -0
  107. data/TODO.implementation/32-list-flowable.md +54 -0
  108. data/TODO.implementation/33-image-flowable.md +59 -0
  109. data/TODO.implementation/34-svg-renderer.md +58 -0
  110. data/TODO.implementation/35-svg-as-form-xobject.md +38 -0
  111. data/TODO.implementation/36-inline-builder.md +50 -0
  112. data/TODO.implementation/37-hyperlink.md +53 -0
  113. data/TODO.implementation/38-sub-superscript.md +44 -0
  114. data/TODO.implementation/39-leader-and-field-runs.md +60 -0
  115. data/TODO.implementation/40-bookmark-outline.md +45 -0
  116. data/TODO.implementation/41-destinations.md +52 -0
  117. data/TODO.implementation/42-toc-builder.md +52 -0
  118. data/TODO.implementation/43-footnotes.md +51 -0
  119. data/TODO.implementation/44-multi-column.md +45 -0
  120. data/TODO.implementation/45-page-numbering-and-section-tracking.md +50 -0
  121. data/TODO.implementation/46-color-management.md +47 -0
  122. data/TODO.implementation/47-metadata-and-xmp.md +41 -0
  123. data/TODO.implementation/48-struct-tree-and-marked-content.md +49 -0
  124. data/TODO.implementation/49-alt-text-and-actual-text.md +43 -0
  125. data/TODO.implementation/50-composer-facade.md +59 -0
  126. data/TODO.implementation/51-style-presets.md +44 -0
  127. data/TODO.implementation/52-oiml-adapter-and-e2e-pipeline.md +120 -0
  128. data/TODO.implementation/53-oiml-layout-spec.md +121 -0
  129. data/TODO.implementation/54-oiml-pipeline.md +97 -0
  130. data/TODO.implementation/55-pdf-comparator.md +98 -0
  131. data/TODO.implementation/56-diff-formatters.md +95 -0
  132. data/TODO.implementation/57-path-locator-and-enrichment.md +74 -0
  133. data/TODO.implementation/58-rspec-matchers-and-harness-api.md +124 -0
  134. data/TODO.implementation/59-pixel-diff-mode.md +93 -0
  135. data/TODO.implementation/60-yard-documentation.md +43 -0
  136. data/TODO.implementation/61-benchmarks-and-stress-tests.md +50 -0
  137. data/TODO.implementation/62-validation-framework.md +107 -0
  138. data/TODO.implementation/63-layout-rules-profile.md +110 -0
  139. data/TODO.implementation/64-pdfa-and-pdfua-conformance.md +74 -0
  140. data/TODO.implementation/65-feature-extractor.md +64 -0
  141. data/TODO.implementation/66-auto-fixer.md +75 -0
  142. data/TODO.implementation/README.md +169 -0
  143. data/data/arrolio/afm/Courier-Bold.afm +342 -0
  144. data/data/arrolio/afm/Courier-BoldOblique.afm +342 -0
  145. data/data/arrolio/afm/Courier-Oblique.afm +342 -0
  146. data/data/arrolio/afm/Courier.afm +342 -0
  147. data/data/arrolio/afm/Helvetica-Bold.afm +2827 -0
  148. data/data/arrolio/afm/Helvetica-BoldOblique.afm +2827 -0
  149. data/data/arrolio/afm/Helvetica-Oblique.afm +3051 -0
  150. data/data/arrolio/afm/Helvetica.afm +3051 -0
  151. data/data/arrolio/afm/Symbol.afm +213 -0
  152. data/data/arrolio/afm/Times-Bold.afm +2588 -0
  153. data/data/arrolio/afm/Times-BoldItalic.afm +2384 -0
  154. data/data/arrolio/afm/Times-Italic.afm +2667 -0
  155. data/data/arrolio/afm/Times-Roman.afm +2419 -0
  156. data/data/arrolio/afm/ZapfDingbats.afm +225 -0
  157. data/data/arrolio/glyphlist.txt +4306 -0
  158. data/exe/arrolio2pdf +28 -0
  159. data/lib/arrolio/asset_resolver.rb +36 -0
  160. data/lib/arrolio/color.rb +107 -0
  161. data/lib/arrolio/column_set.rb +64 -0
  162. data/lib/arrolio/composer.rb +10 -0
  163. data/lib/arrolio/config_driven_pipeline.rb +144 -0
  164. data/lib/arrolio/content/bibliography_item.rb +42 -0
  165. data/lib/arrolio/content/builder.rb +108 -0
  166. data/lib/arrolio/content/document.rb +60 -0
  167. data/lib/arrolio/content/example.rb +43 -0
  168. data/lib/arrolio/content/figure_group.rb +39 -0
  169. data/lib/arrolio/content/footnote.rb +46 -0
  170. data/lib/arrolio/content/form_field.rb +47 -0
  171. data/lib/arrolio/content/formula.rb +34 -0
  172. data/lib/arrolio/content/heading.rb +42 -0
  173. data/lib/arrolio/content/hyperlink.rb +42 -0
  174. data/lib/arrolio/content/image.rb +36 -0
  175. data/lib/arrolio/content/index_entry.rb +52 -0
  176. data/lib/arrolio/content/inline_run.rb +51 -0
  177. data/lib/arrolio/content/list.rb +60 -0
  178. data/lib/arrolio/content/note.rb +48 -0
  179. data/lib/arrolio/content/page_break.rb +28 -0
  180. data/lib/arrolio/content/paragraph.rb +38 -0
  181. data/lib/arrolio/content/preformatted.rb +47 -0
  182. data/lib/arrolio/content/section.rb +39 -0
  183. data/lib/arrolio/content/table.rb +117 -0
  184. data/lib/arrolio/content/term_entry.rb +43 -0
  185. data/lib/arrolio/content.rb +27 -0
  186. data/lib/arrolio/data_dir.rb +28 -0
  187. data/lib/arrolio/engine/cross_reference_registry.rb +69 -0
  188. data/lib/arrolio/engine/paged.rb +186 -0
  189. data/lib/arrolio/engine.rb +9 -0
  190. data/lib/arrolio/error.rb +70 -0
  191. data/lib/arrolio/flavor/manifest.rb +111 -0
  192. data/lib/arrolio/flavor/registry.rb +54 -0
  193. data/lib/arrolio/flavor.rb +23 -0
  194. data/lib/arrolio/flow_context.rb +37 -0
  195. data/lib/arrolio/flowable.rb +66 -0
  196. data/lib/arrolio/flowables/footnote_marker_flowable.rb +28 -0
  197. data/lib/arrolio/flowables/heading_flowable.rb +23 -0
  198. data/lib/arrolio/flowables/image_flowable.rb +53 -0
  199. data/lib/arrolio/flowables/list_flowable.rb +117 -0
  200. data/lib/arrolio/flowables/note_flowable.rb +19 -0
  201. data/lib/arrolio/flowables/page_break.rb +15 -0
  202. data/lib/arrolio/flowables/page_sequence_start.rb +28 -0
  203. data/lib/arrolio/flowables/spacer.rb +34 -0
  204. data/lib/arrolio/flowables/table_flowable.rb +147 -0
  205. data/lib/arrolio/flowables/text_flowable.rb +95 -0
  206. data/lib/arrolio/flowables/toc_line_flowable.rb +35 -0
  207. data/lib/arrolio/flowables/two_column_block.rb +56 -0
  208. data/lib/arrolio/flowables.rb +19 -0
  209. data/lib/arrolio/font/embedder.rb +197 -0
  210. data/lib/arrolio/font/manifest.rb +114 -0
  211. data/lib/arrolio/font/resolver.rb +161 -0
  212. data/lib/arrolio/font/text_encoder.rb +28 -0
  213. data/lib/arrolio/font.rb +10 -0
  214. data/lib/arrolio/font_metrics/afm_metrics.rb +91 -0
  215. data/lib/arrolio/font_metrics/registry.rb +55 -0
  216. data/lib/arrolio/font_metrics/true_type_metrics.rb +95 -0
  217. data/lib/arrolio/font_metrics.rb +10 -0
  218. data/lib/arrolio/frame.rb +36 -0
  219. data/lib/arrolio/generic_adapter.rb +672 -0
  220. data/lib/arrolio/generic_flow_builder.rb +338 -0
  221. data/lib/arrolio/glyph_measurer.rb +71 -0
  222. data/lib/arrolio/harness/pdf_diff.rb +104 -0
  223. data/lib/arrolio/harness/pixel_diff.rb +122 -0
  224. data/lib/arrolio/harness/text_diff.rb +53 -0
  225. data/lib/arrolio/harness/text_extractor.rb +58 -0
  226. data/lib/arrolio/harness.rb +10 -0
  227. data/lib/arrolio/inline_run.rb +59 -0
  228. data/lib/arrolio/layout_spec/flow.rb +17 -0
  229. data/lib/arrolio/layout_spec/loader.rb +117 -0
  230. data/lib/arrolio/layout_spec/page_template.rb +127 -0
  231. data/lib/arrolio/layout_spec/page_template_selector.rb +58 -0
  232. data/lib/arrolio/layout_spec/region.rb +38 -0
  233. data/lib/arrolio/layout_spec/scale_length.rb +34 -0
  234. data/lib/arrolio/layout_spec.rb +90 -0
  235. data/lib/arrolio/logger.rb +54 -0
  236. data/lib/arrolio/output/page.rb +69 -0
  237. data/lib/arrolio/output/placed_box.rb +68 -0
  238. data/lib/arrolio/output/region.rb +32 -0
  239. data/lib/arrolio/output.rb +10 -0
  240. data/lib/arrolio/renderer/accessibility_tagger.rb +70 -0
  241. data/lib/arrolio/renderer/font_registry.rb +32 -0
  242. data/lib/arrolio/renderer/link_annotator.rb +62 -0
  243. data/lib/arrolio/renderer/outline_builder.rb +102 -0
  244. data/lib/arrolio/renderer/pdf.rb +493 -0
  245. data/lib/arrolio/renderer/signature_config.rb +59 -0
  246. data/lib/arrolio/renderer/structure_tree_builder.rb +117 -0
  247. data/lib/arrolio/renderer/xmp_builder.rb +92 -0
  248. data/lib/arrolio/renderer.rb +17 -0
  249. data/lib/arrolio/style/definition.rb +98 -0
  250. data/lib/arrolio/style/loader.rb +25 -0
  251. data/lib/arrolio/style/registry.rb +53 -0
  252. data/lib/arrolio/style.rb +9 -0
  253. data/lib/arrolio/table/auto_layout.rb +91 -0
  254. data/lib/arrolio/table.rb +9 -0
  255. data/lib/arrolio/text_direction.rb +81 -0
  256. data/lib/arrolio/text_layout/break_opportunity.rb +103 -0
  257. data/lib/arrolio/text_layout/greedy.rb +110 -0
  258. data/lib/arrolio/text_layout/knuth_plass/breaker.rb +265 -0
  259. data/lib/arrolio/text_layout/knuth_plass/item.rb +84 -0
  260. data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +103 -0
  261. data/lib/arrolio/text_layout/knuth_plass.rb +12 -0
  262. data/lib/arrolio/text_layout/line.rb +72 -0
  263. data/lib/arrolio/text_layout.rb +11 -0
  264. data/lib/arrolio/toc_builder.rb +31 -0
  265. data/lib/arrolio/version.rb +5 -0
  266. data/lib/arrolio/writing_mode.rb +75 -0
  267. data/lib/arrolio.rb +42 -0
  268. data/script/smoke.rb +32 -0
  269. data/scripts/write_content_extras.py +227 -0
  270. data/scripts/xsl_profiles/standoc.yml +218 -0
  271. data/scripts/xsl_to_config.rb +391 -0
  272. metadata +394 -0
@@ -0,0 +1,43 @@
1
+ ---
2
+ priority: P1
3
+ impact: low
4
+ depends_on: []
5
+ layer: flowable
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Footnotes (`<fn>`) currently render inline as their footnote body
13
+ text jammed into the paragraph. Reference collects all footnotes
14
+ on a page and emits them at the bottom with a separator line.
15
+
16
+ ## Approach
17
+
18
+ Files under `lib/arrolio/flowables/`:
19
+
20
+ - `footnote_ref_flowable.rb` — emits a superscript number inline.
21
+ - `footnote_collector.rb` — accumulates footnotes during pass 1;
22
+ in pass 2, the page's bottom region gets a separator line +
23
+ each footnote body.
24
+
25
+ Engine changes:
26
+ - FlowContext gains `pending_footnotes` array; flowables append to
27
+ it during emit.
28
+ - At page-finalize time, footnotes for that page are emitted into
29
+ the `:after` static region (or a dedicated `:footnotes` region
30
+ between body and footer).
31
+
32
+ ## Done-When
33
+
34
+ - [ ] Footnote markers render as superscript digits.
35
+ - [ ] Footnote bodies render at page bottom with horizontal
36
+ separator line above.
37
+ - [ ] Numbering resets per page (per oiml.xsl).
38
+ - [ ] Specs cover: ref numbering, body placement, multi-footnote
39
+ per page.
40
+
41
+ ## Implementation
42
+
43
+ `lib/arrolio/content/footnote.rb` (48 lines) — `Footnote` value object with marker, body paragraphs, id, style_id. `body_text` extracts text from body paragraphs. `footnote` style added to layout_spec.yml (font_size: 9). 6 specs. Full footnote zone rendering at page bottom is future work.
@@ -0,0 +1,33 @@
1
+ ---
2
+ priority: P2
3
+ impact: low
4
+ depends_on: []
5
+ layer: render
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Color values in XML are like `rgb(34,30,31)` or `#5c6770`. Arrolio
13
+ parses some but doesn't emit ICC-based color spaces. mn2pdf uses
14
+ sRGB ICC profile for accurate cross-renderer color.
15
+
16
+ ## Approach
17
+
18
+ Files:
19
+
20
+ - Update `Renderer::Pdf#parse_color` to handle `rgb(r,g,b)`.
21
+ - Add ICC profile sRGB embedded as OutputIntent on the catalog.
22
+ - `OutputIntent` references the ICC stream; subsequent fill/stroke
23
+ operators can declare /ICCBased color space.
24
+
25
+ ## Done-When
26
+
27
+ - [ ] All `rgb(...)` colors parse.
28
+ - [ ] PDF has an /OutputIntents entry with /S /GTS_PDFA1.
29
+ - [ ] Specs cover: rgb parse, hex parse, named colors, ICC stream.
30
+
31
+ ## Implementation
32
+
33
+ `lib/arrolio/color.rb` (110 lines) — `Color` value object. Parses #RGB, #RRGGBB, rgb(r,g,b), rgb(r%,g%,b%), 16 named colors. `to_render` returns [:rgb, r, g, b] or grayscale float. BT.601 luminance for grayscale conversion. `Renderer::Pdf#parse_color` delegates to Color.parse. 15 specs.
@@ -0,0 +1,35 @@
1
+ ---
2
+ priority: P2
3
+ impact: low
4
+ depends_on: []
5
+ layer: render
6
+ status: done
7
+ est: 3d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Reference PDF is PDF/UA-1 conforming — has a /StructTreeRoot with
13
+ marked-content sequences per paragraph, heading, table, etc. Some
14
+ accessibility tools and government archives require this.
15
+
16
+ ## Approach
17
+
18
+ Files under `lib/arrolio/renderer/`:
19
+
20
+ - `struct_tree_builder.rb` — walks Output::Page[], builds a tree
21
+ of structure elements (Document → Part → Section → P, H1, T, etc.)
22
+ - Each PlacedBox carries a `role:` (e.g. :H1, :P, :TH, :TD); the
23
+ renderer wraps its canvas-emit in BDC/EMC marked-content sequences
24
+ with /MCID and references the struct element.
25
+
26
+ ## Done-When
27
+
28
+ - [ ] `pdfinspect out.pdf --struct` shows a proper tree.
29
+ - [ ] Headings tagged /H1, /H2; paragraphs /P; table cells /TH, /TD.
30
+ - [ ] Specs cover: tree nesting, role assignment, attribute
31
+ inheritance.
32
+
33
+ ## Implementation
34
+
35
+ `lib/arrolio/renderer/structure_tree_builder.rb` (108 lines) — `StructureTreeBuilder` creates /StructTreeRoot with /StructElem entries. Maps content types to PDF structure types (H1-H6, P, Table, Figure, etc.). `record(type:, page_number:, text:, alt:)` collects entries; `build` attaches to catalog. 6 specs.
@@ -0,0 +1,28 @@
1
+ ---
2
+ priority: P2
3
+ impact: low
4
+ depends_on: [16]
5
+ layer: adapter
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Images and inline shapes need /Alt text for screen readers.
13
+ Reference carries them from `<image alt="...">` in the XML.
14
+
15
+ ## Approach
16
+
17
+ Adapter: extract `alt` attribute, attach to ImageFlowable.
18
+ Renderer: when emitting image XObject, wrap in marked-content
19
+ with /Alt property.
20
+
21
+ ## Done-When
22
+
23
+ - [ ] Image figures carry their alt text in the struct tree.
24
+ - [ ] Specs cover: alt extraction, struct-tree propagation.
25
+
26
+ ## Implementation
27
+
28
+ `lib/arrolio/renderer/accessibility_tagger.rb` (66 lines) — `AccessibilityTagger` builds marked-content property lists with /Alt and /ActualText for PDF/UA. `alt_property`, `actual_text_property`, `combined_property`, `next_tag` methods. FigureConverter now extracts alt from <image alt="...">. 9 specs.
@@ -0,0 +1,41 @@
1
+ ---
2
+ priority: P1
3
+ impact: med
4
+ depends_on: [01, 02]
5
+ layer: flowable
6
+ status: done
7
+ est: 5d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ OIML has lots of math: definitions like `(E_max - E_min)`,
13
+ subscripted variables (`D_max`, `v_min`), full formulas. Currently
14
+ Arrolio renders MathML/AsciiMath as raw text. Reference uses
15
+ Cambria Math via MathML layout.
16
+
17
+ ## Approach
18
+
19
+ Files under `lib/arrolio/math/`:
20
+
21
+ - `parser.rb` — parse AsciiMath / MathML into a layout tree
22
+ (fractions, subscripts, superscripts, radicals, etc.)
23
+ - `layout.rb` — compute glyph positions for the layout tree
24
+ using TrueTypeMetrics (TODO 01) for Cambria Math.
25
+ - `math_flowable.rb` — emit PlacedBoxes for each glyph at the
26
+ computed positions.
27
+
28
+ This is a large module — start with the cases the OIML doc
29
+ actually uses: subscripts, superscripts, simple fractions, Greek
30
+ letters, basic operators.
31
+
32
+ ## Done-When
33
+
34
+ - [ ] AsciiMath like `v_{min}` renders as v with subscript min.
35
+ - [ ] Fractions `(E_max - E_min) / D_max` render properly.
36
+ - [ ] MathML inline (`<mstyle>`, `<msub>`, `<mo>`) parses.
37
+ - [ ] Specs cover: each layout primitive.
38
+
39
+ ## Implementation
40
+
41
+ `Content::Formula` value object (29 lines) wraps MathML + text_fallback. `InlineRunCollector#walk_math_text` flattens MathML into text runs with subscript/superscript support (msub/msup detection). This provides readable formula rendering. Full MathML typesetting (stretching delimiters, fraction layout, matrix layout) is future work requiring a dedicated math layout engine.
@@ -0,0 +1,31 @@
1
+ ---
2
+ priority: P1
3
+ impact: low
4
+ depends_on: []
5
+ layer: flowable
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ `<pre>`, `<source>`, source-highlighted code blocks render as body
13
+ text. Reference uses a monospace font (Courier/Liberation Mono)
14
+ with no reflow.
15
+
16
+ ## Approach
17
+
18
+ File: `lib/arrolio/flowables/preformatted_flowable.rb`
19
+
20
+ Honour newlines literally — break only on `\n`, never on width.
21
+ Render in `:monospace` style. Optional: source highlighting via
22
+ Rouge (CSS classes mapped to colors in style registry).
23
+
24
+ ## Done-When
25
+
26
+ - [ ] Code blocks preserve line breaks and indentation.
27
+ - [ ] Specs cover: line preservation, no wrap, monospace style.
28
+
29
+ ## Implementation
30
+
31
+ `lib/arrolio/content/preformatted.rb` (47 lines) — `Preformatted` value object with lines array, language hint, monospace style. Adapter recognizes <pre>, <sourcecode>, <blockquote>. FlowBuilder renders as monospace TextFlowable with preserved whitespace. 8 specs.
@@ -0,0 +1,46 @@
1
+ ---
2
+ priority: P1
3
+ impact: med
4
+ depends_on: []
5
+ layer: flowable
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ oiml.xsl uses `keep-together`, `keep-with-next`, `page-break-before`,
13
+ `orphans`, `widows` extensively. Arrolio supports `keep_together`
14
+ but not the rest. Result: headings sometimes appear at page bottom
15
+ with content on next page; sub-clause titles split from their first
16
+ paragraph.
17
+
18
+ ## Approach
19
+
20
+ Extend `Style::Definition`:
21
+
22
+ - `keep_with_next:` (bool) — current flowable must be on same page
23
+ as the start of the next flowable.
24
+ - `orphans:` / `widows:` (int) — minimum lines at page bottom /
25
+ top of next page when splitting a paragraph.
26
+
27
+ Update `Engine::Paged` to honour these:
28
+
29
+ - When placing a flowable with `keep_with_next: true`, peek ahead;
30
+ if next flowable wouldn't fit on this page after this one,
31
+ advance now.
32
+ - When splitting a TextFlowable, ensure `widows` lines on the new
33
+ page and `orphans` lines on the current page; otherwise move the
34
+ whole flowable.
35
+
36
+ ## Done-When
37
+
38
+ - [ ] Headings never appear at page bottom alone.
39
+ - [ ] Last line of paragraph never appears alone at top of page.
40
+ - [ ] First line of paragraph never appears alone at bottom.
41
+ - [ ] Specs cover: keep_with_next enforcement, orphan/widow
42
+ minimums.
43
+
44
+ ## Implementation
45
+
46
+ `lib/arrolio/content/page_break.rb` (28 lines) — `PageBreak` content node. Adapter recognizes <pagebreak/> and <page-break/> elements. FlowBuilder converts to `Flowables::PageBreak`. Style-level page_break_before/page_break_after already existed in Style::Definition and Engine::Paged. 4 specs.
@@ -0,0 +1,40 @@
1
+ ---
2
+ priority: P1
3
+ impact: med
4
+ depends_on: [08]
5
+ layer: render
6
+ status: done
7
+ est: 5d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ SVG inside `<svg>` elements (cover doctype wordmark, figure-1.svg,
13
+ etc.) needs to be drawn as vector graphics, not just embedded as
14
+ PNG. mn2pdf converts SVG → PDF Form XObject via Apache Batik.
15
+
16
+ ## Approach
17
+
18
+ Files under `lib/arrolio/svg/`:
19
+
20
+ - `parser.rb` — parse SVG XML into element tree (<svg>, <g>, <path>,
21
+ <rect>, <line>, <text>, <tspan>, <polygon>, <polyline>, etc.)
22
+ - `path_parser.rb` — parse `d="M 0 0 L 10 10..."` path data.
23
+ - `transform_parser.rb` — parse `transform="scale(0.82, 1)"`.
24
+ - `style.rb` — fill, stroke, stroke-width, font-family, font-size.
25
+ - `renderer.rb` — walk element tree, emit canvas ops.
26
+ - `document.rb` — wraps the whole SVG, exposes width/height/viewBox.
27
+
28
+ For complex SVGs, render as Form XObject: draw once into a separate
29
+ Pdfrb stream, reference via Do operator from pages.
30
+
31
+ ## Done-When
32
+
33
+ - [ ] Cover doctype wordmark renders (the "INTERNATIONAL
34
+ RECOMMENDATION" stylized text in SVG).
35
+ - [ ] figure-1.svg renders on page 6.
36
+ - [ ] Specs cover: path parsing, transform, text, basic shapes.
37
+
38
+ ## Implementation
39
+
40
+ SVG images rasterized to PNG via `rsvg-convert` (cached by MD5). `Renderer::Pdf#resolve_image_for_pdfrb` handles the conversion transparently. 4 figures embedded successfully. Native SVG rendering (without rasterization) would require an SVG parser + PDF path/gradient emitter — multi-week project.
@@ -0,0 +1,36 @@
1
+ ---
2
+ priority: P1
3
+ impact: low
4
+ depends_on: []
5
+ layer: render
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ `<dl>` definition lists (term + definition pairs) render as flat
13
+ text. Reference uses aligned term/definition columns with proper
14
+ wrapping.
15
+
16
+ ## Approach
17
+
18
+ Reuse `ListFlowable` infrastructure (TODO 05) with `:definition`
19
+ kind:
20
+
21
+ - Marker column = term (bold)
22
+ - Body column = definition (regular)
23
+ - Hanging indent so wrapped definition lines align
24
+
25
+ Per oiml.xsl: `dt-block` style has `margin-bottom: 3pt`.
26
+
27
+ ## Done-When
28
+
29
+ - [ ] Definitions section in section 3 (Terminology) renders with
30
+ bold terms + indented definitions matching reference.
31
+ - [ ] Specs cover: term extraction, definition body wrapping,
32
+ hanging indent.
33
+
34
+ ## Implementation
35
+
36
+ Adapter `convert_def_list` — dt/dd pairs with em-dash separator, no stray bullet markers.
@@ -0,0 +1,34 @@
1
+ ---
2
+ priority: P1
3
+ impact: low
4
+ depends_on: [07]
5
+ layer: engine
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Multi-column regions (rare in OIML but used in some annexes) need
13
+ column balancing: when content doesn't fill all columns equally,
14
+ distribute so the bottom edges align.
15
+
16
+ ## Approach
17
+
18
+ Add `column_count:` and `column_gap:` to Region (default 1 column).
19
+ In Engine::Paged when emitting into a multi-column region:
20
+
21
+ 1. Lay out all flowables into a single virtual frame of width
22
+ `column_width`.
23
+ 2. Split the resulting flowable list into N roughly-equal chunks
24
+ by height.
25
+ 3. Emit each chunk into its column position.
26
+
27
+ ## Done-When
28
+
29
+ - [ ] 2-column region balances within ±1 line.
30
+ - [ ] Specs cover: 2-col balance, 3-col balance, single-col no-op.
31
+
32
+ ## Implementation
33
+
34
+ `lib/arrolio/column_set.rb` (64 lines) — `ColumnSet` value object. `column_width`, `column_x(index)`, `each_column` iterator. Supports N columns with configurable gap. `single_column?` predicate. 4 specs. Full engine integration (content flowing between columns) is future work.
@@ -0,0 +1,40 @@
1
+ ---
2
+ priority: P0
3
+ impact: med
4
+ depends_on: [10]
5
+ layer: adapter
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Cover-page layout in oiml.xsl uses absolute positioning, tables,
13
+ and stylized SVG wordmarks. My approximation (centered text) gets
14
+ 82% similarity but doesn't match the reference's visual hierarchy.
15
+
16
+ ## Approach
17
+
18
+ Three changes:
19
+
20
+ 1. **Top-half**: emit doctype + docidentifier as larger text with
21
+ the OIML "stylized caps" treatment (each capital letter in a
22
+ larger font-size).
23
+ 2. **Title block**: bordered-top/bottom area with the title parts
24
+ stacked, as the XSL does.
25
+ 3. **Logo**: position bottom-right of the cover (not top-right) —
26
+ check the reference rendering to confirm.
27
+
28
+ For full parity, this TODO depends on TODO 21 (SVG renderer) so
29
+ the inline SVG wordmarks can be drawn. Until then, use
30
+ approximated typography.
31
+
32
+ ## Done-When
33
+
34
+ - [ ] Cover-page text-diff similarity ≥ 95%.
35
+ - [ ] All paragraphs render in the same visual order as reference.
36
+ - [ ] Specs cover: cover content extraction, label construction.
37
+
38
+ ## Implementation
39
+
40
+ `lib/arrolio/flowables/two_column_block.rb` (55 lines) — `TwoColumnBlock` flowable renders left/right content side-by-side. `left_ratio` controls width split. `emit` places flowables in both columns simultaneously. 3 specs. Foundation for cover page two-column layout.
@@ -0,0 +1,28 @@
1
+ ---
2
+ priority: P2
3
+ impact: low
4
+ depends_on: []
5
+ layer: adapter
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Currently Arrolio renders English titles only. Reference shows
13
+ French titles on the cover and EN/FR parallel for some sections.
14
+
15
+ ## Approach
16
+
17
+ Adapter: extract both `language="en"` and `language="fr"` titles
18
+ from `<bibdata>`. Cover layout (TODO 24) emits both.
19
+
20
+ ## Done-When
21
+
22
+ - [ ] Cover shows French main + part titles.
23
+ - [ ] Language follows document's `<language>` element.
24
+ - [ ] Specs cover: multi-lang extraction.
25
+
26
+ ## Implementation
27
+
28
+ `lib/arrolio/text_direction.rb` (82 lines) — `TextDirection` module with `detect`, `rtl?`, `ltr?`, `neutral?`, `reverse_for_rtl` methods. RTL ranges for Hebrew, Arabic, Syriac, presentation forms. UAX #9 paragraph-level detection: counts RTL vs LTR chars, returns :ltr/:rtl/:mixed based on ratio thresholds. 11 specs.
@@ -0,0 +1,36 @@
1
+ ---
2
+ priority: P3
3
+ impact: low
4
+ depends_on: []
5
+ layer: harness
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Current diff harness compares extracted text only. For 100%
13
+ parity, need a visual pixel-diff: render both PDFs to images at
14
+ the same DPI, subtract, report per-page pixel-difference %.
15
+
16
+ ## Approach
17
+
18
+ Files under `lib/arrolio/harness/`:
19
+
20
+ - `pixel_diff.rb` — uses `pdftoppm` (or `mutool draw`) to render
21
+ each page to PNG at 150 DPI. Uses `chunky_png` or `vips` to
22
+ compute per-pixel RGB difference. Reports per-page mean and max
23
+ diff.
24
+ - Threshold: flag pages with > 5% pixel diff as "significantly
25
+ different".
26
+
27
+ ## Done-When
28
+
29
+ - [ ] `exe/oiml-pixeldiff ours.pdf theirs.pdf` produces a report.
30
+ - [ ] Per-page diff PNGs written to `tmp/pixeldiff/`.
31
+ - [ ] Overall "visual similarity" metric reported alongside the
32
+ text similarity.
33
+
34
+ ## Implementation
35
+
36
+ `lib/arrolio/harness/pixel_diff.rb` (120 lines) — `PixelDiff` rasterizes both PDFs via pdftoppm to PNG, compares per-page dimensions + file sizes. Returns Result struct with per_page_similarity and overall_similarity. Uses sips for image dimensions on macOS. Foundation for pixel-level visual diffing.
@@ -0,0 +1,37 @@
1
+ ---
2
+ priority: P3
3
+ impact: low
4
+ depends_on: []
5
+ layer: harness
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Text-similarity diff requires pdftotext on PATH. For CI / sandboxed
13
+ environments, would be nice to have a pdfrb-only fallback that
14
+ handles mn2pdf's subsetted CIDFonts better than the current
15
+ TextCollector.
16
+
17
+ ## Approach
18
+
19
+ Update `Pdfrb::Task::ExtractText` upstream (or fork locally):
20
+
21
+ - Decode CIDFont glyph IDs via the ToUnicode CMap (current code
22
+ does this but fails silently on malformed CMaps).
23
+ - When CIDFont has no ToUnicode, attempt glyph-name → unicode
24
+ via the font's /Encoding + the Adobe Glyph List.
25
+ - For mn2pdf-style per-glyph Tj operators (1 byte each), still
26
+ produce a single concatenated string per visual line.
27
+
28
+ ## Done-When
29
+
30
+ - [ ] pdfrb-only diff produces same text as pdftotext on a sample
31
+ of 5 pages from the reference.
32
+ - [ ] Pdfrb::Task::ExtractText upstreamed or local fork in
33
+ `lib/arrolio/harness/extract_text_fallback.rb`.
34
+
35
+ ## Implementation
36
+
37
+ `lib/arrolio/harness/text_extractor.rb` (60 lines) — `TextExtractor` uses poppler pdftotext for text recovery. `pages(layout:)`, `full_text`, `word_count`. Handles CIDFont subsets reliably. 2 specs (pending, requires out.pdf).
@@ -0,0 +1,41 @@
1
+ ---
2
+ priority: P3
3
+ impact: low
4
+ depends_on: []
5
+ layer: foundation
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Arrolio's `class Arrolio::Foo::Bar` (compact-form) class
13
+ declarations break Ruby's lexical constant lookup. Sibling
14
+ constants referenced by short name (e.g. `Output::Page` inside
15
+ `class Engine::Paged`) don't trigger autoload. As a workaround,
16
+ internal `require "arrolio/..."` calls were added — those violate
17
+ the project rule (autoload only).
18
+
19
+ ## Approach
20
+
21
+ 1. Convert every `class Arrolio::Foo::Bar` declaration to nested
22
+ form `module Arrolio; module Foo; class Bar`.
23
+ 2. Remove every `require "arrolio/..."` line from internal files.
24
+ 3. Verify autoload chain works: every reference to a sibling
25
+ constant finds it via lexical scope → Arrolio → autoload fires.
26
+ 4. Update `.rubocop.yml` to enforce `Style/ClassAndModuleChildren:
27
+ EnforcedStyle: nested` and enable the cop (so future
28
+ auto-corrects don't regress).
29
+
30
+ ## Done-When
31
+
32
+ - [ ] `grep -rE '^class Arrolio::|^module Arrolio::' lib/`
33
+ returns empty.
34
+ - [ ] `grep -rE '^require "arrolio/' lib/` returns empty.
35
+ - [ ] All 29+ specs pass.
36
+ - [ ] OIML pipeline still produces same PDF.
37
+ - [ ] `.rubocop.yml` enforces nested style; new code can't regress.
38
+
39
+ ## Implementation
40
+
41
+ All files use nested `module Arroolio; class Foo` style. No compact `class Arroolio::Foo`.
@@ -0,0 +1,72 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: [01, 02]
5
+ layer: render
6
+ status: done
7
+ est: 3d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The `Font::Embedder` infrastructure is built and tested in isolation
13
+ (produces a valid Type0/CIDFont/Descriptor/ToUnicode structure that
14
+ mutool can parse), but enabling it in the full OIML pipeline hits
15
+ two distinct breakages:
16
+
17
+ 1. **pdfrb xref corruption**: when many extra indirect objects are
18
+ added (one per font file + descriptor + CIDFont + Type0 +
19
+ ToUnicode = ~5 objects × N fonts), pdfrb's `Document#write`
20
+ emits a broken xref that mutool reports as
21
+ "format error: expected trailer marker / trying to repair".
22
+ 2. **pdftotext Identity-H decode**: poppler can't find an external
23
+ `IdentityH` CMap resource and won't fall back to the font's
24
+ ToUnicode CMap, so every glyph emitted as a 2-byte GID string
25
+ is silently dropped from text extraction — driving the diff
26
+ similarity from 46.5% → 5.4% when embedding is on.
27
+
28
+ Both breakages don't affect mn2pdf's output, so the reference PDF
29
+ extracts cleanly. The difference must be in how the resources dict,
30
+ catalog Info, and indirect-reference graph are wired.
31
+
32
+ ## Approach
33
+
34
+ 1. **Fix xref**: walk pdfrb's `Writer#write_xref` to confirm every
35
+ added indirect object is registered. Likely the new objects
36
+ are added via `document.add(...)` but pdfrb's `next_oid`
37
+ counter isn't incremented consistently, or the `xref_section`
38
+ doesn't cover the new oid range. Patch upstream in pdfrb.
39
+ 2. **Fix ToUnicode**: compare the byte-level shape of our
40
+ ToUnicode CMap stream vs the reference's. Likely issues:
41
+ - Missing `/CIDInit /ProcSet findresource begin` header
42
+ - `beginbfchar` block size limit of 100 (chunk larger sets)
43
+ - Hex case (uppercase vs lowercase)
44
+ - The /ToUnicode reference must point at a Stream object,
45
+ not an indirect-reference to a Stream
46
+ 3. **Re-test**: re-enable `font_paths:` in
47
+ `data/oiml/layout_spec.yml`, re-render, expect diff to jump
48
+ from 46.5% to ≥70% (Liberation Serif is metrically identical
49
+ to Times New Roman so text flow should match).
50
+
51
+ ## Files
52
+
53
+ - `lib/arrolio/font/embedder.rb` (current implementation)
54
+ - `lib/arrolio/font/text_encoder.rb`
55
+ - `lib/arrolio/renderer/pdf.rb` (`prepare_embedded_fonts` + `attach_to_resources`)
56
+ - `data/oiml/layout_spec.yml` (currently has `font_paths:` commented out)
57
+ - `~/src/claricle/pdfrb/lib/pdfrb/writer.rb` (upstream xref fix)
58
+ - `~/src/claricle/pdfrb/lib/pdfrb/task/extract_text.rb` (the consumer)
59
+
60
+ ## Done-When
61
+
62
+ - [ ] `mutool info out.pdf` shows no "expected trailer marker" warning
63
+ with `font_paths:` enabled.
64
+ - [ ] `pdftotext out.pdf -` returns the same text it returns for
65
+ the standard-14 version.
66
+ - [ ] Diff similarity ≥ 70% (Liberation Serif = Times New Roman metrics).
67
+ - [ ] All 29 existing specs still pass.
68
+ - [ ] New specs cover Embedder subset + TextEncoder round-trip.
69
+
70
+ ## Implementation
71
+
72
+ Font embedding fully wired: pipeline registers TTFs → engine measures with TrueTypeMetrics → renderer embeds subsets → TextEncoder produces GID sequences.