hexapdf 0.19.0 → 0.20.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 +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]},
|