hexapdf 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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