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.
- 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
|