hexapdf 0.46.0 → 0.47.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/lib/hexapdf/configuration.rb +11 -0
  4. data/lib/hexapdf/encryption/standard_security_handler.rb +32 -26
  5. data/lib/hexapdf/importer.rb +1 -1
  6. data/lib/hexapdf/layout/table_box.rb +57 -10
  7. data/lib/hexapdf/task/optimize.rb +4 -4
  8. data/lib/hexapdf/type/acro_form/appearance_generator.rb +8 -4
  9. data/lib/hexapdf/type/acro_form/form.rb +8 -5
  10. data/lib/hexapdf/type/acro_form/signature_field.rb +2 -1
  11. data/lib/hexapdf/type/acro_form/variable_text_field.rb +11 -3
  12. data/lib/hexapdf/type/annotations/widget.rb +4 -2
  13. data/lib/hexapdf/version.rb +1 -1
  14. data/lib/hexapdf/writer.rb +1 -0
  15. data/test/data/standard-security-handler/bothpwd-aes-256bit-V5-R5.pdf +43 -0
  16. data/test/data/standard-security-handler/nopwd-aes-256bit-V5-R5.pdf +44 -0
  17. data/test/data/standard-security-handler/ownerpwd-aes-256bit-V5-R5.pdf +43 -0
  18. data/test/data/standard-security-handler/userpwd-aes-256bit-V5-R5.pdf +0 -0
  19. data/test/hexapdf/digital_signature/test_signatures.rb +4 -4
  20. data/test/hexapdf/encryption/test_standard_security_handler.rb +5 -2
  21. data/test/hexapdf/layout/test_table_box.rb +52 -0
  22. data/test/hexapdf/task/test_optimize.rb +2 -0
  23. data/test/hexapdf/test_document.rb +3 -3
  24. data/test/hexapdf/test_importer.rb +7 -0
  25. data/test/hexapdf/test_writer.rb +11 -2
  26. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +22 -5
  27. data/test/hexapdf/type/acro_form/test_form.rb +0 -5
  28. data/test/hexapdf/type/acro_form/test_signature_field.rb +3 -1
  29. data/test/hexapdf/type/acro_form/test_variable_text_field.rb +14 -1
  30. data/test/hexapdf/type/annotations/test_widget.rb +4 -0
  31. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9c1a35d4ad93b48faa1f728bcddc91778da584c8d673e2f15557bd22050a438
4
- data.tar.gz: 2ef8ca70891723643080a402f0808882f66f01a816c422477cc03fad34774859
3
+ metadata.gz: 15f4c7590b5f2ce321519ac0b4871971c12073a9d4b0df36ab2f2e3c156f09ab
4
+ data.tar.gz: 76dac6196e06e80fd88dade3f831b7744c2b763f004d3c389d3efebcace3be82
5
5
  SHA512:
