hexapdf 0.45.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +120 -47
  3. data/examples/019-acro_form.rb +5 -0
  4. data/lib/hexapdf/cli/inspect.rb +5 -0
  5. data/lib/hexapdf/composer.rb +1 -1
  6. data/lib/hexapdf/configuration.rb +19 -0
  7. data/lib/hexapdf/digital_signature/cms_handler.rb +31 -3
  8. data/lib/hexapdf/digital_signature/signing/default_handler.rb +9 -1
  9. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +5 -1
  10. data/lib/hexapdf/document/layout.rb +48 -27
  11. data/lib/hexapdf/document.rb +24 -2
  12. data/lib/hexapdf/encryption/standard_security_handler.rb +32 -26
  13. data/lib/hexapdf/importer.rb +15 -5
  14. data/lib/hexapdf/layout/box.rb +25 -28
  15. data/lib/hexapdf/layout/frame.rb +1 -1
  16. data/lib/hexapdf/layout/inline_box.rb +17 -23
  17. data/lib/hexapdf/layout/list_box.rb +24 -29
  18. data/lib/hexapdf/layout/page_style.rb +23 -16
  19. data/lib/hexapdf/layout/style.rb +2 -2
  20. data/lib/hexapdf/layout/table_box.rb +57 -10
  21. data/lib/hexapdf/layout/text_box.rb +2 -6
  22. data/lib/hexapdf/parser.rb +5 -1
  23. data/lib/hexapdf/revisions.rb +1 -1
  24. data/lib/hexapdf/stream.rb +3 -3
  25. data/lib/hexapdf/task/optimize.rb +4 -4
  26. data/lib/hexapdf/tokenizer.rb +3 -2
  27. data/lib/hexapdf/type/acro_form/appearance_generator.rb +8 -4
  28. data/lib/hexapdf/type/acro_form/button_field.rb +2 -0
  29. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
  30. data/lib/hexapdf/type/acro_form/field.rb +8 -0
  31. data/lib/hexapdf/type/acro_form/form.rb +10 -6
  32. data/lib/hexapdf/type/acro_form/signature_field.rb +2 -1
  33. data/lib/hexapdf/type/acro_form/text_field.rb +2 -0
  34. data/lib/hexapdf/type/acro_form/variable_text_field.rb +11 -3
  35. data/lib/hexapdf/type/annotations/widget.rb +4 -2
  36. data/lib/hexapdf/version.rb +1 -1
  37. data/lib/hexapdf/writer.rb +1 -0
  38. data/test/data/standard-security-handler/bothpwd-aes-256bit-V5-R5.pdf +43 -0
  39. data/test/data/standard-security-handler/nopwd-aes-256bit-V5-R5.pdf +44 -0
  40. data/test/data/standard-security-handler/ownerpwd-aes-256bit-V5-R5.pdf +43 -0
  41. data/test/data/standard-security-handler/userpwd-aes-256bit-V5-R5.pdf +0 -0
  42. data/test/hexapdf/digital_signature/common.rb +66 -84
  43. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +7 -0
  44. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +9 -0
  45. data/test/hexapdf/digital_signature/test_cms_handler.rb +41 -1
  46. data/test/hexapdf/digital_signature/test_handler.rb +2 -1
  47. data/test/hexapdf/digital_signature/test_signatures.rb +4 -4
  48. data/test/hexapdf/document/test_layout.rb +28 -5
  49. data/test/hexapdf/encryption/test_standard_security_handler.rb +5 -2
  50. data/test/hexapdf/layout/test_box.rb +12 -5
  51. data/test/hexapdf/layout/test_frame.rb +12 -2
  52. data/test/hexapdf/layout/test_inline_box.rb +17 -28
  53. data/test/hexapdf/layout/test_list_box.rb +5 -5
  54. data/test/hexapdf/layout/test_page_style.rb +7 -2
  55. data/test/hexapdf/layout/test_table_box.rb +52 -0
  56. data/test/hexapdf/layout/test_text_box.rb +3 -9
  57. data/test/hexapdf/layout/test_text_layouter.rb +0 -3
  58. data/test/hexapdf/task/test_optimize.rb +2 -0
  59. data/test/hexapdf/test_document.rb +30 -3
  60. data/test/hexapdf/test_importer.rb +24 -0
  61. data/test/hexapdf/test_revisions.rb +54 -41
  62. data/test/hexapdf/test_writer.rb +11 -2
  63. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +22 -5
  64. data/test/hexapdf/type/acro_form/test_form.rb +9 -5
  65. data/test/hexapdf/type/acro_form/test_signature_field.rb +3 -1
  66. data/test/hexapdf/type/acro_form/test_variable_text_field.rb +14 -1
  67. data/test/hexapdf/type/annotations/test_widget.rb +4 -0
  68. metadata +6 -2
