hexapdf 1.1.1 → 1.3.0

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/README.md +1 -1
  4. data/lib/hexapdf/cli/command.rb +63 -63
  5. data/lib/hexapdf/cli/inspect.rb +14 -5
  6. data/lib/hexapdf/cli/modify.rb +0 -1
  7. data/lib/hexapdf/cli/optimize.rb +5 -5
  8. data/lib/hexapdf/composer.rb +14 -0
  9. data/lib/hexapdf/configuration.rb +26 -0
  10. data/lib/hexapdf/content/graphics_state.rb +1 -1
  11. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +1 -1
  12. data/lib/hexapdf/document/annotations.rb +173 -0
  13. data/lib/hexapdf/document/layout.rb +45 -6
  14. data/lib/hexapdf/document.rb +28 -7
  15. data/lib/hexapdf/error.rb +11 -3
  16. data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
  17. data/lib/hexapdf/font/true_type_wrapper.rb +1 -0
  18. data/lib/hexapdf/font/type1_wrapper.rb +1 -0
  19. data/lib/hexapdf/layout/style.rb +101 -7
  20. data/lib/hexapdf/object.rb +2 -2
  21. data/lib/hexapdf/pdf_array.rb +25 -3
  22. data/lib/hexapdf/tokenizer.rb +4 -1
  23. data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
  24. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  25. data/lib/hexapdf/type/acro_form/form.rb +7 -6
  26. data/lib/hexapdf/type/acro_form/java_script_actions.rb +9 -2
  27. data/lib/hexapdf/type/acro_form/text_field.rb +9 -2
  28. data/lib/hexapdf/type/annotation.rb +71 -1
  29. data/lib/hexapdf/type/annotations/appearance_generator.rb +348 -0
  30. data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
  31. data/lib/hexapdf/type/annotations/border_styling.rb +160 -0
  32. data/lib/hexapdf/type/annotations/circle.rb +65 -0
  33. data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
  34. data/lib/hexapdf/type/annotations/line.rb +490 -0
  35. data/lib/hexapdf/type/annotations/square.rb +65 -0
  36. data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
  37. data/lib/hexapdf/type/annotations/widget.rb +52 -116
  38. data/lib/hexapdf/type/annotations.rb +8 -0
  39. data/lib/hexapdf/type/form.rb +2 -2
  40. data/lib/hexapdf/version.rb +1 -1
  41. data/lib/hexapdf/writer.rb +0 -1
  42. data/lib/hexapdf/xref_section.rb +7 -4
  43. data/test/hexapdf/content/test_graphics_state.rb +2 -3
  44. data/test/hexapdf/content/test_operator.rb +4 -5
  45. data/test/hexapdf/digital_signature/test_cms_handler.rb +7 -8
  46. data/test/hexapdf/digital_signature/test_handler.rb +2 -3
  47. data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +1 -2
  48. data/test/hexapdf/document/test_annotations.rb +55 -0
  49. data/test/hexapdf/document/test_layout.rb +24 -2
  50. data/test/hexapdf/font/test_true_type_wrapper.rb +7 -0
  51. data/test/hexapdf/font/test_type1_wrapper.rb +7 -0
  52. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  53. data/test/hexapdf/layout/test_style.rb +27 -2
  54. data/test/hexapdf/task/test_optimize.rb +1 -1
  55. data/test/hexapdf/test_composer.rb +7 -0
  56. data/test/hexapdf/test_document.rb +11 -3
  57. data/test/hexapdf/test_object.rb +1 -1
  58. data/test/hexapdf/test_pdf_array.rb +36 -3
  59. data/test/hexapdf/test_stream.rb +1 -2
  60. data/test/hexapdf/test_xref_section.rb +1 -1
  61. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
  62. data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
  63. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  64. data/test/hexapdf/type/acro_form/test_form.rb +17 -1
  65. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +21 -0
  66. data/test/hexapdf/type/acro_form/test_text_field.rb +7 -1
  67. data/test/hexapdf/type/annotations/test_appearance_generator.rb +482 -0
  68. data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
  69. data/test/hexapdf/type/annotations/test_border_styling.rb +114 -0
  70. data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
  71. data/test/hexapdf/type/annotations/test_line.rb +169 -0
  72. data/test/hexapdf/type/annotations/test_widget.rb +35 -81
  73. data/test/hexapdf/type/test_annotation.rb +55 -0
  74. data/test/hexapdf/type/test_form.rb +6 -0
  75. metadata +17 -2
