hexapdf 0.45.0 → 0.46.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -47
  3. data/examples/019-acro_form.rb +5 -0
  4. data/lib/hexapdf/cli/inspect.rb +5 -0
  5. data/lib/hexapdf/composer.rb +1 -1
  6. data/lib/hexapdf/configuration.rb +8 -0
  7. data/lib/hexapdf/digital_signature/cms_handler.rb +31 -3
  8. data/lib/hexapdf/digital_signature/signing/default_handler.rb +9 -1
  9. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +5 -1
  10. data/lib/hexapdf/document/layout.rb +48 -27
  11. data/lib/hexapdf/document.rb +24 -2
  12. data/lib/hexapdf/importer.rb +15 -5
  13. data/lib/hexapdf/layout/box.rb +25 -28
  14. data/lib/hexapdf/layout/frame.rb +1 -1
  15. data/lib/hexapdf/layout/inline_box.rb +17 -23
  16. data/lib/hexapdf/layout/list_box.rb +24 -29
  17. data/lib/hexapdf/layout/page_style.rb +23 -16
  18. data/lib/hexapdf/layout/style.rb +2 -2
  19. data/lib/hexapdf/layout/text_box.rb +2 -6
  20. data/lib/hexapdf/parser.rb +5 -1
  21. data/lib/hexapdf/revisions.rb +1 -1
  22. data/lib/hexapdf/stream.rb +3 -3
  23. data/lib/hexapdf/tokenizer.rb +3 -2
  24. data/lib/hexapdf/type/acro_form/button_field.rb +2 -0
  25. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
  26. data/lib/hexapdf/type/acro_form/field.rb +8 -0
  27. data/lib/hexapdf/type/acro_form/form.rb +2 -1
  28. data/lib/hexapdf/type/acro_form/text_field.rb +2 -0
  29. data/lib/hexapdf/version.rb +1 -1
  30. data/test/hexapdf/digital_signature/common.rb +66 -84
  31. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +7 -0
  32. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +9 -0
  33. data/test/hexapdf/digital_signature/test_cms_handler.rb +41 -1
  34. data/test/hexapdf/digital_signature/test_handler.rb +2 -1
  35. data/test/hexapdf/document/test_layout.rb +28 -5
  36. data/test/hexapdf/layout/test_box.rb +12 -5
  37. data/test/hexapdf/layout/test_frame.rb +12 -2
  38. data/test/hexapdf/layout/test_inline_box.rb +17 -28
  39. data/test/hexapdf/layout/test_list_box.rb +5 -5
  40. data/test/hexapdf/layout/test_page_style.rb +7 -2
  41. data/test/hexapdf/layout/test_text_box.rb +3 -9
  42. data/test/hexapdf/layout/test_text_layouter.rb +0 -3
  43. data/test/hexapdf/test_document.rb +27 -0
  44. data/test/hexapdf/test_importer.rb +17 -0
  45. data/test/hexapdf/test_revisions.rb +54 -41
  46. data/test/hexapdf/type/acro_form/test_form.rb +9 -0
  47. metadata +2 -2
@@ -12,26 +12,10 @@ module HexaPDF
12
12
  def ca_certificate
13
13
  @ca_certificate ||=
14
14
  begin
15
- ca_name = OpenSSL::X509::Name.parse('/C=AT/O=HexaPDF/CN=HexaPDF Test Root CA')
16
-
17
- ca_cert = OpenSSL::X509::Certificate.new
18
- ca_cert.serial = 0
19
- ca_cert.version = 2
20
- ca_cert.not_before = Time.now - 86400
21
- ca_cert.not_after = Time.now + 86400
22
- ca_cert.public_key = ca_key.public_key
23
- ca_cert.subject = ca_name
24
- ca_cert.issuer = ca_name
25
-
26
- extension_factory = OpenSSL::X509::ExtensionFactory.new
27
- extension_factory.subject_certificate = ca_cert
28
- extension_factory.issuer_certificate = ca_cert
29
- ca_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
30
- ca_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:TRUE', true))
31
- ca_cert.add_extension(extension_factory.create_extension('keyUsage', 'cRLSign,keyCertSign', true))
32
- ca_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
33
-
34
- ca_cert
15
+ cert = create_cert(name: '/C=AT/O=HexaPDF/CN=HexaPDF Test Root CA', serial: 0,
16
+ public_key: ca_key.public_key)
17
+ add_extensions(cert, cert, ca_key, is_ca: true, key_usage: 'cRLSign,keyCertSign')
18
+ cert
35
19
  end
