asciidoctor-pdf 1.5.0.alpha.7 → 1.5.0.alpha.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/NOTICE.adoc +2 -2
  3. data/README.adoc +127 -128
  4. data/Rakefile +5 -4
  5. data/bin/asciidoctor-pdf +15 -2
  6. data/data/fonts/notoserif-regular-latin.ttf +0 -0
  7. data/data/themes/default-theme.yml +15 -13
  8. data/docs/theme-schema.json +114 -0
  9. data/docs/theming-guide.adoc +386 -132
  10. data/lib/asciidoctor-pdf/asciidoctor_ext.rb +2 -0
  11. data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +18 -0
  12. data/lib/asciidoctor-pdf/converter.rb +377 -221
  13. data/lib/asciidoctor-pdf/core_ext.rb +2 -0
  14. data/lib/asciidoctor-pdf/core_ext/array.rb +10 -4
  15. data/lib/asciidoctor-pdf/core_ext/numeric.rb +11 -0
  16. data/lib/asciidoctor-pdf/core_ext/ostruct.rb +1 -1
  17. data/lib/asciidoctor-pdf/formatted_text.rb +8 -0
  18. data/lib/asciidoctor-pdf/{prawn_ext/formatted_text → formatted_text}/formatter.rb +6 -9
  19. data/lib/asciidoctor-pdf/formatted_text/inline_destination_marker.rb +16 -0
  20. data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +125 -0
  21. data/lib/asciidoctor-pdf/formatted_text/inline_image_renderer.rb +45 -0
  22. data/lib/asciidoctor-pdf/{prawn_ext/formatted_text → formatted_text}/parser.rb +252 -218
  23. data/lib/asciidoctor-pdf/{prawn_ext/formatted_text → formatted_text}/parser.treetop +18 -9
  24. data/lib/asciidoctor-pdf/{prawn_ext/formatted_text → formatted_text}/transform.rb +80 -69
  25. data/lib/asciidoctor-pdf/prawn_ext.rb +2 -2
  26. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +164 -35
  27. data/lib/asciidoctor-pdf/prawn_ext/formatted_text/fragment.rb +37 -0
  28. data/lib/asciidoctor-pdf/prawn_ext/images.rb +11 -9
  29. data/lib/asciidoctor-pdf/temporary_path.rb +9 -0
  30. data/lib/asciidoctor-pdf/theme_loader.rb +40 -33
  31. data/lib/asciidoctor-pdf/version.rb +1 -1
  32. metadata +30 -14
@@ -1,6 +1,8 @@
1
+ # regenerate parser.rb using `tt parser.treetop`
1
2
  module Asciidoctor
2
- module Prawn
3
- grammar FormattedText
3
+ module Pdf
4
+ module FormattedText
5
+ grammar Markup
4
6
  rule text
5
7
  complex
6
8
  end
@@ -15,20 +17,20 @@ grammar FormattedText
15
17
 
16
18
  rule element
17
19
  # strict tag matching (costs a minor toll)
18
- # empty_element / start_tag complex end_tag &{|seq| seq[0].name == seq[2].name } {
20
+ # void_element / start_tag complex end_tag &{|seq| seq[0].name == seq[2].name } {
19
21
 
