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.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/.rubocop.yml +343 -0
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +154 -0
- data/Gemfile +15 -0
- data/LICENSE +24 -0
- data/README.adoc +95 -0
- data/Rakefile +21 -0
- data/TODO.complete/01-true-type-font-metrics.md +56 -0
- data/TODO.complete/02-font-embedding.md +51 -0
- data/TODO.complete/03-knuth-plass-breaker.md +42 -0
- data/TODO.complete/04-table-flowable.md +60 -0
- data/TODO.complete/05-list-flowable.md +50 -0
- data/TODO.complete/06-notes-and-examples.md +41 -0
- data/TODO.complete/07-odd-even-templates-and-page-numbering.md +63 -0
- data/TODO.complete/08-image-flowable.md +45 -0
- data/TODO.complete/09-toc-and-leaders.md +66 -0
- data/TODO.complete/10-hyperlinks-and-destinations.md +46 -0
- data/TODO.complete/11-pdf-outline-bookmarks.md +47 -0
- data/TODO.complete/12-metadata-info-dict.md +42 -0
- data/TODO.complete/13-xmp-metadata.md +43 -0
- data/TODO.complete/14-footnotes.md +43 -0
- data/TODO.complete/15-color-management.md +33 -0
- data/TODO.complete/16-pdfua-struct-tree.md +35 -0
- data/TODO.complete/17-alt-text-and-actual-text.md +28 -0
- data/TODO.complete/18-math-rendering.md +41 -0
- data/TODO.complete/19-preformatted-and-code.md +31 -0
- data/TODO.complete/20-page-break-controls.md +46 -0
- data/TODO.complete/21-svg-renderer.md +40 -0
- data/TODO.complete/22-definition-lists.md +36 -0
- data/TODO.complete/23-multi-column.md +34 -0
- data/TODO.complete/24-cover-page-fidelity.md +40 -0
- data/TODO.complete/25-i18n-and-multi-language.md +28 -0
- data/TODO.complete/26-pixel-diff-harness.md +36 -0
- data/TODO.complete/27-pdfrb-only-text-extraction.md +37 -0
- data/TODO.complete/28-restore-nested-module-style.md +41 -0
- data/TODO.complete/29-font-embedding-wiring-debug.md +72 -0
- data/TODO.complete/30-adapter-decomposition.md +76 -0
- data/TODO.complete/31-subscript-superscript-inline-runs.md +56 -0
- data/TODO.complete/32-debug-output-cleanup.md +59 -0
- data/TODO.complete/33-comprehensive-adapter-specs.md +55 -0
- data/TODO.complete/34-table-column-auto-layout.md +50 -0
- data/TODO.complete/35-image-path-resolution.md +66 -0
- data/TODO.complete/36-xsl-converter-completeness.md +40 -0
- data/TODO.complete/37-content-model-completeness.md +54 -0
- data/TODO.complete/38-pipeline-config-externalization.md +55 -0
- data/TODO.complete/39-two-pass-cross-reference-resolution.md +66 -0
- data/TODO.complete/40-vertical-writing-modes.md +63 -0
- data/TODO.complete/41-pdf-annotations-hyperlinks.md +53 -0
- data/TODO.complete/42-font-manifest-configuration.md +75 -0
- data/TODO.complete/43-shrink-to-fit-containers.md +52 -0
- data/TODO.complete/44-back-of-book-index-generation.md +51 -0
- data/TODO.complete/45-pdf-forms-acroform.md +52 -0
- data/TODO.complete/46-pdf-digital-signatures.md +47 -0
- data/TODO.complete/47-ocp-flavor-separation.md +50 -0
- data/TODO.complete/48-oiml-as-pure-configuration.md +80 -0
- data/TODO.complete/49-oiml-flowbuilder-as-configuration.md +39 -0
- data/TODO.complete/50-delete-oiml-ruby-code.md +53 -0
- data/TODO.complete/51-xsl-as-authoritative-source.md +68 -0
- data/TODO.complete/52-flavor-packaging-separation.md +49 -0
- data/TODO.complete/53-second-flavor-spike.md +74 -0
- data/TODO.complete/54-flavor-manifest-contract.md +72 -0
- data/TODO.complete/55-header-footer-style-configuration.md +57 -0
- data/TODO.complete/56-generic-adapter-mece-decomposition.md +56 -0
- data/TODO.complete/57-strict-section-typing.md +49 -0
- data/TODO.complete/58-flavor-packaging-gem-convention.md +47 -0
- data/TODO.complete/59-xsl-converter-profiles.md +76 -0
- data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +62 -0
- data/TODO.complete/61-error-model-and-strict-mode.md +62 -0
- data/TODO.complete/62-font-resolution-and-strict-fonts.md +66 -0
- data/TODO.complete/63-purge-hardcoded-vocabulary-from-generic-core.md +68 -0
- data/TODO.complete/64-selector-driven-adapter-specs.md +46 -0
- data/TODO.complete/66-semantic-content-types.md +171 -0
- data/TODO.complete/README.md +48 -0
- data/TODO.implementation/01-foundation.md +46 -0
- data/TODO.implementation/02-content-contract.md +47 -0
- data/TODO.implementation/03-layoutspec-contract.md +46 -0
- data/TODO.implementation/04-style-system.md +43 -0
- data/TODO.implementation/05-visitor-pattern.md +40 -0
- data/TODO.implementation/06-afm-data-and-parser.md +37 -0
- data/TODO.implementation/07-afm-metrics.md +41 -0
- data/TODO.implementation/08-truetype-tables-and-metrics.md +48 -0
- data/TODO.implementation/09-font-registry-and-glyph-measurer.md +49 -0
- data/TODO.implementation/10-break-opportunity-and-inline-run.md +47 -0
- data/TODO.implementation/11-greedy-line-breaker.md +48 -0
- data/TODO.implementation/12-knuth-plass-line-breaker.md +46 -0
- data/TODO.implementation/13-line-and-alignment.md +45 -0
- data/TODO.implementation/14-knuth-elements-and-breaker.md +56 -0
- data/TODO.implementation/15-refactor-text-layout-on-knuth.md +47 -0
- data/TODO.implementation/16-flowable-and-frame.md +52 -0
- data/TODO.implementation/17-flow-context.md +50 -0
- data/TODO.implementation/18-text-flowable.md +52 -0
- data/TODO.implementation/19-spacer-and-page-break.md +52 -0
- data/TODO.implementation/20-engine-paged.md +63 -0
- data/TODO.implementation/21-output-tree.md +56 -0
- data/TODO.implementation/22-pdf-renderer.md +77 -0
- data/TODO.implementation/23-end-to-end-smoke.md +53 -0
- data/TODO.implementation/24-page-template-and-region.md +51 -0
- data/TODO.implementation/25-page-sequence-master.md +54 -0
- data/TODO.implementation/26-two-pass-layout.md +66 -0
- data/TODO.implementation/27-table-model.md +48 -0
- data/TODO.implementation/28-table-layouts.md +53 -0
- data/TODO.implementation/29-table-renderer.md +55 -0
- data/TODO.implementation/30-table-flowable.md +53 -0
- data/TODO.implementation/31-list-model.md +45 -0
- data/TODO.implementation/32-list-flowable.md +54 -0
- data/TODO.implementation/33-image-flowable.md +59 -0
- data/TODO.implementation/34-svg-renderer.md +58 -0
- data/TODO.implementation/35-svg-as-form-xobject.md +38 -0
- data/TODO.implementation/36-inline-builder.md +50 -0
- data/TODO.implementation/37-hyperlink.md +53 -0
- data/TODO.implementation/38-sub-superscript.md +44 -0
- data/TODO.implementation/39-leader-and-field-runs.md +60 -0
- data/TODO.implementation/40-bookmark-outline.md +45 -0
- data/TODO.implementation/41-destinations.md +52 -0
- data/TODO.implementation/42-toc-builder.md +52 -0
- data/TODO.implementation/43-footnotes.md +51 -0
- data/TODO.implementation/44-multi-column.md +45 -0
- data/TODO.implementation/45-page-numbering-and-section-tracking.md +50 -0
- data/TODO.implementation/46-color-management.md +47 -0
- data/TODO.implementation/47-metadata-and-xmp.md +41 -0
- data/TODO.implementation/48-struct-tree-and-marked-content.md +49 -0
- data/TODO.implementation/49-alt-text-and-actual-text.md +43 -0
- data/TODO.implementation/50-composer-facade.md +59 -0
- data/TODO.implementation/51-style-presets.md +44 -0
- data/TODO.implementation/52-oiml-adapter-and-e2e-pipeline.md +120 -0
- data/TODO.implementation/53-oiml-layout-spec.md +121 -0
- data/TODO.implementation/54-oiml-pipeline.md +97 -0
- data/TODO.implementation/55-pdf-comparator.md +98 -0
- data/TODO.implementation/56-diff-formatters.md +95 -0
- data/TODO.implementation/57-path-locator-and-enrichment.md +74 -0
- data/TODO.implementation/58-rspec-matchers-and-harness-api.md +124 -0
- data/TODO.implementation/59-pixel-diff-mode.md +93 -0
- data/TODO.implementation/60-yard-documentation.md +43 -0
- data/TODO.implementation/61-benchmarks-and-stress-tests.md +50 -0
- data/TODO.implementation/62-validation-framework.md +107 -0
- data/TODO.implementation/63-layout-rules-profile.md +110 -0
- data/TODO.implementation/64-pdfa-and-pdfua-conformance.md +74 -0
- data/TODO.implementation/65-feature-extractor.md +64 -0
- data/TODO.implementation/66-auto-fixer.md +75 -0
- data/TODO.implementation/README.md +169 -0
- data/data/arrolio/afm/Courier-Bold.afm +342 -0
- data/data/arrolio/afm/Courier-BoldOblique.afm +342 -0
- data/data/arrolio/afm/Courier-Oblique.afm +342 -0
- data/data/arrolio/afm/Courier.afm +342 -0
- data/data/arrolio/afm/Helvetica-Bold.afm +2827 -0
- data/data/arrolio/afm/Helvetica-BoldOblique.afm +2827 -0
- data/data/arrolio/afm/Helvetica-Oblique.afm +3051 -0
- data/data/arrolio/afm/Helvetica.afm +3051 -0
- data/data/arrolio/afm/Symbol.afm +213 -0
- data/data/arrolio/afm/Times-Bold.afm +2588 -0
- data/data/arrolio/afm/Times-BoldItalic.afm +2384 -0
- data/data/arrolio/afm/Times-Italic.afm +2667 -0
- data/data/arrolio/afm/Times-Roman.afm +2419 -0
- data/data/arrolio/afm/ZapfDingbats.afm +225 -0
- data/data/arrolio/glyphlist.txt +4306 -0
- data/exe/arrolio2pdf +28 -0
- data/lib/arrolio/asset_resolver.rb +36 -0
- data/lib/arrolio/color.rb +107 -0
- data/lib/arrolio/column_set.rb +64 -0
- data/lib/arrolio/composer.rb +10 -0
- data/lib/arrolio/config_driven_pipeline.rb +144 -0
- data/lib/arrolio/content/bibliography_item.rb +42 -0
- data/lib/arrolio/content/builder.rb +108 -0
- data/lib/arrolio/content/document.rb +60 -0
- data/lib/arrolio/content/example.rb +43 -0
- data/lib/arrolio/content/figure_group.rb +39 -0
- data/lib/arrolio/content/footnote.rb +46 -0
- data/lib/arrolio/content/form_field.rb +47 -0
- data/lib/arrolio/content/formula.rb +34 -0
- data/lib/arrolio/content/heading.rb +42 -0
- data/lib/arrolio/content/hyperlink.rb +42 -0
- data/lib/arrolio/content/image.rb +36 -0
- data/lib/arrolio/content/index_entry.rb +52 -0
- data/lib/arrolio/content/inline_run.rb +51 -0
- data/lib/arrolio/content/list.rb +60 -0
- data/lib/arrolio/content/note.rb +48 -0
- data/lib/arrolio/content/page_break.rb +28 -0
- data/lib/arrolio/content/paragraph.rb +38 -0
- data/lib/arrolio/content/preformatted.rb +47 -0
- data/lib/arrolio/content/section.rb +39 -0
- data/lib/arrolio/content/table.rb +117 -0
- data/lib/arrolio/content/term_entry.rb +43 -0
- data/lib/arrolio/content.rb +27 -0
- data/lib/arrolio/data_dir.rb +28 -0
- data/lib/arrolio/engine/cross_reference_registry.rb +69 -0
- data/lib/arrolio/engine/paged.rb +186 -0
- data/lib/arrolio/engine.rb +9 -0
- data/lib/arrolio/error.rb +70 -0
- data/lib/arrolio/flavor/manifest.rb +111 -0
- data/lib/arrolio/flavor/registry.rb +54 -0
- data/lib/arrolio/flavor.rb +23 -0
- data/lib/arrolio/flow_context.rb +37 -0
- data/lib/arrolio/flowable.rb +66 -0
- data/lib/arrolio/flowables/footnote_marker_flowable.rb +28 -0
- data/lib/arrolio/flowables/heading_flowable.rb +23 -0
- data/lib/arrolio/flowables/image_flowable.rb +53 -0
- data/lib/arrolio/flowables/list_flowable.rb +117 -0
- data/lib/arrolio/flowables/note_flowable.rb +19 -0
- data/lib/arrolio/flowables/page_break.rb +15 -0
- data/lib/arrolio/flowables/page_sequence_start.rb +28 -0
- data/lib/arrolio/flowables/spacer.rb +34 -0
- data/lib/arrolio/flowables/table_flowable.rb +147 -0
- data/lib/arrolio/flowables/text_flowable.rb +95 -0
- data/lib/arrolio/flowables/toc_line_flowable.rb +35 -0
- data/lib/arrolio/flowables/two_column_block.rb +56 -0
- data/lib/arrolio/flowables.rb +19 -0
- data/lib/arrolio/font/embedder.rb +197 -0
- data/lib/arrolio/font/manifest.rb +114 -0
- data/lib/arrolio/font/resolver.rb +161 -0
- data/lib/arrolio/font/text_encoder.rb +28 -0
- data/lib/arrolio/font.rb +10 -0
- data/lib/arrolio/font_metrics/afm_metrics.rb +91 -0
- data/lib/arrolio/font_metrics/registry.rb +55 -0
- data/lib/arrolio/font_metrics/true_type_metrics.rb +95 -0
- data/lib/arrolio/font_metrics.rb +10 -0
- data/lib/arrolio/frame.rb +36 -0
- data/lib/arrolio/generic_adapter.rb +672 -0
- data/lib/arrolio/generic_flow_builder.rb +338 -0
- data/lib/arrolio/glyph_measurer.rb +71 -0
- data/lib/arrolio/harness/pdf_diff.rb +104 -0
- data/lib/arrolio/harness/pixel_diff.rb +122 -0
- data/lib/arrolio/harness/text_diff.rb +53 -0
- data/lib/arrolio/harness/text_extractor.rb +58 -0
- data/lib/arrolio/harness.rb +10 -0
- data/lib/arrolio/inline_run.rb +59 -0
- data/lib/arrolio/layout_spec/flow.rb +17 -0
- data/lib/arrolio/layout_spec/loader.rb +117 -0
- data/lib/arrolio/layout_spec/page_template.rb +127 -0
- data/lib/arrolio/layout_spec/page_template_selector.rb +58 -0
- data/lib/arrolio/layout_spec/region.rb +38 -0
- data/lib/arrolio/layout_spec/scale_length.rb +34 -0
- data/lib/arrolio/layout_spec.rb +90 -0
- data/lib/arrolio/logger.rb +54 -0
- data/lib/arrolio/output/page.rb +69 -0
- data/lib/arrolio/output/placed_box.rb +68 -0
- data/lib/arrolio/output/region.rb +32 -0
- data/lib/arrolio/output.rb +10 -0
- data/lib/arrolio/renderer/accessibility_tagger.rb +70 -0
- data/lib/arrolio/renderer/font_registry.rb +32 -0
- data/lib/arrolio/renderer/link_annotator.rb +62 -0
- data/lib/arrolio/renderer/outline_builder.rb +102 -0
- data/lib/arrolio/renderer/pdf.rb +493 -0
- data/lib/arrolio/renderer/signature_config.rb +59 -0
- data/lib/arrolio/renderer/structure_tree_builder.rb +117 -0
- data/lib/arrolio/renderer/xmp_builder.rb +92 -0
- data/lib/arrolio/renderer.rb +17 -0
- data/lib/arrolio/style/definition.rb +98 -0
- data/lib/arrolio/style/loader.rb +25 -0
- data/lib/arrolio/style/registry.rb +53 -0
- data/lib/arrolio/style.rb +9 -0
- data/lib/arrolio/table/auto_layout.rb +91 -0
- data/lib/arrolio/table.rb +9 -0
- data/lib/arrolio/text_direction.rb +81 -0
- data/lib/arrolio/text_layout/break_opportunity.rb +103 -0
- data/lib/arrolio/text_layout/greedy.rb +110 -0
- data/lib/arrolio/text_layout/knuth_plass/breaker.rb +265 -0
- data/lib/arrolio/text_layout/knuth_plass/item.rb +84 -0
- data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +103 -0
- data/lib/arrolio/text_layout/knuth_plass.rb +12 -0
- data/lib/arrolio/text_layout/line.rb +72 -0
- data/lib/arrolio/text_layout.rb +11 -0
- data/lib/arrolio/toc_builder.rb +31 -0
- data/lib/arrolio/version.rb +5 -0
- data/lib/arrolio/writing_mode.rb +75 -0
- data/lib/arrolio.rb +42 -0
- data/script/smoke.rb +32 -0
- data/scripts/write_content_extras.py +227 -0
- data/scripts/xsl_profiles/standoc.yml +218 -0
- data/scripts/xsl_to_config.rb +391 -0
- metadata +394 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
# A paragraph of flowing text. Selects a line-breaking strategy
|
|
6
|
+
# based on the style's +line_break+ attribute (:greedy or
|
|
7
|
+
# :knuth_plass). Both strategies produce TextLayout::Line[]
|
|
8
|
+
# with the same shape, so the rest of the pipeline is agnostic.
|
|
9
|
+
class TextFlowable < Flowable
|
|
10
|
+
attr_reader :runs, :measurer
|
|
11
|
+
|
|
12
|
+
def initialize(runs, style: Style::Definition.new, measurer: nil)
|
|
13
|
+
super(style: style)
|
|
14
|
+
@runs = Array(runs)
|
|
15
|
+
@measurer = measurer || GlyphMeasurer.new(font_name: style.font_name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def height(width, _context = nil)
|
|
19
|
+
laid_out(width).length * line_height
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def emit(x, y, width, _context = nil)
|
|
23
|
+
lines = laid_out(width)
|
|
24
|
+
return [[], 0.0] if lines.empty?
|
|
25
|
+
|
|
26
|
+
box = Output::PlacedBox.text(
|
|
27
|
+
x: x, y: y - (line_height * lines.length),
|
|
28
|
+
width: width, height: line_height * lines.length,
|
|
29
|
+
lines: lines, line_height: line_height, style: @style
|
|
30
|
+
)
|
|
31
|
+
[[box], box.height]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def splittable?
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def do_split(width, remaining_height, _context = nil)
|
|
39
|
+
lines = laid_out(width)
|
|
40
|
+
line_h = line_height
|
|
41
|
+
head_count = (remaining_height.to_f / line_h).floor
|
|
42
|
+
head_count = [head_count, lines.length].min
|
|
43
|
+
head_count = 1 if head_count < 1 && lines.any?
|
|
44
|
+
|
|
45
|
+
head_lines = lines[0...head_count]
|
|
46
|
+
tail_lines = lines[head_count..] || []
|
|
47
|
+
|
|
48
|
+
head_runs = collect_runs(head_lines)
|
|
49
|
+
tail_runs = collect_runs(tail_lines)
|
|
50
|
+
|
|
51
|
+
head = head_runs.empty? ? nil : self.class.new(head_runs, style: @style, measurer: @measurer)
|
|
52
|
+
tail = tail_runs.empty? ? nil : self.class.new(tail_runs, style: @style, measurer: @measurer)
|
|
53
|
+
[head, tail]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def laid_out(width)
|
|
59
|
+
case @style.line_break
|
|
60
|
+
when :knuth_plass
|
|
61
|
+
layout_knuth_plass(width)
|
|
62
|
+
else
|
|
63
|
+
layout_greedy(width)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def layout_greedy(width)
|
|
68
|
+
TextLayout::Greedy.new(@runs, measurer: @measurer,
|
|
69
|
+
width: width,
|
|
70
|
+
align: @style.align).layout
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def layout_knuth_plass(width)
|
|
74
|
+
items = TextLayout::KnuthPlass::ItemBuilder.new(@runs, measurer: @measurer).build
|
|
75
|
+
TextLayout::KnuthPlass::Breaker.new(
|
|
76
|
+
items: items, line_widths: [width], runs: @runs,
|
|
77
|
+
measurer: @measurer, align: @style.align
|
|
78
|
+
).layout
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def line_height
|
|
82
|
+
@measurer.line_height(font_size: @style.font_size,
|
|
83
|
+
line_spacing: @style.line_spacing)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def collect_runs(lines)
|
|
87
|
+
runs = []
|
|
88
|
+
lines.each do |line|
|
|
89
|
+
line.placed_runs.each { |pr| runs << pr.run }
|
|
90
|
+
end
|
|
91
|
+
runs
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
# One ToC line: title + dot-leader + page number. One line tall.
|
|
6
|
+
class TocLineFlowable < Flowable
|
|
7
|
+
attr_reader :title, :page_number, :level
|
|
8
|
+
|
|
9
|
+
def initialize(title, page_number, level: 1, style: Style::Definition.new)
|
|
10
|
+
super(style: style)
|
|
11
|
+
@title = title.to_s
|
|
12
|
+
@page_number = page_number.to_i
|
|
13
|
+
@level = level.to_i
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def height(_width, _context = nil)
|
|
17
|
+
GlyphMeasurer.new(font_name: @style.font_name)
|
|
18
|
+
.line_height(font_size: @style.font_size,
|
|
19
|
+
line_spacing: @style.line_spacing)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def emit(x, y, width, context = nil)
|
|
23
|
+
page_str = @page_number.to_s
|
|
24
|
+
measurer = GlyphMeasurer.new(font_name: @style.font_name)
|
|
25
|
+
title_w = measurer.width_of_string(@title, font_size: @style.font_size)
|
|
26
|
+
page_w = measurer.width_of_string(page_str, font_size: @style.font_size)
|
|
27
|
+
gap = width - title_w - page_w - 10
|
|
28
|
+
dots = gap.positive? ? ('.' * (gap / measurer.space_width(font_size: @style.font_size)).floor) : ' '
|
|
29
|
+
text = "#{@title} #{dots} #{page_str}"
|
|
30
|
+
tf = TextFlowable.new([InlineRun.new(text, style: @style)], style: @style)
|
|
31
|
+
tf.emit(x, y, width, context)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
# Two-column layout for a cover-page header. Renders left and right
|
|
6
|
+
# content blocks side-by-side without table borders, matching the
|
|
7
|
+
# reference PDF's two-column fo:table layout. Used by flow builders
|
|
8
|
+
# to place flavor-specific cover text in two columns.
|
|
9
|
+
class TwoColumnBlock < Flowable
|
|
10
|
+
attr_reader :left_flowables, :right_flowables, :left_ratio, :style
|
|
11
|
+
|
|
12
|
+
# +left_flowables:+ Array of TextFlowable for the left column.
|
|
13
|
+
# +right_flowables:+ Array of TextFlowable for the right column.
|
|
14
|
+
# +left_ratio:+ Fraction of width for the left column (default 0.5).
|
|
15
|
+
def initialize(left_flowables:, right_flowables:, left_ratio: 0.5,
|
|
16
|
+
style: Style::Definition.new)
|
|
17
|
+
super(style: style)
|
|
18
|
+
@left_flowables = Array(left_flowables)
|
|
19
|
+
@right_flowables = Array(right_flowables)
|
|
20
|
+
@left_ratio = left_ratio.to_f
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def height(width, context = nil)
|
|
24
|
+
left_w = width * @left_ratio
|
|
25
|
+
right_w = width * (1.0 - @left_ratio)
|
|
26
|
+
left_h = @left_flowables.sum { |f| f.height(left_w, context) }
|
|
27
|
+
right_h = @right_flowables.sum { |f| f.height(right_w, context) }
|
|
28
|
+
[left_h, right_h].max
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def emit(x, y, width, context = nil)
|
|
32
|
+
left_w = width * @left_ratio
|
|
33
|
+
right_w = width * (1.0 - @left_ratio)
|
|
34
|
+
right_x = x + left_w
|
|
35
|
+
boxes = []
|
|
36
|
+
|
|
37
|
+
left_cursor = y
|
|
38
|
+
@left_flowables.each do |f|
|
|
39
|
+
fboxes, consumed = f.emit(x, left_cursor, left_w, context)
|
|
40
|
+
boxes.concat(fboxes)
|
|
41
|
+
left_cursor -= consumed
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
right_cursor = y
|
|
45
|
+
@right_flowables.each do |f|
|
|
46
|
+
fboxes, consumed = f.emit(right_x, right_cursor, right_w, context)
|
|
47
|
+
boxes.concat(fboxes)
|
|
48
|
+
right_cursor -= consumed
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
consumed = [y - left_cursor, y - right_cursor].max
|
|
52
|
+
[boxes, consumed]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Arrolio::Flowables — concrete Flowable subclasses.
|
|
4
|
+
module Arrolio
|
|
5
|
+
module Flowables
|
|
6
|
+
autoload :TextFlowable, 'arrolio/flowables/text_flowable'
|
|
7
|
+
autoload :Spacer, 'arrolio/flowables/spacer'
|
|
8
|
+
autoload :PageBreak, 'arrolio/flowables/page_break'
|
|
9
|
+
autoload :HeadingFlowable, 'arrolio/flowables/heading_flowable'
|
|
10
|
+
autoload :PageSequenceStart, 'arrolio/flowables/page_sequence_start'
|
|
11
|
+
autoload :ListFlowable, 'arrolio/flowables/list_flowable'
|
|
12
|
+
autoload :NoteFlowable, 'arrolio/flowables/note_flowable'
|
|
13
|
+
autoload :ImageFlowable, 'arrolio/flowables/image_flowable'
|
|
14
|
+
autoload :TocLineFlowable, 'arrolio/flowables/toc_line_flowable'
|
|
15
|
+
autoload :TwoColumnBlock, 'arrolio/flowables/two_column_block'
|
|
16
|
+
autoload :TableFlowable, 'arrolio/flowables/table_flowable'
|
|
17
|
+
autoload :FootnoteMarkerFlowable, 'arrolio/flowables/footnote_marker_flowable'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fontisan'
|
|
4
|
+
|
|
5
|
+
module Arrolio
|
|
6
|
+
module Font
|
|
7
|
+
class Embedder
|
|
8
|
+
attr_reader :document, :font_path, :base_font_name
|
|
9
|
+
|
|
10
|
+
def initialize(document, font_path, base_font_name: nil)
|
|
11
|
+
@document = document
|
|
12
|
+
@font_path = font_path
|
|
13
|
+
@base_font_name = base_font_name || File.basename(font_path, '.*')
|
|
14
|
+
@fontisan_font = Fontisan::FontLoader.load(font_path)
|
|
15
|
+
@unicode_to_gid = @fontisan_font.table('cmap').unicode_mappings.transform_keys(&:to_i)
|
|
16
|
+
@subset_cp_to_gid = nil
|
|
17
|
+
@subset_gid_to_cp = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def glyph_id_for_codepoint(codepoint)
|
|
21
|
+
@unicode_to_gid[codepoint.to_i] || 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def subset_gid_for_codepoint(codepoint)
|
|
25
|
+
return glyph_id_for_codepoint(codepoint) unless @subset_cp_to_gid
|
|
26
|
+
@subset_cp_to_gid[codepoint.to_i] || 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def subset(codepoints)
|
|
30
|
+
gids = codepoints.map { |cp| glyph_id_for_codepoint(cp) }.uniq.sort
|
|
31
|
+
gids << 0
|
|
32
|
+
options = Fontisan::Subset::Options.new(profile: 'pdf')
|
|
33
|
+
builder = Fontisan::Subset::Builder.new(@fontisan_font, gids, options)
|
|
34
|
+
{ bytes: builder.build, gid_map: builder.mapping }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def embed(codepoints)
|
|
38
|
+
sub = subset(codepoints)
|
|
39
|
+
bytes = sub[:bytes]
|
|
40
|
+
extract_subset_cmap(bytes)
|
|
41
|
+
stream = @document.add(
|
|
42
|
+
{ Length: bytes.bytesize, Length1: bytes.bytesize },
|
|
43
|
+
type: Pdfrb::Model::Cos::Stream
|
|
44
|
+
)
|
|
45
|
+
stream.stream = bytes
|
|
46
|
+
font_file2 = Pdfrb::Model::Reference.new(stream.oid, stream.gen)
|
|
47
|
+
descriptor = @document.add(
|
|
48
|
+
{ Type: :FontDescriptor, FontName: @base_font_name.to_sym,
|
|
49
|
+
Flags: 32, FontBBox: font_bbox, ItalicAngle: 0,
|
|
50
|
+
Ascent: ascent, Descent: descent, CapHeight: cap_height,
|
|
51
|
+
StemV: 80, FontFile2: font_file2 },
|
|
52
|
+
type: Pdfrb::Model::Type::FontDescriptor
|
|
53
|
+
)
|
|
54
|
+
cid_font = @document.add(
|
|
55
|
+
{ Type: :Font, Subtype: :CIDFontType2,
|
|
56
|
+
BaseFont: @base_font_name.to_sym,
|
|
57
|
+
CIDSystemInfo: { Registry: 'Adobe', Ordering: 'Identity', Supplement: 0 },
|
|
58
|
+
CIDToGIDMap: :Identity,
|
|
59
|
+
FontDescriptor: Pdfrb::Model::Reference.new(descriptor.oid, descriptor.gen),
|
|
60
|
+
W: widths_array },
|
|
61
|
+
type: Pdfrb::Model::Type::CIDFont
|
|
62
|
+
)
|
|
63
|
+
tounicode_data = build_tounicode_stream
|
|
64
|
+
tounicode = @document.add(
|
|
65
|
+
{ Length: tounicode_data.bytesize },
|
|
66
|
+
type: Pdfrb::Model::Cos::Stream
|
|
67
|
+
)
|
|
68
|
+
tounicode.stream = tounicode_data
|
|
69
|
+
type0 = @document.add(
|
|
70
|
+
{ Type: :Font, Subtype: :Type0,
|
|
71
|
+
BaseFont: @base_font_name.to_sym,
|
|
72
|
+
Encoding: :'Identity-H',
|
|
73
|
+
DescendantFonts: [Pdfrb::Model::Reference.new(cid_font.oid, cid_font.gen)],
|
|
74
|
+
ToUnicode: Pdfrb::Model::Reference.new(tounicode.oid, tounicode.gen) },
|
|
75
|
+
type: Pdfrb::Model::Type::FontType0
|
|
76
|
+
)
|
|
77
|
+
Pdfrb::Model::Reference.new(type0.oid, type0.gen)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def extract_subset_cmap(bytes)
|
|
83
|
+
require 'tmpdir'
|
|
84
|
+
Dir.mktmpdir do |dir|
|
|
85
|
+
path = File.join(dir, 'subset.ttf')
|
|
86
|
+
File.binwrite(path, bytes)
|
|
87
|
+
sf = Fontisan::FontLoader.load(path)
|
|
88
|
+
cm = sf.table('cmap').unicode_mappings
|
|
89
|
+
@subset_cp_to_gid = cm.transform_keys(&:to_i)
|
|
90
|
+
@subset_gid_to_cp = @subset_cp_to_gid.invert
|
|
91
|
+
end
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
Arrolio::Logger.warn "subset cmap extract failed: #{e.message[0,80]}"
|
|
94
|
+
@subset_cp_to_gid = @unicode_to_gid
|
|
95
|
+
@subset_gid_to_cp = @unicode_to_gid.invert
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_tounicode_stream
|
|
99
|
+
gid_to_cp = @subset_gid_to_cp || @unicode_to_gid.invert
|
|
100
|
+
lines = []
|
|
101
|
+
lines << '/CIDInit /ProcSet findresource begin'
|
|
102
|
+
lines << '12 dict begin'
|
|
103
|
+
lines << 'begincmap'
|
|
104
|
+
lines << '/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def'
|
|
105
|
+
lines << '/CMapName /Adobe-Identity-UCS def'
|
|
106
|
+
lines << '/CMapType 2 def'
|
|
107
|
+
lines << '1 begincodespacerange'
|
|
108
|
+
lines << '<0000> <FFFF>'
|
|
109
|
+
lines << 'endcodespacerange'
|
|
110
|
+
entries = gid_to_cp.map { |gid, cp| "<#{format('%04X', gid)}> <#{format('%04X', cp)}>" }
|
|
111
|
+
entries.each_slice(100) do |slice|
|
|
112
|
+
lines << "#{slice.length} beginbfchar"
|
|
113
|
+
slice.each { |e| lines << e }
|
|
114
|
+
lines << 'endbfchar'
|
|
115
|
+
end
|
|
116
|
+
lines << 'endcmap'
|
|
117
|
+
lines << 'CMapName currentdict /CMap defineresource pop'
|
|
118
|
+
lines << 'end'
|
|
119
|
+
lines << 'end'
|
|
120
|
+
lines.join("\n") + "\n"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def widths_array
|
|
124
|
+
return [] unless @subset_gid_to_cp
|
|
125
|
+
upem = head_units_per_em
|
|
126
|
+
hmtx = @fontisan_font.table('hmtx')
|
|
127
|
+
return [] unless hmtx
|
|
128
|
+
groups = {}
|
|
129
|
+
@subset_gid_to_cp.each do |subset_gid, cp|
|
|
130
|
+
orig_gid = @unicode_to_gid[cp]
|
|
131
|
+
next unless orig_gid
|
|
132
|
+
metric = hmtx.metric_for(orig_gid)
|
|
133
|
+
w = (metric && metric[:advance_width]) || 0
|
|
134
|
+
groups[subset_gid] = (w * 1000 / upem).to_i
|
|
135
|
+
end
|
|
136
|
+
result = []
|
|
137
|
+
sorted = groups.keys.sort
|
|
138
|
+
cur_s = nil
|
|
139
|
+
cur_w = []
|
|
140
|
+
sorted.each do |gid|
|
|
141
|
+
if cur_s.nil?
|
|
142
|
+
cur_s = gid
|
|
143
|
+
cur_w = [groups[gid]]
|
|
144
|
+
elsif gid == cur_s + cur_w.length
|
|
145
|
+
cur_w << groups[gid]
|
|
146
|
+
else
|
|
147
|
+
result << cur_s << cur_w
|
|
148
|
+
cur_s = gid
|
|
149
|
+
cur_w = [groups[gid]]
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
result << cur_s << cur_w if cur_s
|
|
153
|
+
result
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def head_units_per_em
|
|
157
|
+
head = @fontisan_font.table('head')
|
|
158
|
+
head ? head.units_per_em : 1000
|
|
159
|
+
rescue StandardError
|
|
160
|
+
1000
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def font_bbox
|
|
164
|
+
head = @fontisan_font.table('head')
|
|
165
|
+
return [0, 0, 1000, 1000] unless head
|
|
166
|
+
s = 1000.0 / head.units_per_em
|
|
167
|
+
[(head.x_min * s).to_i, (head.y_min * s).to_i, (head.x_max * s).to_i, (head.y_max * s).to_i]
|
|
168
|
+
rescue StandardError
|
|
169
|
+
[0, 0, 1000, 1000]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def ascent
|
|
173
|
+
hhea = @fontisan_font.table('hhea')
|
|
174
|
+
return 800 unless hhea
|
|
175
|
+
(hhea.ascender * 1000 / head_units_per_em).to_i
|
|
176
|
+
rescue StandardError
|
|
177
|
+
800
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def descent
|
|
181
|
+
hhea = @fontisan_font.table('hhea')
|
|
182
|
+
return -200 unless hhea
|
|
183
|
+
(hhea.descender * 1000 / head_units_per_em).to_i
|
|
184
|
+
rescue StandardError
|
|
185
|
+
-200
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def cap_height
|
|
189
|
+
os2 = @fontisan_font.table('OS/2')
|
|
190
|
+
return 700 unless os2
|
|
191
|
+
(os2.cap_height * 1000 / head_units_per_em).to_i
|
|
192
|
+
rescue StandardError
|
|
193
|
+
700
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Font
|
|
5
|
+
# Declarative font configuration. Maps family names to variant
|
|
6
|
+
# file paths (regular, bold, italic, bold_italic) and provides
|
|
7
|
+
# fallback chain resolution. Replaces the flat +font_paths+
|
|
8
|
+
# hash on LayoutSpec with a richer structure.
|
|
9
|
+
#
|
|
10
|
+
# Manifest format (YAML):
|
|
11
|
+
# families:
|
|
12
|
+
# "Body Font":
|
|
13
|
+
# regular: /path/to/times.ttf
|
|
14
|
+
# bold: /path/to/timesbd.ttf
|
|
15
|
+
# italic: /path/to/timesi.ttf
|
|
16
|
+
# bold_italic: /path/to/timesbi.ttf
|
|
17
|
+
# fallback: ["Fallback1", "Fallback2"]
|
|
18
|
+
class Manifest
|
|
19
|
+
attr_reader :families, :fallback_chain
|
|
20
|
+
|
|
21
|
+
# +families:+ Hash mapping family name (String) to a Hash of
|
|
22
|
+
# variant → file path. Variants: :regular, :bold, :italic,
|
|
23
|
+
# :bold_italic.
|
|
24
|
+
# +fallback_chain:+ Array of family names tried in order when
|
|
25
|
+
# the primary family doesn't have a needed variant.
|
|
26
|
+
def initialize(families:, fallback_chain: [])
|
|
27
|
+
@families = normalize_families(families)
|
|
28
|
+
@fallback_chain = Array(fallback_chain).map(&:to_s)
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Resolves a font name + weight + style to a file path.
|
|
33
|
+
# Tries the primary family first, then the fallback chain.
|
|
34
|
+
# Returns nil if no path is found.
|
|
35
|
+
def resolve(font_name, weight: :regular, style: :regular)
|
|
36
|
+
variant = variant_key(weight, style)
|
|
37
|
+
path = lookup(@families[font_name.to_s], variant)
|
|
38
|
+
return path if path
|
|
39
|
+
|
|
40
|
+
@fallback_chain.each do |family|
|
|
41
|
+
path = lookup(@families[family], variant)
|
|
42
|
+
return path if path
|
|
43
|
+
end
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns all (family, variant, path) triples as a flat hash
|
|
48
|
+
# suitable for the existing +font_paths+ API.
|
|
49
|
+
def to_flat_paths
|
|
50
|
+
result = {}
|
|
51
|
+
@families.each do |family, variants|
|
|
52
|
+
variants.each do |variant, path|
|
|
53
|
+
key = variant_name(family, variant)
|
|
54
|
+
result[key] = path if path && File.exist?(path)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
result
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Factory: builds a Manifest from a YAML/Hash representation.
|
|
61
|
+
def self.from_hash(hash)
|
|
62
|
+
new(
|
|
63
|
+
families: hash.fetch('families', hash.fetch(:families, {})),
|
|
64
|
+
fallback_chain: hash.fetch('fallback', hash.fetch(:fallback_chain, []))
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ==(other)
|
|
69
|
+
other.is_a?(self.class) &&
|
|
70
|
+
families == other.families &&
|
|
71
|
+
fallback_chain == other.fallback_chain
|
|
72
|
+
end
|
|
73
|
+
alias eql? ==
|
|
74
|
+
|
|
75
|
+
def hash
|
|
76
|
+
[self.class, families, fallback_chain].hash
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def normalize_families(raw)
|
|
82
|
+
raw.to_h.transform_keys(&:to_s).transform_values do |variants|
|
|
83
|
+
variants.to_h.transform_keys(&:to_sym)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def variant_key(weight, style)
|
|
88
|
+
is_bold = weight.to_s.downcase == 'bold'
|
|
89
|
+
is_italic = style.to_s.downcase == 'italic'
|
|
90
|
+
if is_bold && is_italic then :bold_italic
|
|
91
|
+
elsif is_bold then :bold
|
|
92
|
+
elsif is_italic then :italic
|
|
93
|
+
else :regular
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def lookup(family_variants, variant)
|
|
98
|
+
return nil unless family_variants
|
|
99
|
+
|
|
100
|
+
family_variants[variant] || family_variants[:regular]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def variant_name(family, variant)
|
|
104
|
+
case variant
|
|
105
|
+
when :regular then family
|
|
106
|
+
when :bold then "#{family} Bold"
|
|
107
|
+
when :italic then "#{family} Italic"
|
|
108
|
+
when :bold_italic then "#{family} BoldItalic"
|
|
109
|
+
else family
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Arrolio
|
|
6
|
+
module Font
|
|
7
|
+
# Resolves font family names to absolute TTF paths using a
|
|
8
|
+
# configurable chain of resolvers. Replaces the absolute-path
|
|
9
|
+
# entries in `layout_spec.yml` with portable family-name
|
|
10
|
+
# declarations.
|
|
11
|
+
#
|
|
12
|
+
# Resolution order (first hit wins):
|
|
13
|
+
# 1. FontManifest (explicit mapping in the flavor)
|
|
14
|
+
# 2. font_paths from layout_spec (legacy explicit paths)
|
|
15
|
+
# 3. fontist manifest (if the +fontist+ gem is available at
|
|
16
|
+
# runtime; otherwise this step is skipped silently)
|
|
17
|
+
# 4. PDF standard 14 fallback (e.g. "Helvetica" → built-in)
|
|
18
|
+
#
|
|
19
|
+
# Strict mode (strict: true) raises RenderError listing every
|
|
20
|
+
# unresolved required font; lenient mode silently produces nil
|
|
21
|
+
# entries and the renderer falls back to a default.
|
|
22
|
+
class Resolver
|
|
23
|
+
STANDARD_14 = [
|
|
24
|
+
'Helvetica', 'Helvetica-Bold', 'Helvetica-Oblique', 'Helvetica-BoldOblique',
|
|
25
|
+
'Times-Roman', 'Times-Bold', 'Times-Italic', 'Times-BoldItalic',
|
|
26
|
+
'Courier', 'Courier-Bold', 'Courier-Oblique', 'Courier-BoldOblique',
|
|
27
|
+
'Symbol', 'ZapfDingbats'
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
attr_reader :manifest, :font_paths, :use_fontist, :strict,
|
|
31
|
+
:resolved, :unresolved
|
|
32
|
+
|
|
33
|
+
def initialize(manifest: nil, font_paths: {}, use_fontist: true,
|
|
34
|
+
strict: false)
|
|
35
|
+
@manifest = manifest
|
|
36
|
+
@font_paths = font_paths.transform_keys(&:to_s)
|
|
37
|
+
@use_fontist = use_fontist
|
|
38
|
+
@strict = strict
|
|
39
|
+
@resolved = {}
|
|
40
|
+
@unresolved = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Resolve a single family name. Returns the absolute path (String)
|
|
44
|
+
# or nil if not found. Does NOT raise in strict mode — use
|
|
45
|
+
# +#resolve_all!+ for that.
|
|
46
|
+
def resolve(family_name)
|
|
47
|
+
return resolved[family_name.to_s] if resolved.key?(family_name.to_s)
|
|
48
|
+
|
|
49
|
+
path = lookup(family_name.to_s)
|
|
50
|
+
resolved[family_name.to_s] = path
|
|
51
|
+
track_unresolved(family_name.to_s, path)
|
|
52
|
+
path
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Resolve every family declared by the manifest or font_paths.
|
|
56
|
+
# In strict mode, raises RenderError with the full unresolved
|
|
57
|
+
# list. Returns a Hash { family => path } otherwise.
|
|
58
|
+
def resolve_all!
|
|
59
|
+
families_to_resolve.each { |family| resolve(family) }
|
|
60
|
+
return resolved unless strict && unresolved.any?
|
|
61
|
+
|
|
62
|
+
raise ::Arrolio::RenderError.new(
|
|
63
|
+
"strict mode: unresolved required fonts: #{unresolved.join(', ')}",
|
|
64
|
+
missing_fonts: unresolved.map { |name| [name, nil] }
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.from_layout_spec(layout_spec, strict: false)
|
|
69
|
+
new(
|
|
70
|
+
manifest: layout_font_manifest(layout_spec),
|
|
71
|
+
font_paths: layout_spec.font_paths,
|
|
72
|
+
use_fontist: true,
|
|
73
|
+
strict: strict
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.standard_14?(family_name)
|
|
78
|
+
STANDARD_14.include?(family_name.to_s)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def lookup(family)
|
|
84
|
+
return manifest_path(family) if @manifest&.path_for(family)
|
|
85
|
+
return @font_paths[family] if @font_paths.key?(family) && File.exist?(@font_paths[family].to_s)
|
|
86
|
+
|
|
87
|
+
fontist_path = fontist_lookup(family)
|
|
88
|
+
return fontist_path if fontist_path
|
|
89
|
+
|
|
90
|
+
STANDARD_14.include?(family) ? family : nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def manifest_path(family)
|
|
94
|
+
path = @manifest.path_for(family)
|
|
95
|
+
path && File.exist?(path) ? path : nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def fontist_lookup(family)
|
|
99
|
+
return nil unless @use_fontist
|
|
100
|
+
return nil unless fontist_available?
|
|
101
|
+
|
|
102
|
+
fontist_manifest = ::Fontist.manifest
|
|
103
|
+
fontist_manifest.install(family) unless fontist_manifest.any? { |e| e.name == family }
|
|
104
|
+
::Fontist::Font.find(family).paths.first
|
|
105
|
+
rescue StandardError
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def fontist_available?
|
|
110
|
+
Object.const_defined?(:Fontist)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def families_to_resolve
|
|
114
|
+
required = @manifest ? @manifest.required_families : []
|
|
115
|
+
(required + @font_paths.keys).uniq
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def track_unresolved(family, path)
|
|
119
|
+
return if path
|
|
120
|
+
|
|
121
|
+
is_standard = self.class.standard_14?(family)
|
|
122
|
+
@unresolved << family unless is_standard
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.layout_font_manifest(layout_spec)
|
|
126
|
+
return nil unless layout_spec.is_a?(::Arrolio::LayoutSpec)
|
|
127
|
+
|
|
128
|
+
config = layout_spec.font_manifest_config
|
|
129
|
+
return nil unless config && config['families']
|
|
130
|
+
|
|
131
|
+
FontManifest.new(config['families'], config.fetch('fallback', []))
|
|
132
|
+
end
|
|
133
|
+
private_class_method :layout_font_manifest
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Internal lightweight view over a `font_manifest:` block in
|
|
137
|
+
# layout_spec.yml. Maps family names to variant paths and exposes
|
|
138
|
+
# the required-family list.
|
|
139
|
+
class FontManifest
|
|
140
|
+
attr_reader :families, :fallback
|
|
141
|
+
|
|
142
|
+
def initialize(families, fallback = [])
|
|
143
|
+
@families = families.transform_keys(&:to_s)
|
|
144
|
+
@fallback = Array(fallback)
|
|
145
|
+
freeze
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def path_for(family)
|
|
149
|
+
entry = @families[family.to_s]
|
|
150
|
+
return nil unless entry
|
|
151
|
+
|
|
152
|
+
path = entry['regular'] || entry[:regular]
|
|
153
|
+
path&.to_s
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def required_families
|
|
157
|
+
@families.keys
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|