asciidoctor-pdf 1.5.0.alpha.17 → 1.5.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +75 -2
  3. data/NOTICE.adoc +14 -11
  4. data/README.adoc +105 -27
  5. data/asciidoctor-pdf.gemspec +4 -1
  6. data/data/themes/base-theme.yml +4 -0
  7. data/data/themes/default-theme.yml +17 -34
  8. data/data/themes/default-with-fallback-font-theme.yml +22 -0
  9. data/docs/theming-guide.adoc +1057 -867
  10. data/lib/asciidoctor-pdf/asciidoctor_ext/abstract_block.rb +5 -0
  11. data/lib/asciidoctor-pdf/asciidoctor_ext/document.rb +3 -0
  12. data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +4 -4
  13. data/lib/asciidoctor-pdf/asciidoctor_ext/logging_shim.rb +8 -2
  14. data/lib/asciidoctor-pdf/asciidoctor_ext/section.rb +16 -8
  15. data/lib/asciidoctor-pdf/asciidoctor_ext.rb +3 -1
  16. data/lib/asciidoctor-pdf/converter.rb +758 -499
  17. data/lib/asciidoctor-pdf/core_ext/hash.rb +5 -0
  18. data/lib/asciidoctor-pdf/core_ext/regexp.rb +3 -0
  19. data/lib/asciidoctor-pdf/core_ext.rb +2 -0
  20. data/lib/asciidoctor-pdf/formatted_text/formatter.rb +8 -1
  21. data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +3 -1
  22. data/lib/asciidoctor-pdf/formatted_text/parser.rb +24 -12
  23. data/lib/asciidoctor-pdf/formatted_text/parser.treetop +1 -1
  24. data/lib/asciidoctor-pdf/formatted_text/text_background_and_border_renderer.rb +45 -0
  25. data/lib/asciidoctor-pdf/formatted_text/transform.rb +44 -21
  26. data/lib/asciidoctor-pdf/formatted_text.rb +1 -0
  27. data/lib/asciidoctor-pdf/index_catalog.rb +9 -3
  28. data/lib/asciidoctor-pdf/measurements.rb +1 -1
  29. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +37 -21
  30. data/lib/asciidoctor-pdf/prawn_ext/images.rb +18 -7
  31. data/lib/asciidoctor-pdf/roman_numeral.rb +12 -0
  32. data/lib/asciidoctor-pdf/theme_loader.rb +99 -69
  33. data/lib/asciidoctor-pdf/version.rb +1 -1
  34. metadata +45 -5
@@ -0,0 +1,5 @@
1
+ class Hash
2
+ def compact
3
+ select {|_, val| val }
4
+ end unless method_defined? :compact
5
+ end
@@ -0,0 +1,3 @@
1
+ class Regexp
2
+ alias match? === unless Regexp.method_defined? :match?
3
+ end
@@ -1,4 +1,6 @@
1
1
  require_relative 'core_ext/object'
2
2
  require_relative 'core_ext/array'
3
+ require_relative 'core_ext/hash'
3
4
  require_relative 'core_ext/numeric'
4
5
  require_relative 'core_ext/string'
6
+ require_relative 'core_ext/regexp'
@@ -19,7 +19,7 @@ class Formatter
19
19
  def format string, *args
20
20
  options = args[0] || {}
21
21
  string = string.tr_s(WHITESPACE, ' ') if options[:normalize]
22
- return [text: string] unless string.match(FormattingSnifferPattern)
22
+ return [text: string] unless FormattingSnifferPattern.match? string
23
23
  if (parsed = @parser.parse(string))
24
24
  @transform.apply(parsed.content)
25
25
  else
@@ -27,6 +27,13 @@ class Formatter
27
27
  [text: string]
28
28
  end
29
29
  end
30
+
31
+ # The original purpose of this method is to split paragraphs, but our formatter only works on paragraphs that have
32
+ # been presplit. Therefore, we just need to wrap the fragments in a single-element array (representing a single
33
+ # paragraph) and return them.
34
+ def array_paragraphs fragments
35
+ [fragments]
36
+ end
30
37
  end
31
38
  end
32
39
  end
@@ -66,7 +66,9 @@ module InlineImageArranger
66
66
  svg_obj = ::Prawn::SVG::Interface.new ::File.read(image_path), doc,
67
67
  at: doc.bounds.top_left,
68
68
  width: image_w,
