hexapdf 0.27.0 → 0.29.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -11
  3. data/examples/019-acro_form.rb +14 -3
  4. data/examples/023-images.rb +30 -0
  5. data/examples/024-digital-signatures.rb +23 -0
  6. data/lib/hexapdf/cli/info.rb +5 -1
  7. data/lib/hexapdf/cli/inspect.rb +2 -2
  8. data/lib/hexapdf/cli/split.rb +2 -2
  9. data/lib/hexapdf/configuration.rb +13 -14
  10. data/lib/hexapdf/content/canvas.rb +8 -3
  11. data/lib/hexapdf/dictionary.rb +1 -5
  12. data/lib/hexapdf/dictionary_fields.rb +6 -2
  13. data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
  14. data/lib/hexapdf/digital_signature/handler.rb +138 -0
  15. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
  16. data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
  17. data/lib/hexapdf/digital_signature/signatures.rb +210 -0
  18. data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
  19. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
  20. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
  21. data/lib/hexapdf/digital_signature/signing.rb +101 -0
  22. data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
  23. data/lib/hexapdf/digital_signature.rb +56 -0
  24. data/lib/hexapdf/document.rb +27 -24
  25. data/lib/hexapdf/encryption/standard_security_handler.rb +2 -1
  26. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  27. data/lib/hexapdf/importer.rb +32 -27
  28. data/lib/hexapdf/layout/list_box.rb +1 -5
  29. data/lib/hexapdf/object.rb +5 -0
  30. data/lib/hexapdf/parser.rb +13 -0
  31. data/lib/hexapdf/revision.rb +15 -12
  32. data/lib/hexapdf/revisions.rb +4 -0
  33. data/lib/hexapdf/tokenizer.rb +14 -8
  34. data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
  35. data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
  36. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
  37. data/lib/hexapdf/type/acro_form/field.rb +11 -5
  38. data/lib/hexapdf/type/acro_form/form.rb +33 -7
  39. data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
  40. data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
  41. data/lib/hexapdf/type/annotations/widget.rb +3 -0
  42. data/lib/hexapdf/type/font_true_type.rb +14 -0
  43. data/lib/hexapdf/type/object_stream.rb +2 -2
  44. data/lib/hexapdf/type/outline.rb +1 -1
  45. data/lib/hexapdf/type/page.rb +56 -46
  46. data/lib/hexapdf/type.rb +0 -1
  47. data/lib/hexapdf/version.rb +1 -1
  48. data/lib/hexapdf/writer.rb +2 -3
  49. data/test/hexapdf/content/test_canvas.rb +5 -0
  50. data/test/hexapdf/{type/signature → digital_signature}/common.rb +34 -4
  51. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
  52. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
  53. data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
  54. data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
  55. data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
  56. data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
  57. data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
  58. data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
  59. data/test/hexapdf/digital_signature/test_signing.rb +53 -0
  60. data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
  61. data/test/hexapdf/document/test_pages.rb +2 -2
  62. data/test/hexapdf/encryption/test_aes.rb +1 -1
  63. data/test/hexapdf/filter/test_predictor.rb +0 -1
  64. data/test/hexapdf/layout/test_box.rb +2 -1
  65. data/test/hexapdf/layout/test_column_box.rb +1 -1
  66. data/test/hexapdf/layout/test_list_box.rb +1 -1
  67. data/test/hexapdf/test_dictionary_fields.rb +2 -1
  68. data/test/hexapdf/test_document.rb +3 -9
  69. data/test/hexapdf/test_importer.rb +13 -6
  70. data/test/hexapdf/test_parser.rb +17 -0
  71. data/test/hexapdf/test_revision.rb +15 -14
  72. data/test/hexapdf/test_revisions.rb +43 -0
  73. data/test/hexapdf/test_stream.rb +1 -1
  74. data/test/hexapdf/test_tokenizer.rb +3 -4
  75. data/test/hexapdf/test_writer.rb +3 -3
  76. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
  77. data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
  78. data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
  79. data/test/hexapdf/type/acro_form/test_field.rb +4 -4
  80. data/test/hexapdf/type/acro_form/test_form.rb +18 -0
  81. data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
  82. data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
  83. data/test/hexapdf/type/test_font_true_type.rb +20 -0
  84. data/test/hexapdf/type/test_object_stream.rb +2 -1
  85. data/test/hexapdf/type/test_outline.rb +3 -0
  86. data/test/hexapdf/type/test_page.rb +67 -30
  87. data/test/hexapdf/type/test_page_tree_node.rb +4 -2
  88. metadata +69 -16
  89. data/lib/hexapdf/document/signatures.rb +0 -546
  90. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
  91. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
  92. data/lib/hexapdf/type/signature/handler.rb +0 -140
  93. data/test/hexapdf/document/test_signatures.rb +0 -352
