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
@@ -105,6 +105,27 @@ describe HexaPDF::Revision do
105
105
  end
106
106
  end
107
107
 
108
+ describe "update" do
109
+ before do
110
+ @rev.add(@obj)
111
+ end
112
+
113
+ it "updates the object if it has the same data instance" do
114
+ x = HexaPDF::Object.new(@obj.data)
115
+ y = @rev.update(x)
116
+ assert_same(x, y)
117
+ refute_same(x, @obj)
118
+ assert_same(x, @rev.object(@ref))
119
+ end
120
+
121
+ it "doesn't update the object if it refers to a different data instance" do
122
+ x = HexaPDF::Object.new(:value, oid: 5)
123
+ assert_nil(@rev.update(x))
124
+ x.data.oid = 1
125
+ assert_nil(@rev.update(x))
126
+ end
127
+ end
128
+
108
129
  describe "delete" do
109
130
  before do
110
131
  @rev.add(@obj)
@@ -58,6 +58,9 @@ describe HexaPDF::Serializer do
58
58
  assert_serialized("0.000005", 0.000005)
59
59
  assert_serialized("-0.000005", -0.000005)
60
60
  assert_serialized("0.0", 0.0)
61
+ assert_raises(HexaPDF::Error) { @serializer.serialize(0.0 / 0) }
62
+ assert_raises(HexaPDF::Error) { @serializer.serialize(1.0 / 0) }
63
+ assert_raises(HexaPDF::Error) { @serializer.serialize(-1.0 / 0) }
61
64
  end
62
65
 
63
66
  it "serializes numerics" do
@@ -153,6 +156,13 @@ describe HexaPDF::Serializer do
153
156
  assert_equal("<</Key(value)/Length 6>>stream\nsome\nendstream", io.string)
154
157
  end
155
158
 
159
+ it "doesn't reset the internal recursion flag if the stream is serialized as part of another object" do
160
+ object = HexaPDF::Dictionary.new({}, oid: 5)
161
+ object[:Stream] = @stream
162
+ object[:Self] = object # needs to be the last entry so that :Stream gets serialized first!
163
+ assert_serialized("<</Stream 2 0 R/Self 5 0 R>>", object)
164
+ end
165
+
156
166
  it "fails if a stream without object identifier is serialized" do
157
167
  @stream.oid = 0
158
168
  assert_raises(HexaPDF::Error) { @serializer.serialize(@stream) }
@@ -27,4 +27,54 @@ describe HexaPDF::Tokenizer do
27
27
  5.times {|i| assert_equal(i, @tokenizer.next_token) }
28
28
  end
29
29
  end
30
+
31
+ it "has a special token scanning method for use with xref reconstruction" do
32
+ create_tokenizer(<<-EOF.chomp.gsub(/^ {8}/, ''))
33
+ % Comment
34
+ true
35
+ 123 50
36
+ obj
37
+ (ignored)
38
+ /Ignored
39
+ [/Ignored]
40
+ <</Ignored /Values>>
41
+ EOF
42
+
43
+ scan_to_newline = proc { @tokenizer.scan_until(/(\n|\r\n?)+|\z/) }
44
+
45
+ assert_nil(@tokenizer.next_integer_or_keyword)
46
+ scan_to_newline.call
47
+ assert_equal(true, @tokenizer.next_integer_or_keyword)
48
+ assert_equal(123, @tokenizer.next_integer_or_keyword)
49
+ assert_equal(50, @tokenizer.next_integer_or_keyword)
50
+ assert_equal('obj', @tokenizer.next_integer_or_keyword)
51
+ 4.times do
52
+ assert_nil(@tokenizer.next_integer_or_keyword)
53
+ scan_to_newline.call
54
+ end
55
+ assert_equal(HexaPDF::Tokenizer::NO_MORE_TOKENS, @tokenizer.next_integer_or_keyword)
56
+ end
57
+
58
+ describe "can correct some problems" do
59
+ describe "invalid inf/-inf/nan/-nan tokens" do
60
+ it "turns them into zeros" do
61
+ count = 0
62
+ on_correctable_error = lambda do |msg, pos|
63
+ count += 1
64
+ assert_match(/Invalid object, got token/, msg)
65
+ assert_equal(8, pos) if count == 2
66
+ false
67
+ end
68
+ tokenizer = HexaPDF::Tokenizer.new(StringIO.new("inf -Inf NaN -nan"),
69
+ on_correctable_error: on_correctable_error)
70
+ assert_equal([0, 0, 0, 0], 4.times.map { tokenizer.next_object })
71
+ assert_equal(4, count)
72
+ end
73
+
74
+ it "raises an error if configured so" do
75
+ tokenizer = HexaPDF::Tokenizer.new(StringIO.new("inf"), on_correctable_error: proc { true })
76
+ assert_raises(HexaPDF::MalformedPDFError) { tokenizer.next_object }
77
+ end
78
+ end
79
+ end
30
80
  end
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.14.2)>>
43
+ <</Producer(HexaPDF version 0.15.2)>>
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.14.2)>>
75
+ <</Producer(HexaPDF version 0.15.2)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -407,10 +407,12 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
407
407
  @generator.create_appearances
408
408
  form = @widget[:AP][:N]
409
409
  form[:key] = :value
410
+ form.delete(:Subtype)
411
+ @widget[:AP][:N] = @doc.wrap(form, type: HexaPDF::Dictionary)
410
412
 
411
413
  @field[:V] = 'test1'
412
414
  @generator.create_appearances
413
- assert_same(form, @widget[:AP][:N])
415
+ assert_equal(form, @widget[:AP][:N])
414
416
  refute(form.key?(:key))
415
417
  assert_match(/test1/, form.contents)
416
418
  end
@@ -746,17 +748,36 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
746
748
 
747
749
  describe "font resolution in case the referenced font is not usable" do
748
750
  before do
749
- @doc.config['acro_form.fallback_font'] = ['Times', {variant: :none}]
751
+ @doc.config['acro_form.fallback_font'] = ['Times', {variant: :italic}]
750
752
  @field[:V] = 'Test'
751
753
  end
752
754
 
753
755
  it "uses the fallback font if the font is not usable" do
754
756
  def (@form.default_resources.font(:F1)).font_wrapper; nil; end
755
757
  @generator.create_appearances
756
- assert_equal(:'Times-Roman', @widget[:AP][:N][:Resources][:Font][:F2][:BaseFont])
758
+ assert_equal(:'Times-Italic', @widget[:AP][:N][:Resources][:Font][:F2][:BaseFont])
757
759
  end
758
760
 
759
761
  it "uses the fallback font if the font is not found" do
762
+ @form.default_resources[:Font].delete(:F1)
763
+ @generator.create_appearances
764
+ assert_equal(:'Times-Italic', @widget[:AP][:N][:Resources][:Font][:F1][:BaseFont])
765
+ end
766
+
767
+ it "respects a simple fallback font name" do
768
+ @doc.config['acro_form.fallback_font'] = 'Times'
769
+ @form.default_resources[:Font].delete(:F1)
770
+ @generator.create_appearances
771
+ assert_equal(:'Times-Roman', @widget[:AP][:N][:Resources][:Font][:F1][:BaseFont])
772
+ end
773
+
774
+ it "respects a fallback font callable object" do
775
+ field = @field
776
+ @doc.config['acro_form.fallback_font'] = proc do |field_arg, font_arg|
777
+ assert_same(field.data, field_arg.data)
778
+ assert_nil(font_arg)
779
+ 'Times'
780
+ end
760
781
  @form.default_resources[:Font].delete(:F1)
761
782
  @generator.create_appearances
762
783
  assert_equal(:'Times-Roman', @widget[:AP][:N][:Resources][:Font][:F1][:BaseFont])
@@ -225,20 +225,26 @@ describe HexaPDF::Type::AcroForm::ButtonField do
225
225
  it "won't generate appearances if they already exist" do
226
226
  widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
227
227
  @field.create_appearances
228
- yes = widget.appearance.normal_appearance[:Yes]
229
- off = widget.appearance.normal_appearance[:Off]
230
- widget.appearance.normal_appearance.delete(:Off)
228
+ yes = widget.appearance_dict.normal_appearance[:Yes]
229
+ off = widget.appearance_dict.normal_appearance[:Off]
231
230
  @field.create_appearances
232
- assert_same(yes, widget.appearance.normal_appearance[:Yes])
233
- refute_same(off, widget.appearance.normal_appearance[:Off])
231
+ assert_same(yes, widget.appearance_dict.normal_appearance[:Yes])
232
+ assert_same(off, widget.appearance_dict.normal_appearance[:Off])
233
+
234
+ @field.delete_widget(widget)
235
+ @field.flag(:push_button)
236
+ widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0], AP: {N: @doc.wrap({}, stream: '')})
237
+ appearance = widget.appearance_dict.normal_appearance
238
+ @field.create_appearances
239
+ assert_same(appearance, widget.appearance_dict.normal_appearance)
234
240
  end
235
241
 
236
242
  it "always generates appearances if force is true" do
237
243
  widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
238
244
  @field.create_appearances
239
- yes = widget.appearance.normal_appearance[:Yes]
245
+ yes = widget.appearance_dict.normal_appearance[:Yes]
240
246
  @field.create_appearances(force: true)
241
- refute_same(yes, widget.appearance.normal_appearance[:Yes])
247
+ refute_same(yes, widget.appearance_dict.normal_appearance[:Yes])
242
248
  end
243
249
 
244
250
  it "fails for unsupported button types" do
@@ -200,9 +200,14 @@ describe HexaPDF::Type::AcroForm::Field do
200
200
 
201
201
  it "deletes the widget if it is embedded" do
202
202
  widget = @field.create_widget(@page)
203
+ @doc.revisions.current.update(widget)
204
+ assert_same(widget, @doc.object(widget))
205
+ refute_same(@field, @doc.object(@field))
206
+
203
207
  @field.delete_widget(widget)
204
208
  refute(@field.key?(:Subtype))
205
209
  assert(@page[:Annots].empty?)
210
+ assert_same(@field, @doc.object(@field))
206
211
  end
207
212
 
208
213
  it "deletes the widget if it is not embedded" do
@@ -254,8 +254,8 @@ describe HexaPDF::Type::AcroForm::Form do
254
254
 
255
255
  it "creates the appearances of all field widgets if necessary" do
256
256
  @acro_form.create_appearances
257
- assert(@tf.each_widget.all? {|w| w.appearance.normal_appearance.kind_of?(HexaPDF::Stream) })
258
- assert(@cb.each_widget.all? {|w| w.appearance.normal_appearance[:Yes].kind_of?(HexaPDF::Stream) })
257
+ assert(@tf.each_widget.all? {|w| w.appearance_dict.normal_appearance.kind_of?(HexaPDF::Stream) })
258
+ assert(@cb.each_widget.all? {|w| w.appearance_dict.normal_appearance[:Yes].kind_of?(HexaPDF::Stream) })
259
259
  end
260
260
 
261
261
  it "force the creation of appearances if force is true" do
@@ -268,6 +268,50 @@ describe HexaPDF::Type::AcroForm::Form do
268
268
  end
269
269
  end
270
270
 
271
+ describe "flatten" do
272
+ before do
273
+ @acro_form.root_fields << @doc.wrap({T: 'test'})
274
+ @tf = @acro_form.create_text_field('textfields')
275
+ @tf.set_default_appearance_string
276
+ @tf[:V] = 'Test'
277
+ @tf.create_widget(@doc.pages.add)
278
+ @cb = @acro_form.create_check_box('test.checkbox')
279
+ @cb.create_widget(@doc.pages[0])
280
+ @cb.create_widget(@doc.pages.add)
281
+ end
282
+
283
+ it "creates the missing appearances if instructed to do so" do
284
+ assert_equal(3, @acro_form.flatten(create_appearances: false).size)
285
+ assert_equal(0, @acro_form.flatten(create_appearances: true).size)
286
+ end
287
+
288
+ it "flattens the whole interactive form" do
289
+ result = @acro_form.flatten
290
+ assert(result.empty?)
291
+ assert(@tf.null?)
292
+ assert(@cb.null?)
293
+ assert(@acro_form.null?)
294
+ refute(@doc.catalog.key?(:AcroForm))
295
+ end
296
+
297
+ it "flattens the given fields" do
298
+ result = @acro_form.flatten(fields: [@cb])
299
+ assert(result.empty?)
300
+ assert(@cb.null?)
301
+ refute(@tf.null?)
302
+ refute(@acro_form.null?)
303
+ assert(@doc.catalog.key?(:AcroForm))
304
+ end
305
+
306
+ it "doesn't delete the form object if not all fields were flattened" do
307
+ @acro_form.create_appearances
308
+ @tf.delete(:AP)
309
+ result = @acro_form.flatten(create_appearances: false)
310
+ assert_equal(1, result.size)
311
+ assert(@doc.catalog.key?(:AcroForm))
312
+ end
313
+ end
314
+
271
315
  describe "perform_validation" do
272
316
  it "checks whether the /DR field is available when /DA is set" do
273
317
  @acro_form[:DA] = 'test'
@@ -0,0 +1,38 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/acro_form/signature_field'
6
+
7
+ describe HexaPDF::Type::AcroForm::SignatureField::LockDictionary do
8
+ it "validates the presence of the /Fields key" do
9
+ doc = HexaPDF::Document.new
10
+ obj = HexaPDF::Type::AcroForm::SignatureField::LockDictionary.new({Action: :All}, document: doc)
11
+ assert(obj.validate)
12
+ obj[:Action] = :Include
13
+ refute(obj.validate)
14
+ end
15
+ end
16
+
17
+ describe HexaPDF::Type::AcroForm::SignatureField do
18
+ before do
19
+ @doc = HexaPDF::Document.new
20
+ @field = @doc.wrap({}, type: :XXAcroFormField, subtype: :Sig)
21
+ end
22
+
23
+ it "sets the field value" do
24
+ @field.field_value = {Empty: :True}
25
+ assert_equal({Empty: :True}, @field[:V].value)
26
+ end
27
+
28
+ it "gets the field value" do
29
+ @field[:V] = {Empty: :True}
30
+ assert_equal({Empty: :True}, @field.field_value.value)
31
+ end
32
+
33
+ it "validates the value of the /FT field" do
34
+ refute(@field.validate(auto_correct: false))
35
+ assert(@field.validate)
36
+ assert_equal(:Sig, @field.field_type)
37
+ end
38
+ end
@@ -56,6 +56,8 @@ describe HexaPDF::Type::Annotations::Widget do
56
56
 
57
57
  describe "background_color" do
58
58
  it "returns the current background color" do
59
+ assert_nil(@widget.background_color)
60
+ @widget[:MK] = {BG: []}
59
61
  assert_nil(@widget.background_color)
60
62
  @widget[:MK] = {BG: [1]}
61
63
  assert_equal([1], @widget.background_color.components)
@@ -40,19 +40,29 @@ describe HexaPDF::Type::Annotation do
40
40
 
41
41
  it "returns the appearance dictionary" do
42
42
  @annot[:AP] = :yes
43
- assert_equal(:yes, @annot.appearance)
43
+ assert_equal(:yes, @annot.appearance_dict)
44
44
  end
45
45
 
46
- it "checks whether an appearance exists" do
47
- refute(@annot.appearance?)
46
+ it "returns the appearance stream of the given type" do
47
+ assert_nil(@annot.appearance)
48
+
48
49
  @annot[:AP] = {N: {}}
49
- refute(@annot.appearance?)
50
- @annot[:AP][:N] = @doc.wrap({}, stream: '')
51
- assert(@annot.appearance?)
52
- @annot[:AP][:N] = {okay: @doc.wrap({}, stream: '')}
53
- assert(@annot.appearance?)
54
- @annot[:AP][:N][:Off] = :other
55
- refute(@annot.appearance?)
50
+ assert_nil(@annot.appearance)
51
+
52
+ stream = @doc.wrap({}, stream: '')
53
+ @annot[:AP][:N] = stream
54
+ appearance = @annot.appearance
55
+ assert_same(stream.data, appearance.data)
56
+ assert_equal(:Form, appearance[:Subtype])
57
+
58
+ @annot[:AP][:N] = {X: stream}
59
+ assert_nil(@annot.appearance)
60
+
61
+ @annot[:AS] = :X
62
+ assert_same(stream.data, @annot.appearance.data)
63
+
64
+ @annot[:AP][:D] = {X: stream}
65
+ assert_same(stream.data, @annot.appearance(:down).data)
56
66
  end
57
67
 
58
68
  describe "flags" do
@@ -44,6 +44,13 @@ describe HexaPDF::Type::FontDescriptor do
44
44
  refute(@font_desc.validate)
45
45
  end
46
46
 
47
+ it "deletes the /FontWeight value if it doesn't contain a valid value" do
48
+ @font_desc[:FontWeight] = 350
49
+ refute(@font_desc.validate(auto_correct: false))
50
+ assert(@font_desc.validate)
51
+ refute(@font_desc.key?(:FontWeight))
52
+ end
53
+
47
54
  it "updates the /Descent value if it is not a negative number" do
48
55
  @font_desc[:Descent] = 5
49
56
  refute(@font_desc.validate(auto_correct: false))
@@ -26,7 +26,7 @@ describe HexaPDF::Type::Page do
26
26
  end
27
27
 
28
28
  # Asserts that the page's contents contains the operators.
29
- def assert_operators(page, operators)
29
+ def assert_page_operators(page, operators)
30
30
  processor = TestHelper::OperatorRecorder.new
31
31
  page.process_contents(processor)
32
32
  assert_equal(operators, processor.recorded_ops)
@@ -293,8 +293,8 @@ describe HexaPDF::Type::Page do
293
293
  it "parses the contents and processes it" do
294
294
  page = @doc.pages.add
295
295
  page[:Contents] = @doc.wrap({}, stream: 'q 10 w Q')
296
- assert_operators(page, [[:save_graphics_state], [:set_line_width, [10]],
297
- [:restore_graphics_state]])
296
+ assert_page_operators(page, [[:save_graphics_state], [:set_line_width, [10]],
297
+ [:restore_graphics_state]])
298
298
  end
299
299
  end
300
300
 
@@ -333,60 +333,60 @@ describe HexaPDF::Type::Page do
333
333
 
334
334
  it "works correctly if invoked on an empty page, using type :page in first invocation" do
335
335
  @page.canvas.line_width = 10
336
- assert_operators(@page, [[:set_line_width, [10]]])
336
+ assert_page_operators(@page, [[:set_line_width, [10]]])
337
337
 
338
338
  @page.canvas(type: :overlay).line_width = 5
339
- assert_operators(@page, [[:save_graphics_state], [:restore_graphics_state],
340
- [:save_graphics_state], [:set_line_width, [10]],
341
- [:restore_graphics_state], [:save_graphics_state],
342
- [:set_line_width, [5]], [:restore_graphics_state]])
339
+ assert_page_operators(@page, [[:save_graphics_state], [:restore_graphics_state],
340
+ [:save_graphics_state], [:set_line_width, [10]],
341
+ [:restore_graphics_state], [:save_graphics_state],
342
+ [:set_line_width, [5]], [:restore_graphics_state]])
343
343
 
344
344
  @page.canvas(type: :underlay).line_width = 2
345
- assert_operators(@page, [[:save_graphics_state], [:set_line_width, [2]],
346
- [:restore_graphics_state], [:save_graphics_state],
347
- [:set_line_width, [10]],
348
- [:restore_graphics_state], [:save_graphics_state],
349
- [:set_line_width, [5]], [:restore_graphics_state]])
345
+ assert_page_operators(@page, [[:save_graphics_state], [:set_line_width, [2]],
346
+ [:restore_graphics_state], [:save_graphics_state],
347
+ [:set_line_width, [10]],
348
+ [:restore_graphics_state], [:save_graphics_state],
349
+ [:set_line_width, [5]], [:restore_graphics_state]])
350
350
  end
351
351
 
352
352
  it "works correctly if invoked on an empty page, using type :underlay in first invocation" do
353
353
  @page.canvas(type: :underlay).line_width = 2
354
- assert_operators(@page, [[:save_graphics_state], [:set_line_width, [2]],
355
- [:restore_graphics_state], [:save_graphics_state],
356
- [:restore_graphics_state], [:save_graphics_state],
357
- [:restore_graphics_state]])
354
+ assert_page_operators(@page, [[:save_graphics_state], [:set_line_width, [2]],
355
+ [:restore_graphics_state], [:save_graphics_state],
356
+ [:restore_graphics_state], [:save_graphics_state],
357
+ [:restore_graphics_state]])
358
358
 
359
359
  @page.canvas.line_width = 10
360
- assert_operators(@page, [[:save_graphics_state], [:set_line_width, [2]],
361
- [:restore_graphics_state], [:save_graphics_state],
362
- [:set_line_width, [10]], [:restore_graphics_state],
363
- [:save_graphics_state], [:restore_graphics_state]])
360
+ assert_page_operators(@page, [[:save_graphics_state], [:set_line_width, [2]],
361
+ [:restore_graphics_state], [:save_graphics_state],
362
+ [:set_line_width, [10]], [:restore_graphics_state],
363
+ [:save_graphics_state], [:restore_graphics_state]])
364
364
 
365
365
  @page.canvas(type: :overlay).line_width = 5
366
- assert_operators(@page, [[:save_graphics_state], [:set_line_width, [2]],
367
- [:restore_graphics_state], [:save_graphics_state],
368
- [:set_line_width, [10]],
369
- [:restore_graphics_state], [:save_graphics_state],
370
- [:set_line_width, [5]], [:restore_graphics_state]])
366
+ assert_page_operators(@page, [[:save_graphics_state], [:set_line_width, [2]],
367
+ [:restore_graphics_state], [:save_graphics_state],
368
+ [:set_line_width, [10]],
369
+ [:restore_graphics_state], [:save_graphics_state],
370
+ [:set_line_width, [5]], [:restore_graphics_state]])
371
371
  end
372
372
 
373
373
  it "works correctly if invoked on a page with existing contents" do
374
374
  @page.contents = "10 w"
375
375
 
376
376
  @page.canvas(type: :overlay).line_width = 5
377
- assert_operators(@page, [[:save_graphics_state], [:restore_graphics_state],
378
- [:save_graphics_state], [:set_line_width, [10]],
379
- [:restore_graphics_state],
380
- [:save_graphics_state], [:set_line_width, [5]],
381
- [:restore_graphics_state]])
377
+ assert_page_operators(@page, [[:save_graphics_state], [:restore_graphics_state],
378
+ [:save_graphics_state], [:set_line_width, [10]],
379
+ [:restore_graphics_state],
380
+ [:save_graphics_state], [:set_line_width, [5]],
381
+ [:restore_graphics_state]])
382
382
 
383
383
  @page.canvas(type: :underlay).line_width = 2
384
- assert_operators(@page, [[:save_graphics_state], [:set_line_width, [2]],
385
- [:restore_graphics_state], [:save_graphics_state],
386
- [:set_line_width, [10]],
387
- [:restore_graphics_state],
388
- [:save_graphics_state], [:set_line_width, [5]],
389
- [:restore_graphics_state]])
384
+ assert_page_operators(@page, [[:save_graphics_state], [:set_line_width, [2]],
385
+ [:restore_graphics_state], [:save_graphics_state],
386
+ [:set_line_width, [10]],
387
+ [:restore_graphics_state],
388
+ [:save_graphics_state], [:set_line_width, [5]],
389
+ [:restore_graphics_state]])
390
390
  end
391
391
 
392
392
  it "works correctly if the page has its origin not at (0,0)" do
@@ -395,20 +395,20 @@ describe HexaPDF::Type::Page do
395
395
  @page.canvas(type: :page).line_width = 2
396
396
  @page.canvas(type: :overlay).line_width = 2
397
397
 
398
- assert_operators(@page, [[:save_graphics_state],
399
- [:concatenate_matrix, [1, 0, 0, 1, -10, -5]],
400
- [:set_line_width, [2]],
401
- [:restore_graphics_state],
398
+ assert_page_operators(@page, [[:save_graphics_state],
399
+ [:concatenate_matrix, [1, 0, 0, 1, -10, -5]],
400
+ [:set_line_width, [2]],
401
+ [:restore_graphics_state],
402
402
 
403
- [:save_graphics_state],
404
- [:concatenate_matrix, [1, 0, 0, 1, -10, -5]],
405
- [:set_line_width, [2]],
406
- [:restore_graphics_state],
403
+ [:save_graphics_state],
404
+ [:concatenate_matrix, [1, 0, 0, 1, -10, -5]],
405
+ [:set_line_width, [2]],
406
+ [:restore_graphics_state],
407
407
 
408
- [:save_graphics_state],
409
- [:concatenate_matrix, [1, 0, 0, 1, -10, -5]],
410
- [:set_line_width, [2]],
411
- [:restore_graphics_state]])
408
+ [:save_graphics_state],
409
+ [:concatenate_matrix, [1, 0, 0, 1, -10, -5]],
410
+ [:set_line_width, [2]],
411
+ [:restore_graphics_state]])
412
412
  end
413
413
 
414
414
  it "fails if the page canvas is requested for a page with existing contents" do
@@ -446,4 +446,142 @@ describe HexaPDF::Type::Page do
446
446
  refute_same(form.raw_stream, page[:Contents].raw_stream)
447
447
  end
448
448
  end