@@ -127,6 +127,13 @@ describe HexaPDF::Composer do
127
127
  end
128
128
  end
129
129
 
130
+ describe "style?" do
131
+ it "delegates to layout.style?" do
132
+ @composer.document.layout.style(:header, font_size: 20)
133
+ assert(@composer.style?(:header))
134
+ end
135
+ end
136
+
130
137
  describe "styles" do
131
138
  it "delegates to layout.styles" do
132
139
  @composer.styles(base: {font_size: 30}, other: {font_size: 40})
@@ -54,7 +54,7 @@ describe HexaPDF::Document do
54
54
  describe "::open" do
55
55
  before do
56
56
  @file = Tempfile.new('hexapdf-document')
57
- @io_doc.write(@file)
57
+ @io_doc.write(@file, compact: false)
58
58
  @file.close
59
59
  end
60
60
 
@@ -370,7 +370,7 @@ describe HexaPDF::Document do
370
370
  it "writes the document to a file" do
371
371
  file = Tempfile.new('hexapdf-write')
372
372
  file.close
373
- @io_doc.write(file.path)
373
+ @io_doc.write(file.path, compact: false)
374
374
  HexaPDF::Document.open(file.path) do |doc|
375
375
  assert_equal(200, doc.object(2).value)
376
376
  end
@@ -422,10 +422,18 @@ describe HexaPDF::Document do
422
422
 
423
423
  it "allows optimizing the file by using object streams" do
424
424
  io = StringIO.new(''.b)
425
- @io_doc.write(io, optimize: true)
425
+ @io_doc.write(io, optimize: true, compact: false)
426
426
  doc = HexaPDF::Document.new(io: io)
427
427
  assert_equal(2, doc.each.count {|o| o.type == :ObjStm })
428
428
  end
429
+
430
+ it "automatically compacts the file" do
431
+ io = StringIO.new(''.b)
432
+ @io_doc.write(io)
433
+ doc = HexaPDF::Document.new(io: io)
434
+ assert_equal(1, doc.revisions.count)
435
+ assert_equal(4, doc.each.count)
436
+ end
429
437
  end
430
438
 
431
439
  describe "version" do
@@ -197,7 +197,7 @@ describe HexaPDF::Object do
197
197
  @obj.define_singleton_method(:perform_validation) { raise "Unknown" }
198
198
  invoked = []
199
199
  refute(@obj.validate {|*a| invoked << a })
200
- assert_equal([["Error: Unexpected value encountered", false, @obj]], invoked)
200
+ assert_equal([["Unexpected error encountered: Unknown", false, @obj]], invoked)
201
201
  end
202
202
  end
203
203
 
@@ -131,9 +131,42 @@ describe HexaPDF::PDFArray do
131
131
  end
132
132
  end
133
133
 
134
- it "allows deleting elements that are selected using a block" do
135
- @array.reject! {|item| item == :data }
136
- assert_equal([1, "deref", @array[2]], @array[0, 5])
134
+ describe "reject!" do
135
+ it "allows deleting elements that are selected using a block" do
136
+ assert_same(@array, @array.reject! {|item| item == :data })
137
+ assert_equal([1, "deref", @array[2]], @array.to_a)
138
+ end
139
+
140
+ it "returns nil if no elements were deleted" do
141
+ assert_nil(@array.reject! {|item| false })
142
+ end
143
+
144
+ it "returns an enumerator if no block is given" do
145
+ assert_kind_of(Enumerator, @array.reject!)
146
+ end
147
+ end
148
+
149
+ describe "map!" do
150
+ it "maps elements in-place to the return values of the block" do
151
+ assert_same(@array, @array.map! {|item| 5 })
152
+ assert_equal([5, 5, 5, 5], @array.to_a)
153
+ end
154
+
155
+ it "returns an enumerator if no block is given" do
156
+ assert_kind_of(Enumerator, @array.reject!)
157
+ end
158
+ end
159
+
160
+ describe "compact!" do
161
+ it "removes all nil elements and returns self" do
162
+ @array << nil
163
+ assert_same(@array, @array.compact!)
164
+ assert_equal(4, @array.size)
165
+ end
166
+
167
+ it "returns nil if no elements were removed" do
168
+ assert_nil(@array.compact!)
169
+ end
137
170
  end
