hexapdf 0.27.0 → 0.28.0

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -1
  3. data/examples/019-acro_form.rb +14 -3
  4. data/examples/023-images.rb +30 -0
  5. data/lib/hexapdf/cli/info.rb +5 -1
  6. data/lib/hexapdf/cli/inspect.rb +2 -2
  7. data/lib/hexapdf/cli/split.rb +2 -2
  8. data/lib/hexapdf/configuration.rb +1 -2
  9. data/lib/hexapdf/content/canvas.rb +8 -3
  10. data/lib/hexapdf/dictionary.rb +1 -5
  11. data/lib/hexapdf/document.rb +6 -10
  12. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  13. data/lib/hexapdf/importer.rb +32 -27
  14. data/lib/hexapdf/layout/list_box.rb +1 -5
  15. data/lib/hexapdf/object.rb +5 -0
  16. data/lib/hexapdf/parser.rb +13 -0
  17. data/lib/hexapdf/revision.rb +15 -12
  18. data/lib/hexapdf/revisions.rb +4 -0
  19. data/lib/hexapdf/tokenizer.rb +14 -8
  20. data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
  21. data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
  22. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
  23. data/lib/hexapdf/type/acro_form/field.rb +11 -5
  24. data/lib/hexapdf/type/acro_form/form.rb +33 -7
  25. data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
  26. data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
  27. data/lib/hexapdf/type/annotations/widget.rb +3 -0
  28. data/lib/hexapdf/type/font_true_type.rb +14 -0
  29. data/lib/hexapdf/type/object_stream.rb +2 -2
  30. data/lib/hexapdf/type/outline.rb +1 -1
  31. data/lib/hexapdf/type/page.rb +56 -46
  32. data/lib/hexapdf/version.rb +1 -1
  33. data/lib/hexapdf/writer.rb +2 -3
  34. data/test/hexapdf/content/test_canvas.rb +5 -0
  35. data/test/hexapdf/document/test_pages.rb +2 -2
  36. data/test/hexapdf/encryption/test_aes.rb +1 -1
  37. data/test/hexapdf/filter/test_predictor.rb +0 -1
  38. data/test/hexapdf/layout/test_box.rb +2 -1
  39. data/test/hexapdf/layout/test_column_box.rb +1 -1
  40. data/test/hexapdf/layout/test_list_box.rb +1 -1
  41. data/test/hexapdf/test_document.rb +2 -8
  42. data/test/hexapdf/test_importer.rb +13 -6
  43. data/test/hexapdf/test_parser.rb +17 -0
  44. data/test/hexapdf/test_revision.rb +15 -14
  45. data/test/hexapdf/test_revisions.rb +43 -0
  46. data/test/hexapdf/test_stream.rb +1 -1
  47. data/test/hexapdf/test_tokenizer.rb +3 -4
  48. data/test/hexapdf/test_writer.rb +3 -3
  49. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
  50. data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
  51. data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
  52. data/test/hexapdf/type/acro_form/test_field.rb +4 -4
  53. data/test/hexapdf/type/acro_form/test_form.rb +18 -0
  54. data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
  55. data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
  56. data/test/hexapdf/type/signature/common.rb +3 -1
  57. data/test/hexapdf/type/test_font_true_type.rb +20 -0
  58. data/test/hexapdf/type/test_object_stream.rb +2 -1
  59. data/test/hexapdf/type/test_outline.rb +3 -0
  60. data/test/hexapdf/type/test_page.rb +67 -30
  61. data/test/hexapdf/type/test_page_tree_node.rb +4 -2
  62. metadata +46 -3
@@ -73,6 +73,8 @@ module HexaPDF
73
73
  # See: PDF1.7 s12.7.4.3
74
74
  class TextField < VariableTextField
75
75
 
76
+ define_type :XXAcroFormField
77
+
76
78
  define_field :MaxLen, type: Integer
77
79
 
78
80
  # All inheritable dictionary fields for text fields.
