hexapdf 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/README.md +1 -1
  4. data/lib/hexapdf/cli/form.rb +9 -4
  5. data/lib/hexapdf/cli/inspect.rb +13 -4
  6. data/lib/hexapdf/composer.rb +14 -0
  7. data/lib/hexapdf/configuration.rb +15 -0
  8. data/lib/hexapdf/dictionary_fields.rb +1 -1
  9. data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -2
  10. data/lib/hexapdf/document/annotations.rb +107 -2
  11. data/lib/hexapdf/document/layout.rb +94 -15
  12. data/lib/hexapdf/document/metadata.rb +10 -3
  13. data/lib/hexapdf/document.rb +9 -0
  14. data/lib/hexapdf/encryption/standard_security_handler.rb +7 -2
  15. data/lib/hexapdf/error.rb +11 -3
  16. data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
  17. data/lib/hexapdf/layout/box.rb +5 -0
  18. data/lib/hexapdf/layout/container_box.rb +63 -28
  19. data/lib/hexapdf/layout/style.rb +129 -20
  20. data/lib/hexapdf/layout/table_box.rb +20 -2
  21. data/lib/hexapdf/object.rb +2 -2
  22. data/lib/hexapdf/pdf_array.rb +25 -3
  23. data/lib/hexapdf/tokenizer.rb +4 -1
  24. data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
  25. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  26. data/lib/hexapdf/type/acro_form/form.rb +7 -6
  27. data/lib/hexapdf/type/annotation.rb +12 -0
  28. data/lib/hexapdf/type/annotations/appearance_generator.rb +169 -16
  29. data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
  30. data/lib/hexapdf/type/annotations/circle.rb +65 -0
  31. data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
  32. data/lib/hexapdf/type/annotations/line.rb +5 -192
  33. data/lib/hexapdf/type/annotations/line_ending_styling.rb +208 -0
  34. data/lib/hexapdf/type/annotations/markup_annotation.rb +0 -1
  35. data/lib/hexapdf/type/annotations/polygon.rb +64 -0
  36. data/lib/hexapdf/type/annotations/polygon_polyline.rb +109 -0
  37. data/lib/hexapdf/type/annotations/polyline.rb +64 -0
  38. data/lib/hexapdf/type/annotations/square.rb +65 -0
  39. data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
  40. data/lib/hexapdf/type/annotations/widget.rb +50 -20
  41. data/lib/hexapdf/type/annotations.rb +9 -0
  42. data/lib/hexapdf/type/measure.rb +57 -0
  43. data/lib/hexapdf/type.rb +1 -0
  44. data/lib/hexapdf/version.rb +1 -1
  45. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +0 -1
  46. data/test/hexapdf/document/test_annotations.rb +42 -0
  47. data/test/hexapdf/document/test_layout.rb +38 -10
  48. data/test/hexapdf/document/test_metadata.rb +13 -1
  49. data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -1
  50. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  51. data/test/hexapdf/layout/test_box.rb +8 -0
  52. data/test/hexapdf/layout/test_container_box.rb +34 -6
  53. data/test/hexapdf/layout/test_page_style.rb +1 -1
  54. data/test/hexapdf/layout/test_style.rb +46 -2
  55. data/test/hexapdf/layout/test_table_box.rb +14 -1
  56. data/test/hexapdf/test_composer.rb +7 -0
  57. data/test/hexapdf/test_dictionary_fields.rb +1 -0
  58. data/test/hexapdf/test_object.rb +1 -1
  59. data/test/hexapdf/test_pdf_array.rb +36 -3
  60. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
  61. data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
  62. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  63. data/test/hexapdf/type/acro_form/test_form.rb +17 -1
  64. data/test/hexapdf/type/annotations/test_appearance_generator.rb +210 -0
  65. data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
  66. data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
  67. data/test/hexapdf/type/annotations/test_line.rb +0 -45
  68. data/test/hexapdf/type/annotations/test_line_ending_styling.rb +42 -0
  69. data/test/hexapdf/type/annotations/test_polygon_polyline.rb +29 -0
  70. data/test/hexapdf/type/annotations/test_widget.rb +35 -0
  71. metadata +16 -2
