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,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
|