@@ -220,7 +222,15 @@ module HexaPDF
220
222
  current_value = field_value
221
223
  appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
222
224
  each_widget do |widget|
223
- next if !force && widget.cached?(:last_value) && widget.cache(:last_value) == current_value
225
+ is_cached = widget.cached?(:last_value)
226
+ unless force
227
+ if is_cached && widget.cache(:last_value) == current_value
228
+ next
229
+ elsif !is_cached && widget.appearance?
230
+ widget.cache(:last_value, current_value, update: true)
231
+ next
232
+ end
233
+ end
224
234
  widget.cache(:last_value, current_value, update: true)
225
235
  appearance_generator_class.new(widget).create_text_appearances
226
236
  end
@@ -228,7 +238,7 @@ module HexaPDF
228
238
 
229
239
  # Updates the widgets so that they reflect the current field value.
230
240
  def update_widgets
231
- create_appearances
241
+ create_appearances(force: true)
232
242
  end
233
243
 
234
244
  private
@@ -224,6 +224,9 @@ module HexaPDF
224
224
  # The kind of marker that is shown inside the widget. Can either be one of the symbols
225
225
  # +:check+, +:circle+, +:cross+, +:diamond+, +:square+ or +:star+, or a one character
226
226
  # string. The latter is interpreted using the ZapfDingbats font.
227
+ #
228
+ # If an empty string is set, it is treated as if +nil+ was set, i.e. it shows the default
229
+ # marker for the field type.
227
230
  attr_reader :style
228
231
 
229
232
  # The size of the marker in PDF points that is shown inside the widget. The special value
@@ -35,6 +35,7 @@
35
35
  #++
36
36
 
37
37
  require 'hexapdf/type/font_simple'
38
+ require 'hexapdf/font/true_type_wrapper'
38
39
 
39
40
  module HexaPDF
40
41
  module Type
@@ -45,6 +46,19 @@ module HexaPDF
45
46
  define_field :Subtype, type: Symbol, required: true, default: :TrueType
46
47
  define_field :BaseFont, type: Symbol, required: true
47
48
 
49
+ # Overrides the default to provide a font wrapper in case none is set and a complete TrueType
50
+ # is embedded.
51
+ #
52
+ # See: Font#font_wrapper
53
+ def font_wrapper
54
+ if (tmp = super)
55
+ tmp
56
+ elsif (font_file = self.font_file) && self[:BaseFont].to_s !~ /\A[A-Z]{6}\+/
57
+ font = HexaPDF::Font::TrueType::Font.new(StringIO.new(font_file.stream))
58
+ @font_wrapper = HexaPDF::Font::TrueTypeWrapper.new(document, font, subset: true)
59
+ end
60
+ end
61
+
48
62
  private
49
63
 
50
64
  def perform_validation
@@ -179,7 +179,7 @@ module HexaPDF
179
179
  # Due to a bug in Adobe Acrobat, the Catalog may not be in an object stream if the
180
180
  # document is encrypted
181
181
  if obj.nil? || obj.null? || obj.gen != 0 || obj.kind_of?(Stream) || obj == encrypt_dict ||
182
- (encrypt_dict && obj.type == :Catalog) ||
182
+ obj.type == :Catalog ||
183
183
  obj.type == :Sig || obj.type == :DocTimeStamp ||
184
184
  (obj.respond_to?(:key?) && obj.key?(:ByteRange) && obj.key?(:Contents))
185
185
  delete_object(objects[index])
@@ -220,7 +220,7 @@ module HexaPDF
220
220
 
221
221
  # Returns the container with the to-be-stored objects.
222
222
  def objects
223
- @objects ||=
223
+ @objects ||=
224
224
  begin
225
225
  @objects = {}
226
226
  parse_stream
@@ -128,7 +128,7 @@ module HexaPDF
128
128
  node, dir = first ? [first, :Next] : [last, :Prev]
129
129
  node = node[dir] while node.key?(dir)