36
20
  end
37
21
 
@@ -39,88 +23,86 @@ module HexaPDF
39
23
  @signer_key ||= OpenSSL::PKey::RSA.new(2048)
40
24
  end
41
25
 
42
- def dsa_signer_key
43
- @dsa_signer_key ||= OpenSSL::PKey::DSA.new(2048)
44
- end
45
-
46
26
  def signer_certificate
47
27
  @signer_certificate ||=
48
28
  begin
49
- name = OpenSSL::X509::Name.parse('/CN=RSA signer/DC=gettalong')
50
-
51
- signer_cert = OpenSSL::X509::Certificate.new
52
- signer_cert.serial = 2
53
- signer_cert.version = 2
54
- signer_cert.not_before = Time.now - 86400
55
- signer_cert.not_after = Time.now + 86400
56
- signer_cert.public_key = signer_key.public_key
57
- signer_cert.subject = name
58
- signer_cert.issuer = ca_certificate.subject
59
-
60
- extension_factory = OpenSSL::X509::ExtensionFactory.new
61
- extension_factory.subject_certificate = signer_cert
62
- extension_factory.issuer_certificate = ca_certificate
63
- signer_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
64
- signer_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE'))
65
- signer_cert.add_extension(extension_factory.create_extension('keyUsage', 'digitalSignature'))
66
- signer_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
67
-
68
- signer_cert
29
+ cert = create_cert(name: '/CN=RSA signer/DC=gettalong', serial: 2,
30
+ public_key: signer_key.public_key, issuer: ca_certificate)
31
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature')
32
+ cert
33
+ end
34
+ end
35
+
36
+ def non_repudiation_signer_certificate
37
+ @non_repudiation_signer_certificate ||=
38
+ begin
39
+ cert = create_cert(name: '/CN=Non repudiation signer/DC=gettalong', serial: 2,
40
+ public_key: signer_key.public_key, issuer: ca_certificate)
41
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'nonRepudiation')
42
+ cert
69
43
  end
70
44
  end
71
45
 
46
+ def dsa_signer_key
47
+ @dsa_signer_key ||= OpenSSL::PKey::DSA.new(2048)
48
+ end
49
+
72
50
  def dsa_signer_certificate
73
51
  @dsa_signer_certificate ||=
74
52
  begin
75
- signer_cert = OpenSSL::X509::Certificate.new
76
- signer_cert.serial = 3
77
- signer_cert.version = 2
78
- signer_cert.not_before = Time.now - 86400
79
- signer_cert.not_after = Time.now + 86400
80
- signer_cert.public_key = dsa_signer_key.public_key
81
- signer_cert.subject = OpenSSL::X509::Name.parse('/CN=DSA signer/DC=gettalong')
82
- signer_cert.issuer = ca_certificate.subject
83
-
84
- extension_factory = OpenSSL::X509::ExtensionFactory.new
85
- extension_factory.subject_certificate = signer_cert
86
- extension_factory.issuer_certificate = ca_certificate
87
- signer_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
88
- signer_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE'))
89
- signer_cert.add_extension(extension_factory.create_extension('keyUsage', 'digitalSignature'))
90
- signer_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
91
-
92
- signer_cert
53
+ cert = create_cert(name: '/CN=DSA signer/DC=gettalong', serial: 3,
54
+ public_key: dsa_signer_key.public_key, issuer: ca_certificate)
55
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature')
56
+ cert
93
57
  end
94
58
  end
95
59
 
96
60
  def timestamp_certificate
97
61
  @timestamp_certificate ||=
98
62
  begin