@@ -63,6 +63,16 @@ module HexaPDF
63
63
  def use_glyph(glyph_id)
64
64
  return @glyph_map[glyph_id] if @glyph_map.key?(glyph_id)
65
65
  @last_id += 1
66
+ # Handle codes for ASCII characters \r (13), (, ) (40, 41) and \ (92) specially so that
67
+ # they never appear in the output (PDF serialization would need to escape them)
68
+ if @last_id == 13 || @last_id == 40 || @last_id == 92
69
+ @glyph_map[:"s#{@last_id}"] = @last_id
70
+ if @last_id == 40
71
+ @last_id += 1
72
+ @glyph_map[:"s#{@last_id}"] = @last_id
73
+ end
74
+ @last_id += 1
75
+ end
66
76
  @glyph_map[glyph_id] = @last_id
67
77
  end
68
78
 
@@ -107,7 +117,7 @@ module HexaPDF
107
117
  locations = []
108
118
 
109
119
  @glyph_map.each_key do |old_gid|
110
- glyph = orig_glyf[old_gid]
120
+ glyph = orig_glyf[old_gid.kind_of?(Symbol) ? 0 : old_gid]
111
121
  locations << table.size
112
122
  data = glyph.raw_data
113
123
  if glyph.compound?
@@ -166,7 +176,10 @@ module HexaPDF
166
176
  # Adds the components of compound glyphs to the subset.
167
177
  def add_glyph_components
168
178
  glyf = @font[:glyf]
169
- @glyph_map.keys.each {|gid| glyf[gid].components&.each {|cgid| use_glyph(cgid) } }
179
+ @glyph_map.keys.each do |gid|
180
+ next if gid.kind_of?(Symbol)
181
+ glyf[gid].components&.each {|cgid| use_glyph(cgid) }
182
+ end
170
183
  end
171
184
 
172
185
  end
@@ -166,6 +166,11 @@ module HexaPDF
166
166
  @status == :overflow
167
167
  end
168
168
 
169
+ # Sets the result status to failure.
170
+ def failure!
171
+ @status = :failure
172
+ end
173
+
169
174
  # Returns +true+ if fitting was a failure.
170
175
  def failure?
171
176
  @status == :failure
@@ -44,21 +44,22 @@ module HexaPDF
44
44
  # under the :container name.
45
45
  #
46
46
  # The box does not support the value :flow for the style property position, so the child boxes
47
- # are laid out in the current region only. Since the boxes should be laid out together, if any
48
- # box doesn't fit, the whole container doesn't fit. Splitting the container is also not possible
49
- # for the same reason.
47
+ # are laid out in the current region only.
50
48
  #
51
- # By default the child boxes are laid out from top to bottom by default. By appropriately
52
- # setting the style properties 'mask_mode', 'align' and 'valign', it is possible to lay out the
53
- # children bottom to top, left to right, or right to left:
49
+ # If #splitable is +false+ (the default) and if any box doesn't fit, the whole container doesn't
50
+ # fit.
51
+ #
52
+ # By default the child boxes are laid out from top to bottom. By appropriately setting the style
53
+ # properties 'mask_mode', 'align' and 'valign', it is possible to lay out the children bottom to
54
+ # top, left to right, or right to left:
54
55
  #
55
56
  # * The standard top-to-bottom layout:
56
57
  #
57
58
  # #>pdf-composer100
58
59
  # composer.container do |container|
59
- # container.box(:base, height: 20, style: {background_color: "hp-blue-dark"})
60
- # container.box(:base, height: 20, style: {background_color: "hp-blue"})
61
- # container.box(:base, height: 20, style: {background_color: "hp-blue-light"})
60
+ # container.box(height: 20, style: {background_color: "hp-blue-dark"})
61
+ # container.box(height: 20, style: {background_color: "hp-blue"})
62
+ # container.box(height: 20, style: {background_color: "hp-blue-light"})
62
63
  # end
63
64
  #
64
65
  # * The bottom-to-top layout (using valign = :bottom to fill up from the bottom and mask_mode =
@@ -66,12 +67,12 @@ module HexaPDF
66
67
  #
