hexapdf 0.14.4 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,24 @@ 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
+ if entry.kind_of?(HexaPDF::Stream)
142
+ entry[:Subtype] == :Form ? entry : document.wrap(entry, type: :XObject, subtype: :Form)
143
+ end
141
144
  end
145
+ alias appearance? appearance
142
146
 
143
147
  private
144
148
 
@@ -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.
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.14.4'
40
+ VERSION = '0.15.0'
41
41
 
42
42
  end
@@ -50,7 +50,8 @@ describe HexaPDF::Parser do
50
50
  end
51
51
 
52
52
  def create_parser(str)
53
- @parser = HexaPDF::Parser.new(StringIO.new(str), @document)
53
+ @parse_io = StringIO.new(str)
54
+ @parser = HexaPDF::Parser.new(@parse_io, @document)
54
55
  end
55
56
 
56
57
  describe "parse_indirect_object" do
@@ -94,6 +95,12 @@ describe HexaPDF::Parser do
94
95
  assert_equal('12', TestHelper.collector(stream.fiber))
95
96
  end
96
97
 
98
+ it "handles keyword stream followed by space and CR LF" do
99
+ create_parser("1 0 obj<</Length 2>> stream \r\n12\nendstream endobj")
100
+ *, stream = @parser.parse_indirect_object
101
+ assert_equal('12', TestHelper.collector(stream.fiber))
102
+ end
103
+
97
104
  it "handles invalid indirect object value consisting of number followed by endobj without space" do
98
105
  create_parser("1 0 obj 749endobj")
99
106
  object, * = @parser.parse_indirect_object
@@ -166,7 +173,13 @@ describe HexaPDF::Parser do
166
173
  it "fails if keyword stream is followed by space and CR or LF instead of LF or CR/LF" do
167
174
  create_parser("1 0 obj<</Length 2>> stream \n12\nendstream endobj")
168
175
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
169
- assert_match(/must be followed by LF or CR\/LF/, exp.message)
176
+ assert_match(/followed by space instead/, exp.message)
177
+ end
178
+
179
+ it "fails if keyword stream is followed by space and CR LF instead of LF or CR/LF" do
180
+ create_parser("1 0 obj<</Length 2>> stream \r\n12\nendstream endobj")
181
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
182
+ assert_match(/followed by space instead/, exp.message)
170
183
  end
171
184
 
172
185
  it "fails for numbers followed by endobj without space" do
@@ -511,6 +524,13 @@ describe HexaPDF::Parser do
511
524
  assert_match(/not a cross-reference stream/, exp.message)
512
525
  end
513
526
 
527
+ it "fails if the cross-reference stream is missing data" do
528
+ @parse_io.string[287..288] = ''
529
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.load_revision(212) }
530
+ assert_match(/missing data/, exp.message)
531
+ assert_equal(212, exp.pos)
532
+ end
533
+
514
534
  it "fails on strict parsing if the cross-reference stream doesn't contain an entry for itself" do
515
535
  @document.config['parser.on_correctable_error'] = proc { true }
516
536
  create_parser("2 0 obj\n<</Type/XRef/Length 3/W [1 1 1]/Size 1>>" \
@@ -578,7 +598,7 @@ describe HexaPDF::Parser do
578
598
  end
579
599
 
580
600
  it "uses the first trailer in case of a linearized file" do
581
- create_parser("trailer <</Size 1/Prev 342>>\ntrailer <</Size 2>>")
601
+ create_parser("1 0 obj\n<</Linearized true>>\nendobj\ntrailer <</Size 1/Prev 342>>\ntrailer <</Size 2>>")
582
602
  assert_equal({Size: 1}, @parser.reconstructed_revision.trailer.value)
583
603
  end
584
604