99
- name = OpenSSL::X509::Name.parse('/CN=timestamp/DC=gettalong')
100
-
101
- signer_cert = OpenSSL::X509::Certificate.new
102
- signer_cert.serial = 3
103
- signer_cert.version = 2
104
- signer_cert.not_before = Time.now - 86400
105
- signer_cert.not_after = Time.now + 86400
106
- signer_cert.public_key = signer_key.public_key
107
- signer_cert.subject = name
108
- signer_cert.issuer = ca_certificate.subject
109
-
110
- extension_factory = OpenSSL::X509::ExtensionFactory.new
111
- extension_factory.subject_certificate = signer_cert
112
- extension_factory.issuer_certificate = ca_certificate
113
- signer_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
114
- signer_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE'))
115
- signer_cert.add_extension(extension_factory.create_extension('keyUsage', 'digitalSignature'))
116
- signer_cert.add_extension(extension_factory.create_extension('extendedKeyUsage',
117
- 'timeStamping', true))
118
- signer_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
119
-
120
- signer_cert
63
+ cert = create_cert(name: '/CN=timestamp/DC=gettalong', serial: 3,
64
+ public_key: signer_key.public_key, issuer: ca_certificate)
65
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
66
+ extended_key_usage: 'timeStamping')
67
+ cert
121
68
  end
122
69
  end
123
70
 
71
+ def create_cert(name:, serial:, public_key:, issuer: nil)
72
+ name = OpenSSL::X509::Name.parse(name)
73
+ cert = OpenSSL::X509::Certificate.new
74
+ cert.serial = serial
75
+ cert.version = 2
76
+ cert.not_before = Time.now - 86400
77
+ cert.not_after = Time.now + 86400
78
+ cert.public_key = public_key
79
+ cert.subject = name
80
+ cert.issuer = (issuer ? issuer.subject : name)
81
+ cert
82
+ end
83
+
84
+ def add_extensions(subject_cert, issuer_cert, signing_key, is_ca: false, key_usage: nil,
85
+ extended_key_usage: nil)
86
+ extension_factory = OpenSSL::X509::ExtensionFactory.new
87
+ extension_factory.subject_certificate = subject_cert
88
+ extension_factory.issuer_certificate = issuer_cert
89
+ subject_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
90
+ if is_ca
91
+ subject_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:TRUE', true))
92
+ else
93
+ subject_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE'))
94
+ end
95
+ if key_usage
96
+ subject_cert.add_extension(extension_factory.create_extension('keyUsage', key_usage, true))
97
+ end
98
+ if extended_key_usage
99
+ subject_cert.add_extension(extension_factory.create_extension('extendedKeyUsage',
100
+ extended_key_usage, true))
101
+ end
102
+ subject_cert.sign(signing_key, OpenSSL::Digest.new('SHA1'))
103
+ end
104
+ private :add_extensions
105
+
124
106
  def start_tsa_server
125
107
  return if defined?(@tsa_server)
126
108
  require 'webrick'
@@ -133,6 +133,13 @@ describe HexaPDF::DigitalSignature::Signing::DefaultHandler do
133
133
  assert_equal(['Reason', 'Location', 'Contact'], @obj.value.values_at(:Reason, :Location, :ContactInfo))
134
134
  end
135
135
 
136
+ it "sets the signing time" do
137
+ time = Time.now
138
+ @handler.signing_time = time
139
+ @handler.finalize_objects(@field, @obj)
140
+ assert_equal(time, @obj[:M])
141
+ end
142
+
136
143
  it "fills the build properties dictionary with appropriate application information" do
137
144
  @handler.finalize_objects(@field, @obj)
138
145
  assert_equal(:HexaPDF, @obj[:Prop_Build][:App][:Name])
@@ -214,6 +214,15 @@ describe HexaPDF::DigitalSignature::Signing::SignedDataCreator do
214
214
  assert_equal(Time.now.utc, attr.value[1].value[0].value)
215
215
  end
216
216
  end
217
+
218
+ it "can use a user-defined time as signing time" do
219
+ current_time = Time.now
220
+ @signed_data.signing_time = current_time
221
+ asn1 = OpenSSL::ASN1.decode(@signed_data.create("data"))
222
+ attr = asn1.value[1].value[0].value[4].value[0].value[3].value.
223
+ find {|obj| obj.value[0].value == 'signingTime' }
224
+ assert_equal(current_time.floor.utc, attr.value[1].value[0].value)
225
+ end
217
226
  end