67
68
  # #>pdf-composer100
68
69
  # composer.container do |container|
69
- # container.box(:base, height: 20, style: {background_color: "hp-blue-dark",
70
- # mask_mode: :fill_horizontal, valign: :bottom})
71
- # container.box(:base, height: 20, style: {background_color: "hp-blue",
72
- # mask_mode: :fill_horizontal, valign: :bottom})
73
- # container.box(:base, height: 20, style: {background_color: "hp-blue-light",
74
- # mask_mode: :fill_horizontal, valign: :bottom})
70
+ # container.box(height: 20, style: {background_color: "hp-blue-dark",
71
+ # mask_mode: :fill_horizontal, valign: :bottom})
72
+ # container.box(height: 20, style: {background_color: "hp-blue",
73
+ # mask_mode: :fill_horizontal, valign: :bottom})
74
+ # container.box(height: 20, style: {background_color: "hp-blue-light",
75
+ # mask_mode: :fill_horizontal, valign: :bottom})
75
76
  # end
76
77
  #
77
78
  # * The left-to-right layout (using mask_mode = :fill_vertical to fill the area to the top and
@@ -79,12 +80,12 @@ module HexaPDF
79
80
  #
80
81
  # #>pdf-composer100
81
82
  # composer.container do |container|
82
- # container.box(:base, width: 20, style: {background_color: "hp-blue-dark",
83
- # mask_mode: :fill_vertical})
84
- # container.box(:base, width: 20, style: {background_color: "hp-blue",
85
- # mask_mode: :fill_vertical})
86
- # container.box(:base, width: 20, style: {background_color: "hp-blue-light",
87
- # mask_mode: :fill_vertical})
83
+ # container.box(width: 20, style: {background_color: "hp-blue-dark",
84
+ # mask_mode: :fill_vertical})
85
+ # container.box(width: 20, style: {background_color: "hp-blue",
86
+ # mask_mode: :fill_vertical})
87
+ # container.box(width: 20, style: {background_color: "hp-blue-light",
88
+ # mask_mode: :fill_vertical})
88
89
  # end
89
90
  #
90
91
  # * The right-to-left layout (using align = :right to fill up from the right and mask_mode =
@@ -92,18 +93,42 @@ module HexaPDF
92
93
  #
93
94
  # #>pdf-composer100
94
95
  # composer.container do |container|
95
- # container.box(:base, width: 20, style: {background_color: "hp-blue-dark",
96
- # mask_mode: :fill_vertical, align: :right})
97
- # container.box(:base, width: 20, style: {background_color: "hp-blue",
98
- # mask_mode: :fill_vertical, align: :right})
99
- # container.box(:base, width: 20, style: {background_color: "hp-blue-light",
100
- # mask_mode: :fill_vertical, align: :right})
96
+ # container.box(width: 20, style: {background_color: "hp-blue-dark",
97
+ # mask_mode: :fill_vertical, align: :right})
98
+ # container.box(width: 20, style: {background_color: "hp-blue",
99
+ # mask_mode: :fill_vertical, align: :right})
100
+ # container.box(width: 20, style: {background_color: "hp-blue-light",
101
+ # mask_mode: :fill_vertical, align: :right})
101
102
  # end
102
103
  class ContainerBox < Box
103
104
 
104
105
  # The child boxes of this ContainerBox. They need to be finalized before #fit is called.
105
106
  attr_reader :children
106
107
 
108
+ # Specifies whether the container box allows splitting its content.
109
+ #
110
+ # If splitting is not allowed (the default), all child boxes must fit together into one
111
+ # region.
112
+ #
113
+ # Examples:
114
+ #
115
+ # # Fails with an error because the content of the container box is too big
116
+ # composer.column do |col|
117
+ # col.container do |container|
118
+ # container.lorem_ipsum
119
+ # end
120
+ # end
121
+ #
122
+ # ---
123
+ #
124
+ # #>pdf-composer
125
+ # composer.column do |col|
126
+ # col.container(splitable: true) do |container|
127
+ # container.lorem_ipsum
128
+ # end
129
+ # end
130
+ attr_reader :splitable
131
+
107
132
  # Creates a new container box, optionally accepting an array of child boxes.
