hexapdf 0.27.0 → 0.29.0

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