69
- fallback_font_name: doc.default_svg_font
69
+ fallback_font_name: doc.default_svg_font,
70
+ enable_web_requests: doc.allow_uri_read,
71
+ enable_file_requests_with_root: (::File.dirname image_path)
70
72
  svg_size = image_w ? svg_obj.document.sizing :
71
73
  # NOTE convert intrinsic dimensions to points; constrain to content width
72
74
  (svg_obj.resize width: [(to_pt svg_obj.document.sizing.output_width, :px), available_w].min)
@@ -356,44 +356,44 @@ module Markup
356
356
  r1 = SyntaxNode.new(input, (index-1)...index) if r1 == true
357
357
  r0 = r1
358
358
  else
359
- if (match_len = has_terminal?('code', false, index))
359
+ if (match_len = has_terminal?('strong', false, index))
360
360
  r2 = instantiate_node(SyntaxNode,input, index...(index + match_len))
361
361
  @index += match_len
362
362
  else
363
- terminal_parse_failure('code')
363
+ terminal_parse_failure('strong')
364
364
  r2 = nil
365
365
  end
366
366
  if r2
367
367
  r2 = SyntaxNode.new(input, (index-1)...index) if r2 == true
368
368
  r0 = r2
369
369
  else
370
- if (match_len = has_terminal?('color', false, index))
370
+ if (match_len = has_terminal?('em', false, index))
371
371
  r3 = instantiate_node(SyntaxNode,input, index...(index + match_len))
372
372
  @index += match_len
373
373
  else
374
- terminal_parse_failure('color')
374
+ terminal_parse_failure('em')
375
375
  r3 = nil
376
376
  end
377
377
  if r3
378
378
  r3 = SyntaxNode.new(input, (index-1)...index) if r3 == true
379
379
  r0 = r3
380
380
  else
381
- if (match_len = has_terminal?('del', false, index))
381
+ if (match_len = has_terminal?('code', false, index))
382
382
  r4 = instantiate_node(SyntaxNode,input, index...(index + match_len))
383
383
  @index += match_len
384
384
  else
385
- terminal_parse_failure('del')
385
+ terminal_parse_failure('code')
386
386
  r4 = nil
387
387
  end
388
388
  if r4
389
389
  r4 = SyntaxNode.new(input, (index-1)...index) if r4 == true
390
390
  r0 = r4
391
391
  else
392
- if (match_len = has_terminal?('em', false, index))
392
+ if (match_len = has_terminal?('color', false, index))
393
393
  r5 = instantiate_node(SyntaxNode,input, index...(index + match_len))
394
394
  @index += match_len
395
395
  else
396
- terminal_parse_failure('em')
396
+ terminal_parse_failure('color')
397
397
  r5 = nil
398
398
  end
399
399
  if r5
@@ -422,11 +422,11 @@ module Markup
422
422
  r7 = SyntaxNode.new(input, (index-1)...index) if r7 == true
423
423
  r0 = r7
424
424
  else
425
- if (match_len = has_terminal?('strong', false, index))
425
+ if (match_len = has_terminal?('button', false, index))
426
426
  r8 = instantiate_node(SyntaxNode,input, index...(index + match_len))
427
427
  @index += match_len
428
428
  else
429
- terminal_parse_failure('strong')
429
+ terminal_parse_failure('button')
430
430
  r8 = nil
431
431
  end
432
432
  if r8
@@ -455,8 +455,20 @@ module Markup
455
455
  r10 = SyntaxNode.new(input, (index-1)...index) if r10 == true
456
456
  r0 = r10
457
457
  else
458
- @index = i0
459
- r0 = nil
458
+ if (match_len = has_terminal?('del', false, index))
459
+ r11 = instantiate_node(SyntaxNode,input, index...(index + match_len))
460
+ @index += match_len
461
+ else
462
+ terminal_parse_failure('del')
463
+ r11 = nil
464
+ end
465
+ if r11
466
+ r11 = SyntaxNode.new(input, (index-1)...index) if r11 == true
467
+ r0 = r11
468
+ else
469
+ @index = i0
470
+ r0 = nil
471
+ end
460
472
  end
461
473
  end
462
474
  end
@@ -51,7 +51,7 @@ grammar Markup
51
51
  # QUESTION faster to do regex?
52
52
  # QUESTION can we cut stuff we aren't using? what about supporting hr?