108
133
  #
109
134
  # Example:
@@ -117,9 +142,10 @@ module HexaPDF
117
142
  # container.text("here", mask_mode: :fill_vertical, valign: :bottom)
118
143
  # end
119
144
  # composer.text("Another paragraph")
120
- def initialize(children: [], **kwargs)
145
+ def initialize(children: [], splitable: false, **kwargs)
121
146
  super(**kwargs)
122
147
  @children = children
148
+ @splitable = splitable
123
149
  end
124
150
 
125
151
  # Returns +true+ if no box was fitted into the container.
@@ -144,9 +170,18 @@ module HexaPDF
144
170
  end
145
171
  update_content_height { @box_fitter.content_heights.max }
146
172
  fit_result.success!
173
+ elsif !@box_fitter.fit_results.empty? && @splitable
174
+ fit_result.overflow!
147
175
  end
148
176
  end
149
177
 
178
+ # Splits the content of the container box. This method is called from Box#split.
179
+ def split_content
180
+ box = create_split_box
181
+ box.instance_variable_set(:@children, @box_fitter.remaining_boxes)
182
+ [self, box]
183
+ end
184
+
150
185
  # Draws the children onto the canvas at position [x, y].
151
186
  def draw_content(canvas, x, y)
152
187
  dx = x - @fit_x
@@ -150,14 +150,15 @@ module HexaPDF
150
150
  end
151
151
 
152
152
  # :call-seq:
153
- # quad.set(value)
154
- # quad.set(array)
155
- # quad.set(quad)
153
+ # quad.set(value) -> quad
154
+ # quad.set(array) -> quad
155
+ # quad.set(hash) -> quad
156
+ # quad.set(quad) -> quad
156
157
  #
157
- # Sets all values of the quad.
158
+ # Sets all values of the quad and returns it.
158
159
  #
159
- # * If a single value is provided that is neither a Quad nor an array, it is handled as if
160
- # an array with one value was given.
160
+ # * If a single value is provided that is neither a Quad nor an array nor a hash, it is
161
+ # handled as if an array with one value was given.
161
162
  #
162
163
  # * If a Quad is provided, its values are used.
163
164
  #
@@ -170,6 +171,9 @@ module HexaPDF
170
171
  # third value.
171
172
  # * Four or more values: Top is set to the first, right to the second, bottom to the third
172
173
  # and left to the fourth value.
174
+ #
175
+ # * If a hash is provided, the keys +:top+, +:bottom+, +:left+ and +:right+ are used to set
176
+ # the respective value. All unspecified keys that have not been set before are set to 0.
173
177
  def set(obj)
174
178
  case obj
175
179
  when Quad
@@ -182,9 +186,15 @@ module HexaPDF
182
186
  @bottom = obj[2] || obj[0]
183
187
  @left = obj[3] || obj[1] || obj[0]
184
188
  @right = obj[1] || obj[0]
189
+ when Hash
190
+ @top = obj[:top] || @top || 0
191
+ @bottom = obj[:bottom] || @bottom || 0
192
+ @left = obj[:left] || @left || 0
193
+ @right = obj[:right] || @right || 0
185
194
  else
186
195
  @top = @bottom = @left = @right = obj
187
196
  end
197
+ self
188
198
  end
189
199
 
190
200
  # Returns +true+ if the quad effectively contains only one value.
@@ -398,6 +408,9 @@ module HexaPDF
398
408
  # bottom-left corner of the box during the drawing operations.
399
409
  class Layers
400
410
 
411
+ # The array holding all raw layer definitions.
412
+ attr_reader :layers
413
+
401
414
  # Creates a new Layers object popuplated with the given +layers+.
402
415
  def initialize(layers = nil)
403
416
  @layers = []
@@ -603,11 +616,36 @@ module HexaPDF
603
616
  # style.update(**properties) -> style
604
617
  #
605
618
  # Updates the style's properties using the key-value pairs specified by the +properties+ hash.
619
+ #
620
+ # Also see: #merge
606
621
  def update(**properties)
607
622
  properties.each {|key, value| send(key, value) }