138
171
 
139
172
  describe "index" do
@@ -1,7 +1,6 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
3
  require 'test_helper'
4
- require 'ostruct'
5
4
  require 'stringio'
6
5
  require 'tempfile'
7
6
  require 'hexapdf/configuration'
@@ -80,7 +79,7 @@ end
80
79
 
81
80
  describe HexaPDF::Stream do
82
81
  before do
83
- @document = OpenStruct.new
82
+ @document = Struct.new(:config).new
84
83
  @document.config = HexaPDF::Configuration.with_defaults
85
84
  @document.instance_variable_set(:@version, '1.2')
86
85
  def (@document).unwrap(obj); obj; end
@@ -66,7 +66,7 @@ describe HexaPDF::XRefSection do
66
66
  @xref_section.add_in_use_entry(1, 0, 0)
67
67
  @xref_section.add_in_use_entry(2, 0, 0)
68
68
  result = @xref_section.each_subsection.map {|s| s.map {|e| [e.oid, e.type] }}
69
- assert_equal([[[1, :in_use], [2, :in_use],
69
+ assert_equal([[[0, :free], [1, :in_use], [2, :in_use],
70
70
  [3, :free], [4, :free], [5, :free],
71
71
  [6, :in_use], [7, :in_use],
72
72
  [8, :free],
@@ -373,12 +373,87 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
373
373
  describe "push buttons" do
374
374
  before do
375
375
  @field.initialize_as_push_button
376
- @widget = @field.create_widget(@page, Rect: [0, 0, 0, 0])
376
+ @widget = @field.create_widget(@page, Rect: [0, 0, 100, 50])
377
+ @widget.marker_style(style: 'Test')
377
378
  @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
378
379
  end
379
380
 
380
- it "fails because it is not implemented yet" do
381
- assert_raises(HexaPDF::Error) { @generator.create_appearances }
381
+ it "set the print flag on the widgets" do
382
+ @generator.create_appearances
383
+ assert(@widget.flagged?(:print))
384
+ end
385
+
386
+ it "removes the hidden flag on the widgets" do
387
+ @widget.flag(:hidden)
388
+ @generator.create_appearances
389
+ refute(@widget.flagged?(:hidden))
390
+ end
391
+
392
+ it "adds an appropriate form XObject" do
393
+ @generator.create_appearances
394
+ form = @widget[:AP][:N]
395
+ assert_equal(:XObject, form.type)
396
+ assert_equal(:Form, form[:Subtype])
397
+ assert_equal([0, 0, 100, 50], form[:BBox])
398
+ assert_equal(@doc.acro_form.default_resources[:Font][:F1], form[:Resources][:Font][:F1])
399
+ end
400
+
401
+ it "re-uses the existing form XObject" do
402
+ @generator.create_appearances
403
+ form = @widget[:AP][:N]
404
+ form[:key] = :value
405
+ form.delete(:Subtype)
406
+ @widget[:AP][:N] = @doc.wrap(form, type: HexaPDF::Dictionary)
407
+
408
+ @generator.create_appearances
409
+ assert_equal(form, @widget[:AP][:N])
410
+ refute(form.key?(:key))
411
+ end
412
+
413
+ describe "takes the rotation into account" do
414
+ def check_rotation(angle, width, height, matrix)
415
+ @widget[:MK][:R] = angle
416
+ @generator.create_appearances
417
+ form = @widget[:AP][:N]
418
+ assert_equal([0, 0, width, height], form[:BBox].value)
419
+ assert_equal(matrix, form[:Matrix].value)
420
+ end
421
+
422
+ it "works for 0 degrees" do
423
+ check_rotation(-360, @widget[:Rect].width, @widget[:Rect].height, [1, 0, 0, 1, 0, 0])
424
+ end
425
+
426
+ it "works for 90 degrees" do
427
+ check_rotation(450, @widget[:Rect].height, @widget[:Rect].width, [0, 1, -1, 0, 0, 0])
428
+ end
429
+
430
+ it "works for 180 degrees" do
431
+ check_rotation(180, @widget[:Rect].width, @widget[:Rect].height, [0, -1, -1, 0, 0, 0])
432
+ end
433
+
434
+ it "works for 270 degrees" do
435
+ check_rotation(-90, @widget[:Rect].height, @widget[:Rect].width, [0, -1, 1, 0, 0, 0])
436
+ end
437
+ end
438
+
439
+ it "adds the button title in the center" do
440
+ @generator.create_appearances
441
+ assert_operators(@widget[:AP][:N].stream,
442
+ [[:save_graphics_state],
443
+ [:set_device_gray_non_stroking_color, [0.5]],
444
+ [:append_rectangle, [0, 0, 100, 50]],
445
+ [:fill_path_non_zero],
446
+ [:append_rectangle, [0.5, 0.5, 99.0, 49.0]],
447
+ [:stroke_path],
448
+ [:restore_graphics_state],
449
+ [:save_graphics_state],
450
+ [:set_font_and_size, [:F1, 9]],
451
+ [:begin_text],
452
+ [:move_text, [41.2475, 22.7005]],
453
+ [:show_text, ["Test"]],
454
+ [:end_text],
455
+ [:restore_graphics_state]],
456
+ )
382
457
  end
383
458
  end
384
459
  end
@@ -236,6 +236,13 @@ describe HexaPDF::Type::AcroForm::ButtonField do
236
236
  assert(widget[:AP][:N][:test])
237
237
  end
238
238
 
239
+ it "works for push buttons" do
240
+ @field.initialize_as_push_button
241
+ @field.create_widget(@doc.pages.add, Rect: [0, 0, 100, 50])
242
+ @field.create_appearances
243
+ assert(@field[:AP][:N])
244
+ end
245
+
239
246
  it "won't generate appearances if they already exist" do
240
247
  widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
241
248
  @field.create_appearances
@@ -262,12 +269,6 @@ describe HexaPDF::Type::AcroForm::ButtonField do
262
269
  refute_same(yes, widget.appearance_dict.normal_appearance[:Yes])
263
270
  end
264
271
 
265
- it "fails for push buttons as they are not implemented yet" do
266
- @field.flag(:push_button)
267
- @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
268
- assert_raises(HexaPDF::Error) { @field.create_appearances }
269
- end
270
-
271
272
  it "uses the configuration option acro_form.appearance_generator" do
272
273
  @doc.config['acro_form.appearance_generator'] = 'NonExistent'
273
274
  assert_raises(Exception) { @field.create_appearances }
@@ -193,9 +193,14 @@ describe HexaPDF::Type::AcroForm::Field do
193
193
 
194
194
  it "extracts an embedded widget into a standalone object if necessary" do
195
195
  widget1 = @field.create_widget(@page, Rect: [1, 2, 3, 4])
196
+ # Make sure that the field/widget looks like as if it has been loaded from a file
197
+ @doc.revisions.current.update(widget1)
198
+ assert_equal(@field, widget1)
199
+
196
200
  widget2 = @field.create_widget(@doc.pages.add, Rect: [2, 1, 4, 3])
197
201
  kids = @field[:Kids]
198
202
 
203
+ assert_kind_of(HexaPDF::Type::AcroForm::Field, @doc.object(@field.oid))
199
204
  assert_equal(2, kids.length)
200
205
  refute_same(widget1, kids[0])
201
206
  assert_same(widget2, kids[1])
@@ -558,13 +558,23 @@ describe HexaPDF::Type::AcroForm::Form do
558
558
  assert_equal(:Tx4, @acro_form[:Fields][2][:Kids][0][:T])
559
559
  assert_equal(@acro_form[:Fields][2], @acro_form[:Fields][2][:Kids][0][:Parent])
560
560
  end
561
+
562
+ it "ensures that objects loaded as widget are stored as field" do
563
+ @acro_form[:Fields][2] = @doc.add({T: :WidgetField, Type: :Annot, Subtype: :Widget})
564
+ assert_kind_of(HexaPDF::Type::Annotations::Widget, @acro_form[:Fields][2])
565
+
566
+ assert(@acro_form.validate)
567
+ field = @acro_form[:Fields][0]
568
+ assert_kind_of(HexaPDF::Type::AcroForm::Field, field)
569
+ assert_equal(:WidgetField, field.full_field_name)
570
+ end
561
571
  end
562
572
 
563
573
  describe "combining fields with the same name" do
564
574
  before do
565
575
  @acro_form[:Fields] = [
566
576
  @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 1]}),
567
- @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 2]}),
577
+ @merged_field = @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 2]}),
568
578
  @doc.add({T: 'Tx2'}),