53
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'
54
+ 'a' / 'strong' / 'em' / 'code' / 'color' / 'font' / 'span' / 'button' / 'sub' / 'sup' / 'del'
55
55
  end
56
56
 
57
57
  rule void_tag_name
@@ -0,0 +1,45 @@
1
+ module Asciidoctor::Pdf::FormattedText
2
+ module TextBackgroundAndBorderRenderer
3
+ module_function
4
+
5
+ # render_behind is called before the text is printed
6
+ def render_behind fragment
7
+ return if (pdf = fragment.document).scratch?
8
+ data = fragment.format_state
9
+ if (border_offset = data[:border_offset])
10
+ at = [fragment.left - border_offset, fragment.top + border_offset]
11
+ width = fragment.width + border_offset * 2
12
+ height = fragment.height + border_offset * 2
13
+ else
14
+ at = fragment.top_left
15
+ width = fragment.width
16
+ height = fragment.height
17
+ end
18
+ border_radius = data[:border_radius]
19
+ if (background_color = data[:background_color])
20
+ prev_fill_color = pdf.fill_color
21
+ pdf.fill_color background_color
22
+ if border_radius
23
+ pdf.fill_rounded_rectangle at, width, height, border_radius
24
+ else
25
+ pdf.fill_rectangle at, width, height
26
+ end
27
+ pdf.fill_color prev_fill_color
28
+ end
29
+ if (border_width = data[:border_width])
30
+ border_color = data[:border_color]
31
+ prev_stroke_color = pdf.stroke_color
32
+ prev_line_width = pdf.line_width
33
+ pdf.stroke_color border_color
34
+ pdf.line_width border_width
35
+ if border_radius
36
+ pdf.stroke_rounded_rectangle at, width, height, border_radius
37
+ else
38
+ pdf.stroke_rectangle at, width, height
39
+ end
40
+ pdf.stroke_color prev_stroke_color
41
+ pdf.line_width prev_line_width
42
+ end
43
+ end
44
+ end
45
+ end
@@ -20,21 +20,44 @@ class Transform
20
20
  @merge_adjacent_text_nodes = options[:merge_adjacent_text_nodes]
21
21
  # TODO add support for character spacing
22
22
  if (theme = options[:theme])
23
- @link_font_settings = {
24
- color: theme.link_font_color,
25
- font: theme.link_font_family,
26
- size: theme.link_font_size,
27
- styles: to_styles(theme.link_font_style, theme.link_text_decoration)
28
- }.select! {|_, val| val }
29
- @monospaced_font_settings = {
30
- color: theme.literal_font_color,
31
- font: theme.literal_font_family,
32
- size: theme.literal_font_size,
33
- styles: to_styles(theme.literal_font_style)
34
- }.select! {|_, val| val }
23
+ @theme_settings = {
24
+ button: {
25
+ color: theme.button_font_color,
26
+ font: theme.button_font_family,
27
+ size: theme.button_font_size,
28
+ styles: to_styles(theme.button_font_style),
29
+ background_color: (button_bg_color = theme.button_background_color),
30
+ border_width: (button_border_width = theme.button_border_width),
31
+ border_color: button_border_width && (theme.button_border_color || theme.base_border_color),
32
+ border_offset: (button_bg_or_border = button_bg_color || button_border_width) && theme.button_border_offset,
33
+ border_radius: button_bg_or_border && theme.button_border_radius,
34
+ callback: button_bg_or_border && [TextBackgroundAndBorderRenderer],
35
+ }.compact,
36
+ code: {
37
+ color: theme.literal_font_color,
38
+ font: theme.literal_font_family,
39
+ size: theme.literal_font_size,
40
+ styles: to_styles(theme.literal_font_style),
41
+ background_color: (monospaced_bg_color = theme.literal_background_color),
42
+ border_width: (monospaced_border_width = theme.literal_border_width),
43
+ border_color: monospaced_border_width && (theme.literal_border_color || theme.base_border_color),
44
+ border_offset: (monospaced_bg_or_border = monospaced_bg_color || monospaced_border_width) && theme.literal_border_offset,
45
+ border_radius: monospaced_bg_or_border && theme.literal_border_radius,
46
+ callback: monospaced_bg_or_border && [TextBackgroundAndBorderRenderer],
47
+ }.compact,
48
+ link: {
49
+ color: theme.link_font_color,
50
+ font: theme.link_font_family,
51
+ size: theme.link_font_size,
52
+ styles: to_styles(theme.link_font_style, theme.link_text_decoration)
53
+ }.compact,
54
+ }
35
55
  else
