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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/lib/hexapdf/cli/form.rb +30 -8
- data/lib/hexapdf/configuration.rb +18 -3
- data/lib/hexapdf/error.rb +4 -3
- data/lib/hexapdf/parser.rb +18 -6
- data/lib/hexapdf/revision.rb +16 -0
- data/lib/hexapdf/type/acro_form.rb +1 -0
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +29 -17
- data/lib/hexapdf/type/acro_form/button_field.rb +8 -4
- data/lib/hexapdf/type/acro_form/field.rb +1 -0
- data/lib/hexapdf/type/acro_form/form.rb +37 -0
- data/lib/hexapdf/type/acro_form/signature_field.rb +223 -0
- data/lib/hexapdf/type/annotation.rb +13 -9
- data/lib/hexapdf/type/annotations/widget.rb +3 -1
- data/lib/hexapdf/type/font_descriptor.rb +9 -2
- data/lib/hexapdf/type/page.rb +81 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/test_parser.rb +23 -3
- data/test/hexapdf/test_revision.rb +21 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +21 -2
- data/test/hexapdf/type/acro_form/test_button_field.rb +13 -7
- data/test/hexapdf/type/acro_form/test_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_form.rb +46 -2
- data/test/hexapdf/type/acro_form/test_signature_field.rb +38 -0
- data/test/hexapdf/type/annotations/test_widget.rb +2 -0
- data/test/hexapdf/type/test_annotation.rb +20 -10
- data/test/hexapdf/type/test_font_descriptor.rb +7 -0
- data/test/hexapdf/type/test_page.rb +187 -49
- metadata +4 -2
@@ -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
|
128
|
+
def appearance_dict
|
129
129
|
self[:AP]
|
130
130
|
end
|
131
131
|
|
132
|
-
# Returns
|
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
|
-
#
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
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)
|
data/lib/hexapdf/type/page.rb
CHANGED
@@ -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.
|
data/lib/hexapdf/version.rb
CHANGED
data/test/hexapdf/test_parser.rb
CHANGED
@@ -50,7 +50,8 @@ describe HexaPDF::Parser do
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def create_parser(str)
|
53
|
-
@
|
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(/
|
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("
|
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
|
|