hexapdf 1.1.0 → 1.2.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/lib/hexapdf/cli/command.rb +63 -63
  4. data/lib/hexapdf/cli/inspect.rb +1 -1
  5. data/lib/hexapdf/cli/modify.rb +0 -1
  6. data/lib/hexapdf/cli/optimize.rb +5 -5
  7. data/lib/hexapdf/configuration.rb +21 -0
  8. data/lib/hexapdf/content/graphics_state.rb +1 -1
  9. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +1 -1
  10. data/lib/hexapdf/document/annotations.rb +115 -0
  11. data/lib/hexapdf/document.rb +30 -7
  12. data/lib/hexapdf/font/true_type_wrapper.rb +1 -0
  13. data/lib/hexapdf/font/type1_wrapper.rb +1 -0
  14. data/lib/hexapdf/type/acro_form/java_script_actions.rb +9 -2
  15. data/lib/hexapdf/type/acro_form/text_field.rb +9 -2
  16. data/lib/hexapdf/type/annotation.rb +59 -1
  17. data/lib/hexapdf/type/annotations/appearance_generator.rb +273 -0
  18. data/lib/hexapdf/type/annotations/border_styling.rb +160 -0
  19. data/lib/hexapdf/type/annotations/line.rb +521 -0
  20. data/lib/hexapdf/type/annotations/widget.rb +2 -96
  21. data/lib/hexapdf/type/annotations.rb +3 -0
  22. data/lib/hexapdf/type/form.rb +2 -2
  23. data/lib/hexapdf/version.rb +1 -1
  24. data/lib/hexapdf/writer.rb +0 -1
  25. data/lib/hexapdf/xref_section.rb +7 -4
  26. data/test/hexapdf/content/test_graphics_state.rb +2 -3
  27. data/test/hexapdf/content/test_operator.rb +4 -5
  28. data/test/hexapdf/digital_signature/test_cms_handler.rb +7 -8
  29. data/test/hexapdf/digital_signature/test_handler.rb +2 -3
  30. data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +1 -2
  31. data/test/hexapdf/document/test_annotations.rb +33 -0
  32. data/test/hexapdf/font/test_true_type_wrapper.rb +7 -0
  33. data/test/hexapdf/font/test_type1_wrapper.rb +7 -0
  34. data/test/hexapdf/task/test_optimize.rb +1 -1
  35. data/test/hexapdf/test_document.rb +11 -3
  36. data/test/hexapdf/test_stream.rb +1 -2
  37. data/test/hexapdf/test_xref_section.rb +1 -1
  38. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +21 -0
  39. data/test/hexapdf/type/acro_form/test_text_field.rb +7 -1
  40. data/test/hexapdf/type/annotations/test_appearance_generator.rb +398 -0
  41. data/test/hexapdf/type/annotations/test_border_styling.rb +114 -0
  42. data/test/hexapdf/type/annotations/test_line.rb +189 -0
  43. data/test/hexapdf/type/annotations/test_widget.rb +0 -81
  44. data/test/hexapdf/type/test_annotation.rb +55 -0
  45. data/test/hexapdf/type/test_form.rb +6 -0
  46. metadata +10 -2
@@ -113,9 +113,10 @@ module HexaPDF
113
113
 
114
114
  # Marks this XRefSection object as being the first cross-reference section in a PDF file.
115
115
  #
116
- # This has the consequence that only a single sub-section is created.
116
+ # This has the consequence that only a single sub-section starting a zero is created.
117
117
  def mark_as_initial_section!
118
118
  @initial_section = true
119
+ add_free_entry(0, 65535)
119
120
  end
120
121
 
121
122
  # Adds an in-use entry to the cross-reference section.
@@ -161,9 +162,10 @@ module HexaPDF
161
162
  return to_enum(__method__) unless block_given?
162
163
 
163
164
  temp = []
164
- oids.sort.each do |oid|
165
- expected_next_oid = !temp.empty? && temp[-1].oid + 1
166
- if expected_next_oid && expected_next_oid != oid
165
+ sorted_oids = oids.sort
166
+ expected_next_oid = sorted_oids[0]
167
+ sorted_oids.each do |oid|
168
+ if expected_next_oid != oid
167
169
  if @initial_section
168
170
  expected_next_oid.upto(oid - 1) do |free_oid|
169
171
  temp << self.class.free_entry(free_oid, 0)
@@ -174,6 +176,7 @@ module HexaPDF
174
176
  end
175
177
  end
