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
@@ -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