hexapdf 0.19.3 → 0.20.3

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.
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.