hexapdf 0.42.0 → 0.43.0

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