176
178
  temp << self[oid]
179
+ expected_next_oid = oid + 1
177
180
  end
178
181
  yield(temp)
179
182
  self
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'test_helper'
4
4
  require 'hexapdf/content/graphics_state'
5
- require 'ostruct'
6
5
 
7
6
  # Dummy class used as wrapper so that constant lookup works correctly
8
7
  class GraphicsStateWrapper < Minitest::Spec
@@ -149,8 +148,8 @@ class GraphicsStateWrapper < Minitest::Spec
149
148
  end
150
149
 
151
150
  it "uses the correct glyph to text space scaling" do
152
- font = OpenStruct.new
153
- font.glyph_scaling_factor = 0.002
151
+ font = Object.new
152
+ font.define_singleton_method(:glyph_scaling_factor) { 0.002 }
154
153
  @gs.font = font
155
154
  @gs.font_size = 10
156
155
  assert_equal(0.02, @gs.scaled_font_size)
@@ -4,7 +4,6 @@ require 'test_helper'
4
4
  require 'hexapdf/content/operator'
5
5
  require 'hexapdf/content/processor'
6
6
  require 'hexapdf/serializer'
7
- require 'ostruct'
8
7
 
9
8
  describe HexaPDF::Content::Operator::BaseOperator do
10
9
  before do
@@ -191,8 +190,8 @@ end
191
190
 
192
191
  describe_operator :SetGraphicsStateParameters, :gs do
193
192
  it "applies parameters from an ExtGState dictionary" do
194
- font = OpenStruct.new
195
- font.glyph_scaling_factor = 0.01
193
+ font = Object.new
194
+ font.define_singleton_method(:glyph_scaling_factor) { 0.01 }
196
195
  @processor.resources[:ExtGState] = {Name: {LW: 10, LC: 2, LJ: 2, ML: 2, D: [[3, 5], 2],
197
196
  RI: 2, SA: true, BM: :Multiply, CA: 0.5, ca: 0.5,
198
197
  AIS: true, TK: false, Font: [font, 10],
@@ -453,8 +452,8 @@ describe_operator :SetFontAndSize, :Tf do
453
452
  self[:Font] && self[:Font][name]
454
453
  end
455
454
 
456
- font = OpenStruct.new
457
- font.glyph_scaling_factor = 0.01
455
+ font = Object.new
456
+ font.define_singleton_method(:glyph_scaling_factor) { 0.01 }
458
457
  @processor.resources[:Font] = {F1: font}
459
458
  invoke(:F1, 10)
460
459
  assert_equal(@processor.resources.font(:F1), @processor.graphics_state.font)
@@ -4,17 +4,16 @@ require 'digest'
4
4
  require 'test_helper'
5
5
  require_relative 'common'
6
6
  require 'hexapdf/digital_signature'
7
- require 'ostruct'
8
7
 
9
8
  describe HexaPDF::DigitalSignature::CMSHandler do
10
9
  before do
11
- @data = 'Some data'
12
- @dict = OpenStruct.new
13
- @pkcs7 = OpenSSL::PKCS7.sign(CERTIFICATES.signer_certificate, CERTIFICATES.signer_key,
14
- @data, [CERTIFICATES.ca_certificate],
15
- OpenSSL::PKCS7::DETACHED)
16
- @dict.contents = @pkcs7.to_der
17
- @dict.signed_data = @data
10
+ @data = data = 'Some data'
11
+ @dict = Struct.new(:contents, :signed_data, :signature_type, :Reference, :M).new
12
+ @pkcs7 = pkcs7 = OpenSSL::PKCS7.sign(CERTIFICATES.signer_certificate, CERTIFICATES.signer_key,
13
+ @data, [CERTIFICATES.ca_certificate],
14
+ OpenSSL::PKCS7::DETACHED)
15
+ @dict.contents = pkcs7.to_der
16
+ @dict.signed_data = data
18
17
  @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
19
18
  end
20
19
 
@@ -4,7 +4,6 @@ require 'test_helper'
4
4
  require 'hexapdf/digital_signature'
5
5
  require 'hexapdf/document'
6
6
  require 'time'
7
- require 'ostruct'
8
7
  require 'openssl'
9
8
 
10
9
  describe HexaPDF::DigitalSignature::Handler do
@@ -33,7 +32,7 @@ describe HexaPDF::DigitalSignature::Handler do
33
32
 
34
33
  describe "store_verification_callback" do
35
34
  before do
36
- @context = OpenStruct.new
35
+ @context = Struct.new(:error).new
37
36
  end
38
37
 
39
38
  it "can allow self-signed certificates" do
@@ -60,7 +59,7 @@ describe HexaPDF::DigitalSignature::Handler do
60
59
  ].each do |success, not_before, not_after|
61
60
  @result.messages.clear
62
61
  @handler.define_singleton_method(:signer_certificate) do
63
- OpenStruct.new.tap do |struct|
62
+ Struct.new(:not_before, :not_after).new.tap do |struct|
64
63
  struct.not_before = Time.parse("2021-11-14 #{not_before}")
65
64
  struct.not_after = Time.parse("2021-11-14 #{not_after}")
66
65
  end
@@ -3,12 +3,11 @@
3
3
  require 'test_helper'
4
4
  require_relative 'common'
5
5
  require 'hexapdf/digital_signature'
6
- require 'ostruct'
7
6
 
8
7
  describe HexaPDF::DigitalSignature::PKCS1Handler do
9
8
  before do
10
9
  @data = 'Some data'
11
- @dict = OpenStruct.new
10
+ @dict = Struct.new(:signed_data, :contents, :Cert, :Reference, :M).new
12
11
  @dict.signed_data = @data
13
12
  encoded_data = CERTIFICATES.signer_key.sign(OpenSSL::Digest.new('SHA1'), @data)
14
13
  @dict.contents = OpenSSL::ASN1::OctetString.new(encoded_data).to_der
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+
6
+ describe HexaPDF::Document::Annotations do
7
+ before do
8
+ @doc = HexaPDF::Document.new
9
+ @page = @doc.pages.add
10
+ @annots = @doc.annotations
11
+ end
12
+
13
+ describe "create" do
14
+ it "fails if the type argument doesn't refer to an implemented method" do
15
+ assert_raises(ArgumentError) { @annots.create(:unknown, @page) }
16
+ end
17
+
18
+ it "delegates to the actual create_TYPE implementation" do
19
+ annot = @annots.create(:line, @page, start_point: [0, 0], end_point: [10, 10])
20
+ assert_equal(:Line, annot[:Subtype])
21
+ end
22
+ end
23
+
24
+ describe "create_line" do
25
+ it "creates an appropriate line annotation object" do
26
+ annot = @annots.create(:line, @page, start_point: [0, 5], end_point: [10, 15])
27
+ assert_equal(:Annot, annot[:Type])
28
+ assert_equal(:Line, annot[:Subtype])
29
+ assert_equal([0, 5, 10, 15], annot.line)
30
+ assert_equal(annot, @page[:Annots].first)
31
+ end
32
+ end
33
+ end
@@ -209,6 +209,13 @@ describe HexaPDF::Font::TrueTypeWrapper do
209
209
  dict[:Encoding].stream)