@@ -0,0 +1,148 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2022 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'openssl'
38
+ require 'net/http'
39
+ require 'hexapdf/error'
40
+ require 'stringio'
41
+
42
+ module HexaPDF
43
+ module DigitalSignature
44
+ module Signing
45
+
46
+ # This is a signing handler for adding a timestamp signature (a PDF2.0 feature) to a PDF
47
+ # document. It is registered under the :timestamp name.
48
+ #
49
+ # The timestamp is provided by a timestamp authority and establishes the document contents at
50
+ # the time indicated in the timestamp. Timestamping a PDF document is usually done in context
51
+ # of long term validation but can also be done standalone.
52
+ #
53
+ # == Usage
54
+ #
55
+ # It is necessary to provide at least the URL of the timestamp authority server (TSA) via
56
+ # #tsa_url, everything else is optional and uses default values. The TSA server must not use
57
+ # authentication to be usable.
58
+ #
59
+ # Example:
60
+ #
61
+ # document.sign("output.pdf", handler: :timestamp, tsa_url: 'https://freetsa.org/tsr')
62
+ class TimestampHandler
63
+
64
+ # The URL of the timestamp authority server.
65
+ #
66
+ # This value is required.
67
+ attr_accessor :tsa_url
68
+
69
+ # The hash algorithm to use for timestamping. Defaults to SHA512.
70
+ attr_accessor :tsa_hash_algorithm
71
+
72
+ # The policy OID to use for timestamping. Defaults to +nil+.
73
+ attr_accessor :tsa_policy_id
74
+
75
+ # The size of the serialized signature that should be reserved.
76
+ #
77
+ # If this attribute has not been set, an empty string will be signed using #sign to
78
+ # determine the signature size which will contact the TSA server
79
+ #
80
+ # The size needs to be at least as big as the final signature, otherwise signing results in
81
+ # an error.
82
+ attr_writer :signature_size
83
+
84
+ # The reason for timestamping. If used, will be set on the signature object.
85
+ attr_accessor :reason
86
+
87
+ # The timestamping location. If used, will be set on the signature object.
88
+ attr_accessor :location
89
+
90
+ # The contact information. If used, will be set on the signature object.
91
+ attr_accessor :contact_info
92
+
93
+ # Creates a new TimestampHandler with the given attributes.
94
+ def initialize(**arguments)
95
+ @signature_size = nil
96
+ arguments.each {|name, value| send("#{name}=", value) }
97
+ end
98
+
99
+ # Returns the size of the serialized signature that should be reserved.
100
+ def signature_size
101
+ @signature_size || (sign(StringIO.new, [0, 0, 0, 0]).size * 1.5).to_i
102
+ end
103
+
104
+ # Finalizes the signature field as well as the signature dictionary before writing.
105
+ def finalize_objects(_signature_field, signature)
106
+ signature.document.version = '2.0'
107
+ signature[:Type] = :DocTimeStamp
108
+ signature[:Filter] = :'Adobe.PPKLite'
109
+ signature[:SubFilter] = :'ETSI.RFC3161'
110
+ signature[:Reason] = reason if reason
111
+ signature[:Location] = location if location
112
+ signature[:ContactInfo] = contact_info if contact_info
113
+ end
114
+
115
+ # Returns the DER serialized OpenSSL::PKCS7 structure containing the timestamp token for the
116
+ # given IO byte ranges.
117
+ def sign(io, byte_range)
118
+ hash_algorithm = tsa_hash_algorithm || 'SHA512'
119
+ digest = OpenSSL::Digest.new(hash_algorithm)
120
+ io.pos = byte_range[0]
121
+ digest << io.read(byte_range[1])
122
+ io.pos = byte_range[2]
123
+ digest << io.read(byte_range[3])
124
+
125
+ req = OpenSSL::Timestamp::Request.new
126
+ req.algorithm = hash_algorithm
127
+ req.message_imprint = digest.digest
128
+ req.policy_id = tsa_policy_id if tsa_policy_id
129
+
130
+ http_response = Net::HTTP.post(URI(tsa_url), req.to_der,
131
+ 'content-type' => 'application/timestamp-query')
132
+ if http_response.kind_of?(Net::HTTPOK)
133
+ response = OpenSSL::Timestamp::Response.new(http_response.body)
134
+ if response.status == 0
135
+ response.token.to_der
136
+ else
137
+ raise HexaPDF::Error, "Timestamp token could not be created: #{response.failure_info}"
138
+ end
139
+ else
140
+ raise HexaPDF::Error, "Invalid TSA server response: #{http_response.body}"
141
+ end
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,101 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2022 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'hexapdf/document'
38
+
39
+ module HexaPDF
40
+ module DigitalSignature
41
+
42
+ # This module contains everything related to the signing of a PDF document, i.e. signing
43
+ # handlers and the actual code for signing.
44
+ module Signing
45
+
46
+ autoload(:DefaultHandler, 'hexapdf/digital_signature/signing/default_handler')
47
+ autoload(:TimestampHandler, 'hexapdf/digital_signature/signing/timestamp_handler')
48
+ autoload(:SignedDataCreator, 'hexapdf/digital_signature/signing/signed_data_creator')
49
+
50
+ # Embeds the given +signature+ into the /Contents value of the newest signature dictionary of
51
+ # the PDF document given by the +io+ argument.
52
+ #
53
+ # This functionality can be used together with the support for external signing (see
54
+ # DefaultHandler and DefaultHandler#external_signing) to implement asynchronous signing.
55
+ #
56
+ # Note: This will, most probably, only work on documents prepared for external signing by
57
+ # HexaPDF and not by other libraries.
58
+ def self.embed_signature(io, signature)
59
+ doc = HexaPDF::Document.new(io: io)
60
+ signature_dict = doc.signatures.find {|sig| doc.revisions.current.object(sig) == sig }
61
+ signature_dict_offset, signature_dict_length = locate_signature_dict(
62
+ doc.revisions.current.xref_section,
63
+ doc.revisions.parser.startxref_offset,
64
+ signature_dict.oid
65
+ )
66
+ io.pos = signature_dict_offset
67
+ signature_data = io.read(signature_dict_length)
68
+ replace_signature_contents(signature_data, signature)
69
+ io.pos = signature_dict_offset
70
+ io.write(signature_data)
71
+ end
72
+
73
+ # Uses the information in the given cross-reference section as well as the byte offset of the
74
+ # cross-reference section to calculate the offset and length of the signature dictionary with
75
+ # the given object id.
76
+ def self.locate_signature_dict(xref_section, start_xref_position, signature_oid)
77
+ data = xref_section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort <<
78
+ [start_xref_position, nil]
79
+ index = data.index {|_pos, oid| oid == signature_oid }
80
+ [data[index][0], data[index + 1][0] - data[index][0]]
81
+ end
82
+
83
+ # Replaces the value of the /Contents key in the serialized +signature_data+ with the value of
84
+ # +contents+.
85
+ def self.replace_signature_contents(signature_data, contents)
86
+ signature_data.sub!(/Contents(?:\(.*?\)|<.*?>)/) do |match|
87
+ length = match.size
88
+ result = "Contents<#{contents.unpack1('H*')}"
89
+ if length < result.size
90
+ raise HexaPDF::Error, "The reserved space for the signature was too small " \
91
+ "(#{(length - 10) / 2} vs #{(result.size - 10) / 2}) - use the handlers " \
92
+ "#signature_size method to increase the reserved space"
93
+ end
94
+ "#{result.ljust(length - 1, '0')}>"
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ end
101
+ end
@@ -34,59 +34,55 @@
34
34
  # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
