hexapdf 0.19.3 → 0.20.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -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/data/hexapdf/encoding/glyphlist.txt +4283 -4282
  9. data/data/hexapdf/encoding/zapfdingbats.txt +203 -202
  10. data/lib/hexapdf/cli/form.rb +9 -1
  11. data/lib/hexapdf/cli/info.rb +21 -1
  12. data/lib/hexapdf/configuration.rb +26 -0
  13. data/lib/hexapdf/content/processor.rb +1 -1
  14. data/lib/hexapdf/document/signatures.rb +327 -0
  15. data/lib/hexapdf/document.rb +26 -0
  16. data/lib/hexapdf/encryption/security_handler.rb +5 -1
  17. data/lib/hexapdf/font/encoding/glyph_list.rb +5 -6
  18. data/lib/hexapdf/importer.rb +1 -1
  19. data/lib/hexapdf/object.rb +5 -3
  20. data/lib/hexapdf/rectangle.rb +0 -6
  21. data/lib/hexapdf/revision.rb +13 -6
  22. data/lib/hexapdf/task/dereference.rb +12 -4
  23. data/lib/hexapdf/task/optimize.rb +3 -3
  24. data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
  25. data/lib/hexapdf/type/acro_form/field.rb +2 -0
  26. data/lib/hexapdf/type/acro_form/form.rb +9 -1
  27. data/lib/hexapdf/type/annotation.rb +37 -4
  28. data/lib/hexapdf/type/font_simple.rb +1 -1
  29. data/lib/hexapdf/type/object_stream.rb +3 -1
  30. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +121 -0
  31. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +95 -0
  32. data/lib/hexapdf/type/signature/handler.rb +140 -0
  33. data/lib/hexapdf/type/signature/verification_result.rb +92 -0
  34. data/lib/hexapdf/type/signature.rb +236 -0
  35. data/lib/hexapdf/type.rb +1 -0
  36. data/lib/hexapdf/version.rb +1 -1
  37. data/lib/hexapdf/writer.rb +27 -11
  38. data/test/hexapdf/content/test_processor.rb +1 -1
  39. data/test/hexapdf/document/test_signatures.rb +225 -0
  40. data/test/hexapdf/encryption/test_security_handler.rb +11 -3
  41. data/test/hexapdf/task/test_optimize.rb +4 -1
  42. data/test/hexapdf/test_document.rb +28 -0
  43. data/test/hexapdf/test_object.rb +7 -2
  44. data/test/hexapdf/test_rectangle.rb +0 -7
  45. data/test/hexapdf/test_revision.rb +44 -14
  46. data/test/hexapdf/test_writer.rb +15 -3
  47. data/test/hexapdf/type/acro_form/test_field.rb +11 -1
  48. data/test/hexapdf/type/acro_form/test_form.rb +5 -0
  49. data/test/hexapdf/type/signature/common.rb +71 -0
  50. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
  51. data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
  52. data/test/hexapdf/type/signature/test_handler.rb +102 -0
  53. data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
  54. data/test/hexapdf/type/test_annotation.rb +47 -3
  55. data/test/hexapdf/type/test_font_simple.rb +5 -5
  56. data/test/hexapdf/type/test_object_stream.rb +9 -0
  57. data/test/hexapdf/type/test_signature.rb +131 -0
  58. metadata +21 -3
@@ -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
  #
@@ -249,6 +249,10 @@ module HexaPDF
249
249
  @document = document
250
250
  @encrypt_dict_hash = nil
251
251
  @encryption_details = {}
252
+
253
+ @is_encrypt_dict = document.revisions.each.with_object({}) do |rev, hash|
254
+ hash[rev.trailer[:Encrypt]] = true
255
+ end
252
256
  end
253
257
 
254
258
  # Checks if the encryption key computed by this security handler is derived from the
@@ -262,7 +266,7 @@ module HexaPDF
262
266
  #
263
267
  # See: PDF1.7 s7.6.2
264
268
  def decrypt(obj)
265
- return obj if obj == document.trailer[:Encrypt] || obj.type == :XRef
269
+ return obj if @is_encrypt_dict[obj] || obj.type == :XRef
266
270
 
267
271
  key = object_key(obj.oid, obj.gen, string_algorithm)
268
272
  each_string_in_object(obj.value) do |str|
@@ -131,13 +131,12 @@ module HexaPDF
131
131
  def load_file(file)
132
132
  name2uni = {}
133
133
  uni2name = {}
134
- File.open(file, 'rb') do |f|
134
+ File.open(file, 'r:UTF-8') do |f|
135
+ 25.times { f.gets } # Skip comments
135
136
  while (line = f.gets)
136
- next if line.start_with?('#')
137
- index = line.index(';')
138
- name = line[0, index].to_sym
139
- codes = line[index + 1, 50].split(" ").map(&:hex).pack('U*')
140
- name2uni[name] = codes
137
+ name, codes = line.split(';', 2)
138
+ name = name.to_sym
139
+ name2uni[name] = codes.chomp!
141
140
  uni2name[codes] = name unless uni2name.key?(codes)
142
141
  end
143
142
  end
@@ -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)
@@ -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.
@@ -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
@@ -68,9 +68,9 @@ module HexaPDF
68
68
 
69
69
  def execute #:nodoc:
70
70
  if @object
71
- dereference(@object)
71
+ dereference_all(@object)
72
72
  else
73
- dereference(@doc.trailer)
73
+ dereference_all(@doc.trailer)
74
74
  @result = []
75
75
  @doc.each(only_current: false) do |obj|
76
76
  if !@seen.key?(obj.data) && obj.type != :ObjStm && obj.type != :XRef