218
227
 
219
228
  describe "pades signature" do
@@ -22,7 +22,7 @@ describe HexaPDF::DigitalSignature::CMSHandler do
22
22
  assert_equal("RSA signer", @handler.signer_name)
23
23
  end
24
24
 
25
- it "returns the signing time" do
25
+ it "returns the signing time from the signed attributes" do
26
26
  assert_equal(@pkcs7.signers.first.signed_time, @handler.signing_time)
27
27
  end
28
28
 
@@ -86,6 +86,18 @@ describe HexaPDF::DigitalSignature::CMSHandler do
86
86
  assert_match(/key usage is missing 'Digital Signature'/, result.messages.first.content)
87
87
  end
88
88
 
89
+ it "provides info for a non-repudiation signature" do
90
+ @pkcs7 = OpenSSL::PKCS7.sign(CERTIFICATES.non_repudiation_signer_certificate,
91
+ CERTIFICATES.signer_key,
92
+ @data, [CERTIFICATES.ca_certificate],
93
+ OpenSSL::PKCS7::DETACHED)
94
+ @dict.contents = @pkcs7.to_der
95
+ @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
96
+ result = @handler.verify(@store)
97
+ assert_equal(:info, result.messages.first.type)
98
+ assert_match(/Certificate used for non-repudiation/, result.messages.first.content)
99
+ end
100
+
89
101
  it "verifies the signature itself" do
90
102
  result = @handler.verify(@store)
91
103
  assert_equal(:info, result.messages.last.type)
@@ -117,4 +129,32 @@ describe HexaPDF::DigitalSignature::CMSHandler do
117
129
  assert_match(/Signature valid/, result.messages.last.content)
118
130
  end
119
131
  end
132
+
133
+ describe "with embedded TSA signature" do
134
+ before do
135
+ CERTIFICATES.start_tsa_server
136
+ tsh = HexaPDF::DigitalSignature::Signing::TimestampHandler.new(
137
+ signature_size: 10_000, tsa_url: 'http://127.0.0.1:34567'
138
+ )
139
+ cms = HexaPDF::DigitalSignature::Signing::SignedDataCreator.create(
140
+ @data, type: :pades, certificate: CERTIFICATES.signer_certificate,
141
+ key: CERTIFICATES.signer_key, timestamp_handler: tsh,
142
+ certificates: [CERTIFICATES.ca_certificate]
143
+ )
144
+ @dict.contents = cms.to_der
145
+ @dict.signed_data = @data
146
+ @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
147
+ end
148
+
149
+ it "returns the signing time from the TSA signature" do
150
+ assert_equal(@handler.embedded_tsa_signature.signers.first.signed_time, @handler.signing_time)
151
+ end
152
+
153
+ it "provides informational output if the time is from a TSA signature" do
154
+ store = OpenSSL::X509::Store.new
155
+ result = @handler.verify(store)
156
+ assert_equal(:info, result.messages.first.type)
157
+ assert_match(/Signing time.*timestamp authority/, result.messages.first.content)
158
+ end
159
+ end
120
160
  end
@@ -5,6 +5,7 @@ require 'hexapdf/digital_signature'
5
5
  require 'hexapdf/document'
6
6
  require 'time'
7
7
  require 'ostruct'
8
+ require 'openssl'
8
9
 
9
10
  describe HexaPDF::DigitalSignature::Handler do
10
11
  before do
@@ -36,7 +37,7 @@ describe HexaPDF::DigitalSignature::Handler do
36
37
  end
37
38
 
38
39
  it "can allow self-signed certificates" do