@@ -77,9 +77,9 @@ module HexaPDF
77
77
  #
78
78
  # One style property, Layout::Style#font, is handled specially:
79
79
  #
80
- # * If no font is set on a style, the font "Times" is automatically set because otherwise there
81
- # would be problems with text drawing operations (font is the only style property that has no
82
- # valid default value).
80
+ # * If no font is set on a style, the default font specified via the configuration option
81
+ # 'font.default' is automatically set because otherwise there would be problems with text
82
+ # drawing operations (font is the only style property that has no valid default value).
83
83
  #
84
84
  # * Standard style objects only allow font wrapper objects to be set via the Layout::Style#font
85
85
  # method. This class makes usage easier by allowing strings or an array [name, options_hash]
@@ -151,7 +151,9 @@ module HexaPDF
151
151
  # :nodoc:
152
152
  def method_missing(name, *args, **kwargs, &block)
153
153
  if @layout.box_creation_method?(name)
154
- @children << @layout.send(name, *args, **kwargs, &block)
154
+ box = @layout.send(name, *args, **kwargs, &block)
155
+ @children << box
156
+ box
155
157
  else
156
158
  super
157
159
  end
@@ -354,6 +356,7 @@ module HexaPDF
354
356
  width: width, height: height, properties: properties,
355
357
  style: box_style)
356
358
  end
359
+ alias text text_box
357
360
 
358
361
  # Creates a HexaPDF::Layout::TextBox like #text_box but allows parts of the text to be
359
362
  # formatted differently.
@@ -456,6 +459,7 @@ module HexaPDF
456
459
  box_class_for_name(:text).new(items: data, width: width, height: height,
457
460
  properties: properties, style: box_style)
458
461
  end
462
+ alias formatted_text formatted_text_box
459
463
 
460
464
  # Creates a HexaPDF::Layout::ImageBox for the given image.
461
465
  #
@@ -477,6 +481,7 @@ module HexaPDF
477
481
  box_class_for_name(:image).new(image: image, width: width, height: height,
478
482
  properties: properties, style: style)
479
483
  end
484
+ alias image image_box
480
485
 
481
486
  # This helper class is used by Layout#table_box to allow specifying the keyword arguments used
482
487
  # when converting cell data to box instances.
@@ -495,8 +500,25 @@ module HexaPDF
495
500
  @number_of_columns = number_of_columns
496
501
  end
497
502
 
498
- # Stores the keyword arguments in +args+ for the given 0-based rows and columns which can
499
- # either be a single number or a range of numbers.
503
+ # Stores the hash +args+ containing styling properties for the cells specified via the given
504
+ # 0-based rows and columns.
505
+ #
506
+ # Rows and columns can either be single numbers, ranges of numbers or stepped ranges (i.e.
507
+ # Enumerator::ArithmeticSequence instances).
508
+ #
509
+ # Examples:
510
+ #
511
+ # # Gray background for all cells
512
+ # args[] = {cell: {background_color: "gray"}}
513
+ #
514
+ # # Cell at (2, 3) gets a bigger font size
515
+ # args[2, 3] = {font_size: 50}
516
+ #
517
+ # # First column of every row has bold font
518
+ # args[0..-1, 0] = {font: 'Helvetica bold'}
519
+ #
520
+ # # Every second row has a blue background
521
+ # args[(0..-1).step(2)] = {cell: {background_color: "blue"}}
500
522
  def []=(rows = 0..-1, cols = 0..-1, args)