35
  #++
36
36
 
37
- require 'hexapdf/type/signature'
38
-
39
37
  module HexaPDF
40
- module Type
41
- class Signature
38
+ module DigitalSignature
42
39
 
43
- # Holds the result information when verifying a signature.
44
- class VerificationResult
40
+ # Holds the result information when verifying a signature.
41
+ class VerificationResult
45
42
 
46
- # :nodoc:
47
- MESSAGE_SORT_MAP = {
48
- info: {warning: 1, error: 1, info: 0},
49
- warning: {info: -1, error: 1, warning: 0},
50
- error: {info: -1, warning: -1, error: 0},
51
- }
43
+ # :nodoc:
44
+ MESSAGE_SORT_MAP = {
45
+ info: {warning: 1, error: 1, info: 0},
46
+ warning: {info: -1, error: 1, warning: 0},
47
+ error: {info: -1, warning: -1, error: 0},
48
+ }
52
49
 
53
- # This structure represents a single status message, containing the type (:info, :warning,
54
- # :error) and the content of the message.
55
- Message = Struct.new(:type, :content) do
56
- def <=>(other)
57
- MESSAGE_SORT_MAP[type][other.type]
58
- end
50
+ # This structure represents a single status message, containing the type (:info, :warning,
51
+ # :error) and the content of the message.
52
+ Message = Struct.new(:type, :content) do
53
+ def <=>(other)
54
+ MESSAGE_SORT_MAP[type][other.type]
59
55
  end
