hexapdf 0.14.4 → 0.15.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/lib/hexapdf/cli/form.rb +30 -8
  4. data/lib/hexapdf/configuration.rb +18 -3
  5. data/lib/hexapdf/content/canvas.rb +1 -0
  6. data/lib/hexapdf/encryption/standard_security_handler.rb +16 -0
  7. data/lib/hexapdf/error.rb +4 -3
  8. data/lib/hexapdf/parser.rb +18 -6
  9. data/lib/hexapdf/revision.rb +16 -0
  10. data/lib/hexapdf/type/acro_form.rb +1 -0
  11. data/lib/hexapdf/type/acro_form/appearance_generator.rb +29 -17
  12. data/lib/hexapdf/type/acro_form/button_field.rb +8 -4
  13. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  14. data/lib/hexapdf/type/acro_form/form.rb +37 -0
  15. data/lib/hexapdf/type/acro_form/signature_field.rb +223 -0
  16. data/lib/hexapdf/type/annotation.rb +18 -9
  17. data/lib/hexapdf/type/annotations/widget.rb +3 -1
  18. data/lib/hexapdf/type/font_descriptor.rb +9 -2
  19. data/lib/hexapdf/type/page.rb +81 -0
  20. data/lib/hexapdf/utils/graphics_helpers.rb +4 -4
  21. data/lib/hexapdf/version.rb +1 -1
  22. data/test/hexapdf/content/test_canvas.rb +21 -0
  23. data/test/hexapdf/encryption/test_standard_security_handler.rb +27 -0
  24. data/test/hexapdf/test_parser.rb +23 -3
  25. data/test/hexapdf/test_revision.rb +21 -0
  26. data/test/hexapdf/test_writer.rb +2 -2
  27. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +21 -2
  28. data/test/hexapdf/type/acro_form/test_button_field.rb +13 -7
  29. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  30. data/test/hexapdf/type/acro_form/test_form.rb +46 -2
  31. data/test/hexapdf/type/acro_form/test_signature_field.rb +38 -0
  32. data/test/hexapdf/type/annotations/test_widget.rb +2 -0
  33. data/test/hexapdf/type/test_annotation.rb +24 -10
  34. data/test/hexapdf/type/test_font_descriptor.rb +7 -0
  35. data/test/hexapdf/type/test_page.rb +187 -49
  36. data/test/hexapdf/utils/test_graphics_helpers.rb +8 -0
  37. metadata +4 -2
@@ -315,6 +315,7 @@ module HexaPDF
315
315
 
316
316
  if embedded_widget?
317
317
  WIDGET_FIELDS.each {|key| delete(key) }
318
+ document.revisions.each {|revision| break if revision.update(self)}
318
319
  else
319
320
  self[:Kids].delete_at(widget_index)
320
321
  document.delete(widget)
@@ -331,6 +331,43 @@ module HexaPDF
331
331
  end
332
332
  end
333
333
 
334
+ # Flattens the whole interactive form or only the given fields, and returns the fields that
335
+ # couldn't be flattened.
336
+ #
337
+ # Flattening means making the appearance streams of the field widgets part of the respective
338
+ # page's content stream and removing the fields themselves.
339
+ #
340
+ # If the whole interactive form is flattened, the form object itself is also removed if all
341
+ # fields were flattened.
342
+ #
343
+ # The +create_appearances+ argument controls whether missing appearances should
344
+ # automatically be created.
345
+ #
346
+ # See: HexaPDF::Type::Page#flatten_annotations
347
+ def flatten(fields: nil, create_appearances: true)
348
+ remove_form = fields.nil?
349
+ fields ||= each_field.to_a
350
+ if create_appearances
351
+ fields.each {|field| field.create_appearances if field.respond_to?(:create_appearances) }
352
+ end
353
+
354
+ not_flattened = fields.map {|field| field.each_widget.to_a }.flatten
355
+ document.pages.each {|page| not_flattened = page.flatten_annotations(not_flattened) }
356
+ fields -= not_flattened.map(&:form_field)
357
+
358
+ fields.each do |field|
359
+ (field[:Parent]&.[](:Kids) || self[:Fields]).delete(field)
360
+ document.delete(field)
361
+ end
362
+
363
+ if remove_form && not_flattened.empty?
364
+ document.catalog.delete(:AcroForm)
365
+ document.delete(self)
366
+ end
367
+
368
+ not_flattened
369
+ end
370
+
334
371
  private
335
372
 
336
373
  # Helper method for bit field getter access.
