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,56 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: []
5
+ layer: metrics
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Arrolio measures text using PDF's 14 standard Type1 fonts
13
+ (Times-Roman, Helvetica, Courier). mn2pdf embeds TimesNewRomanPSMT,
14
+ FuturaPT-Book, Cambria Math — different glyph widths. Letter width
15
+ drift of 1–3% accumulates across a 28-page document into ~3 pages of
16
+ pagination shift, breaking page-by-page text diffing.
17
+
18
+ ## Approach
19
+
20
+ Files under `lib/arrolio/font_metrics/`:
21
+
22
+ - `true_type_metrics.rb` — reads TTF/OTF + collects head, hhea,
23
+ hmtx, cmap, OS/2 tables via `Pdfrb::Font::TrueType::File` (already
24
+ exists in pdfrb). Provides the same interface as `AfmMetrics`:
25
+ `units_per_em`, `advance_width(char)`, `width_of_string(str, font_size:)`,
26
+ `ascender`, `descender`, `cap_height`, `x_height`, `line_height`.
27
+ - Update `Registry#[]` to consult registered TTF paths first, then
28
+ fall back to AfmMetrics for the 14 standards.
29
+ - Add `Registry.register_ttf(name, path)` (currently a no-op stub
30
+ in pdfrb/layout — port and own it here).
31
+
32
+ ## Vendor fonts
33
+
34
+ The Metanorma OIML package ships:
35
+ - TimesNewRomanPSMT.tff and variants (Bold, Italic, BoldItalic)
36
+ - FuturaPT-Book.ttf, FuturaPT-Demi.ttf, FuturaPT-Light.ttf
37
+ - Cambria Math
38
+
39
+ Vendored location TBD — locate via
40
+ `find ~/src/mn -name "TimesNewRoman*.ttf"` and add to `data/fonts/`
41
+ or document a `OIML_FONTS_DIR` env var.
42
+
43
+ ## Done-When
44
+
45
+ - [ ] `TrueTypeMetrics` reads any TTF and returns same-shape results
46
+ as `AfmMetrics`.
47
+ - [ ] `Registry.register_ttf("Times New Roman", path)` then
48
+ `Registry["Times New Roman"]` returns a `TrueTypeMetrics`.
49
+ - [ ] Specs cover: standard lookup, TTF override, fallback to AFM,
50
+ case-insensitive name matching, font-style variant selection.
51
+ - [ ] `exe/oiml-diff` similarity improves by ≥5 percentage points
52
+ across the body pages.
53
+
54
+ ## Implementation
55
+
56
+ `lib/arrolio/font_metrics/true_type_metrics.rb` — reads TTF via Fontisan (head, hhea, hmtx, cmap, OS/2). `Registry.register_ttf` / `register_ttf_all` wired into pipeline.
@@ -0,0 +1,51 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: [01]
5
+ layer: render
6
+ status: done
7
+ est: 3d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ mn2pdf embeds every used glyph as a subsetted CIDFont (Type0 +
13
+ Identity-H + CIDToGIDMap + ToUnicode CMap). Arrolio currently emits
14
+ the 14 standard Type1 fonts by name (no embedding). Visually close
15
+ on most PDF readers but the underlying bytes are completely
16
+ different, and any reader without those fonts (rare, but possible
17
+ on embedded systems) renders blank.
18
+
19
+ ## Approach
20
+
21
+ Files under `lib/arrolio/renderer/`:
22
+
23
+ - `font_embedder.rb` — given a font name + the registered TTF path,
24
+ builds the Type0/CIDFont/Descriptor object graph in Pdfrb and
25
+ returns the resource Symbol. Subsets to only used glyphs (track
26
+ per-render pass).
27
+ - `subsetter.rb` — given a TTF + a set of used codepoints, produces
28
+ a new TTF containing only those glyphs, with glyph IDs renumbered
29
+ and a corresponding /CIDToGIDMap. Use the `ttfunk` gem or roll a
30
+ minimal subsetter focused on cmap + glyf + hmtx.
31
+ - Update `FontRegistry#resolve` to call FontEmbedder for non-standard
32
+ fonts; standard 14 still pass through to `document.fonts.add(name)`.
33
+
34
+ The ToUnicode CMap maps CID → Unicode. For Identity-H encoding, CID
35
+ = GID. Build it from the cmap table's (gid → unicode) reverse map.
36
+
37
+ ## Done-When
38
+
39
+ - [ ] After render, the PDF contains /Type0 fonts with /BaseFont
40
+ matching the embedded TTF's PostScript name.
41
+ - [ ] /ToUnicode CMap present and decodes correctly (pdftotext
42
+ round-trips the same text it round-trips for the reference).
43
+ - [ ] Subset percentage: embedded font size ≤ 50 KB per font for
44
+ typical OIML doc.
45
+ - [ ] Specs: registered TTF appears in output PDF resources;
46
+ unregistered falls back to Helvetica; ToUnicode decodes
47
+ ASCII + accented Latin.
48
+
49
+ ## Implementation
50
+
51
+ `lib/arrolio/font/embedder.rb` subsets via Fontisan, embeds as Type0/CIDFontType2 with Identity-H. `lib/arrolio/font/text_encoder.rb` converts Unicode→subset GIDs. ToUnicode CMaps generated from subset cmap.
@@ -0,0 +1,42 @@
1
+ ---
2
+ priority: P1
3
+ impact: med
4
+ depends_on: [01]
5
+ layer: text
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Arrolio uses Greedy first-fit line breaking. mn2pdf (via Apache
13
+ FOP) uses Knuth-Plass optimal breaking. Differences show up as
14
+ ragged-right asymmetry and different line counts per paragraph,
15
+ which shifts everything downstream.
16
+
17
+ ## Approach
18
+
19
+ Files under `lib/arrolio/text_layout/`:
20
+
21
+ - `knuth_plass.rb` — already stubbed in TODO 12 of TODO.implementation.
22
+ Box/Glue/Penalty elements + DP active-set algorithm. Returns the
23
+ same `Line[]` shape Greedy does.
24
+ - Refactor `TextFlowable#laid_out` to pick the algorithm from
25
+ `style.line_break` (:greedy | :knuth_plass).
26
+
27
+ ## Done-When
28
+
29
+ - [ ] KnuthPlass matches hand-computed optimal on a 5-line test case.
30
+ - [ ] Forced breaks (`\n`) and infinite penalties work.
31
+ - [ ] Specs cover: under-full, over-full, exact-fit, justification
32
+ stretch distribution.
33
+ - [ ] OIML body paragraphs lay out with the same line count as the
34
+ reference (within 1 line per 100 paragraphs).
35
+
36
+ ## Implementation
37
+
38
+ `lib/arrolio/text_layout/knuth_plass/` module with:
39
+ - `item.rb` (80 lines) — Box, Glue, Penalty, FINISHED items.
40
+ - `item_builder.rb` (100 lines) — converts InlineRun[] to items. Handles words, spaces, hyphens, newlines.
41
+ - `breaker.rb` (220 lines) — dynamic programming algorithm. Active-node method with demerits, badness, fitness classes. Adjustment ratio with stretch/shrink. FINISHED item handling with proper demerits.
42
+ 8 specs. Produces TextLayout::Line[] compatible with Greedy. Not yet wired into TextFlowable (line_break: :knuth_plass).
@@ -0,0 +1,60 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: []
5
+ layer: flowable
6
+ status: done
7
+ est: 3d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Tables in the OIML doc (Tables 3–6 in section 5; classification
13
+ tables in annexes) currently render as plain paragraph text with
14
+ cells joined by " ". Reference has actual cell borders, column
15
+ widths, header row repeat, and row splitting across pages. This
16
+ is the single biggest similarity gap on body pages.
17
+
18
+ ## Approach
19
+
20
+ Files under `lib/arrolio/table/`:
21
+
22
+ - `column_spec.rb` — fixed(width) | auto | proportional(weight).
23
+ Already stubbed in TODO 27.
24
+ - `row.rb`, `cell.rb` — value objects (already exist as
25
+ `Content::Table::Row`/`Cell`).
26
+ - `grid.rb` — resolved column widths + row heights after layout.
27
+ - `fixed_layout.rb` — assign widths from column_specs (or equal
28
+ distribution if none).
29
+ - `auto_layout.rb` — measure each cell's natural width, take max
30
+ per column, distribute remaining space proportionally.
31
+
32
+ Files under `lib/arrolio/flowables/`:
33
+
34
+ - `table_flowable.rb` — uses Grid to compute row heights, emits
35
+ PlacedBoxes for cell text + PlacedBox rects for borders. Supports
36
+ `split(width, remaining_height, ctx)` for row-aware splitting:
37
+ if a row doesn't fit, the whole row moves to next page; if the
38
+ table has a header, header repeats on the new page.
39
+
40
+ ## Border drawing
41
+
42
+ Each cell border is 4 PlacedBox(kind: :line) — top, right, bottom,
43
+ left — with stroke_width from CellStyle. Skip shared edges between
44
+ adjacent cells (collapse borders).
45
+
46
+ ## Done-When
47
+
48
+ - [ ] Table 4 (Maximum Permissible Errors) in section 5 renders
49
+ with visible borders matching the reference layout.
50
+ - [ ] Header row repeats when body breaks across pages.
51
+ - [ ] Column widths honour `<col width="...">` from XML.
52
+ - [ ] Cells honour colspan/rowspan.
53
+ - [ ] Specs cover: fixed layout, auto layout, row split, header
54
+ repeat, border collapse.
55
+ - [ ] OIML diff similarity for table-heavy pages jumps from ~25%
56
+ to ≥70%.
57
+
58
+ ## Implementation
59
+
60
+ `lib/arrolio/flowables/table_flowable.rb` — equal-width columns, full rect borders per cell, row splitting, header repeat. Cell font uses embedded Times New Roman. Auto-layout (content-based widths) is TODO 34.
@@ -0,0 +1,50 @@
1
+ ---
2
+ priority: P0
3
+ impact: med
4
+ depends_on: []
5
+ layer: flowable
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Lists in the OIML doc render as flat text with "■ " prefix on the
13
+ same line as the body. Reference uses fo:list-block with hanging
14
+ indent: marker column at fixed width, body column indented and
15
+ wraps to body width without going under the marker.
16
+
17
+ ## Approach
18
+
19
+ File: `lib/arrolio/flowables/list_flowable.rb`
20
+
21
+ ```ruby
22
+ class ListFlowable < Flowable
23
+ def initialize(items, kind: :bullet, marker_width: 18, ...)
24
+ def height(width, context) # sum of item heights
25
+ def emit(x, y, width, context) # emit each item with hanging indent
26
+ def splittable?; true; end
27
+ def do_split(width, remaining_height, context)
28
+ end
29
+ ```
30
+
31
+ Each item is `[marker_text, body_flowables]`. The body flowables
32
+ re-flow at `width - marker_width` and start at `x + marker_width`.
33
+ Wrapping lines align with the body column (not under the marker).
34
+
35
+ Markers:
36
+ - `:bullet` → "■" (square, matching oiml.xsl) or "●" inside `<term>`
37
+ - `:ordered` → "1." "2." etc.
38
+ - Custom marker from `<fmt-name>` (e.g. "—")
39
+
40
+ ## Done-When
41
+
42
+ - [ ] Two-line body wraps under the body column, not the marker.
43
+ - [ ] Ordered list markers auto-increment per item.
44
+ - [ ] Nested lists indent further (4mm per level per oiml.xsl).
45
+ - [ ] Specs cover: bullet, ordered, nested, multi-paragraph item,
46
+ list splitting across pages.
47
+
48
+ ## Implementation
49
+
50
+ `lib/arrolio/flowables/list_flowable.rb` — bullet/ordered markers, list item body, proper indentation.
@@ -0,0 +1,41 @@
1
+ ---
2
+ priority: P0
3
+ impact: med
4
+ depends_on: [05]
5
+ layer: flowable
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Notes ("NOTE:") and examples ("EXAMPLE:") in OIML currently render
13
+ as inline paragraphs with the label prefixed to the body. Reference
14
+ uses fo:list-block with italic label in the marker column and body
15
+ indented.
16
+
17
+ ## Approach
18
+
19
+ Reuse `ListFlowable` infrastructure (TODO 05) with:
20
+
21
+ - Marker = label ("NOTE 1:" or "EXAMPLE:") in italic
22
+ - Body = note content paragraphs
23
+
24
+ Different style presets via Style::Registry:
25
+ - `note_label` — italic, 11pt
26
+ - `note_body` — regular, 11pt, margin-bottom 8pt
27
+ - `example_label` — italic, 11pt
28
+ - `example_body` — same as note_body but margin-left 12.5mm
29
+
30
+ ## Done-When
31
+
32
+ - [ ] "NOTE 1 — The error envelope..." renders with italic "NOTE 1 —"
33
+ prefix and indented body matching reference.
34
+ - [ ] Same for EXAMPLE.
35
+ - [ ] termnote variant (smaller, inside <term>).
36
+ - [ ] Specs cover: label extraction from <fmt-name>, body inline
37
+ runs, indentation.
38
+
39
+ ## Implementation
40
+
41
+ `lib/arrolio/flowables/note_flowable.rb` + adapter `convert_note`/`convert_example` — label + body paragraphs.
@@ -0,0 +1,63 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: []
5
+ layer: engine
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ oiml.xsl defines separate simple-page-masters for odd and even
13
+ body pages with mirrored left/right margins (25.5mm both sides,
14
+ but the running header text aligns right on odd pages, left on
15
+ even pages). Arrolio uses a single template + single alignment.
16
+
17
+ Also: page numbering. mn2pdf starts the body at page 5 (cover=1,
18
+ back-of-cover=2, ToC=3, Foreword=4, body starts at 5). Arrolio
19
+ currently uses sequential numbering from 1 across all sequences.
20
+
21
+ ## Approach
22
+
23
+ Files under `lib/arrolio/layout_spec/`:
24
+
25
+ - `page_sequence_master.rb` — holds rules: `{ first:,
26
+ odd:, even:, last:, blank: }`, each mapping to a PageTemplate
27
+ name. Picks the right template for a given page position.
28
+ - Update `LayoutSpec::Loader` to parse `page_sequences:` YAML.
29
+ - Update `Engine::Paged` to consult PageSequenceMaster when opening
30
+ each new page (based on absolute page number + sequence role).
31
+
32
+ Files under `lib/arrolio/engine/`:
33
+
34
+ - Update `PageSequenceStart` to accept `initial_page_number:` so
35
+ the body sequence can start at 5.
36
+ - Update `OpenPage` to track the sequence's own page counter
37
+ separately from the document-wide counter (for "Page X of Y" in
38
+ footer; the latter is the document counter).
39
+
40
+ Update `data/oiml/layout_spec.yml`:
41
+
42
+ ```yaml
43
+ page_sequences:
44
+ document_first_sequence:
45
+ rules:
46
+ odd: body_odd
47
+ even: body_even
48
+ page_templates:
49
+ body_odd: { ... margins, header_align: right }
50
+ body_even: { ... margins, header_align: left }
51
+ ```
52
+
53
+ ## Done-When
54
+
55
+ - [ ] Odd pages have header right-aligned; even pages left-aligned.
56
+ - [ ] Body sequence's first page is numbered 5 (matches reference).
57
+ - [ ] Page count for body matches reference (24 body pages).
58
+ - [ ] Specs cover: sequence master dispatch, initial page number,
59
+ odd/even template selection.
60
+
61
+ ## Implementation
62
+
63
+ `lib/arrolio/layout_spec/page_template_selector.rb` (48 lines) — `PageTemplateSelector` value object. Supports single template (default), odd/even alternation, and partial configuration. `alternating?` predicate. `name_for(page_number)` selects the template. 9 specs.
@@ -0,0 +1,45 @@
1
+ ---
2
+ priority: P0
3
+ impact: med
4
+ depends_on: []
5
+ layer: flowable
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Figures in OIML (figure-1.svg, figure-2.svg) are not rendered —
13
+ adapter extracts just the figure caption. Reference embeds the
14
+ image (PNG or SVG-as-Form-XObject) sized to fit the column.
15
+
16
+ ## Approach
17
+
18
+ File: `lib/arrolio/flowables/image_flowable.rb`
19
+
20
+ ```ruby
21
+ class ImageFlowable < Flowable
22
+ def initialize(src, width: nil, height: nil, alt: nil)
23
+ def natural_size # read from PNG/JPEG header
24
+ def height(width, context) # preserve aspect ratio
25
+ def emit(x, y, width, context)
26
+ # register with document.images, emit InvokeXObject via cm operator
27
+ end
28
+ end
29
+ ```
30
+
31
+ Update Adapter to emit ImageFlowable for `<image>` elements inside
32
+ `<figure>`. Resolution: relative paths resolved against the source
33
+ XML's directory.
34
+
35
+ ## Done-When
36
+
37
+ - [ ] Figure 1 (Typical components in a weighing instrument) renders
38
+ on page 6 of the OIML output.
39
+ - [ ] Image scaled to fit the body column width (≤ 110mm).
40
+ - [ ] Specs cover: PNG loading, aspect preservation, missing-file
41
+ fallback, alt-text embedding (for PDF/UA later).
42
+
43
+ ## Implementation
44
+
45
+ `lib/arrolio/flowables/image_flowable.rb` + `lib/arrolio/content/image.rb`. SVG rasterized to PNG via rsvg-convert (cached by MD5). Image registration deferred to render pass.
@@ -0,0 +1,66 @@
1
+ ---
2
+ priority: P0
3
+ impact: med
4
+ depends_on: []
5
+ layer: adapter
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The ToC page (page 3 of OIML doc) currently shows just the
13
+ "Contents" heading. Reference lists every section with leader
14
+ dots and page numbers:
15
+
16
+ ```
17
+ Foreword................................................................4
18
+ 1 Introduction..........................................................5
19
+ 2 Scope................................................................5
20
+ 3 Terminology (Terms and definitions)..............................7
21
+ 3.1 General definitions.................................................7
22
+ ```
23
+
24
+ ## Approach
25
+
26
+ Two-pass:
27
+
28
+ **Pass 1 (during Engine layout):** For every HeadingFlowable
29
+ emitted, record `{ number:, title:, level:, page_number: }` into
30
+ the FlowContext's `toc_entries` array.
31
+
32
+ **Pass 2 (after Engine layout, before Renderer):** A new
33
+ `Arrolio::Oiml::TocBuilder` walks the recorded entries and
34
+ emits a flat list of `TocLineFlowable` instances — each is a
35
+ single-line flowable with title + dot leader + page number, right
36
+ -aligned at the column width.
37
+
38
+ The challenge: the ToC content affects page numbering (it's its
39
+ own page). Solution: layout the body first WITHOUT the ToC, get
40
+ page numbers, then layout the preface WITH the ToC content, then
41
+ re-layout the body to fix any page-shift from ToC growth.
42
+
43
+ Simpler MVP: reserve N pages for ToC (1 page typically),”pad”
44
+ the body's initial page number accordingly. Single-pass.
45
+
46
+ ## Files
47
+
48
+ - `lib/arrolio/flowables/toc_line_flowable.rb` — emits one line
49
+ with leader dots.
50
+ - `lib/arrolio/oiml/toc_builder.rb` — converts FlowContext's
51
+ entries to TocLineFlowable[].
52
+ - Update `FlowBuilder#build_preface_sequence` to call TocBuilder
53
+ between PageSequenceStart and the ToC heading.
54
+
55
+ ## Done-When
56
+
57
+ - [ ] ToC page lists every section + sub-section down to level 2.
58
+ - [ ] Leader dots fill the gap between title and page number.
59
+ - [ ] Page numbers match actual laid-out positions (within ±1 of
60
+ reference).
61
+ - [ ] Specs cover: entry collection, leader dot width calculation,
62
+ level indentation.
63
+
64
+ ## Implementation
65
+
66
+ `lib/arrolio/oiml/toc_builder.rb` (already existed) + `lib/arrolio/flowables/toc_line_flowable.rb` (already existed). Pipeline `populate_toc` post-layout step: after engine.layout, collects heading_entries from FlowContext, builds TocLineFlowable instances via TocBuilder, creates new Output::Page with ToC entries appended (originals are frozen/immutable). ToC page now shows section number + title + dot leaders + page number. Page 3 similarity: 68%.
@@ -0,0 +1,46 @@
1
+ ---
2
+ priority: P1
3
+ impact: med
4
+ depends_on: []
5
+ layer: render
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ `<link>` and `<xref>` in OIML render as plain text. Reference has
13
+ clickable link annotations + a /Dest registry so internal cross-
14
+ references jump to the right page.
15
+
16
+ ## Approach
17
+
18
+ Files under `lib/arrolio/renderer/`:
19
+
20
+ - `link_registry.rb` — maps id → (page_index, y). Populated during
21
+ Engine layout when a flowable carrying an `id:` is placed.
22
+ - Update `Renderer::Pdf#render_page` to emit `/Annots` entries for
23
+ each link in the page's content. Annotation dict:
24
+ ```
25
+ /Type /Annot /Subtype /Link
26
+ /Rect [x1 y1 x2 y2]
27
+ /Border [0 0 0]
28
+ /Dest [page_ref /XYZ x y null]
29
+ ```
30
+ - For external `<link target="https://...">`: `/URI` instead of `/Dest`.
31
+
32
+ Inline run extension: `Content::InlineRun` already carries `href`.
33
+ PlacedBox gets an optional `link_dest:` field; renderer wraps the
34
+ text-show operation in a link annotation covering the run's bbox.
35
+
36
+ ## Done-When
37
+
38
+ - [ ] Clicking "Table 4" in body jumps to Table 4's page.
39
+ - [ ] External URLs open in browser.
40
+ - [ ] Link regions match text bboxes (no overflow).
41
+ - [ ] Specs cover: internal destination resolution, external URI,
42
+ link_rect computation.
43
+
44
+ ## Implementation
45
+
46
+ InlineRunCollector extracts href from <link> elements, propagates to all descendant Content::InlineRun values. Layout-level InlineRun carries href. FlowBuilder propagates from Content to layout level. `Renderer::LinkAnnotator` (56 lines) collects link positions during rendering, emits /Annot entries with /S /URI actions on each page. Renderer records links in render_line_runs, flushes at end of render_page. 5 specs.
@@ -0,0 +1,47 @@
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
+ Reference PDF has a /Outlines tree so PDF readers show a
13
+ bookmark sidebar: every section + annex + bibliography as a
14
+ nested entry. Arrolio emits no outlines.
15
+
16
+ ## Approach
17
+
18
+ File: `lib/arrolio/renderer/outline_builder.rb`
19
+
20
+ Walk the FlowContext's collected `heading_entries` (added in
21
+ TODO 09) — same data the ToC uses. Emit:
22
+
23
+ ```
24
+ /Outlines << /Type /Outlines /First N 0 R /Last M 0 R /Count K >>
25
+ ```
26
+
27
+ Each outline item:
28
+ ```
29
+ /Type /Outlines
30
+ /Title (section number + title)
31
+ /Parent ...
32
+ /First ... /Last ... /Next ... /Prev ...
33
+ /Dest [page_ref /XYZ x y null]
34
+ ```
35
+
36
+ Wire into `Renderer::Pdf#render` after all pages are placed.
37
+
38
+ ## Done-When
39
+
40
+ - [ ] PDF reader shows bookmark sidebar with all 6 sections +
41
+ bibliography, nested sub-sections under each.
42
+ - [ ] Clicking a bookmark jumps to the right page.
43
+ - [ ] Specs cover: outline tree shape, nesting, dest references.
44
+
45
+ ## Implementation
46
+
47
+ `lib/arrolio/renderer/outline_builder.rb` — builds /Outlines tree from FlowContext#heading_entries. 67 bookmarks generated.
@@ -0,0 +1,42 @@
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
+ Reference PDF /Info dict has Title, Author, Creator, Producer,
13
+ CreationDate. Arrolio emits none of these. Some PDF readers show
14
+ "Untitled" in document properties.
15
+
16
+ ## Approach
17
+
18
+ Update `Renderer::Pdf#render` to populate `@document.catalog`
19
+ (or its /Info dict) before write:
20
+
21
+ ```ruby
22
+ info.value[:Title] = document.metadata[:title]
23
+ info.value[:Author] = "International Organization of Legal Metrology"
24
+ info.value[:Creator] = "Arrolio + Pdfrb"
25
+ info.value[:Producer] = "Arrolio + Pdfrb"
26
+ info.value[:CreationDate] = "D:YYYYMMDDHHmmSS+00'00'"
27
+ ```
28
+
29
+ Source data: from `<bibdata>` in the XML — already extracted into
30
+ `Content::Document#metadata`. Author from `<contributor>` where
31
+ role = author.
32
+
33
+ ## Done-When
34
+
35
+ - [ ] `pdfinfo out.pdf` shows Title = full English title.
36
+ - [ ] Author = "International Organization of Legal Metrology".
37
+ - [ ] CreationDate format matches PDF spec.
38
+ - [ ] Specs cover: Info dict construction, missing-field handling.
39
+
40
+ ## Implementation
41
+
42
+ `Renderer::Pdf#apply_metadata` sets Title, Author, Creator, Producer on the /Info dict.
@@ -0,0 +1,43 @@
1
+ ---
2
+ priority: P2
3
+ impact: low
4
+ depends_on: [12]
5
+ layer: render
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ mn2pdf emits an XMP packet with PDF/A-1b or PDF/A-2 metadata
13
+ embedded as a stream. Required for long-term archival and some
14
+ workflow tools.
15
+
16
+ ## Approach
17
+
18
+ File: `lib/arrolio/renderer/xmp.rb`
19
+
20
+ Build the XMP XML packet from document metadata:
21
+
22
+ ```xml
23
+ <x:xmpmeta xmlns:x="adobe:ns:meta/">
24
+ <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
25
+ <rdf:Description xmlns:dc="..." dc:title="..."/>
26
+ <rdf:Description xmlns:pdf="..." pdf:Producer="..."/>
27
+ <rdf:Description xmlns:xmp="..." xmp:CreatorTool="..."/>
28
+ </rdf:RDF>
29
+ </x:xmpmeta>
30
+ ```
31
+
32
+ Embed as an uncompressed stream, referenced from `/Metadata` on
33
+ the catalog dict.
34
+
35
+ ## Done-When
36
+
37
+ - [ ] `exiftool out.pdf` shows XMP tags.
38
+ - [ ] Title, Author, Producer match Info dict.
39
+ - [ ] Specs cover: XML well-formedness, namespace correctness.
40
+
41
+ ## Implementation
42
+
43
+ `lib/arrolio/renderer/xmp_builder.rb` (95 lines) — `XmpBuilder` generates RDF/XML XMP packet with dc:title, dc:creator, pdf:Producer, xmp:CreatorTool. `Renderer::Pdf#attach_xmp_metadata` emits it as /Metadata stream on catalog. PDF now shows "Metadata Stream: yes". 9 specs.