501
523
  rows = adjust_range(rows.kind_of?(Integer) ? rows..rows : rows, @number_of_rows)
502
524
  cols = adjust_range(cols.kind_of?(Integer) ? cols..cols : cols, @number_of_columns)
@@ -509,7 +531,7 @@ module HexaPDF
509
531
  # is merged.
510
532
  def retrieve_arguments_for(row, col)
511
533
  @argument_infos.each_with_object({}) do |arg_info, result|
512
- next unless arg_info.rows.cover?(row) && arg_info.cols.cover?(col)
534
+ next unless arg_info.rows.include?(row) && arg_info.cols.include?(col)
513
535
  if arg_info.args[:cell]
514
536
  arg_info.args[:cell] = (result[:cell] || {}).merge(arg_info.args[:cell])
515
537
  end
@@ -522,7 +544,8 @@ module HexaPDF
522
544
  # Adjusts the +range+ so that both the begin and the end of the range are zero or positive
523
545
  # integers smaller than +max+.
524
546
  def adjust_range(range, max)
525
- (range.begin % max)..(range.end % max)
547
+ r = (range.begin % max)..(range.end % max)
548
+ range.kind_of?(Range) ? r : r.step(range.step)
526
549
  end
527
550
 
528
551
  end
@@ -540,7 +563,8 @@ module HexaPDF
540
563
  # Additional arguments for the #text_box invocations can be specified using the optional block
541
564
  # that yields a CellArgumentCollector instance. This allows customization of the text boxes.
542
565
  # By specifying the special key +:cell+ it is also possible to assign style properties to the
543
- # cells themselves.
566
+ # cells themselves, irrespective of the type of content of the cells. See
567
+ # CellArgumentCollector#[]= for details.
544
568
  #
545
569
  # See HexaPDF::Layout::TableBox::new for details on +column_widths+, +header+, +footer+, and
546
570
  # +cell_style+.
@@ -586,6 +610,7 @@ module HexaPDF
586
610
  footer: footer, cell_style: cell_style, width: width,
587
611
  height: height, properties: properties, style: style)
588
612
  end
613
+ alias table table_box
589
614
 