@@ -0,0 +1,223 @@
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 'hexapdf/type/acro_form/field'
38
+ require 'hexapdf/type/acro_form/appearance_generator'
39
+
40
+ module HexaPDF
41
+ module Type
42
+ module AcroForm
43
+
44
+ # AcroForm signature fields represent a digital signature.
45
+ #
46
+ # It serves two purposes: To visually display the signature and to hold the information of the
47
+ # digital signature itself.
48
+ #
49
+ # If the signature should not be visible, the associated widget annotation should have zero
50
+ # width and height; and/or the 'hidden' or 'no_view' flags of the annotation should be set.
51
+ #
52
+ # See: PDF1.7 s12.7.4.5
53
+ class SignatureField < Field
54
+
55
+ # A signature field lock dictionary specifies a set of form fields that should be locked
56
+ # once the associated signature field is signed.
57
+ #
58
+ # See: PDF1.7 s12.7.4.5
59
+ class LockDictionary < Dictionary
60
+
61
+ define_type :SigFieldLock
62
+
63
+ define_field :Type, type: Symbol, default: type
64
+ define_field :Action, type: Symbol, required: true,
65
+ allowed_values: [:All, :Include, :Exclude]
66
+ define_field :Fields, type: PDFArray
67
+
68
+ private
69
+
70
+ def perform_validation #:nodoc:
71
+ if self[:Action] != :All && !key?(:Fields)
72
+ yield("The /Fields key of the signature lock dictionary is missing")
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ # A seed value dictionary contains information that constrains the properties of a signature
79
+ # that is applied to the associated signature field.
80
+ #
81
+ # == Flags
82
+ #
83
+ # If a flag is set it means that the associated entry is a required constraint. Otherwise it
84
+ # is optional.
85
+ #
86
+ # The available flags are: filter, sub_filter, v, reasons, legal_attestation, add_rev_info
87
+ # and digest_method.
88
+ #
89
+ # See: PDF1.7 s12.7.4.5
90
+ class SeedValueDictionary < Dictionary
91
+
92
+ extend Utils::BitField
93
+
94
+ define_type :SV
95
+
96
+ define_field :Type, type: Symbol, default: type
97
+ define_field :Ff, type: Integer, default: 0
98
+ define_field :Filter, type: Symbol
99
+ define_field :SubFilter, type: PDFArray
100
+ define_field :DigestMethod, type: PDFArray, version: '1.7'
101
+ define_field :V, type: Float
102
+ define_field :Cert, type: :SVCert
103
+ define_field :Reasons, type: PDFArray
104
+ define_field :MDP, type: Dictionary, version: '1.6'
105
+ define_field :TimeStamp, type: Dictionary, version: '1.6'
106
+ define_field :LegalAttestation, type: PDFArray, version: '1.6'
107
+ define_field :AddRevInfo, type: Boolean, version: '1.7'
108
+
109
+ ##
110
+ # :method: flags
111
+ #
112
+ # Returns an array of flag names representing the set bit flags.
113
+ #
114
+
115
+ ##
116
+ # :method: flagged?
117
+ # :call-seq:
118
+ # flagged?(flag)
119
+ #
120
+ # Returns +true+ if the given flag is set. The argument can either be the flag name or the
121
+ # bit index.
122
+ #
123
+
124
+ ##
125
+ # :method: flag
126
+ # :call-seq:
127
+ # flag(*flags, clear_existing: false)
128
+ #
129
+ # Sets the given flags, given as flag names or bit indices. If +clear_existing+ is +true+,
130
+ # all prior flags will be cleared.
131
+ #
132
+ bit_field(:flags, {filter: 0, sub_filter: 1, v: 2, reasons: 3, legal_attestation: 4,
133
+ add_rev_info: 5, digest_method: 6},
134
+ lister: "flags", getter: "flagged?", setter: "flag", unsetter: "unflag",
135
+ value_getter: "self[:Ff]", value_setter: "self[:Ff]")
136
+
137
+ end
138
+
139
+ # A certificate seed value dictionary contains information about the characteristics of the
140
+ # certificate that shall be used when signing.
141
+ #
142
+ # == Flags
143
+ #
144
+ # The flags describe the entries that a signer is required to use.
145
+ #
146
+ # The available flags are: subject, issuer, oid, subject_dn, reserved, key_usage and url.
147
+ #
148
+ # See: PDF1.7 s12.7.4.5
149
+ class CertificateSeedValueDictionary < Dictionary
150
+
151
+ extend Utils::BitField
152
+
153
+ define_type :SVCert
154
+
155
+ define_field :Type, type: Symbol, default: type
156
+ define_field :Ff, type: Integer, default: 0
157
+ define_field :Subject, type: PDFArray
158
+ define_field :SubjectDN, type: PDFArray, version: '1.7'
159
+ define_field :KeyUsage, type: PDFArray, version: '1.7'
160
+ define_field :Issuer, type: PDFArray
161
+ define_field :OID, type: PDFArray
162
+ define_field :URL, type: String
163
+ define_field :URLType, type: Symbol, default: :Browser
164
+
165
+ ##
166
+ # :method: flags
167
+ #
168
+ # Returns an array of flag names representing the set bit flags.
169
+ #
170
+
171
+ ##
172
+ # :method: flagged?
173
+ # :call-seq:
174
+ # flagged?(flag)
175
+ #
176
+ # Returns +true+ if the given flag is set. The argument can either be the flag name or the
177
+ # bit index.
178
+ #
179
+
180
+ ##
181
+ # :method: flag
182
+ # :call-seq:
183
+ # flag(*flags, clear_existing: false)
184
+ #
185
+ # Sets the given flags, given as flag names or bit indices. If +clear_existing+ is +true+,
186
+ # all prior flags will be cleared.
187
+ #
188
+ bit_field(:flags, {subject: 0, issuer: 1, oid: 2, subject_dn: 3, reserved: 4,
189
+ key_usage: 5, url: 6},
190
+ lister: "flags", getter: "flagged?", setter: "flag", unsetter: "unflag",
191
+ value_getter: "self[:Ff]", value_setter: "self[:Ff]")
192
+
193
+ end
194
+
195
+ define_field :Lock, type: :SigFieldLock, indirect: true, version: '1.5'
196
+ define_field :SV, type: :SV, indirect: true, version: '1.5'
197
+
198
+ # Returns the associated signature dictionary or +nil+ if the signature is not filled in.
199
+ def field_value
200
+ self[:V]
201
+ end
202
+
203
+ # Sets the signature dictionary as value of this signature field.
204
+ def field_value=(sig_dict)
205
+ self[:V] = sig_dict
206
+ end
207
+
208
+ private
209
+
210
+ def perform_validation #:nodoc:
211
+ if field_type != :Sig
212
+ yield("Field /FT of AcroForm signature field has to be :Sig", true)
213
+ self[:FT] = :Sig
214
+ end
215
+
216
+ super
217
+ end
218
+
219
+ end
220
+
221
+ end
222
+ end
223
+ end
@@ -125,20 +125,29 @@ module HexaPDF
125
125
 
