hexapdf 0.19.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/data/hexapdf/cert/demo_cert.rb +22 -0
  4. data/data/hexapdf/cert/root-ca.crt +119 -0
  5. data/data/hexapdf/cert/signing.crt +125 -0
  6. data/data/hexapdf/cert/signing.key +52 -0
  7. data/data/hexapdf/cert/sub-ca.crt +125 -0
  8. data/lib/hexapdf/cli/info.rb +21 -1
  9. data/lib/hexapdf/configuration.rb +26 -0
  10. data/lib/hexapdf/content/graphics_state.rb +24 -5
  11. data/lib/hexapdf/content/processor.rb +1 -1
  12. data/lib/hexapdf/document/signatures.rb +327 -0
  13. data/lib/hexapdf/document.rb +26 -0
  14. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -2
  15. data/lib/hexapdf/importer.rb +1 -1
  16. data/lib/hexapdf/layout/style.rb +2 -1
  17. data/lib/hexapdf/object.rb +5 -3
  18. data/lib/hexapdf/parser.rb +21 -9
  19. data/lib/hexapdf/rectangle.rb +0 -6
  20. data/lib/hexapdf/revision.rb +13 -6
  21. data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
  22. data/lib/hexapdf/type/acro_form/field.rb +2 -0
  23. data/lib/hexapdf/type/acro_form/form.rb +9 -1
  24. data/lib/hexapdf/type/annotation.rb +36 -3
  25. data/lib/hexapdf/type/font.rb +5 -0
  26. data/lib/hexapdf/type/font_simple.rb +1 -1
  27. data/lib/hexapdf/type/font_type3.rb +20 -0
  28. data/lib/hexapdf/type/object_stream.rb +3 -1
  29. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +125 -0
  30. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +99 -0
  31. data/lib/hexapdf/type/signature/handler.rb +112 -0
  32. data/lib/hexapdf/type/signature/verification_result.rb +92 -0
  33. data/lib/hexapdf/type/signature.rb +236 -0
  34. data/lib/hexapdf/type.rb +1 -0
  35. data/lib/hexapdf/version.rb +1 -1
  36. data/lib/hexapdf/writer.rb +24 -10
  37. data/test/hexapdf/content/test_graphics_state.rb +9 -1
  38. data/test/hexapdf/content/test_operator.rb +8 -3
  39. data/test/hexapdf/content/test_processor.rb +1 -1
  40. data/test/hexapdf/document/test_signatures.rb +225 -0
  41. data/test/hexapdf/encryption/test_standard_security_handler.rb +8 -6
  42. data/test/hexapdf/layout/test_style.rb +11 -0
  43. data/test/hexapdf/test_document.rb +28 -0
  44. data/test/hexapdf/test_object.rb +7 -2
  45. data/test/hexapdf/test_parser.rb +14 -0
  46. data/test/hexapdf/test_rectangle.rb +0 -7
  47. data/test/hexapdf/test_revision.rb +44 -14
  48. data/test/hexapdf/test_writer.rb +44 -14
  49. data/test/hexapdf/type/acro_form/test_field.rb +11 -1
  50. data/test/hexapdf/type/acro_form/test_form.rb +5 -0
  51. data/test/hexapdf/type/signature/common.rb +71 -0
  52. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
  53. data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
  54. data/test/hexapdf/type/signature/test_handler.rb +76 -0
  55. data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
  56. data/test/hexapdf/type/test_annotation.rb +40 -2
  57. data/test/hexapdf/type/test_font.rb +4 -0
  58. data/test/hexapdf/type/test_font_simple.rb +5 -5
  59. data/test/hexapdf/type/test_font_type3.rb +16 -1
  60. data/test/hexapdf/type/test_object_stream.rb +9 -0
  61. data/test/hexapdf/type/test_signature.rb +131 -0
  62. metadata +21 -33
  63. data/test/data/cert/create.sh +0 -171
  64. data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF39.pem +0 -119
  65. data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF3A.pem +0 -125
  66. data/test/data/cert/root-ca/db/crlnumber +0 -1
  67. data/test/data/cert/root-ca/db/index +0 -2
  68. data/test/data/cert/root-ca/db/index.attr +0 -1
  69. data/test/data/cert/root-ca/db/index.attr.old +0 -1
  70. data/test/data/cert/root-ca/db/index.old +0 -1
  71. data/test/data/cert/root-ca/db/serial +0 -1
  72. data/test/data/cert/root-ca/db/serial.old +0 -1
  73. data/test/data/cert/root-ca/private/root-ca.key +0 -52
  74. data/test/data/cert/root-ca/root-ca.conf +0 -65
  75. data/test/data/cert/root-ca/root-ca.crt +0 -119
  76. data/test/data/cert/root-ca/root-ca.csr +0 -28
  77. data/test/data/cert/signature-1-pkcs7-detached.pdf +0 -182
  78. data/test/data/cert/sub-ca/certs/453FF080E3EDCD6A388D5368DFC320D9.pem +0 -125
  79. data/test/data/cert/sub-ca/db/crlnumber +0 -1
  80. data/test/data/cert/sub-ca/db/index +0 -1
  81. data/test/data/cert/sub-ca/db/index.attr +0 -1
  82. data/test/data/cert/sub-ca/db/index.old +0 -0
  83. data/test/data/cert/sub-ca/db/serial +0 -1
  84. data/test/data/cert/sub-ca/db/serial.old +0 -1
  85. data/test/data/cert/sub-ca/private/signing.key +0 -52
  86. data/test/data/cert/sub-ca/private/sub-ca.key +0 -52
  87. data/test/data/cert/sub-ca/signing.crt +0 -125
  88. data/test/data/cert/sub-ca/signing.csr +0 -28
  89. data/test/data/cert/sub-ca/signing.p12 +0 -0
  90. data/test/data/cert/sub-ca/sub-ca.conf +0 -65
  91. data/test/data/cert/sub-ca/sub-ca.crt +0 -125
  92. 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