130
130
  self[dir == :Next ? :Last : :First] = node
131
- elsif !first && !last && self[:Count]
131
+ elsif !first && !last && self[:Count] && self[:Count] != 0
132
132
  yield('Outline dictionary key /Count set but no items exist', true)
133
133
  delete(:Count)
134
134
  end
@@ -267,6 +267,7 @@ module HexaPDF
267
267
  raise ArgumentError, "Page rotation has to be multiple of 90 degrees"
268
268
  end
269
269
 
270
+ # /Rotate and therefore cw_angle is angle in clockwise orientation
270
271
  cw_angle = (self[:Rotate] - angle) % 360
271
272
 
272
273
  if flatten
@@ -274,27 +275,41 @@ module HexaPDF
274
275
  return if cw_angle == 0
275
276
 
276
277
  matrix = case cw_angle
277
- when 90
278
- HexaPDF::Content::TransformationMatrix.new(0, -1, 1, 0)
279
- when 180
280
- HexaPDF::Content::TransformationMatrix.new(-1, 0, 0, -1)
281
- when 270
282
- HexaPDF::Content::TransformationMatrix.new(0, 1, -1, 0)
278
+ when 90 then Content::TransformationMatrix.new(0, -1, 1, 0, -box.bottom, box.right)
279
+ when 180 then Content::TransformationMatrix.new(-1, 0, 0, -1, box.right, box.top)
280
+ when 270 then Content::TransformationMatrix.new(0, 1, -1, 0, box.top, -box.left)
283
281
  end
284
282
 
285
- [:MediaBox, :CropBox, :BleedBox, :TrimBox, :ArtBox].each do |box_name|
286
- next unless key?(box_name)
287
- box = self[box_name]
283
+ rotate_box = lambda do |box|
288
284
  llx, lly, urx, ury = \
289
285
  case cw_angle
290
- when 90
291
- [box.right, box.bottom, box.left, box.top]
292
- when 180
293
- [box.right, box.top, box.left, box.bottom]
294
- when 270
295
- [box.left, box.top, box.right, box.bottom]
286
+ when 90 then [box.right, box.bottom, box.left, box.top]
287
+ when 180 then [box.right, box.top, box.left, box.bottom]
288
+ when 270 then [box.left, box.top, box.right, box.bottom]
296
289
  end
297
- self[box_name].value = matrix.evaluate(llx, lly).concat(matrix.evaluate(urx, ury))
290
+ box.value.replace(matrix.evaluate(llx, lly).concat(matrix.evaluate(urx, ury)))
291
+ end
292
+
293
+ [:MediaBox, :CropBox, :BleedBox, :TrimBox, :ArtBox].each do |box_name|
294
+ next unless key?(box_name)
295
+ rotate_box.call(self[box_name])
296
+ end
297
+
298
+ each_annotation do |annot|
299
+ rotate_box.call(annot[:Rect])
300
+ if (quad_points = annot[:QuadPoints])
301
+ quad_points = quad_points.value if quad_points.respond_to?(:value)
302
+ result = []
303
+ quad_points.each_slice(2) {|x, y| result.concat(matrix.evaluate(x, y)) }
304
+ quad_points.replace(result)
305
+ end
306
+ if (appearance = annot.appearance)
307
+ appearance[:Matrix] = matrix.dup.premultiply(*appearance[:Matrix].value).to_a
308
+ end
309
+ if annot[:Subtype] == :Widget
310
+ app_ch = annot[:MK] ||= document.wrap({}, type: :XXAppearanceCharacteristics)
311
+ app_ch[:R] = (app_ch[:R] + 360 - cw_angle) % 360
312
+ end
298
313
  end
299
314
 
300
315
  before_contents = document.add({}, stream: " q #{matrix.to_a.join(' ')} cm ")
@@ -519,15 +534,15 @@ module HexaPDF
519
534
  # If an annotation is a form field widget, only the widget will be deleted but not the form
520
535
  # field itself.