56
+ end
60
57
 
61
- # An array with all result messages.
62
- attr_reader :messages
63
-
64
- # Creates an empty result object.
65
- def initialize
66
- @messages = []
67
- end
58
+ # An array with all result messages.
59
+ attr_reader :messages
68
60
 
69
- # Returns +true+ if there are no error messages.
70
- def success?
71
- @messages.none? {|message| message.type == :error }
72
- end
61
+ # Creates an empty result object.
62
+ def initialize
63
+ @messages = []
64
+ end
73
65
 
74
- # Returns +true+ if there is at least one error message.
75
- def failure?
76
- !success?
77
- end
66
+ # Returns +true+ if there are no error messages.
67
+ def success?
68
+ @messages.none? {|message| message.type == :error }
69
+ end
78
70
 
79
- # Adds a new message of the given type to this result object.
80
- #
81
- # +type+:: One of :info, :warning or :error.
82
- #
83
- # +content+:: The log message.
84
- def log(type, content)
85
- @messages << Message.new(type, content)
86
- end
71
+ # Returns +true+ if there is at least one error message.
72
+ def failure?
73
+ !success?
74
+ end
87
75
 
76
+ # Adds a new message of the given type to this result object.
77
+ #
78
+ # +type+:: One of :info, :warning or :error.
79
+ #
80
+ # +content+:: The log message.
81
+ def log(type, content)
82
+ @messages << Message.new(type, content)
88
83
  end
89
84
 
90
85
  end
86
+
91
87
  end
92
88
  end
@@ -0,0 +1,56 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2022 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ module HexaPDF
38
+
39
+ # PDF documents can be signed using digital signatures. Such a signature can be used to
40
+ # authenticate the identity of the signer and the contents of the documents.
41
+ #
42
+ # This module contains all code related to digital signatures in PDF.
43
+ #
44
+ # See: PDF1.7/2.0 s12.8
45
+ module DigitalSignature
46
+
47
+ autoload(:Signatures, 'hexapdf/digital_signature/signatures')
48
+ autoload(:Signature, "hexapdf/digital_signature/signature")
49
+ autoload(:Handler, 'hexapdf/digital_signature/handler')
50
+ autoload(:CMSHandler, "hexapdf/digital_signature/cms_handler")
51
+ autoload(:PKCS1Handler, "hexapdf/digital_signature/pkcs1_handler")
52
+ autoload(:VerificationResult, 'hexapdf/digital_signature/verification_result')
53
+ autoload(:Signing, 'hexapdf/digital_signature/signing')
54
+
55
+ end
56
+ end
@@ -52,6 +52,7 @@ require 'hexapdf/importer'
52
52
  require 'hexapdf/image_loader'