- attr_accessor :font
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 divided by 1000 multiplied by #scaled_horizontal_scaling.
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
- @scaled_font_size = size / 1000.0 * @scaled_horizontal_scaling
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
- @scaled_font_size = @font_size / 1000.0 * @scaled_horizontal_scaling
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 NotImplementedError
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
@@ -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
- raise(HexaPDF::EncryptionError,
332
- "Document ID for needed for decryption")
331
+ document.trailer[:ID] = ['', '']
333
332
  end
334
333
  @trailer_id_hash = trailer_id_hash
335
334
 
@@ -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 == @destination.object(mapped_object)
96
+ elsif mapped_object && !mapped_object.null?
97
97
  mapped_object
98
98
  else
99
99
  duplicate(object)
@@ -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 / 1000.0 * scaled_horizontal_scaling
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.
@@ -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, or if the
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.
@@ -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
- unless eof_index
354
+ if !eof_index
347
355
  eof_not_found = true
348
- next
349
- end
350
- unless eof_index >= 2 && lines[eof_index - 2].strip == "startxref"
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
- next
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 = lines[eof_index - 1].to_i
378
+ @startxref_offset = startxref_offset
367
379
  end
368
380
 
369
381
  # Returns the reconstructed revision.
@@ -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
@@ -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(nil, oid: obj.oid, gen: obj.gen))
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 obj.data.value != stored_obj.data.value || obj.data.stream != stored_obj.data.stream
242
- yield(obj)
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[:I] || []
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)
@@ -310,6 +310,8 @@ module HexaPDF
310
310
  widget = document.wrap(self)
311
311
  end
312
312
 
313
+ widget.flag(:print)
314
+ widget[:P] = page
313
315
  (page[:Annots] ||= []) << widget
314
316
 
315
317
  widget
@@ -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]},