521
536
  def flatten_annotations(annotations = self[:Annots])
522
- return [] unless key?(:Annots)
537
+ not_flattened = (annotations || []).to_ary
538
+ return not_flattened unless key?(:Annots)
523
539
 
524
- not_flattened = annotations.to_ary
525
540
  annotations = not_flattened & self[:Annots] if annotations != self[:Annots]
526
541
  return not_flattened if annotations.empty?
527
542
 
528
543
  canvas = self.canvas(type: :overlay)
529
- canvas.save_graphics_state
530
544
  if (pos = canvas.graphics_state.ctm.evaluate(0, 0)) != [0, 0]
545
+ canvas.save_graphics_state
531
546
  canvas.translate(-pos[0], -pos[1])
532
547
  end
533
548
 
@@ -546,36 +561,31 @@ module HexaPDF
546
561
 
547
562
  rect = annotation[:Rect]
548
563
  box = appearance.box
549
- matrix = appearance[:Matrix]
550
-
551
- # Adjust position based on matrix
552
- pos = [rect.left - matrix[4], rect.bottom - matrix[5]]
553
-
554
- # In case of a rotation we need to counter the default translation in #xobject by adding
555
- # box.left and box.bottom, and then translate the origin for the rotation
556
- angle = (-Math.atan2(matrix[2], matrix[0]) * 180 / Math::PI).to_i
557
- case angle
558
- when 0
559
- # Nothing to do, no rotation
560
- when 90
561
- pos[0] += box.top + box.left
562
- pos[1] += -box.left + box.bottom
563
- when -90
564
- pos[0] += -box.bottom + box.left
565
- pos[1] += box.right + box.bottom
566
- when 180, -180
567
- pos[0] += box.right + box.left
568
- pos[1] += box.top + box.bottom
569
- else
570
- not_flattened << annotation
571
- next
572
- end
573
564
 
574
- width, height = (angle.abs == 90 ? [rect.height, rect.width] : [rect.width, rect.height])
575
- canvas.xobject(appearance, at: pos, width: width, height: height)
565
+ # PDF1.7 12.5.5 algorithm
566
+ # Step a) Calculate smallest rectangle containing transformed bounding box
567
+ matrix = HexaPDF::Content::TransformationMatrix.new(*appearance[:Matrix].value)
568
+ llx, lly = matrix.evaluate(box.left, box.bottom)
569
+ ulx, uly = matrix.evaluate(box.left, box.top)
570
+ lrx, lry = matrix.evaluate(box.right, box.bottom)
571
+ left, right = [llx, ulx, lrx, lrx + (ulx - llx)].minmax
572
+ bottom, top = [lly, uly, lry, lry + (uly - lly)].minmax
573
+
574
+ # Step b) Fit calculated rectangle to annotation rectangle by translating/scaling
575
+ a = HexaPDF::Content::TransformationMatrix.new
576
+ a.translate(rect.left - left, rect.bottom - bottom)
577
+ a.scale(rect.width.fdiv(right - left), rect.height.fdiv(top - bottom))
578
+
579
+ # Step c) Premultiply form matrix - done implicitly when drawing the XObject
580
+
581
+ canvas.transform(*a) do
582
+ # Use [box.left, box.bottom] to counter default translation in #xobject since that
583
+ # is already taken care of in matrix a
584
+ canvas.xobject(appearance, at: [box.left, box.bottom])
585
+ end
576
586
  to_delete << annotation
577
587
  end
578
- canvas.restore_graphics_state
588
+ canvas.restore_graphics_state unless pos == [0, 0]
579
589
 
580
590
  to_delete.each do |annotation|
581
591
  if annotation[:Subtype] == :Widget
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.27.0'
40
+ VERSION = '0.28.0'
41
41
 
42
42
  end
@@ -114,7 +114,7 @@ module HexaPDF
114
114
  @document.catalog[:Version] = @document.version.to_sym
115
115
  end
116
116
  @document.revisions.each do |rev|