39
- [OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN,
40
+ [OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT,
40
41
  OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN].each do |error|
41
42
  [true, false].each do |allow_self_signed|
42
43
  @result.messages.clear
@@ -35,11 +35,11 @@ describe HexaPDF::Document::Layout::ChildrenCollector do
35
35
  end
36
36
 
37
37
  it "allows appending boxes created by the Layout class" do
38
- @collector.lorem_ipsum
39
- @collector.lorem_ipsum_box
40
- @collector.column
41
- @collector.column_box
42
- assert_equal(4, @collector.children.size)
38
+ box1 = @collector.lorem_ipsum
39
+ box2 = @collector.lorem_ipsum_box
40
+ box3 = @collector.column
41
+ box4 = @collector.column_box
42
+ assert_equal([box1, box2, box3, box4], @collector.children)
43
43
  assert_kind_of(HexaPDF::Layout::TextBox, @collector.children[0])
44
44
  assert_kind_of(HexaPDF::Layout::TextBox, @collector.children[1])
45
45
  assert_kind_of(HexaPDF::Layout::ColumnBox, @collector.children[2])
@@ -95,6 +95,11 @@ describe HexaPDF::Document::Layout::CellArgumentCollector do
95
95
  @args[-3..-1, -5..-2] = {key: :value}
96
96
  check_argument_info(@args.argument_infos.first, 17..19, 5..8, {key: :value})
97
97
  end
98
+
99
+ it "allows using stepped ranges" do
100
+ @args[(0..-1).step(2)] = {key: :value}
101
+ check_argument_info(@args.argument_infos.first, (0..19).step(2), 0..9, {key: :value})
102
+ end
98
103
  end
99
104
 
100
105
  describe "retrieve_arguments_for" do
@@ -157,6 +162,24 @@ describe HexaPDF::Document::Layout do
157
162
  end
158
163
  end
159
164
 
165
+ describe "private retrieve_style" do
166
+ it "resolves a font name to a font wrapper" do
167
+ style = @layout.send(:retrieve_style, {font: 'Helvetica'})
168
+ assert_kind_of(HexaPDF::Font::Type1Wrapper, style.font)
169
+ end
170
+
171
+ it "sets the :base style's font if no font is set" do
172
+ @layout.style(:base, font: 'Helvetica')
173
+ style = @layout.send(:retrieve_style, {})
174
+ assert_equal('Helvetica', style.font.wrapped_font.font_name)
175
+ end
176
+
177
+ it "sets the font specified in the config option font.default as fallback" do
178
+ style = @layout.send(:retrieve_style, {})
179
+ assert_equal('Times-Roman', style.font.wrapped_font.font_name)
180
+ end
181
+ end
182
+
160
183
  describe "inline_box" do
161
184
  it "takes a box as argument" do
162
185
  box = HexaPDF::Layout::Box.create(width: 10, height: 10)
@@ -148,29 +148,36 @@ describe HexaPDF::Layout::Box do
148
148
  assert_equal(49.9999996, box.height)
149
149
  end
150
150
 
151
- it "fails if the box doesn't fit, position != :flow and its width is greater than the available width" do
151
+ it "works for boxes with no space for the content" do
152
+ box = create_box(height: 1, style: {border: {width: [1, 0, 0]}})
153
+ assert(box.fit(100, 100, @frame).success?)
154
+ assert_equal(1, box.height)
155
+ assert_equal(100, box.width)
156
+ end
157
+
158
+ it "fails if position != :flow and its width is greater than the available width" do
152
159
  box = create_box(width: 101)
153
160
  assert(box.fit(100, 100, @frame).failure?)
154
161
  end
155
162
 
156
- it "fails if the box doesn't fit, position != :flow and its width is greater than the available width" do
163
+ it "fails if position != :flow and its width is greater than the available width" do
157
164
  box = create_box(height: 101)
158
165
  assert(box.fit(100, 100, @frame).failure?)
159
166
  end
160
167
 
161
- it "fails if its content width is zero" do
168
+ it "fails if position != :flow and the reserved width is greater than the width" do
162
169
  box = create_box(height: 100)
163
170
  box.style.padding = [0, 100]
164
171
  assert(box.fit(150, 150, @frame).failure?)
165
172
  end
166
173
 
167
- it "fails if its content height is zero" do
174
+ it "fails if position != :flow and the reserved height is greater than the height" do
168
175
  box = create_box(width: 100)
169
176
  box.style.padding = [100, 0]
170
177
  assert(box.fit(150, 150, @frame).failure?)
171
178
  end
172
179
 
173
- it "can use the #content_width/#content_height helper methods" do
180
+ it "can use the #update_content_width/#update_content_height helper methods" do
174
181
  box = create_box
175
182
  box.define_singleton_method(:fit_content) do |_aw, _ah, _frame|
176
183
  update_content_width { 10 }
@@ -316,10 +316,20 @@ describe HexaPDF::Layout::Frame do
316
316
 
317
317
  describe "flowing boxes" do
318
318
  it "flows inside the frame's outline" do
319
+ remove_area(:left)
319
320
  check_box({width: 10, height: 20, margin: 10, position: :flow},
320
- [0, 90],
321
+ [10, 90],
321
322
  [10, 80, 110, 110],
322
- [[[10, 10], [110, 10], [110, 80], [10, 80]]])
323
+ [[[20, 10], [110, 10], [110, 80], [20, 80]]])
324
+ assert_equal(10, @box.fit_result.x)
325
+ end
326
+
327
+ it "doesn't overwrite fit_result.x" do
328
+ box = HexaPDF::Layout::Box.create(position: :flow) {}
329
+ box.define_singleton_method(:supports_position_flow?) { true }
330
+ box.define_singleton_method(:fit_content) {|*args| fit_result.x = 30; super(*args) }
331
+ fit_result = @frame.fit(box)
332
+ assert_equal(30, fit_result.x)
323
333
  end