126
126
  # Returns the AppearanceDictionary instance associated with the annotation or +nil+ if none is
127
127
  # set.
128
- def appearance
128
+ def appearance_dict
129
129
  self[:AP]
130
130
  end
131
131
 
132
- # Returns +true+ if the widget's normal appearance exists.
132
+ # Returns the annotation's appearance stream of the given type (:normal, :rollover, or :down)
133
+ # or +nil+ if it doesn't exist.
133
134
  #
134
- # Note that this checks only if the appearance exists but not if the structure of the
135
- # appearance dictionary conforms to the expectations of the annotation.
136
- def appearance?
137
- return false unless (normal_appearance = appearance&.normal_appearance)
138
- normal_appearance.kind_of?(HexaPDF::Stream) ||
139
- (!normal_appearance.empty? &&
140
- normal_appearance.each.all? {|_k, v| v.kind_of?(HexaPDF::Stream) })
135
+ # The appearance state is taken into account if necessary.
136
+ def appearance(type = :normal)
137
+ entry = appearance_dict&.send("#{type}_appearance")
138
+ if entry.kind_of?(HexaPDF::Dictionary) && !entry.kind_of?(HexaPDF::Stream)
139
+ entry = entry[self[:AS]]
140
+ end
141
+ return unless entry.kind_of?(HexaPDF::Stream)
142
+
143
+ if entry.type == :XObject && entry[:Subtype] == :Form
144
+ entry
145
+ elsif (entry[:Type].nil? || entry[:Type] == :XObject) &&
146
+ (entry[:Subtype].nil? || entry[:Subtype] == :Form) && entry[:BBox]
147
+ document.wrap(entry, type: :XObject, subtype: :Form)
148
+ end
141
149
  end
150
+ alias appearance? appearance
142
151
 
143
152
  private
144
153
 
@@ -112,7 +112,9 @@ module HexaPDF
112
112
  def background_color(*color)
113
113
  if color.empty?
114
114
  components = self[:MK]&.[](:BG)
115
- components.nil? ? nil : Content::ColorSpace.prenormalized_device_color(components)
115
+ if components && !components.empty?
116
+ Content::ColorSpace.prenormalized_device_color(components)
117
+ end
116
118
  else
117
119
  color = Content::ColorSpace.device_color_from_specification(color)
118
120
  (self[:MK] ||= {})[:BG] = color.components
@@ -57,8 +57,7 @@ module HexaPDF
57
57
  define_field :FontStretch, type: Symbol, version: '1.5',
58
58
  allowed_values: [:UltraCondensed, :ExtraCondensed, :Condensed, :SemiCondensed,
59
59
  :Normal, :SemiExpanded, :Expanded, :ExtraExpanded, :UltraExpanded]