608
623
  self
609
624
  end
610
625
 
626
+ # Yields all set properties.
627
+ def each_property # :yield: property, value
628
+ return to_enum(__method__) unless block_given?
629
+ instance_variables.each do |iv|
630
+ (val = PROPERTIES[iv]) && yield(val, instance_variable_get(iv))
631
+ end
632
+ end
633
+
634
+ # :call-seq:
635
+ # style.merge(other_style) -> style
636
+ #
637
+ # Merges the set properties of the +other_style+ object into this one.
638
+ #
639
+ # Note that merging is done on a per-property basis. So if a complex property is set on
640
+ # +other_style+ and also on +self+, the +other_style+ value completely overwrites the one from
641
+ # +self+.
642
+ #
643
+ # Also see: #update
644
+ def merge(other)
645
+ other.each_property {|property, value| send(property, value) }
646
+ self
647
+ end
648
+
611
649
  ##
612
650
  # :method: font
613
651
  # :call-seq:
@@ -615,8 +653,9 @@ module HexaPDF
615
653
  #
616
654
  # The font to be used, must be set to a valid font wrapper object before it can be used.
617
655
  #
618
- # HexaPDF::Composer handles this property specially in that it resolves a set string or array
619
- # to a font wrapper object before doing anything else with the style object.
656
+ # HexaPDF::Document::Layout handles this property - together with #font_bold and #font_italic
657
+ # - specially in that it resolves a set string or array to a font wrapper object before doing
658
+ # anything else with the style object.
620
659
  #
621
660
  # This is the only style property without a default value!
622
661
  #
@@ -633,6 +672,48 @@ module HexaPDF
633
672
  # composer.text("Courier Bold", font: "Courier bold")
634
673
  # composer.text("Courier Bold also", font: ["Courier", variant: :bold])
635
674
 
675
+ ##
676
+ # :method: font_bold
677
+ # :call-seq:
678
+ # font_bold(bold = false)
679
+ #
680
+ # Specifies whether the bold variant of the font is used.
681
+ #
682
+ # Note that this property only has affect if #font is not already set to a font wrapper
683
+ # object and if it is set explicitly (i.e. #font_bold? returns +true+).
684
+ #
685
+ # See #font, #font_italic
686
+ #
687
+ # Examples:
688
+ #
689
+ # #>pdf-composer100
690
+ # composer.text("Helvetica bold", font: "Helvetica", font_bold: true)
691
+ #
692
+ # helvetica_bold = composer.document.fonts.add("Helvetica", variant: :bold)
693
+ # composer.text("Helvetica bold", font: helvetica_bold, font_bold: false)
694
+ # composer.text("Helvetica", font: ["Helvetica", {variant: :bold}], font_bold: false)
695
+
696
+ ##
697
+ # :method: font_italic
698
+ # :call-seq:
699
+ # font_italic(bold = false)
700
+ #
701
+ # Specifies whether the italic variant of the font is used.
702
+ #
703
+ # Note that this property only has affect if #font is not already set to a font wrapper
704
+ # object and if it is set explicitly (i.e. #font_italic? returns +true+).
705
+ #
706
+ # See #font, #font_bold.
707
+ #
708
+ # Examples:
709
+ #
710
+ # #>pdf-composer100
711
+ # composer.text("Helvetica italic", font: "Helvetica", font_italic: true)
712
+ #
713
+ # helvetica_bold = composer.document.fonts.add("Helvetica", variant: :italic)
714
+ # composer.text("Helvetica italic", font: helvetica_bold, font_italic: false)
715
+ # composer.text("Helvetica", font: ["Helvetica", {variant: :italic}], font_italic: false)
716
+
636
717
  ##
637
718
  # :method: font_size
638
719
  # :call-seq:
@@ -1021,7 +1102,7 @@ module HexaPDF
1021
1102
  #
1022
1103
  # This method can set the line spacing in two ways:
1023
1104
  #
1024
- # * Using two positional arguments +type+ and +value+.
1105
+ # * Using the positional, mandatory argument +type+ and the optional +value+.
1025
1106
  # * Or a hash with the keys +type+ and +value+.
