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,110 @@
1
+ ---
2
+ priority: P1
3
+ phase: 19
4
+ depends_on: [62]
5
+ layer: conformance
6
+ est: 2d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ A baseline ruleset for "did Arrolio itself produce a clean layout?"
13
+ This is the **render-time self-check**: catch Arrolio's own
14
+ regressions, not the source document's. Includes rules every
15
+ production publisher expects:
16
+
17
+ - No orphan headings (heading at very bottom of page).
18
+ - No widow/orphan lines (single line of paragraph alone on a page).
19
+ - Every page has the running header.
20
+ - Tables don't break mid-row.
21
+ - List items don't break mid-item.
22
+ - Footnote and reference on the same page.
23
+ - TOC entries resolve (no broken citations).
24
+ - Page numbering is contiguous.
25
+
26
+ These rules complement semantic-diff (TODO 55): diff catches
27
+ "output changed", layout-rules catch "output is wrong even if it
28
+ matches yesterday's output".
29
+
30
+ ## Approach
31
+
32
+ File: `lib/arrolio/conformance/profiles/layout_rules.rb`.
33
+
34
+ ```ruby
35
+ module Arrolio::Conformance::Profiles
36
+ module LayoutRules
37
+ def self.profile
38
+ Arrolio::Conformance::Profile.new(
39
+ name: "layout-baseline",
40
+ version: "1.0.0",
41
+ spec_reference: "Arrolio baseline layout expectations",
42
+ rules: [
43
+ # Headings
44
+ Rule.new(id: "no-orphan-heading",
45
+ applies_to: :flowable,
46
+ severity: :warning,
47
+ test: ->(t, ctx) { !(t.heading? && t.last_on_page?) }),
48
+ Rule.new(id: "heading-keep-with-next",
49
+ applies_to: :flowable,
50
+ severity: :error,
51
+ test: ->(t, ctx) { !t.heading? || t.kept_with_next? }),
52
+
53
+ # Paragraphs
54
+ Rule.new(id: "no-widow",
55
+ applies_to: :paragraph,
56
+ severity: :warning,
57
+ test: ->(t, ctx) { t.line_count > 1 || t.first_on_page? }),
58
+
59
+ # Tables
60
+ Rule.new(id: "table-no-mid-row-break",
61
+ applies_to: :table_row,
62
+ severity: :error,
63
+ test: ->(t, ctx) { !t.split_mid_row? }),
64
+
65
+ # Pagination
66
+ Rule.new(id: "page-numbering-contiguous",
67
+ applies_to: :document,
68
+ severity: :error,
69
+ test: ->(t, ctx) { t.page_numbers_contiguous? }),
70
+ Rule.new(id: "every-page-has-footer",
71
+ applies_to: :page,
72
+ severity: :warning,
73
+ test: ->(t, ctx) { t.region(:after).placed_boxes.any? }),
74
+
75
+ # Cross-references
76
+ Rule.new(id: "citations-resolved",
77
+ applies_to: :field_run,
78
+ severity: :error,
79
+ test: ->(t, ctx) { !t.citation? || ctx.citation_for(t.ref_id) }),
80
+ ]
81
+ )
82
+ end
83
+ end
84
+ end
85
+ ```
86
+
87
+ Plus an OIML-specific extension profile that adds rules particular
88
+ to OIML house style (e.g. "every clause has a number", "running
89
+ header shows current top-level section title").
90
+
91
+ ### Engine integration
92
+
93
+ Engine emits an `Output::Page[]` with enough metadata for the rules
94
+ to evaluate: `heading?`, `last_on_page?`, `kept_with_next?`,
95
+ `line_count`, `first_on_page?`, `split_mid_row?`,
96
+ `page_numbers_contiguous?`, etc.
97
+
98
+ Run the layout-rules profile automatically after layout (configurable).
99
+ Failed `:error` rules raise `LayoutError`; `:warning` rules log.
100
+
101
+ ## Done-When
102
+
103
+ - [ ] LayoutRules.profile has at least 8 rules covering the
104
+ categories above.
105
+ - [ ] A document with an orphan heading fails `no-orphan-heading`.
106
+ - [ ] A document with a mid-row table break fails
107
+ `table-no-mid-row-break`.
108
+ - [ ] A document with broken citations fails `citations-resolved`.
109
+ - [ ] Engine integration: rules run automatically unless disabled.
110
+ - [ ] OIML extension profile applies on top of baseline.
@@ -0,0 +1,74 @@
1
+ ---
2
+ priority: P2
3
+ phase: 19
4
+ depends_on: [62, 48]
5
+ layer: conformance
6
+ est: 5d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ veraPDF ships rule profiles for ISO 19005 (PDF/A) and ISO 14289
13
+ (PDF/UA). Arrolio should be able to validate its own output against
14
+ these standards — without requiring veraPDF itself.
15
+
16
+ This is a pragmatic subset, not full veraPDF parity. Goal: catch
17
+ the common 80% of conformance failures before users hit them, then
18
+ let veraPDF do the authoritative check.
19
+
20
+ ## Approach
21
+
22
+ Files under `lib/arrolio/conformance/profiles/`:
23
+
24
+ - `pdfa_1b.rb` — PDF/A-1b baseline rules:
25
+ - `/MarkInfo` not required (Level B).
26
+ - All fonts embedded (no standard-14 references).
27
+ - No unembedded JPG2000 in /JBIG2Decode without /DecodeParms.
28
+ - No JavaScript actions.
29
+ - No encrypted streams.
30
+ - `/ColorSpace` does not reference /DeviceN or /DevRGB without ICC.
31
+
32
+ - `pdfa_2b.rb` — PDF/A-2b additions:
33
+ - JPEG2000 allowed (with restrictions).
34
+ - Transparency allowed (with restrictions).
35
+ - Object-level XMP metadata.
36
+
37
+ - `pdfua_1.rb` — PDF/UA-1 rules:
38
+ - `/MarkInfo /true` on Catalog.
39
+ - `/StructTreeRoot` present.
40
+ - Every page has `/Tabs /S` (structured reading order).
41
+ - Every image has `/Alt` text.
42
+ - Every heading maps to the right `<Hn>` element.
43
+ - No nested marked-content sequences beyond depth limit.
44
+ - Font has `/ToUnicode` CMap (text extractable).
45
+
46
+ - `pdfua_2.rb` — PDF/UA-2 (when standard is final).
47
+
48
+ Each profile is a Ruby module with `.profile` returning a
49
+ `Conformance::Profile`. Rules are written in the same DSL as TODO 63.
50
+
51
+ ### veraPDF cross-validation
52
+
53
+ - A spec under `spec/conformance/veraPDF_cross_check_spec.rb` runs
54
+ both Arrolio's profiles and veraPDF (via shell) on the same
55
+ fixture PDFs; expected disagreements are catalogued.
56
+ - Rake task `rake conformance:cross_check` runs the suite.
57
+
58
+ ## Approach for partial-coverage
59
+
60
+ Be honest in the docs: "Arrolio catches N% of PDF/A-1b rules.
61
+ Run veraPDF for authoritative validation." List the rules NOT
62
+ implemented so users know what's missing.
63
+
64
+ ## Done-When
65
+
66
+ - [ ] `pdfa_1b` profile covers ≥ 30 of the most common rules.
67
+ - [ ] `pdfua_1` profile covers ≥ 20 of the most common rules.
68
+ - [ ] An Arrolio-rendered PDF (without fonts embedded) fails
69
+ `pdfa_1b` with a useful error.
70
+ - [ ] An Arrolio-rendered PDF (without StructTree) fails
71
+ `pdfua_1`.
72
+ - [ ] veraPDF cross-check spec runs (when veraPDF is installed) and
73
+ documents the disagreements.
74
+ - [ ] README documents the partial-coverage scope clearly.
@@ -0,0 +1,64 @@
1
+ ---
2
+ priority: P2
3
+ phase: 19
4
+ depends_on: [22]
5
+ layer: conformance
6
+ est: 2d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Modeled on `veraPDF/features/`, Arrolio needs a feature extractor
13
+ that walks a PDF (ours or a third-party's) and produces a structured
14
+ feature report — fonts used, images embedded, colour spaces, page
15
+ sizes, annotations, signatures, metadata. Useful for:
16
+
17
+ - Debugging: "why does this PDF render wrong on viewer X?"
18
+ - Conformance pre-checks before running full validation.
19
+ - Diff inputs (compare features between two PDFs as a quick scan).
20
+ - Documentation: "what does this PDF contain?"
21
+
22
+ ## Approach
23
+
24
+ Files under `lib/arrolio/conformance/features/`:
25
+
26
+ - `feature_extractor.rb` — top-level orchestrator; walks a
27
+ `Pdfrb::Document`, dispatches per object type, accumulates
28
+ `FeatureReport`.
29
+
30
+ - `feature_report.rb` — frozen value object: pages, fonts, images,
31
+ colour_spaces, annotations, signatures, metadata, embedded_files,
32
+ outlines, acroform_fields.
33
+
34
+ - Per-category extractors:
35
+ - `font_features.rb` — base_font, subtype, embedded?, encoding,
36
+ to_unicode?, num_glyphs.
37
+ - `image_features.rb` — width, height, bits_per_component, color_space,
38
+ filter, embedded?, smask?.
39
+ - `color_space_features.rb` — name, type (DeviceRGB/CMYK/ICC/etc.),
40
+ icc_profile?.
41
+ - `annotation_features.rb` — subtype, rect, action.
42
+ - `page_features.rb` — mediabox, rotate, contents_size,
43
+ annotation_count.
44
+ - `metadata_features.rb` — info dict fields, xmp_packet?.
45
+
46
+ - `feature_serializer.rb` — `to_yaml`, `to_json`, `to_html_table`.
47
+
48
+ ### Diff integration
49
+
50
+ A `feature_diff` mode that compares two FeatureReports and emits a
51
+ summary ("font Helvetica added", "image /Im3 missing in v2"). This
52
+ is a fast pre-check before the full semantic comparator (TODO 55).
53
+
54
+ ## Done-When
55
+
56
+ - [ ] FeatureExtractor produces a complete FeatureReport on a
57
+ fixture PDF.
58
+ - [ ] All category extractors run without error on FOP-produced PDFs.
59
+ - [ ] YAML/JSON/HTML serialisation works.
60
+ - [ ] A simple "feature_diff" highlights added/removed/changed
61
+ features between two PDFs.
62
+ - [ ] Round-trip: extract from rendered PDF shows expected font
63
+ list, image count, page count.
64
+ - [ ] Spec coverage for each category extractor.
@@ -0,0 +1,75 @@
1
+ ---
2
+ priority: P2
3
+ phase: 19
4
+ depends_on: [62, 64]
5
+ layer: conformance
6
+ est: 3d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Modeled on `veraPDF/metadata/fixer/`, Arrolio needs an auto-fixer
13
+ that takes the failed assertions from a conformance profile and
14
+ applies known-safe fixes:
15
+
16
+ - Missing `/CreationDate` → fill in `Time.now`.
17
+ - Missing `/Producer` → `"Arrolio #{Arrolio::VERSION} + Pdfrb"`.
18
+ - Missing `/MarkInfo /true` → add (when StructTree exists).
19
+ - Missing `/Tabs /S` on pages → add.
20
+ - Missing font `/ToUnicode` → generate from font encoding (best effort).
21
+ - XMP packet missing → emit one from `/Info` fields.
22
+
23
+ The fixer is **opt-in**: it never modifies without being asked. It
24
+ also reports what it changed.
25
+
26
+ ## Approach
27
+
28
+ Files under `lib/arrolio/conformance/fixer/`:
29
+
30
+ - `base_fixer.rb` — abstract; takes a `Pdfrb::Document` + a
31
+ `ValidationResult`, returns a `FixResult`.
32
+
33
+ - `fix_result.rb` — value object: applied_fixes (`Array<AppliedFix>`),
34
+ skipped_fixes (`Array<SkippedFix>`), unchanged.
35
+
36
+ - `applied_fix.rb` — value object: rule_id, fix_kind, before, after,
37
+ location.
38
+
39
+ - Per-rule fixers (one file each, loaded only when the rule failed):
40
+ - `creation_date_fixer.rb`
41
+ - `producer_fixer.rb`
42
+ - `mark_info_fixer.rb`
43
+ - `tabs_fixer.rb`
44
+ - `to_unicode_fixer.rb`
45
+ - `xmp_packet_fixer.rb`
46
+
47
+ Each fixer is **conservative**: if it can't be sure the fix is
48
+ correct, it skips (records the reason) rather than guessing.
49
+
50
+ ### Pipeline integration
51
+
52
+ ```ruby
53
+ # After render:
54
+ doc = render_to_pdfrb_doc(content, layout_spec)
55
+ report = Arrolio::Conformance::Validator.(
56
+ Arrolio::Conformance::Profiles::Pdfa1b.profile,
57
+ doc
58
+ )
59
+ fixed_doc, fix_result = Arrolio::Conformance::Fixer.(doc, report)
60
+ fixed_doc.write(io: output_io)
61
+ ```
62
+
63
+ `Fixer` is opt-in via a render option `fix_conformance: true`.
64
+
65
+ ## Done-When
66
+
67
+ - [ ] A PDF missing `/Producer` gets it added.
68
+ - [ ] A PDF missing `/CreationDate` gets it added.
69
+ - [ ] A PDF missing `/MarkInfo /true` (but with StructTree) gets it
70
+ added.
71
+ - [ ] Each applied fix is recorded with before/after evidence.
72
+ - [ ] Fixer skips fixes it's not sure about (no guessing).
73
+ - [ ] Round-trip: fixed PDF re-validated passes the rules it failed
74
+ before.
75
+ - [ ] `render(fix_conformance: true)` flag wires it in.
@@ -0,0 +1,169 @@
1
+ # TODO.implementation — Arrolio Work Breakdown
2
+
3
+ Arrolio is the **Ruby-only alternative to Apache FOP / XSL-FO**. It takes
4
+ a typed content tree plus a typed layout spec, lays out pages, and renders
5
+ to PDF via the sibling `pdfrb` gem. No XML input. No FO intermediate.
6
+ The Ruby model is the source of truth.
7
+
8
+ This directory tracks all implementation work as one file per TODO,
9
+ named `{NN}-{slug}.md`.
10
+
11
+ ## Architectural layering (recap)
12
+
13
+ ```
14
+ Content Tree (Ruby, typed, medium-independent)
15
+
16
+ LayoutSpec (Ruby, typed: PageTemplate, Region, Style, Flow)
17
+
18
+ Engine::Paged (Knuth-Plass breaking, frame threading)
19
+
20
+ Output::Page[] (laid-out pages with placed boxes)
21
+
22
+ Renderer::Pdf (walks Output tree, emits bytes via Pdfrb)
23
+
24
+ PDF bytes
25
+ ```
26
+
27
+ Each layer is independently testable. Output::Page[] is the contract
28
+ between Engine and Renderer.
29
+
30
+ ## Primary end-to-end correctness: three subsystems
31
+
32
+ Arrolio is validated by three complementary subsystems, all
33
+ exercised against real OIML documents:
34
+
35
+ 1. **OIML E2E pipeline** (TODOs 52–54) — parse real OIML XML via
36
+ `metanorma-document`, apply an OIML LayoutSpec, render to PDF via
37
+ Pdfrb. Proves the full pipeline works end-to-end.
38
+
39
+ 2. **PDF diff subsystem** (TODOs 55–59), modeled on
40
+ `~/src/lutaml/canon/` — semantic comparator with dimensions
41
+ (text, font, position, colour, image, vector, structure),
42
+ normative/informative classifier, multiple formatters, RSpec
43
+ matchers, optional pixel diff. Catches "output changed in a way
44
+ that matters".
45
+
46
+ 3. **Conformance subsystem** (TODOs 62–66), modeled on
47
+ `~/src/external/veraPDF-library/` — rule-based validator with
48
+ profiles for layout baseline (TODO 63), PDF/A (TODO 64), and
49
+ PDF/UA (TODO 64); feature extractor for debugging (TODO 65);
50
+ auto-fixer for known-safe repairs (TODO 66). Catches "output is
51
+ wrong even when it matches yesterday's output".
52
+
53
+ ## Critical path (P0)
54
+
55
+ To reach **minimum viable PDF** (a one-page document with text), in order:
56
+
57
+ ```
58
+ 01 → 02 → 06 → 07 → 09 → 10 → 11 → 13
59
+
60
+ 21 → 22 → 23 (output) ←── 20 (engine) ←── 16-19 (flowables)
61
+ ```
62
+
63
+ After that, page templates (Phase 7) and tables (Phase 8) deliver
64
+ FOP-equivalent capability. Then the OIML E2E pipeline (Phase 17)
65
+ proves the engine on real-world documents, and the diff subsystem
66
+ makes correctness verifiable per-commit.
67
+
68
+ ## Phase index
69
+
70
+ | Phase | Range | Layer | Outcome |
71
+ |---|---|---|---|
72
+ | 1 | 01–05 | foundation | Skeleton, content contract, layout spec, style system, visitors |
73
+ | 2 | 06–09 | metrics | AFM + TrueType glyph metrics + Registry + GlyphMeasurer |
74
+ | 3 | 10–13 | text | Break opportunities, Greedy + Knuth-Plass, Line + alignment |
75
+ | 4 | 14–15 | knuth | Universal Knuth elements + Breaker (refactor of Phase 3) |
76
+ | 5 | 16–20 | flowable | Flowable hierarchy + Frame + Engine::Paged driver |
77
+ | 6 | 21–23 | output | Output::Page[] + Renderer::Pdf + end-to-end smoke |
78
+ | 7 | 24–26 | template | PageTemplate, PageSequenceMaster, two-pass citations |
79
+ | 8 | 27–30 | table | Table model + Fixed/Auto layouts + TableFlowable |
80
+ | 9 | 31–32 | list | List model + ListFlowable (proper label/body columns) |
81
+ | 10 | 33–35 | media | Image loading via Pdfrb + SVG renderer + Form XObjects |
82
+ | 11 | 36–38 | inline | InlineBuilder + Hyperlink + Sub/superscript |
83
+ | 12 | 39–42 | xref | Leader + FieldRuns + Bookmark/outline + Destinations + TOC |
84
+ | 13 | 43–45 | page | Footnotes + Multi-column + Page numbering + Section tracking |
85
+ | 14 | 46–47 | color | Color management + XMP/Info metadata write |
86
+ | 15 | 48–49 | a11y | PDF/UA StructTree + marked content + alt text |
87
+ | 16 | 50–51 | composer | High-level Composer facade + style presets |
88
+ | 17 | **52–59** | **harness/diff** | **OIML E2E pipeline + canon-style diff subsystem** |
89
+ | 18 | 60–61 | polish | YARD docs + performance/stress tests |
90
+ | 19 | **62–66** | **conformance** | **veraPDF-style validator + profiles + feature extractor + fixer** |
91
+
92
+ **Total: 66 TODOs.**
93
+
94
+ ## Phase 17 in detail — OIML E2E + diff subsystem
95
+
96
+ | TODO | What it builds |
97
+ |---|---|
98
+ | 52 | OIML adapter (metanorma-document → Arrolio content) + fixture corpus |
99
+ | 53 | OIML LayoutSpec — Ruby encoding of `oiml.xsl`'s visual intent |
100
+ | 54 | Pipeline (`Arrolio::Oiml::Pipeline.(xml, io:)`) + CLI + sanity spec |
101
+ | 55 | **PDF comparator** — semantic tree diff with dimensions, normative/informative classifier, profiles |
102
+ | 56 | **Diff formatters** — summary, pretty, by-page, by-element, JSON, HTML, JUnit |
103
+ | 57 | **Path locator + enrichment** — canonical paths, char-level diffs, source positions |
104
+ | 58 | **RSpec matchers + Harness API** — `match_pdf`, `be_equivalent_to_pdf`, `Arrolio::Harness.diff` |
105
+ | 59 | **Pixel diff mode** — optional raster-based diff for visual regressions |
106
+
107
+ ## Phase 19 in detail — Conformance & validation (veraPDF-inspired)
108
+
109
+ | TODO | What it builds |
110
+ |---|---|
111
+ | 62 | Validation framework — Rule, Profile, TestAssertion, ValidationResult, Validator, Location |
112
+ | 63 | Layout baseline rules — no orphan headings, no widow/orphans, contiguous page numbering |
113
+ | 64 | PDF/A + PDF/UA conformance profiles — pragmatic rule subset; cross-check vs veraPDF |
114
+ | 65 | Feature extractor — fonts/images/colour/annotations dump for debugging |
115
+ | 66 | Auto-fixer — applies known-safe fixes (CreationDate, Producer, MarkInfo, XMP) |
116
+
117
+ After TODO 66 lands, Arrolio can self-validate its own output against
118
+ PDF/A and PDF/UA rules — a veraPDF in Ruby for documents Arrolio
119
+ produces.
120
+
121
+ ## Front-matter shape
122
+
123
+ ```yaml
124
+ ---
125
+ priority: P0 # P0 blocks MVP; P1 = FOP parity; P2 = production; P3 = polish
126
+ phase: 1
127
+ depends_on: [] # TODO numbers that must be done first
128
+ layer: foundation
129
+ est: 2d # 1d = single focused session; 5d = multi-week module
130
+ status: pending # pending | in_progress | done
131
+ ---
132
+ ```
133
+
134
+ ## What Arrolio deliberately does NOT do
135
+
136
+ - Parse XML input (FO or otherwise). **Loom's** job.
137
+ - Author DSLs (YAML, custom syntax). **Loom's** job.
138
+ - Compile to multiple targets (CSS, FO, IDML). **Loom's** job.
139
+ - Speak PDF byte format directly. **Pdfrb's** job.
140
+ - Own the semantic content model. **metanorma-document's** job —
141
+ Arrolio defines a content contract, metanorma-document fills it.
142
+ - Replace veraPDF authoritatively for PDF/A. **veraPDF's** job.
143
+ Arrolio's conformance profiles (Phase 19) are a pragmatic
144
+ pre-check; veraPDF remains authoritative.
145
+
146
+ If a TODO crosses these boundaries, it belongs in another gem.
147
+
148
+ ## Verification
149
+
150
+ Every TODO carries Done-When criteria. The global verification is:
151
+
152
+ - `bundle exec rake` — runs rspec + rubocop; must be green after every TODO.
153
+ - After TODO 23 (end-to-end smoke): a single-paragraph PDF renders correctly.
154
+ - After TODO 30 (TableFlowable): an OIML-style multi-page doc with tables
155
+ renders.
156
+ - After TODO 54 (OIML pipeline): real OIML XML renders to PDF.
157
+ - After TODO 58 (RSpec matchers): every commit validates against FOP
158
+ references with `be_equivalent_to_pdf`.
159
+ - After TODO 64 (PDF/A + PDF/UA): Arrolio's output passes the
160
+ common-subset conformance rules.
161
+
162
+ ## Status legend
163
+
164
+ - `pending` — not started.
165
+ - `in_progress` — actively being worked.
166
+ - `done` — complete; specs pass; ready for review.
167
+
168
+ (Initial seed: all 66 TODOs are `pending`. TODO 01 is partially done —
169
+ the gem skeleton already exists.)