60
- define_field :FontWeight, type: Numeric, version: '1.5',
61
- allowed_values: [100, 200, 300, 400, 500, 600, 700, 800, 900]
60
+ define_field :FontWeight, type: Numeric, version: '1.5'
62
61
  define_field :Flags, type: Integer, required: true
63
62
  define_field :FontBBox, type: Rectangle
64
63
  define_field :ItalicAngle, type: Numeric, required: true
@@ -98,12 +97,20 @@ module HexaPDF
98
97
  self[:Flags] = value
99
98
  end
100
99
 
100
+ ALLOWED_FONT_WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900] #:nodoc:
101
+
101
102
  def perform_validation #:nodoc:
102
103
  super
103
104
  if [self[:FontFile], self[:FontFile2], self[:FontFile3]].compact.size > 1
104
105
  yield("Only one of /FontFile, /FontFile2 or /FontFile3 may be set", false)
105
106
  end
106
107
 
108
+ font_weight = self[:FontWeight]
109
+ if font_weight && !ALLOWED_FONT_WEIGHTS.include?(font_weight)
110
+ yield("Field FontWeight does not contain an allowed value", true)
111
+ delete(:FontWeight)
112
+ end
113
+
107
114
  descent = self[:Descent]
108
115
  if descent && descent > 0
109
116
  yield("The /Descent value needs to be a negative number", true)
@@ -465,6 +465,87 @@ module HexaPDF
465
465
  document.wrap(dict, stream: stream)
466
466
  end
467
467
 
468
+ # Flattens all or the given annotations of the page. Returns an array with all the annotations
469
+ # that couldn't be flattened because they don't have an appearance stream.
470
+ #
471
+ # Flattening means making the appearances of the annotations part of the content stream of the
472
+ # page and deleting the annotations themselves. Invisible and hidden fields are deleted but
473
+ # not rendered into the content stream.
474
+ #
475
+ # If an annotation is a form field widget, only the widget will be deleted but not the form
476
+ # field itself.
477
+ def flatten_annotations(annotations = self[:Annots])
478
+ return [] unless key?(:Annots)
479
+
480
+ not_flattened = annotations.to_ary
481
+ annotations = not_flattened & self[:Annots] if annotations != self[:Annots]
482
+ return not_flattened if annotations.empty?
483
+
484
+ canvas = self.canvas(type: :overlay)
485
+ canvas.save_graphics_state
486
+ media_box = box(:media)
487
+ if media_box.left != 0 || media_box.bottom != 0
488
+ canvas.translate(-media_box.left, -media_box.bottom) # revert initial translation of origin
489
+ end
490
+
491
+ to_delete = []
492
+ not_flattened -= annotations
493
+ annotations.each do |annotation|
494
+ annotation = document.wrap(annotation, type: :Annot)
495
+ appearance = annotation.appearance
496
+ if annotation.flagged?(:hidden) || annotation.flagged?(:invisible)
497
+ to_delete << annotation
498
+ next
499
+ elsif !appearance
500
+ not_flattened << annotation
501
+ next
502
+ end
503
+
504
+ rect = annotation[:Rect]
505
+ box = appearance.box
506
+ matrix = appearance[:Matrix]
507
+
508
+ # Adjust position based on matrix
509
+ pos = [rect.left - matrix[4], rect.bottom - matrix[5]]
510
+
511
+ # In case of a rotation we need to counter the default translation in #xobject by adding
512
+ # box.left and box.bottom, and then translate the origin for the rotation
513
+ angle = (-Math.atan2(matrix[2], matrix[0]) * 180 / Math::PI).to_i
514
+ case angle
515
+ when 0
516
+ # Nothing to do, no rotation
517
+ when 90
518
+ pos[0] += box.top + box.left
519
+ pos[1] += -box.left + box.bottom
520
+ when -90
521
+ pos[0] += -box.bottom + box.left
522
+ pos[1] += box.right + box.bottom
523
+ when 180, -180
524
+ pos[0] += box.right + box.left
525
+ pos[1] += box.top + box.bottom
526
+ else
527
+ not_flattened << annotation
528
+ next
529
+ end
530
+
531
+ width, height = (angle.abs == 90 ? [rect.height, rect.width] : [rect.width, rect.height])
532
+ canvas.xobject(appearance, at: pos, width: width, height: height)
533
+ to_delete << annotation
534
+ end
535
+ canvas.restore_graphics_state
536
+
537
+ to_delete.each do |annotation|
538
+ if annotation[:Subtype] == :Widget
539
+ annotation.form_field.delete_widget(annotation)
540
+ else
541
+ self[:Annots].delete(annotation)
542
+ document.delete(annotation)
543
+ end
544
+ end
545
+
546
+ not_flattened
547
+ end
548
+
468
549
  private
469
550
 
470
551
  # Ensures that the required inheritable fields are set.