36
- @link_font_settings = { color: '0000FF' }
37
- @monospaced_font_settings = { font: 'Courier', size: 0.9 }
56
+ @theme_settings = {
57
+ button: { font: 'Courier', styles: [:bold].to_set },
58
+ code: { font: 'Courier', size: 0.9 },
59
+ link: { color: '0000FF' },
60
+ }
38
61
  end
39
62
  end
40
63
 
@@ -79,7 +102,7 @@ class Transform
79
102
  image_format: attributes[:format],
80
103
  # a zero-width space in the text will cause the image to be duplicated
81
104
  text: (attributes[:alt].delete ZeroWidthSpace),
82
- callback: InlineImageRenderer
105
+ callback: [InlineImageRenderer],
83
106
  }
84
107
  if (img_w = attributes[:width])
85
108
  fragment[:image_width] = img_w
@@ -123,9 +146,9 @@ class Transform
123
146
  styles << :bold
124
147
  when :em
125
148
  styles << :italic
126
- when :code
127
- # NOTE prefer old value, except for styles, which should be combined
128
- fragment.update(@monospaced_font_settings) {|k, old_v, new_v| k == :styles ? old_v.merge(new_v) : old_v }
149
+ when :code, :button
150
+ # NOTE prefer old value, except for styles and callback, which should be combined
151
+ fragment.update(@theme_settings[tag_name]) {|k, oval, nval| k == :styles ? oval.merge(nval) : (k == :callback ? oval.union(nval) : oval) }
129
152
  when :color
130
153
  if !fragment[:color]
131
154
  if (rgb = attrs[:rgb])
@@ -168,7 +191,7 @@ class Transform
168
191
  fragment[:width] = value
169
192
  if (value = attrs[:align])
170
193
  fragment[:align] = value.to_sym
171
- fragment[:callback] = InlineTextAligner
194
+ (fragment[:callback] ||= []) << InlineTextAligner
172
195
  end
173
196
  end
174
197
  #if !fragment[:character_spacing] && (value = attrs[:character_spacing])
@@ -191,12 +214,12 @@ class Transform
191
214
  if (type = attrs[:type])
192
215
  fragment[:type] = type.to_sym
193
216
  end
194
- fragment[:callback] = InlineDestinationMarker
217
+ (fragment[:callback] ||= []) << InlineDestinationMarker
195
218
  visible = false
196
219
  end
197
220
  end
198
221
  # NOTE prefer old value, except for styles, which should be combined
199
- fragment.update(@link_font_settings) {|k, old_v, new_v| k == :styles ? old_v.merge(new_v) : old_v } if visible
222
+ fragment.update(@theme_settings[:link]) {|k, oval, nval| k == :styles ? oval.merge(nval) : oval } if visible
200
223
  when :sub
201
224
  styles << :subscript
202
225
  when :sup
@@ -7,3 +7,4 @@ require_relative 'formatted_text/inline_destination_marker'
7
7
  require_relative 'formatted_text/inline_image_arranger'
8
8
  require_relative 'formatted_text/inline_image_renderer'
9
9
  require_relative 'formatted_text/inline_text_aligner'
10
+ require_relative 'formatted_text/text_background_and_border_renderer'
@@ -1,5 +1,6 @@
1
1
  module Asciidoctor; module PDF
2
2
  class IndexCatalog
3
+ include Sanitizer
3
4
  LeadingAlphaRx = /^\p{Alpha}/
4
5
 
5
6
  attr_accessor :start_page_number
@@ -8,6 +9,11 @@ module Asciidoctor; module PDF
8
9
  @categories = {}
9
10
  @start_page_number = 1
10
11
  @dests = {}