569
579
  @doc.add({T: 'e', Kids: [{Subtype: :Widget, Rect: [0, 0, 0, 3]}]}),
570
580
  ]
@@ -576,6 +586,12 @@ describe HexaPDF::Type::AcroForm::Form do
576
586
  assert_equal([[0, 0, 0, 1], [0, 0, 0, 2], [0, 0, 0, 3]],
577
587
  @acro_form.field_by_name('e').each_widget.map {|w| w[:Rect] })
578
588
  end
589
+
590
+ it "deletes the combined and now unneeded field objects" do
591
+ assert(@acro_form.validate)
592
+ assert(@merged_field.null?)
593
+ assert(@doc.object(@merged_field.oid).null?)
594
+ end
579
595
  end
580
596
 
581
597
  describe "automatically creates the terminal fields; appearances" do
@@ -82,6 +82,20 @@ describe HexaPDF::Type::AcroForm::JavaScriptActions do
82
82
  assert_equal('1.234,57', value)
83
83
  end
84
84
 
85
+ it "works with the special Infinity and NaN values" do
86
+ @value = 'Infinity'
87
+ assert_format('2, 2, 0, 0, "", false', "Inf", "black")
88
+ @value = '-Infinity'
89
+ assert_format('2, 2, 0, 0, "", false', "-Inf", "black")
90
+ @value = 'Nan'
91
+ assert_format('2, 2, 0, 0, "", false', "NaN", "black")
92
+ end
93
+
94
+ it "works if the value is nil" do
95
+ @value = nil
96
+ assert_format('2, 2, 0, 0, "", false', "0,00", "black")
97
+ end
98
+
85
99
  it "does nothing to the value if the JavaScript method could not be determined " do
