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,47 @@
1
+ ---
2
+ priority: P2
3
+ phase: 14
4
+ depends_on: [04, 22]
5
+ layer: color
6
+ est: 2d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Currently colours are Symbols (`:black`, `:red`) or hex strings.
13
+ Production documents need RGB, CMYK, named spot colours, and ICC
14
+ profile-based colours for accurate print reproduction.
15
+
16
+ ## Approach
17
+
18
+ Files under `lib/arrolio/color/`:
19
+
20
+ - `color.rb` — base + factory: `Color.parse("black")`, `Color.rgb(255,
21
+ 128, 0)`, `Color.cmyk(0, 1, 1, 0)`, `Color.spot("PMS 200")`.
22
+
23
+ - `rgb.rb` — `RGB = Struct.new(:r, :g, :b)`. Each 0..1 Float.
24
+
25
+ - `cmyk.rb` — `CMYK = Struct.new(:c, :m, :y, :k)`.
26
+
27
+ - `spot.rb` — `Spot = Struct.new(:name, :tint)`.
28
+
29
+ - `icc.rb` — `ICC = Struct.new(:profile_ref, :components)`. Profile
30
+ loaded from `data/arrolio/icc/*.icc`.
31
+
32
+ - `color_space_registry.rb` — registers ICC profiles; assigns
33
+ `/ColorSpace` objects in the PDF; caches by profile path.
34
+
35
+ PDF rendering (TODO 22 extension):
36
+ - RGB → `rg`/`RG` operators.
37
+ - CMYK → `k`/`K` operators.
38
+ - Spot → separation colour space with alternate colour.
39
+ - ICC → ICCBased colour space with the embedded profile.
40
+
41
+ ## Done-When
42
+
43
+ - [ ] `Color.parse("#FF8800")` returns an RGB instance.
44
+ - [ ] CMYK colour renders correctly in the PDF.
45
+ - [ ] Spot colour renders with a fallback alternate colour.
46
+ - [ ] ICC profile embedded once per document, referenced by name.
47
+ - [ ] Round-trip: re-read PDF has correct `/ColorSpace` entries.
@@ -0,0 +1,41 @@
1
+ ---
2
+ priority: P2
3
+ phase: 14
4
+ depends_on: [22]
5
+ layer: color
6
+ est: 2d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Production PDFs carry metadata: title, author, subject, keywords,
13
+ creation/modification dates, plus an XMP stream for ISO 19005-2
14
+ (PDF/A) compliance. Arrolio currently writes nothing.
15
+
16
+ ## Approach
17
+
18
+ Files:
19
+
20
+ - `lib/arrolio/metadata/info.rb` — `Info` value object with the
21
+ classic `/Info` dict fields: `:Title`, `:Author`, `:Subject`,
22
+ `:Keywords`, `:Creator`, `:Producer`, `:CreationDate`,
23
+ `:ModDate`.
24
+
25
+ - `lib/arrolio/metadata/xmp.rb` — builds an XMP packet (XML)
26
+ containing Dublin Core + PDF + PDF/A schemas. Stored as a
27
+ `/Metadata` stream on Catalog (`/Type /Metadata /Subtype /XML`).
28
+
29
+ - Renderer (TODO 22) integration: after rendering pages, write
30
+ `/Info` to trailer and `/Metadata` to Catalog.
31
+
32
+ Date format: PDF date strings `D:YYYYMMDDHHmmSSOHH'mm'`.
33
+
34
+ ## Done-When
35
+
36
+ - [ ] A document rendered with `metadata: { title: "My Doc", author:
37
+ "Jane" }` has those fields in `/Info`.
38
+ - [ ] `/CreationDate` and `/ModDate` are valid PDF date strings.
39
+ - [ ] XMP packet is valid XML; parses with REXML.
40
+ - [ ] XMP contains `dc:title`, `dc:creator`, `pdf:Producer`.
41
+ - [ ] Round-trip: re-read PDF exposes the metadata via Pdfrb.
@@ -0,0 +1,49 @@
1
+ ---
2
+ priority: P2
3
+ phase: 15
4
+ depends_on: [22, 21]
5
+ layer: a11y
6
+ est: 3d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ PDF/UA (ISO 14289-1) requires a structure tree: every visible
13
+ element on the page maps to a structure element (H1, P, Table, TR,
14
+ TD, Figure, etc.). The structure tree is the screen-reader path
15
+ through the document. Without it, the PDF is unreadable to
16
+ assistive technology.
17
+
18
+ ## Approach
19
+
20
+ Files:
21
+
22
+ - `lib/arrolio/structure/element.rb` — `StructElement` value
23
+ object: `type` (`:H1`, `:P`, `:Table`, `:Figure`, etc.),
24
+ `content_ref` (link to the PlacedBox that materialises this),
25
+ `children`, `attributes`.
26
+
27
+ - `lib/arrolio/structure/tree.rb` — `StructTreeRoot` builder.
28
+ Walks the Output tree; for each PlacedBox with a `struct_role`
29
+ attribute, emits a StructElement.
30
+
31
+ - Marked content in the content stream: every visible drawing
32
+ operation is wrapped in `BDC`/`EMC` operators with a `/MC`
33
+ property list referencing the StructElement.
34
+
35
+ - `lib/arrolio/renderer/pdf/struct_emitter.rb` — emits:
36
+ - `/StructTreeRoot` on Catalog.
37
+ - `/Type /StructElem` entries.
38
+ - `/ParentTree` mapping content-stream MCIDs to StructElems.
39
+ - `/MarkInfo /true` on Catalog.
40
+
41
+ ## Done-When
42
+
43
+ - [ ] A simple paragraph emits `<P>` structure element.
44
+ - [ ] A heading emits `<H1>` with correct heading level.
45
+ - [ ] A table emits `<Table><TR><TH>...</TH></TR></Table>`.
46
+ - [ ] An image emits `<Figure>` with `/Alt` text.
47
+ - [ ] `/MarkInfo /true` is present on Catalog.
48
+ - [ ] Round-trip: re-read PDF has a `/StructTreeRoot`.
49
+ - [ ] A screen reader (NVDA or similar) can navigate the document.
@@ -0,0 +1,43 @@
1
+ ---
2
+ priority: P2
3
+ phase: 15
4
+ depends_on: [48, 33]
5
+ layer: a11y
6
+ est: 1d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ PDF/UA mandates alt text on every image (`/Alt` on the Figure
13
+ structure element) and supports `/ActualText` on any element (for
14
+ cases where the visible text isn't the accessible text — e.g.
15
+ mathematical equations rendered as glyphs).
16
+
17
+ ## Approach
18
+
19
+ Extend `Arrolio::Content::Image` (TODO 02) with `alt` (String).
20
+ Extend `Arrolio::Flowables::ImageFlowable` (TODO 33) to propagate
21
+ `alt` to the PlacedBox.
22
+
23
+ Extend `Arrolio::Output::PlacedBox` with `alt_text` and
24
+ `actual_text` fields.
25
+
26
+ Renderer integration (TODO 48 StructElement):
27
+ - When emitting a `<Figure>` StructElement for an image, include
28
+ `/Alt (alt text)` if `alt_text` is set.
29
+ - When emitting any StructElement, include `/ActualText` if the
30
+ PlacedBox carries it.
31
+
32
+ Validation:
33
+ - A `Content::Image` without `alt` produces a warning (configurable:
34
+ error in strict mode).
35
+ - A `Content::Equation` without `actual_text` produces a warning.
36
+
37
+ ## Done-When
38
+
39
+ - [ ] Image with `alt: "Diagram of the system"` emits `/Alt` on its
40
+ Figure element.
41
+ - [ ] Equation with `actual_text: "E = mc^2"` emits `/ActualText`.
42
+ - [ ] Missing alt text raises a warning (or error in strict mode).
43
+ - [ ] Screen reader reads the alt text when navigating to an image.
@@ -0,0 +1,59 @@
1
+ ---
2
+ priority: P2
3
+ phase: 16
4
+ depends_on: [02, 03, 20, 22]
5
+ layer: composer
6
+ est: 3d
7
+ status: in_progress
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Building Content + LayoutSpec by hand for every document is verbose.
13
+ The Composer facade provides a streaming "write a report" API:
14
+ `composer.heading("Intro"); composer.paragraph("...")`. It builds
15
+ Content + uses a default LayoutSpec under the hood.
16
+
17
+ ## Approach
18
+
19
+ File: `lib/arrolio/composer.rb`.
20
+
21
+ ```ruby
22
+ class Arrolio::Composer
23
+ def initialize(page_size: :A4, margins: 25.mm, preset: :technical_report)
24
+ @document = Arrolio::Content::Document.new
25
+ @layout_spec = Arrolio::Presets.load(preset, page_size:, margins:)
26
+ @current_section = @document
27
+ end
28
+
29
+ def heading(text, level: 1); ... end
30
+ def paragraph(text, **style); ... end
31
+ def table(headers:, rows:); ... end
32
+ def list(items, marker: :bullet); ... end
33
+ def image(path, **opts); ... end
34
+ def page_break; ... end
35
+
36
+ def write(path_or_io)
37
+ pages = Arrolio::Engine::Paged.new(layout_spec: @layout_spec,
38
+ flowables: build_flowables).layout
39
+ Arrolio::Renderer::Pdf.new.render(pages, io: path_or_io)
40
+ end
41
+
42
+ private
43
+ def build_flowables
44
+ # Walk @document, produce Flowable[] per content node.
45
+ end
46
+ end
47
+ ```
48
+
49
+ Top-level convenience: `Arrolio.compose("out.pdf") { |c| ... }`.
50
+
51
+ ## Done-When
52
+
53
+ - [ ] `Arrolio.compose("out.pdf") { |c| c.heading "Hi"; c.paragraph "World" }`
54
+ produces a valid PDF.
55
+ - [ ] Heading uses the preset's heading style.
56
+ - [ ] Multiple paragraphs flow across pages.
57
+ - [ ] Tables and lists integrate.
58
+ - [ ] Image integration.
59
+ - [ ] Specs cover the public API surface.
@@ -0,0 +1,44 @@
1
+ ---
2
+ priority: P2
3
+ phase: 16
4
+ depends_on: [04]
5
+ layer: composer
6
+ est: 1d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Composer needs default LayoutSpecs that look professional without
13
+ requiring the user to specify everything. Built-in presets: technical
14
+ report, book, article, manual. Each preset is a LayoutSpec factory.
15
+
16
+ ## Approach
17
+
18
+ File: `lib/arrolio/presets.rb` (namespace) + files under
19
+ `lib/arrolio/presets/`:
20
+
21
+ - `technical_report.rb` — A4, 25mm margins, single column, Times body,
22
+ Helvetica headings, page number bottom-center, section title in
23
+ header. Reads like an ISO/IEC standard.
24
+
25
+ - `book.rb` — Letter, 1in margins, single column with optional
26
+ header, running header with chapter title, page number outside
27
+ (left on even, right on odd). Two-page sequence (odd/even templates).
28
+
29
+ - `article.rb` — A4, 20mm margins, single column, no headers, simple
30
+ footer. Compact.
31
+
32
+ - `manual.rb` — Letter, 0.75in margins, two-column body, sidebar
33
+ notes. Uses multi-column (TODO 44).
34
+
35
+ Each preset is a `LayoutSpec.build { ... }` block, returned by
36
+ `Presets.load(:technical_report, page_size:, margins:)`.
37
+
38
+ ## Done-When
39
+
40
+ - [ ] `Presets.load(:technical_report)` returns a valid LayoutSpec.
41
+ - [ ] Each preset has documented font + page geometry choices.
42
+ - [ ] Each preset renders a sample document end-to-end.
43
+ - [ ] Presets can be subclassed/extended.
44
+ - [ ] Specs cover loading + structure of each preset.
@@ -0,0 +1,120 @@
1
+ ---
2
+ priority: P0
3
+ phase: 17
4
+ depends_on: [23, 30, 32, 33, 39, 42]
5
+ layer: harness
6
+ est: 3d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ **This is Arrolio's primary end-to-end correctness test.** The user's
13
+ requirement: define OIML PDF output (from the existing `oiml.xsl`),
14
+ use the `metanorma-document` gem to parse real OIML presentation XML,
15
+ convert that into Arrolio's content model, apply an OIML LayoutSpec,
16
+ and render via Pdfrb. If Arrolio can produce FOP-equivalent PDFs for
17
+ real OIML documents, the engine is correct.
18
+
19
+ This TODO sets up the **adapter gem** (or namespace) that bridges
20
+ metanorma-document → Arrolio. Phase-17 TODOs 53–55 build the OIML
21
+ LayoutSpec and the diff harness around this.
22
+
23
+ ## Approach
24
+
25
+ The bridge has three parts:
26
+
27
+ ### 1. OIML fixture corpus
28
+
29
+ Live in `spec/fixtures/oiml/` (gitignored if licensing requires):
30
+ - 3–5 real OIML presentation XML files at varying complexity
31
+ (cover + TOC + 5 sections + tables; multi-section with figures;
32
+ full 50-page reference doc).
33
+ - For each, a FOP-rendered reference PDF (the ground truth).
34
+
35
+ A rake task `rake fixtures:refresh_oiml` regenerates the reference
36
+ PDFs by running `xsltproc oiml.xsl + fop` if the user has FOP
37
+ installed; otherwise fetches pre-rendered ones from a private repo.
38
+
39
+ ### 2. `Arrolio::Oiml` adapter (in this gem, spec-only initially)
40
+
41
+ Files under `lib/arrolio/oiml/`:
42
+
43
+ - `content_adapter.rb` — converts
44
+ `Metanorma::Oiml::Document::Root` → `Arrolio::Content::Document`.
45
+ Walks the typed model, dispatches per node class via
46
+ `BlockDispatcher` (mirrors the STS transformer pattern):
47
+
48
+ ```ruby
49
+ class ContentAdapter
50
+ def convert(oiml_root) -> Arrolio::Content::Document
51
+ def convert_clause(clause) -> Arrolio::Content::Section
52
+ def convert_paragraph(p) -> Arrolio::Content::Paragraph
53
+ def convert_table(t) -> Arrolio::Content::Table
54
+ def convert_list(l) -> Arrolio::Content::List
55
+ def convert_image(i) -> Arrolio::Content::Image
56
+ # etc.
57
+ end
58
+ ```
59
+
60
+ Inline content (text + spans + cross-references) maps to
61
+ `Arrolio::InlineRun[]`.
62
+
63
+ - `layout_spec_loader.rb` — loads the OIML LayoutSpec (TODO 53).
64
+
65
+ - `pipeline.rb` — orchestrates:
66
+ ```ruby
67
+ oiml_root = Metanorma::Oiml::Document::Root.parse(xml)
68
+ content = ContentAdapter.new.convert(oiml_root)
69
+ layout = LayoutSpecLoader.load("oiml.layout_spec.rb")
70
+ flowables = FlowBuilder.(content, layout)
71
+ pages = Arrolio::Engine::Paged.new(layout_spec: layout,
72
+ flowables:).layout
73
+ Arrolio::Renderer::Pdf.new.render(pages, io:)
74
+ ```
75
+
76
+ ### 3. End-to-end spec
77
+
78
+ `spec/arrolio/oiml_e2e_spec.rb` (tagged `:e2e`):
79
+
80
+ ```ruby
81
+ RSpec.describe "OIML → PDF end-to-end", :e2e do
82
+ Dir.glob("spec/fixtures/oiml/*.presentation.xml").each do |xml_path|
83
+ it "renders #{File.basename(xml_path)} to a valid PDF" do
84
+ out = StringIO.new
85
+ Arrolio::Oiml::Pipeline.(File.read(xml_path), io: out)
86
+
87
+ # Sanity: valid PDF
88
+ expect(out.string).to start_with("%PDF-")
89
+ expect(out.string).to include("%%EOF")
90
+
91
+ # Re-readable
92
+ reopened = Pdfrb::Document.new(io: StringIO.new(out.string))
93
+ expect(reopened.pages.count).to be > 0
94
+
95
+ # Diff vs FOP reference (TODO 55)
96
+ ref_path = xml_path.sub(".presentation.xml", ".fop.pdf")
97
+ if File.exist?(ref_path)
98
+ diff = Arrolio::Harness::TextDiff.(
99
+ ours: out.string,
100
+ reference: File.binread(ref_path)
101
+ )
102
+ expect(diff.page_count_delta).to be <= 1 # tolerate off-by-one
103
+ expect(diff.text_similarity).to be > 0.95
104
+ end
105
+ end
106
+ end
107
+ end
108
+ ```
109
+
110
+ ## Done-When
111
+
112
+ - [ ] `Metanorma::Oiml::Document::Root` (or its base class) loads
113
+ fixture XMLs without error.
114
+ - [ ] `ContentAdapter` converts every fixture without raising.
115
+ - [ ] `Pipeline` produces bytes starting with `%PDF-` for every fixture.
116
+ - [ ] Each rendered PDF re-reads via Pdfrb with the expected page count.
117
+ - [ ] Adapter covers at minimum: clause, paragraph, table, list,
118
+ image, heading (sections 1, 1.1, 1.1.1), footnote, basic-link.
119
+ - [ ] Each adapter method is unit-tested with a minimal input.
120
+ - [ ] `rake oiml:render[xml_path]` CLI command renders a single doc.
@@ -0,0 +1,121 @@
1
+ ---
2
+ priority: P0
3
+ phase: 17
4
+ depends_on: [52]
5
+ layer: harness
6
+ est: 5d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The OIML stylesheet (`oiml.xsl`) encodes OIML's house style:
13
+ specific page geometry, section-numbering format, running headers
14
+ with chapter title + doc number, table styling, TOC layout, etc.
15
+ We need to encode the *intent* of `oiml.xsl` as a Ruby
16
+ `Arrolio::LayoutSpec` — not by translating the XSL element-for-
17
+ element, but by reading what it produces visually and reproducing
18
+ that in Ruby.
19
+
20
+ ## Approach
21
+
22
+ Read these sources of truth (in order):
23
+
24
+ 1. **`~/src/mn/metanorma-taste/data/oiml/oiml.xsl`** — the XSL itself.
25
+ 2. **Sample FOP-rendered OIML PDFs** — `~/src/mn/mn-samples-oiml/_site/documents/*/*.pdf`.
26
+ Open in a viewer; measure geometry with a PDF inspector.
27
+ 3. **The OIML document model** — `~/src/mn/metanorma-oiml/lib/metanorma/oiml/document/`
28
+ to know what elements exist.
29
+
30
+ File: `lib/arrolio/oiml/layout_spec.rb` (Ruby-encoded OIML house style).
31
+
32
+ ```ruby
33
+ module Arrolio::Oiml
34
+ module LayoutSpec
35
+ OIML_A4 = [595.28, 841.89].freeze # A4 in pt
36
+
37
+ def self.build
38
+ Arrolio::LayoutSpec.build do
39
+ # Page templates: cover, body (with running header + footer)
40
+ page_template(:cover) do
41
+ page_size OIML_A4
42
+ margins top: 50, bottom: 50, left: 30, right: 30
43
+ end
44
+
45
+ page_template(:body) do
46
+ page_size OIML_A4
47
+ margins top: 35, bottom: 30, left: 25, right: 25
48
+ region_extents before: 15, after: 15
49
+ end
50
+
51
+ # Page sequences: cover (single page) + body (odd/even differ)
52
+ page_sequence(:front_matter, first: :cover, next: :cover)
53
+ page_sequence(:main,
54
+ first: :body, odd: :body, even: :body,
55
+ page_numbering: { format: :arabic, start: 1 })
56
+
57
+ # Styles (excerpt)
58
+ style(:title, font: "Helvetica-Bold", size: 24)
59
+ style(:subtitle, font: "Helvetica", size: 14)
60
+ style(:heading_1, font: "Helvetica-Bold", size: 14,
61
+ space_before: 18, keep_with_next: true)
62
+ style(:heading_2, font: "Helvetica-Bold", size: 12)
63
+ style(:body, font: "Times-Roman", size: 11,
64
+ align: :justify, line_spacing: 1.2)
65
+ style(:table_header, font: "Helvetica-Bold", size: 10,
66
+ background_color: "#EEEEEE")
67
+ style(:table_cell, font: "Times-Roman", size: 10)
68
+ style(:running_header, font: "Helvetica", size: 9)
69
+ style(:footer, font: "Helvetica", size: 9, align: :center)
70
+
71
+ # Static content bindings (running header + footer)
72
+ static_content(:body, :before) do
73
+ [
74
+ # Left frame: current top-level section title
75
+ Arrolio::PlacedText.new(
76
+ content: ->(ctx) { ctx.current_section.numbered_title },
77
+ style: :running_header,
78
+ position: [0, 0, 80.mm, 15]
79
+ ),
80
+ # Right frame: doc reference
81
+ Arrolio::PlacedText.new(
82
+ content: ->(ctx) { ctx.metadata[:doc_reference] },
83
+ style: :running_header,
84
+ position: [100.mm, 0, 30.mm, 15]
85
+ )
86
+ ]
87
+ end
88
+
89
+ static_content(:body, :after) do
90
+ [
91
+ Arrolio::PlacedField.new(
92
+ field: :page_number,
93
+ style: :footer,
94
+ position: [90.mm, 0, 10.mm, 15]
95
+ )
96
+ ]
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ ```
103
+
104
+ This file is the **single source of truth** for OIML styling. To
105
+ change a colour, edit this file. To change page geometry, edit this
106
+ file. The XSL becomes irrelevant.
107
+
108
+ ## Done-When
109
+
110
+ - [ ] `LayoutSpec.build` produces a valid Arrolio::LayoutSpec.
111
+ - [ ] Page geometry matches the OIML sample PDFs (measured to 1pt).
112
+ - [ ] Section numbering format matches OIML convention (`1`, `1.1`,
113
+ `1.1.1`).
114
+ - [ ] Running header on a body page shows the current section title +
115
+ doc reference.
116
+ - [ ] Footer shows page number centered.
117
+ - [ ] Tables in the OIML sample render with the right header styling.
118
+ - [ ] Cover page matches the OIML sample layout (title block, doc
119
+ reference, etc.).
120
+ - [ ] Style is documented: every property choice cites the OIML
121
+ sample PDF or the XSL line it was derived from.
@@ -0,0 +1,97 @@
1
+ ---
2
+ priority: P0
3
+ phase: 17
4
+ depends_on: [52, 53]
5
+ layer: harness
6
+ est: 2d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ End-to-end pipeline that ties together:
13
+ 1. metanorma-document parses OIML presentation XML.
14
+ 2. `Oiml::ContentAdapter` converts to `Arrolio::Content::Document`.
15
+ 3. `Oiml::LayoutSpec.build` (TODO 53) supplies the OIML house style.
16
+ 4. `Engine::Paged` lays out.
17
+ 5. `Renderer::Pdf` produces bytes.
18
+
19
+ The PDF **diff** against FOP's reference is a separate concern —
20
+ covered by the comparator subsystem (TODOs 55–59). This TODO only
21
+ ships the pipeline and a minimal sanity-check spec.
22
+
23
+ ## Approach
24
+
25
+ ### Pipeline entry point
26
+
27
+ File: `lib/arrolio/oiml/pipeline.rb`.
28
+
29
+ ```ruby
30
+ module Arrolio::Oiml
31
+ module Pipeline
32
+ module_function
33
+
34
+ def call(presentation_xml_or_path, io:, **opts)
35
+ require "metanorma/oiml/document" # lazy-load heavy dep
36
+
37
+ oiml_root = Metanorma::Oiml::Document::Root.parse(
38
+ presentation_xml_or_path
39
+ )
40
+ content = ContentAdapter.new.convert(oiml_root)
41
+ layout = LayoutSpec.build
42
+ flowables = FlowBuilder.(content, layout)
43
+ pages = Arrolio::Engine::Paged.new(
44
+ layout_spec: layout,
45
+ flowables: flowables
46
+ ).layout
47
+ Arrolio::Renderer::Pdf.new.render(pages, io: io)
48
+ end
49
+ end
50
+ end
51
+ ```
52
+
53
+ ### CLI
54
+
55
+ File: `exe/arrolio-oiml` (executable).
56
+
57
+ ```sh
58
+ arrolio-oiml render document.presentation.xml > document.pdf
59
+ arrolio-oiml diff document.presentation.xml \
60
+ --reference=document.fop.pdf
61
+ ```
62
+
63
+ The `diff` subcommand shells out to the comparator subsystem
64
+ (TODO 55) — this TODO only delivers `render`.
65
+
66
+ ### Minimal sanity spec
67
+
68
+ `spec/arrolio/oiml/pipeline_spec.rb`:
69
+
70
+ ```ruby
71
+ RSpec.describe "OIML → PDF pipeline", :e2e do
72
+ Dir.glob("spec/fixtures/oiml/*.presentation.xml").each do |xml|
73
+ it "#{File.basename(xml)} renders a valid PDF" do
74
+ out = StringIO.new
75
+ Arrolio::Oiml::Pipeline.(xml, io: out)
76
+ expect(out.string).to start_with("%PDF-")
77
+ expect(out.string).to include("%%EOF")
78
+
79
+ reopened = Pdfrb::Document.new(io: StringIO.new(out.string))
80
+ expect(reopened.pages.count).to be > 0
81
+ end
82
+ end
83
+ end
84
+ ```
85
+
86
+ This is intentionally weak — no comparison vs FOP. The diff
87
+ subsystem (TODO 55–59) layers comparison on top.
88
+
89
+ ## Done-When
90
+
91
+ - [ ] `Arrolio::Oiml::Pipeline.(xml, io:)` produces PDF bytes from
92
+ every fixture.
93
+ - [ ] `exe/arrolio-oiml render doc.presentation.xml > out.pdf`
94
+ produces a readable PDF.
95
+ - [ ] Pipeline spec runs against all fixtures without raising.
96
+ - [ ] Pipeline accepts either a path or a raw XML string.
97
+ - [ ] No FOP/diff code lives in this TODO — that's TODOs 55–59.