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.
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) }