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,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module Font
5
+ # Converts a Unicode string into a 2-byte-big-endian
6
+ # glyph-ID sequence for an Identity-H-encoded CIDFont.
7
+ class TextEncoder
8
+ attr_reader :embedder
9
+
10
+ def initialize(embedder)
11
+ @embedder = embedder
12
+ end
13
+
14
+ # Returns a binary-encoded String of 2-byte GIDs for the
15
+ # codepoints in +text+. The renderer passes this to
16
+ # +canvas.text+ instead of the Unicode string.
17
+ def encode(text)
18
+ bytes = String.new.force_encoding(Encoding::BINARY)
19
+ text.each_codepoint do |cp|
20
+ gid = @embedder.subset_gid_for_codepoint(cp)
21
+ bytes << (gid >> 8).chr(Encoding::BINARY)
22
+ bytes << (gid & 0xff).chr(Encoding::BINARY)
23
+ end
24
+ bytes
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module Font
5
+ autoload :TextEncoder, 'arrolio/font/text_encoder'
6
+ autoload :Embedder, 'arrolio/font/embedder'
7
+ autoload :Manifest, 'arrolio/font/manifest'
8
+ autoload :Resolver, 'arrolio/font/resolver'
9
+ end
10
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module FontMetrics
5
+ # Glyph-width lookup against Pdfrb's AFM data for one of the 14
6
+ # PDF standard Type1 fonts.
7
+ class AfmMetrics
8
+ UNITS_PER_EM = 1000
9
+
10
+ attr_reader :font_name, :afm
11
+
12
+ def initialize(font_name, afm)
13
+ @font_name = font_name.to_s
14
+ @afm = afm
15
+ end
16
+
17
+ def self.for_name(name)
18
+ afm = Pdfrb::Font::Type1::FontMetrics.for_standard_font(name.to_s)
19
+ return nil unless afm
20
+
21
+ new(name, afm)
22
+ end
23
+
24
+ def units_per_em
25
+ UNITS_PER_EM
26
+ end
27
+
28
+ def advance_width(char)
29
+ metric = metric_for(char)
30
+ metric ? metric[:width] : 0
31
+ end
32
+
33
+ def width_of_string(str, font_size:)
34
+ scale = font_size.to_f / units_per_em
35
+ str.each_char.sum { |c| advance_width(c) * scale }
36
+ end
37
+
38
+ def ascender(font_size: 12)
39
+ (afm.ascender || 800) * font_size.to_f / units_per_em
40
+ end
41
+
42
+ def descender(font_size: 12)
43
+ (afm.descender || -200) * font_size.to_f / units_per_em
44
+ end
45
+
46
+ def cap_height(font_size: 12)
47
+ (afm.cap_height || 700) * font_size.to_f / units_per_em
48
+ end
49
+
50
+ def line_height(font_size:, line_spacing: 1.2)
51
+ ((afm.ascender || 800).to_f - (afm.descender || -200).to_f) *
52
+ font_size.to_f / units_per_em * line_spacing.to_f
53
+ end
54
+
55
+ private
56
+
57
+ def metric_for(char)
58
+ codepoint = char.ord
59
+ if codepoint < 0x80
60
+ m = afm.metrics.char_metrics[codepoint]
61
+ return m if m
62
+ end
63
+
64
+ name = glyph_name_for(char)
65
+ afm.metrics.char_metrics[name] if name
66
+ end
67
+
68
+ def glyph_name_for(char)
69
+ @glyph_name_cache ||= {}
70
+ return @glyph_name_cache[char] if @glyph_name_cache.key?(char)
71
+
72
+ @glyph_name_cache[char] = unicode_to_name[char.ord]
73
+ end
74
+
75
+ def unicode_to_name
76
+ return @unicode_to_name if @unicode_to_name
77
+
78
+ table = Pdfrb::Font::GlyphList.table
79
+ @unicode_to_name = {}
80
+ table.each do |gname, str|
81
+ cp = str.ord
82
+ @unicode_to_name[cp] ||= gname if cp
83
+ end
84
+ (0x21..0x7E).each do |cp|
85
+ @unicode_to_name[cp] ||= cp.chr
86
+ end
87
+ @unicode_to_name
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module FontMetrics
5
+ module Registry
6
+ @cache = {}
7
+ @ttf_paths = {}
8
+
9
+ class << self
10
+ attr_reader :ttf_paths
11
+
12
+ def register_ttf(font_name, path)
13
+ @ttf_paths[font_name.to_s] = path
14
+ @cache.delete(font_name.to_s)
15
+ end
16
+
17
+ def register_ttf_all(paths, strict: false)
18
+ missing = []
19
+ paths.each do |name, path|
20
+ register_ttf(name, path)
21
+ missing << [name, path] if strict && path && File.exist?(path) == false
22
+ end
23
+ return unless strict && missing.any?
24
+
25
+ raise Arrolio::RenderError.new(
26
+ 'strict mode: missing required font files: ' +
27
+ missing.map { |name, path| "#{name}=#{path}" }.join(', '),
28
+ missing_fonts: missing
29
+ )
30
+ end
31
+
32
+ def [](font_name)
33
+ key = font_name.to_s
34
+ return @cache[key] if @cache.key?(key)
35
+
36
+ if @ttf_paths.key?(key) && File.exist?(@ttf_paths[key])
37
+ @cache[key] = TrueTypeMetrics.from_file(key, @ttf_paths[key])
38
+ return @cache[key]
39
+ end
40
+
41
+ @cache[key] = AfmMetrics.for_name(key)
42
+ end
43
+
44
+ def reset!
45
+ @cache.clear
46
+ @ttf_paths.clear
47
+ end
48
+
49
+ def known?(font_name)
50
+ !self[font_name].nil?
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fontisan'
4
+
5
+ module Arrolio
6
+ module FontMetrics
7
+ # Glyph-width lookup from a TrueType/OpenType font file.
8
+ # Reads head, hhea, hmtx, cmap, OS/2 tables via Fontisan.
9
+ # Same interface as AfmMetrics so callers can swap freely.
10
+ class TrueTypeMetrics
11
+ attr_reader :font_name, :fontisan_font
12
+
13
+ def initialize(font_name, fontisan_font)
14
+ @font_name = font_name.to_s
15
+ @fontisan_font = fontisan_font
16
+ @cmap = fontisan_font.table('cmap')
17
+ @unicode_to_gid = @cmap ? @cmap.unicode_mappings.transform_keys(&:to_i) : {}
18
+ @head = fontisan_font.table('head')
19
+ @hhea = fontisan_font.table('hhea')
20
+ @os2 = fontisan_font.table('OS/2')
21
+ @hmtx = fontisan_font.table('hmtx')
22
+ parse_hmtx
23
+ end
24
+
25
+ def parse_hmtx
26
+ return unless @hmtx && @hhea
27
+
28
+ maxp = @fontisan_font.table('maxp')
29
+ num_metrics = @hhea.number_of_h_metrics rescue nil
30
+ num_glyphs = maxp&.num_glyphs
31
+ return unless num_metrics && num_glyphs
32
+
33
+ @hmtx.parse_with_context(num_metrics, num_glyphs)
34
+ rescue StandardError
35
+ nil
36
+ end
37
+
38
+ def self.from_file(font_name, path)
39
+ font = Fontisan::FontLoader.load(path)
40
+ new(font_name, font)
41
+ end
42
+
43
+ def units_per_em
44
+ @head ? @head.units_per_em : 1000
45
+ rescue StandardError
46
+ 1000
47
+ end
48
+
49
+ def advance_width(char)
50
+ gid = @unicode_to_gid[char.ord]
51
+ return 0 unless gid && @hmtx
52
+
53
+ metric = @hmtx.metric_for(gid)
54
+ metric ? metric[:advance_width] : 0
55
+ rescue StandardError
56
+ 0
57
+ end
58
+
59
+ def width_of_string(str, font_size:)
60
+ scale = font_size.to_f / units_per_em
61
+ str.each_char.sum { |c| advance_width(c) * scale }
62
+ end
63
+
64
+ def ascender(font_size: 12)
65
+ return font_size * 0.8 unless @hhea
66
+
67
+ ((@hhea.ascender || 800).to_f / units_per_em) * font_size.to_f
68
+ rescue StandardError
69
+ font_size * 0.8
70
+ end
71
+
72
+ def descender(font_size: 12)
73
+ return -font_size * 0.2 unless @hhea
74
+
75
+ ((@hhea.descender || -200).to_f / units_per_em) * font_size.to_f
76
+ rescue StandardError
77
+ -font_size * 0.2
78
+ end
79
+
80
+ def cap_height(font_size: 12)
81
+ return font_size * 0.7 unless @os2
82
+
83
+ ((@os2.cap_height || 700).to_f / units_per_em) * font_size.to_f
84
+ rescue StandardError
85
+ font_size * 0.7
86
+ end
87
+
88
+ def line_height(font_size:, line_spacing: 1.2)
89
+ # CSS/XSL convention: line-height is a multiplier of font-size,
90
+ # NOT derived from the font ascender/descender ratio.
91
+ font_size.to_f * line_spacing.to_f
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Arrolio::FontMetrics — glyph-width lookup.
4
+ module Arrolio
5
+ module FontMetrics
6
+ autoload :TrueTypeMetrics, 'arrolio/font_metrics/true_type_metrics'
7
+ autoload :AfmMetrics, 'arrolio/font_metrics/afm_metrics'
8
+ autoload :Registry, 'arrolio/font_metrics/registry'
9
+ end
10
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ class Frame
5
+ attr_reader :x, :y, :width, :height, :consumed
6
+
7
+ def initialize(x:, y:, width:, height:, consumed: 0.0)
8
+ @x = x.to_f
9
+ @y = y.to_f
10
+ @width = width.to_f
11
+ @height = height.to_f
12
+ @consumed = consumed.to_f
13
+ end
14
+
15
+ def remaining_height
16
+ @height - @consumed
17
+ end
18
+
19
+ def full?
20
+ remaining_height <= 0.5
21
+ end
22
+
23
+ def consume!(h)
24
+ @consumed += h.to_f if h.to_f.positive?
25
+ self
26
+ end
27
+
28
+ def cursor_y
29
+ @y + @height - @consumed
30
+ end
31
+
32
+ def clone_empty
33
+ self.class.new(x: @x, y: @y, width: @width, height: @height)
34
+ end
35
+ end
36
+ end