1026
1107
  #
1027
1108
  # Note that the last line has no additional spacing after it by default. Set #last_line_gap
@@ -1422,8 +1503,33 @@ module HexaPDF
1422
1503
  # composer.text("This is some longer text that does not appear in two lines.",
1423
1504
  # height: 15, overflow: :truncate)
1424
1505
 
1425
- [
1506
+ ##
1507
+ # :method: box_options
1508
+ # :call-seq:
1509
+ # box_options(**options)
1510
+ #
1511
+ # Contains initialization arguments for the box instance that is created with this
1512
+ # style. Together with the other style properties this allows the complete specification of a
1513
+ # box instance just via a Style instance.
1514
+ #
1515
+ # Note that this property is only used by the HexaPDF::Document::Layout methods when a box
1516
+ # instance is created. If a box instance is created directly, this property has no effect.
1517
+ #
1518
+ # Examples:
1519
+ #
1520
+ # #>pdf-composer100
1521
+ # composer.style(:my_list, box_options: {marker_type: :decimal, item_spacing: 15})
1522
+ # composer.list(style: :my_list) do |list|
1523
+ # list.text("This is some text.")
1524
+ # list.text("This is some other text.")
1525
+ # end
1526
+
1527
+
1528
+ # :nodoc:
1529
+ PROPERTIES = [
1426
1530
  [:font, "raise HexaPDF::Error, 'No font set'"],
1531
+ [:font_bold, false],
1532
+ [:font_italic, false],
1427
1533
  [:font_size, 10],
1428
1534
  [:line_height, nil],
1429
1535
  [:character_spacing, 0],
@@ -1434,10 +1540,10 @@ module HexaPDF
1434
1540
  [:text_rendering_mode, "Content::TextRenderingMode::FILL",
1435
1541
  {setter: "Content::TextRenderingMode.normalize(value)"}],
1436
1542
  [:subscript, false,
1437
- {setter: "value; superscript(false) if superscript",
1543
+ {setter: "value; superscript(false) if value && superscript? && superscript",
1438
1544
  valid_values: [true, false]}],
1439
1545
  [:superscript, false,
1440
- {setter: "value; subscript(false) if subscript",
1546
+ {setter: "value; subscript(false) if value && subscript? && subscript",
1441
1547
  valid_values: [true, false]}],
1442
1548
  [:underline, false, {valid_values: [true, false]}],
1443
1549
  [:strikeout, false, {valid_values: [true, false]}],
@@ -1457,15 +1563,17 @@ module HexaPDF
1457
1563
  [:text_valign, :top, {valid_values: [:top, :center, :bottom]}],
1458
1564
  [:text_indent, 0],
1459
1565
  [:line_spacing, "LineSpacing.new(type: :single)",
1460
- {setter: "LineSpacing.new(**(value.kind_of?(Symbol) || value.kind_of?(Numeric) ? " \
1461
- "{type: value, value: extra_arg} : value))",
1566
+ {setter: "LineSpacing.new(**(value.kind_of?(Symbol) || value.kind_of?(Numeric) || " \
1567
+ "value.kind_of?(LineSpacing) ? {type: value, value: extra_arg} : value))",
1462
1568
  extra_args: ", extra_arg = nil"}],
1463
1569
  [:last_line_gap, false, {valid_values: [true, false]}],
1464
1570
  [:fill_horizontal, nil],
1465
1571
  [:background_color, nil],
1466
1572
  [:background_alpha, 1],
1467
- [:padding, "Quad.new(0)", {setter: "Quad.new(value)"}],
1468
- [:margin, "Quad.new(0)", {setter: "Quad.new(value)"}],
1573
+ [:padding, "Quad.new(0)",
1574
+ {setter: "value.kind_of?(Hash) && @name ? @name.set(value) : Quad.new(value)"}],
1575
+ [:margin, "Quad.new(0)",
1576
+ {setter: "value.kind_of?(Hash) && @name ? @name.set(value) : Quad.new(value)"}],
1469
1577
  [:border, "Border.new", {setter: "Border.new(**value)"}],
1470
1578
  [:overlays, "Layers.new", {setter: "Layers.new(value)"}],
1471
1579
  [:underlays, "Layers.new", {setter: "Layers.new(value)"}],
@@ -1475,6 +1583,7 @@ module HexaPDF
1475
1583
  [:mask_mode, :default, {valid_values: [:default, :none, :box, :fill_horizontal,
1476
1584
  :fill_frame_horizontal, :fill_vertical, :fill]}],
1477
1585
  [:overflow, :error],
1586
+ [:box_options, {}],
1478
1587
  ].each do |name, default, options = {}|
1479
1588
  default = default.inspect unless default.kind_of?(String)
1480
1589
  setter = options.delete(:setter) || "value"
@@ -1500,7 +1609,7 @@ module HexaPDF
1500
1609
  end
1501
1610
  EOF
1502
1611
  alias_method("#{name}=", name)
1503
- end
1612
+ end.each_with_object({}) {|arr, hash| hash[:"@#{arr.first}"] = arr.first }
1504
1613
 
1505
1614
  ##
1506
1615
  # :method: text_segmentation_algorithm
@@ -1547,9 +1656,9 @@ module HexaPDF
1547
1656
 
1548
1657
  # The calculated text rise, taking superscript and subscript into account.
1549
1658
  def calculated_text_rise
1550
- if superscript
1659
+ if superscript? && superscript
1551
1660
  text_rise + font_size * 0.33
1552
- elsif subscript
1661
+ elsif subscript? && subscript
1553
1662
  text_rise - font_size * 0.20
1554
1663
  else
1555
1664
  text_rise
@@ -1558,7 +1667,7 @@ module HexaPDF
1558
1667
 
1559
1668
  # The calculated font size, taking superscript and subscript into account.
1560
1669
  def calculated_font_size
1561
- (superscript || subscript ? 0.583 : 1) * font_size
1670
+ ((superscript? && superscript) || (subscript? && subscript) ? 0.583 : 1) * font_size
1562
1671
  end
1563
1672
 
1564
1673
  # Returns the correct offset from the baseline for the underline.
@@ -126,6 +126,15 @@ module HexaPDF
126
126
  # [layout.text('E'), layout.text('F')]]
127
127
  # composer.column(height: 90) {|col| col.table(cells, header: header, footer: footer) }
128
128
  #
129
+ # While the width of a cell is determined by the #column_widths array, the height is
130
+ # automatically determined during fitting of the content. However, it is also possible to use a
131
+ # fixed height (only if the actual content is smaller or equal than it):
132
+ #
133
+ # #>pdf-composer
134
+ # cells = [[{content: layout.text('A'), height: 5}, layout.text('B')],
135
+ # [{content: layout.text('C'), height: 40}, layout.text('D')]]
136
+ # composer.table(cells)
137
+ #
129
138
  # The cells can be styled using a callable object for more complex styling:
130
139
  #
131
140
  # #>pdf-composer
@@ -191,13 +200,14 @@ module HexaPDF
191
200
  attr_accessor :children
192
201
 
193
202
  # Creates a new Cell instance.
194
- def initialize(row:, column:, children: nil, row_span: nil, col_span: nil, **kwargs)
203
+ def initialize(row:, column:, children: nil, min_height: nil, row_span: nil, col_span: nil, **kwargs)
195
204
  super(**kwargs, width: 0, height: 0)
196
205
  @children = children
197
206
  @row = row
198
207
  @column = column
199
208
  @row_span = row_span || 1
200
209
  @col_span = col_span || 1
210
+ @min_height = min_height
201
211
  style.border.width.set(1) unless style.border?
202
212
  style.border.draw_on_bounds = true
203
213
  style.padding.set(5) unless style.padding?
@@ -257,6 +267,11 @@ module HexaPDF
257
267
  @fit_results = []
258
268
  fit_result.success!
259
269
  end
270
+
271
+ if @min_height && @height < @min_height
272
+ @height = @preferred_height = @min_height
273
+ fit_result.failure! if available_height < @height
274
+ end
260
275
  end
261
276
 
262
277
  # Draws the content of the cell.
@@ -298,6 +313,8 @@ module HexaPDF
298
313
  #
299
314
  # +:col_span+:: An integer specifying the number of columsn this cell should span.
300
315
  #
316
+ # +:min_height+:: A number specifying the minimum height of the table cell.
317
+ #
301
318
  # +:properties+:: A hash of properties (see Box#properties) to be set on the cell itself.
302
319
  #
303
320
  # All other key-value pairs are taken to be cell styling information (like
@@ -513,11 +530,12 @@ module HexaPDF
513
530
  children = content.delete(:content)
514
531
  row_span = content.delete(:row_span)
515
532
  col_span = content.delete(:col_span)
533
+ min_height = content.delete(:min_height)
516
534
  properties = content.delete(:properties)
517
535
  style = content
518
536
  end
519
537
  cell = Cell.new(children: children, row: row_index, column: col_index,
520
- row_span: row_span, col_span: col_span)
538
+ row_span: row_span, col_span: col_span, min_height: min_height)
521
539
  cell_style_block&.call(cell)
522
540
  cell.style.update(**style) if style
523
541
  cell.properties.update(properties) if properties
@@ -305,8 +305,8 @@ module HexaPDF
305
305
  result
306
306
  rescue HexaPDF::Error
307
307
  raise
308
- rescue StandardError
309
- yield("Error: Unexpected value encountered", false, self) if block_given?
308
+ rescue StandardError => e
309
+ yield("Unexpected error encountered: #{e.message}", false, self) if block_given?
310
310
  false
311
311
  end
312
312
 
@@ -143,10 +143,32 @@ module HexaPDF
143
143
  # array.reject! {|item| block } -> array or nil
144
144
  # array.reject! -> Enumerator
145
145
  #
146
- # Deletes all elements from the array for which the block returns +true+. If no changes were
147
- # done, returns +nil+.
146
+ # Deletes all elements from the array for which the block returns +true+ and returns +self+. If
147
+ # no changes were done, returns +nil+.
148
148
  def reject!
149
- value.reject! {|item| yield(process_entry(item)) }
149
+ return to_enum(__method__) unless block_given?
150
+ value.reject! {|item| yield(process_entry(item)) } && self
151
+ end
152
+
153
+ # :call-seq:
154
+ # array.map! {|item| block } -> array
155
+ # array.map! -> Enumerator
156
+ #
157
+ # Maps all elements from the array in-place to the respective return value of the block+ and
158
+ # returns +self+.
159
+ def map!
160
+ return to_enum(__method__) unless block_given?
161
+ value.map! {|item| yield(process_entry(item)) }
162
+ self
163
+ end
164
+
165
+ # :call-seq:
166
+ # array.compact! -> array or nil
167
+ #
168
+ # Removes all +nil+ elements from the array. Returns +self+ if any elements were removed, +nil+
169
+ # otherwise.
170
+ def compact!
171
+ value.compact! && self
150
172
  end
151
173
 
152
174
  # :call-seq:
@@ -278,6 +278,9 @@ module HexaPDF
278
278
 
279
279
  REFERENCE_RE = /[#{WHITESPACE}]+([+]?\d+)[#{WHITESPACE}]+R#{WHITESPACE_OR_DELIMITER_RE}/ # :nodoc:
280
280
 
281
+ WHITESPACE_OR_DELIMITER_LUT = [] # :nodoc:
282
+ (WHITESPACE + DELIMITER).each_byte {|x| WHITESPACE_OR_DELIMITER_LUT[x] = true }
283
+
281
284
  # Parses the number (integer or real) at the current position.
282
285
  #
283
286
  # See: PDF2.0 s7.3.3
@@ -285,7 +288,7 @@ module HexaPDF
285
288
  prepare_string_scanner(40)
286
289
  pos = self.pos
287
290
  if (tmp = @ss.scan_integer)
288
- if @ss.eos? || @ss.match?(WHITESPACE_OR_DELIMITER_RE)
291
+ if @ss.eos? || WHITESPACE_OR_DELIMITER_LUT[@ss.peek_byte]
289
292
  # Handle object references, see PDF2.0 s7.3.10
290
293
  prepare_string_scanner(10)
291
294
  if @ss.scan(REFERENCE_RE)