hexapdf 0.42.0 → 0.43.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd69db2c04dc802784438f9038c9a6ab32dac0806edca43b6c59406bba9d7ea6
4
- data.tar.gz: a51a510a6a98b547b2b11fb07804ca9039a666adf95b52f65db9b070c3ffe9c3
3
+ metadata.gz: 24f6839e903fd945678915625b1e2ef6a12221f29a82cf1c7a6d8bcea38af288
4
+ data.tar.gz: 2f8309e2ef2406dd279e00643bf7fbf73ded63a1076c0067684a356fea8ee89e
5
5
  SHA512:
6
- metadata.gz: 714cba0b758960066498413b545dc7cf2806e1e483f99b017958450bfe29fd12aac1eef40f670d189f7cd7dab5784dd83e7b6f95533b97cf71c883dd9f307485
7
- data.tar.gz: c93ceed7393b57fa5c6ca83141a8307b6015a1fa6907886b52c57bf72384d4d23ac8356adede0dabc35578cad5e1cef074f75c94e01e5c9532cadd0a688bbd57
6
+ metadata.gz: 8c6ddd8ec6f19d39daa9a6422fdb3595d255b528fb93ca7907ef12dab0eca23c74de9fcfc27d02c15b3a9d3c24d47c7f7db6ea472f4c1ed34c2b7c06d4a8a351
7
+ data.tar.gz: 1d77c2da70d9048cbef6935afacde40fb6d4041414fce571a331a4bee33b0e3ee2ff59bb28eee02e515798c3c156a57f42f139356e946f64e878ea962756a1d8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## 0.43.0 - 2024-05-26
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Type::AcroForm::Form#create_namespace_field] for creating a pure
6
+ namespace field
7
+ * [HexaPDF::Type::AcroForm::Form#delete_field] for deleting fields
8
+
9
+ ### Changed
10
+
11
+ * Minimum Ruby version to be 3.0
12
+ * **Breaking change**: Renamed `HexaPDF::Layout::BoxFitter#fit_successful?` to
13
+ [HexaPDF::Layout::BoxFitter#success?]
14
+ * **Breaking Change**: Removed HexaPDF::Dictionary#to_h
15
+ * Form field creation methods of [HexaPDF::Type::AcroForm::Form] to
16
+ automatically create parent fields as namespace fields
17
+
18
+ ### Fixed
19
+
20
+ * [HexaPDF::Layout::TextBox#fit] to correctly calculate width in case of flowing
21
+ text around other boxes
22
+ * [HexaPDF::Layout::TextBox#draw] to correctly draw border, background... on
23
+ boxes using position 'flow'
24
+ * Comparison of Hash with [HexaPDF::Dictionary] objects by implementing
25
+ `#to_hash`
26
+ * Parsing of invalid files having multiple end-of-file markers with the last one
27
+ being invalid
28
+
29
+
1
30
  ## 0.42.0 - 2024-05-12
2
31
 
3
32
  ### 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
@@ -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
@@ -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
@@ -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.
@@ -126,7 +126,7 @@ module HexaPDF
126
126
  end
127
127
 
128
128
  # Returns +true+ if all boxes were successfully fitted.
129
- def fit_successful?
129
+ def success?
130
130
  @remaining_boxes.empty?
131
131
  end
132
132
 
@@ -186,7 +186,7 @@ module HexaPDF
186
186
 
187
187
  children.each {|box| @box_fitter.fit(box) }
188
188
 
189
- fit_successful = @box_fitter.fit_successful?
189
+ fit_successful = @box_fitter.success?
190
190
  initial_fit_successful = fit_successful if initial_fit_successful.nil?
191
191
 
192
192
  if fit_successful
@@ -211,7 +211,7 @@ module HexaPDF
211
211
  @draw_pos_x = frame.x + reserved_width_left
212
212
  @draw_pos_y = frame.y - @height + reserved_height_bottom
213
213
 
214
- @box_fitter.fit_successful?
214
+ @box_fitter.success?
215
215
  end
216
216
 
217
217
  private
@@ -137,7 +137,7 @@ module HexaPDF
137
137
  @box_fitter = BoxFitter.new([my_frame])
138
138
  children.each {|box| @box_fitter.fit(box) }
139
139
 
140
- if @box_fitter.fit_successful?
140
+ if @box_fitter.success?
141
141
  update_content_width do
142
142
  result = @box_fitter.fit_results.max_by {|r| r.mask.x + r.mask.width }
143
143
  children.empty? ? 0 : result.mask.x + result.mask.width - my_frame.left
@@ -173,6 +173,10 @@ module HexaPDF
173
173
  attr_accessor :items
174
174
 
175
175
  # An optional horizontal offset that should be taken into account when positioning the line.
176
+ #
177
+ # This offset always describes the offset from the left side (and not, for example, the offset
178
+ # from the right side of another line even if those two lines are actually on the same
179
+ # horizontal level).
176
180
  attr_accessor :x_offset
177
181
 
178
182
  # An optional vertical offset that should be taken into account when positioning the line.
@@ -248,14 +248,14 @@ module HexaPDF
248
248
  top -= item_result.height + item_spacing
249
249
  height -= item_result.height + item_spacing
250
250
 
251
- break if !box_fitter.fit_successful? || height <= 0
251
+ break if !box_fitter.success? || height <= 0
252
252
  end
253
253
 
254
254
  @height = @results.sum(&:height) + (@results.count - 1) * item_spacing + reserved_height
255
255
 
256
256
  @draw_pos_x = frame.x + reserved_width_left
257
257
  @draw_pos_y = frame.y - @height + reserved_height_bottom
258
- @all_items_fitted = @results.all? {|r| r.box_fitter.fit_successful? } &&
258
+ @all_items_fitted = @results.all? {|r| r.box_fitter.success? } &&
259
259
  @results.size == @children.size
260
260
  @fit_successful = @all_items_fitted || (@initial_height > 0 && style.overflow == :truncate)
261
261
  end
@@ -233,7 +233,7 @@ module HexaPDF
233
233
  @preferred_width = max_x_result.x + max_x_result.box.width + reserved_width
234
234
  @height = @preferred_height = box_fitter.content_heights[0] + reserved_height
235
235
  @fit_results = box_fitter.fit_results
236
- @fit_successful = box_fitter.fit_successful?
236
+ @fit_successful = box_fitter.success?
237
237
  else
238
238
  @preferred_width = reserved_width
239
239
  @height = @preferred_height = reserved_height
@@ -52,6 +52,7 @@ module HexaPDF
52
52
  @tl = TextLayouter.new(style)
53
53
  @items = items
54
54
  @result = nil
55
+ @x_offset = 0
55
56
  end
56
57
 
57
58
  # Returns the text that will be drawn.
@@ -80,7 +81,7 @@ module HexaPDF
80
81
  (@initial_height > 0 && @initial_height > available_height)
81
82
 
82
83
  frame = frame.child_frame(box: self)
83
- @width = @height = 0
84
+ @width = @x_offset = @height = 0
84
85
  @result = if style.position == :flow
85
86
  @tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
86
87
  apply_first_text_indent: !split_box?, frame: frame)
@@ -93,6 +94,14 @@ module HexaPDF
93
94
  end
94
95
  @width += if @initial_width > 0 || style.text_align == :center || style.text_align == :right
95
96
  width
97
+ elsif style.position == :flow
98
+ min_x = +Float::INFINITY
99
+ max_x = -Float::INFINITY
100
+ @result.lines.each do |line|
101
+ min_x = [min_x, line.x_offset].min
102
+ max_x = [max_x, line.x_offset + line.width].max
103
+ end
104
+ min_x.finite? ? (@x_offset = min_x; max_x - min_x) : 0
96
105
  else
97
106
  @result.lines.max_by(&:width)&.width || 0
98
107
  end
@@ -125,6 +134,11 @@ module HexaPDF
125
134
  end
126
135
  end
127
136
 
137
+ # :nodoc:
138
+ def draw(canvas, x, y)
139
+ super(canvas, x + @x_offset, y)
140
+ end
141
+
128
142
  # :nodoc:
129
143
  def empty?
130
144
  super && (!@result || @result.lines.empty?)
@@ -142,7 +156,7 @@ module HexaPDF
142
156
  end
143
157
 
144
158
  return if @result.lines.empty?
145
- @result.draw(canvas, x, y + content_height)
159
+ @result.draw(canvas, x - @x_offset, y + content_height)
146
160
  end
147
161
 
148
162
  # Creates a new TextBox instance for the items remaining after fitting the box.
@@ -362,29 +362,32 @@ module HexaPDF
362
362
  pos = @io.pos
363
363
  lines = @io.read(step_size + 40).split(/[\r\n]+/)
364
364
 
365
- eof_index = lines.rindex {|l| l.strip == '%%EOF' }
366
- if !eof_index
367
- eof_not_found = true
368
- elsif lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
369
- startxref_offset = $1.to_i
370
- startxref_mangled = true
371
- break # we found it even if it the syntax is not entirely correct
372
- elsif eof_index < 2 || lines[eof_index - 2].strip != "startxref"
373
- startxref_missing = true
374
- else
375
- startxref_offset = lines[eof_index - 1].to_i
376
- break # we found it
365
+ # Need to iterate through the whole lines array in case there are multiple %%EOF to try
366
+ eof_index = 0
367
+ while (eof_index = lines[0..(eof_index - 1)].rindex {|l| l.strip == '%%EOF' })
368
+ if lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
369
+ startxref_offset = $1.to_i
370
+ startxref_mangled = true
371
+ break # we found it even if it the syntax is not entirely correct
372
+ elsif eof_index < 2 || lines[eof_index - 2].strip != "startxref"
373
+ startxref_missing = true
374
+ else
375
+ startxref_offset = lines[eof_index - 1].to_i
376
+ break # we found it
377
+ end
377
378
  end
379
+ eof_not_found ||= !eof_index
380
+ break if startxref_offset
378
381
  end
379
382
 
380
- if eof_not_found
381
- maybe_raise("PDF file trailer with end-of-file marker not found", pos: pos,
382
- force: !eof_index)
383
- elsif startxref_mangled
383
+ if startxref_mangled
384
384
  maybe_raise("PDF file trailer keyword startxref on same line as value", pos: pos)
385
385
  elsif startxref_missing
386
386
  maybe_raise("PDF file trailer is missing startxref keyword", pos: pos,
387
- force: eof_index < 2 || lines[eof_index - 2].strip != "startxref")
387
+ force: !startxref_offset)
388
+ elsif eof_not_found
389
+ maybe_raise("PDF file trailer with end-of-file marker not found", pos: pos,
390
+ force: !startxref_offset)
388
391
  end
389
392
 
390
393
  @startxref_offset = startxref_offset
@@ -163,10 +163,21 @@ module HexaPDF
163
163
  field
164
164
  end
165
165
 
166
+ # Creates an untyped namespace field for creating hierarchies.
167
+ #
168
+ # Example:
169
+ #
170
+ # form.create_namespace_field('text')
171
+ # form.create_text_field('text.a1')
172
+ def create_namespace_field(name)
173
+ create_field(name)
174
+ end
175
+
166
176
  # Creates a new text field with the given name and adds it to the form.
167
177
  #
168
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
169
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
178
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
179
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
180
+ # the +name+ doesn't contain dots, a top-level field is created.
170
181
  #
171
182
  # The optional keyword arguments allow setting often used properties of the field:
172
183
  #
@@ -202,8 +213,9 @@ module HexaPDF
202
213
 
203
214
  # Creates a new multiline text field with the given name and adds it to the form.
204
215
  #
205
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
206
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
216
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
217
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
218
+ # the +name+ doesn't contain dots, a top-level field is created.
207
219
  #
208
220
  # The optional keyword arguments allow setting often used properties of the field, see
209
221
  # #create_text_field for details.
@@ -221,8 +233,9 @@ module HexaPDF
221
233
  # The +max_chars+ argument defines the maximum number of characters the comb text field can
222
234
  # accommodate.
223
235
  #
224
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
225
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
236
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
237
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
238
+ # the +name+ doesn't contain dots, a top-level field is created.
226
239
  #
227
240
  # The optional keyword arguments allow setting often used properties of the field, see
228
241
  # #create_text_field for details.
@@ -238,8 +251,9 @@ module HexaPDF
238
251
 
239
252
  # Creates a new file select field with the given name and adds it to the form.
240
253
  #
241
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
242
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
254
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
255
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
256
+ # the +name+ doesn't contain dots, a top-level field is created.
243
257
  #
244
258
  # The optional keyword arguments allow setting often used properties of the field, see
245
259
  # #create_text_field for details.
@@ -254,8 +268,9 @@ module HexaPDF
254
268
 
255
269
  # Creates a new password field with the given name and adds it to the form.
256
270
  #
257
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
258
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
271
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
272
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
273
+ # the +name+ doesn't contain dots, a top-level field is created.
259
274
  #
260
275
  # The optional keyword arguments allow setting often used properties of the field, see
261
276
  # #create_text_field for details.
@@ -270,8 +285,9 @@ module HexaPDF
270
285
 
271
286
  # Creates a new check box with the given name and adds it to the form.
272
287
  #
273
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
274
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
288
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
289
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
290
+ # the +name+ doesn't contain dots, a top-level field is created.
275
291
  #
276
292
  # Before a field value other than +false+ can be assigned to the check box, a widget needs
277
293
  # to be created.
@@ -281,8 +297,9 @@ module HexaPDF
281
297
 
282
298
  # Creates a radio button with the given name and adds it to the form.
283
299
  #
284
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
285
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
300
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
301
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
302
+ # the +name+ doesn't contain dots, a top-level field is created.
286
303
  #
287
304
  # Before a field value other than +nil+ can be assigned to the radio button, at least one
288
305
  # widget needs to be created.
@@ -292,8 +309,9 @@ module HexaPDF
292
309
 
293
310
  # Creates a combo box with the given name and adds it to the form.
294
311
  #
295
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
296
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
312
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
313
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
314
+ # the +name+ doesn't contain dots, a top-level field is created.
297
315
  #
298
316
  # The optional keyword arguments allow setting often used properties of the field:
299
317
  #
@@ -319,8 +337,9 @@ module HexaPDF
319
337
 
320
338
  # Creates a list box with the given name and adds it to the form.
321
339
  #
322
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
323
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
340
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
341
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
342
+ # the +name+ doesn't contain dots, a top-level field is created.
324
343
  #
325
344
  # The optional keyword arguments allow setting often used properties of the field:
326
345
  #
@@ -345,10 +364,38 @@ module HexaPDF
345
364
 
346
365
  # Creates a signature field with the given name and adds it to the form.
347
366
  #
348
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
349
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
367
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
368
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
369
+ # the +name+ doesn't contain dots, a top-level field is created.
350
370
  def create_signature_field(name)
351
- create_field(name, :Sig) {}
371
+ create_field(name, :Sig)
372
+ end
373
+
374
+ # :call-seq:
375
+ # form.delete_field(name)
376
+ # form.delete_field(field)
377
+ #
378
+ # Deletes the field specified by the given name or via the given field object.
379
+ #
380
+ # If the field is a signature field, the associated signature dictionary is also deleted.
381
+ def delete_field(name_or_field)
382
+ field = (name_or_field.kind_of?(String) ? field_by_name(name_or_field) : name_or_field)
383
+ document.delete(field[:V]) if field.field_type == :Sig
384
+
385
+ to_delete = field.each_widget(direct_only: false).to_a
386
+ document.pages.each do |page|
387
+ next unless page.key?(:Annots)
388
+ page_annots = page[:Annots].to_a - to_delete
389
+ page[:Annots].value.replace(page_annots)
390
+ end
391
+ to_delete.each {|widget| document.delete(widget) }
392
+
393
+ if field[:Parent]
394
+ field[:Parent][:Kids].delete(field)
395
+ else
396
+ self[:Fields].delete(field)
397
+ end
398
+ document.delete(field)
352
399
  end
353
400
 
354
401
  # Fills form fields with the values from the given +data+ hash.
@@ -485,23 +532,27 @@ module HexaPDF
485
532
 
486
533
  private
487
534
 
488
- # Creates a new field with the full name +name+ and the field type +type+.
489
- def create_field(name, type)
535
+ # Creates a new field with the full name +name+ and the optional field type +type+.
536
+ def create_field(name, type = nil)
490
537
  parent_name, _, name = name.rpartition('.')
491
538
  parent_field = parent_name.empty? ? nil : field_by_name(parent_name)
492
539
  if !parent_name.empty? && !parent_field
493
- raise HexaPDF::Error, "Parent field '#{parent_name}' not found"
540
+ parent_field = create_namespace_field(parent_name)
494
541
  end
495
542
 
496
- field = document.add({FT: type, T: name, Parent: parent_field},
497
- type: :XXAcroFormField, subtype: type)
543
+ field = if type
544
+ document.add({FT: type, T: name, Parent: parent_field},
545
+ type: :XXAcroFormField, subtype: type)
546
+ else
547
+ document.add({T: name, Parent: parent_field}, type: :XXAcroFormField)
548
+ end
498
549
  if parent_field
499
550
  (parent_field[:Kids] ||= []) << field
500
551
  else
501
552
  (self[:Fields] ||= []) << field
502
553
  end
503
554
 
504
- yield(field)
555
+ yield(field) if block_given?
505
556
 
506
557
  field
507
558
  end
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.42.0'
40
+ VERSION = '0.43.0'
41
41
 
42
42
  end
@@ -20,13 +20,13 @@ describe HexaPDF::Layout::BoxFitter do
20
20
  @box_fitter.fit(HexaPDF::Layout::TextBox.new(items: [ibox] * count))
21
21
  end
22
22
 
23
- def check_result(*pos, content_heights:, successful: true, boxes_remain: false)
23
+ def check_result(*pos, content_heights:, success: true, boxes_remain: false)
24
24
  pos.each_slice(2).with_index do |(x, y), index|
25
25
  assert_equal(x, @box_fitter.fit_results[index].x, "x #{index}")
26
26
  assert_equal(y, @box_fitter.fit_results[index].y, "y #{index}")
27
27
  end
28
28
  assert_equal(content_heights, @box_fitter.content_heights)
29
- successful ? assert(@box_fitter.fit_successful?) : refute(@box_fitter.fit_successful?)
29
+ success ? assert(@box_fitter.success?) : refute(@box_fitter.success?)
30
30
  rboxes = @box_fitter.remaining_boxes.empty?
31
31
  boxes_remain ? refute(rboxes) : assert(rboxes)
32
32
  end
@@ -55,7 +55,7 @@ describe HexaPDF::Layout::BoxFitter do
55
55
  fit_box(70)
56
56
  fit_box(40)
57
57
  fit_box(20)
58
- check_result(10, 80, 0, 10, 0, 0, 100, 100, successful: false, boxes_remain: true,
58
+ check_result(10, 80, 0, 10, 0, 0, 100, 100, success: false, boxes_remain: true,
59
59
  content_heights: [90, 50])
60
60
  assert_equal(2, @box_fitter.remaining_boxes.size)
61
61
  end
@@ -50,10 +50,12 @@ describe HexaPDF::Layout::TextBox do
50
50
  end
51
51
 
52
52
  it "fits into the frame's outline" do
53
+ @frame.remove_area(Geom2D::Rectangle(0, 80, 20, 20))
54
+ @frame.remove_area(Geom2D::Rectangle(80, 70, 20, 20))
53
55
  box = create_box([@inline_box] * 20, style: {position: :flow})
54
56
  assert(box.fit(100, 100, @frame))
55
57
  assert_equal(100, box.width)
56
- assert_equal(20, box.height)
58
+ assert_equal(30, box.height)
57
59
  end
58
60
 
59
61
  it "takes the style option last_line_gap into account" do
@@ -190,6 +192,30 @@ describe HexaPDF::Layout::TextBox do
190
192
  [:restore_graphics_state]])
191
193
  end
192
194
 
195
+ it "correctly draws borders, backgrounds... for position :flow" do
196
+ @frame.remove_area(Geom2D::Rectangle(0, 0, 40, 100))
197
+ box = create_box([@inline_box], style: {position: :flow, border: {width: 1}})
198
+ box.fit(60, 100, @frame)
199
+ box.draw(@canvas, 0, 90)
200
+ assert_operators(@canvas.contents, [[:save_graphics_state],
201
+ [:append_rectangle, [40, 90, 10, 10]],
202
+ [:clip_path_non_zero],
203
+ [:end_path],
204
+ [:append_rectangle, [40.5, 90.5, 9.0, 9.0]],
205
+ [:stroke_path],
206
+ [:restore_graphics_state],
207
+ [:save_graphics_state],
208
+ [:restore_graphics_state],
209
+ [:save_graphics_state],
210
+ [:concatenate_matrix, [1, 0, 0, 1, 41, 89]],
211
+ [:save_graphics_state],
212
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 0]],
213
+ [:restore_graphics_state],
214
+ [:restore_graphics_state],
215
+ [:save_graphics_state],
216
+ [:restore_graphics_state]])
217
+ end
218
+
193
219
  it "draws nothing onto the canvas if the box is empty" do
