hexapdf 0.19.0 → 0.20.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -0
- data/data/hexapdf/cert/demo_cert.rb +22 -0
- data/data/hexapdf/cert/root-ca.crt +119 -0
- data/data/hexapdf/cert/signing.crt +125 -0
- data/data/hexapdf/cert/signing.key +52 -0
- data/data/hexapdf/cert/sub-ca.crt +125 -0
- data/lib/hexapdf/cli/info.rb +21 -1
- data/lib/hexapdf/configuration.rb +26 -0
- data/lib/hexapdf/content/graphics_state.rb +24 -5
- data/lib/hexapdf/content/processor.rb +1 -1
- data/lib/hexapdf/document/signatures.rb +327 -0
- data/lib/hexapdf/document.rb +26 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +1 -2
- data/lib/hexapdf/importer.rb +1 -1
- data/lib/hexapdf/layout/style.rb +2 -1
- data/lib/hexapdf/object.rb +5 -3
- data/lib/hexapdf/parser.rb +21 -9
- data/lib/hexapdf/rectangle.rb +0 -6
- data/lib/hexapdf/revision.rb +13 -6
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
- data/lib/hexapdf/type/acro_form/field.rb +2 -0
- data/lib/hexapdf/type/acro_form/form.rb +9 -1
- data/lib/hexapdf/type/annotation.rb +36 -3
- data/lib/hexapdf/type/font.rb +5 -0
- data/lib/hexapdf/type/font_simple.rb +1 -1
- data/lib/hexapdf/type/font_type3.rb +20 -0
- data/lib/hexapdf/type/object_stream.rb +3 -1
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +125 -0
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +99 -0
- data/lib/hexapdf/type/signature/handler.rb +112 -0
- data/lib/hexapdf/type/signature/verification_result.rb +92 -0
- data/lib/hexapdf/type/signature.rb +236 -0
- data/lib/hexapdf/type.rb +1 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +24 -10
- data/test/hexapdf/content/test_graphics_state.rb +9 -1
- data/test/hexapdf/content/test_operator.rb +8 -3
- data/test/hexapdf/content/test_processor.rb +1 -1
- data/test/hexapdf/document/test_signatures.rb +225 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +8 -6
- data/test/hexapdf/layout/test_style.rb +11 -0
- data/test/hexapdf/test_document.rb +28 -0
- data/test/hexapdf/test_object.rb +7 -2
- data/test/hexapdf/test_parser.rb +14 -0
- data/test/hexapdf/test_rectangle.rb +0 -7
- data/test/hexapdf/test_revision.rb +44 -14
- data/test/hexapdf/test_writer.rb +44 -14
- data/test/hexapdf/type/acro_form/test_field.rb +11 -1
- data/test/hexapdf/type/acro_form/test_form.rb +5 -0
- data/test/hexapdf/type/signature/common.rb +71 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
- data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
- data/test/hexapdf/type/signature/test_handler.rb +76 -0
- data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
- data/test/hexapdf/type/test_annotation.rb +40 -2
- data/test/hexapdf/type/test_font.rb +4 -0
- data/test/hexapdf/type/test_font_simple.rb +5 -5
- data/test/hexapdf/type/test_font_type3.rb +16 -1
- data/test/hexapdf/type/test_object_stream.rb +9 -0
- data/test/hexapdf/type/test_signature.rb +131 -0
- metadata +21 -33
- data/test/data/cert/create.sh +0 -171
- data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF39.pem +0 -119
- data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF3A.pem +0 -125
- data/test/data/cert/root-ca/db/crlnumber +0 -1
- data/test/data/cert/root-ca/db/index +0 -2
- data/test/data/cert/root-ca/db/index.attr +0 -1
- data/test/data/cert/root-ca/db/index.attr.old +0 -1
- data/test/data/cert/root-ca/db/index.old +0 -1
- data/test/data/cert/root-ca/db/serial +0 -1
- data/test/data/cert/root-ca/db/serial.old +0 -1
- data/test/data/cert/root-ca/private/root-ca.key +0 -52
- data/test/data/cert/root-ca/root-ca.conf +0 -65
- data/test/data/cert/root-ca/root-ca.crt +0 -119
- data/test/data/cert/root-ca/root-ca.csr +0 -28
- data/test/data/cert/signature-1-pkcs7-detached.pdf +0 -182
- data/test/data/cert/sub-ca/certs/453FF080E3EDCD6A388D5368DFC320D9.pem +0 -125
- data/test/data/cert/sub-ca/db/crlnumber +0 -1
- data/test/data/cert/sub-ca/db/index +0 -1
- data/test/data/cert/sub-ca/db/index.attr +0 -1
- data/test/data/cert/sub-ca/db/index.old +0 -0
- data/test/data/cert/sub-ca/db/serial +0 -1
- data/test/data/cert/sub-ca/db/serial.old +0 -1
- data/test/data/cert/sub-ca/private/signing.key +0 -52
- data/test/data/cert/sub-ca/private/sub-ca.key +0 -52
- data/test/data/cert/sub-ca/signing.crt +0 -125
- data/test/data/cert/sub-ca/signing.csr +0 -28
- data/test/data/cert/sub-ca/signing.p12 +0 -0
- data/test/data/cert/sub-ca/sub-ca.conf +0 -65
- data/test/data/cert/sub-ca/sub-ca.crt +0 -125
- data/test/data/cert/sub-ca/sub-ca.csr +0 -28
@@ -512,7 +512,7 @@ module HexaPDF
|
|
512
512
|
attr_accessor :leading
|
513
513
|
|
514
514
|
# The font for the text.
|
515
|
-
|
515
|
+
attr_reader :font
|
516
516
|
|
517
517
|
# The font size.
|
518
518
|
attr_reader :font_size
|
@@ -552,9 +552,11 @@ module HexaPDF
|
|
552
552
|
|
553
553
|
# The scaled font size used in glyph displacement calculations.
|
554
554
|
#
|
555
|
-
# This returns the font size
|
555
|
+
# This returns the font size multiplied by the scaling factor from glyph space to text space
|
556
|
+
# (0.001 for all fonts except Type3 fonts or the scaling specified in /FontMatrix for Type3
|
557
|
+
# fonts) and multiplied by #scaled_horizontal_scaling.
|
556
558
|
#
|
557
|
-
# See PDF1.7 s9.4.4
|
559
|
+
# See PDF1.7 s9.4.4, HexaPDF::Type::FontType3
|
558
560
|
attr_reader :scaled_font_size
|
559
561
|
|
560
562
|
# The scaled horizontal scaling used in glyph displacement calculations.
|
@@ -665,6 +667,15 @@ module HexaPDF
|
|
665
667
|
self.fill_color = color_space.default_color
|
666
668
|
end
|
667
669
|
|
670
|
+
##
|
671
|
+
# :attr_writer: font
|
672
|
+
#
|
673
|
+
# Sets the font and updates the glyph space to text space scaling.
|
674
|
+
def font=(font)
|
675
|
+
@font = font
|
676
|
+
update_scaled_font_size
|
677
|
+
end
|
678
|
+
|
668
679
|
##
|
669
680
|
# :attr_writer: character_spacing
|
670
681
|
#
|
@@ -689,7 +700,7 @@ module HexaPDF
|
|
689
700
|
# Sets the font size and updates the scaled font size.
|
690
701
|
def font_size=(size)
|
691
702
|
@font_size = size
|
692
|
-
|
703
|
+
update_scaled_font_size
|
693
704
|
end
|
694
705
|
|
695
706
|
##
|
@@ -702,7 +713,15 @@ module HexaPDF
|
|
702
713
|
@scaled_horizontal_scaling = scaling / 100.0
|
703
714
|
@scaled_character_spacing = @character_spacing * @scaled_horizontal_scaling
|
704
715
|
@scaled_word_spacing = @word_spacing * @scaled_horizontal_scaling
|
705
|
-
|
716
|
+
update_scaled_font_size
|
717
|
+
end
|
718
|
+
|
719
|
+
private
|
720
|
+
|
721
|
+
# Updates the cached value for the scaled font size.
|
722
|
+
def update_scaled_font_size
|
723
|
+
@scaled_font_size = @font_size * (@font&.glyph_scaling_factor || 0.001) *
|
724
|
+
@scaled_horizontal_scaling
|
706
725
|
end
|
707
726
|
|
708
727
|
end
|
@@ -455,7 +455,7 @@ module HexaPDF
|
|
455
455
|
# Decodes the given array containing text and positioning information while assuming that the
|
456
456
|
# writing direction is vertical.
|
457
457
|
def decode_vertical_text(_data)
|
458
|
-
raise
|
458
|
+
raise "Not yet implemented"
|
459
459
|
end
|
460
460
|
|
461
461
|
end
|
@@ -0,0 +1,327 @@
|
|
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-2021 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 'hexapdf/error'
|
39
|
+
|
40
|
+
module HexaPDF
|
41
|
+
class Document
|
42
|
+
|
43
|
+
# This class provides methods for interacting with digital signatures of a PDF file.
|
44
|
+
class Signatures
|
45
|
+
|
46
|
+
# This is the default signing handler which provides the ability to sign a document with a
|
47
|
+
# provided certificate using the adb.pkcs7.detached algorithm.
|
48
|
+
#
|
49
|
+
# Additional functionality:
|
50
|
+
#
|
51
|
+
# * Optionally setting the reason, location and contact information.
|
52
|
+
# * Making the signature a certification signature by applying the DocMDP transform method.
|
53
|
+
#
|
54
|
+
# == Implementing a Signing Handler
|
55
|
+
#
|
56
|
+
# This class also serves as an example on how to create a custom handler: The public methods
|
57
|
+
# #filter_name, #sub_filter_name, #signature_size, #finalize_objects and #sign are used by the
|
58
|
+
# digital signature algorithm.
|
59
|
+
#
|
60
|
+
# Once a custom signing handler has been created, it can be registered under the
|
61
|
+
# 'signature.signing_handler' configuration option for easy use. It has to take keyword
|
62
|
+
# arguments in its initialize method to be compatible with the Signatures#handler method.
|
63
|
+
class DefaultHandler
|
64
|
+
|
65
|
+
# The certificate with which to sign the PDF.
|
66
|
+
attr_accessor :certificate
|
67
|
+
|
68
|
+
# The private key for the #certificate.
|
69
|
+
attr_accessor :key
|
70
|
+
|
71
|
+
# The certificate chain that should be embedded in the PDF; normally contains all
|
72
|
+
# certificates up to the root certificate.
|
73
|
+
attr_accessor :certificate_chain
|
74
|
+
|
75
|
+
# The reason for signing. If used, will be set on the signature object.
|
76
|
+
attr_accessor :reason
|
77
|
+
|
78
|
+
# The signing location. If used, will be set on the signature object.
|
79
|
+
attr_accessor :location
|
80
|
+
|
81
|
+
# The contact information. If used, will be set on the signature object.
|
82
|
+
attr_accessor :contact_info
|
83
|
+
|
84
|
+
# The DocMDP permissions that should be set on the document.
|
85
|
+
#
|
86
|
+
# See #doc_mdp_permissions=
|
87
|
+
attr_reader :doc_mdp_permissions
|
88
|
+
|
89
|
+
# Creates a new DefaultHandler with the given attributes.
|
90
|
+
def initialize(**arguments)
|
91
|
+
arguments.each {|name, value| send("#{name}=", value) }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the name to be set on the /Filter key when using this signing handler.
|
95
|
+
def filter_name
|
96
|
+
:"Adobe.PPKLite"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the name to be set on the /SubFilter key when using this signing handler.
|
100
|
+
def sub_filter_name
|
101
|
+
:"adbe.pkcs7.detached"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Sets the DocMDP permissions that should be applied to the document.
|
105
|
+
#
|
106
|
+
# Valid values for +permissions+ are:
|
107
|
+
#
|
108
|
+
# +nil+::
|
109
|
+
# Don't set any DocMDP permissions (default).
|
110
|
+
#
|
111
|
+
# +:no_changes+ or 1::
|
112
|
+
# No changes whatsoever are allowed.
|
113
|
+
#
|
114
|
+
# +:form_filling+ or 2::
|
115
|
+
# Only filling in forms and signing are allowed.
|
116
|
+
#
|
117
|
+
# +:form_filling_and_annotations+ or 3::
|
118
|
+
# Only filling in forms, signing and annotation creation/deletion/modification are
|
119
|
+
# allowed.
|
120
|
+
def doc_mdp_permissions=(permissions)
|
121
|
+
case permissions
|
122
|
+
when :no_changes, 1 then @doc_mdp_permissions = 1
|
123
|
+
when :form_filling, 2 then @doc_mdp_permissions = 2
|
124
|
+
when :form_filling_and_annotations, 3 then @doc_mdp_permissions = 3
|
125
|
+
when nil then @doc_mdp_permissions = nil
|
126
|
+
else
|
127
|
+
raise ArgumentError, "Invalid permissions value '#{permissions.inspect}'"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns the size of the signature that would be created.
|
132
|
+
def signature_size
|
133
|
+
sign("").size
|
134
|
+
end
|
135
|
+
|
136
|
+
# Finalizes the signature field as well as the signature dictionary before writing.
|
137
|
+
def finalize_objects(_signature_field, signature)
|
138
|
+
signature[:Reason] = reason if reason
|
139
|
+
signature[:Location] = location if location
|
140
|
+
signature[:ContactInfo] = contact_info if contact_info
|
141
|
+
|
142
|
+
if doc_mdp_permissions
|
143
|
+
doc = signature.document
|
144
|
+
if doc.signatures.count > 1
|
145
|
+
raise HexaPDF::Error, "Can set DocMDP access permissions only on first signature"
|
146
|
+
end
|
147
|
+
params = doc.add({Type: :TransformParams, V: :'1.2', P: doc_mdp_permissions})
|
148
|
+
sigref = doc.add({Type: :SigRef, TransformMethod: :DocMDP, DigestMethod: :SHA1,
|
149
|
+
TransformParams: params})
|
150
|
+
signature[:Reference] = [sigref]
|
151
|
+
(doc.catalog[:Perms] ||= {})[:DocMDP] = signature
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns the DER serialized OpenSSL::PKCS7 structure containing the signature for the given
|
156
|
+
# data.
|
157
|
+
def sign(data)
|
158
|
+
OpenSSL::PKCS7.sign(@certificate, @key, data, @certificate_chain,
|
159
|
+
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
include Enumerable
|
165
|
+
|
166
|
+
# Creates a new Signatures object for the given PDF document.
|
167
|
+
def initialize(document)
|
168
|
+
@document = document
|
169
|
+
end
|
170
|
+
|
171
|
+
# Creates a signing handler with the given options and returns it.
|
172
|
+
#
|
173
|
+
# A signing handler name is mapped to a class via the 'signature.signing_handler'
|
174
|
+
# configuration option.
|
175
|
+
def handler(name: :default, **options)
|
176
|
+
handler = @document.config.constantize('signature.signing_handler', name) do
|
177
|
+
raise HexaPDF::Error, "No signing handler named '#{name}' is available"
|
178
|
+
end
|
179
|
+
handler.new(**options)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Adds a signature to the document and returns the corresponding signature object.
|
183
|
+
#
|
184
|
+
# This method will add a new signature to the document and write the updated document to the
|
185
|
+
# given file or IO stream. Afterwards the document can't be modified anymore and still retain
|
186
|
+
# a correct digital signature; create a new document based on the file or IO stream instead.
|
187
|
+
#
|
188
|
+
# +signature+::
|
189
|
+
# Can either be a signature object (determined via the /Type key), a signature field or
|
190
|
+
# +nil+. Providing a signature object or signature field provides for more control, e.g.:
|
191
|
+
#
|
192
|
+
# * Setting values for optional signature object fields like /Reason and /Location.
|
193
|
+
# * (In)directly specifying which signature field should be used.
|
194
|
+
#
|
195
|
+
# If a signature object is provided and it is not associated with an AcroForm signature
|
196
|
+
# field, a new signature field is created and added to the main AcroForm object, creating
|
197
|
+
# that if necessary.
|
198
|
+
#
|
199
|
+
# If a signature field is provided and it already has a signature object as field value,
|
200
|
+
# that signature object is discarded.
|
201
|
+
#
|
202
|
+
# If the signature field doesn't have a widget, a non-visible one is created on the first
|
203
|
+
# page.
|
204
|
+
#
|
205
|
+
# +handler+::
|
206
|
+
# The signing handler that provides the necessary methods for signing and adjusting the
|
207
|
+
# signature and signature field objects to one's liking, see #handler and DefaultHandler.
|
208
|
+
#
|
209
|
+
# +write_options+::
|
210
|
+
# The key-value pairs of this hash will be passed on to the HexaPDF::Document#write
|
211
|
+
# command. Note that +incremental+ will be automatically set if signing an already
|
212
|
+
# existing file.
|
213
|
+
def add(file_or_io, handler, signature: nil, write_options: {})
|
214
|
+
if signature && signature.type != :Sig
|
215
|
+
signature_field = signature
|
216
|
+
signature = signature_field.field_value
|
217
|
+
end
|
218
|
+
signature ||= @document.add({Type: :Sig})
|
219
|
+
|
220
|
+
# Prepare AcroForm
|
221
|
+
form = @document.acro_form(create: true)
|
222
|
+
form.signature_flag(:signatures_exist, :append_only)
|
223
|
+
|
224
|
+
# Prepare signature field
|
225
|
+
signature_field ||= form.each_field.find {|field| field.field_value == signature } ||
|
226
|
+
form.create_signature_field(generate_field_name)
|
227
|
+
signature_field.field_value = signature
|
228
|
+
|
229
|
+
if signature_field.each_widget.to_a.empty?
|
230
|
+
signature_field.create_widget(@document.pages[0], Rect: [0, 0, 0, 0])
|
231
|
+
end
|
232
|
+
|
233
|
+
# Prepare signature object
|
234
|
+
signature[:Filter] = handler.filter_name
|
235
|
+
signature[:SubFilter] = handler.sub_filter_name
|
236
|
+
signature[:ByteRange] = [0, 1_000_000_000_000, 1_000_000_000_000, 1_000_000_000_000]
|
237
|
+
signature[:Contents] = '00' * handler.signature_size # twice the size due to hex encoding
|
238
|
+
signature[:M] = Time.now
|
239
|
+
|
240
|
+
io = if file_or_io.kind_of?(String)
|
241
|
+
File.open(file_or_io, 'w+')
|
242
|
+
else
|
243
|
+
file_or_io
|
244
|
+
end
|
245
|
+
|
246
|
+
# Save the current state so that we can determine the correct /ByteRange value and set the
|
247
|
+
# values
|
248
|
+
handler.finalize_objects(signature_field, signature)
|
249
|
+
section = @document.write(io, incremental: true, **write_options)
|
250
|
+
data = section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort
|
251
|
+
index = data.index {|_pos, oid| oid == signature.oid }
|
252
|
+
signature_offset = data[index][0]
|
253
|
+
signature_length = data[index + 1][0] - data[index][0]
|
254
|
+
io.pos = signature_offset
|
255
|
+
signature_data = io.read(signature_length)
|
256
|
+
|
257
|
+
io.rewind
|
258
|
+
file_data = io.read
|
259
|
+
|
260
|
+
# Calculate the offsets for the /ByteRange
|
261
|
+
contents_offset = signature_offset + signature_data.index('Contents(') + 8
|
262
|
+
offset2 = contents_offset + signature[:Contents].size + 2 # +2 because of the needed < and >
|
263
|
+
length2 = file_data.size - offset2
|
264
|
+
signature[:ByteRange] = [0, contents_offset, offset2, length2]
|
265
|
+
|
266
|
+
# Set the correct /ByteRange value
|
267
|
+
signature_data.sub!(/ByteRange\[0 1000000000000 1000000000000 1000000000000\]/) do |match|
|
268
|
+
length = match.size
|
269
|
+
result = "ByteRange[0 #{contents_offset} #{offset2} #{length2}]"
|
270
|
+
result.ljust(length)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Now everything besides the /Contents value is correct, so we can read the contents for
|
274
|
+
# signing
|
275
|
+
file_data[signature_offset, signature_length] = signature_data
|
276
|
+
signed_contents = file_data[0, contents_offset] << file_data[offset2, length2]
|
277
|
+
signature[:Contents] = handler.sign(signed_contents)
|
278
|
+
|
279
|
+
# Set the correct /Contents value as hexstring
|
280
|
+
signature_data.sub!(/Contents\(0+\)/) do |match|
|
281
|
+
length = match.size
|
282
|
+
result = "Contents<#{signature[:Contents].unpack1('H*')}"
|
283
|
+
"#{result.ljust(length - 1, '0')}>"
|
284
|
+
end
|
285
|
+
|
286
|
+
io.pos = signature_offset
|
287
|
+
io.write(signature_data)
|
288
|
+
|
289
|
+
signature
|
290
|
+
ensure
|
291
|
+
io.close if io && io != file_or_io
|
292
|
+
end
|
293
|
+
|
294
|
+
# :call-seq:
|
295
|
+
# signatures.each {|signature| block } -> signatures
|
296
|
+
# signatures.each -> Enumerator
|
297
|
+
#
|
298
|
+
# Iterates over all signatures in the order they are found.
|
299
|
+
def each
|
300
|
+
return to_enum(__method__) unless block_given?
|
301
|
+
|
302
|
+
return [] unless (form = @document.acro_form)
|
303
|
+
form.each_field do |field|
|
304
|
+
yield(field.field_value) if field.field_type == :Sig && field.field_value
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Returns the number of signatures in the PDF document. May be zero if the document has no
|
309
|
+
# signatures.
|
310
|
+
def count
|
311
|
+
each.to_a.size
|
312
|
+
end
|
313
|
+
|
314
|
+
private
|
315
|
+
|
316
|
+
# Generates a field name for a signature field.
|
317
|
+
def generate_field_name
|
318
|
+
index = (@document.acro_form.each_field.
|
319
|
+
map {|field| field.full_field_name.scan(/\ASignature(\d+)/).first&.first.to_i }.
|
320
|
+
max || 0) + 1
|
321
|
+
"Signature#{index}"
|
322
|
+
end
|
323
|
+
|
324
|
+
end
|
325
|
+
|
326
|
+
end
|
327
|
+
end
|
data/lib/hexapdf/document.rb
CHANGED
@@ -36,6 +36,7 @@
|
|
36
36
|
|
37
37
|
require 'stringio'
|
38
38
|
require 'hexapdf/error'
|
39
|
+
require 'hexapdf/data_dir'
|
39
40
|
require 'hexapdf/content'
|
40
41
|
require 'hexapdf/configuration'
|
41
42
|
require 'hexapdf/reference'
|
@@ -104,6 +105,7 @@ module HexaPDF
|
|
104
105
|
autoload(:Fonts, 'hexapdf/document/fonts')
|
105
106
|
autoload(:Images, 'hexapdf/document/images')
|
106
107
|
autoload(:Files, 'hexapdf/document/files')
|
108
|
+
autoload(:Signatures, 'hexapdf/document/signatures')
|
107
109
|
|
108
110
|
# :call-seq:
|
109
111
|
# Document.open(filename, **docargs) -> doc
|
@@ -617,6 +619,30 @@ module HexaPDF
|
|
617
619
|
@security_handler
|
618
620
|
end
|
619
621
|
|
622
|
+
# Returns +true+ if the document is signed, i.e. contains digital signatures.
|
623
|
+
def signed?
|
624
|
+
acro_form&.signature_flag?(:signatures_exist)
|
625
|
+
end
|
626
|
+
|
627
|
+
# Returns an array with the digital signatures of this document.
|
628
|
+
def signatures
|
629
|
+
@signatures ||= Signatures.new(self)
|
630
|
+
end
|
631
|
+
|
632
|
+
# Signs the document and writes it to the given file or IO object.
|
633
|
+
#
|
634
|
+
# For details on the arguments +file_or_io+, +signature+ and +write_options+ see
|
635
|
+
# HexaPDF::Document::Signatures#add.
|
636
|
+
#
|
637
|
+
# The signing handler to be used is determined by the +handler+ argument together with the rest
|
638
|
+
# of the keyword arguments (see HexaPDF::Document::Signatures#handler for details).
|
639
|
+
#
|
640
|
+
# If not changed, the default signing handler is HexaPDF::Document::Signatures::DefaultHandler.
|
641
|
+
def sign(file_or_io, handler: :default, signature: nil, write_options: {}, **handler_options)
|
642
|
+
handler = signatures.handler(name: handler, **handler_options)
|
643
|
+
signatures.add(file_or_io, handler, signature: signature, write_options: write_options)
|
644
|
+
end
|
645
|
+
|
620
646
|
# Validates all objects, or, if +only_loaded+ is +true+, only loaded objects, with optional
|
621
647
|
# auto-correction, and returns +true+ if everything is fine.
|
622
648
|
#
|
@@ -328,8 +328,7 @@ module HexaPDF
|
|
328
328
|
raise(HexaPDF::UnsupportedEncryptionError,
|
329
329
|
"Invalid /R value for standard security handler")
|
330
330
|
elsif dict[:R] <= 4 && !document.trailer[:ID].kind_of?(PDFArray)
|
331
|
-
|
332
|
-
"Document ID for needed for decryption")
|
331
|
+
document.trailer[:ID] = ['', '']
|
333
332
|
end
|
334
333
|
@trailer_id_hash = trailer_id_hash
|
335
334
|
|
data/lib/hexapdf/importer.rb
CHANGED
@@ -93,7 +93,7 @@ module HexaPDF
|
|
93
93
|
mapped_object = @mapper[object.data]&.__getobj__ if object.kind_of?(HexaPDF::Object)
|
94
94
|
if object.kind_of?(HexaPDF::Object) && object.document? && @source != object.document
|
95
95
|
raise HexaPDF::Error, "Import error: Incorrect document object for importer"
|
96
|
-
elsif mapped_object && mapped_object
|
96
|
+
elsif mapped_object && !mapped_object.null?
|
97
97
|
mapped_object
|
98
98
|
else
|
99
99
|
duplicate(object)
|
data/lib/hexapdf/layout/style.rb
CHANGED
@@ -1069,7 +1069,8 @@ module HexaPDF
|
|
1069
1069
|
|
1070
1070
|
# The font size scaled appropriately.
|
1071
1071
|
def scaled_font_size
|
1072
|
-
@scaled_font_size ||= calculated_font_size
|
1072
|
+
@scaled_font_size ||= calculated_font_size * font.pdf_object.glyph_scaling_factor *
|
1073
|
+
scaled_horizontal_scaling
|
1073
1074
|
end
|
1074
1075
|
|
1075
1076
|
# The character spacing scaled appropriately.
|
data/lib/hexapdf/object.rb
CHANGED
@@ -333,10 +333,12 @@ module HexaPDF
|
|
333
333
|
(oid == other.oid ? gen <=> other.gen : oid <=> other.oid)
|
334
334
|
end
|
335
335
|
|
336
|
-
# Returns +true+ if the other object is an Object and wraps the same #data structure,
|
337
|
-
# other object is a Reference with the same oid/gen
|
336
|
+
# Returns +true+ if the other object is an Object and wraps the same #data structure, if the
|
337
|
+
# other object is a Reference with the same oid/gen, or if this object is not indirect and its
|
338
|
+
# value is equal to the other object.
|
338
339
|
def ==(other)
|
339
|
-
(other.kind_of?(Object) && data == other.data) || (other.kind_of?(Reference) && other == self)
|
340
|
+
(other.kind_of?(Object) && data == other.data) || (other.kind_of?(Reference) && other == self) ||
|
341
|
+
(!indirect? && other == data.value)
|
340
342
|
end
|
341
343
|
|
342
344
|
# Returns +true+ if the other object references the same PDF object as this object.
|
data/lib/hexapdf/parser.rb
CHANGED
@@ -62,9 +62,15 @@ module HexaPDF
|
|
62
62
|
@object_stream_data = {}
|
63
63
|
@reconstructed_revision = nil
|
64
64
|
@in_reconstruct_revision = false
|
65
|
+
@contains_xref_streams = false
|
65
66
|
retrieve_pdf_header_offset_and_version
|
66
67
|
end
|
67
68
|
|
69
|
+
# Returns +true+ if the PDF file contains cross-reference streams.
|
70
|
+
def contains_xref_streams?
|
71
|
+
@contains_xref_streams
|
72
|
+
end
|
73
|
+
|
68
74
|
# Loads the indirect (potentially compressed) object specified by the given cross-reference
|
69
75
|
# entry.
|
70
76
|
#
|
@@ -230,6 +236,7 @@ module HexaPDF
|
|
230
236
|
maybe_raise("Cross-reference stream doesn't contain entry for itself", pos: pos)
|
231
237
|
xref_section.add_in_use_entry(obj.oid, obj.gen, pos)
|
232
238
|
end
|
239
|
+
@contains_xref_streams = true
|
233
240
|
end
|
234
241
|
xref_section.delete(0)
|
235
242
|
[xref_section, trailer]
|
@@ -335,7 +342,8 @@ module HexaPDF
|
|
335
342
|
step_size = 1024
|
336
343
|
pos = @io.pos
|
337
344
|
eof_not_found = pos == 0
|
338
|
-
startxref_missing = false
|
345
|
+
startxref_missing = startxref_mangled = false
|
346
|
+
startxref_offset = nil
|
339
347
|
|
340
348
|
while pos != 0
|
341
349
|
@io.pos = [pos - step_size, 0].max
|
@@ -343,27 +351,31 @@ module HexaPDF
|
|
343
351
|
lines = @io.read(step_size + 40).split(/[\r\n]+/)
|
344
352
|
|
345
353
|
eof_index = lines.rindex {|l| l.strip == '%%EOF' }
|
346
|
-
|
354
|
+
if !eof_index
|
347
355
|
eof_not_found = true
|
348
|
-
|
349
|
-
|
350
|
-
|
356
|
+
elsif lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
|
357
|
+
startxref_offset = $1.to_i
|
358
|
+
startxref_mangled = true
|
359
|
+
break # we found it even if it the syntax is not entirely correct
|
360
|
+
elsif eof_index < 2 || lines[eof_index - 2].strip != "startxref"
|
351
361
|
startxref_missing = true
|
352
|
-
|
362
|
+
else
|
363
|
+
startxref_offset = lines[eof_index - 1].to_i
|
364
|
+
break # we found it
|
353
365
|
end
|
354
|
-
|
355
|
-
break # we found the startxref offset
|
356
366
|
end
|
357
367
|
|
358
368
|
if eof_not_found
|
359
369
|
maybe_raise("PDF file trailer with end-of-file marker not found", pos: pos,
|
360
370
|
force: !eof_index)
|
371
|
+
elsif startxref_mangled
|
372
|
+
maybe_raise("PDF file trailer keyword startxref on same line as value", pos: pos)
|
361
373
|
elsif startxref_missing
|
362
374
|
maybe_raise("PDF file trailer is missing startxref keyword", pos: pos,
|
363
375
|
force: eof_index < 2 || lines[eof_index - 2].strip != "startxref")
|
364
376
|
end
|
365
377
|
|
366
|
-
@startxref_offset =
|
378
|
+
@startxref_offset = startxref_offset
|
367
379
|
end
|
368
380
|
|
369
381
|
# Returns the reconstructed revision.
|
data/lib/hexapdf/rectangle.rb
CHANGED
@@ -114,12 +114,6 @@ module HexaPDF
|
|
114
114
|
self[3] = self[1] + val
|
115
115
|
end
|
116
116
|
|
117
|
-
# Compares this rectangle to +other+ like in Object#== but also allows comparison to simple
|
118
|
-
# arrays if the rectangle is a direct object.
|
119
|
-
def ==(other)
|
120
|
-
super || (other.kind_of?(Array) && !indirect? && other == data.value)
|
121
|
-
end
|
122
|
-
|
123
117
|
private
|
124
118
|
|
125
119
|
# Ensures that the value is an array containing four numbers that specify the bottom left and
|
data/lib/hexapdf/revision.rb
CHANGED
@@ -190,7 +190,7 @@ module HexaPDF
|
|
190
190
|
obj.data.value = nil
|
191
191
|
obj.document = nil
|
192
192
|
if mark_as_free
|
193
|
-
add_without_check(HexaPDF::Object.new(
|
193
|
+
add_without_check(HexaPDF::Object.new(obj.data))
|
194
194
|
else
|
195
195
|
@xref_section.delete(ref_or_oid)
|
196
196
|
@objects.delete(ref_or_oid)
|
@@ -228,7 +228,8 @@ module HexaPDF
|
|
228
228
|
# revision.each_modified_object {|obj| block } -> revision
|
229
229
|
# revision.each_modified_object -> Enumerator
|
230
230
|
#
|
231
|
-
# Calls the given block once for each object that has been modified since it was loaded.
|
231
|
+
# Calls the given block once for each object that has been modified since it was loaded. Deleted
|
232
|
+
# object and cross-reference streams are ignored.
|
232
233
|
#
|
233
234
|
# Note that this also means that for revisions without an associated cross-reference section all
|
234
235
|
# loaded objects will be yielded.
|
@@ -238,12 +239,18 @@ module HexaPDF
|
|
238
239
|
@objects.each do |oid, gen, obj|
|
239
240
|
if @xref_section.entry?(oid, gen)
|
240
241
|
stored_obj = @loader.call(@xref_section[oid, gen])
|
241
|
-
if
|
242
|
-
|
242
|
+
next if (stored_obj.type == :ObjStm || stored_obj.type == :XRef) && obj.null?
|
243
|
+
|
244
|
+
streams_are_same = (obj.data.stream == stored_obj.data.stream)
|
245
|
+
next if obj.value == stored_obj.value && streams_are_same
|
246
|
+
|
247
|
+
if obj.value.kind_of?(Hash) && stored_obj.value.kind_of?(Hash)
|
248
|
+
keys = obj.value.keys | stored_obj.value.keys
|
249
|
+
next if keys.all? {|key| obj[key] == stored_obj[key] } && streams_are_same
|
243
250
|
end
|
244
|
-
else
|
245
|
-
yield(obj)
|
246
251
|
end
|
252
|
+
|
253
|
+
yield(obj)
|
247
254
|
end
|
248
255
|
|
249
256
|
self
|
@@ -458,10 +458,8 @@ module HexaPDF
|
|
458
458
|
option_items = @field.option_items
|
459
459
|
top_index = @field.list_box_top_index
|
460
460
|
items = [Layout::TextFragment.create(option_items[top_index..-1].join("\n"), style)]
|
461
|
-
|
462
|
-
indices = @field
|
463
|
-
value_indices = [@field.field_value].flatten.compact.map {|val| option_items.index(val) }
|
464
|
-
indices = value_indices if indices != value_indices
|
461
|
+
# Should use /I but if it differs from /V, we need to use /V; so just use /V...
|
462
|
+
indices = [@field.field_value].flatten.compact.map {|val| option_items.index(val) }
|
465
463
|
|
466
464
|
layouter = Layout::TextLayouter.new(style)
|
467
465
|
layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
|
@@ -75,7 +75,7 @@ module HexaPDF
|
|
75
75
|
|
76
76
|
define_type :XXAcroForm
|
77
77
|
|
78
|
-
define_field :Fields, type: PDFArray, required: true, version: '1.2'
|
78
|
+
define_field :Fields, type: PDFArray, required: true, default: [], version: '1.2'
|
79
79
|
define_field :NeedAppearances, type: Boolean, default: false
|
80
80
|
define_field :SigFlags, type: Integer, version: '1.3'
|
81
81
|
define_field :CO, type: PDFArray, version: '1.3'
|
@@ -323,6 +323,14 @@ module HexaPDF
|
|
323
323
|
end
|
324
324
|
end
|
325
325
|
|
326
|
+
# Creates a signature field with the given name and adds it to the form.
|
327
|
+
#
|
328
|
+
# The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
|
329
|
+
# fields must already exist. If it doesn't contain dots, a top-level field is created.
|
330
|
+
def create_signature_field(name)
|
331
|
+
create_field(name, :Sig) {}
|
332
|
+
end
|
333
|
+
|
326
334
|
# Returns the dictionary containing the default resources for form field appearance streams.
|
327
335
|
def default_resources
|
328
336
|
self[:DR] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
|