hexapdf 0.42.0 → 0.44.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 +46 -0
  3. data/Rakefile +1 -1
  4. data/examples/030-pdfa.rb +1 -0
  5. data/lib/hexapdf/composer.rb +1 -0
  6. data/lib/hexapdf/dictionary.rb +3 -3
  7. data/lib/hexapdf/document/files.rb +7 -2
  8. data/lib/hexapdf/document/metadata.rb +12 -1
  9. data/lib/hexapdf/document.rb +14 -1
  10. data/lib/hexapdf/encryption.rb +17 -0
  11. data/lib/hexapdf/layout/box.rb +161 -61
  12. data/lib/hexapdf/layout/box_fitter.rb +4 -3
  13. data/lib/hexapdf/layout/column_box.rb +23 -25
  14. data/lib/hexapdf/layout/container_box.rb +3 -3
  15. data/lib/hexapdf/layout/frame.rb +13 -95
  16. data/lib/hexapdf/layout/image_box.rb +4 -4
  17. data/lib/hexapdf/layout/line.rb +4 -0
  18. data/lib/hexapdf/layout/list_box.rb +12 -20
  19. data/lib/hexapdf/layout/style.rb +5 -1
  20. data/lib/hexapdf/layout/table_box.rb +48 -55
  21. data/lib/hexapdf/layout/text_box.rb +38 -39
  22. data/lib/hexapdf/parser.rb +23 -17
  23. data/lib/hexapdf/type/acro_form/form.rb +78 -27
  24. data/lib/hexapdf/type/file_specification.rb +9 -5
  25. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  26. data/lib/hexapdf/version.rb +1 -1
  27. data/test/hexapdf/document/test_files.rb +5 -0
  28. data/test/hexapdf/document/test_metadata.rb +21 -0
  29. data/test/hexapdf/layout/test_box.rb +82 -37
  30. data/test/hexapdf/layout/test_box_fitter.rb +10 -3
  31. data/test/hexapdf/layout/test_column_box.rb +7 -13
  32. data/test/hexapdf/layout/test_container_box.rb +1 -1
  33. data/test/hexapdf/layout/test_frame.rb +0 -48
  34. data/test/hexapdf/layout/test_image_box.rb +14 -6
  35. data/test/hexapdf/layout/test_list_box.rb +25 -26
  36. data/test/hexapdf/layout/test_table_box.rb +39 -53
  37. data/test/hexapdf/layout/test_text_box.rb +65 -67
  38. data/test/hexapdf/test_composer.rb +6 -0
  39. data/test/hexapdf/test_dictionary.rb +6 -4
  40. data/test/hexapdf/test_parser.rb +20 -0
  41. data/test/hexapdf/type/acro_form/test_form.rb +63 -2
  42. data/test/hexapdf/type/test_file_specification.rb +2 -1
  43. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd69db2c04dc802784438f9038c9a6ab32dac0806edca43b6c59406bba9d7ea6
4
- data.tar.gz: a51a510a6a98b547b2b11fb07804ca9039a666adf95b52f65db9b070c3ffe9c3
3
+ metadata.gz: 3b30b54b90222fbcc5469b23153efc4b15083797f527af6c5b34b3f6e161832d
4
+ data.tar.gz: 969c67ab85591e3b7ccad17023bd3bc820ea5effa4c01e53d254915ab1626d71
5
5
  SHA512:
6
- metadata.gz: 714cba0b758960066498413b545dc7cf2806e1e483f99b017958450bfe29fd12aac1eef40f670d189f7cd7dab5784dd83e7b6f95533b97cf71c883dd9f307485
7
- data.tar.gz: c93ceed7393b57fa5c6ca83141a8307b6015a1fa6907886b52c57bf72384d4d23ac8356adede0dabc35578cad5e1cef074f75c94e01e5c9532cadd0a688bbd57
6
+ metadata.gz: b60934dd6534ccd019953b278fa188b77996634b3e3878332302b9a2acccfd13b493b57432cff2113b7cc7727f028f24301c2d050f9b908c1b43cf66fbdeebfd
7
+ data.tar.gz: fdd792109306bc7cf0e30bb2da770e58e81e333843e3c785a9a31873a296b7d027121b467b54a80405332735e99bdaf6b0d167c9eaf08dbc04a26922b890bb51
data/CHANGELOG.md CHANGED
@@ -1,3 +1,49 @@
1
+ ## 0.44.0 - 2024-06-05
2
+
3
+ ### Added
4
+
5
+ * Support for specifying the MIME type when embedding files
6
+ * Support for adding custom XMP metadata
7
+
8
+ ### Changed
9
+
10
+ * **Breaking change**: Refactored the box implementation of the document layout
11
+ system
12
+
13
+ ### Fixed
14
+
15
+ * Parsing of invalid files with garbage bytes at the end
16
+
17
+
18
+ ## 0.43.0 - 2024-05-26
19
+
20
+ ### Added
21
+
22
+ * [HexaPDF::Type::AcroForm::Form#create_namespace_field] for creating a pure
23
+ namespace field
24
+ * [HexaPDF::Type::AcroForm::Form#delete_field] for deleting fields
25
+
26
+ ### Changed
27
+
28
+ * Minimum Ruby version to be 3.0
29
+ * **Breaking change**: Renamed `HexaPDF::Layout::BoxFitter#fit_successful?` to
30
+ [HexaPDF::Layout::BoxFitter#success?]
31
+ * **Breaking Change**: Removed HexaPDF::Dictionary#to_h
32
+ * Form field creation methods of [HexaPDF::Type::AcroForm::Form] to
33
+ automatically create parent fields as namespace fields
34
+
35
+ ### Fixed
36
+
37
+ * [HexaPDF::Layout::TextBox#fit] to correctly calculate width in case of flowing
38
+ text around other boxes
39
+ * [HexaPDF::Layout::TextBox#draw] to correctly draw border, background... on
40
+ boxes using position 'flow'
41
+ * Comparison of Hash with [HexaPDF::Dictionary] objects by implementing
42
+ `#to_hash`
43
+ * Parsing of invalid files having multiple end-of-file markers with the last one
44
+ being invalid
45
+
46
+
1
47
  ## 0.42.0 - 2024-05-12
2
48
 
3
49
  ### Added
data/Rakefile CHANGED
@@ -47,7 +47,7 @@ namespace :dev do
47
47
  end
48
48
 
49
49
  task :test_all do
50
- versions = `rbenv versions --bare | grep -i ^2.7\\\\\\|^3.`.split("\n")
50
+ versions = `rbenv versions --bare | grep -i ^3.`.split("\n")
51
51
  versions.each do |version|
52
52
  sh "eval \"$(rbenv init -)\"; rbenv shell #{version} && ruby -v && rake test"
53
53
  end
data/examples/030-pdfa.rb CHANGED
@@ -8,6 +8,7 @@
8
8
  # Usage:
9
9
  # : `ruby pdfa.rb`
10
10
  #
11
+
11
12
  require 'hexapdf'
12
13
 
13
14
  HexaPDF::Composer.create('pdfa.pdf') do |composer|
@@ -425,6 +425,7 @@ module HexaPDF
425
425
  if draw_box
426
426
  @frame.draw(@canvas, result)
427
427
  drawn_on_page = true
428
+ (box = draw_box; break) unless box
428
429
  elsif !@frame.find_next_region
429
430
  unless drawn_on_page
430
431
  raise HexaPDF::Error, "Box doesn't fit on empty page"
@@ -228,9 +228,9 @@ module HexaPDF
228
228
  value.empty?
229
229
  end
230
230
 
231
- # Returns a dup of the underlying hash.
232
- def to_h
233
- value.dup
231
+ # Returns a hash containing the preprocessed values (like in #[]).
232
+ def to_hash
233
+ value.each_with_object({}) {|(k, _), h| h[k] = self[k] }
234
234
  end
235
235
 
236
236
  private
@@ -70,6 +70,9 @@ module HexaPDF
70
70
  # description::
71
71
  # A description of the file.
72
72
  #
73
+ # mime_type::
74
+ # The MIME type that should be set for embedded files (so only used if +embed+ is +true+).
75
+ #
73
76
  # embed::
74
77
  # When an IO object is given, it is always embedded and this option is ignored.
75
78
  #
@@ -77,7 +80,7 @@ module HexaPDF
77
80
  # only a reference to it is stored.
78
81
  #
79
82
  # See: HexaPDF::Type::FileSpecification
80
- def add(file_or_io, name: nil, description: nil, embed: true)
83
+ def add(file_or_io, name: nil, description: nil, mime_type: nil, embed: true)
81
84
  name ||= File.basename(file_or_io) if file_or_io.kind_of?(String)
82
85
  if name.nil?
83
86
  raise ArgumentError, "The name argument is mandatory when given an IO object"
@@ -86,7 +89,9 @@ module HexaPDF
86
89
  spec = @document.add({Type: :Filespec})
87
90
  spec.path = name
88
91
  spec[:Desc] = description if description
89
- spec.embed(file_or_io, name: name, register: true) if embed || !file_or_io.kind_of?(String)
92
+ if embed || !file_or_io.kind_of?(String)
93
+ spec.embed(file_or_io, name: name, mime_type: mime_type, register: true)
94
+ end
90
95
  spec
91
96
  end
92
97
 
@@ -161,6 +161,7 @@ module HexaPDF
161
161
  @properties = PREDEFINED_PROPERTIES.transform_values(&:dup)
162
162
  @default_language = document.catalog[:Lang] || 'x-default'
163
163
  @metadata = Hash.new {|h, k| h[k] = {} }
164
+ @custom_metadata = []
164
165
  write_info_dict(true)
165
166
  write_metadata_stream(true)
166
167
  @document.register_listener(:complete_objects, &method(:write_metadata))
@@ -248,6 +249,16 @@ module HexaPDF
248
249
  end
249
250
  end
250
251
 
252
+ # Adds the given +data+ string as custom metadata to the XMP document.
253
+ #
254
+ # The +data+ string must contain a fully valid 'rdf:Description' element.
255
+ #
256
+ # Using this method allows adding metadata like PDF/A schema definitions for which there is no
257
+ # direct support by HexaPDF.
258
+ def custom_metadata(data)
259
+ @custom_metadata << data
260
+ end
261
+
251
262
  # :call-seq:
252
263
  # metadata.delete
253
264
  # metadata.delete(ns_prefix)
@@ -469,7 +480,7 @@ module HexaPDF
469
480
  <?xpacket begin="\u{FEFF}" id="#{SecureRandom.uuid.tr('-', '')}"?>
470
481
  <x:xmpmeta xmlns:x="adobe:ns:meta/">
471
482
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
472
- #{data}
483
+ #{data}#{@custom_metadata.empty? ? '' : "\n#{@custom_metadata.join("\n")}"}
473
484
  </rdf:RDF>
474
485
  </x:xmpmeta>
475
486
  <?xpacket end="r"?>
@@ -278,6 +278,14 @@ module HexaPDF
278
278
  # If the same argument is provided in multiple invocations, the import is done only once and
279
279
  # the previously imported object is returned.
280
280
  #
281
+ # Note: If you first create a PDF document from scratch and then want to import objects from it
282
+ # into another PDF document, you need to run the following on the source document:
283
+ #
284
+ # doc.dispatch_message(:complete_objects)
285
+ # doc.validate
286
+ #
287
+ # This ensures that the source document has all the necessary PDF structures set-up correctly.
288
+ #
281
289
  # See: Importer
282
290
  def import(obj)
283
291
  source = (obj.kind_of?(HexaPDF::Object) ? obj.document : nil)
@@ -617,13 +625,18 @@ module HexaPDF
617
625
  # writing the document.
618
626
  #
619
627
  # The security handler used for encrypting is selected via the +name+ argument. All other
620
- # arguments are passed on the security handler.
628
+ # arguments are passed on to the security handler.
621
629
  #
622
630
  # If the document should not be encrypted, the +name+ argument has to be set to +nil+. This
623
631
  # removes the security handler and deletes the trailer's Encrypt dictionary.
624
632
  #
625
633
  # See: Encryption::SecurityHandler#set_up_encryption and
626
634
  # Encryption::StandardSecurityHandler::EncryptionOptions for possible encryption options.
635
+ #
636
+ # Examples:
637
+ #
638
+ # document.encrypt(name: nil) # remove the existing encryption
639
+ # document.encrypt(algorithm: :aes, key_length: 256, permissions: [:print, :extract_content]
627
640
  def encrypt(name: :Standard, **options)
628
641
  if name.nil?
629
642
  trailer.delete(:Encrypt)
@@ -46,6 +46,23 @@ module HexaPDF
46
46
  #
47
47
  # This module contains all encryption and security related code to facilitate PDF encryption.
48
48
  #
49
+ # === Working With Encrypted Documents
50
+ #
51
+ # When a PDF document is opened, an encryption password can be specified. This is necessary if a
52
+ # user password is set on the file and optional otherwise (because the default password is
53
+ # automatically tried):
54
+ #
55
+ # HexaPDF::Document.open(filename, decryption_opts: {password: 'somepassword'}) do |doc|
56
+ # end
57
+ #
58
+ # To remove the encryption from a PDF document, use the following:
59
+ #
60
+ # document.encrypt(name: nil)
61
+ #
62
+ # To encrypt a PDF document, use the same method but specify the required encryption options:
63
+ #
64
+ # document.encrypt(algorithm: :aes, key_length: 256)
65
+ #
49
66
  #
50
67
  # === Security Handlers
51
68
  #
@@ -35,6 +35,7 @@
35
35
  #++
36
36
  require 'hexapdf/layout/style'
37
37
  require 'geom2d/utils'
38
+ require 'hexapdf/utils'
38
39
 
39
40
  module HexaPDF
40
41
  module Layout
@@ -60,9 +61,9 @@ module HexaPDF
60
61
  # instantiated from the common convenience method HexaPDF::Document::Layout#box. To use this
61
62
  # facility subclasses need to be registered with the configuration option 'layout.boxes.map'.
62
63
  #
63
- # The methods #supports_position_flow?, #empty?, #fit or #fit_content, #split or #split_content,
64
- # and #draw or #draw_content need to be customized according to the subclass's use case (also
65
- # see the documentation of the methods besides the informatione below):
64
+ # The methods #supports_position_flow?, #empty?, #fit_content, #split_content, and #draw_content
65
+ # need to be customized according to the subclass's use case (also see the documentation of the
66
+ # methods besides the information below):
66
67
  #
67
68
  # #supports_position_flow?::
68
69
  # If the subclass supports the value :flow of the 'position' style property, this method
@@ -71,25 +72,22 @@ module HexaPDF
71
72
  # #empty?::
72
73
  # This method should return +true+ if the subclass won't draw anything when #draw is called.
73
74
  #
74
- # #fit::
75
- # This method should return +true+ if fitting was successful. Additionally, the
76
- # @fit_successful instance variable needs to be set to the fit result as it is used in
77
- # #split.
75
+ # #fit_content::
76
+ # This method determines whether the box fits into the available region and should set the
77
+ # status of #fit_result appropriately.
78
78
  #
79
- # The default implementation provides code common to most use-cases and delegates the
80
- # specifics to the #fit_content method which needs to return +true+ if fitting was
81
- # successful.
79
+ # It is called from the #fit method which should not be overridden in most cases. The
80
+ # default implementations of both methods provide code common to all use-cases and delegates
81
+ # the specifics to the subclass-specific #fit_content method.
82
82
  #
83
- # #split::
84
- # This method splits the content so that the current region is used as good as possible. The
85
- # default implementation should be fine for most use-cases, so only #split_content needs to
86
- # be implemented. The method #create_split_box should be used for getting a basic cloned
87
- # box.
83
+ # #split_content::
84
+ # This method is called from #split which handles the common cases based on the status of
85
+ # the #fit_result. It needs to handle the case when only some part of the box fits. The
86
+ # method #create_split_box should be used for getting a basic cloned box.
88
87
  #
89
- # #draw::
90
- # This method draws the content and the default implementation already handles things like
91
- # drawing the border and background. So it should not be overridden. The box specific
92
- # drawing commands should be implemented in the #draw_content method.
88
+ # #draw_content::
89
+ # This method draws the box specific content and is called from #draw which already handles
90
+ # things like drawing the border and background. So #draw should usually not be overridden.
93
91
  #
94
92
  # This base class provides various private helper methods for use in the above methods:
95
93
  #
@@ -117,6 +115,104 @@ module HexaPDF
117
115
 
118
116
  include HexaPDF::Utils
119
117
 
118
+ # Stores the result of fitting a box in a frame.
119
+ class FitResult
120
+
121
+ # The box that was fitted into the frame.
122
+ attr_accessor :box
123
+
124
+ # The frame into which the box was fitted.
125
+ attr_accessor :frame
126
+
127
+ # The horizontal position where the box will be drawn.
128
+ attr_accessor :x
129
+
130
+ # The vertical position where the box will be drawn.
131
+ attr_accessor :y
132
+
133
+ # The rectangle (a Geom2D::Rectangle object) that will be removed from the frame when
134
+ # drawing the box.
135
+ attr_accessor :mask
136
+
137
+ # The status result of fitting the box in the frame.
138
+ #
139
+ # Allowed values are:
140
+ #
141
+ # +:failure+:: (default) Indicates fitting the box has failed.
142
+ # +:success+:: Indicates that the box was completely fitted.
143
+ # +:overflow+:: Indicates that only a part of the box was fitted.
144
+ attr_reader :status
145
+
146
+ # Initializes the result object for the given box and, optionally, frame.
147
+ def initialize(box, frame: nil)
148
+ @box = box
149
+ reset(frame)
150
+ end
151
+
152
+ # Resets the result object.
153
+ def reset(frame)
154
+ @frame = frame
155
+ @x = @y = @mask = nil
156
+ @status = :failure
157
+ self
158
+ end
159
+
160
+ # Sets the result status to success.
161
+ def success!
162
+ @status = :success
163
+ end
164
+
165
+ # Returns +true+ if fitting was successful.
166
+ def success?
167
+ @status == :success
168
+ end
169
+
170
+ # Sets the result status to overflow.
171
+ def overflow!
172
+ @status = :overflow
173
+ end
174
+
175
+ # Returns +true+ if only parts of the box were fitted.
176
+ def overflow?
177
+ @status == :overflow
178
+ end
179
+
180
+ # Returns +true+ if fitting was a failure.
181
+ def failure?
182
+ @status == :failure
183
+ end
184
+
185
+ # Draws the #box onto the canvas at (#x + *dx*, #y + *dy*).
186
+ #
187
+ # The relative offset (dx, dy) is useful when rendering results that were accumulated and
188
+ # then need to be moved because the container holding them changes its position.
189
+ #
190
+ # The configuration option "debug" can be used to add visual debug output with respect to
191
+ # box placement.
192
+ def draw(canvas, dx: 0, dy: 0)
193
+ return if box.height == 0 || box.width == 0
194
+ doc = canvas.context.document
195
+ if doc.config['debug']
196
+ name = (frame.parent_boxes + [box]).map do |box|
197
+ box.class.to_s.sub(/.*::/, '')
198
+ end.join('-') << "##{box.object_id}"
199
+ name = "#{name} (#{(x + dx).to_i},#{(y + dy).to_i}-#{mask.width.to_i}x#{mask.height.to_i})"
200
+ ocg = doc.optional_content.ocg(name)
201
+ canvas.optional_content(ocg) do
202
+ canvas.translate(dx, dy) do
203
+ canvas.fill_color("green").stroke_color("darkgreen").
204
+ opacity(fill_alpha: 0.1, stroke_alpha: 0.2).
205
+ draw(:geom2d, object: mask, path_only: true).fill_stroke
206
+ end
207
+ end
208
+ page = "Page #{canvas.context.index + 1}" rescue "XObject"
209
+ doc.optional_content.default_configuration.add_ocg_to_ui(ocg, path: ['Debug', page])
210
+ end
211
+ box.draw(canvas, x + dx, y + dy)
212
+ end
213
+
214
+ end
215
+
120
216
  # Creates a new Box object, using the provided block as drawing block (see ::new).
121
217
  #
122
218
  # If +content_box+ is +true+, the width and height are taken to mean the content width and
@@ -142,10 +238,15 @@ module HexaPDF
142
238
  # The height of the box, including padding and/or borders.
143
239
  attr_reader :height
144
240
 
241
+ # The FitResult instance holding the result after a call to #fit.
242
+ attr_reader :fit_result
243
+
145
244
  # The style to be applied.
146
245
  #
147
246
  # Only the following properties are used:
148
247
  #
248
+ # * Style#position
249
+ # * Style#overflow
149
250
  # * Style#background_color
150
251
  # * Style#background_alpha
151
252
  # * Style#padding
@@ -189,7 +290,7 @@ module HexaPDF
189
290
  @style = Style.create(style)
190
291
  @properties = properties || {}
191
292
  @draw_block = block
192
- @fit_successful = false
293
+ @fit_result = FitResult.new(self)
193
294
  @split_box = false
194
295
  end
195
296
 
@@ -216,7 +317,7 @@ module HexaPDF
216
317
  height < 0 ? 0 : height
217
318
  end
218
319
 
219
- # Fits the box into the *frame* and returns +true+ if fitting was successful.
320
+ # Fits the box into the *frame* and returns the #fit_result.
220
321
  #
221
322
  # The arguments +available_width+ and +available_height+ are the width and height of the
222
323
  # current region of the frame, adjusted for this box. The frame itself is provided as third
@@ -224,70 +325,68 @@ module HexaPDF
224
325
  #
225
326
  # The default implementation uses the given available width and height for the box width and
226
327
  # height if they were initially set to 0. Otherwise the intially specified dimensions are
227
- # used. Then the #fit_content method is called which allows sub-classes to fit their content.
328
+ # used. The method returns early if the thus configured box already doesn't fit. Otherwise,
329
+ # the #fit_content method is called which allows sub-classes to fit their content.
228
330
  #
229
331
  # The following variables are set that may later be used during splitting or drawing:
230
332
  #
231
333
  # * (@fit_x, @fit_y): The lower-left corner of the content box where fitting was done. Can be
232
- # used to adjust the drawing position in #draw/#draw_content if necessary.
233
- # * @fit_successful: +true+ if fitting was successful.
334
+ # used to adjust the drawing position in #draw_content if necessary.
234
335
  def fit(available_width, available_height, frame)
336
+ @fit_result.reset(frame)
235
337
  @width = (@initial_width > 0 ? @initial_width : available_width)
236
338
  @height = (@initial_height > 0 ? @initial_height : available_height)
237
- @fit_successful = float_compare(@width, available_width) <= 0 &&
238
- float_compare(@height, available_height) <= 0
239
- return unless @fit_successful
339
+ return @fit_result if style.position != :flow && (float_compare(@width, available_width) > 0 ||
340
+ float_compare(@height, available_height) > 0)
240
341
 
241
- @fit_successful = fit_content(available_width, available_height, frame)
342
+ fit_content(available_width, available_height, frame)
242
343
 
243
344
  @fit_x = frame.x + reserved_width_left
244
345
  @fit_y = frame.y - @height + reserved_height_bottom
245
346
 
246
- @fit_successful
347
+ @fit_result
247
348
  end
248
349
 
249
350
  # Tries to split the box into two, the first of which needs to fit into the current region of
250
- # the frame, and returns the parts as array.
351
+ # the frame, and returns the parts as array. The method #fit needs to be called before this
352
+ # method to correctly set-up the #fit_result.
251
353
  #
252
354
  # If the first item in the result array is not +nil+, it needs to be this box and it means
253
355
  # that even when #fit fails, a part of the box may still fit. Note that #fit should not be
254
- # called before #draw on the first box since it is already fitted. If not even a part of this
255
- # box fits into the current region, +nil+ should be returned as the first array element.
356
+ # called again before #draw on the first box since it is already fitted. If not even a part of
357
+ # this box fits into the current region, +nil+ should be returned as the first array element.
256
358
  #
257
359
  # Possible return values:
258
360
  #
259
- # [self]:: The box fully fits into the current region.
361
+ # [self, nil]:: The box fully fits into the current region.
260
362
  # [nil, self]:: The box can't be split or no part of the box fits into the current region.
261
363
  # [self, new_box]:: A part of the box fits and a new box is returned for the rest.
262
364
  #
263
- # This default implementation provides the basic functionality based on the #fit result that
264
- # should be sufficient for most subclasses; only #split_content needs to be implemented if
265
- # necessary.
266
- def split(available_width, available_height, frame)
267
- if @fit_successful
268
- [self, nil]
269
- elsif (style.position != :flow &&
270
- (float_compare(@width, available_width) > 0 ||
271
- float_compare(@height, available_height) > 0)) ||
272
- content_height == 0 || content_width == 0
273
- [nil, self]
274
- else
275
- split_content(available_width, available_height, frame)
365
+ # This default implementation provides the basic functionality based on the status of the
366
+ # #fit_result that should be sufficient for most subclasses; only #split_content needs to be
367
+ # implemented if necessary.
368
+ def split
369
+ case @fit_result.status
370
+ when :overflow then (@initial_height > 0 ? [self, nil] : split_content)
371
+ when :failure then [nil, self]
372
+ when :success then [self, nil]
276
373
  end
277
374
  end
278
375
 
279
376
  # Draws the content of the box onto the canvas at the position (x, y).
280
377
  #
281
- # The coordinate system is translated so that the origin is at the bottom left corner of the
282
- # **content box** during the drawing operations when +@draw_block+ is used.
378
+ # When +@draw_block+ is used (the block specified when creating the box), the coordinate
379
+ # system is translated so that the origin is at the bottom left corner of the **content box**.
283
380
  #
284
- # The block specified when creating the box is invoked with the canvas and the box as
285
- # arguments. Subclasses can specify an on-demand drawing method by setting the +@draw_block+
286
- # instance variable to +nil+ or a valid block. This is useful to avoid unnecessary set-up
287
- # operations when the block does nothing.
288
- #
289
- # Alternatively, if a #draw_content method is defined, this method is called.
381
+ # Subclasses should not rely on the +@draw_block+ but implement the #draw_content method. The
382
+ # coordinates passed to it are also modified to represent the bottom left corner of the
383
+ # content box but the coordinate system is not translated.
290
384
  def draw(canvas, x, y)
385
+ if @fit_result.overflow? && @initial_height > 0 && style.overflow == :error
386
+ raise HexaPDF::Error, "Box with limited height doesn't completely fit and " \
387
+ "style property overflow is set to :error"
388
+ end
389
+
291
390
  if (oc = properties['optional_content'])
292
391
  canvas.optional_content(oc)
293
392
  end
@@ -380,12 +479,13 @@ module HexaPDF
380
479
 
381
480
  # Fits the content of the box and returns whether fitting was successful.
382
481
  #
383
- # This is just a stub implementation that returns +true+. Subclasses should override it to
384
- # provide the box specific behaviour.
482
+ # This is just a stub implementation that sets the #fit_result status to success if the
483
+ # content rectangle is not degenerate. Subclasses should override it to provide the box
484
+ # specific behaviour.
385
485
  #
386
486
  # See #fit for details.
387
487
  def fit_content(_available_width, _available_height, _frame)
388
- true
488
+ fit_result.success! if content_width > 0 && content_height > 0
389
489
  end
390
490
 
391
491
  # Splits the content of the box.
@@ -394,12 +494,12 @@ module HexaPDF
394
494
  # the content when it didn't fit.
395
495
  #
396
496
  # Subclasses that support splitting content need to provide an appropriate implementation and
397
- # use #create_split_box to create a cloned box to supply as the second argument.
398
- def split_content(_available_width, _available_height, _frame)
497
+ # use #create_split_box to create a cloned box to supply as the second return argument.
498
+ def split_content
399
499
  [nil, self]
400
500
  end
401
501
 
402
- # Draws the content of the box at position [x, y] which is the bottom-left corner of the
502
+ # Draws the content of the box at position [x, y] which is the bottom left corner of the
403
503
  # content box.
404
504
  #
405
505
  # This implementation uses the drawing block provided on initialization, if set, to draw the
@@ -421,7 +521,7 @@ module HexaPDF
421
521
  box = clone
422
522
  box.instance_variable_set(:@width, @initial_width)
423
523
  box.instance_variable_set(:@height, @initial_height)
424
- box.instance_variable_set(:@fit_successful, nil)
524
+ box.instance_variable_set(:@fit_result, FitResult.new(box))
425
525
  box.instance_variable_set(:@split_box, split_box_value)
426
526
  box
427
527
  end
@@ -47,8 +47,8 @@ module HexaPDF
47
47
  #
48
48
  # * Then use the #fit method to fit boxes one after the other. No drawing is done.
49
49
  #
50
- # * Once all boxes have been fitted, the #fit_results, #remaining_boxes and #fit_successful?
51
- # methods can be used to get the result:
50
+ # * Once all boxes have been fitted, the #fit_results, #remaining_boxes and #success? methods
51
+ # can be used to get the result:
52
52
  #
53
53
  # - If there are no remaining boxes, all boxes were successfully fitted into the frames.
54
54
  # - If there are remaining boxes but no fit results, the first box could not be fitted.
@@ -111,6 +111,7 @@ module HexaPDF
111
111
  @content_heights[@frame_index] = [@content_heights[@frame_index],
112
112
  @initial_frame_y[@frame_index] - result.mask.y].max
113
113
  @fit_results << result
114
+ break unless box
114
115
  elsif !current_frame.find_next_region
115
116
  @frame_index += 1
116
117
  end
@@ -126,7 +127,7 @@ module HexaPDF
126
127
  end
127
128
 
128
129
  # Returns +true+ if all boxes were successfully fitted.
129
- def fit_successful?
130
+ def success?
130
131
  @remaining_boxes.empty?
131
132
  end
132
133