hexapdf 0.14.4 → 0.15.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/lib/hexapdf/cli/form.rb +30 -8
  4. data/lib/hexapdf/configuration.rb +18 -3
  5. data/lib/hexapdf/content/canvas.rb +1 -0
  6. data/lib/hexapdf/encryption/standard_security_handler.rb +16 -0
  7. data/lib/hexapdf/error.rb +4 -3
  8. data/lib/hexapdf/parser.rb +18 -6
  9. data/lib/hexapdf/revision.rb +16 -0
  10. data/lib/hexapdf/type/acro_form.rb +1 -0
  11. data/lib/hexapdf/type/acro_form/appearance_generator.rb +29 -17
  12. data/lib/hexapdf/type/acro_form/button_field.rb +8 -4
  13. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  14. data/lib/hexapdf/type/acro_form/form.rb +37 -0
  15. data/lib/hexapdf/type/acro_form/signature_field.rb +223 -0
  16. data/lib/hexapdf/type/annotation.rb +18 -9
  17. data/lib/hexapdf/type/annotations/widget.rb +3 -1
  18. data/lib/hexapdf/type/font_descriptor.rb +9 -2
  19. data/lib/hexapdf/type/page.rb +81 -0
  20. data/lib/hexapdf/utils/graphics_helpers.rb +4 -4
  21. data/lib/hexapdf/version.rb +1 -1
  22. data/test/hexapdf/content/test_canvas.rb +21 -0
  23. data/test/hexapdf/encryption/test_standard_security_handler.rb +27 -0
  24. data/test/hexapdf/test_parser.rb +23 -3
  25. data/test/hexapdf/test_revision.rb +21 -0
  26. data/test/hexapdf/test_writer.rb +2 -2
  27. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +21 -2
  28. data/test/hexapdf/type/acro_form/test_button_field.rb +13 -7
  29. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  30. data/test/hexapdf/type/acro_form/test_form.rb +46 -2
  31. data/test/hexapdf/type/acro_form/test_signature_field.rb +38 -0
  32. data/test/hexapdf/type/annotations/test_widget.rb +2 -0
  33. data/test/hexapdf/type/test_annotation.rb +24 -10
  34. data/test/hexapdf/type/test_font_descriptor.rb +7 -0
  35. data/test/hexapdf/type/test_page.rb +187 -49
  36. data/test/hexapdf/utils/test_graphics_helpers.rb +8 -0
  37. metadata +4 -2
@@ -47,18 +47,18 @@ module HexaPDF
47
47
  #
48
48
  # +rwidth+::
49
49
  # The requested width. If +rheight+ is not specified, it is chosen so that the aspect
50
- # ratio is maintained
50
+ # ratio is maintained. In case of +width+ begin zero, +height+ is used for the height.
51
51
  #
52
52
  # +rheight+::
53
53
  # The requested height. If +rwidth+ is not specified, it is chosen so that the aspect
54
- # ratio is maintained
54
+ # ratio is maintained. In case of +height+ begin zero, +width+ is used for the width.
55
55
  def calculate_dimensions(width, height, rwidth: nil, rheight: nil)
56
56
  if rwidth && rheight
57
57
  [rwidth, rheight]
58
58
  elsif rwidth
59
- [rwidth, height * rwidth / width.to_f]
59
+ [rwidth, width == 0 ? height : height * rwidth / width.to_f]
60
60
  elsif rheight
61
- [width * rheight / height.to_f, rheight]
61
+ [height == 0 ? width : width * rheight / height.to_f, rheight]
62
62
  else
63
63
  [width, height]
64
64
  end
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.14.4'
40
+ VERSION = '0.15.4'
41
41
 
42
42
  end
@@ -831,6 +831,17 @@ describe HexaPDF::Content::Canvas do
831
831
  [:restore_graphics_state]])
832
832
  end
833
833
 
