hexapdf 0.14.4 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|