324
334
 
325
335
  it "uses position=default if the box indicates it doesn't support flowing contents" do
@@ -23,47 +23,36 @@ describe HexaPDF::Layout::InlineBox do
23
23
  ibox = inline_box(box, valign: :top)
24
24
  assert_equal(:top, ibox.valign)
25
25
  end
26
-
27
- it "fails if the wrapped box has not width set" do
28
- box = HexaPDF::Document.new.layout.text("test is not going good")
29
- assert_raises(HexaPDF::Error) { inline_box(box) }
30
- end
31
26
  end
32
27
 
33
28
  describe "fit_wrapped_box" do
34
- it "automatically fits the provided box into the given frame" do
35
- ibox = inline_box(HexaPDF::Document.new.layout.text("test is going good", width: 20))
29
+ it "automatically fits the provided box when given a frame" do
30
+ ibox = inline_box(HexaPDF::Document.new.layout.text("test is going good", margin: [5, 10]))
36
31
  ibox.fit_wrapped_box(HexaPDF::Layout::Frame.new(0, 0, 50, 50))
37
- assert_equal(20, ibox.width)
38
- assert_equal(45, ibox.height)
39
- end
40
-
41
- it "automatically fits the provided box into a custom frame" do
42
- ibox = inline_box(HexaPDF::Document.new.layout.text("test is going good", width: 20))
43
- ibox.fit_wrapped_box(nil)
44
- assert_equal(20, ibox.width)
45
- assert_equal(45, ibox.height)
46
- end
47
-
48
- it "fails if the wrapped box could not be fit" do
49
- box = HexaPDF::Document.new.layout.text("test is not going good", width: 1)
50
- assert_raises(HexaPDF::Error) { inline_box(box).fit_wrapped_box(nil) }
32
+ assert_equal(90.84, ibox.width)
33
+ assert_equal(19, ibox.height)
34
+ fit_result = ibox.instance_variable_get(:@fit_result)
35
+ assert_equal(10, fit_result.x)
36
+ assert_equal(5, fit_result.y)
37
+ assert_equal(70.84 + 2 * 10, fit_result.mask.width)
38
+ assert_equal(9 + 2 * 5, fit_result.mask.height)
51
39
  end
52
40
 
53
- it "fails if the height is not set explicitly and during fitting" do
54
- assert_raises(HexaPDF::Error) do
55
- inline_box(HexaPDF::Layout::Box.create(width: 10)).fit_wrapped_box(nil)
56
- end
41
+ it "automatically fits the provided box without a given frame" do
42
+ ibox = inline_box(HexaPDF::Document.new.layout.text("test is going good"))
43
+ ibox.fit_wrapped_box
44
+ assert_equal(70.84, ibox.width)
45
+ assert_equal(9, ibox.height)
57
46
  end
58
47
  end
59
48
 
60
49
  it "draws the wrapped box at the correct position" do
61
50
  doc = HexaPDF::Document.new
62
51
  canvas = doc.pages.add.canvas