20
- empty_element / start_tag complex end_tag {
21
- # NOTE content only applies to non-empty element
22
+ void_element / start_tag complex end_tag {
23
+ # NOTE content only applies to non-void elements (second part of rule)
22
24
  def content
23
25
  { type: :element, name: (tag_element = elements[0]).name.to_sym, attributes: tag_element.attributes, pcdata: elements[1].content }
24
26
  end
25
27
  }
26
28
  end
27
29
 
28
- rule empty_element
29
- '<br' (spaces? '/')? '>' {
30
+ rule void_element
31
+ '<' void_tag_name attributes (spaces? '/')? '>' {
30
32
  def content
31
- { type: :element, name: :br, attributes: {} }
33
+ { type: :element, name: elements[1].text_value.to_sym, attributes: elements[2].content }
32
34
  end
33
35
  }
34
36
  end
@@ -47,7 +49,13 @@ grammar FormattedText
47
49
 
48
50
  rule tag_name
49
51
  # QUESTION faster to do regex?
50
- 'a' / 'b' / 'code' / 'color' / 'del' / 'em' / 'font' / 'i' / 'link' / 'span' / 'strikethrough' / 'strong' / 'sub' / 'sup' / 'u'
52
+ # QUESTION can we cut stuff we aren't using? what about supporting hr?
53
+ #'a' / 'b' / 'code' / 'color' / 'del' / 'em' / 'font' / 'i' / 'img' / 'link' / 'span' / 'strikethrough' / 'strong' / 'sub' / 'sup' / 'u'
54
+ 'a' / 'code' / 'color' / 'del' / 'em' / 'font' / 'span' / 'strong' / 'sub' / 'sup'
55
+ end
56
+
57
+ rule void_tag_name
58
+ 'br' / 'img'
51
59
  end
52
60
 
53
61
  rule attributes
@@ -113,3 +121,4 @@ grammar FormattedText
113
121
  end
114
122
  end
115
123
  end
124
+ end
@@ -1,10 +1,19 @@
1
1
  module Asciidoctor
2
- module Prawn
3
- class FormattedTextTransform
2
+ module Pdf
3
+ module FormattedText
4
+ class Transform
5
+ EOL = "\n"
6
+ NamedEntityTable = {
7
+ :lt => '<',
8
+ :gt => '>',
9
+ :amp => '&',
10
+ :quot => '"',
11
+ :apos => '\''
12
+ }
4
13
  #ZeroWidthSpace = [0x200b].pack 'U*'
5
14
 
6
15
  def initialize(options = {})
7
- @merge_adjacent_text_nodes = options.fetch(:merge_adjacent_text_nodes, false)
16
+ @merge_adjacent_text_nodes = options[:merge_adjacent_text_nodes]
8
17
  if (theme = options[:theme])
9
18
  @link_font_color = theme.link_font_color
10
19
  @monospaced_font_color = theme.literal_font_color
@@ -20,69 +29,79 @@ class FormattedTextTransform
20
29
  end
21
30
  end
22
31
 
23
- # FIXME might want to pass styles downwards rather than decorating on way up
32
+ # FIXME pass styles downwards to child elements rather than decorating on way out of hierarchy
24
33
  def apply(parsed)
25
34
  fragments = []
26
35
  previous_fragment_is_text = false
27
- # NOTE using inject is slower than a manual loop
36
+ # NOTE we use each since using inject is slower than a manual loop
28
37
  parsed.each {|node|
29
- case (node_type = node[:type])
38
+ case node[:type]
30
39
  when :element
31
- if (tag_name = node[:name]) == :br
32
- if @merge_adjacent_text_nodes && previous_fragment_is_text
33
- fragments << { text: %(#{fragments.pop[:text]}\n) }
34
- else
35
- fragments << { text: "\n" }
36
- end
37
- previous_fragment_is_text = true
38
- else
39
- if (pcdata = node[:pcdata]) && pcdata.size > 0
40
+ # case 1: non-void element
41
+ if (pcdata = node[:pcdata])
42
+ if pcdata.size > 0
43
+ tag_name = node[:name]
40
44
  attributes = node[:attributes]
41
45
  fragments << apply(pcdata).map {|fragment|
42
46
  # decorate child fragments with styles from this element
43
47
  build_fragment(fragment, tag_name, attributes)
44
48
  }
49
+ previous_fragment_is_text = false
50
+ # NOTE skip element if it has no children
45
51
  #else
46
- # # NOTE special case, handle an empty <a> element
47
- # if tag_name == :a
52
+ # # NOTE handle an empty anchor element (i.e., <a ...></a>)
53
+ # if (tag_name = node[:name]) == :a
48
54
  # fragments << build_fragment({ text: ZeroWidthSpace }, tag_name, node[:attributes])
55
+ # previous_fragment_is_text = false
49
56
  # end
50
57
  end
51
- previous_fragment_is_text = false
52
- end
53
- when :text, :entity
54
- # TODO could avoid this redundant type check by splitting :text and :entity cases
55
- node_text = if node_type == :text
56
- node[:value]
57
- elsif node_type == :entity
58
- if (entity_name = node[:name])
59
- case entity_name
60
- when :lt
61
- '<'
62
- when :gt
63
- '>'
64
- when :amp
65
- '&'
66
- when :quot
67
- '"'
68
- when :apos
69
- '\''
58
+ # case 2: void element
59
+ else
60
+ case node[:name]
61
+ when :br
62
+ if @merge_adjacent_text_nodes && previous_fragment_is_text
63
+ fragments << { text: %(#{fragments.pop[:text]}#{EOL}) }
64
+ else
65
+ fragments << { text: EOL }
70
66
  end
71
- else
72
- [node[:number]].pack('U*')
73
- # afm fonts do not include a thin space glyph
74
- # set fallback_fonts to allow glyph to be resolved
75
- #if (node_number = node[:number]) == 8201
76
- # ' '
77
- #else
78
- # [node_number].pack('U*')
79
- #end
67
+ previous_fragment_is_text = true
68
+ when :img
69
+ attributes = node[:attributes]
70
+ fragment = {
71
+ image_path: attributes[:src],
72
+ image_type: attributes[:type],
73
+ image_tmp: (attributes[:tmp] == 'true'),
74
+ text: attributes[:alt],
75
+ callback: InlineImageRenderer
76
+ }
77
+ if (img_w = attributes[:width])
78
+ fragment[:image_width] = img_w.to_f
79
+ end
80
+ fragments << fragment
81
+ previous_fragment_is_text = false
80
82
  end
81
83
  end
84
+ when :text
85
+ text = node[:value]
86
+ # NOTE the remaining logic is shared with :entity
87
+ if @merge_adjacent_text_nodes && previous_fragment_is_text
88
+ fragments << { text: %(#{fragments.pop[:text]}#{text}) }
89
+ else
90
+ fragments << { text: text }
91
+ end
92
+ previous_fragment_is_text = true
93
+ when :entity
94
+ if (name = node[:name])
95
+ text = NamedEntityTable[name]
96
+ else
97
+ # NOTE AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
98
+ text = [node[:number]].pack('U*')
99
+ end
100
+ # NOTE the remaining logic is shared with :text
82
101
  if @merge_adjacent_text_nodes && previous_fragment_is_text
83
- fragments << { text: %(#{fragments.pop[:text]}#{node_text}) }
102
+ fragments << { text: %(#{fragments.pop[:text]}#{text}) }
84
103
  else
85
- fragments << { text: node_text }
104
+ fragments << { text: text }
86
105
  end
87
106
  previous_fragment_is_text = true
88
107
  end
@@ -91,12 +110,13 @@ class FormattedTextTransform
91
110
  end
92
111
 
93
112
  def build_fragment(fragment, tag_name = nil, attrs = {})
94
- #return { text: fragment } if tag_name.nil?
113
+ # QUESTION should we short-circuit if tag_name is nil?
114
+ #return { text: fragment } unless tag_name
95
115
  styles = (fragment[:styles] ||= ::Set.new)
96
116
  case tag_name
97
- when :b, :strong
117
+ when :strong
98
118
  styles << :bold
99
- when :i, :em
119
+ when :em
100
120
  styles << :italic
101
121
  when :code
102
122
  fragment[:font] ||= @monospaced_font_family
@@ -140,12 +160,17 @@ class FormattedTextTransform
140
160
  fragment[:font] = value
141
161
  end
142
162
  if !fragment[:size] && (value = attrs[:size])
143
- fragment[:size] = value.to_f
163
+ # FIXME can we make this comparison more robust / accurate?
164
+ if %(#{f_value = value.to_f}) == value || %(#{value.to_i}) == value
165
+ fragment[:size] = f_value
166
+ elsif value != '1em'
167
+ fragment[:size] = value
168
+ end
144
169
  end
145
170
  #if !fragment[:character_spacing] && (value = attrs[:character_spacing])
146
171
  # fragment[:character_spacing] = value.to_f
147
172
  #end
148
- when :a, :link
173
+ when :a
149
174
  if !fragment[:anchor] && (value = attrs[:anchor])
150
175
  fragment[:anchor] = value
151
176
  end
@@ -164,9 +189,9 @@ class FormattedTextTransform
164
189
  styles << :subscript
165
190
  when :sup
166
191
  styles << :superscript
167
- when :u
168
- styles << :underline
169
- when :del, :strikethrough
192
+ #when :u
193
+ # styles << :underline
194
+ when :del
170
195
  styles << :strikethrough
171
196
  when :span
172
197
  # span logic with normal style parsing
@@ -201,20 +226,6 @@ class FormattedTextTransform
201
226
  fragment
202
227
  end
203
228
  end
204
-
205
- module InlineDestinationMarker
206
- module_function
207
-
208
- def render_behind fragment
209
- unless (pdf_doc = fragment.instance_variable_get :@document).scratch?
210
- if (name = (fragment.instance_variable_get :@format_state)[:name])
211
- # get precise position of the reference
212
- dest_rect = fragment.absolute_bounding_box
213
- # QUESTION should we set precise x value of destination or just 0?
214
- pdf_doc.add_dest name, (pdf_doc.dest_xyz dest_rect.first, dest_rect.last)
215
- end
216
- end
217
- end
218
229
  end
219
230
  end
220
231
  end
@@ -1,4 +1,4 @@
1
- # the following modules / classes are organized under the Asciidoctor::Prawn namespace
1
+ # the following are organized under the Asciidoctor::Prawn namespace
2
2
  require_relative 'prawn_ext/images'
3
+ require_relative 'prawn_ext/formatted_text/fragment'
3
4
  require_relative 'prawn_ext/extensions'
4
- require_relative 'prawn_ext/formatted_text/formatter'
@@ -5,6 +5,8 @@ module Extensions
5
5
  include ::Asciidoctor::Pdf::Sanitizer
6
6
  include ::Asciidoctor::PdfCore::PdfObject
7
7
 
8
+ MeasurementValueRx = /(\d+|\d*\.\d+)(in|mm|cm|px|pt)?$/
9
+
8
10
  # - :height is the height of a line
9
11
  # - :leading is spacing between adjacent lines
10
12
  # - :padding_top is half line spacing, plus any line_gap in the font
@@ -28,12 +30,24 @@ module Extensions
28
30
  page.dimensions[2]
29
31
  end
30
32
 
33
+ # Returns the effective (writable) width of the page
34
+ #
35
+ def effective_page_width
36
+ reference_bounds.width
37
+ end
38
+
31
39
  # Returns the height of the current page from edge-to-edge
32
40
  #
33
41
  def page_height
34
42
  page.dimensions[3]
35
43
  end
36
44
 
45
+ # Returns the effective (writable) height of the page
46
+ #
47
+ def effective_page_height
48
+ reference_bounds.height
49
+ end
50
+
37
51
  # Returns the width of the left margin for the current page
38
52
  #
39
53
  def page_margin_left
@@ -68,6 +82,37 @@ module Extensions
68
82
  @y == @margin_box.absolute_top
69
83
  end
70
84
 
85
+ # Converts the specified float value to a pt value from the
86
+ # specified unit of measurement (e.g., in, cm, mm, etc).
87
+ def to_pt num, units
88
+ case units
89
+ when nil, 'pt'
90
+ num
91
+ when 'in'
92
+ num * 72
93
+ when 'mm'
94
+ num * (72 / 25.4)
95
+ when 'cm'
96
+ num * (720 / 25.4)
97
+ when 'px'
98
+ num * 0.75
99
+ end
100
+ end
101
+
102
+ # Convert the specified string value to a pt value from the
103
+ # specified unit of measurement (e.g., in, cm, mm, etc).
104
+ #
105
+ # Examples:
106
+ #
107
+ # 0.5in => 36.0
108
+ # 100px => 75.0
109
+ #
110
+ def str_to_pt val
111
+ if MeasurementValueRx =~ val
112
+ to_pt $1.to_f, $2
113
+ end
114
+ end
115
+
71
116
  # Destinations
72
117
 
73
118
  # Generates a destination object that resolves to the top of the page
@@ -95,10 +140,11 @@ module Extensions
95
140
  #
96
141
  # Example:
97
142
  #
98
- # register_font GillSans: {
99
- # normal: 'assets/fonts/GillSans.ttf',
100
- # bold: 'assets/fonts/GillSans-Bold.ttf',
101
- # italic: 'assets/fonts/GillSans-Italic.ttf',
143
+ # register_font Roboto: {
144
+ # normal: 'fonts/roboto-normal.ttf',
145
+ # italic: 'fonts/roboto-italic.ttf',
146
+ # bold: 'fonts/roboto-bold.ttf',
147
+ # bold_italic: 'fonts/roboto-bold_italic.ttf'
102
148
  # }
103
149
  #
104
150
  def register_font data
@@ -126,7 +172,7 @@ module Extensions
126
172
  # Retrieves the current font info (family, style, size) as a Hash
127
173
  #
128
174
  def font_info
129
- { family: font.options[:family], style: font.options[:style] || :normal, size: @font_size }
175
+ { family: font.options[:family], style: (font.options[:style] || :normal), size: @font_size }
130
176
  end
131
177
 
132
178
  # Sets the font style for the scope of the block to which this method
@@ -153,17 +199,62 @@ module Extensions
153
199
  # QUESTION should we round the result?
154
200
  def font_size points = nil
155
201
  return @font_size unless points
156
- #if points.is_a? String
157
- # # QUESTION should we round?
158
- # points = (@font_size * (points.chop.to_f / 100.0)).round
159
- # warn points
160
- #elsif points <= 1
161
- # points = (@font_size * points)
162
- #end
163
- if points <= 1
164
- points = (@font_size * points)
202
+ if points == 1
203
+ super @font_size
204
+ elsif String === points
205
+ if points.end_with? 'rem'
206
+ super (@theme.base_font_size * points.to_f)
207
+ elsif points.end_with? 'em'
208
+ super (@font_size * points.to_f)
209
+ elsif points.end_with? '%'
210
+ super (@font_size * (points.to_f / 100.0))
211
+ else
212
+ super points.to_f
213
+ end
214
+ # FIXME HACK assume em value
215
+ elsif points < 1
216
+ super (@font_size * points)
217
+ else
218
+ super points
219
+ end
220
+ end
221
+
222
+ def resolve_font_style styles
223
+ if styles.include? :bold
224
+ (styles.include? :italic) ? :bold_italic : :bold
225
+ elsif styles.include? :italic
226
+ :italic
227
+ else
228
+ :normal
229
+ end
230
+ end
231
+
232
+ # Apply the font settings (family, size, styles and character spacing) from
233
+ # the fragment to the document, then yield to the block.
234
+ #
235
+ # The original font settings are restored before this method returns.
236
+ #
237
+ def fragment_font fragment
238
+ f_info = font_info
239
+ f_family = fragment[:font] || f_info[:family]
240
+ f_size = fragment[:size] || f_info[:size]
241
+ if (f_styles = fragment[:styles])
242
+ f_style = resolve_font_style f_styles
243
+ else
244
+ f_style = :normal
245
+ end
246
+
247
+ if (c_spacing = fragment[:character_spacing])
248
+ character_spacing c_spacing do
249
+ font f_family, size: f_size, style: f_style do
250
+ yield
251
+ end
252
+ end
253
+ else
254
+ font f_family, size: f_size, style: f_style do
255
+ yield
256
+ end
165
257
  end
166
- super points
167
258
  end
168
259
 
169
260
  def calc_line_metrics line_height = 1, font = self.font, font_size = self.font_size
@@ -177,7 +268,7 @@ module Extensions
177
268
 
178
269
  =begin
179
270
  # these line metrics attempted to figure out a correction based on the reported height and the font_size
180
- # however, it only works for some fonts, and breaks down for fonts like NotoSerif
271
+ # however, it only works for some fonts, and breaks down for fonts like Noto Serif
181
272
  def calc_line_metrics line_height = 1, font = self.font, font_size = self.font_size
182
273
  line_height_length = font_size * line_height
183
274
  line_gap = line_height_length - font_size
@@ -210,21 +301,38 @@ module Extensions
210
301
  end
211
302
  end
212
303
 
213
- # Performs the same work as text except that the first_line_options
214
- # are applied to the first line of text renderered.
215
- def text_with_formatted_first_line string, first_line_options, opts
304
+ # Performs the same work as text except that the first_line_opts
305
+ # are applied to the first line of text renderered. It's necessary
306
+ # to use low-level APIs in this method so that we only style the
307
+ # first line and not the remaining lines (which is the default
308
+ # behavior in Prawn).
309
+ def text_with_formatted_first_line string, first_line_opts, opts
310
+ color = opts.delete :color
216
311
  fragments = parse_text string, opts
312
+ # NOTE the low-level APIs we're using don't recognize the :styles option, so we must resolve
313
+ if (styles = opts.delete :styles)
314
+ opts[:style] = resolve_font_style styles
315
+ end
316
+ if (first_line_styles = first_line_opts.delete :styles)
317
+ first_line_opts[:style] = resolve_font_style first_line_styles
318
+ end
319
+ first_line_color = (first_line_opts.delete :color) || color
217
320
  opts = opts.merge document: self
218
- box = ::Prawn::Text::Formatted::Box.new fragments, (opts.merge single_line: true)
321
+ # QUESTION should we merge more carefully here? (hand-select keys?)
322
+ first_line_opts = opts.merge(first_line_opts).merge single_line: true
323
+ box = ::Prawn::Text::Formatted::Box.new fragments, first_line_opts
324
+ # NOTE get remaining_fragments before we add color to fragments on first line
219
325
  remaining_fragments = box.render dry_run: true
220
- # HACK prawn removes the color from remaining_fragments, so we have to explicitly restore
221
- if (color = opts[:color])
222
- remaining_fragments.each {|fragment| fragment[:color] ||= color }
326
+ # NOTE color must be applied per-fragment
327
+ if first_line_color
328
+ fragments.each {|fragment| fragment[:color] ||= first_line_color}
223
329
  end
224
- # FIXME merge options more intelligently so as not to clobber other styles in set
225
- fragments = fragments.map {|fragment| fragment.merge first_line_options }
226
- fill_formatted_text_box fragments, (opts.merge single_line: true)
227
- if remaining_fragments.size > 0
330
+ fill_formatted_text_box fragments, first_line_opts
331
+ unless remaining_fragments.empty?
332
+ # NOTE color must be applied per-fragment
333
+ if color
334
+ remaining_fragments.each {|fragment| fragment[:color] ||= color }
335
+ end
228
336
  # as of Prawn 1.2.1, we have to handle the line gap after the first line manually
229
337
  move_down opts[:leading]
230
338
  remaining_fragments = fill_formatted_text_box remaining_fragments, opts
@@ -364,7 +472,7 @@ module Extensions
364
472
  #
365
473
  def fill_and_stroke_bounds f_color = fill_color, s_color = stroke_color, options = {}
366
474
  no_fill = !f_color || f_color == 'transparent'
367
- no_stroke = !s_color || s_color == 'transparent'
475
+ no_stroke = !s_color || s_color == 'transparent' || options[:line_width] == 0
368
476
  return if no_fill && no_stroke
369
477
  save_graphics_state do
370
478
  radius = options[:radius] || 0
@@ -378,7 +486,7 @@ module Extensions
378
486
  # stroke
379
487
  unless no_stroke
380
488
  stroke_color s_color
381
- line_width options[:line_width] || 0.5
489
+ line_width(options[:line_width] || 0.5)
382
490
  # FIXME think about best way to indicate dashed borders
383
491
  #if options.has_key? :dash_width
384
492
  # dash options[:dash_width], space: options[:dash_space] || 1
@@ -429,9 +537,26 @@ module Extensions
429
537
  #
430
538
  def stroke_horizontal_rule s_color = stroke_color, options = {}
431
539
  save_graphics_state do
432
- line_width options[:line_width] || 0.5
540
+ line_width(l_width = options[:line_width] || 0.5)
433
541
  stroke_color s_color
434
- stroke_horizontal_line bounds.left, bounds.right
542
+ case (options[:line_style] || :solid)
543
+ when :solid
544
+ stroke_horizontal_line bounds.left, bounds.right
545
+ when :double
546
+ move_up l_width * 1.5
547
+ stroke_horizontal_line bounds.left, bounds.right
548
+ move_down l_width * 3
549
+ stroke_horizontal_line bounds.left, bounds.right
550
+ move_up l_width * 1.5
551
+ when :dashed
552
+ dash l_width * 4
553
+ stroke_horizontal_line bounds.left, bounds.right
554
+ undash
555
+ when :dotted
556
+ dash l_width
557
+ stroke_horizontal_line bounds.left, bounds.right
558
+ undash
559
+ end
435
560
  end
436
561
  end
437
562
 
@@ -511,20 +636,24 @@ module Extensions
511
636
  end
512
637
  end
513
638
 
514
- def is_scratch?
515
- !!state.store.info.data[:Scratch]
639
+ def scratch?
640
+ (@_label ||= (state.store.info.data[:Scratch] ? :scratch : :primary)) == :scratch
516
641
  end
517
- alias :scratch? :is_scratch?
642
+ alias :is_scratch? :scratch?
518
643
 
519
644
  # TODO document me
520
645
  def dry_run &block
521
646
  scratch = get_scratch_document
522
647
  scratch.start_new_page
523
648
  start_page_number = scratch.page_number
649
+ # QUESTION is it enough to just set the padding or do we need to clone the bounds?
650
+ default_bounds = scratch.bounds
651
+ scratch.bounds = bounds.deep_copy.tap {|b| b.instance_variable_set :@document, scratch }
524
652
  start_y = scratch.y
525
653
  scratch.font font_family, style: font_style, size: font_size do
526
654
  scratch.instance_exec(&block)
527
655
  end
656
+ scratch.bounds = default_bounds
528
657
  whole_pages = scratch.page_number - start_page_number
529
658
  [(whole_pages * bounds.height + (start_y - scratch.y)), whole_pages, (start_y - scratch.y)]
530
659
  end
@@ -537,7 +666,7 @@ module Extensions
537
666
  total_height, _whole_pages, _remainder = dry_run(&block)
538
667
  # NOTE technically, if we're at the page top, we don't even need to do the
539
668
  # dry run, except several uses of this method rely on the calculated height
540
- if total_height > available_space && !at_page_top?
669
+ if total_height > available_space && !at_page_top? && total_height <= effective_page_height
541
670
  start_new_page
542
671
  started_new_page = true
543
672
  else