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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +100 -11
- data/examples/019-acro_form.rb +14 -3
- data/examples/023-images.rb +30 -0
- data/examples/024-digital-signatures.rb +23 -0
- data/lib/hexapdf/cli/info.rb +5 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/cli/split.rb +2 -2
- data/lib/hexapdf/configuration.rb +13 -14
- data/lib/hexapdf/content/canvas.rb +8 -3
- data/lib/hexapdf/dictionary.rb +1 -5
- data/lib/hexapdf/dictionary_fields.rb +6 -2
- data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
- data/lib/hexapdf/digital_signature/handler.rb +138 -0
- data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
- data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
- data/lib/hexapdf/digital_signature/signatures.rb +210 -0
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
- data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
- data/lib/hexapdf/digital_signature/signing.rb +101 -0
- data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
- data/lib/hexapdf/digital_signature.rb +56 -0
- data/lib/hexapdf/document.rb +27 -24
- data/lib/hexapdf/encryption/standard_security_handler.rb +2 -1
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/importer.rb +32 -27
- data/lib/hexapdf/layout/list_box.rb +1 -5
- data/lib/hexapdf/object.rb +5 -0
- data/lib/hexapdf/parser.rb +13 -0
- data/lib/hexapdf/revision.rb +15 -12
- data/lib/hexapdf/revisions.rb +4 -0
- data/lib/hexapdf/tokenizer.rb +14 -8
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/field.rb +11 -5
- data/lib/hexapdf/type/acro_form/form.rb +33 -7
- data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -0
- data/lib/hexapdf/type/font_true_type.rb +14 -0
- data/lib/hexapdf/type/object_stream.rb +2 -2
- data/lib/hexapdf/type/outline.rb +1 -1
- data/lib/hexapdf/type/page.rb +56 -46
- data/lib/hexapdf/type.rb +0 -1
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +2 -3
- data/test/hexapdf/content/test_canvas.rb +5 -0
- data/test/hexapdf/{type/signature → digital_signature}/common.rb +34 -4
- data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
- data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
- data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
- data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
- data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
- data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
- data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
- data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
- data/test/hexapdf/digital_signature/test_signing.rb +53 -0
- data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
- data/test/hexapdf/document/test_pages.rb +2 -2
- data/test/hexapdf/encryption/test_aes.rb +1 -1
- data/test/hexapdf/filter/test_predictor.rb +0 -1
- data/test/hexapdf/layout/test_box.rb +2 -1
- data/test/hexapdf/layout/test_column_box.rb +1 -1
- data/test/hexapdf/layout/test_list_box.rb +1 -1
- data/test/hexapdf/test_dictionary_fields.rb +2 -1
- data/test/hexapdf/test_document.rb +3 -9
- data/test/hexapdf/test_importer.rb +13 -6
- data/test/hexapdf/test_parser.rb +17 -0
- data/test/hexapdf/test_revision.rb +15 -14
- data/test/hexapdf/test_revisions.rb +43 -0
- data/test/hexapdf/test_stream.rb +1 -1
- data/test/hexapdf/test_tokenizer.rb +3 -4
- data/test/hexapdf/test_writer.rb +3 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
- data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
- data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_field.rb +4 -4
- data/test/hexapdf/type/acro_form/test_form.rb +18 -0
- data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
- data/test/hexapdf/type/test_font_true_type.rb +20 -0
- data/test/hexapdf/type/test_object_stream.rb +2 -1
- data/test/hexapdf/type/test_outline.rb +3 -0
- data/test/hexapdf/type/test_page.rb +67 -30
- data/test/hexapdf/type/test_page_tree_node.rb +4 -2
- metadata +69 -16
- data/lib/hexapdf/document/signatures.rb +0 -546
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
- data/lib/hexapdf/type/signature/handler.rb +0 -140
- 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
|
|
41
|
-
class Signature
|
|
38
|
+
module DigitalSignature
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
# Holds the result information when verifying a signature.
|
|
41
|
+
class VerificationResult
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
# Creates an empty result object.
|
|
62
|
+
def initialize
|
|
63
|
+
@messages = []
|
|
64
|
+
end
|
|
73
65
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
# Returns +true+ if there are no error messages.
|
|
67
|
+
def success?
|
|
68
|
+
@messages.none? {|message| message.type == :error }
|
|
69
|
+
end
|
|
78
70
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
data/lib/hexapdf/document.rb
CHANGED
|
@@ -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::
|
|
156
|
-
#
|
|
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::
|
|
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::
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
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
|
|
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
|
|
261
|
+
# the previously imported object is returned.
|
|
259
262
|
#
|
|
260
263
|
# See: Importer
|
|
261
264
|
def import(obj)
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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::
|
|
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::
|
|
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
|
|
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.
|
|
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
|
data/lib/hexapdf/importer.rb
CHANGED
|
@@ -60,64 +60,69 @@ module HexaPDF
|
|
|
60
60
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
# Returns the Importer object for copying objects
|
|
64
|
-
|
|
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.
|
|
68
|
-
source = NullableWeakRef.new(source)
|
|
66
|
+
@map.keep_if {|_, v| v.destination.weakref_alive? }
|
|
69
67
|
destination = NullableWeakRef.new(destination)
|
|
70
|
-
@map[
|
|
68
|
+
@map[destination.hash] ||= new(destination)
|
|
71
69
|
end
|
|
72
70
|
|
|
73
71
|
private_class_method :new
|
|
74
72
|
|
|
75
|
-
attr_reader :
|
|
73
|
+
attr_reader :destination #:nodoc:
|
|
76
74
|
|
|
77
|
-
# Initializes a new importer that can import objects
|
|
78
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
#
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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])
|