hexapdf 0.27.0 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
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