@@ -83,6 +83,11 @@ module HexaPDF
83
83
  end
84
84
  end
85
85
 
86
+ def dereference_all(object) # :nodoc:
87
+ @dereference_later = [object]
88
+ dereference(@dereference_later.pop) until @dereference_later.empty?
89
+ end
90
+
86
91
  def dereference(object) #:nodoc:
87
92
  return object if object.nil? || @seen.key?(object.data)
88
93
  @seen[object.data] = true
@@ -97,9 +102,12 @@ module HexaPDF
97
102
  when Array
98
103
  val.map! {|v| recurse(v) }
99
104
  when HexaPDF::Reference
100
- dereference(@doc.object(val))
105
+ val = @doc.object(val)
106
+ @dereference_later.push(val)
107
+ val
101
108
  when HexaPDF::Object
102
- dereference(val)
109
+ @dereference_later.push(val)
110
+ val
103
111
  else
104
112
  val
105
113
  end
@@ -234,7 +234,7 @@ module HexaPDF
234
234
  page.contents = processor.result
235
235
  page[:Contents].set_filter(:FlateDecode)
236
236
  xobjects = page.resources[:XObject]
237
- processor.used_references.each {|ref| used_refs[xobjects[ref]] = true }
237
+ processor.used_references.each {|ref| used_refs[xobjects[ref]] = true } if xobjects
238
238
  end
239
239
  used_refs
240
240
  end
@@ -245,7 +245,7 @@ module HexaPDF
245
245
  unless used_refs
246
246
  used_refs = {}
247
247
  doc.pages.each do |page|
248
- xobjects = page.resources[:XObject]
248
+ next unless (xobjects = page.resources[:XObject])
249
249
  HexaPDF::Content::Parser.parse(page.contents) do |op, operands|
250
250
  used_refs[xobjects[operands[0]]] = true if op == :Do
251
251
  end
@@ -253,7 +253,7 @@ module HexaPDF
253
253
  end
254
254
 
255
255
  doc.pages.each do |page|
256
- xobjects = page.resources[:XObject]
256
+ next unless (xobjects = page.resources[:XObject])
257
257
  xobjects.each do |key, obj|
258
258
  next if used_refs[obj]
259
259
  xobjects.delete(key)
@@ -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]},
@@ -78,6 +78,25 @@ module HexaPDF
78
78
  self[:D] || self[:N]
79
79
  end
80
80
 
81
+ APPEARANCE_TYPE_TO_KEY = {normal: :N, rollover: :R, down: :D}.freeze #:nodoc:
82
+
83
+ # Sets the appearance of the given appearance +type+, which can either be :normal, :rollover
84
+ # or :down, to +appearance+.
85
+ #
86
+ # If the +state_name+ argument is provided, the +appearance+ is stored under the
87
+ # +state_name+ key in a sub-dictionary of the appearance.
88
+ def set_appearance(appearance, type: :normal, state_name: nil)
89
+ key = APPEARANCE_TYPE_TO_KEY.fetch(type) do
90
+ raise ArgumentError, "Invalid value for type specified: #{type.inspect}"
91
+ end
92
+ if state_name
93
+ self[key] = {} unless value[key].kind_of?(Hash)
94
+ self[key][state_name] = appearance
95
+ else
96
+ self[key] = appearance
97
+ end
98
+ end
99
+
81
100
  end
82
101
 
83
102
  # Border style dictionary used by various annotation types.
@@ -132,15 +151,16 @@ module HexaPDF
132
151
  # Returns the annotation's appearance stream of the given type (:normal, :rollover, or :down)
133
152
  # or +nil+ if it doesn't exist.
134
153
  #
135
- # The appearance state is taken into account if necessary.
136
- def appearance(type = :normal)
154
+ # The appearance state in /AS or the one provided via +state_name+ is taken into account if
155
+ # necessary.
156
+ def appearance(type: :normal, state_name: self[:AS])
137
157
  entry = appearance_dict&.send("#{type}_appearance")
138
158
  if entry.kind_of?(HexaPDF::Dictionary) && !entry.kind_of?(HexaPDF::Stream)
139
- entry = entry[self[:AS]]
159
+ entry = entry[state_name]
140
160
  end
141
161
  return unless entry.kind_of?(HexaPDF::Stream)
142
162
 
143
- if entry.type == :XObject && entry[:Subtype] == :Form
163
+ if entry.type == :XObject && entry[:Subtype] == :Form && !entry.instance_of?(HexaPDF::Stream)
144
164
  entry
145
165
  elsif (entry[:Type].nil? || entry[:Type] == :XObject) &&
146
166
  (entry[:Subtype].nil? || entry[:Subtype] == :Form) && entry[:BBox]
@@ -149,6 +169,19 @@ module HexaPDF
149
169
  end
150
170
  alias appearance? appearance
151
171
 
172
+ # Creates an empty appearance stream (a Form XObject) of the given type (:normal, :rollover,
173
+ # or :down) and returns it. If an appearance stream already exist, it is overwritten.
174
+ #
175
+ # If there can be multiple appearance streams for the annotation, use the +state_name+
176
+ # argument to provide the appearance state name.
177
+ def create_appearance(type: :normal, state_name: self[:AS])
178
+ xobject = document.add({Type: :XObject, Subtype: :Form,
179
+ BBox: [0, 0, self[:Rect].width, self[:Rect].height]})
180
+ self[:AP] ||= {}
181
+ appearance_dict.set_appearance(xobject, type: type, state_name: state_name)
182
+ xobject
183
+ end
184
+
152
185
  private
153
186
 
154
187
  # Helper method for bit field getter access.