hexapdf 0.45.0 → 0.47.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +120 -47
- data/examples/019-acro_form.rb +5 -0
- data/lib/hexapdf/cli/inspect.rb +5 -0
- data/lib/hexapdf/composer.rb +1 -1
- data/lib/hexapdf/configuration.rb +19 -0
- data/lib/hexapdf/digital_signature/cms_handler.rb +31 -3
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +9 -1
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +5 -1
- data/lib/hexapdf/document/layout.rb +48 -27
- data/lib/hexapdf/document.rb +24 -2
- data/lib/hexapdf/encryption/standard_security_handler.rb +32 -26
- data/lib/hexapdf/importer.rb +15 -5
- data/lib/hexapdf/layout/box.rb +25 -28
- data/lib/hexapdf/layout/frame.rb +1 -1
- data/lib/hexapdf/layout/inline_box.rb +17 -23
- data/lib/hexapdf/layout/list_box.rb +24 -29
- data/lib/hexapdf/layout/page_style.rb +23 -16
- data/lib/hexapdf/layout/style.rb +2 -2
- data/lib/hexapdf/layout/table_box.rb +57 -10
- data/lib/hexapdf/layout/text_box.rb +2 -6
- data/lib/hexapdf/parser.rb +5 -1
- data/lib/hexapdf/revisions.rb +1 -1
- data/lib/hexapdf/stream.rb +3 -3
- data/lib/hexapdf/task/optimize.rb +4 -4
- data/lib/hexapdf/tokenizer.rb +3 -2
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +8 -4
- data/lib/hexapdf/type/acro_form/button_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/field.rb +8 -0
- data/lib/hexapdf/type/acro_form/form.rb +10 -6
- data/lib/hexapdf/type/acro_form/signature_field.rb +2 -1
- data/lib/hexapdf/type/acro_form/text_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/variable_text_field.rb +11 -3
- data/lib/hexapdf/type/annotations/widget.rb +4 -2
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +1 -0
- data/test/data/standard-security-handler/bothpwd-aes-256bit-V5-R5.pdf +43 -0
- data/test/data/standard-security-handler/nopwd-aes-256bit-V5-R5.pdf +44 -0
- data/test/data/standard-security-handler/ownerpwd-aes-256bit-V5-R5.pdf +43 -0
- data/test/data/standard-security-handler/userpwd-aes-256bit-V5-R5.pdf +0 -0
- data/test/hexapdf/digital_signature/common.rb +66 -84
- data/test/hexapdf/digital_signature/signing/test_default_handler.rb +7 -0
- data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +9 -0
- data/test/hexapdf/digital_signature/test_cms_handler.rb +41 -1
- data/test/hexapdf/digital_signature/test_handler.rb +2 -1
- data/test/hexapdf/digital_signature/test_signatures.rb +4 -4
- data/test/hexapdf/document/test_layout.rb +28 -5
- data/test/hexapdf/encryption/test_standard_security_handler.rb +5 -2
- data/test/hexapdf/layout/test_box.rb +12 -5
- data/test/hexapdf/layout/test_frame.rb +12 -2
- data/test/hexapdf/layout/test_inline_box.rb +17 -28
- data/test/hexapdf/layout/test_list_box.rb +5 -5
- data/test/hexapdf/layout/test_page_style.rb +7 -2
- data/test/hexapdf/layout/test_table_box.rb +52 -0
- data/test/hexapdf/layout/test_text_box.rb +3 -9
- data/test/hexapdf/layout/test_text_layouter.rb +0 -3
- data/test/hexapdf/task/test_optimize.rb +2 -0
- data/test/hexapdf/test_document.rb +30 -3
- data/test/hexapdf/test_importer.rb +24 -0
- data/test/hexapdf/test_revisions.rb +54 -41
- data/test/hexapdf/test_writer.rb +11 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +22 -5
- data/test/hexapdf/type/acro_form/test_form.rb +9 -5
- data/test/hexapdf/type/acro_form/test_signature_field.rb +3 -1
- data/test/hexapdf/type/acro_form/test_variable_text_field.rb +14 -1
- data/test/hexapdf/type/annotations/test_widget.rb +4 -0
- 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
|
81
|
-
#
|
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
|
-
|
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
|
499
|
-
#
|
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.
|
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
|
-
|
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
|
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
|
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
|
662
|
-
# necessary to ensure that the style object
|
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
|
-
|
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 || {})))
|
data/lib/hexapdf/document.rb
CHANGED
@@ -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
|
282
|
-
# into another PDF document, you need to run the
|
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
|
-
#
|
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]
|
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
|
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]
|
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
|
431
|
-
# with the owner password. If the password is the user password,
|
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]
|
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]
|
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
|
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
|
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]
|
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
|
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]
|
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]
|
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
|
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
|
-
|
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]
|
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
|
data/lib/hexapdf/importer.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/hexapdf/layout/box.rb
CHANGED
@@ -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
|
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
|
-
#
|
112
|
-
#
|
113
|
-
#
|
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
|
-
|
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
|
498
|
-
#
|
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!
|
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
|
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
|
data/lib/hexapdf/layout/frame.rb
CHANGED
@@ -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
|
-
#
|
47
|
-
#
|
48
|
-
#
|
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
|
-
#
|
51
|
-
#
|
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
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
@fit_result =
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|