hexapdf 0.14.2 → 0.15.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -0
  3. data/lib/hexapdf/cli/form.rb +30 -8
  4. data/lib/hexapdf/configuration.rb +19 -4
  5. data/lib/hexapdf/content/canvas.rb +1 -0
  6. data/lib/hexapdf/dictionary.rb +3 -0
  7. data/lib/hexapdf/dictionary_fields.rb +1 -1
  8. data/lib/hexapdf/encryption/security_handler.rb +7 -2
  9. data/lib/hexapdf/encryption/standard_security_handler.rb +12 -0
  10. data/lib/hexapdf/error.rb +4 -3
  11. data/lib/hexapdf/filter.rb +1 -0
  12. data/lib/hexapdf/filter/crypt.rb +60 -0
  13. data/lib/hexapdf/font/true_type/subsetter.rb +5 -1
  14. data/lib/hexapdf/font/type1/afm_parser.rb +2 -1
  15. data/lib/hexapdf/parser.rb +46 -14
  16. data/lib/hexapdf/pdf_array.rb +3 -0
  17. data/lib/hexapdf/revision.rb +16 -0
  18. data/lib/hexapdf/serializer.rb +10 -3
  19. data/lib/hexapdf/tokenizer.rb +44 -3
  20. data/lib/hexapdf/type/acro_form.rb +1 -0
  21. data/lib/hexapdf/type/acro_form/appearance_generator.rb +32 -17
  22. data/lib/hexapdf/type/acro_form/button_field.rb +8 -4
  23. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  24. data/lib/hexapdf/type/acro_form/form.rb +37 -0
  25. data/lib/hexapdf/type/acro_form/signature_field.rb +223 -0
  26. data/lib/hexapdf/type/annotation.rb +13 -9
  27. data/lib/hexapdf/type/annotations/widget.rb +3 -1
  28. data/lib/hexapdf/type/font_descriptor.rb +9 -2
  29. data/lib/hexapdf/type/page.rb +81 -0
  30. data/lib/hexapdf/type/resources.rb +4 -0
  31. data/lib/hexapdf/type/xref_stream.rb +7 -0
  32. data/lib/hexapdf/utils/graphics_helpers.rb +4 -4
  33. data/lib/hexapdf/version.rb +1 -1
  34. data/test/hexapdf/content/test_canvas.rb +21 -0
  35. data/test/hexapdf/encryption/test_security_handler.rb +15 -0
  36. data/test/hexapdf/encryption/test_standard_security_handler.rb +26 -0
  37. data/test/hexapdf/filter/test_crypt.rb +21 -0
  38. data/test/hexapdf/font/true_type/test_subsetter.rb +2 -0
  39. data/test/hexapdf/font/type1/test_afm_parser.rb +5 -0
  40. data/test/hexapdf/test_dictionary_fields.rb +7 -0
  41. data/test/hexapdf/test_parser.rb +82 -2
  42. data/test/hexapdf/test_revision.rb +21 -0
  43. data/test/hexapdf/test_serializer.rb +10 -0
  44. data/test/hexapdf/test_tokenizer.rb +50 -0
  45. data/test/hexapdf/test_writer.rb +2 -2
  46. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +24 -3
  47. data/test/hexapdf/type/acro_form/test_button_field.rb +13 -7
  48. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  49. data/test/hexapdf/type/acro_form/test_form.rb +46 -2
  50. data/test/hexapdf/type/acro_form/test_signature_field.rb +38 -0
  51. data/test/hexapdf/type/annotations/test_widget.rb +2 -0
  52. data/test/hexapdf/type/test_annotation.rb +20 -10
  53. data/test/hexapdf/type/test_font_descriptor.rb +7 -0
  54. data/test/hexapdf/type/test_page.rb +187 -49
  55. data/test/hexapdf/type/test_resources.rb +6 -0
  56. data/test/hexapdf/type/test_xref_stream.rb +7 -0
  57. data/test/hexapdf/utils/test_graphics_helpers.rb +8 -0
  58. 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 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.
@@ -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
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.14.2'
40
+ VERSION = '0.15.2'
41
41
 
42
42
  end
@@ -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
@@ -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
@@ -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("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>>")
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) }