590
615
  LOREM_IPSUM = [ # :nodoc:
591
616
  "Lorem ipsum dolor sit amet, con\u{00AD}sectetur adipis\u{00AD}cing elit, sed " \
@@ -605,22 +630,13 @@ module HexaPDF
605
630
  def lorem_ipsum_box(sentences: 4, count: 1, **text_box_properties)
606
631
  text_box(([LOREM_IPSUM[0, sentences].join(" ")] * count).join("\n\n"), **text_box_properties)
607
632
  end
633
+ alias lorem_ipsum lorem_ipsum_box
608
634
 
609
- BOX_METHOD_NAMES = [:text, :formatted_text, :image, :table, :lorem_ipsum] #:nodoc:
610
-
611
- # Allows creating boxes using more convenient method names:
612
- #
613
- # * #text for #text_box
614
- # * #formatted_text for #formatted_text_box
615
- # * #image for #image_box
616
- # * #lorem_ipsum for #lorem_ipsum_box
617
- # * The name of a pre-defined box class like #column will invoke #box appropriately. Same if
618
- # used with a '_box' suffix.
635
+ # Allows creating boxes using more convenient method names: The name of a pre-defined box
636
+ # class like #column will invoke #box appropriately. Same if used with a '_box' suffix.
619
637
  def method_missing(name, *args, **kwargs, &block)
620
638
  name_without_box = name.to_s.sub(/_box$/, '').intern
621
- if BOX_METHOD_NAMES.include?(name)
622
- send("#{name}_box", *args, **kwargs, &block)
623
- elsif @document.config['layout.boxes.map'].key?(name_without_box)
639
+ if @document.config['layout.boxes.map'].key?(name_without_box)
624
640
  box(name_without_box, *args, **kwargs, &block)
625
641
  else
626
642
  super
@@ -632,6 +648,8 @@ module HexaPDF
632
648
  box_creation_method?(name) || super
633
649
  end
634
650
 
651
+ BOX_METHOD_NAMES = [:text, :formatted_text, :image, :table, :lorem_ipsum] #:nodoc:
652
+
635
653
  # :nodoc:
636
654
  def box_creation_method?(name)
637
655
  name = name.to_s.sub(/_box$/, '').intern
@@ -648,8 +666,8 @@ module HexaPDF
648
666
  end
649
667
  end
650
668
 
651
- # Retrieves the appropriate HexaPDF::Layout::Style object based on the +style+ and +properties+
652
- # arguments.
669
+ # Retrieves the appropriate HexaPDF::Layout::Style object based on the +style+ and
670
+ # +properties+ arguments.
653
671
  #
654
672
  # The +style+ argument specifies the style to retrieve. It can either be a registered style
655
673
  # name (see #style), a hash with style properties or +nil+. In the latter case the registered
@@ -658,15 +676,18 @@ module HexaPDF
658
676
  # If the +properties+ hash is not empty, the retrieved style is duplicated and the properties
659
677
  # hash is applied to it.
660
678
  #
661
- # Finally, a default font (the one from the :base style or otherwise 'Times') is set if
662
- # necessary to ensure that the style object works in all cases.
679
+ # Finally, a default font (the one from the :base style or otherwise the one set using the
680
+ # configuration option 'font.default') is set if necessary to ensure that the style object
681
+ # works in all cases.
663
682
  def retrieve_style(style, properties = nil)
664
683
  if style.kind_of?(Symbol) && !@styles.key?(style)
665
684
  raise HexaPDF::Error, "Style #{style} not defined"
666
685
  end
667
686
  style = HexaPDF::Layout::Style.create(@styles[style] || style || @styles[:base])
668
687
  style = style.dup.update(**properties) unless properties.nil? || properties.empty?
669
- style.font(@styles[:base].font? && @styles[:base].font || 'Times') unless style.font?
688
+ unless style.font?
689
+ style.font(@styles[:base].font? && @styles[:base].font || @document.config['font.default'])
690
+ end
670
691
  unless style.font.respond_to?(:pdf_object)
671
692
  name, options = *style.font
672
693
  style.font(@document.fonts.add(name, **(options || {})))
@@ -278,8 +278,9 @@ module HexaPDF
278
278
  # If the same argument is provided in multiple invocations, the import is done only once and
279
279
  # the previously imported object is returned.
280
280
  #
281
- # Note: If you first create a PDF document from scratch and then want to import objects from it
282
- # into another PDF document, you need to run the following on the source document:
281
+ # Note: If you first create a PDF document from scratch or if you modify an existing document,
282
+ # and then want to import objects from it into another PDF document, you need to run the
283
+ # following on the source document:
283
284
  #
284
285
  # doc.dispatch_message(:complete_objects)
285
286
  # doc.validate
@@ -701,6 +702,27 @@ module HexaPDF
701
702
  result
702
703
  end
703
704
 
705
+ # Returns an in-memory copy of the PDF document.
706
+ #
707
+ # In the context of this method this means that the returned PDF document contains the same PDF
708
+ # object tree as this document, starting at the trailer. A possibly set encryption is not
709
+ # transferred to the returned document.
710
+ #
711
+ # Note: If this PDF document was created from scratch or if it is an existing document that was
712
+ # modified, the following commands need to be run on this document beforehand:
713
+ #
714
+ # doc.dispatch_message(:complete_objects)
715
+ # doc.validate
716
+ #
717
+ # This ensures that all the necessary PDF structures set-up correctly.
718
+ def duplicate
719
+ dest = HexaPDF::Document.new
720
+ dupped_trailer = HexaPDF::Importer.copy(dest, trailer, allow_all: true)
721
+ dest.revisions.current.trailer.value.replace(dupped_trailer.value)
722
+ dest.trailer.delete(:Encrypt)
723
+ dest
724
+ end
725
+
704
726
  # :call-seq:
705
727
  # doc.write(filename, incremental: false, validate: true, update_fields: true, optimize: false)
706
728
  # doc.write(io, incremental: false, validate: true, update_fields: true, optimize: false)
@@ -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
@@ -71,14 +71,18 @@ module HexaPDF
71
71
  # Imports the given +object+ (belonging to the +source+ document) by completely copying it and
72
72
  # all referenced objects into the +destination+ object.
73
73
  #
74
+ # If the +allow_all+ argument is set to +true+, then the usually omitted catalog and page tree
75
+ # node objects (see the class description for details) are also copied which allows one to make
76
+ # an in-memory duplicate of a HexaPDF::Document object.
77
+ #
74
78
  # Specifying +source+ is optionial if it can be determined through +object+.
75
79
  #
76
80
  # After the operation is finished, all state is discarded. This means that another call to this
77
81
  # method for the same object will yield a new - and different - object. This is in contrast to
78
82
  # using ::for together with #import which remembers and returns already imported objects (which
79
83
  # is generally what one wants).
80
- def self.copy(destination, object, source: nil)
81
- new(NullableWeakRef.new(destination)).import(object, source: source)
84
+ def self.copy(destination, object, allow_all: false, source: nil)
85
+ new(NullableWeakRef.new(destination), allow_all: allow_all).import(object, source: source)
82
86
  end
83
87
 
84
88
  private_class_method :new
@@ -86,9 +90,10 @@ module HexaPDF
86
90
  attr_reader :destination #:nodoc:
87
91
 
88
92
  # Initializes a new importer that can import objects to the +destination+ document.
89
- def initialize(destination)
93
+ def initialize(destination, allow_all: false)
90
94
  @destination = destination
91
95
  @mapper = {}
96
+ @allow_all = allow_all
92
97
  end
93
98
 
94
99
  SourceWrapper = Struct.new(:source) #:nodoc:
@@ -136,7 +141,7 @@ module HexaPDF
136
141
  internal_import(wrapper.source.object(object), wrapper)
137
142
  when HexaPDF::Object
138
143
  wrapper.source ||= object.document
139
- if object.type == :Catalog || object.type == :Pages
144
+ if object.null? || (!@allow_all && (object.type == :Catalog || object.type == :Pages))
140
145
  @mapper[object.data] = nil
141
146
  elsif (mapped_object = @mapper[object.data]&.__getobj__) && !mapped_object.null?
142
147
  mapped_object
@@ -149,7 +154,12 @@ module HexaPDF
149
154
  obj.data.gen = 0
150
155
  @destination.add(obj) if object.indirect?
151
156
 
152
- obj.data.stream = obj.data.stream.dup if obj.data.stream.kind_of?(String)
157
+ stream = obj.data.stream
158
+ if stream.kind_of?(String)
159
+ obj.data.stream = stream.dup
160
+ elsif stream&.source.kind_of?(FiberDoubleForString)
161
+ obj.data.stream = stream.fiber.resume.dup
162
+ end
153
163
  obj.data.value = duplicate(obj.data.value, wrapper)
154
164
  obj.data.value.update(duplicate(object.copy_inherited_values, wrapper)) if object.type == :Page
155
165
  obj
@@ -69,6 +69,10 @@ module HexaPDF
69
69
  # If the subclass supports the value :flow of the 'position' style property, this method
70
70
  # needs to be overridden to return +true+.
71
71
  #
72
+ # Additionally, if a box object uses flow positioning, #fit_result.x should be set to the
73
+ # correct value since Frame#fit can't determine this and uses Frame#left in the absence of a
74
+ # set value.
75
+ #
72
76
  # #empty?::
73
77
  # This method should return +true+ if the subclass won't draw anything when #draw is called.
74
78
  #
@@ -89,28 +93,13 @@ module HexaPDF
89
93
  # This method draws the box specific content and is called from #draw which already handles
90
94
  # things like drawing the border and background. So #draw should usually not be overridden.
91
95
  #
92
- # This base class provides various private helper methods for use in the above methods:
93
- #
94
- # +reserved_width+, +reserved_height+::
95
- # Returns the width respectively the height of the reserved space inside the box that is
96
- # used for the border and padding.
97
- #
98
- # +reserved_width_left+, +reserved_width_right+, +reserved_height_top+,
99
- # +reserved_height_bottom+::
100
- # Returns the reserved space inside the box at the specified edge (left, right, top,
101
- # bottom).
102
- #
103
- # +update_content_width+, +update_content_height+::
104
- # Takes a block that should return the content width respectively height and sets the box's
105
- # width respectively height accordingly.
106
- #
107
- # +create_split_box+::
108
- # Creates a new box based on this one and resets the internal data back to their original
109
- # values.
96
+ # This base class also provides various protected helper methods for use in the above methods:
110
97
  #
111
- # The keyword argument +split_box_value+ (defaults to +true+) is used to set the
112
- # +@split_box+ variable to make the new box aware that it is a split box. This can be set to
113
- # any other truthy value to convey more meaning.
98
+ # * #reserved_width, #reserved_height
99
+ # * #reserved_width_left, #reserved_width_right, #reserved_height_top,
100
+ # #reserved_height_bottom
101
+ # * #update_content_width, #update_content_height
102
+ # * #create_split_box
114
103
  class Box
115
104
 
116
105
  include HexaPDF::Utils
@@ -352,7 +341,9 @@ module HexaPDF
352
341
  available_height
353
342
  end
354
343
  return @fit_result if !position_flow && (float_compare(@width, available_width) > 0 ||
355
- float_compare(@height, available_height) > 0)
344
+ float_compare(@height, available_height) > 0 ||
345
+ @width - reserved_width < 0 ||
346
+ @height - reserved_height < 0)
356
347
 
