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,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module Output
5
+ class Page
6
+ attr_reader :number, :template_name, :template_role, :page_size,
7
+ :regions, :static_regions, :header_text, :footer_text,
8
+ :header_align, :footer_align, :footnotes
9
+
10
+ def initialize(number:, template_name:, page_size:,
11
+ regions:, static_regions: {}, template_role: :body,
12
+ header_text: nil, footer_text: nil,
13
+ header_align: :right, footer_align: :center,
14
+ footnotes: [])
15
+ @number = number.to_i
16
+ @template_name = template_name.to_sym
17
+ @template_role = template_role.to_sym
18
+ @page_size = [page_size[0].to_f, page_size[1].to_f].freeze
19
+ @regions = regions.transform_keys(&:to_sym).freeze
20
+ @static_regions = static_regions.transform_keys(&:to_sym).freeze
21
+ @header_text = header_text
22
+ @footer_text = footer_text
23
+ @header_align = header_align.to_sym
24
+ @footer_align = footer_align.to_sym
25
+ @footnotes = Array(footnotes).freeze
26
+ freeze
27
+ end
28
+
29
+ def body_region
30
+ @regions[:body]
31
+ end
32
+
33
+ def width
34
+ @page_size[0]
35
+ end
36
+
37
+ def height
38
+ @page_size[1]
39
+ end
40
+
41
+ def each_box(&block)
42
+ return enum_for(:each_box) unless block_given?
43
+
44
+ @static_regions.each_value { |r| r.placed_boxes.each(&block) }
45
+ @regions.each_value { |r| r.placed_boxes.each(&block) }
46
+ end
47
+
48
+ def ==(other)
49
+ other.is_a?(self.class) &&
50
+ number == other.number &&
51
+ template_name == other.template_name &&
52
+ template_role == other.template_role &&
53
+ page_size == other.page_size &&
54
+ regions == other.regions &&
55
+ static_regions == other.static_regions &&
56
+ header_text == other.header_text &&
57
+ footer_text == other.footer_text &&
58
+ footnotes == other.footnotes
59
+ end
60
+
61
+ alias eql? ==
62
+
63
+ def hash
64
+ [self.class, number, template_name, template_role, page_size,
65
+ regions, static_regions, header_text, footer_text, footnotes].hash
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module Output
5
+ class PlacedBox
6
+ attr_reader :x, :y, :width, :height, :kind, :data, :link_dest
7
+
8
+ def initialize(x:, y:, width:, height:, kind:, data: {}, link_dest: nil)
9
+ @x = x.to_f
10
+ @y = y.to_f
11
+ @width = width.to_f
12
+ @height = height.to_f
13
+ @kind = kind
14
+ @data = data.to_h.freeze
15
+ @link_dest = link_dest
16
+ freeze
17
+ end
18
+
19
+ class << self
20
+ def text(x:, y:, width:, height:, lines:, line_height:, style:, link_dest: nil)
21
+ new(x: x, y: y, width: width, height: height, kind: :text,
22
+ data: { lines: Array(lines).freeze,
23
+ line_height: line_height.to_f,
24
+ style: style },
25
+ link_dest: link_dest)
26
+ end
27
+
28
+ def rect(x:, y:, width:, height:, stroke_width: 0.5,
29
+ stroke_color: nil, fill_color: nil)
30
+ new(x: x, y: y, width: width, height: height, kind: :rect,
31
+ data: { stroke_width: stroke_width.to_f,
32
+ stroke_color: stroke_color,
33
+ fill_color: fill_color })
34
+ end
35
+
36
+ def line(x1:, y1:, x2:, y2:, stroke_width: 0.5, stroke_color: nil)
37
+ new(x: [x1, x2].min, y: [y1, y2].min,
38
+ width: (x2 - x1).abs, height: (y2 - y1).abs,
39
+ kind: :line,
40
+ data: { x1: x1.to_f, y1: y1.to_f,
41
+ x2: x2.to_f, y2: y2.to_f,
42
+ stroke_width: stroke_width.to_f,
43
+ stroke_color: stroke_color })
44
+ end
45
+
46
+ def image(x:, y:, width:, height:, image_ref:, link_dest: nil)
47
+ new(x: x, y: y, width: width, height: height, kind: :image,
48
+ data: { image_ref: image_ref },
49
+ link_dest: link_dest)
50
+ end
51
+ end
52
+
53
+ def ==(other)
54
+ other.is_a?(self.class) &&
55
+ x == other.x && y == other.y &&
56
+ width == other.width && height == other.height &&
57
+ kind == other.kind && data == other.data &&
58
+ link_dest == other.link_dest
59
+ end
60
+
61
+ alias eql? ==
62
+
63
+ def hash
64
+ [self.class, x, y, width, height, kind, data, link_dest].hash
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module Output
5
+ class Region
6
+ attr_reader :name, :x, :y, :width, :height, :placed_boxes
7
+
8
+ def initialize(name:, x:, y:, width:, height:, placed_boxes: [])
9
+ @name = name.to_sym
10
+ @x = x.to_f
11
+ @y = y.to_f
12
+ @width = width.to_f
13
+ @height = height.to_f
14
+ @placed_boxes = Array(placed_boxes).freeze
15
+ freeze
16
+ end
17
+
18
+ def ==(other)
19
+ other.is_a?(self.class) &&
20
+ name == other.name && x == other.x && y == other.y &&
21
+ width == other.width && height == other.height &&
22
+ placed_boxes == other.placed_boxes
23
+ end
24
+
25
+ alias eql? ==
26
+
27
+ def hash
28
+ [self.class, name, x, y, width, height, placed_boxes].hash
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Arrolio::Output — the medium-neutral laid-out page tree.
4
+ module Arrolio
5
+ module Output
6
+ autoload :Page, 'arrolio/output/page'
7
+ autoload :Region, 'arrolio/output/region'
8
+ autoload :PlacedBox, 'arrolio/output/placed_box'
9
+ end
10
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module Renderer
5
+ # Builds marked-content property lists for PDF/UA accessibility.
6
+ # Wraps non-text content (images, formulas) with /Alt and
7
+ # /ActualText entries so screen readers can access the semantic
8
+ # meaning even when the visual rendering is a rasterized image
9
+ # or a flattened formula.
10
+ #
11
+ # In the PDF content stream, marked content looks like:
12
+ # /MP << /Alt (alternative text) >> BDC
13
+ # ... drawing operators ...
14
+ # EMC
15
+ #
16
+ # This class builds the property list dictionaries; the renderer
17
+ # wraps them in BDC/EMC operators when drawing.
18
+ class AccessibilityTagger
19
+ attr_reader :document
20
+
21
+ def initialize(document)
22
+ @document = document
23
+ @sequence = 0
24
+ end
25
+
26
+ # Builds a property list dictionary for an image's alt text.
27
+ # Returns a Pdfrb reference to the dictionary, or nil if no
28
+ # alt text is provided.
29
+ def alt_property(alt_text)
30
+ return nil if alt_text.nil? || alt_text.to_s.empty?
31
+
32
+ @document.add(
33
+ { Alt: alt_text.to_s },
34
+ type: Pdfrb::Model::Cos::Dictionary
35
+ )
36
+ end
37
+
38
+ # Builds a property list for actual text (the visible text
39
+ # representation of non-text content, e.g. a formula's
40
+ # flattened text).
41
+ def actual_text_property(text)
42
+ return nil if text.nil? || text.to_s.empty?
43
+
44
+ @document.add(
45
+ { ActualText: text.to_s },
46
+ type: Pdfrb::Model::Cos::Dictionary
47
+ )
48
+ end
49
+
50
+ # Combines alt + actual text into a single property list.
51
+ # When both are present, the dictionary carries both entries.
52
+ def combined_property(alt_text: nil, actual_text: nil)
53
+ props = {}
54
+ props[:Alt] = alt_text.to_s if alt_text && !alt_text.to_s.empty?
55
+ props[:ActualText] = actual_text.to_s if actual_text && !actual_text.to_s.empty?
56
+ return nil if props.empty?
57
+
58
+ @document.add(props, type: Pdfrb::Model::Cos::Dictionary)
59
+ end
60
+
61
+ # Next unique marked-content sequence number. Used by the
62
+ # renderer to tag BDC/EMC pairs so structure-tree entries can
63
+ # reference them.
64
+ def next_tag
65
+ @sequence += 1
66
+ :"MC#{@sequence}"
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module Renderer
5
+ # Maps font family names to Pdfrb resource Symbols.
6
+ class FontRegistry
7
+ attr_reader :document
8
+
9
+ def initialize(document)
10
+ @document = document
11
+ @refs = {}
12
+ end
13
+
14
+ def resolve(font_name)
15
+ key = font_name.to_s
16
+ return @refs[key] if @refs.key?(key)
17
+
18
+ @refs[key] = begin
19
+ ref = @document.fonts.add(key)
20
+ ref || key.to_sym
21
+ rescue StandardError
22
+ fallback = @refs['Helvetica'] ||= @document.fonts.add('Helvetica')
23
+ fallback
24
+ end
25
+ end
26
+
27
+ def known_fonts
28
+ @refs.keys
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module Renderer
5
+ # Collects hyperlink annotations during rendering and emits them
6
+ # as /Annot entries on each PDF page. The renderer calls
7
+ # +record_link+ for each text run that has an href; after the
8
+ # page is fully rendered, +flush+ emits the /Annots array.
9
+ class LinkAnnotator
10
+ Link = Struct.new(:x, :y, :width, :height, :uri, keyword_init: true)
11
+
12
+ attr_reader :document, :pending_links
13
+
14
+ def initialize(document)
15
+ @document = document
16
+ @pending_links = []
17
+ end
18
+
19
+ def record_link(x:, y:, width:, height:, uri:)
20
+ @pending_links << Link.new(x: x, y: y, width: width, height: height, uri: uri)
21
+ end
22
+
23
+ # Emits collected links as /Annot entries on the given pdfrb
24
+ # page. Clears the pending list after emitting.
25
+ def flush(pdfrb_page)
26
+ return if @pending_links.empty?
27
+
28
+ annots = @pending_links.map { |link| build_link_annot(link) }
29
+ existing = pdfrb_page.value[:Annots]
30
+ if existing.is_a?(Array)
31
+ existing.concat(annots)
32
+ else
33
+ pdfrb_page.value[:Annots] = annots
34
+ end
35
+ @pending_links.clear
36
+ end
37
+
38
+ private
39
+
40
+ def build_link_annot(link)
41
+ rect = [
42
+ link.x,
43
+ link.y,
44
+ link.x + link.width,
45
+ link.y + link.height
46
+ ]
47
+ action = @document.add(
48
+ { S: :URI, URI: link.uri },
49
+ type: Pdfrb::Model::Cos::Dictionary
50
+ )
51
+ annot = @document.add(
52
+ { Type: :Annot, Subtype: :Link,
53
+ Rect: rect,
54
+ Border: [0, 0, 0],
55
+ A: Pdfrb::Model::Reference.new(action.oid, action.gen) },
56
+ type: Pdfrb::Model::Cos::Dictionary
57
+ )
58
+ Pdfrb::Model::Reference.new(annot.oid, annot.gen)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module Renderer
5
+ # Builds the PDF /Outlines tree from FlowContext#heading_entries.
6
+ # Each entry becomes an /Outlines item with /Title and /Dest
7
+ # pointing at its source page's top. Called by Renderer::Pdf
8
+ # AFTER all pages have been emitted (so page refs exist).
9
+ class OutlineBuilder
10
+ attr_reader :document, :entries, :pdfrb_pages
11
+
12
+ def initialize(document:, entries:, pdfrb_pages:)
13
+ @document = document
14
+ @entries = entries
15
+ @pdfrb_pages = pdfrb_pages
16
+ end
17
+
18
+ def build
19
+ return nil if entries.empty?
20
+
21
+ items = build_items
22
+ return nil if items.empty?
23
+
24
+ root = document.add(
25
+ {
26
+ Type: :Outlines,
27
+ First: ref(items.first),
28
+ Last: ref(items.last),
29
+ Count: items.length
30
+ },
31
+ type: Pdfrb::Model::Cos::Dictionary
32
+ )
33
+ catalog = document.catalog
34
+ catalog.value[:Outlines] = ref(root)
35
+ root
36
+ end
37
+
38
+ private
39
+
40
+ def build_items
41
+ items = []
42
+ prev = nil
43
+ entries.each do |entry|
44
+ outline_entry = build_one(entry)
45
+ next unless outline_entry
46
+
47
+ if prev
48
+ prev.value[:Next] = ref(outline_entry)
49
+ outline_entry.value[:Prev] = ref(prev)
50
+ end
51
+ items << outline_entry
52
+ prev = outline_entry
53
+ end
54
+ items
55
+ end
56
+
57
+ def build_one(entry)
58
+ page_ref = page_ref_at(entry[:page_number])
59
+ return nil unless page_ref
60
+
61
+ title = [entry[:number], entry[:title]].compact.join(' ').to_s
62
+ document.add(
63
+ {
64
+ Type: :Outlines,
65
+ Title: title.encode('UTF-8', invalid: :replace, undef: :replace),
66
+ Parent: parent_ref,
67
+ Dest: [page_ref, :XYZ, 0, page_height(entry[:page_number]), nil]
68
+ },
69
+ type: Pdfrb::Model::Cos::Dictionary
70
+ )
71
+ end
72
+
73
+ def page_ref_at(page_number)
74
+ page = pdfrb_pages[page_number - 1]
75
+ return nil unless page
76
+
77
+ Pdfrb::Model::Reference.new(page.oid, page.gen)
78
+ end
79
+
80
+ def page_height(page_number)
81
+ page = pdfrb_pages[page_number - 1]
82
+ return 800.0 unless page
83
+
84
+ box = page.value[:MediaBox]
85
+ return 800.0 unless box
86
+
87
+ box.is_a?(Array) ? box[3].to_f : 800.0
88
+ end
89
+
90
+ def parent_ref
91
+ return @parent_ref if @parent_ref
92
+
93
+ root = document.catalog.value[:Outlines]
94
+ @parent_ref = root
95
+ end
96
+
97
+ def ref(obj)
98
+ Pdfrb::Model::Reference.new(obj.oid, obj.gen)
99
+ end
100
+ end
101
+ end
102
+ end