hexapdf 0.14.4 → 0.15.0

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