53
53
  require 'hexapdf/font_loader'
54
54
  require 'hexapdf/layout'
55
+ require 'hexapdf/digital_signature'
55
56
 
56
57
  begin
57
58
  require 'hexapdf/cext'
@@ -105,7 +106,6 @@ module HexaPDF
105
106
  autoload(:Fonts, 'hexapdf/document/fonts')
106
107
  autoload(:Images, 'hexapdf/document/images')
107
108
  autoload(:Files, 'hexapdf/document/files')
108
- autoload(:Signatures, 'hexapdf/document/signatures')
109
109
  autoload(:Destinations, 'hexapdf/document/destinations')
110
110
  autoload(:Layout, 'hexapdf/document/layout')
111
111
 
@@ -152,18 +152,24 @@ module HexaPDF
152
152
  #
153
153
  # Options:
154
154
  #
155
- # io:: If an IO object is provided, then this document can read PDF objects from this IO
156
- # object, otherwise it can only contain created PDF objects.
155
+ # io::
156
+ # If an IO object is provided, then this document can read PDF objects from this IO object,
157
+ # otherwise it can only contain created PDF objects.
157
158
  #
158
- # decryption_opts:: A hash with options for decrypting the PDF objects loaded from the IO.
159
+ # decryption_opts::
160
+ # A hash with options for decrypting the PDF objects loaded from the IO. The PDF standard
161
+ # security handler expects a :password key to be set to either the user or owner password of
162
+ # the PDF file.
159
163
  #
160
- # config:: A hash with configuration options that is deep-merged into the default configuration
161
- # (see
162
- # HexaPDF::DefaultDocumentConfiguration[../index.html#DefaultDocumentConfiguration],
163
- # meaning that direct sub-hashes are merged instead of overwritten.
164
+ # config::
165
+ # A hash with configuration options that is deep-merged into the default configuration (see
166
+ # HexaPDF::DefaultDocumentConfiguration[../index.html#DefaultDocumentConfiguration], meaning
167
+ # that direct sub-hashes are merged instead of overwritten.
164
168
  def initialize(io: nil, decryption_opts: {}, config: {})
165
169
  @config = Configuration.with_defaults(config)
166
170
  @version = '1.2'
171
+ @cache = Hash.new {|h, k| h[k] = {} }
172
+ @listeners = {}
167
173
 
168
174
  @revisions = Revisions.from_io(self, io)
169
175
  @security_handler = if encrypted? && @config['document.auto_decrypt']
@@ -171,9 +177,6 @@ module HexaPDF
171
177
  else
172
178
  nil
173
179
  end
174
-
175
- @listeners = {}
176
- @cache = Hash.new {|h, k| h[k] = {} }
177
180
  end
178
181
 
179
182
  # :call-seq:
@@ -251,19 +254,16 @@ module HexaPDF
251
254
  # :call-seq:
252
255
  # doc.import(obj) -> imported_object
253
256
  #
254
- # Imports the given, with a different document associated PDF object and returns the imported
257
+ # Imports the given object from a different HexaPDF::Document instance and returns the imported
255
258
  # object.
256
259
  #
257
260
  # If the same argument is provided in multiple invocations, the import is done only once and
258
- # the previously imoprted object is returned.
261
+ # the previously imported object is returned.
259
262
  #
260
263
  # See: Importer
261
264
  def import(obj)
262
- if !obj.kind_of?(HexaPDF::Object) || !obj.document? || obj.document == self
263
- raise ArgumentError, "Importing only works for PDF objects associated " \
264
- "with another document"
265
- end
266
- HexaPDF::Importer.for(source: obj.document, destination: self).import(obj)
265
+ source = (obj.kind_of?(HexaPDF::Object) ? obj.document : nil)
266
+ HexaPDF::Importer.for(self).import(obj, source: source)
267
267
  end
268
268
 
269
269
  # Wraps the given object inside a HexaPDF::Object class which allows one to use
@@ -589,25 +589,28 @@ module HexaPDF
589
589
  acro_form&.signature_flag?(:signatures_exist)
590
590
  end
591
591
 
