arrolio 0.1.0 → 0.1.2
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 +4 -4
- data/.rubocop.yml +2 -2
- data/Rakefile +15 -0
- data/TODO.complete/56-generic-adapter-mece-decomposition.md +20 -0
- data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +92 -55
- data/TODO.complete/70-ttf-font-metrics-parity.md +68 -0
- data/TODO.complete/71-cover-layout-parity.md +45 -0
- data/TODO.complete/72-table-layout-parity.md +49 -0
- data/TODO.complete/73-toc-leaders-parity.md +55 -0
- data/TODO.complete/74-svg-image-rendering-parity.md +76 -0
- data/TODO.complete/75-mathml-formula-rendering-parity.md +52 -0
- data/TODO.complete/76-odd-even-page-templates-parity.md +50 -0
- data/TODO.complete/77-text-formatting-parity.md +55 -0
- data/TODO.complete/78-bibliography-formatting-parity.md +62 -0
- data/TODO.complete/79-line-breaking-parity.md +80 -0
- data/TODO.complete/80-parity-diff-harness-in-ci.md +68 -0
- data/TODO.complete/81-conditional-style-refinements.md +70 -0
- data/TODO.complete/82-semantic-presentation-dedup.md +65 -0
- data/TODO.complete/83-nested-list-support.md +74 -0
- data/TODO.complete/84-figure-svg-rendering.md +70 -0
- data/TODO.complete/85-locality-reference-formatting.md +48 -0
- data/TODO.complete/86-list-item-text-overflow.md +80 -0
- data/TODO.complete/87-odd-even-page-templates.md +55 -0
- data/TODO.complete/88-hyphenation-support.md +52 -0
- data/TODO.complete/89-table-rowspan.md +49 -0
- data/TODO.complete/90-mathml-stacked-layout.md +65 -0
- data/TODO.complete/91-parity-diff-diagnostics.md +50 -0
- data/TODO.complete/92-table-cell-height-parity.md +66 -0
- data/TODO.complete/93-figure-text-searchability.md +49 -0
- data/TODO.complete/94-fo-space-resolution.md +63 -0
- data/TODO.complete/95-title-header-placement.md +55 -0
- data/TODO.complete/96-pagination-drift-parity.md +517 -0
- data/TODO.complete/README.md +58 -38
- data/lib/arrolio/config_driven_pipeline.rb +45 -1
- data/lib/arrolio/content/document.rb +4 -3
- data/lib/arrolio/content/section.rb +3 -0
- data/lib/arrolio/content/table.rb +25 -14
- data/lib/arrolio/embedded_block_extractor.rb +54 -0
- data/lib/arrolio/engine/paged.rb +109 -28
- data/lib/arrolio/flavor/manifest.rb +0 -1
- data/lib/arrolio/flavor/registry.rb +1 -1
- data/lib/arrolio/flowable.rb +21 -0
- data/lib/arrolio/flowables/figure_flowable.rb +17 -0
- data/lib/arrolio/flowables/footnote_marker_flowable.rb +32 -13
- data/lib/arrolio/flowables/group_flowable.rb +57 -0
- data/lib/arrolio/flowables/list_flowable.rb +46 -18
- data/lib/arrolio/flowables/note_flowable.rb +84 -9
- data/lib/arrolio/flowables/page_sequence_start.rb +8 -4
- data/lib/arrolio/flowables/positioned_block.rb +55 -0
- data/lib/arrolio/flowables/rotated_text.rb +51 -0
- data/lib/arrolio/flowables/table_flowable.rb +303 -71
- data/lib/arrolio/flowables/text_flowable.rb +67 -11
- data/lib/arrolio/flowables/toc_line_flowable.rb +70 -7
- data/lib/arrolio/flowables.rb +4 -0
- data/lib/arrolio/font/embedder.rb +22 -3
- data/lib/arrolio/font_scanner.rb +133 -0
- data/lib/arrolio/generic_adapter/document_extraction.rb +101 -0
- data/lib/arrolio/generic_adapter/footnote_extraction.rb +81 -0
- data/lib/arrolio/generic_adapter/heading_extraction.rb +92 -0
- data/lib/arrolio/generic_adapter/inline_run_collection.rb +136 -0
- data/lib/arrolio/generic_adapter/list_conversion.rb +70 -0
- data/lib/arrolio/generic_adapter/metadata_extraction.rb +46 -0
- data/lib/arrolio/generic_adapter/table_conversion.rb +105 -0
- data/lib/arrolio/generic_adapter.rb +263 -317
- data/lib/arrolio/generic_flow_builder/bibliography.rb +47 -0
- data/lib/arrolio/generic_flow_builder/figures.rb +90 -0
- data/lib/arrolio/generic_flow_builder/lists.rb +66 -0
- data/lib/arrolio/generic_flow_builder/notes.rb +61 -0
- data/lib/arrolio/generic_flow_builder/sequences.rb +102 -0
- data/lib/arrolio/generic_flow_builder/tables.rb +28 -0
- data/lib/arrolio/generic_flow_builder/terms.rb +89 -0
- data/lib/arrolio/generic_flow_builder.rb +92 -187
- data/lib/arrolio/glyph_measurer.rb +16 -5
- data/lib/arrolio/inline_run.rb +2 -1
- data/lib/arrolio/layout_spec/page_template.rb +14 -8
- data/lib/arrolio/math_ml/inline_run_extractor.rb +272 -0
- data/lib/arrolio/math_ml.rb +7 -0
- data/lib/arrolio/output/page.rb +5 -4
- data/lib/arrolio/output/placed_box.rb +5 -2
- data/lib/arrolio/renderer/pdf/assets.rb +94 -0
- data/lib/arrolio/renderer/pdf/font_embedding.rb +77 -0
- data/lib/arrolio/renderer/pdf/metadata.rb +52 -0
- data/lib/arrolio/renderer/pdf.rb +140 -215
- data/lib/arrolio/style/definition.rb +7 -3
- data/lib/arrolio/style/registry.rb +6 -1
- data/lib/arrolio/table/auto_layout.rb +19 -10
- data/lib/arrolio/table/grid.rb +105 -0
- data/lib/arrolio/table.rb +1 -0
- data/lib/arrolio/text_layout/greedy.rb +15 -1
- data/lib/arrolio/text_layout/knuth_plass/breaker.rb +216 -92
- data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +31 -17
- data/lib/arrolio/text_layout/knuth_plass.rb +3 -0
- data/lib/arrolio/text_layout/line.rb +10 -2
- data/lib/arrolio/version.rb +1 -1
- data/lib/arrolio.rb +8 -4
- data/scripts/parity_check.rb +52 -0
- data/scripts/parity_diff.rb +89 -0
- data/scripts/xsl_profiles/standoc.yml +8 -2
- data/scripts/xsl_to_config.rb +28 -1
- metadata +91 -5
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'tmpdir'
|
|
3
4
|
require 'yaml'
|
|
4
5
|
|
|
5
6
|
module Arrolio
|
|
6
7
|
class GenericFlowBuilder
|
|
8
|
+
autoload :Sequences, 'arrolio/generic_flow_builder/sequences'
|
|
9
|
+
autoload :Terms, 'arrolio/generic_flow_builder/terms'
|
|
10
|
+
autoload :Notes, 'arrolio/generic_flow_builder/notes'
|
|
11
|
+
autoload :Figures, 'arrolio/generic_flow_builder/figures'
|
|
12
|
+
autoload :Lists, 'arrolio/generic_flow_builder/lists'
|
|
13
|
+
autoload :Bibliography, 'arrolio/generic_flow_builder/bibliography'
|
|
14
|
+
autoload :Tables, 'arrolio/generic_flow_builder/tables'
|
|
15
|
+
|
|
16
|
+
include Sequences
|
|
17
|
+
include Terms
|
|
18
|
+
include Notes
|
|
19
|
+
include Figures
|
|
20
|
+
include Lists
|
|
21
|
+
include Bibliography
|
|
22
|
+
include Tables
|
|
23
|
+
|
|
24
|
+
|
|
7
25
|
attr_reader :layout_spec, :rules, :asset_resolver
|
|
8
26
|
|
|
9
27
|
def initialize(layout_spec:, rules:, asset_resolver: nil)
|
|
@@ -19,112 +37,45 @@ module Arrolio
|
|
|
19
37
|
source = content_for(document, sequence['role'])
|
|
20
38
|
next if sequence['role'].to_s != 'cover' && sequence['role'].to_s != 'back_of_cover' && source.empty?
|
|
21
39
|
|
|
22
|
-
|
|
40
|
+
start = sequence_start(document, sequence)
|
|
41
|
+
flowables << start
|
|
42
|
+
# The part-title overlay consumes layout space in the
|
|
43
|
+
# reference (title glyph top to the first heading: 58pt on
|
|
44
|
+
# the first body page); reserve it so following content
|
|
45
|
+
# lands where the reference puts it.
|
|
46
|
+
flowables << Flowables::Spacer.new(title_block_space) if start.title_template
|
|
23
47
|
build_sequence_content(document, source, sequence, flowables)
|
|
24
48
|
flowables << Flowables::PageBreak.new if sequence['page_break_after']
|
|
25
49
|
end
|
|
26
|
-
append_inline_footnote_markers(document, flowables)
|
|
27
50
|
append_endnotes(document, flowables)
|
|
28
51
|
flowables
|
|
29
52
|
end
|
|
30
53
|
|
|
31
|
-
# Emits a FootnoteMarkerFlowable for each Document#footnote when the
|
|
32
|
-
# flavor opts in via `flow_rules.page_bottom_footnotes: true`. The
|
|
33
|
-
# engine collects these onto the page where they appear (the last
|
|
34
|
-
# body page by default) and the renderer draws them at the bottom.
|
|
35
|
-
#
|
|
36
|
-
# This is a pragmatic bridge until the adapter can emit markers
|
|
37
|
-
# inline at `<fn>` reference sites (TODO 60). Opt-in keeps it
|
|
38
|
-
# disabled by default so existing flavors are unaffected.
|
|
39
|
-
def append_inline_footnote_markers(document, flowables)
|
|
40
|
-
return if document.footnotes.empty?
|
|
41
|
-
return unless @rules['page_bottom_footnotes']
|
|
42
|
-
|
|
43
|
-
document.footnotes.each do |footnote|
|
|
44
|
-
flowables << Flowables::FootnoteMarkerFlowable.new(footnote)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def append_endnotes(document, flowables)
|
|
49
|
-
return if document.footnotes.empty?
|
|
50
|
-
return unless @rules['endnotes']
|
|
51
|
-
|
|
52
|
-
flowables << Flowables::PageSequenceStart.new(
|
|
53
|
-
role: :endnotes,
|
|
54
|
-
header_template: nil,
|
|
55
|
-
footer_template: nil
|
|
56
|
-
)
|
|
57
|
-
document.footnotes.each do |footnote|
|
|
58
|
-
marker_text = footnote.marker.to_s
|
|
59
|
-
body = footnote.body.map { |paragraph| paragraph_flowable(paragraph) }
|
|
60
|
-
flowables << Flowables::NoteFlowable.new(
|
|
61
|
-
marker_text,
|
|
62
|
-
body,
|
|
63
|
-
style: resolve(footnote.style_id),
|
|
64
|
-
marker_width: 22.0
|
|
65
|
-
)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
54
|
|
|
69
55
|
private
|
|
70
56
|
|
|
71
|
-
def page_sequences
|
|
72
|
-
Array(@rules['page_sequences'])
|
|
73
|
-
end
|
|
74
57
|
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
role: sequence.fetch('role'),
|
|
78
|
-
header_template: interpolate(sequence['header'], document),
|
|
79
|
-
footer_template: sequence['footer'],
|
|
80
|
-
header_align: sequence['header_align'] || :right,
|
|
81
|
-
footer_align: sequence['footer_align'] || :center,
|
|
82
|
-
initial_page_number: sequence['initial_page_number']
|
|
83
|
-
)
|
|
58
|
+
def title_block_space
|
|
59
|
+
(@rules.dig('title', 'block_space') || 0.0).to_f
|
|
84
60
|
end
|
|
85
61
|
|
|
86
|
-
def
|
|
87
|
-
if sequence['build_content'] == 'cover_content'
|
|
88
|
-
build_cover_content(document, out)
|
|
89
|
-
elsif source.is_a?(Array)
|
|
90
|
-
build_sections(source, out)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def content_for(document, role)
|
|
95
|
-
case role.to_s
|
|
96
|
-
when 'preface' then document.preface
|
|
97
|
-
when 'body' then document.sections
|
|
98
|
-
when 'bibliography' then document.bibliography
|
|
99
|
-
else []
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def build_cover_content(document, out)
|
|
104
|
-
Array(@rules['cover_content']).each do |entry|
|
|
105
|
-
next unless condition_matches?(entry['when'], document)
|
|
106
|
-
|
|
107
|
-
case entry['type'].to_s
|
|
108
|
-
when 'spacer'
|
|
109
|
-
out << Flowables::Spacer.new(entry.fetch('size').to_f)
|
|
110
|
-
when 'text'
|
|
111
|
-
style = resolve(entry.fetch('style'))
|
|
112
|
-
style = style.with(align: :center) if entry.fetch('align', 'center').to_sym == :center
|
|
113
|
-
text = interpolate(entry.fetch('source'), document)
|
|
114
|
-
out << Flowables::TextFlowable.new([InlineRun.new(text, style: style)], style: style)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def build_sections(sections, out)
|
|
62
|
+
def build_sections(sections, out, between_breaks: false)
|
|
120
63
|
section_rules = @rules.fetch('section', {})
|
|
121
64
|
break_between = section_rules.fetch('insert_page_break_before', false)
|
|
65
|
+
break_numbers = section_rules.fetch('page_break_before_numbers', [])
|
|
122
66
|
sections.each_with_index do |section, index|
|
|
123
|
-
|
|
67
|
+
forced = break_numbers.include?(section.number.to_s)
|
|
68
|
+
needs_break = (break_between || forced || between_breaks) &&
|
|
69
|
+
index.positive? && !bibliography_section?(section)
|
|
70
|
+
out << Flowables::PageBreak.new if needs_break
|
|
124
71
|
build_section(section, out)
|
|
125
72
|
end
|
|
126
73
|
end
|
|
127
74
|
|
|
75
|
+
def bibliography_section?(section)
|
|
76
|
+
section.children.any?(Content::BibliographyItem)
|
|
77
|
+
end
|
|
78
|
+
|
|
128
79
|
def build_section(section, out)
|
|
129
80
|
if section.heading?
|
|
130
81
|
out << Flowables::HeadingFlowable.new(
|
|
@@ -138,14 +89,24 @@ module Arrolio
|
|
|
138
89
|
|
|
139
90
|
children = section.children
|
|
140
91
|
children = prefix_number(section.number, children) if !section.heading? && section.number
|
|
141
|
-
children.
|
|
92
|
+
children.each_with_index do |child, index|
|
|
93
|
+
append_child(child, out, followed_by_keep: keeps_with_previous?(children[index + 1]))
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Tables and lists pull their preceding paragraph with them
|
|
98
|
+
# across page boundaries (XSL-FO keep-with-next on the lead-in;
|
|
99
|
+
# the reference breaks freely between text and figures — 2.3's
|
|
100
|
+
# paragraph ends p5 with Figure 1 opening p6).
|
|
101
|
+
def keeps_with_previous?(child)
|
|
102
|
+
child.is_a?(Content::Table) || child.is_a?(Content::List)
|
|
142
103
|
end
|
|
143
104
|
|
|
144
105
|
def prefix_number(number, children)
|
|
145
106
|
return children if children.empty? || !children.first.is_a?(Content::Paragraph)
|
|
146
107
|
|
|
147
108
|
first = children.first
|
|
148
|
-
prefix = Content::InlineRun.new("#{number}
|
|
109
|
+
prefix = Content::InlineRun.new("#{number} ", style_id: :caption_label)
|
|
149
110
|
updated = Content::Paragraph.new(
|
|
150
111
|
[prefix] + first.inline_runs,
|
|
151
112
|
style_id: first.style_id,
|
|
@@ -157,23 +118,53 @@ module Arrolio
|
|
|
157
118
|
|
|
158
119
|
def emit_footnote_markers_for(paragraph, out)
|
|
159
120
|
return if paragraph.footnote_refs.empty?
|
|
121
|
+
return unless @rules['page_bottom_footnotes']
|
|
160
122
|
return unless @document
|
|
161
123
|
|
|
162
124
|
paragraph.footnote_refs.each do |ref_id|
|
|
163
125
|
footnote = @document.footnotes.find { |fn| fn.id == ref_id || fn.marker == ref_id }
|
|
164
126
|
next unless footnote
|
|
165
127
|
|
|
166
|
-
out << Flowables::FootnoteMarkerFlowable.new(
|
|
128
|
+
out << Flowables::FootnoteMarkerFlowable.new(
|
|
129
|
+
footnote, style: resolve(:footnote)
|
|
130
|
+
)
|
|
167
131
|
end
|
|
168
132
|
end
|
|
169
133
|
|
|
170
|
-
def append_child(child, out
|
|
134
|
+
def append_child(child, out, standalone: true, followed_by_keep: false,
|
|
135
|
+
in_term: false)
|
|
171
136
|
case child
|
|
172
137
|
when Content::Paragraph
|
|
173
|
-
|
|
138
|
+
flowable = paragraph_flowable(child, standalone: standalone)
|
|
139
|
+
# mn2pdf keeps a paragraph with the table/figure that
|
|
140
|
+
# follows it, so the caption is never orphaned at a page
|
|
141
|
+
# bottom and the reference's page-end whitespace before
|
|
142
|
+
# table sections is reproduced.
|
|
143
|
+
if followed_by_keep
|
|
144
|
+
flowable = Flowables::TextFlowable.new(
|
|
145
|
+
flowable.runs,
|
|
146
|
+
style: flowable.style.with(keep_with_next: true),
|
|
147
|
+
measurer: flowable.measurer
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
out << flowable
|
|
174
151
|
emit_footnote_markers_for(child, out)
|
|
175
152
|
when Content::Note
|
|
176
|
-
|
|
153
|
+
note = note_flowable(child)
|
|
154
|
+
before = in_term ? term_config.fetch('note_spacing', 0.0).to_f : 0.0
|
|
155
|
+
after = in_term ? term_config.fetch('note_spacing_after', before) : 0.0
|
|
156
|
+
parts = []
|
|
157
|
+
parts << Flowables::Spacer.new(before) if before.positive?
|
|
158
|
+
parts << note
|
|
159
|
+
parts << Flowables::Spacer.new(after) if after.positive?
|
|
160
|
+
out << if parts.length == 1
|
|
161
|
+
note
|
|
162
|
+
else
|
|
163
|
+
Flowables::GroupFlowable.new(parts,
|
|
164
|
+
style: Style::Definition.new(keep_together: false))
|
|
165
|
+
end
|
|
166
|
+
when Content::Example
|
|
167
|
+
out << example_flowable(child)
|
|
177
168
|
when Content::FigureGroup
|
|
178
169
|
figure_group_flowable(child, out)
|
|
179
170
|
when Content::TermEntry
|
|
@@ -183,7 +174,11 @@ module Arrolio
|
|
|
183
174
|
when Content::Section
|
|
184
175
|
build_section(child, out)
|
|
185
176
|
when Content::Table
|
|
186
|
-
out << Flowables::TableFlowable.new(child,
|
|
177
|
+
out << Flowables::TableFlowable.new(child,
|
|
178
|
+
style: resolve(child.style_id),
|
|
179
|
+
caption_runs: caption_runs_for(child),
|
|
180
|
+
caption_style: resolve(:figure_caption).with(align: :left),
|
|
181
|
+
**table_geometry)
|
|
187
182
|
when Content::List
|
|
188
183
|
out << list_flowable(child)
|
|
189
184
|
when Content::Image
|
|
@@ -195,52 +190,7 @@ module Arrolio
|
|
|
195
190
|
end
|
|
196
191
|
end
|
|
197
192
|
|
|
198
|
-
def
|
|
199
|
-
body = note.body.map { |paragraph| paragraph_flowable(paragraph) }
|
|
200
|
-
Flowables::NoteFlowable.new(
|
|
201
|
-
note.label,
|
|
202
|
-
body,
|
|
203
|
-
style: resolve(note.style_id)
|
|
204
|
-
)
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def figure_group_flowable(group, out)
|
|
208
|
-
out << image_flowable(group.image) if group.image
|
|
209
|
-
return unless group.caption
|
|
210
|
-
|
|
211
|
-
caption_style = resolve(:figure_caption)
|
|
212
|
-
runs = group.caption.inline_runs.map do |run|
|
|
213
|
-
InlineRun.new(run.text, style: caption_style)
|
|
214
|
-
end
|
|
215
|
-
out << Flowables::TextFlowable.new(runs, style: caption_style)
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def term_entry_flowable(entry, out)
|
|
219
|
-
if entry.number
|
|
220
|
-
out << Flowables::TextFlowable.new(
|
|
221
|
-
[InlineRun.new(entry.number, style: resolve(:term))],
|
|
222
|
-
style: resolve(:term)
|
|
223
|
-
)
|
|
224
|
-
end
|
|
225
|
-
out << paragraph_flowable(entry.preferred) if entry.preferred
|
|
226
|
-
entry.definition.each { |paragraph| out << paragraph_flowable(paragraph) }
|
|
227
|
-
return unless entry.source
|
|
228
|
-
|
|
229
|
-
out << paragraph_flowable(entry.source)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def bibliography_item_flowable(item, out)
|
|
233
|
-
marker_text = item.tag.to_s
|
|
234
|
-
body = item.formattedref ? [paragraph_flowable(item.formattedref)] : []
|
|
235
|
-
out << Flowables::NoteFlowable.new(
|
|
236
|
-
marker_text,
|
|
237
|
-
body,
|
|
238
|
-
style: resolve(item.style_id),
|
|
239
|
-
marker_width: 36.0
|
|
240
|
-
)
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def paragraph_flowable(paragraph)
|
|
193
|
+
def paragraph_flowable(paragraph, standalone: false)
|
|
244
194
|
runs = paragraph.inline_runs.map do |run|
|
|
245
195
|
InlineRun.new(
|
|
246
196
|
run.text,
|
|
@@ -250,7 +200,9 @@ module Arrolio
|
|
|
250
200
|
href: run.href
|
|
251
201
|
)
|
|
252
202
|
end
|
|
253
|
-
|
|
203
|
+
style = resolve(paragraph.style_id)
|
|
204
|
+
style = style.with(margin_bottom: 10.0) if standalone && paragraph.style_id == :body
|
|
205
|
+
Flowables::TextFlowable.new(runs, style: style)
|
|
254
206
|
end
|
|
255
207
|
|
|
256
208
|
def preformatted_flowable(preformatted)
|
|
@@ -263,53 +215,6 @@ module Arrolio
|
|
|
263
215
|
Flowables::TextFlowable.new(runs, style: style)
|
|
264
216
|
end
|
|
265
217
|
|
|
266
|
-
def list_flowable(list)
|
|
267
|
-
items = list.items.each_with_index.map do |item, index|
|
|
268
|
-
marker = item.marker.nil? ? default_marker(list, index) : item.marker
|
|
269
|
-
body = item.content.filter_map do |content|
|
|
270
|
-
paragraph_flowable(content) if content.is_a?(Content::Paragraph)
|
|
271
|
-
end
|
|
272
|
-
[marker, body]
|
|
273
|
-
end
|
|
274
|
-
Flowables::ListFlowable.new(items, kind: list.kind, style: resolve(list.style_id))
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def default_marker(list, index)
|
|
278
|
-
defaults = @rules.dig('list', 'defaults') || {}
|
|
279
|
-
if list.ordered?
|
|
280
|
-
format = defaults['ordered_marker'] || '%d.'
|
|
281
|
-
format.sub('%d', (index + 1).to_s)
|
|
282
|
-
else
|
|
283
|
-
defaults['bullet_marker'] || '■ '
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
def image_flowable(image)
|
|
288
|
-
source = asset_resolver.resolve(image.src)
|
|
289
|
-
image_rules = @rules['image'] || {}
|
|
290
|
-
default_width = (image_rules['default_natural_width'] || 400).to_f
|
|
291
|
-
default_height = (image_rules['default_natural_height'] || 300).to_f
|
|
292
|
-
max_width = (image_rules['max_display_width'] || 106).to_f
|
|
293
|
-
natural_width = image.width || svg_dimension(source, 'width') || default_width
|
|
294
|
-
natural_height = image.height || svg_dimension(source, 'height') || default_height
|
|
295
|
-
display_width = [image.width || natural_width, max_width].min
|
|
296
|
-
Flowables::ImageFlowable.new(
|
|
297
|
-
source,
|
|
298
|
-
natural_width: natural_width,
|
|
299
|
-
natural_height: natural_height,
|
|
300
|
-
display_width: display_width,
|
|
301
|
-
alt: image.alt,
|
|
302
|
-
style: resolve(image.style_id)
|
|
303
|
-
)
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def svg_dimension(source, name)
|
|
307
|
-
return nil unless source && File.exist?(source) && source.match?(/\.svg\z/i)
|
|
308
|
-
|
|
309
|
-
value = File.read(source)[/(?:#{name})=["']([\d.]+)/, 1]
|
|
310
|
-
value&.to_f
|
|
311
|
-
end
|
|
312
|
-
|
|
313
218
|
def resolve(style_id)
|
|
314
219
|
@layout_spec.resolve_style(style_id.to_sym)
|
|
315
220
|
end
|
|
@@ -21,17 +21,28 @@ module Arrolio
|
|
|
21
21
|
!@metrics.nil?
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
return
|
|
24
|
+
def metrics_for(font_name)
|
|
25
|
+
return @metrics if font_name.nil? || font_name.to_s == @font_name
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
::Arrolio::FontMetrics::Registry[font_name.to_s]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# +font_name+ lets a paragraph-level measurer price a run that
|
|
31
|
+
# carries a different font (e.g. a bold run inside a regular
|
|
32
|
+
# paragraph). Falls back to the instance font when nil.
|
|
33
|
+
def width_of_string(str, font_size:, font_name: nil)
|
|
34
|
+
metrics = metrics_for(font_name)
|
|
35
|
+
return estimate(str, font_size) if metrics.nil?
|
|
36
|
+
|
|
37
|
+
metrics.width_of_string(str, font_size: font_size)
|
|
28
38
|
end
|
|
29
39
|
|
|
30
40
|
# Width of a run including character_spacing (Tc — extra space
|
|
31
41
|
# added after every glyph) and word_spacing (Tw — extra space
|
|
32
42
|
# added after every space glyph). Matches PDF rendering rules.
|
|
33
|
-
def width_of_run(str, font_size:, character_spacing: 0, word_spacing: 0
|
|
34
|
-
|
|
43
|
+
def width_of_run(str, font_size:, character_spacing: 0, word_spacing: 0,
|
|
44
|
+
font_name: nil)
|
|
45
|
+
base = width_of_string(str, font_size: font_size, font_name: font_name)
|
|
35
46
|
char_count = str.length
|
|
36
47
|
word_count = str.count(' ')
|
|
37
48
|
extra = (character_spacing.to_f * [char_count - 1, 0].max) +
|
data/lib/arrolio/inline_run.rb
CHANGED
|
@@ -87,27 +87,33 @@ module Arrolio
|
|
|
87
87
|
{ before: 0.0, after: 0.0, start: 0.0, end: 0.0 }
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
# XSL-FO geometry: the body region is the page content
|
|
91
|
+
# rectangle (page minus page margins). The before/after/start/
|
|
92
|
+
# end extents size the auxiliary regions that sit INSIDE the
|
|
93
|
+
# margins — they never displace the body. Shrinking the body
|
|
94
|
+
# by the extents wasted the whole header/footer strip on
|
|
95
|
+
# every page.
|
|
90
96
|
def compute_regions
|
|
91
97
|
pw = width
|
|
92
98
|
ph = height
|
|
93
99
|
m = @margins
|
|
94
100
|
e = @region_extents
|
|
95
|
-
inner_w = pw - m[:left] - m[:right]
|
|
96
|
-
inner_h = ph - m[:top] - m[:bottom]
|
|
101
|
+
inner_w = pw - m[:left] - m[:right]
|
|
102
|
+
inner_h = ph - m[:top] - m[:bottom]
|
|
97
103
|
{
|
|
98
104
|
body: Region.new(name: :body,
|
|
99
|
-
x: m[:left]
|
|
100
|
-
y: m[:bottom]
|
|
105
|
+
x: m[:left],
|
|
106
|
+
y: m[:bottom],
|
|
101
107
|
width: inner_w,
|
|
102
108
|
height: inner_h),
|
|
103
109
|
before: Region.new(name: :before,
|
|
104
|
-
x: m[:left]
|
|
105
|
-
y: ph - m[:top]
|
|
110
|
+
x: m[:left],
|
|
111
|
+
y: ph - m[:top],
|
|
106
112
|
width: inner_w,
|
|
107
113
|
height: e[:before]),
|
|
108
114
|
after: Region.new(name: :after,
|
|
109
|
-
x: m[:left]
|
|
110
|
-
y: m[:bottom],
|
|
115
|
+
x: m[:left],
|
|
116
|
+
y: m[:bottom] - e[:after],
|
|
111
117
|
width: inner_w,
|
|
112
118
|
height: e[:after]),
|
|
113
119
|
start: Region.new(name: :start,
|