117
- rev.each_modified_object {|obj| revision.send(:add_without_check, obj) }
117
+ rev.each_modified_object(all: true) {|obj| revision.send(:add_without_check, obj) }
118
118
  end
119
119
 
120
120
  write_revision(revision, parser.startxref_offset)
@@ -135,8 +135,7 @@ module HexaPDF
135
135
 
136
136
  revision = @document.revisions.add
137
137
  @document.revisions.all[0..-2].each do |rev|
138
- rev.each_modified_object {|obj| revision.send(:add_without_check, obj) }
139
- rev.reset_objects
138
+ rev.each_modified_object(delete: true) {|obj| revision.send(:add_without_check, obj) }
140
139
  end
141
140
  @document.revisions.merge(-2..-1)
142
141
  end
@@ -934,6 +934,11 @@ describe HexaPDF::Content::Canvas do
934
934
  [:restore_graphics_state]])
935
935
  end
936
936
 
937
+ it "correctly serializes the form when no transformation is needed" do
938
+ @canvas.image(@form, at: [100, 50])
939
+ assert_operators(@page.contents, [[:paint_xobject, [:XO1]]])
940
+ end
941
+
937
942
  it "doesn't do anything if the form's width or height is zero" do
938
943
  @form[:BBox] = [100, 50, 100, 200]
939
944
  @canvas.xobject(@form, at: [0, 0])
@@ -166,7 +166,7 @@ describe HexaPDF::Document::Pages do
166
166
  it "works for a single page label entry" do
167
167
  @doc.catalog[:PageLabels] = {Nums: [0, {S: :r}]}
168
168
  result = @doc.pages.each_labelling_range.to_a
169
- assert_equal([[0, 10, {S: :r}]], result.map {|s, c, l| [s, c, l.value]})
169
+ assert_equal([[0, 10, {S: :r}]], result.map {|s, c, l| [s, c, l.value] })
170
170
  assert_equal(:lowercase_roman, result[0].last.numbering_style)
171
171
  end
172
172
 
@@ -174,7 +174,7 @@ describe HexaPDF::Document::Pages do
174
174
  @doc.catalog[:PageLabels] = {Nums: [0, {S: :r}, 2, {S: :d}, 7, {S: :A}]}
175
175
  result = @doc.pages.each_labelling_range.to_a
176
176
  assert_equal([[0, 2, {S: :r}], [2, 5, {S: :d}], [7, 3, {S: :A}]],
177
- result.map {|s, c, l| [s, c, l.value]})
177
+ result.map {|s, c, l| [s, c, l.value] })
178
178
  end
179
179
 
180
180
  it "returns a zero or negative count for the last range if there aren't enough pages" do
@@ -122,7 +122,7 @@ describe HexaPDF::Encryption::AES do
122
122
  [4, 20, 40].each do |length|
123
123
  assert_raises(HexaPDF::EncryptionError) do
124
124
  collector(@algorithm_class.decryption_fiber('some' * 4,
125
- Fiber.new { 'a' * length }))
125
+ Fiber.new { 'a' * length }))
126
126
  end
127
127
  end
128
128
  end
@@ -4,7 +4,6 @@ require_relative 'common'
4
4
  require 'hexapdf/filter/predictor'
5
5
 
6
6
  describe HexaPDF::Filter::Predictor do
7
-
8
7
  module CommonPredictorTests
9
8
  def test_decoding_through_decoder_method
10
9
  @testcases.each do |name, data|
@@ -131,7 +131,8 @@ describe HexaPDF::Layout::Box do
131
131
  assert_equal([nil, box], box.split(150, 150, nil))
132
132
  end
133
133
 
134
- it "can't be split if it doesn't (completely) fit as the default implementation knows nothing about the content" do
134
+ it "can't be split if it doesn't (completely) fit as the default implementation " \
135
+ "knows nothing about the content" do
135
136
  @box.style.position = :flow # make sure we would generally be splitable