357
348
  fit_content(available_width, available_height, frame)
358
349
 
@@ -432,7 +423,7 @@ module HexaPDF
432
423
  (style.overlays? && !style.overlays.none?))
433
424
  end
434
425
 
435
- private
426
+ protected
436
427
 
437
428
  # Returns the width that is reserved by the padding and border style properties.
438
429
  def reserved_width
@@ -480,12 +471,18 @@ module HexaPDF
480
471
  result
481
472
  end
482
473
 
474
+ # :call-seq:
475
+ # update_content_width { block }
476
+ #
483
477
  # Updates the width of the box using the content width returned by the block.
484
478
  def update_content_width
485
479
  return if @initial_width > 0
486
480
  @width = yield + reserved_width
487
481
  end
488
482
 
483
+ # :call-seq:
484
+ # update_content_height { block }
485
+ #
489
486
  # Updates the height of the box using the content height returned by the block.
490
487
  def update_content_height
491
488
  return if @initial_height > 0
@@ -494,13 +491,12 @@ module HexaPDF
494
491
 
495
492
  # Fits the content of the box and returns whether fitting was successful.
496
493
  #
497
- # This is just a stub implementation that sets the #fit_result status to success if the
498
- # content rectangle is not degenerate. Subclasses should override it to provide the box
499
- # specific behaviour.
494
+ # This is just a stub implementation that sets the #fit_result status to success. Subclasses
495
+ # should override it to provide the box specific behaviour.
500
496
  #