194
220
  box = create_box([])
195
221
  box.draw(@canvas, 5, 5)
@@ -323,11 +323,13 @@ describe HexaPDF::Dictionary do
323
323
  end
324
324
  end
325
325
 
326
- describe "to_h" do
327
- it "returns a shallow copy of the value" do
328
- obj = @dict.to_h
326
+ describe "to_hash" do
327
+ it "returns a copy of the value where each entry is pre-processed" do
328
+ @dict[:value] = HexaPDF::Reference.new(1, 0)
329
+ obj = @dict.to_hash
329
330
  refute_equal(obj.object_id, @dict.value.object_id)
330
- assert_equal(obj, @dict.value)
331
+ assert_equal(:obj, obj[:Object])
332
+ assert_equal("deref", obj[:value])
331
333
  end
332
334
  end
333
335
 
@@ -367,6 +367,11 @@ describe HexaPDF::Parser do
367
367
  assert_equal(5, @parser.startxref_offset)
368
368
  end
369
369
 
370
+ it "handles the case of multiple %%EOF and the last one being invalid" do
371
+ create_parser("startxref\n5\n%%EOF\ntartxref\n3\n%%EOF")
372
+ assert_equal(5, @parser.startxref_offset)
373
+ end
374
+
370
375
  it "fails even in big files when nothing is found" do