210
210
  assert_equal([glyph.id, [glyph.width]], dict[:DescendantFonts][0][:W].value)
211
211
  end
212
+
213
+ it "handles the case where the font is added but then not used and deleted" do
214
+ @doc.task(:optimize, compact: true)
215
+ assert(@font_wrapper.pdf_object.null?)
216
+ @doc.dispatch_message(:complete_objects)
217
+ assert(@font_wrapper.pdf_object.null?)
218
+ end
212
219
  end
213
220
 
214
221
  describe "font file embedding" do
@@ -140,5 +140,12 @@ describe HexaPDF::Font::Type1Wrapper do
140
140
  it "makes sure that the PDF dictionaries are indirect" do
141
141
  assert(@times_wrapper.pdf_object.indirect?)
142
142
  end
143
+
144
+ it "handles the case where the font is added but then not used and deleted" do
145
+ @doc.task(:optimize, compact: true)
146
+ assert(@times_wrapper.pdf_object.null?)
147
+ @doc.dispatch_message(:complete_objects)
148
+ assert(@times_wrapper.pdf_object.null?)
149
+ end
143
150
  end
144
151
  end
@@ -99,7 +99,7 @@ describe HexaPDF::Task::Optimize do
99
99
  objstm = @doc.add({}, type: HexaPDF::Type::ObjectStream)
100
100
  @doc.add({}, type: HexaPDF::Type::XRefStream)
101
101
  objstm.add_object(@doc.add({Type: :Test}))
102
- @doc.write(io)
102
+ @doc.write(io, compact: false)
103
103
  io.rewind
104
104
  @doc = HexaPDF::Document.new(io: io)
105
105
  end
@@ -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
@@ -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],
@@ -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