501
497
  # See #fit for details.
502
498
  def fit_content(_available_width, _available_height, _frame)
503
- fit_result.success! if content_width > 0 && content_height > 0
499
+ fit_result.success!
504
500
  end
505
501
 
506
502
  # Splits the content of the box.
@@ -525,7 +521,8 @@ module HexaPDF
525
521
  end
526
522
  end
527
523
 
528
- # Creates a new box based on this one and resets the data back to their original values.
524
+ # Creates a new box based on this one and resets the internal data back to their original
525
+ # values.
529
526
  #
530
527
  # The variable +@split_box+ is set to +split_box_value+ (defaults to +true+) to make the new
531
528
  # box aware that it is a split box. If needed, subclasses can set the variable to other truthy
@@ -252,7 +252,7 @@ module HexaPDF
252
252
  end
253
253
  end
254
254
  when :flow
255
- x = 0
255
+ x = fit_result.x || left
256
256
  y = @y - height
257
257
  else
258
258
  raise HexaPDF::Error, "Invalid value '#{position}' for style property position"
@@ -43,14 +43,12 @@ module HexaPDF
43
43
  # An InlineBox wraps a regular Box so that it can be used as an item for a Line. This enables
44
44
  # inline graphics.
45
45
  #
46
- # Complete box auto-sizing is not possible since the available space cannot be determined
47
- # beforehand! This means the box *must* have at least its width set. The height may either also
48
- # be set or determined during fitting.
46
+ # When an inline box gets placed on a line, the method #fit_wrapped_box is called to fit the
47
+ # wrapped box. This allows the wrapped box to correctly set its width and height which are
48
+ # needed by the TextLayouter algorithm.
49
49
  #
