hexapdf 0.45.0 → 0.46.0

Sign up to get free protection for your applications and to get access to all the features.
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