63
- box = inline_box(doc.layout.text("", width: 20, margin: [15, 10]))
64
- box.fit_wrapped_box(nil)
52
+ box = HexaPDF::Layout::InlineBox.create(width: 10, margin: [15, 10]) {}
53
+ box.fit_wrapped_box
65
54
  box.draw(canvas, 100, 200)
66
- assert_equal("q\n1 0 0 1 110 -99785 cm\nQ\n", canvas.contents)
55
+ assert_equal("q\n1 0 0 1 110 215 cm\nQ\n", canvas.contents)
67
56
  end
68
57
 
69
58
  it "returns true if the inline box is empty with no drawing operations" do
@@ -65,13 +65,13 @@ describe HexaPDF::Layout::ListBox do
65
65
  describe "fit" do
66
66
  [:default, :flow].each do |position|
67
67
  it "respects the set initial width, position #{position}" do
68
- box = create_box(children: @text_boxes[0, 2], width: 50, style: {position: position})
69
- check_box(box, 50, 80)
68
+ box = create_box(children: @text_boxes[0, 2], width: 55, style: {position: position})
69
+ check_box(box, 55, 80)
70
70
  end
71
71
 
72
72
  it "respects the set initial height, position #{position}" do
73
- box = create_box(children: @text_boxes[0, 2], height: 50, style: {position: position})
74
- check_box(box, 100, 40)
73
+ box = create_box(children: @text_boxes[0, 2], height: 55, style: {position: position})
74
+ check_box(box, 100, 55)
75
75
  end
76
76
 
77
77
  it "respects the set initial height even when it doesn't fit completely" do
@@ -123,7 +123,7 @@ describe HexaPDF::Layout::ListBox do
123
123
 
124
124
  it "fails if not even a part of the first list item fits" do
125
125
  box = create_box(children: @text_boxes[0, 2], height: 5)
126
- check_box(box, 100, 0, status: :failure)
126
+ check_box(box, 100, 5, status: :failure)
127
127
  end
128
128
 
129
129
  it "fails for unknown marker types" do
@@ -44,8 +44,13 @@ describe HexaPDF::Layout::PageStyle do
44
44
 
45
45
  it "works when no template is set" do
46
46
  style = HexaPDF::Layout::PageStyle.new
47
- page = style.create_page(@doc)
48
- assert_equal("", page.contents)
47
+ page1 = style.create_page(@doc)
48
+ frame1 = style.frame
49
+ assert_equal("", page1.contents)
50
+ assert_equal(523.275591, style.frame.width)
51
+
52
+ page2 = style.create_page(@doc)
53
+ refute_same(frame1, style.frame)
49
54
  end
50
55
 
51
56
  it "creates a default frame if none is set beforehand or during template execution" do
@@ -178,15 +178,12 @@ describe HexaPDF::Layout::TextBox do
178
178
  assert_operators(@canvas.contents, [[:save_graphics_state],
179
179
  [:restore_graphics_state],
180
180
  [:save_graphics_state],
181
- [:concatenate_matrix, [1, 0, 0, 1, 5, 10]],
182
- [:save_graphics_state],
183
- [:append_rectangle, [0, 0, 10, 10]],
181
+ [:append_rectangle, [5, 10, 10, 10]],
184
182
  [:clip_path_non_zero],
185
183
  [:end_path],
186
- [:append_rectangle, [0.5, 0.5, 9.0, 9.0]],
184
+ [:append_rectangle, [5.5, 10.5, 9.0, 9.0]],
187
185
  [:stroke_path],
188
186
  [:restore_graphics_state],
189
- [:restore_graphics_state],
190
187
  [:save_graphics_state],
191
188
  [:restore_graphics_state]])
192
189
  end
@@ -195,7 +192,7 @@ describe HexaPDF::Layout::TextBox do
195
192
  @frame.remove_area(Geom2D::Rectangle(0, 0, 40, 100))
196
193
  box = create_box([@inline_box], style: {position: :flow, border: {width: 1}})
197
194
  box.fit(60, 100, @frame)