12
+ @sequence = 0
13
+ end
14
+
15
+ def next_anchor_name
16
+ %(__indexterm-#{@sequence += 1})
11
17
  end
12
18
 
13
19
  def store_term names, dest = nil
@@ -22,7 +28,7 @@ module Asciidoctor; module PDF
22
28
 
23
29
  def store_primary_term name, dest = nil
24
30
  store_dest dest if dest
25
- (init_category name.chr.upcase).store_term name, dest
31
+ (init_category uppercase_mb name.chr).store_term name, dest
26
32
  end
27
33
 
28
34
  def store_secondary_term primary_name, secondary_name, dest = nil
@@ -36,8 +42,8 @@ module Asciidoctor; module PDF
36
42
  end
37
43
 
38
44
  def init_category name
39
- name = '@' unless LeadingAlphaRx =~ name
40
- @categories[name] ||= (IndexTermCategory.new name)
45
+ name = '@' unless LeadingAlphaRx.match? name
46
+ @categories[name] ||= IndexTermCategory.new name
41
47
  end
42
48
 
43
49
  def find_category name
@@ -48,7 +48,7 @@ module Asciidoctor; module PDF
48
48
 
49
49
  # Resolve measurement values in the string to PDF points.
50
50
  def resolve_measurement_values str
51
- if MeasurementValueHintRx =~ str
51
+ if MeasurementValueHintRx.match? str
52
52
  str.gsub(InsetMeasurementValueRx) { to_pt $1.to_f, $2 }
53
53
  else
54
54
  str
@@ -39,7 +39,7 @@ module Extensions
39
39
 
40
40
  # Returns the effective (writable) width of the page
41
41
  #
42
- # If inside a fixed-height bounding box, returns height of box.
42
+ # If inside a bounding box, returns width of box.
43
43
  #
44
44
  def effective_page_width
45
45
  reference_bounds.width
@@ -357,11 +357,19 @@ module Extensions
357
357
  end
358
358
  end
359
359
 
360
- # Performs the same work as text except that the first_line_opts
361
- # are applied to the first line of text renderered. It's necessary
362
- # to use low-level APIs in this method so that we only style the
363
- # first line and not the remaining lines (which is the default
364
- # behavior in Prawn).
360
+ # NOTE override built-in draw_indented_formatted_line to insert leading before second line
361
+ def draw_indented_formatted_line string, opts
362
+ result = super
363
+ unless @no_text_printed || @all_text_printed
364
+ # as of Prawn 1.2.1, we have to handle the line gap after the first line manually
365
+ move_down opts[:leading]
366
+ end
367
+ result
368
+ end
369
+
370
+ # Performs the same work as Prawn::Text.text except that the first_line_opts are applied to the first line of text
371
+ # renderered. It's necessary to use low-level APIs in this method so we only style the first line and not the
372
+ # remaining lines (which is the default behavior in Prawn).
365
373
  def text_with_formatted_first_line string, first_line_opts, opts
366
374
  color = opts.delete :color
367
375
  fragments = parse_text string, opts
@@ -378,17 +386,27 @@ module Extensions
378
386
  first_line_opts = opts.merge(first_line_opts).merge single_line: true
379
387
  box = ::Prawn::Text::Formatted::Box.new fragments, first_line_opts
380
388
  # NOTE get remaining_fragments before we add color to fragments on first line
381
- remaining_fragments = box.render dry_run: true
389
+ if (text_indent = opts.delete :indent_paragraphs)
390
+ remaining_fragments = indent text_indent do
391
+ box.render dry_run: true
392
+ end
393
+ else
394
+ remaining_fragments = box.render dry_run: true
395
+ end
382
396
  # NOTE color must be applied per-fragment
383
397
  if first_line_color
384
398
  fragments.each {|fragment| fragment[:color] ||= first_line_color}
385
399
  end
386
- fill_formatted_text_box fragments, first_line_opts
400
+ if text_indent
401
+ indent text_indent do
402
+ fill_formatted_text_box fragments, first_line_opts
403
+ end
404
+ else
405
+ fill_formatted_text_box fragments, first_line_opts
406
+ end
387
407
  unless remaining_fragments.empty?
388
408
  # NOTE color must be applied per-fragment
389
- if color
390
- remaining_fragments.each {|fragment| fragment[:color] ||= color }
391
- end
409
+ remaining_fragments.each {|fragment| fragment[:color] ||= color } if color
392
410
  # as of Prawn 1.2.1, we have to handle the line gap after the first line manually
393
411
  move_down opts[:leading]
394
412
  remaining_fragments = fill_formatted_text_box remaining_fragments, opts
@@ -730,20 +748,18 @@ module Extensions
730
748
  nil
731
749
  end
732
750
 
733
- # Create a new page for the specified image. If the
734
- # canvas option is true, the image is stretched to the
735
- # edges of the page (full coverage).
751
+ # Create a new page for the specified image. If the canvas option is true,
752
+ # the image is positioned relative to the boundaries of the page.
736
753
  def image_page file, options = {}
737
754
  start_new_page_discretely
738
- if options[:canvas]
739
- canvas do
740
- image file, width: bounds.width, height: bounds.height
741
- end
755
+ image_page_number = page_number
756
+ if options.delete :canvas
757
+ canvas { image file, ({ position: :center, vposition: :center }.merge options) }
742
758
  else
743
- image file, fit: [bounds.width, bounds.height]
759
+ image file, (options.merge position: :center, vposition: :center, fit: [bounds.width, bounds.height])
744
760
  end
745
- # FIXME shouldn't this be `go_to_page prev_page_number + 1`?
746
- go_to_page page_count
761
+ # NOTE advance to new page just in case the image function threw off the cursor
762
+ go_to_page image_page_number
747
763
  nil
748
764
  end
749
765
 
@@ -9,10 +9,20 @@ module Images
9
9
 
10
10
  # Dispatch to suitable image method in Prawn based on file extension.
11
11
  def image file, opts = {}
12
- # FIXME handle case when SVG is a File or IO object
13
- if ::String === file && (file.downcase.end_with? '.svg')
14
- opts[:fallback_font_name] ||= default_svg_font if respond_to? :default_svg_font
15
- svg((::File.read file), opts)
12
+ # FIXME handle case when SVG is an IO object
13
+ if ::String === file && (((opts = opts.dup).delete :format) == 'svg' || (file.downcase.end_with? '.svg'))
14
+ #opts[:enable_file_requests_with_root] = (::File.dirname file) unless opts.key? :enable_file_requests_with_root
15
+ #opts[:enable_web_requests] = allow_uri_read if !(opts.key? :enable_web_requests) && (respond_to? :allow_uri_read)
16
+ #opts[:fallback_font_name] = default_svg_font if !(opts.key? :fallback_font_name) && (respond_to? :default_svg_font)
17
+ if (opts.key? :fit) && (fit = opts.delete :fit) && !opts[:width] && !opts[:height]
18
+ svg (::File.read file), opts do |svg_doc|
19
+ max_width, max_height = fit
20
+ svg_doc.calculate_sizing requested_width: max_width if max_width && svg_doc.sizing.output_width != max_width
21
+ svg_doc.calculate_sizing requested_height: max_height if max_height && svg_doc.sizing.output_height > max_height
22
+ end
23
+ else
24
+ svg (::File.read file), opts
25
+ end
16
26
  else
17
27
  _initial_image file, opts
18
28
  end
@@ -22,9 +32,9 @@ module Images
22
32
  #
23
33
  # Returns a Hash containing :width and :height keys that map to the image's
24
34
  # intrinsic width and height values (in pixels)
25
- def intrinsic_image_dimensions path
26
- if path.end_with? '.svg'
27
- img_obj = ::Prawn::SVG::Interface.new ::File.read(path), self, {}
35
+ def intrinsic_image_dimensions path, format
36
+ if format == 'svg'
37
+ img_obj = ::Prawn::SVG::Interface.new (::File.read path), self, {}
28
38
  img_size = img_obj.document.sizing
29
39
  { width: img_size.output_width, height: img_size.output_height }
30
40
  else
@@ -32,6 +42,7 @@ module Images
32
42
  _, img_size = ::File.open(path, 'rb') {|fd| build_image_object fd }
33
43
  { width: img_size.width, height: img_size.height }
34
44
  end
45
+ rescue
35
46
  end
36
47
  end
37
48
 
@@ -73,6 +73,14 @@ class RomanNumeral
73
73
  @integer_value
74
74
  end
75
75
 
76
+ def odd?
77
+ to_i.odd?
78
+ end
79
+
80
+ def even?
81
+ to_i.even?
82
+ end
83
+
76
84
  def next
77
85
  RomanNumeral.new @integer_value + 1, @letter_case
78
86
  end
@@ -86,6 +94,10 @@ class RomanNumeral
86
94
  RomanNumeral.new @integer_value - 1, @letter_case
87
95
  end
88
96
 
97
+ def empty?
98
+ false
99
+ end
100
+
89
101
  def self.int_to_roman value
90
102
  result = []
91
103
  BaseDigits.keys.reverse_each do |ival|