136
137
  @box.fit(90, 100, nil)
137
138
  assert_equal([nil, @box], @box.split(150, 150, nil))
@@ -11,7 +11,7 @@ describe HexaPDF::Layout::ColumnBox do
11
11
  @text_boxes = 5.times.map do
12
12
  HexaPDF::Layout::TextBox.new(items: [inline_box] * 15, style: {position: :default})
13
13
  end
14
- draw_block = lambda do |canvas, box|
14
+ draw_block = lambda do |canvas, _box|
15
15
  canvas.move_to(0, 0).end_path
16
16
  end
17
17
  @fixed_size_boxes = 15.times.map { HexaPDF::Layout::Box.new(width: 20, height: 10, &draw_block) }
@@ -222,7 +222,7 @@ describe HexaPDF::Layout::ListBox do
222
222
  end
223
223
 
224
224
  it "allows drawing custom markers" do
225
- marker = lambda do |doc, list_box, index|
225
+ marker = lambda do |_doc, _list_box, _index|
226
226
  HexaPDF::Layout::Box.create(width: 10, height: 10) {}
227
227
  end
228
228
  box = create_box(children: @fixed_size_boxes[0, 1], item_type: marker)
@@ -163,14 +163,8 @@ describe HexaPDF::Document do
163
163
  refute_equal(0, obj.oid)
164
164
  end
165
165
 
166
- it "fails if the given object is not a PDF object" do
167
- assert_raises(ArgumentError) { @doc.import(5) }
168
- end
169
-
170
- it "fails if the given object is associated with no or the destination document" do
171
- assert_raises(ArgumentError) { @doc.import(HexaPDF::Object.new(5)) }
172
- obj = @doc.add(5)
173
- assert_raises(ArgumentError) { @doc.import(obj) }
166
+ it "works if the given object is not a PDF object" do
167
+ assert_equal(5, @doc.import(5))
174
168
  end
175
169
  end
176
170
 
@@ -31,12 +31,12 @@ describe HexaPDF::Importer do
31
31
  @source.pages.add
32
32
  @source.pages.root[:Rotate] = 90
33
33
  @dest = HexaPDF::Document.new
34
- @importer = HexaPDF::Importer.for(source: @source, destination: @dest)
34
+ @importer = HexaPDF::Importer.for(@dest)
35
35
  end
36
36
 
37
37
  describe "::for" do
38
38
  it "caches the importer" do
39
- assert_same(@importer, HexaPDF::Importer.for(source: @source, destination: @dest))
39
+ assert_same(@importer, HexaPDF::Importer.for(@dest))
40
40
  end
41
41
  end
42
42
 
@@ -61,8 +61,14 @@ describe HexaPDF::Importer do
61
61
  end
62
62
 
63
63
  it "can import a direct object" do
64
- obj = @importer.import(key: @obj)
65
- assert(@dest.object?(obj[:key]))
64
+ assert_nil(@importer.import(nil))
65
+ assert_equal(5, @importer.import(5))
66
+ assert(@dest.object?(@importer.import({key: @obj})[:key]))
67
+ end
68
+
69
+ it "determines the source document dynamically" do
70
+ obj = @importer.import(@obj.value)
71
+ assert_equal("test", obj[:ref].value)
66
72
  end
67
73
 
68
74
  it "copies the data of the imported objects" do
@@ -120,10 +126,11 @@ describe HexaPDF::Importer do
120
126
  assert_equal(90, page[:Rotate])
121
127
  end
122
128
 
123
- it "raise an error if the given object doesn't belong to the source document" do
129
+ it "works for importing objects from different documents" do
124
130
  other_doc = HexaPDF::Document.new
125
131
  other_obj = other_doc.add("test")
126
- assert_raises(HexaPDF::Error) { @importer.import(other_obj) }
132
+ imported = @importer.import(other_obj)
133
+ assert_equal("test", imported.value)
127
134
  end
128
135
  end
129
136
  end