371
376
  create_parser("\nhallo" * 5000)
372
377
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
@@ -402,6 +407,13 @@ describe HexaPDF::Parser do
402
407
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
403
408
  assert_match(/startxref on same line/, exp.message)
404
409
  end
410
+
411
+ it "fails on strict parsing if there are multiple %%EOF and the last one is invalid" do
412
+ @document.config['parser.on_correctable_error'] = proc { true }
413
+ create_parser("startxref\n5\n%%EOF\ntartxref\n3\n%%EOF")
414
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
415
+ assert_match(/missing startxref keyword/, exp.message)
416
+ end
405
417
  end
406
418
 
407
419
  describe "file_header_version" do
@@ -128,6 +128,12 @@ describe HexaPDF::Type::AcroForm::Form do
128
128
  @acro_form = @doc.acro_form(create: true)
129
129
  end
130
130
 
131
+ it "creates a pure namespace field" do
132
+ field = @acro_form.create_namespace_field('text')
133
+ assert_equal('text', field.full_field_name)
134
+ assert_nil(field.concrete_field_type)
135
+ end
136
+
131
137
  describe "handles the general case" do
132
138
  it "works for names with a dot" do
133
139
  @acro_form[:Fields] = [{T: "root"}]
@@ -142,8 +148,13 @@ describe HexaPDF::Type::AcroForm::Form do
142
148
  assert([field], @acro_form[:Fields])