834
+ it "doesn't do anything if the image's width or height is zero" do
835
+ @image[:Width] = 0
836
+ @canvas.xobject(@image, at: [0, 0])
837
+ assert_operators(@page.contents, [])
838
+
839
+ @image[:Width] = 10
840
+ @image[:Height] = 0
841
+ @canvas.xobject(@image, at: [0, 0])
842
+ assert_operators(@page.contents, [])
843
+ end
844
+
834
845
  it "correctly serializes the form with no options" do
835
846
  @canvas.xobject(@form, at: [1, 2])
836
847
  assert_operators(@page.contents, [[:save_graphics_state],
@@ -862,6 +873,16 @@ describe HexaPDF::Content::Canvas do
862
873
  [:paint_xobject, [:XO1]],
863
874
  [:restore_graphics_state]])
864
875
  end
876
+
877
+ it "doesn't do anything if the form's width or height is zero" do
878
+ @form[:BBox] = [100, 50, 100, 200]
879
+ @canvas.xobject(@form, at: [0, 0])
880
+ assert_operators(@page.contents, [])
881
+
882
+ @form[:BBox] = [100, 50, 150, 50]
883
+ @canvas.xobject(@form, at: [0, 0])
884
+ assert_operators(@page.contents, [])
885
+ end
865
886
  end
866
887
 
867
888
  describe "character_spacing" do
@@ -292,4 +292,31 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
292
292
  @handler.set_up_encryption(permissions: perms)
293
293
  assert_equal([:copy_content, :modify_content], @handler.permissions.sort)
294
294
  end
295
+
296
+ describe "handling of metadata streams" do
297
+ before do
298
+ @doc = HexaPDF::Document.new
299
+ @output = StringIO.new(''.b)
300
+ end
301
+
302
+ it "doesn't decrypt or encrypt a metadata stream if /EncryptMetadata is false" do
303
+ @doc.encrypt(encrypt_metadata: false)
304
+ @doc.catalog[:Metadata] = @doc.wrap({Type: :Metadata, Subtype: :XML}, stream: "HELLODATA")
305
+ @doc.write(@output)
306
+ assert_match(/stream\nHELLODATA\nendstream/, @output.string)
307
+
308
+ doc = HexaPDF::Document.new(io: @output)
309
+ assert_equal('HELLODATA', doc.catalog[:Metadata].stream)
310
+ end
311
+
312
+ it "doesn't modify decryption/encryption for metadata streams if /V is not 4 or 5" do
313
+ @doc.encrypt(encrypt_metadata: false, algorithm: :arc4)
314
+ @doc.catalog[:Metadata] = @doc.wrap({Type: :Metadata, Subtype: :XML}, stream: "HELLODATA")
315
+ @doc.write(@output)
316
+ refute_match(/stream\nHELLODATA\nendstream/, @output.string)
317
+
318
+ doc = HexaPDF::Document.new(io: @output)
319
+ assert_equal('HELLODATA', doc.catalog[:Metadata].stream)
320
+ end
321
+ end
295
322
  end
@@ -50,7 +50,8 @@ describe HexaPDF::Parser do
50
50
  end
51
51
 
52
52
  def create_parser(str)
53
- @parser = HexaPDF::Parser.new(StringIO.new(str), @document)
53
+ @parse_io = StringIO.new(str)
54
+ @parser = HexaPDF::Parser.new(@parse_io, @document)
54
55
  end
55
56
 
56
57
  describe "parse_indirect_object" do
@@ -94,6 +95,12 @@ describe HexaPDF::Parser do
94
95
  assert_equal('12', TestHelper.collector(stream.fiber))
95
96
  end
96
97
 
98
+ it "handles keyword stream followed by space and CR LF" do
99
+ create_parser("1 0 obj<</Length 2>> stream \r\n12\nendstream endobj")
100
+ *, stream = @parser.parse_indirect_object
101
+ assert_equal('12', TestHelper.collector(stream.fiber))
102
+ end
103
+
97
104
  it "handles invalid indirect object value consisting of number followed by endobj without space" do
98
105
  create_parser("1 0 obj 749endobj")
99
106
  object, * = @parser.parse_indirect_object