@@ -54,6 +54,23 @@ describe HexaPDF::Parser do
54
54
  @parser = HexaPDF::Parser.new(@parse_io, @document)
55
55
  end
56
56
 
57
+ describe "linearized?" do
58
+ it "can determine whether a document is linearized" do
59
+ create_parser("%PDF-1.7\n%abcdefgh\n1 0 obj\n<</Linearized 1/H [2 4]/O 1/E 1/N 1/T 1>>\nendobj")
60
+ assert(@parser.linearized?)
61
+ end
62
+
63
+ it "returns false if the first object is not a linearization dictionary" do
64
+ create_parser("%PDF-1.7\n%abcdefgh\n1 0 obj\n<</Length 2 0 R>>\nstream\nhallo\nendstream\nendobj")
65
+ refute(@parser.linearized?)
66
+ end
67
+
68
+ it "returns false if there is a parse error" do
69
+ create_parser("%PDF-1.7\n%abcdefgh\n1 a obj thing")
70
+ refute(@parser.linearized?)
71
+ end
72
+ end
73
+
57
74
  describe "parse_indirect_object" do
58
75
  it "reads indirect objects sequentially" do
59
76
  object, oid, gen, stream = @parser.parse_indirect_object
@@ -199,6 +199,14 @@ describe HexaPDF::Revision do
199
199
  deleted = @rev.object(6)
200
200
  @rev.delete(6)
201
201
  assert_equal([obj, @obj, deleted], @rev.each_modified_object.to_a)
202
+ assert_same(obj, @rev.object(3))
203
+ end
204
+
205
+ it "optionally deletes the modified objects from the revision" do
206
+ obj = @rev.object(3)
207
+ obj.value = :other
208
+ assert_equal([obj], @rev.each_modified_object(delete: true).to_a)
209
+ refute_same(obj, @rev.object(3))
202
210
  end
203
211
 
204
212
  it "ignores object and xref streams that were deleted" do
@@ -207,6 +215,13 @@ describe HexaPDF::Revision do
207
215
  assert_equal([], @rev.each_modified_object.to_a)
208
216
  end
209
217
 
218
+ it "handles object and xref streams that were added appropriately depending on the 'all' arg" do
219
+ xref = @rev.add(HexaPDF::Dictionary.new({Type: :XRef}, oid: 8))
220
+ objstm = @rev.add(HexaPDF::Dictionary.new({Type: :ObjStm}, oid: 9))
221
+ assert_equal([], @rev.each_modified_object.to_a)
222
+ assert_equal([xref, objstm], @rev.each_modified_object(all: true).to_a)
223
+ end
224
+
210
225
  it "doesn't return non-modified objects" do
211
226
  @rev.object(2)
212
227
  assert_equal([], @rev.each_modified_object.to_a)
@@ -230,18 +245,4 @@ describe HexaPDF::Revision do
230
245
  assert_equal([], @rev.each_modified_object.to_a)
231
246
  end
232
247
  end
233
-
234
- describe "reset_objects" do
235
- it "deletes loaded objects" do
236
- @rev.object(2)
237
- @rev.reset_objects
238
- assert(@rev.instance_variable_get(:@objects).oids.empty?)
239
- end
240
-
241
- it "deletes added objects" do
242
- @rev.add(@obj)
243
- @rev.reset_objects
244
- assert(@rev.instance_variable_get(:@objects).oids.empty?)
245
- end
246
- end
247
248
  end
@@ -356,4 +356,47 @@ describe HexaPDF::Revisions do
356
356
  HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
357
357
  end
358
358
  end
