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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/Rakefile +15 -0
  4. data/TODO.complete/56-generic-adapter-mece-decomposition.md +20 -0
  5. data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +92 -55
  6. data/TODO.complete/70-ttf-font-metrics-parity.md +68 -0
  7. data/TODO.complete/71-cover-layout-parity.md +45 -0
  8. data/TODO.complete/72-table-layout-parity.md +49 -0
  9. data/TODO.complete/73-toc-leaders-parity.md +55 -0
  10. data/TODO.complete/74-svg-image-rendering-parity.md +76 -0
  11. data/TODO.complete/75-mathml-formula-rendering-parity.md +52 -0
  12. data/TODO.complete/76-odd-even-page-templates-parity.md +50 -0
  13. data/TODO.complete/77-text-formatting-parity.md +55 -0
  14. data/TODO.complete/78-bibliography-formatting-parity.md +62 -0
  15. data/TODO.complete/79-line-breaking-parity.md +80 -0
  16. data/TODO.complete/80-parity-diff-harness-in-ci.md +68 -0
  17. data/TODO.complete/81-conditional-style-refinements.md +70 -0
  18. data/TODO.complete/82-semantic-presentation-dedup.md +65 -0
  19. data/TODO.complete/83-nested-list-support.md +74 -0
  20. data/TODO.complete/84-figure-svg-rendering.md +70 -0
  21. data/TODO.complete/85-locality-reference-formatting.md +48 -0
  22. data/TODO.complete/86-list-item-text-overflow.md +80 -0
  23. data/TODO.complete/87-odd-even-page-templates.md +55 -0
  24. data/TODO.complete/88-hyphenation-support.md +52 -0
  25. data/TODO.complete/89-table-rowspan.md +49 -0
  26. data/TODO.complete/90-mathml-stacked-layout.md +65 -0
  27. data/TODO.complete/91-parity-diff-diagnostics.md +50 -0
  28. data/TODO.complete/92-table-cell-height-parity.md +66 -0
  29. data/TODO.complete/93-figure-text-searchability.md +49 -0
  30. data/TODO.complete/94-fo-space-resolution.md +63 -0
  31. data/TODO.complete/95-title-header-placement.md +55 -0
  32. data/TODO.complete/96-pagination-drift-parity.md +517 -0
  33. data/TODO.complete/README.md +58 -38
  34. data/lib/arrolio/config_driven_pipeline.rb +45 -1
  35. data/lib/arrolio/content/document.rb +4 -3
  36. data/lib/arrolio/content/section.rb +3 -0
  37. data/lib/arrolio/content/table.rb +25 -14
  38. data/lib/arrolio/embedded_block_extractor.rb +54 -0
  39. data/lib/arrolio/engine/paged.rb +109 -28
  40. data/lib/arrolio/flavor/manifest.rb +0 -1
  41. data/lib/arrolio/flavor/registry.rb +1 -1
  42. data/lib/arrolio/flowable.rb +21 -0
  43. data/lib/arrolio/flowables/figure_flowable.rb +17 -0
  44. data/lib/arrolio/flowables/footnote_marker_flowable.rb +32 -13
  45. data/lib/arrolio/flowables/group_flowable.rb +57 -0
  46. data/lib/arrolio/flowables/list_flowable.rb +46 -18
  47. data/lib/arrolio/flowables/note_flowable.rb +84 -9
  48. data/lib/arrolio/flowables/page_sequence_start.rb +8 -4
  49. data/lib/arrolio/flowables/positioned_block.rb +55 -0
  50. data/lib/arrolio/flowables/rotated_text.rb +51 -0
  51. data/lib/arrolio/flowables/table_flowable.rb +303 -71
  52. data/lib/arrolio/flowables/text_flowable.rb +67 -11
  53. data/lib/arrolio/flowables/toc_line_flowable.rb +70 -7
  54. data/lib/arrolio/flowables.rb +4 -0
  55. data/lib/arrolio/font/embedder.rb +22 -3
  56. data/lib/arrolio/font_scanner.rb +133 -0
  57. data/lib/arrolio/generic_adapter/document_extraction.rb +101 -0
  58. data/lib/arrolio/generic_adapter/footnote_extraction.rb +81 -0
  59. data/lib/arrolio/generic_adapter/heading_extraction.rb +92 -0
  60. data/lib/arrolio/generic_adapter/inline_run_collection.rb +136 -0
  61. data/lib/arrolio/generic_adapter/list_conversion.rb +70 -0
  62. data/lib/arrolio/generic_adapter/metadata_extraction.rb +46 -0
  63. data/lib/arrolio/generic_adapter/table_conversion.rb +105 -0
  64. data/lib/arrolio/generic_adapter.rb +263 -317
  65. data/lib/arrolio/generic_flow_builder/bibliography.rb +47 -0
  66. data/lib/arrolio/generic_flow_builder/figures.rb +90 -0
  67. data/lib/arrolio/generic_flow_builder/lists.rb +66 -0
  68. data/lib/arrolio/generic_flow_builder/notes.rb +61 -0
  69. data/lib/arrolio/generic_flow_builder/sequences.rb +102 -0
  70. data/lib/arrolio/generic_flow_builder/tables.rb +28 -0
  71. data/lib/arrolio/generic_flow_builder/terms.rb +89 -0
  72. data/lib/arrolio/generic_flow_builder.rb +92 -187
  73. data/lib/arrolio/glyph_measurer.rb +16 -5
  74. data/lib/arrolio/inline_run.rb +2 -1
  75. data/lib/arrolio/layout_spec/page_template.rb +14 -8
  76. data/lib/arrolio/math_ml/inline_run_extractor.rb +272 -0
  77. data/lib/arrolio/math_ml.rb +7 -0
  78. data/lib/arrolio/output/page.rb +5 -4
  79. data/lib/arrolio/output/placed_box.rb +5 -2
  80. data/lib/arrolio/renderer/pdf/assets.rb +94 -0
  81. data/lib/arrolio/renderer/pdf/font_embedding.rb +77 -0
  82. data/lib/arrolio/renderer/pdf/metadata.rb +52 -0
  83. data/lib/arrolio/renderer/pdf.rb +140 -215
  84. data/lib/arrolio/style/definition.rb +7 -3
  85. data/lib/arrolio/style/registry.rb +6 -1
  86. data/lib/arrolio/table/auto_layout.rb +19 -10
  87. data/lib/arrolio/table/grid.rb +105 -0
  88. data/lib/arrolio/table.rb +1 -0
  89. data/lib/arrolio/text_layout/greedy.rb +15 -1
  90. data/lib/arrolio/text_layout/knuth_plass/breaker.rb +216 -92
  91. data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +31 -17
  92. data/lib/arrolio/text_layout/knuth_plass.rb +3 -0
  93. data/lib/arrolio/text_layout/line.rb +10 -2
  94. data/lib/arrolio/version.rb +1 -1
  95. data/lib/arrolio.rb +8 -4
  96. data/scripts/parity_check.rb +52 -0
  97. data/scripts/parity_diff.rb +89 -0
  98. data/scripts/xsl_profiles/standoc.yml +8 -2
  99. data/scripts/xsl_to_config.rb +28 -1
  100. 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