6
- metadata.gz: 87c109bd8a6711b4df27a40c689a025d816075d6c68c21a1cc22924b5ae827cd44d98e45bff8f43c85403dd82d6a6e54b66c99bfe135298cba33292e9511a1fc
7
- data.tar.gz: e0529e3e244be8366e722ea29ab06a8b922f18698526c79a28d5d11f02da5ee103cba71ecbc6882fefe3de27f02d38f8ce8b505fefbb45839069c74aefaed9ae
6
+ metadata.gz: 8ea19ed17370cad1a1fa48a7e0cb85c16e3ff62b344d2a9a90c42d2ac98d5ce5f75c356facf6bb8276822db460e2d9f912f523783c82b3539200e33f9de9f383
7
+ data.tar.gz: bdf2d93feade9f0654366bbf89711b670cbd03ee88b81f197f756c2032305f9ee7b9265973f31c5a4e18e7afd3e4f9dfd08b46a0483502b896774494294c111a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ ## 0.47.0 - 2024-09-07
2
+
3
+ ### Added
4
+
5
+ * Configuration option 'acro_form.fallback_default_appearance' to allow setting
6
+ a standard default appearance string for a variable text field if none is
7
+ found
8
+ * Support for decrypting files with the proprietary algorithm /R 5
9
+
10
+ ### Changed
11
+
12
+ * [HexaPDF::Task::Optimize] to not remove optional /Type entries containing
13
+ default values
14
+ * Validation of [HexaPDF::Type::AcroForm::Form] to not add a /DA entry
15
+
16
+ ### Fixed
17
+
18
+ * [HexaPDF::Layout::TableBox] to correctly calculcate and distribute row
19
+ heights when row spans are involved
20
+ * [HexaPDF::Type::AcroForm::AppearanceGenerator] to work for files where check
21
+ boxes don't define the name of the on state
22
+ * [HexaPDF::Importer#import] to handle null values in all cases
23
+ * [HexaPDF::Type::AcroForm::VariableTextField] to handle parsing of invalid PDFs
24
+ with symbolic appearance strings
25
+ * [HexaPDF::Type::Annotations::Widget#marker_style] to handle invalid /DA values
26
+ with missing font size or color information
27
+ * [HexaPDF::Type::AcroForm::SignatureField#field_value] to always return a
28
+ correctly wrapped object
29
+ * [HexaPDF::Writer] to remove /Type entry from trailer
30
+ * [HexaPDF::Type::AcroForm::AppearanceGenerator#create_text_appearances] to
31
+ handle invalid appearance streams that are not correct Form XObjects
32
+
33
+
1
34
  ## 0.46.0 - 2024-08-11
2
35
 
3
36
  ### Added
@@ -182,6 +182,16 @@ module HexaPDF
182
182
  # acro_form.default_font_size::
183
183
  # A number specifying the default font size of AcroForm text fields which should be auto-sized.
184
184
  #
185
+ # acro_form.fallback_default_appearance::
186
+ # A hash containging arguments for
187
+ # HexaPDF::Type::AcroForm::VariableTextField#set_defaut_appearance_string which is used as
188
+ # fallback for fields without a default appearance.
189
+ #
190
+ # If this value is set to +nil+, an error is raised in case a variable text field cannot
191
+ # resolve a default appearance string.
192
+ #
193
+ # The default is the empty hash meaning the defaults from the method are used.
194
+ #
185
195
  # acro_form.fallback_font::
186
196
  # The font that should be used when a variable text field references a font that cannot be used.
187
197
  #
@@ -485,6 +495,7 @@ module HexaPDF
485
495
  Configuration.new('acro_form.appearance_generator' => 'HexaPDF::Type::AcroForm::AppearanceGenerator',
486
496
  'acro_form.create_appearances' => true,
487
497
  'acro_form.default_font_size' => 10,
498
+ 'acro_form.fallback_default_appearance' => {},
488
499
  'acro_form.fallback_font' => 'Helvetica',
489
500
  'acro_form.on_invalid_value' => proc do |field, value|
490
501
  raise HexaPDF::Error, "Invalid value #{value.inspect} for " \
@@ -106,6 +106,10 @@ module HexaPDF
106
106
  # password is supplied. To open such an encrypted PDF file, the +decryption_opts+ provided to
107
107
  # HexaPDF::Document.new needs to contain a :password key with the password.
108
108
  #
109
+ # **Note**: While HexaPDF supports reading files encrypted with revision 5, it doesn't support
110
+ # writing such files. This is no problem in practice since revision 5 was an inofficial Adobe
111
+ # extension to PDF 1.7 and revision 6 specified in PDF 2.0 is practically the same.
112
+ #
109
113
  # See: PDF2.0 s7.6.4
110
114
  class StandardSecurityHandler < SecurityHandler
111
115
 
@@ -340,13 +344,13 @@ module HexaPDF
340
344
  # Uses the given password (or the default password if none given) to retrieve the encryption
341
345
  # key.
342
346
  #
343
- # If the optional +check_permissions+ argument is +true+, the permissions for files
344
- # encrypted with revision 6 are checked. Otherwise, permission changes are ignored.
347
+ # If the optional +check_permissions+ argument is +true+, the permissions for files encrypted
348
+ # with revision 5 or 6 are checked. Otherwise, permission changes are ignored.
345
349
  def prepare_decryption(password: '', check_permissions: true)
346
350
  if dict[:Filter] != :Standard
347
351
  raise(HexaPDF::UnsupportedEncryptionError,
348
352
  "Invalid /Filter value #{dict[:Filter]} for standard security handler")
349
- elsif ![2, 3, 4, 6].include?(dict[:R])
353
+ elsif ![2, 3, 4, 5, 6].include?(dict[:R])
350
354
  raise(HexaPDF::UnsupportedEncryptionError,
351
355
  "Invalid /R value #{dict[:R]} for standard security handler")
352
356
  elsif dict[:R] <= 4 && !document.trailer[:ID].kind_of?(PDFArray)
@@ -369,7 +373,7 @@ module HexaPDF
369
373
  raise HexaPDF::EncryptionError, "Invalid password specified"
370
374
  end
371
375
 
372
- check_perms_field(encryption_key) if check_permissions && dict[:R] == 6
376
+ check_perms_field(encryption_key) if check_permissions && dict[:R] >= 5
373
377
 
374
378
  encryption_key
375
379
  end
@@ -396,8 +400,8 @@ module HexaPDF
396
400
  # For revisions <= 4 this is the *only* way for generating the encryption key needed to
397
401
  # encrypt or decrypt a file.
398
402
  #
399
- # For revision 6 the file encryption key is a string of random bytes that has been encrypted
400
- # with the user password. If the password is the owner password,
403
+ # For revision 5 and 6 the file encryption key is a string of random bytes that has been
404
+ # encrypted with the user password. If the password is the owner password,
401
405
  # #compute_owner_encryption_key has to be used instead.
402
406
  #
403
407
  # See: PDF2.0 s7.6.4.3.2 (algorithm 2), PDF2.0 s7.6.4.3.3 (algorithm 2.A (a)-(b),(e))
@@ -416,7 +420,7 @@ module HexaPDF
416
420
  end
417
421
 
418
422
  data[0, n]
419
- elsif dict[:R] == 6
423
+ elsif dict[:R] <= 6
420
424
  key = compute_hash(password, dict[:U][40, 8])
421
425
  aes_algorithm.new(key, "\0" * 16, :decrypt).process(dict[:UE])
422
426
  end
@@ -427,15 +431,15 @@ module HexaPDF
427
431
  # For revisions <= 4 this is done by first retrieving the user password through the use of
428
432
  # the owner password and then using the #compute_user_encryption_key method.
429
433
  #
430
- # For revision 6 the file encryption key is a string of random bytes that has been encrypted
431
- # with the owner password. If the password is the user password, #compute_user_encryption_key
432
- # has to be used.
434
+ # For revisions 5 and 6 the file encryption key is a string of random bytes that has been
435
+ # encrypted with the owner password. If the password is the user password,
436
+ # #compute_user_encryption_key has to be used.
433
437
  #
434
438
  # See: PDF2.0 s7.6.4.3.2 (algorithm 2.A (a)-(d))
435
439
  def compute_owner_encryption_key(password)
436
440
  if dict[:R] <= 4
437
441
  compute_user_encryption_key(user_password_from_owner_password(password))
438
- elsif dict[:R] == 6
442
+ elsif dict[:R] <= 6
439
443
  key = compute_hash(password, dict[:O][40, 8], dict[:U])
440
444
  aes_algorithm.new(key, "\0" * 16, :decrypt).process(dict[:OE])
441
445
  end
@@ -447,7 +451,7 @@ module HexaPDF
447
451
  # the owner password. For revision 6 the /O value is a hash computed from the password and
448
452
  # the /U value with added validation and key salts.
449
453
  #
450
- # *Attention*: If revision 6 is used, the /U value has to be computed and set before this
454
+ # *Attention*: If revision 5 or 6 is used, the /U value has to be computed and set before this
451
455
  # method is used, otherwise the return value is incorrect!
452
456
  #
453
457
  # See: PDF2.0 s7.6.4.4.2 (algorithm 3), PDF2.0 s7.6.4.4.8 (algorithm 9 (a))
@@ -465,14 +469,14 @@ module HexaPDF
465
469
  end
466
470
 
467
471
  data
468
- elsif dict[:R] == 6
472
+ elsif dict[:R] <= 6
469
473
  validation_salt = random_bytes(8)
470
474
  key_salt = random_bytes(8)
471
475
  compute_hash(owner_password, validation_salt, dict[:U]) << validation_salt << key_salt
472
476
  end
473
477
  end
474
478
 
475
- # Computes the encryption dictionary's /OE (owner encryption key) value (for revision 6
479
+ # Computes the encryption dictionary's /OE (owner encryption key) value (for revisions 5 and 6
476
480
  # only).
477
481
  #
478
482
  # Short explanation: Encrypts the file encryption key with a key based on the password and
@@ -487,7 +491,7 @@ module HexaPDF
487
491
  # Computes the encryption dictionary's /U (user password) value.
488
492
  #
489
493
  # Short explanation: For revisions <= 4, the password padding string is encrypted with a key
490
- # based on the user password. For revision 6 the /U value is a hash computed from the
494
+ # based on the user password. For revisions 5 and 6 the /U value is a hash computed from the
491
495
  # password with added validation and key salts.
492
496
  #
493
497
  # See: PDF2.0 s7.6.4.4.3 (algorithm 4 for R=2), PDF s7.6.4.4.4 (algorithm 5 for R=3 and R=4)
@@ -502,14 +506,14 @@ module HexaPDF
502
506
  data = arc4_algorithm.encrypt(key, data)
503
507
  19.times {|i| data = arc4_algorithm.encrypt(xor_key(key, i + 1), data) }
504
508
  data << "hexapdfhexapdfhe"
505
- elsif dict[:R] == 6
509
+ elsif dict[:R] <= 6
506
510
  validation_salt = random_bytes(8)
507
511
  key_salt = random_bytes(8)
508
512
  compute_hash(password, validation_salt) << validation_salt << key_salt
509
513
  end
510
514
  end
511
515
 
512
- # Computes the encryption dictionary's /UE (user encryption key) value (for revision 6
516
+ # Computes the encryption dictionary's /UE (user encryption key) value (for revision 5 and 6
513
517
  # only).
514
518
  #
515
519
  # Short explanation: Encrypts the file encryption key with a key based on the password and
@@ -521,7 +525,8 @@ module HexaPDF
521
525
  aes_algorithm.new(key, "\0" * 16, :encrypt).process(file_encryption_key)
522
526
  end
523
527
 
524
- # Computes the encryption dictionary's /Perms (permissions) value (for revision 6 only).
528
+ # Computes the encryption dictionary's /Perms (permissions) value (for revisions 5 and 6
529
+ # only).
525
530
  #
526
531
  # Uses /P and /EncryptMetadata values, so these have to be set beforehand.
527
532
  #
@@ -543,7 +548,7 @@ module HexaPDF
543
548
  compute_u_field(password) == dict[:U]
544
549
  elsif dict[:R] <= 4
545
550
  compute_u_field(password)[0, 16] == dict[:U][0, 16]
546
- elsif dict[:R] == 6
551
+ elsif dict[:R] <= 6
547
552
  compute_hash(password, dict[:U][32, 8]) == dict[:U][0, 32]
548
553
  end
549
554
  end
@@ -554,14 +559,14 @@ module HexaPDF
554
559
  def owner_password_valid?(password)
555
560
  if dict[:R] <= 4
556
561
  user_password_valid?(user_password_from_owner_password(password))
557
- elsif dict[:R] == 6
562
+ elsif dict[:R] <= 6
558
563
  compute_hash(password, dict[:O][32, 8], dict[:U]) == dict[:O][0, 32]
559
564
  end
560
565
  end
561
566
 
562
567
  # Checks if the decrypted /Perms entry matches the /P and /EncryptMetadata entries.
563
568
  #
564
- # This method can only be used for revision 6.
569
+ # This method can only be used for revisions 5 and 6.
565
570
  #
566
571
  # See: PDF2.0 s7.6.4.4.12 (algorithm 13)
567
572
  def check_perms_field(encryption_key)
@@ -596,17 +601,18 @@ module HexaPDF
596
601
  end
597
602
 
598
603
  # Computes a hash that is used extensively for all operations in security handlers of
599
- # revision 6.
604
+ # revision 5 and 6.
600
605
  #
601
606
  # Note: The original input (as defined by the spec) is calculated as
602
607
  # "#{password}#{salt}#{user_key}" where +user_key+ has to be empty when doing operations
603
608
  # with the user password.
604
609
  #
605
- # See: PDF2.0 s7.6.4.3.4 (algorithm 2.B)
610
+ # See: PDF2.0 s7.6.4.3.4 (algorithm 2.B) and ADB Extension Level 3 s3.5.2
606
611
  def compute_hash(password, salt, user_key = '')
607
612
  k = Digest::SHA256.digest("#{password}#{salt}#{user_key}")
608
- e = ''
613
+ return k if dict[:R] == 5
609
614
 
615
+ e = ''
610
616
  i = 0
611
617
  while i < 64 || e.getbyte(-1) > i - 32
612
618
  k1 = "#{password}#{k}#{user_key}" * 64
@@ -627,7 +633,7 @@ module HexaPDF
627
633
  # * For revisions <= 4, the password is converted into ISO-8859-1 encoding, padded with
628
634
  # PASSWORD_PADDING and truncated to a maximum of 32 bytes.
629
635
  #
630
- # * For revision 6 the password is converted into UTF-8 encoding that is normalized
636
+ # * For revision 5 and 6 the password is converted into UTF-8 encoding that is normalized
631
637
  # according to the PDF2.0 specification.
632
638
  #
633
639
  # See: PDF2.0 s7.6.4.3.2 (algorithm 2 step a)),
@@ -636,7 +642,7 @@ module HexaPDF
636
642
  if dict[:R] <= 4
637
643
  password.to_s[0, 32].encode(Encoding::ISO_8859_1).force_encoding(Encoding::BINARY).
638
644
  ljust(32, PASSWORD_PADDING)
639
- elsif dict[:R] == 6
645
+ elsif dict[:R] <= 6
640
646
  password.to_s.encode(Encoding::UTF_8).force_encoding(Encoding::BINARY)[0, 127]
641
647
  end
642
648
  rescue Encoding::UndefinedConversionError => e
@@ -141,7 +141,7 @@ module HexaPDF
141
141
  internal_import(wrapper.source.object(object), wrapper)
142
142
  when HexaPDF::Object
143
143
  wrapper.source ||= object.document
144
- if !@allow_all && (object.type == :Catalog || object.type == :Pages)
144
+ if object.null? || (!@allow_all && (object.type == :Catalog || object.type == :Pages))
145
145
  @mapper[object.data] = nil
146
146
  elsif (mapped_object = @mapper[object.data]&.__getobj__) && !mapped_object.null?
147
147
  mapped_object
@@ -382,7 +382,14 @@ module HexaPDF
382
382
  def fit_rows(start_row, available_height, column_info, frame)
383
383
  height = available_height
384
384
  last_fitted_row_index = -1
385
+ row_heights = {}
386
+ zero_height_rows = {}
387
+ row_spans = []
388
+
385
389
  @cells[start_row..-1].each.with_index(start_row) do |columns, row_index|
390
+ # 1. Fit all columns of the row and record the max height of all non-row-span cells. If
391
+ # a row has zero height (usually because it only has row-span cells), record that
392
+ # information. Additionally store all cells with row-spans.
386
393
  row_fit = true
387
394
  row_height = 0
388
395
  columns.each_with_index do |cell, col_index|
@@ -396,27 +403,67 @@ module HexaPDF
396
403
  row_fit = false
397
404
  break
398
405
  end
399
- cell.left = column_info[cell.column].first
400
- cell.top = height - available_height
401
- row_height = cell.preferred_height if row_height < cell.preferred_height
406
+ if row_height < cell.preferred_height && cell.row_span == 1
407
+ row_height = cell.preferred_height
408
+ end
409
+ row_spans << cell if cell.row_span > 1
402
410
  end
403
411
 
404
- if row_fit
405
- seen = {}
406
- columns.each do |cell|
407
- next if seen[cell]
408
- cell.update_height(cell.row == row_index ? row_height : cell.height + row_height)
409
- seen[cell] = true
410
- end
412
+ zero_height_rows[row_index] = true if row_height == 0
411
413
 
414
+ if row_fit
415
+ # 2. If all cells of the row fit, we subtract the recorded row height of the
416
+ # non-row-span cells from the available height for the next pass.
412
417
  last_fitted_row_index = row_index
418
+ row_heights[row_index] = row_height
413
419
  available_height -= row_height
420
+
421
+ # 3. We look at all row-span cells that end at the current row index. If the row-span
422
+ # cell is larger than the sum of the row heights, we proportionally enlarge the
423
+ # stored height of each spanned row and subtract the difference from the available
424
+ # height for the next pass. If the row span contains initially zero-height rows,
425
+ # only those rows are enlarged. Row-span cells themselves are not updated at this
426
+ # point!
427
+ row_spans.each do |cell|
428
+ upper_row_index = cell.row + cell.row_span - 1
429
+ next unless upper_row_index == row_index
430
+
431
+ rows = cell.row.upto(upper_row_index)
432
+ row_span_height = rows.sum {|ri| row_heights[ri] }
433
+ if row_span_height < cell.preferred_height
434
+ zero_height_rows_in_span = rows.select {|ri| zero_height_rows[ri] }
435
+ rows = zero_height_rows_in_span if zero_height_rows_in_span.size > 0
436
+ adjustment = (cell.preferred_height - row_span_height) / rows.size.to_f
437
+ rows.each {|ri| row_heights[ri] += adjustment }
438
+ available_height -= cell.preferred_height - row_span_height
439
+ end
440
+ end
414
441
  else
415
442
  last_fitted_row_index = columns.min_by(&:row).row - 1 if height != available_height
416
443
  break
417
444
  end
418
445
  end
419
446
 
447
+ if last_fitted_row_index >= 0
448
+ # 4. Once all possible rows have been fitted and the heights of the rows are fixed, the
449
+ # final height and top-left corner of each cell needs to be set.
450
+ running_height = 0
451
+ @cells[start_row..last_fitted_row_index].each.with_index(start_row) do |columns, row_index|
452
+ columns.each_with_index do |cell, col_index|
453
+ next if cell.row != row_index || cell.column != col_index
454
+ cell.left = column_info[cell.column].first
455
+ cell.top = running_height
456
+ if cell.row_span == 1
457
+ cell.update_height(row_heights[row_index])
458
+ else
459
+ new_height = cell.row.upto(cell.row + cell.row_span - 1).sum {|ri| row_heights[ri] }
460
+ cell.update_height(new_height)
461
+ end
462
+ end
463
+ running_height += row_heights[row_index]
464
+ end
465
+ end
466
+
420
467
  [height - available_height, last_fitted_row_index < start_row ? -1 : last_fitted_row_index]
421
468
  end
422
469
 
@@ -214,13 +214,13 @@ module HexaPDF
214
214
  end
215
215
  end
216
216
 
217
- # Deletes field entries of the object that are optional and currently set to their default
218
- # value.
217
+ # Deletes field entries (except for /Type) of the object that are optional and currently set
218
+ # to their default value.
219
219
  def self.delete_fields_with_defaults(obj)
220
220
  return unless obj.kind_of?(HexaPDF::Dictionary) && !obj.null?
221
221
  obj.each do |name, value|
222
- if (field = obj.class.field(name)) && !field.required? && field.default? &&
223
- value == field.default
222
+ if name != :Type && (field = obj.class.field(name)) && !field.required? &&
223
+ field.default? && value == field.default
224
224
  obj.delete(name)
225
225
  end
226
226
  end
@@ -134,11 +134,12 @@ module HexaPDF
134
134
  if !normal_appearance.kind_of?(HexaPDF::Dictionary) || normal_appearance.kind_of?(HexaPDF::Stream)
135
135
  (@widget[:AP] ||= {})[:N] = {Off: nil}
136
136
  normal_appearance = @widget[:AP][:N]
137
- normal_appearance[@field[:V] == :Off ? :Yes : @field[:V]] = nil
137
+ normal_appearance[@field.field_value&.to_sym || :Yes] = nil
138
138
  end
139
139
  on_name = (normal_appearance.value.keys - [:Off]).first
140
140
  unless on_name
141
- raise HexaPDF::Error, "Widget of button field doesn't define name for on state"
141
+ on_name = @field.field_value&.to_sym || :Yes
142
+ normal_appearance[on_name] = nil
142
143
  end
143
144
 
144
145
  @widget[:AS] = (@field[:V] == on_name ? on_name : :Off)
@@ -226,8 +227,11 @@ module HexaPDF
226
227
 
227
228
  form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
228
229
  # Wrap existing object in Form class in case the PDF writer didn't include the /Subtype
229
- # key; we can do this since we know this has to be a Form object
230
- form = @document.wrap(form, type: :XObject, subtype: :Form) unless form[:Subtype] == :Form
230
+ # key or the type of the object is wrong; we can do this since we know this has to be a
231
+ # Form object
232
+ unless form.type == :XObject && form[:Subtype] == :Form
233
+ form = @document.wrap(form, type: :XObject, subtype: :Form)
234
+ end
231
235
  form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
232
236
  Matrix: matrix, Resources: HexaPDF::Object.deep_copy(default_resources)})
233
237
  form.contents = ''
@@ -186,9 +186,14 @@ module HexaPDF
186
186
  # or +font_color+ is specified but +font+ isn't, the font Helvetica is used.
187
187
  #
188
188
  # If no font is set on the text field, the default font properties of the AcroForm form
189
- # are used. Note that field specific or form specific font properties have to be set.
190
- # Otherwise there will be an error when trying to generate a visual representation of
191
- # the field value.
189
+ # are used. Note that field specific or form specific font properties have to be
190
+ # set. Otherwise there might be problems when creating a visual appearance with other
191
+ # PDF libraries/viewers.
192
+ #
193
+ # If HexaPDF is used to create a visual appearance of the field value and neither field
194
+ # specific nor form specific font properties are available, the configuration option
195
+ # 'acro_form.fallback_default_appearance' defines whether and which field specific font
196
+ # properties are set and used.
192
197
  #
193
198
  # +font_options+::
194
199
  # A hash with font options like :variant that should be used.
@@ -625,8 +630,6 @@ module HexaPDF
625
630
  if font_name && !(self[:DR][:Font] && self[:DR][:Font][font_name])
626
631
  yield("The font specified in /DA is not in the /DR resource dictionary")
627
632
  end
628
- else
629
- set_default_appearance_string
630
633
  end
631
634
 
632
635
  create_appearances if document.config['acro_form.create_appearances']
@@ -199,7 +199,8 @@ module HexaPDF
199
199
 
200
200
  # Returns the associated signature dictionary or +nil+ if the signature is not filled in.
201
201
  def field_value
202
- self[:V]
202
+ val = self[:V]
203
+ val.instance_of?(Dictionary) ? document.wrap(val, type: :Sig) : val
203
204
  end
204
205
 
205
206
  # Sets the signature dictionary as value of this signature field.
@@ -106,7 +106,7 @@ module HexaPDF
106
106
  font_params[2] = HexaPDF::Content::ColorSpace.prenormalized_device_color(params)
107
107
  end
108
108
  end
109
- HexaPDF::Content::Parser.parse(appearance_string.sub(/\/\//, '/'), &block)
109
+ HexaPDF::Content::Parser.parse(appearance_string.to_s.sub(/\/\//, '/'), &block)
110
110
  block_given? ? nil : font_params
111
111
  end
112
112
 
@@ -154,13 +154,21 @@ module HexaPDF
154
154
  # font_size, font_color].
155
155
  #
156
156
  # The default appearance string is taken from the given +widget+ of the field, falls back to
157
- # the field itself or, if still not available, the default appearance string of the form.
157
+ # the field itself and then the default appearance string of the form. If it still not
158
+ # available, a standard default appearance string is set (see
159
+ # #set_default_appearance_string) and used.
158
160
  #
159
161
  # The reason why a specific widget of the field can be specified is because the widgets of a
160
162
  # field might differ in their visual representation.
161
163
  def parse_default_appearance_string(widget = self)
162
164
  da = widget[:DA] || self[:DA] || (document.acro_form && document.acro_form[:DA])
163
- raise HexaPDF::Error, "No default appearance string set" unless da
165
+ unless da
166
+ if (args = document.config['acro_form.fallback_default_appearance'])
167
+ da = set_default_appearance_string(**args)
168
+ else
169
+ raise HexaPDF::Error, "No default appearance string set"
170
+ end
171
+ end
164
172
  self.class.parse_appearance_string(da)
165
173
  end
166
174
 
@@ -259,7 +259,7 @@ module HexaPDF
259
259
  # auto-sized checkmark (i.e. :check for for check boxes) or circle (:circle for radio
260
260
  # buttons). This also means that multiple invocations will reset *all* prior values.
261
261
  #
262
- # Note: The marker is called "normal caption" in the PDF 1.7 spec and the /CA entry of the
262
+ # Note: The marker is called "normal caption" in the PDF 2.0 spec and the /CA entry of the
263
263
  # associated appearance characteristics dictionary. The marker size and color are set using
264
264
  # the /DA key on the widget (although /DA is not defined for widget, this is how Acrobat
265
265
  # does it).
@@ -305,7 +305,9 @@ module HexaPDF
305
305
  size = 0
306
306
  color = HexaPDF::Content::ColorSpace.prenormalized_device_color([0])
307
307
  if (da = self[:DA] || field[:DA])
308
- _, size, color = HexaPDF::Type::AcroForm::VariableTextField.parse_appearance_string(da)
308
+ _, da_size, da_color = AcroForm::VariableTextField.parse_appearance_string(da)
309
+ size = da_size || size
310
+ color = da_color || color
309
311
  end
310
312
 
311
313
  MarkerStyle.new(style, size, color)
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.46.0'
40
+ VERSION = '0.47.0'
41
41
 
42
42
  end
@@ -163,6 +163,7 @@ module HexaPDF
163
163
 
164
164
  trailer = rev.trailer.value.dup
165
165
  trailer.delete(:XRefStm)
166
+ trailer.delete(:Type)
166
167
  if previous_xref_pos
167
168
  trailer[:Prev] = previous_xref_pos
168
169
  else
@@ -0,0 +1,43 @@
1
+ %PDF-1.7
2
+ %����
3
+ 1 0 obj
4
+ << /Extensions << /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> >> /Pages 3 0 R /Type /Catalog >>
5
+ endobj
6
+ 2 0 obj
7
+ << /ModDate <aa1d1637459ea17c7fc9709f0c0c7b64761ff74e905b3765f33425776aa3010163cd5e898cfa7c5fc16159359375c323> >>
8
+ endobj
9
+ 3 0 obj
10
+ << /Count 1 /Kids [ 4 0 R ] /MediaBox [ 0 0 612 446 ] /Type /Pages >>
11
+ endobj
12
+ 4 0 obj
13
+ << /Contents 5 0 R /Parent 3 0 R /Resources 6 0 R /Type /Page >>
14
+ endobj
15
+ 5 0 obj
16
+ << /Length 80 /Filter /FlateDecode >>
17
+ stream
18
+ J<~S�)��9�M�$S�z��g�j���r�.R�"PO���_���X���^�GP�&z�g�*$�2SΈ�z�Kl��5ĩendstream
19
+ endobj
20
+ 6 0 obj
21
+ << /Font << /F1 7 0 R >> /ProcSet [ /PDF /Text ] >>
22
+ endobj
23
+ 7 0 obj
24
+ << /BaseFont /Helvetica /Name /F1 /Subtype /Type1 /Type /Font >>
25
+ endobj
26
+ 8 0 obj
27
+ << /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >> /Filter /Standard /Length 256 /O <c60934c0925e8d3ceebd0609102d9407dcf22d7fb3d87d030fce633af6d2ed99c1c3382bee5e8afc0d55ff8bca441d48> /OE <3fa2e3ecf34ddcb38b44af372a43268dda32111f58dc79da74d960b8fa206ead> /P -4 /Perms <b6c6ab65529f0a3322f03909e8a5547a> /R 5 /StmF /StdCF /StrF /StdCF /U <18fbd94777c28531c495a3116d273de9f8f3ed338b31c07687ca0ba06812842843765a66484d098194bcef0f9ecaaace> /UE <496a81bbb3207dfd12ddca4c16b60a411c6a18e638205824295e09afde826b4a> /V 5 >>
28
+ endobj
29
+ xref
30
+ 0 9
31
+ 0000000000 65535 f
32
+ 0000000015 00000 n
33
+ 0000000130 00000 n
34
+ 0000000259 00000 n
35
+ 0000000344 00000 n
36
+ 0000000424 00000 n
37
+ 0000000574 00000 n
38
+ 0000000641 00000 n
39
+ 0000000721 00000 n
40
+ trailer << /Info 2 0 R /Root 1 0 R /Size 9 /ID [<6790ffa610024e78369114311fc0df96><ed6c0810cb0d19599ac62042a0487749>] /Encrypt 8 0 R >>
41
+ startxref
42
+ 1268
43
+ %%EOF
@@ -0,0 +1,44 @@
1
+ %PDF-1.7
2
+ %����
3
+ 1 0 obj
4
+ << /Extensions << /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> >> /Pages 3 0 R /Type /Catalog >>
5
+ endobj
6
+ 2 0 obj
7
+ << /ModDate <0a09febf75d913e5eff6197f52f28b31321a43f49e0735a25a93dd77b1a2701e73ae05be0803747ca030ee4917544cbb> >>
8
+ endobj
9
+ 3 0 obj
10
+ << /Count 1 /Kids [ 4 0 R ] /MediaBox [ 0 0 612 446 ] /Type /Pages >>
11
+ endobj
12
+ 4 0 obj
13
+ << /Contents 5 0 R /Parent 3 0 R /Resources 6 0 R /Type /Page >>
14
+ endobj
15
+ 5 0 obj
16
+ << /Length 80 /Filter /FlateDecode >>
17
+ stream
18
+ \��sy%1����a��
19
+ �/���F�.�L:pb���C�` ��D�5 �� �کU"�ʇ�3�Fy/�9�<V��ڦ;�?5�S�8endstream
20
+ endobj
21
+ 6 0 obj
22
+ << /Font << /F1 7 0 R >> /ProcSet [ /PDF /Text ] >>
23
+ endobj
24
+ 7 0 obj
25
+ << /BaseFont /Helvetica /Name /F1 /Subtype /Type1 /Type /Font >>
26
+ endobj
27
+ 8 0 obj
28
+ << /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >> /Filter /Standard /Length 256 /O <8034f99b1eff9e91054d7ee490155e22f65170f607d6a614236b089e602517d9a22abe7c1ea53f52dbc9ae8d9701a065> /OE <bdc5c4b46519af7b4041b7341f70e4d8b1c6087dc12d3f16f060313f18e73386> /P -4 /Perms <64028d6bceb0d927477ecf45e6be7f3f> /R 5 /StmF /StdCF /StrF /StdCF /U <5db7b7b5bd8bbe9fac28477756a930cb079e1dcbdcd3b50c1817d3de4ce5184b5189848832df0043f8e1fe19ff696a36> /UE <dc3fed7b473148238ba0689409edd3d80c91bd26802003c152594aa27a50a81e> /V 5 >>
29
+ endobj
30
+ xref
31
+ 0 9
32
+ 0000000000 65535 f
33
+ 0000000015 00000 n
34
+ 0000000130 00000 n
35
+ 0000000259 00000 n
36
+ 0000000344 00000 n
37
+ 0000000424 00000 n
38
+ 0000000574 00000 n
39
+ 0000000641 00000 n
40
+ 0000000721 00000 n
41
+ trailer << /Info 2 0 R /Root 1 0 R /Size 9 /ID [<6790ffa610024e78369114311fc0df96><da8e0d03302398724f68ef24a831285e>] /Encrypt 8 0 R >>
42
+ startxref
43
+ 1268
44
+ %%EOF
@@ -0,0 +1,43 @@
1
+ %PDF-1.7
2
+ %����
3
+ 1 0 obj
4
+ << /Extensions << /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> >> /Pages 3 0 R /Type /Catalog >>
5
+ endobj
6
+ 2 0 obj
7
+ << /ModDate <d2aa32b8d87666bd29df499a424d2e6d23c0b475ebb67598733b5f06429470a37062d3d6a32fc9603301c5eb99a20605> >>
8
+ endobj
9
+ 3 0 obj
10
+ << /Count 1 /Kids [ 4 0 R ] /MediaBox [ 0 0 612 446 ] /Type /Pages >>
11
+ endobj
12
+ 4 0 obj
13
+ << /Contents 5 0 R /Parent 3 0 R /Resources 6 0 R /Type /Page >>
14
+ endobj
15
+ 5 0 obj
16
+ << /Length 80 /Filter /FlateDecode >>
17
+ stream
18
+ j��D3�(���"�ڛ�������xvY$B\4^Y����$��
19
+ endobj
20
+ 6 0 obj
21
+ << /Font << /F1 7 0 R >> /ProcSet [ /PDF /Text ] >>
22
+ endobj
23
+ 7 0 obj
24
+ << /BaseFont /Helvetica /Name /F1 /Subtype /Type1 /Type /Font >>
25
+ endobj
26
+ 8 0 obj
27
+ << /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >> /Filter /Standard /Length 256 /O <95d20f6277a6955bf4bda243c2144e94889bd5fa4225a4cf4e0d496fa1ffa1d991e1a37d46e4afe9e2dfc207eba9ec53> /OE <432a7d086b4338f1020bbc5847e6dd4f49ce586ab2c5de0f5301450e45e3bb3e> /P -4 /Perms <7df865105074d365f2ce50407e8b6dc8> /R 5 /StmF /StdCF /StrF /StdCF /U <930a631e8b2b95ff6b024e9bde92cb73c5c43f0106ec4fb1c336e49608c0740d87395c6ea79b99ee07eeae5ffbacd031> /UE <3f7eb6b9a897049bfca85a5ae71470eca3dfedbe9101e8532b217e4d95bcf51a> /V 5 >>
28
+ endobj
29
+ xref
30
+ 0 9
31
+ 0000000000 65535 f
32
+ 0000000015 00000 n
33
+ 0000000130 00000 n
34
+ 0000000259 00000 n
35
+ 0000000344 00000 n
36
+ 0000000424 00000 n
37
+ 0000000574 00000 n
38
+ 0000000641 00000 n
39
+ 0000000721 00000 n
40
+ trailer << /Info 2 0 R /Root 1 0 R /Size 9 /ID [<6790ffa610024e78369114311fc0df96><dd2e6bd65a9735c0bef37d88d5291ff1>] /Encrypt 8 0 R >>
41
+ startxref
42
+ 1268
43
+ %%EOF
@@ -16,13 +16,13 @@ describe HexaPDF::DigitalSignature::Signatures do
16
16
 
17
17
  it "iterates over all signature dictionaries" do
18
18
  assert_equal([], @doc.signatures.to_a)
19
- @sig1.field_value = :sig1
20
- @sig2.field_value = :sig2
21
- assert_equal([:sig1, :sig2], @doc.signatures.to_a)
19
+ @sig1.field_value = {k: :sig1}
20
+ @sig2.field_value = {k: :sig2}
21
+ assert_equal([{k: :sig1}, {k: :sig2}], @doc.signatures.to_a)
22
22
  end
23
23
 
24
24
  it "returns the number of signature dictionaries" do
25
- @sig1.field_value = :sig1
25
+ @sig1.field_value = {k: :sig1}
26
26
  assert_equal(1, @doc.signatures.count)
27
27
  end
28
28
 
@@ -228,11 +228,14 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
228
228
  end
229
229
 
230
230
  it "fails if the /R value is incorrect" do
231
+ HexaPDF::Encryption::StandardEncryptionDictionary.field(:R).allowed_values << 7
231
232
  exp = assert_raises(HexaPDF::UnsupportedEncryptionError) do
232
- @handler.set_up_decryption({Filter: :Standard, V: 2, R: 5, O: 't' * 32, U: 't' * 32, P: 0,
233
+ @handler.set_up_decryption({Filter: :Standard, V: 2, R: 7, O: 't' * 32, U: 't' * 32, P: 0,
233
234
  Length: 128})
234
235
  end
235
- assert_match(/Invalid \/R value 5/i, exp.message)
236
+ assert_match(/Invalid \/R value 7/i, exp.message)
237
+ ensure
238
+ HexaPDF::Encryption::StandardEncryptionDictionary.field(:R).allowed_values.pop
236
239
  end
237
240
 
238
241
  it "fails if the supplied password is invalid" do
@@ -511,6 +511,58 @@ describe HexaPDF::Layout::TableBox do
511
511
  [0, 66, 39.75, 22], [39.75, 66, 39.75, 22], [79.5, 44, 39.75, 44], [119.25, 66, 39.75, 22]])
512
512
  end
513
513
 
514
+ describe "row spans" do
515
+ # ----------
516
+ # | a | b |
517
+ # | a | |
518
+ # | a |----|
519
+ # | a | c |
520
+ # | a | |
521
+ # ----------
522
+ it "works if content of a row span cell is larger than the rows" do
523
+ cells = [[{row_span: 2, content: @fixed_size_boxes[0..2]}, @fixed_size_boxes[3]],
524
+ [@fixed_size_boxes[4]]]
525
+ check_box(create_box(cells: cells, cell_style: {padding: 0, border: {width: 0}}),
526
+ :success, 160, 30,
527
+ [[0, 0, 80, 30], [80, 0, 80, 15], [0, 0, 80, 30], [80, 15, 80, 15]])
528
+ end
529
+
530
+ # ----------
531
+ # | a | b |
532
+ # | |----|
533
+ # | | c |
534
+ # ----------
535
+ it "works if content of a row span cell is smaller than the rows" do
536
+ cells = [[{row_span: 2, content: @fixed_size_boxes[0]}, @fixed_size_boxes[3]],
537
+ [@fixed_size_boxes[4]]]
538
+ check_box(create_box(cells: cells, cell_style: {padding: 0, border: {width: 0}}),
539
+ :success, 160, 20,
540
+ [[0, 0, 80, 20], [80, 0, 80, 10], [0, 0, 80, 20], [80, 10, 80, 10]])
541
+ end
542
+
543
+ # -----------------
544
+ # | a | b | c | d |
545
+ # | a | b |---| d |
546
+ # | a | b | e | |
547
+ # | a | | e | |
548
+ # --------| e | |
549
+ # | f | g | e | |
550
+ # ----------------
551
+ it "works if multiple, possibly overlapping row spans are involved" do
552
+ cells = [[{row_span: 2, content: @fixed_size_boxes[0..2]},
553
+ {row_span: 2, content: @fixed_size_boxes[3..4]},
554
+ @fixed_size_boxes[5],
555
+ {row_span: 3, content: @fixed_size_boxes[6..7]}],
556
+ [{row_span: 2, content: @fixed_size_boxes[8, 3]}],
557
+ [@fixed_size_boxes[11], @fixed_size_boxes[12]]]
558
+ check_box(create_box(cells: cells, cell_style: {padding: 0, border: {width: 0}}),
559
+ :success, 160, 40,
560
+ [[0, 0, 40, 30], [40, 0, 40, 30], [80, 0, 40, 10], [120, 0, 40, 40],
561
+ [0, 0, 40, 30], [40, 0, 40, 30], [80, 10, 40, 30], [120, 0, 40, 40],
562
+ [0, 30, 40, 10], [40, 30, 40, 10], [80, 10, 40, 30], [120, 0, 40, 40]])
563
+ end
564
+ end
565
+
514
566
  it "fits a table with header rows" do
515
567
  result = [[0, 0, 80, 10], [80, 0, 80, 10], [0, 10, 80, 10], [80, 10, 80, 10]]
516
568
  header = lambda {|_| [@fixed_size_boxes[10, 2], @fixed_size_boxes[12, 2]] }
@@ -8,6 +8,7 @@ describe HexaPDF::Task::Optimize do
8
8
  class TestType < HexaPDF::Dictionary
9
9
 
10
10
  define_type :Test
11
+ define_field :Type, type: Symbol, default: type
11
12
  define_field :Optional, type: Symbol, default: :Optional
12
13
 
13
14
  end
@@ -46,6 +47,7 @@ describe HexaPDF::Task::Optimize do
46
47
  end
47
48
 
48
49
  def assert_default_deleted
50
+ assert(@doc.object(1).key?(:Type))
49
51
  refute(@doc.object(1).key?(:Optional))
50
52
  end
51
53
 
@@ -497,10 +497,10 @@ describe HexaPDF::Document do
497
497
 
498
498
  it "returns all signature fields of the document" do
499
499
  form = @doc.acro_form(create: true)
500
- sig1 = @doc.add({FT: :Sig, T: 'sig1', V: :sig1})
501
- sig2 = @doc.add({FT: :Sig, T: 'sig2', V: :sig2})
500
+ sig1 = @doc.add({FT: :Sig, T: 'sig1', V: {k: :sig1}})
501
+ sig2 = @doc.add({FT: :Sig, T: 'sig2', V: {k: :sig2}})
502
502
  form.root_fields << sig1 << sig2
503
- assert_equal([:sig1, :sig2], @doc.signatures.to_a)
503
+ assert_equal([{k: :sig1}, {k: :sig2}], @doc.signatures.to_a)
504
504
  end
505
505
 
506
506
  it "allows to conveniently sign a document" do
@@ -147,6 +147,13 @@ describe HexaPDF::Importer do
147
147
  assert_nil(obj[:pages])
148
148
  end
149
149
 
150
+ it "handles null values correctly" do
151
+ @source.add(@hash)
152
+ @source.delete(@hash)
153
+ obj = @importer.import(@obj)
154
+ assert_nil(obj[:hash])
155
+ end
156
+
150
157
  it "imports Page objects correctly by copying the inherited values" do
151
158
  page = @importer.import(@source.pages[0])
152
159
  assert_equal(90, page[:Rotate])
@@ -64,7 +64,7 @@ describe HexaPDF::Writer do
64
64
  20
65
65
  endobj
66
66
  3 0 obj
67
- <</Type/XRef/Size 6/W[1 1 2]/Index[0 4 5 1]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 31>>stream
67
+ <</Size 6/Type/XRef/W[1 1 2]/Index[0 4 5 1]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 31>>stream
68
68
  x\xDAcb`\xF8\xFF\x9F\x89\x89\x95\x91\x91\xE9\x7F\x19\x03\x03\x13\x83\x10\x88he`\x00\x00B4\x04\x1E
69
69
  endstream
70
70
  endobj
@@ -80,7 +80,7 @@ describe HexaPDF::Writer do
80
80
  endstream
81
81
  endobj
82
82
  4 0 obj
83
- <</Type/XRef/Size 7/Root<</Type/Catalog>>/Info 6 0 R/Prev 141/W[1 2 2]/Index[2 1 4 1 6 1]/Filter/FlateDecode/DecodeParms<</Columns 5/Predictor 12>>/Length 22>>stream
83
+ <</Size 7/Root<</Type/Catalog>>/Info 6 0 R/Prev 141/Type/XRef/W[1 2 2]/Index[2 1 4 1 6 1]/Filter/FlateDecode/DecodeParms<</Columns 5/Predictor 12>>/Length 22>>stream
84
84
  x\xDAcbdlg``b`\xB0\x04\x93\x93\x18\x18\x00\f\e\x01[
85
85
  endstream
86
86
  endobj
@@ -270,4 +270,13 @@ describe HexaPDF::Writer do
270
270
  doc = HexaPDF::Document.new(io: io)
271
271
  refute(doc.trailer.key?(:XRefStm))
272
272
  end
273
+
274
+ it "removes the /Type entry in a non-xref stream trailer" do
275
+ io = StringIO.new
276
+ doc = HexaPDF::Document.new
277
+ doc.trailer[:Type] = :XRef
278
+ doc.write(io)
279
+ doc = HexaPDF::Document.new(io: io)
280
+ refute(doc.trailer.key?(:Type))
281
+ end
273
282
  end
@@ -291,6 +291,22 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
291
291
  assert_equal(:XObject, @widget[:AP][:N][:Other].type)
292
292
  end
293
293
 
294
+ it "uses the field's value or :Yes for the on state if the appearance dictionary doesn't contain a name for it" do
295
+ @widget[:AP][:N].delete(:Yes)
296
+ @generator.create_appearances
297
+ assert_equal(:XObject, @widget[:AP][:N][:Yes].type)
298
+
299
+ @widget[:AP][:N].delete(:Yes)
300
+ @field[:V] = nil
301
+ @generator.create_appearances
302
+ assert_equal(:XObject, @widget[:AP][:N][:Yes].type)
303
+
304
+ @widget[:AP][:N].delete(:Yes)
305
+ @field[:V] = "other" # some PDFs use a string instead of the correct symbol
306
+ @generator.create_appearances
307
+ assert_equal(:XObject, @widget[:AP][:N][:other].type)
308
+ end
309
+
294
310
  it "creates the needed appearance streams" do
295
311
  @widget[:AP][:N].delete(:Off)
296
312
  @generator.create_appearances
@@ -327,11 +343,6 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
327
343
  [:end_text],
328
344
  [:restore_graphics_state]])
329
345
  end
330
-
331
- it "fails if the appearance dictionary doesn't contain a name for the on state" do
332
- @widget[:AP][:N].delete(:Yes)
333
- assert_raises(HexaPDF::Error) { @generator.create_appearances }
334
- end
335
346
  end
336
347
 
337
348
  describe "radio button" do
@@ -441,6 +452,12 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
441
452
  assert_equal(form, @widget[:AP][:N])
442
453
  refute(form.key?(:key))
443
454
  assert_match(/test1/, form.contents)
455
+
456
+ form.delete(:Type)
457
+ @widget[:AP][:N] = @doc.wrap(form, type: HexaPDF::Type::Annotation)
458
+ @field[:V] = 'test2'
459
+ @generator.create_appearances
460
+ assert_match(/test2/, form.contents)
444
461
  end
445
462
 
446
463
  describe "takes the rotation into account" do
@@ -507,11 +507,6 @@ describe HexaPDF::Type::AcroForm::Form do
507
507
  assert(@acro_form.validate)
508
508
  end
509
509
 
510
- it "set the default appearance string, though optional, to a valid value to avoid problems" do
511
- assert(@acro_form.validate)
512
- assert_equal("0.0 g /F1 0 Tf", @acro_form[:DA])
513
- end
514
-
515
510
  describe "field hierarchy validation" do
516
511
  before do
517
512
  @acro_form[:Fields] = [
@@ -31,7 +31,9 @@ describe HexaPDF::Type::AcroForm::SignatureField do
31
31
 
32
32
  it "gets the field value" do
33
33
  @field[:V] = {Empty: :True}
34
- assert_equal({Empty: :True}, @field.field_value.value)
34
+ value = @field.field_value
35
+ assert_kind_of(HexaPDF::DigitalSignature::Signature, value)
36
+ assert_equal({Empty: :True}, value)
35
37
  end
36
38
 
37
39
  it "validates the value of the /FT field" do
@@ -102,9 +102,22 @@ describe HexaPDF::Type::AcroForm::VariableTextField do
102
102
  @field.parse_default_appearance_string)
103
103
  end
104
104
 
105
- it "fails if no /DA value is set" do
105
+ it "sets a standard /DA value if no other /DA is found" do
106
106
  @doc.acro_form.delete(:DA)
107
+ assert_equal([:F1, 0, HexaPDF::Content::ColorSpace.prenormalized_device_color([0])],
108
+ @field.parse_default_appearance_string)
109
+ end
110
+
111
+ it "converts the /DA to a string in case an invalid PDF uses a Symbol" do
112
+ @field[:DA] = :"1 g /F1 20 Tf"
113
+ assert_equal([:F1, 20, @color], @field.parse_default_appearance_string)
114
+ end
115
+
116
+ it "fails if no /DA value is set and no default appearance string should be set" do
117
+ @doc.acro_form.delete(:DA)
118
+ @doc.config['acro_form.fallback_default_appearance'] = nil
107
119
  assert_raises(HexaPDF::Error) { @field.parse_default_appearance_string }
108
120
  end
121
+
109
122
  end
110
123
  end
@@ -194,6 +194,8 @@ describe HexaPDF::Type::Annotations::Widget do
194
194
 
195
195
  it "returns the default size if none is set" do
196
196
  assert_equal(0, @widget.marker_style.size)
197
+ @widget.form_field[:DA] = "0.0 g"
198
+ assert_equal(0, @widget.marker_style.size)
197
199
  end
198
200
 
199
201
  it "sets the given size" do
@@ -212,6 +214,8 @@ describe HexaPDF::Type::Annotations::Widget do
212
214
 
213
215
  it "returns the default color if none is set" do
214
216
  assert_equal([0], @widget.marker_style.color.components)
217
+ @widget.form_field[:DA] = "/ZaDb 10 Tfg"
218
+ assert_equal([0], @widget.marker_style.color.components)
215
219
  end
216
220
 
217
221
  it "sets the given color" do
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.46.0
4
+ version: 0.47.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-08-11 00:00:00.000000000 Z
11
+ date: 2024-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -594,21 +594,25 @@ files:
594
594
  - test/data/minimal.pdf
595
595
  - test/data/standard-security-handler/README
596
596
  - test/data/standard-security-handler/bothpwd-aes-128bit-V4.pdf
597
+ - test/data/standard-security-handler/bothpwd-aes-256bit-V5-R5.pdf
597
598
  - test/data/standard-security-handler/bothpwd-aes-256bit-V5.pdf
598
599
  - test/data/standard-security-handler/bothpwd-arc4-128bit-V2.pdf
599
600
  - test/data/standard-security-handler/bothpwd-arc4-128bit-V4.pdf
600
601
  - test/data/standard-security-handler/bothpwd-arc4-40bit-V1.pdf
601
602
  - test/data/standard-security-handler/nopwd-aes-128bit-V4.pdf
603
+ - test/data/standard-security-handler/nopwd-aes-256bit-V5-R5.pdf
602
604
  - test/data/standard-security-handler/nopwd-aes-256bit-V5.pdf
603
605
  - test/data/standard-security-handler/nopwd-arc4-128bit-V2.pdf
604
606
  - test/data/standard-security-handler/nopwd-arc4-128bit-V4.pdf
605
607
  - test/data/standard-security-handler/nopwd-arc4-40bit-V1.pdf
606
608
  - test/data/standard-security-handler/ownerpwd-aes-128bit-V4.pdf
609
+ - test/data/standard-security-handler/ownerpwd-aes-256bit-V5-R5.pdf
607
610
  - test/data/standard-security-handler/ownerpwd-aes-256bit-V5.pdf
608
611
  - test/data/standard-security-handler/ownerpwd-arc4-128bit-V2.pdf
609
612
  - test/data/standard-security-handler/ownerpwd-arc4-128bit-V4.pdf
610
613
  - test/data/standard-security-handler/ownerpwd-arc4-40bit-V1.pdf
611
614
  - test/data/standard-security-handler/userpwd-aes-128bit-V4.pdf
615
+ - test/data/standard-security-handler/userpwd-aes-256bit-V5-R5.pdf
612
616
  - test/data/standard-security-handler/userpwd-aes-256bit-V5.pdf
613
617
  - test/data/standard-security-handler/userpwd-arc4-128bit-V2.pdf
614
618
  - test/data/standard-security-handler/userpwd-arc4-128bit-V4.pdf