359
+
360
+ it "merges the two revisions of a linearized PDF into one" do
361
+ io = StringIO.new(<<~EOF)
362
+ %PDF-1.2
363
+ 5 0 obj
364
+ <</Linearized 1>>
365
+ endobj
366
+ xref
367
+ 5 1
368
+ 0000000009 00000 n
369
+ trailer
370
+ <</ID[(a)(b)]/Info 1 0 R/Root 2 0 R/Size 6/Prev 394>>
371
+ %
372
+ 1 0 obj
373
+ <</ModDate(D:20221205233910+01'00')/Producer(HexaPDF version 0.27.0)>>
374
+ endobj
375
+ 2 0 obj
376
+ <</Type/Catalog/Pages 3 0 R>>
377
+ endobj
378
+ 3 0 obj
379
+ <</Type/Pages/Kids[4 0 R]/Count 1>>
380
+ endobj
381
+ 4 0 obj
382
+ <</Type/Page/MediaBox[0 0 595 842]/Parent 3 0 R/Resources<<>>>>
383
+ endobj
384
+ xref
385
+ 0 5
386
+ 0000000000 65535 f
387
+ 0000000133 00000 n
388
+ 0000000219 00000 n
389
+ 0000000264 00000 n
390
+ 0000000315 00000 n
391
+ trailer
392
+ <</ID[(a)(b)]/Info 1 0 R/Root 2 0 R/Size 5>>
393
+ startxref
394
+ 41
395
+ %%EOF
396
+ EOF
397
+ doc = HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
398
+ assert(doc.revisions.parser.linearized?)
399
+ assert_equal(1, doc.revisions.count)
400
+ assert_same(5, doc.revisions.current.xref_section.max_oid)
401
+ end
359
402
  end
@@ -142,7 +142,7 @@ describe HexaPDF::Stream do
142
142
  def encoded_data(str, encoders = [])
143
143
  map = @document.config['filter.map']
144
144
  tmp = feeder(str)
145
- encoders.each {|e| tmp = ::Object.const_get(map[e]).encoder(tmp) }
145
+ encoders.each {|e| tmp = Object.const_get(map[e]).encoder(tmp) }
146
146
  collector(tmp)
147
147
  end
148
148
 
@@ -14,15 +14,14 @@ describe HexaPDF::Tokenizer do
14
14
 
15
15
  it "handles object references" do
16
16
  #HexaPDF::Reference.new(1, 0), HexaPDF::Reference.new(1, 2), 2, -1, 'R', 0, 0, 'R', -1, 0, 'R',
17
- create_tokenizer("1 0 R +2 +15 R 2 -1 R 0 0 R -1 0 R")
17
+ create_tokenizer("1 0 R +2 +15 R 2 -1 R 0 0 R 0 10 R -1 0 R")
18
18
  assert_equal(HexaPDF::Reference.new(1, 0), @tokenizer.next_token)
19
19
  assert_equal(HexaPDF::Reference.new(2, 15), @tokenizer.next_token)
20
20
  assert_equal(2, @tokenizer.next_token)
21
21
  assert_equal(-1, @tokenizer.next_token)
22
22
  assert_equal('R', @tokenizer.next_token)
23
- assert_equal(0, @tokenizer.next_token)
24
- assert_equal(0, @tokenizer.next_token)
25
- assert_equal('R', @tokenizer.next_token)
23
+ assert_nil(@tokenizer.next_token)
24
+ assert_nil(@tokenizer.next_token)
26
25
  assert_equal(-1, @tokenizer.next_token)
27
26
  assert_equal(0, @tokenizer.next_token)
28
27
  assert_equal('R', @tokenizer.next_token)
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.27.0)>>
43
+ <</Producer(HexaPDF version 0.28.0)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.27.0)>>
75
+ <</Producer(HexaPDF version 0.28.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -214,7 +214,7 @@ describe HexaPDF::Writer do
214
214
  <</Type/Page/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<<>>>>
215
215
  endobj
216
216
  5 0 obj
217
- <</Producer(HexaPDF version 0.27.0)>>
217
+ <</Producer(HexaPDF version 0.28.0)>>
218
218
  endobj
219
219
  4 0 obj
220
220
  <</Root 1 0 R/Info 5 0 R/Size 6/Type/XRef/W[1 1 2]/Index[0 6]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 33>>stream