hexapdf 0.14.4 → 0.15.4

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