592
- # Returns an array with the digital signatures of this document.
592
+ # Returns a DigitalSignature::Signatures object that allows working with the digital signatures
593
+ # of this document.
593
594
  def signatures
594
- @signatures ||= Signatures.new(self)
595
+ @signatures ||= DigitalSignature::Signatures.new(self)
595
596
  end
596
597
 
597
598
  # Signs the document and writes it to the given file or IO object.
598
599
  #
599
600
  # For details on the arguments +file_or_io+, +signature+ and +write_options+ see
600
- # HexaPDF::Document::Signatures#add.
601
+ # HexaPDF::DigitalSignature::Signatures#add.
601
602
  #
602
603
  # The signing handler to be used is determined by the +handler+ argument together with the rest
603
- # of the keyword arguments (see HexaPDF::Document::Signatures#handler for details).
604
+ # of the keyword arguments (see HexaPDF::DigitalSignature::Signatures#signing_handler for
605
+ # details).
604
606
  #
605
- # If not changed, the default signing handler is HexaPDF::Document::Signatures::DefaultHandler.
607
+ # If not changed, the default signing handler is
608
+ # HexaPDF::DigitalSignature::Signing::DefaultHandler.
606
609
  #
607
610
  # *Note*: Once signing is done the document cannot be changed anymore since it was written. If a
608
611
  # document needs to be signed multiple times, it needs to be loaded again after writing.
609
612
  def sign(file_or_io, handler: :default, signature: nil, write_options: {}, **handler_options)
610
- handler = signatures.handler(name: handler, **handler_options)
613
+ handler = signatures.signing_handler(name: handler, **handler_options)
611
614
  signatures.add(file_or_io, handler, signature: signature, write_options: write_options)
612
615
  end
613
616
 
@@ -97,7 +97,8 @@ module HexaPDF
97
97
  # a user is allowed to do with a PDF file.
98
98
  #
99
99
  # When a user or owner password is specified, a PDF file can only be opened when the correct
100
- # password is supplied.
100
+ # password is supplied. To open such an encrypted PDF file, the +decryption_opts+ provided to
101
+ # HexaPDF::Document.new needs to contain a :password key with the password.
101
102
  #
102
103
  # See: PDF1.7 s7.6.3, PDF2.0 s7.6.3
103
104
  class StandardSecurityHandler < SecurityHandler
@@ -49,7 +49,7 @@ module HexaPDF
49
49
  module ASCII85Decode
50
50
 
51
51
  VALUE_TO_CHAR = {} #:nodoc:
52
- (0..84).each do |i|
52
+ 85.times do |i|
53
53
  VALUE_TO_CHAR[i] = (i + 33).chr
54
54
  end
55
55
 
@@ -60,64 +60,69 @@ module HexaPDF
60
60
 
61
61
  end
62
62
 
63
- # Returns the Importer object for copying objects from the +source+ to the +destination+
64
- # document.
65
- def self.for(source:, destination:)
63
+ # Returns the Importer object for copying objects to the +destination+ document.
64
+ def self.for(destination)
66
65
  @map ||= {}
67
- @map.keep_if {|_, v| v.source.weakref_alive? && v.destination.weakref_alive? }
68
- source = NullableWeakRef.new(source)
66
+ @map.keep_if {|_, v| v.destination.weakref_alive? }
69
67
  destination = NullableWeakRef.new(destination)
70
- @map[[source.hash, destination.hash]] ||= new(source: source, destination: destination)
68
+ @map[destination.hash] ||= new(destination)
71
69
  end
72
70
 
73
71
  private_class_method :new
74
72
 
75
- attr_reader :source, :destination #:nodoc:
73
+ attr_reader :destination #:nodoc:
76
74
 
77
- # Initializes a new importer that can import objects from the +source+ document to the
78
- # +destination+ document.
79
- def initialize(source:, destination:)
80
- @source = source
75
+ # Initializes a new importer that can import objects to the +destination+ document.
76
+ def initialize(destination)
81
77
  @destination = destination
82
78
  @mapper = {}
83
79
  end
84
80
 
85
- # Imports the given +object+ from the source to the destination object and returns the
86
- # imported object.
81
+ SourceWrapper = Struct.new(:source) #:nodoc:
82
+
83
+ # Imports the given +object+ to the destination object and returns the imported object.
87
84
  #