449
+
450
+ describe "flatten_annotations" do
451
+ before do
452
+ @page = @doc.pages.add
453
+ @appearance = @doc.add({Type: :XObject, Subtype: :Form, BBox: [-10, -5, 50, 20]}, stream: "")
454
+ @annot1 = @doc.add({Type: :Annot, Subtype: :Text, Rect: [100, 100, 160, 125], AP: {N: @appearance}})
455
+ @annot2 = @doc.add({Rect: [10, 10, 70, 35], AP: {N: @appearance}})
456
+ @page[:Annots] = [@annot1, @annot2]
457
+ @canvas = @page.canvas(type: :overlay)
458
+ end
459
+
460
+ it "does nothing if the page doesn't have any annotations" do
461
+ @page.delete(:Annots)
462
+ result = @page.flatten_annotations
463
+ assert(result.empty?)
464
+ assert_operators(@canvas.contents, [])
465
+ end
466
+
467
+ it "flattens all annotations of the page by default" do
468
+ result = @page.flatten_annotations
469
+ assert(result.empty?)
470
+ assert_operators(@canvas.contents, [[:save_graphics_state],
471
+ [:save_graphics_state],
472
+ [:concatenate_matrix, [1.0, 0, 0, 1.0, 110, 105]],
473
+ [:paint_xobject, [:XO1]],
474
+ [:restore_graphics_state],
475
+ [:save_graphics_state],
476
+ [:concatenate_matrix, [1.0, 0, 0, 1.0, 20, 15]],
477
+ [:paint_xobject, [:XO1]],
478
+ [:restore_graphics_state],
479
+ [:restore_graphics_state]])
480
+ assert(@annot1.null?)
481
+ assert(@annot2.null?)
482
+ end
483
+
484
+ it "only deletes the widget annotation of a form field even if it is embedded in the field object" do
485
+ form = @doc.acro_form(create: true)
486
+ field = form.create_text_field('test')
487
+ widget = field.create_widget(@page, Rect: [200, 200, 250, 215])
488
+ field.field_value = 'hello'
489
+
490
+ assert_same(widget.data, field.data)
491
+ result = @page.flatten_annotations([widget])
492
+ assert(result.empty?)
493
+ refute(field.null?)
494
+ end
495
+
496
+ it "does nothing if a given annotation is not part of the page" do
497
+ annots = [{Test: :Object}]
498
+ result = @page.flatten_annotations(annots)
499
+ assert_equal(annots, result)
500
+ end
501
+
502
+ it "deletes hidden annotations but doesn't include them in the content stream" do
503
+ @annot1.flag(:hidden)
504
+ result = @page.flatten_annotations
505
+ assert(result.empty?)
506
+ assert(@annot1.null?)
507
+ assert_operators(@canvas.contents, [[:save_graphics_state],
508
+ [:save_graphics_state],
509
+ [:concatenate_matrix, [1.0, 0, 0, 1.0, 20, 15]],
510
+ [:paint_xobject, [:XO1]],
511
+ [:restore_graphics_state],
512
+ [:restore_graphics_state]])
513
+ end
514
+
515
+ it "deletes invisible annotations but doesn't include them in the content stream" do
516
+ @annot1.flag(:invisible)
517
+ result = @page.flatten_annotations
518
+ assert(result.empty?)
519
+ assert(@annot1.null?)
520
+ assert_operators(@canvas.contents, [[:save_graphics_state],
521
+ [:save_graphics_state],
522
+ [:concatenate_matrix, [1.0, 0, 0, 1.0, 20, 15]],
523
+ [:paint_xobject, [:XO1]],
524
+ [:restore_graphics_state],
525
+ [:restore_graphics_state]])
526
+ end
527
+
528
+ it "ignores annotations without appearane stream" do
529
+ @annot1.delete(:AP)
530
+ result = @page.flatten_annotations
531
+ assert_equal([@annot1], result)
532
+ refute(@annot1.empty?)
533
+ assert_operators(@canvas.contents, [[:save_graphics_state],
534
+ [:save_graphics_state],
535
+ [:concatenate_matrix, [1.0, 0, 0, 1.0, 20, 15]],
536
+ [:paint_xobject, [:XO1]],
537
+ [:restore_graphics_state],
538
+ [:restore_graphics_state]])
539
+ end
540
+
541
+ it "adjusts the position to counter the translation in #canvas based on the page's media box" do
542
+ @page[:MediaBox] = [-15, -15, 100, 100]
543
+ @page.flatten_annotations
544
+ assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 15, 15]], range: 1)
545
+ end
546
+
547
+ it "adjusts the position in case the form /Matrix has an offset" do
548
+ @appearance[:Matrix] = [1, 0, 0, 1, 15, 15]
549
+ @page.flatten_annotations
550
+ assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 95, 90]], range: 2)
551
+ end
552
+
553
+ it "adjusts the position for an appearance with a 90 degree rotation" do
554
+ @appearance[:Matrix] = [0, 1, -1, 0, 0, 0]
555
+ @annot1[:Rect] = [100, 100, 125, 160]
556
+ @page.flatten_annotations
557
+ assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 120, 110]], range: 2)
558
+ end
559
+
560
+ it "adjusts the position for an appearance with a -90 degree rotation" do
561
+ @appearance[:Matrix] = [0, -1, 1, 0, 0, 0]
562
+ @annot1[:Rect] = [100, 100, 125, 160]
563
+ @page.flatten_annotations
564
+ assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 105, 150]], range: 2)
565
+ end
566
+
567
+ it "adjusts the position for an appearance with a 180 degree rotation" do
568
+ @appearance[:Matrix] = [-1, 0, 0, -1, 0, 0]
569
+ @page.flatten_annotations
570
+ assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 150, 120]], range: 2)
571
+ end
572
+
573
+ it "ignores an appearance with a rotation that is not a mulitple of 90" do
574
+ @appearance[:Matrix] = [-1, 0.5, 0.5, -1, 0, 0]
575
+ result = @page.flatten_annotations
576
+ assert_equal([@annot1, @annot2], result)
577
+ assert_operators(@canvas.contents, [[:save_graphics_state], [:restore_graphics_state]])
578
+ end
579
+
580
+ it "scales the appearance to fit into the annotations's rectangle" do
581
+ @annot1[:Rect] = [100, 100, 130, 150]
582
+ @page.flatten_annotations
583
+ assert_operators(@canvas.contents, [:concatenate_matrix, [0.5, 0, 0, 2, 110, 105]], range: 2)
584
+ end
585
+
586
+ end
449
587
  end