- flowables << sequence_start(document, sequence)
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 sequence_start(document, sequence)
76
- Flowables::PageSequenceStart.new(
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 build_sequence_content(document, source, sequence, out)
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
- out << Flowables::PageBreak.new if break_between && index.positive?
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.each { |child| append_child(child, out) }
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} ", style_id: :caption_label)
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(footnote)
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
- out << paragraph_flowable(child)
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
- out << note_flowable(child)
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, style: resolve(child.style_id))
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 note_flowable(note)
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
- Flowables::TextFlowable.new(runs, style: resolve(paragraph.style_id))
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 width_of_string(str, font_size:)
25
- return estimate(str, font_size) unless available?
24
+ def metrics_for(font_name)
25
+ return @metrics if font_name.nil? || font_name.to_s == @font_name
26
26
 
27
- @metrics.width_of_string(str, font_size: font_size)
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
- base = width_of_string(str, font_size: font_size)
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) +
@@ -35,7 +35,8 @@ module Arrolio
35
35
  @text,
36
36
  font_size: font_size * @font_size_scale,
37
37
  character_spacing: style.character_spacing,
38
- word_spacing: style.word_spacing
38
+ word_spacing: style.word_spacing,
39
+ font_name: style.font_name
39
40
  )
40
41
  end
41
42
 
@@ -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] - e[:start] - e[:end]
96
- inner_h = ph - m[:top] - m[:bottom] - e[:before] - e[:after]
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] + e[:start],
100
- y: m[:bottom] + e[:after],
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] + e[:start],
105
- y: ph - m[:top] - e[:before],
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] + e[:start],
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,