198
- box.draw(@canvas, 0, 88)
195
+ box.draw(@canvas, 40, 88)
199
196
  assert_operators(@canvas.contents, [[:save_graphics_state],
200
197
  [:append_rectangle, [40, 88, 12, 12]],
201
198
  [:clip_path_non_zero],
@@ -207,9 +204,6 @@ describe HexaPDF::Layout::TextBox do
207
204
  [:restore_graphics_state],
208
205
  [:save_graphics_state],
209
206
  [:concatenate_matrix, [1, 0, 0, 1, 41, 89]],
210
- [:save_graphics_state],
211
- [:concatenate_matrix, [1, 0, 0, 1, 0, 0]],
212
- [:restore_graphics_state],
213
207
  [:restore_graphics_state],
214
208
  [:save_graphics_state],
215
209
  [:restore_graphics_state]])
@@ -812,11 +812,8 @@ describe HexaPDF::Layout::TextLayouter do
812
812
  [:restore_graphics_state],
813
813
  [:save_graphics_state],
814
814
  [:concatenate_matrix, [1, 0, 0, 1, 10, -40]],
815
- [:save_graphics_state],
816
- [:concatenate_matrix, [1, 0, 0, 1, 0, 0]],
817
815
  [:set_line_width, [2]],
818
816
  [:restore_graphics_state],
819
- [:restore_graphics_state],
820
817
  [:save_graphics_state],
821
818
  [:restore_graphics_state]])
822
819
  end
@@ -568,4 +568,31 @@ describe HexaPDF::Document do
568
568
  it "can be inspected and the output is not too large" do
569
569
  assert_match(/HexaPDF::Document:\d+/, @doc.inspect)
570
570
  end
571
+
572
+ describe "duplicate" do
573
+ it "creates an in-memory copy" do
574
+ doc = HexaPDF::Document.new
575
+ doc.pages.add.canvas.line_width(10)
576
+ doc.trailer.info[:Author] = 'HexaPDF'
577
+ doc.dispatch_message(:complete_objects)
578
+
579
+ dupped = doc.duplicate
580
+ assert_equal('HexaPDF', dupped.trailer.info[:Author])
581
+ doc.pages[0].canvas.line_cap_style(:round)
582
+ assert_equal("10 w\n", dupped.pages[0].contents)
583
+ end
584
+
585
+ it "doesn't copy the encryption state" do
586
+ doc = HexaPDF::Document.new
587
+ doc.pages.add.canvas.line_width(10)
588
+ doc.encrypt
589
+ io = StringIO.new
590
+ doc.write(io)
591
+
592
+ doc = HexaPDF::Document.new(io: io)
593
+ dupped = doc.duplicate
594
+ assert_equal("10 w\n", dupped.pages[0].contents)
595
+ refute(dupped.encrypted?)
596
+ end
597
+ end
571
598
  end
@@ -47,6 +47,13 @@ describe HexaPDF::Importer do
47
47
  refute_same(obj1, obj2)
48
48
  refute_same(obj1[:ref], obj2[:ref])
49
49
  end
50
+
51
+ it "duplicates the whole document" do
52
+ trailer = HexaPDF::Importer.copy(@dest, @source.trailer, allow_all: true)
53
+ refute_same(@source.catalog, trailer[:Root])
54
+ refute_same(@source.pages.root, trailer[:Root][:Pages])
55
+ assert_equal(90, trailer[:Root][:Pages][:Kids][0][:Rotate])
56
+ end
50
57
  end
51
58
 
52
59
  describe "import" do
@@ -121,6 +128,16 @@ describe HexaPDF::Importer do
121
128
  refute_same(dst_obj.data.stream, src_obj.data.stream)
122
129
  end
123
130
 
131
+ it "duplicates the stream if it is a FiberDoubleForString, e.g. when using Canvas" do
132
+ src_page = @source.pages[0]
133
+ src_page.canvas.line_width(10)
134
+ dst_page = @importer.import(src_page)
135
+ refute_same(dst_page, src_page)
136
+ refute_same(dst_page[:Contents].data.stream, src_page[:Contents].data.stream)
137
+ src_page.canvas.line_width(20)
138
+ assert_equal("10 w\n", dst_page.contents)
139
+ end
140
+
124
141
  it "does not import objects of type Catalog or Pages" do
125
142
  @obj[:catalog] = @source.catalog
126
143
  @obj[:pages] = @source.catalog.pages