50
- # Fitting of the wrapped box via #fit_wrapped_box needs to be done before accessing any other
51
- # method that uses the wrapped box. For fitting, a frame is used that has the width of the
52
- # wrapped box and its height, or if not set, a practically infinite height. In the latter case
53
- # the height *must* be set during fitting.
50
+ # Note: It is *mandatory* that the wrapped box sets its width and height without relying on the
51
+ # dimensions of the frame's current region.
54
52
  class InlineBox
55
53
 
56
54
  # Creates an InlineBox that wraps a basic Box. All arguments (except +valign+) and the block
@@ -74,7 +72,6 @@ module HexaPDF
74
72
  # The +valign+ argument can be used to specify the vertical alignment of the box relative to
75
73
  # other items in the Line.
76
74
  def initialize(box, valign: :baseline)
77
- raise HexaPDF::Error, "Width of box not set" if box.width == 0
78
75
  @box = box
79
76
  @valign = valign
80
77
  end
@@ -102,8 +99,7 @@ module HexaPDF
102
99
  # Draws the wrapped box. If the box has margins specified, the x and y offsets are correctly
103
100
  # adjusted.
104
101
  def draw(canvas, x, y)
105
- canvas.translate(x - @fit_result.x + box.style.margin.left,
106
- y - @fit_result.y + box.style.margin.bottom) { @fit_result.draw(canvas) }
102
+ @fit_result.draw(canvas, dx: x, dy: y)
107
103
  end
108
104
 
109
105
  # The minimum x-coordinate which is always 0.
@@ -129,19 +125,17 @@ module HexaPDF
129
125
  # Fits the wrapped box.
130
126
  #
131
127
  # If the +frame+ argument is +nil+, a custom frame is created. Otherwise the given +frame+ is
132
- # used for the fitting operation.
133
- def fit_wrapped_box(frame)
134
- frame = if frame
135
- frame.child_frame(0, 0, box.width, box.height == 0 ? 100_000 : box.height)
136
- else
137
- Frame.new(0, 0, box.width, box.height == 0 ? 100_000 : box.height)
138
- end
139
- @fit_result = frame.fit(box)
140
- if !@fit_result.success?
141
- raise HexaPDF::Error, "Box for inline use could not be fit"
142
- elsif box.height > 99_000
143
- raise HexaPDF::Error, "Box for inline use has no valid height set after fitting"
144
- end
128
+ # used for creating an appropriate child frame for the fitting operation.
129
+ #
130
+ # After this operation the caller is responsible for checking the actual width and height of
131
+ # the inline box and whether it really fits.
132
+ def fit_wrapped_box(frame = nil)
133
+ @fit_result = box.fit(100_000, 100_000, frame || Frame.new(0, 0, 100_000, 100_000))
134
+ margin = box.style.margin if box.style.margin?
135
+ @fit_result.x = margin&.left.to_i
136
+ @fit_result.y = margin&.bottom.to_i
137
+ @fit_result.mask = Geom2D::Rectangle(0, 0, @fit_result.x + box.width + margin&.right.to_i,
138
+ @fit_result.y + box.height + margin&.top.to_i)
145
139
  end
146
140
 
147
141
  end