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,59 @@
1
+ ---
2
+ priority: P1
3
+ phase: 10
4
+ depends_on: [16]
5
+ layer: media
6
+ est: 2d
7
+ status: in_progress
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Need a real ImageFlowable (not the placeholder from the FO spike).
13
+ It loads the image via Pdfrb, scales to target size, optionally
14
+ aligns (left/center/right), and is not splittable. Wraps it as a
15
+ Flowable that the engine places into a frame.
16
+
17
+ ## Approach
18
+
19
+ File: `lib/arrolio/flowables/image_flowable.rb`.
20
+
21
+ ```ruby
22
+ class Arrolio::Flowables::ImageFlowable < Arrolio::Flowable
23
+ def initialize(src, width: nil, height: nil, max_width: nil,
24
+ max_height: nil, align: :left)
25
+ def natural_size -> [w, h] # cached after first measurement
26
+ def height(target_width, context)
27
+ def render(canvas, x, y, width, context)
28
+ def splittable?; false; end
29
+ end
30
+ ```
31
+
32
+ Internals:
33
+ - On first `natural_size` call, ask Pdfrb to load the image
34
+ (Pdfrb::ImageLoader dispatches by format: JPEG, PNG, PDF page).
35
+ Cache the loaded image reference + its pixel dimensions.
36
+ - Scaling rules:
37
+ - Both `width:` and `height:` explicit → use as-is (may distort).
38
+ - Only `width:` → `height = width * natural_h / natural_w`.
39
+ - Only `height:` → symmetric.
40
+ - Neither, but `max_width:` / `max_height:` → scale down to fit,
41
+ never up.
42
+ - Neither → use natural size.
43
+
44
+ Render:
45
+ - `name = context.document.images.add(src)` (Pdfrb handles caching).
46
+ - `canvas.image(name, at: [x + align_offset, y], width:, height:)`.
47
+
48
+ Alignment offset: depends on `align` (:left = 0, :center = (width -
49
+ img_w) / 2, :right = width - img_w).
50
+
51
+ ## Done-When
52
+
53
+ - [ ] An 800×600 image with `width: 200` renders at 200×150.
54
+ - [ ] Same image with `max_width: 1000` renders at natural size.
55
+ - [ ] `align: :center` horizontally centres the image.
56
+ - [ ] Image is not splittable; if it doesn't fit, engine advances.
57
+ - [ ] PNG with alpha (SMask) renders correctly.
58
+ - [ ] JPEG passthrough works (no re-encode).
59
+ - [ ] Round-trip: rendered PDF re-read has expected /XObject.
@@ -0,0 +1,58 @@
1
+ ---
2
+ priority: P1
3
+ phase: 10
4
+ depends_on: [22]
5
+ layer: media
6
+ est: 3d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ SVG inside documents (`<svg>...</svg>` blocks, FOP's
13
+ `fo:instream-foreign-object`) need a renderer. The SVG renderer
14
+ walks the SVG tree and emits drawing operations on a Canvas (Pdfrb
15
+ content stream). Supports the FOP/Batik output subset.
16
+
17
+ ## Approach
18
+
19
+ Files under `lib/arrolio/svg/`:
20
+
21
+ - `document.rb` — wraps a parsed SVG (REXML); exposes `width`,
22
+ `height`, `view_box`, `root`.
23
+
24
+ - `style.rb` — resolved style from element attributes (fill, stroke,
25
+ stroke_width, opacity, font_*). Inherits from parent. Class
26
+ method `color_to_pdf(color_string)` resolves named/hex to PDF
27
+ Canvas colour.
28
+
29
+ - `transform_parser.rb` — `parse("translate(10,20) rotate(45)")`
30
+ → list of op/args. `to_matrix(transforms)` → 6-element affine.
31
+
32
+ - `path_parser.rb` — `parse("M 10 10 L 90 10 Z")` → list of
33
+ `{cmd:, absolute:, args:}`. Supports M L H V C S Q T A Z.
34
+
35
+ - `element.rb` — base class with `register "name"` class method;
36
+ `for_element(xml, style:)` dispatcher.
37
+
38
+ - Element subclasses under `lib/arrolio/svg/element/`:
39
+ - `group.rb` (g) — applies transform + recurses.
40
+ - `path.rb` — PathState accumulator; emits move/line/curve/close.
41
+ - `rect.rb`, `circle.rb`, `ellipse.rb`, `line.rb`,
42
+ `polyline.rb`, `polygon.rb` — basic shapes.
43
+ - `text.rb` — single-line text.
44
+ - `image.rb` — embedded raster (data: URI or href).
45
+
46
+ - `renderer.rb` — walks Document, dispatches to element classes,
47
+ maintains transform/style stack.
48
+
49
+ ## Done-When
50
+
51
+ - [ ] `<rect width="100" height="50" fill="red"/>` renders a red rectangle.
52
+ - [ ] `<circle cx="50" cy="50" r="20"/>` renders via Bezier approximation.
53
+ - [ ] `<path d="M 10 10 L 90 10 L 50 90 Z" fill="red"/>` renders triangle.
54
+ - [ ] `<g transform="translate(10,10)"><rect .../></g>` translates children.
55
+ - [ ] Nested groups compose transforms correctly.
56
+ - [ ] `<text x="10" y="20">Hello</text>` renders text.
57
+ - [ ] `<image xlink:href="data:image/png;base64,..."/>` renders PNG.
58
+ - [ ] Spec coverage for each element type.
@@ -0,0 +1,38 @@
1
+ ---
2
+ priority: P2
3
+ phase: 10
4
+ depends_on: [34]
5
+ layer: media
6
+ est: 1d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ SVG diagrams that repeat on every page (logos, watermarks) currently
13
+ re-render each time. Better: render once into a Form XObject, then
14
+ `canvas.draw_xobject(name, at:, scale:)` on every page. Cuts render
15
+ time and PDF size.
16
+
17
+ ## Approach
18
+
19
+ Files:
20
+
21
+ - `lib/arrolio/svg/form_xobject_builder.rb` — takes an SVG Document,
22
+ renders it once into a `Pdfrb::Model::Cos::Stream` with
23
+ `/Type /XObject`, `/Subtype /Form`, `/BBox`. Returns the OID.
24
+
25
+ - Modify `Arrolio::SVG::Renderer` to optionally emit into a Form
26
+ XObject stream instead of directly into a page's canvas.
27
+
28
+ - ImageFlowable and other consumers gain an option to wrap their
29
+ rendering in a Form XObject when the same image is placed > N times
30
+ (heuristic for "this repeats").
31
+
32
+ ## Done-When
33
+
34
+ - [ ] SVG rendered as Form XObject appears identical to inline render.
35
+ - [ ] Form XObject reused across 10 pages produces a PDF smaller than
36
+ rendering inline 10 times.
37
+ - [ ] Form XObject can be scaled/positioned per page.
38
+ - [ ] Round-trip: rendered PDF re-read has /XObject /Form.
@@ -0,0 +1,50 @@
1
+ ---
2
+ priority: P1
3
+ phase: 11
4
+ depends_on: [10]
5
+ layer: inline
6
+ est: 1d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ A paragraph is a list of InlineRuns. Authoring those runs by hand is
13
+ tedious. The InlineBuilder DSL lets authors compose runs declaratively:
14
+ `.text("Hello ")`, `.bold("world")`, `.color(:red) { .text("apple") }`.
15
+ Style spans (bold, italic, colour) become first-class.
16
+
17
+ ## Approach
18
+
19
+ Files:
20
+
21
+ - `lib/arrolio/inline_builder.rb` — builder DSL on top of Style::Diff:
22
+
23
+ ```ruby
24
+ builder = Arrolio::InlineBuilder.new(base_style)
25
+ builder.text("Hello ")
26
+ builder.bold("world") # creates run with bold-weight style
27
+ builder.italic(" and italic")
28
+ builder.color(:red) { |b| b.text(" apple") }
29
+ builder.link("Click here", ref_id: "ch1")
30
+ runs = builder.runs # => [Run("Hello ", base), Run("world", base+bold), ...]
31
+ ```
32
+
33
+ - `lib/arrolio/style/diff.rb` (extended from TODO 04) — given a base
34
+ Definition + overrides hash, produces a new Definition. Used by
35
+ InlineBuilder's `bold`/`italic`/`color` helpers.
36
+
37
+ Helpers:
38
+ - `bold(str)` / `bold { ... }` — `font_weight: "bold"`.
39
+ - `italic(str)` / `italic { ... }` — `font_style: "italic"`.
40
+ - `color(c, str)` / `color(c) { ... }` — `fill_color: c`.
41
+ - `size(n, str)` / `size(n) { ... }` — `font_size: n`.
42
+ - `font(name, str)` — `font_name: name`.
43
+
44
+ ## Done-When
45
+
46
+ - [ ] `InlineBuilder.new(style).text("a").bold("b").runs.length == 2`.
47
+ - [ ] Bold helper sets `font_weight: "bold"` on the run's style.
48
+ - [ ] Color helper overrides only `fill_color`, keeps other props.
49
+ - [ ] Nested helpers compose (`.bold { .color(:red) { .text("x") } }`).
50
+ - [ ] Link helper creates a HyperlinkRun (TODO 37).
@@ -0,0 +1,53 @@
1
+ ---
2
+ priority: P1
3
+ phase: 11
4
+ depends_on: [36, 22]
5
+ layer: inline
6
+ est: 2d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Hyperlinks (`<a href="...">` in HTML, `fo:basic-link` in FO) need
13
+ both inline rendering (the visible text) and annotation emission (a
14
+ PDF link annotation over the run's bounding box). The bounding box
15
+ isn't known until layout — link annotations are added in a
16
+ post-layout pass.
17
+
18
+ ## Approach
19
+
20
+ Files:
21
+
22
+ - `lib/arrolio/hyperlink.rb` — `Hyperlink < InlineRun` with
23
+ `destination` (URI string or internal destination name).
24
+ Carries an `annotation_id` that the renderer fills in after
25
+ placement.
26
+
27
+ - `lib/arrolio/output/link_annotation.rb` — `LinkAnnotation` value
28
+ object: `rect` (x, y, w, h), `destination` (URI or page ref),
29
+ `border_style`.
30
+
31
+ Engine integration:
32
+ - During TextFlowable render, track each Hyperlink run's bounding box
33
+ (compute from line position + run width).
34
+ - After all body content is placed, walk the Output tree collecting
35
+ Hyperlinks → emit `LinkAnnotation[]` per page.
36
+ - PDF renderer (TODO 22) walks LinkAnnotations → emits `/Annots`
37
+ array entries on each page.
38
+
39
+ Two kinds of destination:
40
+ - **External** (URI): `/S /URI /URI (https://...)`.
41
+ - **Internal** (page reference): `/S /GoTo /D [page_ref /Fit]`.
42
+ Requires a destination registry (TODO 41).
43
+
44
+ ## Done-When
45
+
46
+ - [ ] A paragraph with a hyperlink run renders the text in blue
47
+ underline (style default) and emits a clickable annotation.
48
+ - [ ] External URI links open correctly when the PDF is viewed.
49
+ - [ ] Internal links jump to the correct page (integration with
50
+ TODO 41 destinations).
51
+ - [ ] Multiple hyperlinks on the same page each get distinct
52
+ `/Annots` entries.
53
+ - [ ] Re-read PDF has the expected annotation structure.
@@ -0,0 +1,44 @@
1
+ ---
2
+ priority: P2
3
+ phase: 11
4
+ depends_on: [10]
5
+ layer: inline
6
+ est: 1d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Footnote markers, mathematical exponents, chemical formulae all need
13
+ sub/superscript: small text raised (or lowered) relative to the
14
+ baseline. The TextLayout must place these correctly without breaking
15
+ the line height.
16
+
17
+ ## Approach
18
+
19
+ Extend `Arrolio::InlineRun` (TODO 10) with two style properties:
20
+ - `text_rise` (Float, in points) — vertical offset from baseline.
21
+ Positive = superscript; negative = subscript.
22
+ - The existing `font_size` already supports smaller sizes.
23
+
24
+ Add helper methods to `InlineBuilder` (TODO 36):
25
+ - `superscript(str)` → `style.with(font_size: size * 0.7,
26
+ text_rise: size * 0.5)`.
27
+ - `subscript(str)` → `style.with(font_size: size * 0.7,
28
+ text_rise: -(size * 0.2))`.
29
+
30
+ TextLayout integration:
31
+ - When measuring a run with `text_rise`, line height calculation
32
+ accounts for the raised extent (extends above ascender).
33
+ - When rendering, emit `canvas.text(...)` with Pdfrb's `Tz` (rise)
34
+ parameter (PDF text-rise operator).
35
+
36
+ ## Done-When
37
+
38
+ - [ ] `superscript("2")` renders smaller and raised.
39
+ - [ ] `subscript("n")` renders smaller and lowered.
40
+ - [ ] Line containing a superscript doesn't have its height reduced
41
+ (the rise doesn't push out of the line box; line_height is
42
+ generous enough).
43
+ - [ ] "x² + y²" renders correctly with raised 2's.
44
+ - [ ] "H₂O" renders with subscript 2.
@@ -0,0 +1,60 @@
1
+ ---
2
+ priority: P1
3
+ phase: 12
4
+ depends_on: [13, 17]
5
+ layer: xref
6
+ est: 2d
7
+ status: in_progress
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Two cross-reference primitives:
13
+
14
+ 1. **Leader**: stretchy dot/rule that fills the space between two
15
+ runs on a line (for TOC entries: "Chapter 1 ........... 12").
16
+ 2. **FieldRun**: page-number fields resolved at render time via
17
+ FlowContext (`page_number`, `page_number_citation`,
18
+ `page_count`).
19
+
20
+ Both must integrate with TextLayout: a Leader takes the residual
21
+ space after the line's other runs; a FieldRun produces text after
22
+ context lookup.
23
+
24
+ ## Approach
25
+
26
+ Files:
27
+
28
+ - `lib/arrolio/leader.rb` — `Leader < InlineRun` with
29
+ `leader_pattern` (`:dots`, `:rule`, `:space`), `leader_length_min`,
30
+ `leader_length_max`. Method `fill_text(available_width, measurer)`
31
+ returns the dot string that fits.
32
+
33
+ - `lib/arrolio/field_run.rb` — base `FieldRun < InlineRun` with
34
+ `resolve(context) -> String`.
35
+
36
+ - Under `lib/arrolio/field_run/`:
37
+ - `page_number.rb` — `PageNumberField` returns
38
+ `context.page_number.to_s`.
39
+ - `page_number_citation.rb` — `PageNumberCitationField` returns
40
+ `context.citation_for(ref_id).to_s` (empty if unresolved).
41
+ - `page_count.rb` — `PageCountField` returns
42
+ `context.total_pages.to_s` (empty in pass 1).
43
+
44
+ TextLayout integration:
45
+ - Leaders are width=0 during line breaking (they take residual space).
46
+ - After all other runs on a line are placed, compute the slack;
47
+ Leader fills it with dots.
48
+ - FieldRuns are width=estimated ("000") in pass 1; in pass 2 they
49
+ have resolved text and re-flow if necessary (rare).
50
+
51
+ ## Done-When
52
+
53
+ - [ ] TOC line `"Chapter 1" + leader + "12"` renders as
54
+ `"Chapter 1 ........... 12"`.
55
+ - [ ] `leader_pattern: :rule` renders a horizontal line.
56
+ - [ ] `PageNumberField` in footer renders "3" on page 3.
57
+ - [ ] `PageCountField` renders "7" on every page of a 7-page doc.
58
+ - [ ] `PageNumberCitationField` referencing a section on page 5
59
+ renders "5".
60
+ - [ ] Spec coverage for each.
@@ -0,0 +1,45 @@
1
+ ---
2
+ priority: P1
3
+ phase: 12
4
+ depends_on: [22, 26]
5
+ layer: xref
6
+ est: 2d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ PDF outlines (the bookmark panel in viewers) let users navigate.
13
+ Each entry is a title + destination. The outline tree mirrors the
14
+ document's section structure. Currently Arrolio doesn't emit one.
15
+
16
+ ## Approach
17
+
18
+ Files:
19
+
20
+ - `lib/arrolio/output/outline_node.rb` — value object: `title`,
21
+ `destination` (page_ref + viewport), `children` (Array), `parent`.
22
+
23
+ - `lib/arrolio/renderer/pdf/outline_emitter.rb` — walks the
24
+ OutlineNode tree, builds Pdfrb objects:
25
+ - `/Outlines` (root dict) on Catalog.
26
+ - `/Type /Outlines`, `/First`, `/Last`, `/Count`.
27
+ - Per outline entry: `/Title`, `/Parent`, `/Dest` (page ref + view),
28
+ `/First`/`/Last`/`Next`/`Prev`/`Count` for hierarchy.
29
+
30
+ Engine integration:
31
+ - During pass 1, when placing a flowable that has a `bookmark_target`
32
+ attribute, record `context.record_bookmark(ref_id, page_number, y)`.
33
+ - After pass 1, build the OutlineNode tree from recorded bookmarks.
34
+ - Pass 2 emits the outline via OutlineEmitter.
35
+
36
+ Bookmark target source: `Content::Section` carries an `id`; the
37
+ content adapter (or the Composer) registers these as bookmark targets.
38
+
39
+ ## Done-When
40
+
41
+ - [ ] A document with 3 sections produces a 3-entry outline.
42
+ - [ ] Nested sections produce nested outline entries.
43
+ - [ ] Clicking an outline entry in a viewer jumps to the right page.
44
+ - [ ] Outline `/Count` reflects open/closed state default.
45
+ - [ ] Re-read PDF has the expected `/Outlines` structure.
@@ -0,0 +1,52 @@
1
+ ---
2
+ priority: P1
3
+ phase: 12
4
+ depends_on: [22, 39]
5
+ layer: xref
6
+ est: 1d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Internal hyperlinks (`PageNumberCitationField` links,
13
+ `Hyperlink` runs pointing to a section) need named destinations
14
+ that resolve to specific (page, viewport) pairs. Currently nothing
15
+ records where a `ref_id` lands.
16
+
17
+ ## Approach
18
+
19
+ Files:
20
+
21
+ - `lib/arrolio/destination.rb` — `Destination = Struct.new(:ref_id,
22
+ :page_number, :x, :y, :zoom, keyword_init: true)`. The `ref_id`
23
+ is the author's identifier (e.g. "section_3"); page_number and
24
+ viewport are filled in during layout.
25
+
26
+ - `lib/arrolio/output/destination_registry.rb` — collects
27
+ `Destination` instances during pass 1; resolves by `ref_id` in
28
+ pass 2.
29
+
30
+ - `lib/arrolio/renderer/pdf/destination_emitter.rb` — emits:
31
+ - **Named destinations**: `/Names /Dests << /Name (page_ref /Fit) >>
32
+ >>` on Catalog (or a dedicated Names dict).
33
+ - **Page-relative destinations**: stored inline in the link
34
+ annotation's `/D` array.
35
+
36
+ Engine integration:
37
+ - When the engine places a flowable with a `destination_id`
38
+ attribute, record a Destination at the current page number and the
39
+ flowable's top y.
40
+ - Pass 2 walks the Output tree; for each Hyperlink or
41
+ PageNumberCitationField pointing to a `ref_id`, look up the
42
+ Destination.
43
+
44
+ ## Done-When
45
+
46
+ - [ ] A section heading with `id: "intro"` records a Destination
47
+ pointing to its page.
48
+ - [ ] A hyperlink to "intro" jumps to the correct page in a viewer.
49
+ - [ ] A `PageNumberCitationField` referencing "intro" shows the
50
+ correct page number.
51
+ - [ ] Missing `ref_id` raises a clear error during pass 2.
52
+ - [ ] Re-read PDF has the expected `/Names /Dests` or `/Dests` dict.
@@ -0,0 +1,52 @@
1
+ ---
2
+ priority: P1
3
+ phase: 12
4
+ depends_on: [39, 41]
5
+ layer: xref
6
+ est: 2d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Authoring a TOC by hand is tedious. The TOC builder walks the
13
+ Content tree's sections, produces a flowable list of TOC entries —
14
+ each is `"section_title" + Leader + "page_number_citation"` — and
15
+ inserts it where the author marked a TOC placeholder.
16
+
17
+ ## Approach
18
+
19
+ File: `lib/arrolio/toc_builder.rb`.
20
+
21
+ ```ruby
22
+ class Arrolio::TocBuilder
23
+ def initialize(content_document, levels: 1..3, style: :toc_entry)
24
+ def build -> [TextFlowable, TextFlowable, ...]
25
+ end
26
+ ```
27
+
28
+ Algorithm:
29
+ 1. Walk `content_document.sections`; collect headings up to `levels`.
30
+ 2. For each heading, build a `TextFlowable` containing:
31
+ - `[InlineRun.new(section.title, style: indent_style(level)),
32
+ Leader.new,
33
+ PageNumberCitationField.new(ref_id: section.id)]`
34
+ 3. Indent per level (level 1 = no indent, level 2 = 12pt, level 3 = 24pt).
35
+
36
+ Engine integration: the author places a `TocPlaceholder` flowable in
37
+ the content stream. The engine replaces it with `TocBuilder.build`'s
38
+ output at the start of pass 1.
39
+
40
+ Two-pass effect: TOC entries need their cited page numbers, which
41
+ aren't known until pass 1 completes. The TOC must be laid out in
42
+ pass 1 with estimated widths; in pass 2 the field runs resolve and
43
+ the TOC text is final.
44
+
45
+ ## Done-When
46
+
47
+ - [ ] A 3-section document produces a TOC with 3 entries.
48
+ - [ ] Each entry has the section title left, dot leader filling
49
+ middle, page number right.
50
+ - [ ] Multi-page TOC flows correctly across pages.
51
+ - [ ] Nested sections (level 2, 3) indent visibly.
52
+ - [ ] Page numbers in TOC match actual page numbers from pass 1.
@@ -0,0 +1,51 @@
1
+ ---
2
+ priority: P2
3
+ phase: 13
4
+ depends_on: [20]
5
+ layer: page
6
+ est: 3d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Footnotes: inline reference number in body text, footnote body at
13
+ the bottom of the page where the reference appears. The footnote
14
+ region shrinks the body frame on that page; footnotes flow into it.
15
+ If a footnote doesn't fit, it continues on the next page (with the
16
+ body reference pointing to the original page).
17
+
18
+ ## Approach
19
+
20
+ Files:
21
+
22
+ - `lib/arrolio/footnote.rb` — `Footnote = Struct.new(:id, :content,
23
+ :marker, keyword_init: true)`. `content` is a Flowable list.
24
+
25
+ - `lib/arrolio/output/footnote_region.rb` — a special Region placed
26
+ at the bottom of the body, separated by a horizontal rule.
27
+
28
+ - `lib/arrolio/engine/footnote_collector.rb` — pass-1 hook: when a
29
+ Footnote flowable is placed on page N, register it on page N's
30
+ footnote region. Body frame on page N shrinks by the footnote
31
+ region's height.
32
+
33
+ Engine integration:
34
+ - Pass 1: place body flowables; when a footnote is encountered,
35
+ compute its height; reserve space at the bottom of the current
36
+ page's body; place the footnote content there.
37
+ - If the footnote doesn't fit even alone on a fresh page, split it:
38
+ first part on the current page, continuation on next.
39
+
40
+ Marker scheme: per-document counter; numeric by default, restart
41
+ option per page or per section.
42
+
43
+ ## Done-When
44
+
45
+ - [ ] A paragraph with one footnote renders the marker in body and
46
+ the footnote text at the page bottom.
47
+ - [ ] Footnote region is separated from body by a horizontal rule.
48
+ - [ ] Body frame shrinks to make room for footnotes.
49
+ - [ ] Long footnote splits across pages.
50
+ - [ ] Multiple footnotes on one page render in order.
51
+ - [ ] Round-trip: re-read PDF has footnotes in expected positions.
@@ -0,0 +1,45 @@
1
+ ---
2
+ priority: P2
3
+ phase: 13
4
+ depends_on: [20]
5
+ layer: page
6
+ est: 3d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Magazines, newspapers, and some technical docs use multi-column body
13
+ layout. Body content flows column-by-column; when one column fills,
14
+ the next column on the same page begins; only when all columns are
15
+ full does the engine advance to the next page.
16
+
17
+ ## Approach
18
+
19
+ Files:
20
+
21
+ - `lib/arrolio/layout_spec/columns.rb` — `Columns = Struct.new(:count,
22
+ :gap, :balance, keyword_init: true)`. `count` = number of columns;
23
+ `gap` = gutter width; `balance` = bool (balance last page's columns).
24
+
25
+ - `lib/arrolio/engine/column_layout.rb` — manages N parallel Frames
26
+ on a page. Body flowables fill column 1 first; when column 1's
27
+ frame is full, advance to column 2; etc.
28
+
29
+ - `lib/arrolio/engine/column_balancer.rb` — for `balance: true` on
30
+ the last page, distribute content evenly across columns (uses
31
+ Breaker from TODO 14 with the column height as target).
32
+
33
+ PageTemplate gains a `columns:` attribute that the engine consults
34
+ when constructing the body frame.
35
+
36
+ ## Done-When
37
+
38
+ - [ ] A 2-column layout places body content in left column first,
39
+ then right column on the same page.
40
+ - [ ] Content overflows to next page only when both columns are full.
41
+ - [ ] Column gap is respected.
42
+ - [ ] `balance: true` produces roughly equal column heights on the
43
+ last page.
44
+ - [ ] Footnotes (TODO 43) integrate: footnote region spans full
45
+ page width below the columns.
@@ -0,0 +1,50 @@
1
+ ---
2
+ priority: P2
3
+ phase: 13
4
+ depends_on: [26, 39]
5
+ layer: page
6
+ est: 2d
7
+ status: pending
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Two related features:
13
+
14
+ 1. **Page numbering schemes**: front matter uses roman numerals (i,
15
+ ii, iii); body uses arabic (1, 2, 3) restarting at 1. Currently
16
+ Arrolio has only one global counter.
17
+ 2. **Section title in running header**: every body page's header
18
+ shows the current top-level section's title. Requires tracking
19
+ which section is "current" as content flows.
20
+
21
+ ## Approach
22
+
23
+ Files:
24
+
25
+ - `lib/arrolio/page_numbering_scheme.rb` — value object: `format`
26
+ (`:arabic`, `:roman_lower`, `:roman_upper`, `:alpha_lower`,
27
+ `:alpha_upper`), `start` (Integer, default 1), `prefix`, `suffix`.
28
+ Method `render(n)` → String.
29
+
30
+ - Extend `PageSequenceMaster` (TODO 25) with `:page_numbering`
31
+ attribute. Each sequence can have its own scheme; restarting at
32
+ sequence start.
33
+
34
+ - Extend `FlowContext` with `current_section` attribute. When the
35
+ engine places a Section heading flowable, it updates
36
+ `context.current_section = section`. Static content with
37
+ `{ text: context.current_section.title }` then resolves correctly.
38
+
39
+ - Add `SectionTitleField < FieldRun` that returns
40
+ `context.current_section.title`.
41
+
42
+ ## Done-When
43
+
44
+ - [ ] Front matter pages show roman numerals (i, ii, iii).
45
+ - [ ] Body pages show arabic numerals starting from 1.
46
+ - [ ] Section title in header changes as content crosses section
47
+ boundaries.
48
+ - [ ] Multi-level section: header shows "Chapter 3" while body is in
49
+ Chapter 3, regardless of subsection depth.
50
+ - [ ] Spec coverage for each scheme + section tracking.