143
149
  end
144
150
 
145
- it "fails if the parent field is not found" do
146
- assert_raises(HexaPDF::Error) { @acro_form.create_text_field("root.field") }
151
+ it "creates the parent fields as namespace fields if necessary" do
152
+ field = @acro_form.create_text_field("root.sub.field")
153
+ level1 = @acro_form.field_by_name('root')
154
+ assert_equal(1, level1[:Kids].size)
155
+ level2 = @acro_form.field_by_name('root.sub')
156
+ assert_equal(1, level2[:Kids].size)
157
+ assert_same(field, level2[:Kids][0])
147
158
  end
148
159
  end
149
160
 
@@ -241,6 +252,56 @@ describe HexaPDF::Type::AcroForm::Form do
241
252
  end
242
253
  end
243
254
 
255
+ describe "delete_field" do
256
+ before do
257
+ @field = @acro_form.create_signature_field("sig")
258
+ end
259
+
260
+ it "deletes a field via name" do
261
+ @acro_form.delete_field('sig')
262
+ assert_equal(0, @acro_form.root_fields.size)
263
+ end
264
+
265
+ it "deletes a field via field object" do
266
+ @acro_form.delete_field(@field)
267
+ assert_equal(0, @acro_form.root_fields.size)
268
+ end
269
+
270
+ it "deletes the set signature object" do
271
+ obj = @doc.add({})
272
+ @field.field_value = obj
273
+ @acro_form.delete_field(@field)
274
+ assert(obj.null?)
275
+ end
276
+
277
+ it "deletes all widget annotations from the document and the annotation array" do
278
+ widget1 = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
279
+ widget2 = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
280
+ refute(@doc.pages[1][:Annots].empty?)
281
+ @acro_form.delete_field(@field)
282
+ assert(@doc.pages[0][:Annots].empty?)
283
+ assert(@doc.pages[1][:Annots].empty?)
284
+ assert(@doc.object(widget1).null?)
285
+ assert(@doc.object(widget2).null?)
286
+ end
287
+
288
+ it "deletes the field from the field hierarchy" do
289
+ @acro_form.delete_field('sig')
290
+ refute(@acro_form.field_by_name('sig'))
291
+ assert(@acro_form[:Fields].empty?)
292
+
293
+ @acro_form.create_signature_field("sub.sub.sig")
294
+ @acro_form.delete_field("sub.sub.sig")
295
+ refute(@acro_form.field_by_name('sub.sub.sig'))
296
+ assert(@acro_form[:Fields][0][:Kids][0][:Kids].empty?)
297
+ end
298
+
299
+ it "deletes the field itself" do
300
+ @acro_form.delete_field('sig')
301
+ assert(@doc.object(@field).null?)
302
+ end
303
+ end
304
+
244
305
  describe "fill" do
245
306
  it "works for text field types" do
246
307
  field = @acro_form.create_text_field('test')
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.42.0
4
+ version: 0.43.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-12 00:00:00.000000000 Z
11
+ date: 2024-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse