hexapdf 1.3.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/lib/hexapdf/cli/form.rb +9 -4
  4. data/lib/hexapdf/configuration.rb +10 -0
  5. data/lib/hexapdf/dictionary_fields.rb +1 -1
  6. data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -2
  7. data/lib/hexapdf/document/annotations.rb +47 -0
  8. data/lib/hexapdf/document/layout.rb +73 -33
  9. data/lib/hexapdf/document/metadata.rb +10 -3
  10. data/lib/hexapdf/document.rb +9 -0
  11. data/lib/hexapdf/encryption/standard_security_handler.rb +7 -2
  12. data/lib/hexapdf/layout/box.rb +5 -0
  13. data/lib/hexapdf/layout/container_box.rb +63 -28
  14. data/lib/hexapdf/layout/style.rb +28 -13
  15. data/lib/hexapdf/layout/table_box.rb +20 -2
  16. data/lib/hexapdf/type/annotations/appearance_generator.rb +94 -16
  17. data/lib/hexapdf/type/annotations/interior_color.rb +1 -1
  18. data/lib/hexapdf/type/annotations/line.rb +1 -157
  19. data/lib/hexapdf/type/annotations/line_ending_styling.rb +208 -0
  20. data/lib/hexapdf/type/annotations/markup_annotation.rb +0 -1
  21. data/lib/hexapdf/type/annotations/polygon.rb +64 -0
  22. data/lib/hexapdf/type/annotations/polygon_polyline.rb +109 -0
  23. data/lib/hexapdf/type/annotations/polyline.rb +64 -0
  24. data/lib/hexapdf/type/annotations.rb +4 -0
  25. data/lib/hexapdf/type/measure.rb +57 -0
  26. data/lib/hexapdf/type.rb +1 -0
  27. data/lib/hexapdf/version.rb +1 -1
  28. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +0 -1
  29. data/test/hexapdf/document/test_annotations.rb +20 -0
  30. data/test/hexapdf/document/test_layout.rb +16 -10
  31. data/test/hexapdf/document/test_metadata.rb +13 -1
  32. data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -1
  33. data/test/hexapdf/layout/test_box.rb +8 -0
  34. data/test/hexapdf/layout/test_container_box.rb +34 -6
  35. data/test/hexapdf/layout/test_page_style.rb +1 -1
  36. data/test/hexapdf/layout/test_style.rb +20 -1
  37. data/test/hexapdf/layout/test_table_box.rb +14 -1
  38. data/test/hexapdf/test_dictionary_fields.rb +1 -0
  39. data/test/hexapdf/type/annotations/test_appearance_generator.rb +126 -0
  40. data/test/hexapdf/type/annotations/test_line.rb +0 -25
  41. data/test/hexapdf/type/annotations/test_line_ending_styling.rb +42 -0
  42. data/test/hexapdf/type/annotations/test_polygon_polyline.rb +29 -0
  43. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0e469ae650b98b48c88b662dab27cb405b676c706639c8c1cf358d6aefc8f53
4
- data.tar.gz: d170526233e1e9aa37403c1bd8d2aa38697641c1b4fb4d4a5f6d8f02f299585b
3
+ metadata.gz: 82d0430964f9f4c6925af5bb076a2e641908c3a4cb6796acc64dbe1303bc3407
4
+ data.tar.gz: 689a86b637a86331ca203d7d7ac90a9fb3c50c275f07b21d8109013faa509f07
5
5
  SHA512:
6
- metadata.gz: fd18408ed2c2474e3395bf59b3a0281cb23005d16bf7d5b9d93f673dfa131fc215f342018d0cdce2de053db85c000a68c28a76ad980b0432b1a159bd51b1ed0c
7
- data.tar.gz: c58b13ed27c980bb9670b9521a12b1640fc3960aea1188f6135951cfd1503a28e7633cb3f6be8c63d478d64a88e0bbdc9b84bdbc982aa3ec000aeac1a8627597
6
+ metadata.gz: 4cfe8038379e5dc7f3bebeb38b44525676b934cb2b2355ac7575fa7a4466a6c4ec9ab9e0a80a184aebab32b2aa87e06dff735c10915abebe11541ada95d8129c
7
+ data.tar.gz: 62562b4bae557ad3dac03cb51913a8d1d6796d6cad9ce0626bc901cc15afeb301e42d061c345a1ebcb30e09a018dbe4b7c26fc4c567423c262f67607d04b95c9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,48 @@
1
+ ## 1.4.0 - 2025-08-03
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Type::Annotations::Polygon] for polygon annotations as well as
6
+ [HexaPDF::Document::Annotations#create_polygon]
7
+ * [HexaPDF::Type::Annotations::Polyline] for polyline annotations as well as
8
+ [HexaPDF::Document::Annotations#create_polyline]
9
+ * [HexaPDF::Layout::ContainerBox#splitable] for specifying whether the container
10
+ box may be split
11
+ * [HexaPDF::Layout::Style::Layers#layers] for retrieving the list of defined
12
+ layers
13
+ * [HexaPDF::Document::Layout#resolve_font] for resolving the font style property
14
+ * [HexaPDF::Type::Measure] for representing the measure dictionary
15
+ * [HexaPDF::Layout::Box::FitResult#failure!] for setting the result status to
16
+ failure
17
+
18
+ ### Changed
19
+
20
+ * **Breaking change**: Extracted `#line_ending_style` and associated data class
21
+ from [HexaPDF::Type::Annotations::Line] into
22
+ [HexaPDF::Type::Annotations::LineEndingStyling]
23
+ * [HexaPDF::Layout::TableBox] implementation to allow setting the minimum height
24
+ of a table cell
25
+ * [HexaPDF::Layout::Style::Quad#set] to allow setting a subset of values using a
26
+ hash
27
+ * CLI command `hp form` to show the names of radio button widgets
28
+ * CLI command `hp form` to show position and size of widgets in easier to
29
+ understand form
30
+ * Default signing handler to not set /DigestMethod entry on signature reference
31
+ dictionary anymore
32
+
33
+ ### Fixed
34
+
35
+ * Parsing and writing the /ModDate and /CreationDate trailer info fields in case
36
+ of string values when using the XMP metadata handler
37
+ * [HexaPDF::Layout::Style] to not accidentally set subscript or superscript
38
+ values
39
+ * [HexaPDF::DictionaryFields::DateConverter] to handle invalid dates with two
40
+ trailing apostrophes
41
+ * [HexaPDF::Document::Layout::CellArgumentCollector#retrieve_arguments_for] to
42
+ not change the stored data
43
+ * Encryption when using AES with 256bits and an owner password
44
+
45
+
1
46
  ## 1.3.0 - 2025-04-23
2
47
 
3
48
  ### Added
@@ -161,8 +161,8 @@ module HexaPDF
161
161
  (field.alternate_field_name ? " (#{field.alternate_field_name})" : '')
162
162
  concrete_field_type = field.concrete_field_type
163
163
  nice_field_type = concrete_field_type.to_s.split('_').map(&:capitalize).join(' ')
164
- size = "(#{widget[:Rect].width.round(3)}x#{widget[:Rect].height.round(3)})"
165
- position = "(#{widget[:Rect].left}, #{widget[:Rect].bottom})"
164
+ size = "#{widget[:Rect].width.round(3)}x#{widget[:Rect].height.round(3)}"
165
+ position = "x=#{widget[:Rect].left}, y=#{widget[:Rect].bottom}"
166
166
  field_value = if !field.field_value || concrete_field_type != :signature_field
167
167
  field.field_value.inspect
168
168
  else
@@ -172,10 +172,15 @@ module HexaPDF
172
172
  temp
173
173
  end
174
174
 
175
+ if concrete_field_type == :radio_button
176
+ rb_name = ((widget.appearance_dict&.normal_appearance&.value&.keys || []) - [:Off]).first
177
+ rb_name = " (#{rb_name.inspect})"
178
+ end
179
+
175
180
  flags = field_flags(field)
176
- puts " #{field_name}" << (flags.empty? ? '' : " (#{flags.join(', ')})")
181
+ puts " #{field_name}#{rb_name}" << (flags.empty? ? '' : " (#{flags.join(', ')})")
177
182
  if command_parser.verbosity_info?
178
- printf(" └─ %-22s | %-20s\n", nice_field_type, "#{size} #{position}")
183
+ printf(" └─ %-22s | %-20s\n", nice_field_type, "#{position}, #{size} ")
179
184
  end
180
185
  puts " └─ #{field_value}"
181
186
  if command_parser.verbosity_info?
@@ -374,6 +374,11 @@ module HexaPDF
374
374
  # The default implementation returns an object of class HexaPDF::Font::InvalidGlyph which, when
375
375
  # not removed before encoding, will raise a HexaPDF::MissingGlyphError.
376
376
  #
377
+ # Note: The 'font.on_invalid_glyph' configuration option does something similar but is used
378
+ # later and only by the layout engine. If this callback hook returns an invalid glyph instance,
379
+ # the 'font.on_invalid_glyph' callback hook is invoked when using the layout engine and it can
380
+ # return a substitute glyph in any font.
381
+ #
377
382
  # If a replacement glyph should be displayed instead of an error, the following provides a good
378
383
  # starting implementation:
379
384
  #
@@ -748,6 +753,7 @@ module HexaPDF
748
753
  Namespace: 'HexaPDF::Type::Namespace',
749
754
  MCR: 'HexaPDF::Type::MarkedContentReference',
750
755
  OBJR: 'HexaPDF::Type::ObjectReference',
756
+ Measure: 'HexaPDF::Type::Measure',
751
757
  },
752
758
  'object.subtype_map' => {
753
759
  nil => {
@@ -769,6 +775,8 @@ module HexaPDF
769
775
  Line: 'HexaPDF::Type::Annotations::Line',
770
776
  Square: 'HexaPDF::Type::Annotations::Square',
771
777
  Circle: 'HexaPDF::Type::Annotations::Circle',
778
+ Polygon: 'HexaPDF::Type::Annotations::Polygon',
779
+ PolyLine: 'HexaPDF::Type::Annotations::Polyline',
772
780
  XML: 'HexaPDF::Type::Metadata',
773
781
  GTS_PDFX: 'HexaPDF::Type::OutputIntent',
774
782
  GTS_PDFA1: 'HexaPDF::Type::OutputIntent',
@@ -800,6 +808,8 @@ module HexaPDF
800
808
  Line: 'HexaPDF::Type::Annotations::Line',
801
809
  Square: 'HexaPDF::Type::Annotations::Square',
802
810
  Circle: 'HexaPDF::Type::Annotations::Circle',
811
+ Polygon: 'HexaPDF::Type::Annotations::Polygon',
812
+ PolyLine: 'HexaPDF::Type::Annotations::Polyline',
803
813
  },
804
814
  XXAcroFormField: {
805
815
  Tx: 'HexaPDF::Type::AcroForm::TextField',
@@ -313,7 +313,7 @@ module HexaPDF
313
313
  [String, Time, Date, DateTime]
314
314
  end
315
315
 
316
- DATE_RE = /\AD:(\d{4})(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([Z+-])?(?:(\d+)(?:'|'(\d+)'?|\z)?)?\z/n # :nodoc:
316
+ DATE_RE = /\AD:(\d{4})(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([Z+-])?(?:(\d+)(?:'|'(\d+)'?'?|\z)?)?\z/n # :nodoc:
317
317
 
318
318
  # Checks if the given object is a string and converts into a Time object if possible.
319
319
  # Otherwise returns +nil+.
@@ -297,8 +297,7 @@ module HexaPDF
297
297
  raise HexaPDF::Error, "Can set DocMDP access permissions only on first signature"
298
298
  end
299
299
  params = doc.add({Type: :TransformParams, V: :'1.2', P: doc_mdp_permissions})
300
- sigref = doc.add({Type: :SigRef, TransformMethod: :DocMDP, DigestMethod: :SHA256,
301
- TransformParams: params})
300
+ sigref = doc.add({Type: :SigRef, TransformMethod: :DocMDP, TransformParams: params})
302
301
  signature[:Reference] = [sigref]
303
302
  (doc.catalog[:Perms] ||= {})[:DocMDP] = signature
304
303
  end
@@ -158,6 +158,53 @@ module HexaPDF
158
158
  annot
159
159
  end
160
160
 
161
+ # :call-seq:
162
+ # annotations.create_polyline(page, *points) -> annotation
163
+ #
164
+ # Creates a polyline annotation for the given +points+ (alternating horizontal and vertical
165
+ # coordinates) on the given page and returns it.
166
+ #
167
+ # The polyline uses a black color and a width of 1pt. It can be further styled using the
168
+ # convenience methods on the returned annotation object.
169
+ #
170
+ # Example:
171
+ #
172
+ # #>pdf-small
173
+ # doc.annotations.create_polyline(doc.pages[0], 20, 20, 30, 70, 80, 60, 40, 30).
174
+ # border_style(color: "hp-blue", width: 2, style: [3, 1]).
175
+ # regenerate_appearance
176
+ #
177
+ # See: Type::Annotations::Polyline
178
+ def create_polyline(page, *points)
179
+ create_and_add_to_page(:PolyLine, page).
180
+ vertices(*points).
181
+ border_style(color: 0, width: 1)
182
+ end
183
+
184
+ # :call-seq:
185
+ # annotations.create_polygon(page, *points) -> annotation
186
+ #
187
+ # Creates a polygon annotation for the given +points+ (alternating horizontal and vertical
188
+ # coordinates) on the given page and returns it.
189
+ #
190
+ # The polygon uses a black color and a width of 1pt for the border and no interior color. It
191
+ # can be further styled using the convenience methods on the returned annotation object.
192
+ #
193
+ # Example:
194
+ #
195
+ # #>pdf-small
196
+ # doc.annotations.create_polygon(doc.pages[0], 20, 20, 30, 70, 80, 60, 40, 30).
197
+ # border_style(color: "hp-blue", width: 2, style: [3, 1]).
198
+ # interior_color("hp-orange").
199
+ # regenerate_appearance
200
+ #
201
+ # See: Type::Annotations::Polygon
202
+ def create_polygon(page, *points)
203
+ create_and_add_to_page(:Polygon, page).
204
+ vertices(*points).
205
+ border_style(color: 0, width: 1)
206
+ end
207
+
161
208
  private
162
209
 
163
210
  # Returns the root of the destinations name tree.
@@ -92,13 +92,23 @@ module HexaPDF
92
92
  #
93
93
  # style.font = ['Helvetica', variant: :bold]
94
94
  #
95
- # Helvetica in bold could also be set the conventional way:
95
+ # Helvetica in bold could also be set in the following ways:
96
96
  #
97
97
  # style.font = 'Helvetica bold'
98
+ # # or
99
+ # style.font_bold = true
100
+ # style.font = 'Helvetica'
101
+ #
102
+ # The font_bold and font_italic style properties are always taken into account. For example,
103
+ # if the font is set to 'Helvetica italic' and font_bold to +true+, the actual font would be
104
+ # the bold _and_ italic Helvetica font.
98
105
  #
99
106
  # However, using an array it is also possible to specify other options when setting a font,
100
107
  # like the :subset option.
101
108
  #
109
+ # * It is possible to resolve the font of a style object manually by using the #resolve_font
110
+ # method.
111
+ #
102
112
  class Layout
103
113
 
104
114
  # This class is used when a box can contain child boxes and the creation of such boxes should
@@ -231,6 +241,64 @@ module HexaPDF
231
241
  @styles.key?(name)
232
242
  end
233
243
 
244
+ FONT_BOLD_VARIANT_MAPPER = { #:nodoc:
245
+ nil => {true => :bold, false: :none},
246
+ none: {true => :bold, false: :none},
247
+ bold: {true => :bold, false: :none},
248
+ italic: {true => :bold_italic, false: :italic},
249
+ bold_italic: {true => :bold_italic, false: :italic},
250
+ }
251
+
252
+ FONT_ITALIC_VARIANT_MAPPER = { #:nodoc:
253
+ nil => {true => :italic, false: :none},
254
+ none: {true => :italic, false: :none},
255
+ italic: {true => :italic, false: :none},
256
+ bold: {true => :bold_italic, false: :bold},
257
+ bold_italic: {true => :bold_italic, false: :bold},
258
+ }
259
+
260
+ # Resolves the font object for the given +style+ and applies the result to it.
261
+ #
262
+ # The Layout::Style#font property is the only one without a default value but is needed for
263
+ # many operations. This method ensures that the +style+ has a valid font object for the font
264
+ # property by resolving the font name.
265
+ #
266
+ # The font object is resolved in the following way:
267
+ #
268
+ # * If the font property is not set, the font value of the :base style is used and if that is
269
+ # also not set, the 'font.default' configuration value is used.
270
+ #
271
+ # * Afterwards, if the font property is a valid font object, nothing needs to be done.
272
+ #
273
+ # * Otherwise, if the font property is a single font name or a [font name, options hash]
274
+ # array, it is resolved to a font object, also taking the font_bold and font_italic style
275
+ # properties into account.
276
+ #
277
+ # Example:
278
+ #
279
+ # style = layout.style(:header, font: 'Helvetica')
280
+ # style.font # => 'Helvetica'
281
+ # layout.resolve_font(style)
282
+ # style.font # => #<HexaPDF::Font::Type1Wrapper>
283
+ #
284
+ # See: The "Box Styles" section in Layout for more details.
285
+ def resolve_font(style)
286
+ unless style.font?
287
+ style.font(@styles[:base].font? && @styles[:base].font || @document.config['font.default'])
288
+ end
289
+ unless style.font.respond_to?(:pdf_object)
290
+ name, options = *style.font
291
+ options ||= {}
292
+ if style.font_bold?
293
+ options[:variant] = FONT_BOLD_VARIANT_MAPPER.dig(options[:variant], style.font_bold)
294
+ end
295
+ if style.font_italic?
296
+ options[:variant] = FONT_ITALIC_VARIANT_MAPPER.dig(options[:variant], style.font_italic)
297
+ end
298
+ style.font(@document.fonts.add(name, **options))
299
+ end
300
+ end
301
+
234
302
  # :call-seq:
235
303
  # layout.styles -> styles
236
304
  # layout.styles(**mapping) -> styles
@@ -548,9 +616,10 @@ module HexaPDF
548
616
  @argument_infos.each_with_object({}) do |arg_info, result|
549
617
  next unless arg_info.rows.include?(row) && arg_info.cols.include?(col)
550
618
  if arg_info.args[:cell]
551
- arg_info.args[:cell] = (result[:cell] || {}).merge(arg_info.args[:cell])
619
+ result.update(arg_info.args, cell: (result[:cell] || {}).merge(arg_info.args[:cell]))
620
+ else
621
+ result.update(arg_info.args)
552
622
  end
553
- result.update(arg_info.args)
554
623
  end
555
624
  end
556
625
 
@@ -682,22 +751,6 @@ module HexaPDF
682
751
  end
683
752
  end
684
753
 
685
- FONT_BOLD_VARIANT_MAPPER = { #:nodoc:
686
- nil => {true => :bold, false: :none},
687
- none: {true => :bold, false: :none},
688
- bold: {true => :bold, false: :none},
689
- italic: {true => :bold_italic, false: :italic},
690
- bold_italic: {true => :bold_italic, false: :italic},
691
- }
692
-
693
- FONT_ITALIC_VARIANT_MAPPER = { #:nodoc:
694
- nil => {true => :italic, false: :none},
695
- none: {true => :italic, false: :none},
696
- italic: {true => :italic, false: :none},
697
- bold: {true => :bold_italic, false: :bold},
698
- bold_italic: {true => :bold_italic, false: :bold},
699
- }
700
-
701
754
  # Retrieves the appropriate HexaPDF::Layout::Style object based on the +style+ and
702
755
  # +properties+ arguments.
703
756
  #
@@ -717,20 +770,7 @@ module HexaPDF
717
770
  end
718
771
  style = HexaPDF::Layout::Style.create(@styles[style] || style || @styles[:base])
719
772
  style = style.dup.update(**properties) unless properties.nil? || properties.empty?
720
- unless style.font?
721
- style.font(@styles[:base].font? && @styles[:base].font || @document.config['font.default'])
722
- end
723
- unless style.font.respond_to?(:pdf_object)
724
- name, options = *style.font
725
- options ||= {}
726
- if style.font_bold?
727
- options[:variant] = FONT_BOLD_VARIANT_MAPPER.dig(options[:variant], style.font_bold)
728
- end
729
- if style.font_italic?
730
- options[:variant] = FONT_ITALIC_VARIANT_MAPPER.dig(options[:variant], style.font_italic)
731
- end
732
- style.font(@document.fonts.add(name, **options))
733
- end
773
+ resolve_font(style)
734
774
  style
735
775
  end
736
776
 
@@ -430,8 +430,12 @@ module HexaPDF
430
430
  @metadata[ns_dc]['creator'] = info_dict[:Author] if info_dict.key?(:Author)
431
431
  @metadata[ns_dc]['description'] = info_dict[:Subject] if info_dict.key?(:Subject)
432
432
  @metadata[ns_xmp]['CreatorTool'] = info_dict[:Creator] if info_dict.key?(:Creator)
433
- @metadata[ns_xmp]['CreateDate'] = info_dict[:CreationDate] if info_dict.key?(:CreationDate)
434
- @metadata[ns_xmp]['ModifyDate'] = info_dict[:ModDate] if info_dict.key?(:ModDate)
433
+ if info_dict.key?(:CreationDate) && !info_dict[:CreationDate].kind_of?(String)
434
+ @metadata[ns_xmp]['CreateDate'] = info_dict[:CreationDate]
435
+ end
436
+ if info_dict.key?(:ModDate) && !info_dict[:ModDate].kind_of?(String)
437
+ @metadata[ns_xmp]['ModifyDate'] = info_dict[:ModDate] if info_dict.key?(:ModDate)
438
+ end
435
439
  @metadata[ns_pdf]['Keywords'] = info_dict[:Keywords] if info_dict.key?(:Keywords)
436
440
  @metadata[ns_pdf]['Producer'] = info_dict[:Producer] if info_dict.key?(:Producer)
437
441
  if info_dict.key?(:Trapped) && info_dict[:Trapped] != :Unknown
@@ -528,7 +532,10 @@ module HexaPDF
528
532
  # Formats the given date-time object (Time, Date, or DateTime) to be a valid XMP date-time
529
533
  # value.
530
534
  def xmp_date(date)
531
- date.strftime("%Y-%m-%dT%H:%M:%S%:z")
535
+ case date
536
+ when Time, Date, DateTime then date.strftime("%Y-%m-%dT%H:%M:%S%:z")
537
+ else ''
538
+ end
532
539
  end
533
540
 
534
541
  end
@@ -743,6 +743,15 @@ module HexaPDF
743
743
  # Before the document is written, it is validated using #validate and an error is raised if the
744
744
  # document is not valid. However, this step can be skipped if needed.
745
745
  #
746
+ # The method dispatches two messages:
747
+ #
748
+ # :complete_objects::
749
+ # This message is dispatched before anything is done and should be used to finalize objects.
750
+ #
751
+ # :before_write::
752
+ # This message is dispatched directly before the document gets serialized and allows, for
753
+ # example, overriding automatic HexaPDF changes (e.g. forcefully setting a document version).
754
+ #
746
755
  # Options:
747
756
  #
748
757
  # incremental::
@@ -325,8 +325,13 @@ module HexaPDF
325
325
  options.user_password = prepare_password(options.user_password)
326
326
  options.owner_password = prepare_password(options.owner_password)
327
327
 
328
- dict[:O] = compute_o_field(options.owner_password, options.user_password)
329
- dict[:U] = compute_u_field(options.user_password)
328
+ if dict[:R] <= 4
329
+ dict[:O] = compute_o_field(options.owner_password, options.user_password)
330
+ dict[:U] = compute_u_field(options.user_password)
331
+ else
332
+ dict[:U] = compute_u_field(options.user_password)
333
+ dict[:O] = compute_o_field(options.owner_password, options.user_password)
334
+ end
330
335
 
331
336
  if dict[:R] <= 4
332
337
  encryption_key = compute_user_encryption_key(options.user_password)
@@ -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