@@ -166,7 +173,13 @@ describe HexaPDF::Parser do
166
173
  it "fails if keyword stream is followed by space and CR or LF instead of LF or CR/LF" do
167
174
  create_parser("1 0 obj<</Length 2>> stream \n12\nendstream endobj")
168
175
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
169
- assert_match(/must be followed by LF or CR\/LF/, exp.message)
176
+ assert_match(/followed by space instead/, exp.message)
177
+ end
178
+
179
+ it "fails if keyword stream is followed by space and CR LF instead of LF or CR/LF" do
180
+ create_parser("1 0 obj<</Length 2>> stream \r\n12\nendstream endobj")
181
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
182
+ assert_match(/followed by space instead/, exp.message)
170
183
  end
171
184
 
172
185
  it "fails for numbers followed by endobj without space" do
@@ -511,6 +524,13 @@ describe HexaPDF::Parser do
511
524
  assert_match(/not a cross-reference stream/, exp.message)
512
525
  end
513
526
 
527
+ it "fails if the cross-reference stream is missing data" do
528
+ @parse_io.string[287..288] = ''
529
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.load_revision(212) }
530
+ assert_match(/missing data/, exp.message)
531
+ assert_equal(212, exp.pos)
532
+ end
533
+
514
534
  it "fails on strict parsing if the cross-reference stream doesn't contain an entry for itself" do
515
535
  @document.config['parser.on_correctable_error'] = proc { true }
516
536
  create_parser("2 0 obj\n<</Type/XRef/Length 3/W [1 1 1]/Size 1>>" \
@@ -578,7 +598,7 @@ describe HexaPDF::Parser do
578
598
  end
579
599
 
580
600
  it "uses the first trailer in case of a linearized file" do
581
- create_parser("trailer <</Size 1/Prev 342>>\ntrailer <</Size 2>>")
601
+ create_parser("1 0 obj\n<</Linearized true>>\nendobj\ntrailer <</Size 1/Prev 342>>\ntrailer <</Size 2>>")
582
602
  assert_equal({Size: 1}, @parser.reconstructed_revision.trailer.value)
583
603
  end
584
604
 
@@ -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)
@@ -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.4)>>
43
+ <</Producer(HexaPDF version 0.15.4)>>
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.4)>>
75
+ <</Producer(HexaPDF version 0.15.4)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -748,17 +748,36 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
748
748
 
749
749
  describe "font resolution in case the referenced font is not usable" do
750
750
  before do
751
- @doc.config['acro_form.fallback_font'] = ['Times', {variant: :none}]
751
+ @doc.config['acro_form.fallback_font'] = ['Times', {variant: :italic}]
752
752
  @field[:V] = 'Test'
753
753
  end
754
754
 
755
755
  it "uses the fallback font if the font is not usable" do
756
756
  def (@form.default_resources.font(:F1)).font_wrapper; nil; end
757
757
  @generator.create_appearances
758
- assert_equal(:'Times-Roman', @widget[:AP][:N][:Resources][:Font][:F2][:BaseFont])
758
+ assert_equal(:'Times-Italic', @widget[:AP][:N][:Resources][:Font][:F2][:BaseFont])
759
759
  end
760
760
 
761
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
762
781
  @form.default_resources[:Font].delete(:F1)
763
782
  @generator.create_appearances
764
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,33 @@ 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
+ assert_nil(@annot.appearance)
55
+
56
+ stream[:BBox] = [1, 2, 3, 4]
57
+ appearance = @annot.appearance
58
+ assert_same(stream.data, appearance.data)
59
+ assert_equal(:Form, appearance[:Subtype])
60
+
61
+ @annot[:AP][:N] = {X: {}}
62
+ assert_nil(@annot.appearance)
63
+
64
+ @annot[:AS] = :X
65
+ @annot[:AP][:N][:X] = stream
66
+ assert_same(stream.data, @annot.appearance.data)
67
+
68
+ @annot[:AP][:D] = {X: stream}
69
+ assert_same(stream.data, @annot.appearance(:down).data)
56
70
  end
57
71
 
58
72
  describe "flags" do