88
85
  # Note: Indirect objects are automatically added to the destination document but direct or
89
86
  # simple objects are not.
90
87
  #
91
- # An error is raised if the object doesn't belong to the +source+ document.
92
- def import(object)
88
+ # The +source+ argument should be +nil+ or set to the source document of the imported object. If
89
+ # it is +nil+, the source document is dynamically identified. If this identification is not
90
+ # possible and the source document would be needed, an error is raised.
91
+ def import(object, source: nil)
92
+ internal_import(object, SourceWrapper.new(source))
93
+ end
94
+
95
+ private
96
+
97
+ # Does the actual importing of the given +object+, using +wrapper+ to store/use the source
98
+ # document.
99
+ def internal_import(object, wrapper)
93
100
  mapped_object = @mapper[object.data]&.__getobj__ if object.kind_of?(HexaPDF::Object)
94
- if object.kind_of?(HexaPDF::Object) && object.document? && @source != object.document
95
- raise HexaPDF::Error, "Import error: Incorrect document object for importer"
96
- elsif mapped_object && !mapped_object.null?
101
+ if mapped_object && !mapped_object.null?
97
102
  if object.class != mapped_object.class
98
103
  mapped_object = @destination.wrap(mapped_object, type: object.class)
99
104
  end
100
105
  mapped_object
101
106
  else
102
- duplicate(object)
107
+ duplicate(object, wrapper)
103
108
  end
104
109
  end
105
110
 
106
- private
107
-
108
111
  # Recursively duplicates the object.
109
112
  #
110
113
  # PDF objects are automatically added to the destination document if they are indirect objects
111
114
  # in the source document.
112
- def duplicate(object)
115
+ def duplicate(object, wrapper)
113
116
  case object
114
117
  when Hash
115
- object.transform_values {|v| duplicate(v) }
118
+ object.transform_values {|v| duplicate(v, wrapper) }
116
119
  when Array
117
- object.map {|v| duplicate(v) }
120
+ object.map {|v| duplicate(v, wrapper) }
118
121
  when HexaPDF::Reference
119
- import(@source.object(object))
122
+ raise HexaPDF::Error, "Import error: No source document specified" unless wrapper.source
123
+ internal_import(wrapper.source.object(object), wrapper)
120
124
  when HexaPDF::Object
125
+ wrapper.source ||= object.document
121
126
  if object.type == :Catalog || object.type == :Pages
122
127
  @mapper[object.data] = nil
123
128
  elsif (mapped_object = @mapper[object.data]&.__getobj__) && !mapped_object.null?
@@ -132,8 +137,8 @@ module HexaPDF
132
137
  @destination.add(obj) if object.indirect?
133
138
 
134
139
  obj.data.stream = obj.data.stream.dup if obj.data.stream.kind_of?(String)
135
- obj.data.value = duplicate(obj.data.value)
136
- obj.data.value.update(duplicate(object.copy_inherited_values)) if object.type == :Page
140
+ obj.data.value = duplicate(obj.data.value, wrapper)
141
+ obj.data.value.update(duplicate(object.copy_inherited_values, wrapper)) if object.type == :Page
137
142
  obj
138
143
  end
139
144
  when String
@@ -207,7 +207,7 @@ module HexaPDF
207
207
  @results = []
208
208
  @results_item_marker_x = []
209
209
 
210
- @children.each_with_index do |child, index|
210
+ @children.each do |child|
211
211
  shape = Geom2D::Polygon([left, top - height],
212
212
  [left + width, top - height],
213
213
  [left + width, top],
@@ -217,11 +217,7 @@ module HexaPDF
217
217
  remove_indent_from_frame_shape(shape) unless shape.polygons.empty?
218
218
  end
219
219
 
220
- #p [:list, left, width, shape]
221
-
222
220
  item_frame = Frame.new(item_frame_left, top - height, item_frame_width, height, shape: shape)
223
-
224
- #p [index, item_frame.x, @results_item_marker_x]
225
221
  @results_item_marker_x << item_frame.x - content_indentation
226
222
 
227
223
  box_fitter = BoxFitter.new([item_frame])