hexapdf 0.14.2 → 0.15.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +96 -0
- data/lib/hexapdf/cli/form.rb +30 -8
- data/lib/hexapdf/configuration.rb +19 -4
- data/lib/hexapdf/content/canvas.rb +1 -0
- data/lib/hexapdf/dictionary.rb +3 -0
- data/lib/hexapdf/dictionary_fields.rb +1 -1
- data/lib/hexapdf/encryption/security_handler.rb +7 -2
- data/lib/hexapdf/encryption/standard_security_handler.rb +12 -0
- data/lib/hexapdf/error.rb +4 -3
- data/lib/hexapdf/filter.rb +1 -0
- data/lib/hexapdf/filter/crypt.rb +60 -0
- data/lib/hexapdf/font/true_type/subsetter.rb +5 -1
- data/lib/hexapdf/font/type1/afm_parser.rb +2 -1
- data/lib/hexapdf/parser.rb +46 -14
- data/lib/hexapdf/pdf_array.rb +3 -0
- data/lib/hexapdf/revision.rb +16 -0
- data/lib/hexapdf/serializer.rb +10 -3
- data/lib/hexapdf/tokenizer.rb +44 -3
- data/lib/hexapdf/type/acro_form.rb +1 -0
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +32 -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/type/resources.rb +4 -0
- data/lib/hexapdf/type/xref_stream.rb +7 -0
- data/lib/hexapdf/utils/graphics_helpers.rb +4 -4
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/content/test_canvas.rb +21 -0
- data/test/hexapdf/encryption/test_security_handler.rb +15 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +26 -0
- data/test/hexapdf/filter/test_crypt.rb +21 -0
- data/test/hexapdf/font/true_type/test_subsetter.rb +2 -0
- data/test/hexapdf/font/type1/test_afm_parser.rb +5 -0
- data/test/hexapdf/test_dictionary_fields.rb +7 -0
- data/test/hexapdf/test_parser.rb +82 -2
- data/test/hexapdf/test_revision.rb +21 -0
- data/test/hexapdf/test_serializer.rb +10 -0
- data/test/hexapdf/test_tokenizer.rb +50 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +24 -3
- 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
- data/test/hexapdf/type/test_resources.rb +6 -0
- data/test/hexapdf/type/test_xref_stream.rb +7 -0
- data/test/hexapdf/utils/test_graphics_helpers.rb +8 -0
- metadata +6 -2
@@ -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.
|
@@ -222,6 +222,10 @@ module HexaPDF
|
|
222
222
|
yield("No procedure set specified", true)
|
223
223
|
self[:ProcSet] = [:PDF, :Text, :ImageB, :ImageC, :ImageI]
|
224
224
|
else
|
225
|
+
if val.kind_of?(Symbol)
|
226
|
+
yield("Procedure set is a single value instead of an Array", true)
|
227
|
+
val = value[:ProcSet] = [val]
|
228
|
+
end
|
225
229
|
val.reject! do |name|
|
226
230
|
case name
|
227
231
|
when :PDF, :Text, :ImageB, :ImageC, :ImageI
|
@@ -135,6 +135,13 @@ module HexaPDF
|
|
135
135
|
w1 = w[1]
|
136
136
|
w2 = w[2]
|
137
137
|
|
138
|
+
needed_bytes = (w0 + w1 + w2) * index.each_slice(2).sum(&:last)
|
139
|
+
|
140
|
+
if needed_bytes > data.size
|
141
|
+
raise HexaPDF::MalformedPDFError, "Cross-reference stream is missing data " \
|
142
|
+
"(#{needed_bytes} bytes needed, got #{data.size})"
|
143
|
+
end
|
144
|
+
|
138
145
|
index.each_slice(2) do |first_oid, number_of_entries|
|
139
146
|
first_oid.upto(first_oid + number_of_entries - 1) do |oid|
|
140
147
|
# Default for first field: type 1
|
@@ -47,18 +47,18 @@ module HexaPDF
|
|
47
47
|
#
|
48
48
|
# +rwidth+::
|
49
49
|
# The requested width. If +rheight+ is not specified, it is chosen so that the aspect
|
50
|
-
# ratio is maintained
|
50
|
+
# ratio is maintained. In case of +width+ begin zero, +height+ is used for the height.
|
51
51
|
#
|
52
52
|
# +rheight+::
|
53
53
|
# The requested height. If +rwidth+ is not specified, it is chosen so that the aspect
|
54
|
-
# ratio is maintained
|
54
|
+
# ratio is maintained. In case of +height+ begin zero, +width+ is used for the width.
|
55
55
|
def calculate_dimensions(width, height, rwidth: nil, rheight: nil)
|
56
56
|
if rwidth && rheight
|
57
57
|
[rwidth, rheight]
|
58
58
|
elsif rwidth
|
59
|
-
[rwidth, height * rwidth / width.to_f]
|
59
|
+
[rwidth, width == 0 ? height : height * rwidth / width.to_f]
|
60
60
|
elsif rheight
|
61
|
-
[width * rheight / height.to_f, rheight]
|
61
|
+
[height == 0 ? width : width * rheight / height.to_f, rheight]
|
62
62
|
else
|
63
63
|
[width, height]
|
64
64
|
end
|
data/lib/hexapdf/version.rb
CHANGED
@@ -831,6 +831,17 @@ describe HexaPDF::Content::Canvas do
|
|
831
831
|
[:restore_graphics_state]])
|
832
832
|
end
|
833
833
|
|
834
|
+
it "doesn't do anything if the image's width or height is zero" do
|
835
|
+
@image[:Width] = 0
|
836
|
+
@canvas.xobject(@image, at: [0, 0])
|
837
|
+
assert_operators(@page.contents, [])
|
838
|
+
|
839
|
+
@image[:Width] = 10
|
840
|
+
@image[:Height] = 0
|
841
|
+
@canvas.xobject(@image, at: [0, 0])
|
842
|
+
assert_operators(@page.contents, [])
|
843
|
+
end
|
844
|
+
|
834
845
|
it "correctly serializes the form with no options" do
|
835
846
|
@canvas.xobject(@form, at: [1, 2])
|
836
847
|
assert_operators(@page.contents, [[:save_graphics_state],
|
@@ -862,6 +873,16 @@ describe HexaPDF::Content::Canvas do
|
|
862
873
|
[:paint_xobject, [:XO1]],
|
863
874
|
[:restore_graphics_state]])
|
864
875
|
end
|
876
|
+
|
877
|
+
it "doesn't do anything if the form's width or height is zero" do
|
878
|
+
@form[:BBox] = [100, 50, 100, 200]
|
879
|
+
@canvas.xobject(@form, at: [0, 0])
|
880
|
+
assert_operators(@page.contents, [])
|
881
|
+
|
882
|
+
@form[:BBox] = [100, 50, 150, 50]
|
883
|
+
@canvas.xobject(@form, at: [0, 0])
|
884
|
+
assert_operators(@page.contents, [])
|
885
|
+
end
|
865
886
|
end
|
866
887
|
|
867
888
|
describe "character_spacing" do
|
@@ -297,6 +297,13 @@ describe HexaPDF::Encryption::SecurityHandler do
|
|
297
297
|
assert_equal(@encrypted, @handler.decrypt(@obj)[:Key])
|
298
298
|
end
|
299
299
|
|
300
|
+
it "defers handling encryption to a Crypt filter is specified" do
|
301
|
+
data = HexaPDF::StreamData.new(proc { 'mydata' }, filter: :Crypt)
|
302
|
+
obj = @document.wrap({}, oid: 1, stream: data)
|
303
|
+
@handler.decrypt(obj)
|
304
|
+
assert_equal('mydata', obj.stream)
|
305
|
+
end
|
306
|
+
|
300
307
|
it "doesn't decrypt XRef streams" do
|
301
308
|
@obj[:Type] = :XRef
|
302
309
|
assert_equal(@encrypted, @handler.decrypt(@obj)[:Key])
|
@@ -343,6 +350,14 @@ describe HexaPDF::Encryption::SecurityHandler do
|
|
343
350
|
assert_equal('string', @handler.encrypt_stream(@stream).resume)
|
344
351
|
end
|
345
352
|
|
353
|
+
it "defers encrypting to a Crypt filter if specified" do
|
354
|
+
@stream.set_filter(:Crypt)
|
355
|
+
assert_equal('string', @handler.encrypt_stream(@stream).resume)
|
356
|
+
|
357
|
+
@stream.set_filter([:Crypt])
|
358
|
+
assert_equal('string', @handler.encrypt_stream(@stream).resume)
|
359
|
+
end
|
360
|
+
|
346
361
|
it "doesn't encrypt the /Contents key of signature dictionaries" do
|
347
362
|
@obj[:Type] = :Sig
|
348
363
|
@obj[:Contents] = "test"
|
@@ -292,4 +292,30 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
|
|
292
292
|
@handler.set_up_encryption(permissions: perms)
|
293
293
|
assert_equal([:copy_content, :modify_content], @handler.permissions.sort)
|
294
294
|
end
|
295
|
+
|
296
|
+
describe "handling of metadata streams" do
|
297
|
+
before do
|
298
|
+
@doc = HexaPDF::Document.new
|
299
|
+
@doc.encrypt(encrypt_metadata: false)
|
300
|
+
@output = StringIO.new(''.b)
|
301
|
+
end
|
302
|
+
|
303
|
+
it "doesn't decrypt or encrypt the document level metadata stream if /EncryptMetadata is false" do
|
304
|
+
@doc.catalog[:Metadata] = @doc.wrap({Type: :Metadata, Subtype: :XML}, stream: "HELLODATA")
|
305
|
+
@doc.write(@output)
|
306
|
+
assert_match(/stream\nHELLODATA\nendstream/, @output.string)
|
307
|
+
|
308
|
+
doc = HexaPDF::Document.new(io: @output)
|
309
|
+
assert_equal('HELLODATA', doc.catalog[:Metadata].stream)
|
310
|
+
end
|
311
|
+
|
312
|
+
it "doesn't modify decryption/encryption for arbitrary metadata streams" do
|
313
|
+
@doc.catalog[:Anything] = @doc.wrap({Type: :Metadata, Subtype: :XML}, stream: "HELLODATA")
|
314
|
+
@doc.write(@output)
|
315
|
+
refute_match(/stream\nHELLODATA\nendstream/, @output.string)
|
316
|
+
|
317
|
+
doc = HexaPDF::Document.new(io: @output)
|
318
|
+
assert_equal('HELLODATA', doc.catalog[:Anything].stream)
|
319
|
+
end
|
320
|
+
end
|
295
321
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'hexapdf/filter/crypt'
|
5
|
+
|
6
|
+
describe HexaPDF::Filter::Crypt do
|
7
|
+
before do
|
8
|
+
@obj = HexaPDF::Filter::Crypt
|
9
|
+
@source = Fiber.new { "hallo" }
|
10
|
+
end
|
11
|
+
|
12
|
+
it "works with the Identity filter" do
|
13
|
+
assert_equal(@source, @obj.decoder(@source, nil))
|
14
|
+
assert_equal(@source, @obj.encoder(@source, {})) # sic: 'encoder'
|
15
|
+
assert_equal(@source, @obj.decoder(@source, {Name: :Identity}))
|
16
|
+
end
|
17
|
+
|
18
|
+
it "fails if crypt filter name is not Identity" do
|
19
|
+
assert_raises(HexaPDF::FilterError) { @obj.decoder(@source, {Name: :Other}) }
|
20
|
+
end
|
21
|
+
end
|
@@ -29,6 +29,8 @@ describe HexaPDF::Font::TrueType::Subsetter do
|
|
29
29
|
|
30
30
|
it "doesn't use certain subset glyph IDs for performance reasons" do
|
31
31
|
1.upto(93) {|i| @subsetter.use_glyph(i) }
|
32
|
+
# glyph 0, 93 used glyph, 4 special glyphs
|
33
|
+
assert_equal(1 + 93 + 4, @subsetter.instance_variable_get(:@glyph_map).size)
|
32
34
|
1.upto(12) {|i| assert_equal(i, @subsetter.subset_glyph_id(i), "id=#{i}") }
|
33
35
|
13.upto(38) {|i| assert_equal(i + 1, @subsetter.subset_glyph_id(i), "id=#{i}") }
|
34
36
|
39.upto(88) {|i| assert_equal(i + 3, @subsetter.subset_glyph_id(i), "id=#{i}") }
|
@@ -39,6 +39,11 @@ describe HexaPDF::Font::Type1::AFMParser do
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
+
it "parses until EOF if no end token is found" do
|
43
|
+
io = StringIO.new("StartFontMetrics 4.1\nFontName Test")
|
44
|
+
assert_equal('Test', HexaPDF::Font::Type1::AFMParser.parse(io).font_name)
|
45
|
+
end
|
46
|
+
|
42
47
|
it "extracts kerning and ligature information" do
|
43
48
|
metrics = FONT_TIMES.metrics
|
44
49
|
glyph = metrics.character_metrics[:f]
|
@@ -234,5 +234,12 @@ describe HexaPDF::DictionaryFields do
|
|
234
234
|
@field.convert(data, doc)
|
235
235
|
doc.verify
|
236
236
|
end
|
237
|
+
|
238
|
+
it "converts to a null value if an (invalid) empty array is given" do
|
239
|
+
doc = Minitest::Mock.new
|
240
|
+
doc.expect(:wrap, :data, [nil])
|
241
|
+
@field.convert([], doc)
|
242
|
+
doc.verify
|
243
|
+
end
|
237
244
|
end
|
238
245
|
end
|
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
|
@@ -88,6 +89,18 @@ describe HexaPDF::Parser do
|
|
88
89
|
assert_equal('12', TestHelper.collector(stream.fiber))
|
89
90
|
end
|
90
91
|
|
92
|
+
it "handles keyword stream followed by space and CR or LF" do
|
93
|
+
create_parser("1 0 obj<</Length 2>> stream \n12\nendstream endobj")
|
94
|
+
*, stream = @parser.parse_indirect_object
|
95
|
+
assert_equal('12', TestHelper.collector(stream.fiber))
|
96
|
+
end
|
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
|
+
|
91
104
|
it "handles invalid indirect object value consisting of number followed by endobj without space" do
|
92
105
|
create_parser("1 0 obj 749endobj")
|
93
106
|
object, * = @parser.parse_indirect_object
|
@@ -157,6 +170,18 @@ describe HexaPDF::Parser do
|
|
157
170
|
assert_match(/not CR alone/, exp.message)
|
158
171
|
end
|
159
172
|
|
173
|
+
it "fails if keyword stream is followed by space and CR or LF instead of LF or CR/LF" do
|
174
|
+
create_parser("1 0 obj<</Length 2>> stream \n12\nendstream endobj")
|
175
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
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)
|
183
|
+
end
|
184
|
+
|
160
185
|
it "fails for numbers followed by endobj without space" do
|
161
186
|
create_parser("1 0 obj 749endobj")
|
162
187
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
@@ -222,6 +247,23 @@ describe HexaPDF::Parser do
|
|
222
247
|
assert_equal([1, 2], obj.value)
|
223
248
|
end
|
224
249
|
|
250
|
+
it "handles an invalid indirect object offset of 0" do
|
251
|
+
obj = @parser.load_object(HexaPDF::XRefSection.in_use_entry(2, 0, 0))
|
252
|
+
assert(obj.null?)
|
253
|
+
assert_equal(2, obj.oid)
|
254
|
+
assert_equal(0, obj.gen)
|
255
|
+
end
|
256
|
+
|
257
|
+
describe "with strict parsing" do
|
258
|
+
it "raises an error if an indirect object has an offset of 0" do
|
259
|
+
@document.config['parser.on_correctable_error'] = proc { true }
|
260
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) do
|
261
|
+
@parser.load_object(HexaPDF::XRefSection.in_use_entry(2, 0, 0))
|
262
|
+
end
|
263
|
+
assert_match(/has offset 0/, exp.message)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
225
267
|
it "fails if another object is found instead of an object stream" do
|
226
268
|
def (@document).object(_oid)
|
227
269
|
:invalid
|
@@ -482,6 +524,13 @@ describe HexaPDF::Parser do
|
|
482
524
|
assert_match(/not a cross-reference stream/, exp.message)
|
483
525
|
end
|
484
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
|
+
|
485
534
|
it "fails on strict parsing if the cross-reference stream doesn't contain an entry for itself" do
|
486
535
|
@document.config['parser.on_correctable_error'] = proc { true }
|
487
536
|
create_parser("2 0 obj\n<</Type/XRef/Length 3/W [1 1 1]/Size 1>>" \
|
@@ -502,16 +551,37 @@ describe HexaPDF::Parser do
|
|
502
551
|
assert_equal(6, @parser.load_object(@xref).value)
|
503
552
|
end
|
504
553
|
|
554
|
+
it "uses a security handler for decrypting indirect objects if necessary" do
|
555
|
+
handler = Minitest::Mock.new
|
556
|
+
handler.expect(:decrypt, HexaPDF::Object.new(:result, oid: 1), [HexaPDF::Object])
|
557
|
+
@document.instance_variable_set(:@security_handler, handler)
|
558
|
+
create_parser("1 0 obj\n6\nendobj\ntrailer\n<</Size 1>>")
|
559
|
+
assert_equal(:result, @parser.load_object(@xref).value)
|
560
|
+
assert(handler.verify)
|
561
|
+
end
|
562
|
+
|
505
563
|
it "ignores parts where the starting line is split across lines" do
|
506
564
|
create_parser("1 0 obj\n5\nendobj\n1 0\nobj\n6\nendobj\ntrailer\n<</Size 1>>")
|
507
565
|
assert_equal(5, @parser.load_object(@xref).value)
|
508
566
|
end
|
509
567
|
|
568
|
+
it "handles the case when the specified object had an xref entry but is not found" do
|
569
|
+
create_parser("3 0 obj\n5\nendobj\ntrailer\n<</Size 1>>")
|
570
|
+
assert(@parser.load_object(@xref).null?)
|
571
|
+
end
|
572
|
+
|
510
573
|
it "handles cases where the line contains an invalid string that exceeds the read buffer" do
|
511
574
|
create_parser("(1" << "(abc" * 32188 << "\n1 0 obj\n6\nendobj\ntrailer\n<</Size 1>>")
|
512
575
|
assert_equal(6, @parser.load_object(@xref).value)
|
513
576
|
end
|
514
577
|
|
578
|
+
it "handles pathalogical cases which contain many opened literal strings" do
|
579
|
+
time = Time.now
|
580
|
+
create_parser("(1" << "(abc\n" * 10000 << "\n1 0 obj\n6\nendobj\ntrailer\n<</Size 1>>")
|
581
|
+
assert_equal(6, @parser.load_object(@xref).value)
|
582
|
+
assert(Time.now - time < 0.5, "Xref reconstruction takes too long")
|
583
|
+
end
|
584
|
+
|
515
585
|
it "ignores invalid objects" do
|
516
586
|
create_parser("1 x obj\n5\nendobj\n1 0 xobj\n6\nendobj\n1 0 obj 4\nendobj\ntrailer\n<</Size 1>>")
|
517
587
|
assert_equal(4, @parser.load_object(@xref).value)
|
@@ -528,10 +598,20 @@ describe HexaPDF::Parser do
|
|
528
598
|
end
|
529
599
|
|
530
600
|
it "uses the first trailer in case of a linearized file" do
|
531
|
-
create_parser("
|
601
|
+
create_parser("1 0 obj\n<</Linearized true>>\nendobj\ntrailer <</Size 1/Prev 342>>\ntrailer <</Size 2>>")
|
532
602
|
assert_equal({Size: 1}, @parser.reconstructed_revision.trailer.value)
|
533
603
|
end
|
534
604
|
|
605
|
+
it "tries the trailer specified at the startxref position if no other is found" do
|
606
|
+
create_parser("1 0 obj\n5\nendobj\nquack xref trailer <</Size 1/Prev 5>>\nstartxref\n22\n%%EOF")
|
607
|
+
assert_equal({Size: 1}, @parser.reconstructed_revision.trailer.value)
|
608
|
+
end
|
609
|
+
|
610
|
+
it "fails if no trailer is found and the trailer specified at the startxref position is not valid" do
|
611
|
+
create_parser("1 0 obj\n5\nendobj\nquack trailer <</Size 1>>\nstartxref\n22\n%%EOF")
|
612
|
+
assert_raises(HexaPDF::MalformedPDFError) { @parser.reconstructed_revision.trailer }
|
613
|
+
end
|
614
|
+
|
535
615
|
it "fails if no valid trailer is found" do
|
536
616
|
create_parser("1 0 obj\n5\nendobj")
|
537
617
|
assert_raises(HexaPDF::MalformedPDFError) { @parser.load_object(@xref) }
|