hexapdf 0.14.2 → 0.15.2
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 +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) }
|