86
100
  assert_format('2, 3, 0, 0, " E", false, a', "1234567.898765", nil)
87
101
  end
@@ -244,6 +258,13 @@ describe HexaPDF::Type::AcroForm::JavaScriptActions do
244
258
  assert_calculation('SUM', [@field1, @field2], "30.54")
245
259
  end
246
260
 
261
+ it "works with the special values Infinity and NaN" do
262
+ @field1.field_value = "Infinity"
263
+ assert_calculation('SUM', [@field1, @field2], "Infinity")
264
+ @field1.field_value = "NaN"
265
+ assert_calculation('SUM', [@field1, @field2], "NaN")
266
+ end
267
+
247
268
  it "returns nil if a field cannot be resolved" do
248
269
  @action[:JS] = 'AFSimple_Calculate("SUM", ["unknown"]);'
249
270
  assert_nil(@klass.calculate(@form, @action))
@@ -130,9 +130,12 @@ describe HexaPDF::Type::AcroForm::TextField do
130
130
  assert_raises(HexaPDF::Error) { @field.field_value = 'test' }
131
131
  end
132
132
 
133
- it "fails if the value exceeds the length set by /MaxLen" do
133
+ it "calls acro_form.text_field.on_max_len_exceeded if the value exceeds the length set by /MaxLen" do
134
134
  @field[:MaxLen] = 5
135
135
  assert_raises(HexaPDF::Error) { @field.field_value = 'testdf' }
136
+ @doc.config['acro_form.text_field.on_max_len_exceeded'] = proc {|f, v| v }
137
+ @field.field_value = 'testdf'
138
+ assert_equal('testdf', @field[:V])
136
139
  end
137
140
  end
138
141
 
@@ -278,6 +281,9 @@ describe HexaPDF::Type::AcroForm::TextField do
278
281
  assert(@field.validate)
279
282
  @field[:MaxLen] = 2
280
283
  refute(@field.validate)
284
+ @doc.config['acro_form.text_field.on_max_len_exceeded'] = proc {|field, str| "Hello" }
285
+ assert(@field.validate)
286
+ assert_equal('Hello', @field[:V])
281
287
  @field[:V] = nil
282
288
  assert(@field.validate)
283
289
  end