hexapdf 0.42.0 → 0.44.0

Sign up to get free protection for your applications and to get access to all the features.
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