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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +87 -47
- data/examples/019-acro_form.rb +5 -0
- data/lib/hexapdf/cli/inspect.rb +5 -0
- data/lib/hexapdf/composer.rb +1 -1
- data/lib/hexapdf/configuration.rb +8 -0
- data/lib/hexapdf/digital_signature/cms_handler.rb +31 -3
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +9 -1
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +5 -1
- data/lib/hexapdf/document/layout.rb +48 -27
- data/lib/hexapdf/document.rb +24 -2
- data/lib/hexapdf/importer.rb +15 -5
- data/lib/hexapdf/layout/box.rb +25 -28
- data/lib/hexapdf/layout/frame.rb +1 -1
- data/lib/hexapdf/layout/inline_box.rb +17 -23
- data/lib/hexapdf/layout/list_box.rb +24 -29
- data/lib/hexapdf/layout/page_style.rb +23 -16
- data/lib/hexapdf/layout/style.rb +2 -2
- data/lib/hexapdf/layout/text_box.rb +2 -6
- data/lib/hexapdf/parser.rb +5 -1
- data/lib/hexapdf/revisions.rb +1 -1
- data/lib/hexapdf/stream.rb +3 -3
- data/lib/hexapdf/tokenizer.rb +3 -2
- data/lib/hexapdf/type/acro_form/button_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/field.rb +8 -0
- data/lib/hexapdf/type/acro_form/form.rb +2 -1
- data/lib/hexapdf/type/acro_form/text_field.rb +2 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/digital_signature/common.rb +66 -84
- data/test/hexapdf/digital_signature/signing/test_default_handler.rb +7 -0
- data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +9 -0
- data/test/hexapdf/digital_signature/test_cms_handler.rb +41 -1
- data/test/hexapdf/digital_signature/test_handler.rb +2 -1
- data/test/hexapdf/document/test_layout.rb +28 -5
- data/test/hexapdf/layout/test_box.rb +12 -5
- data/test/hexapdf/layout/test_frame.rb +12 -2
- data/test/hexapdf/layout/test_inline_box.rb +17 -28
- data/test/hexapdf/layout/test_list_box.rb +5 -5
- data/test/hexapdf/layout/test_page_style.rb +7 -2
- data/test/hexapdf/layout/test_text_box.rb +3 -9
- data/test/hexapdf/layout/test_text_layouter.rb +0 -3
- data/test/hexapdf/test_document.rb +27 -0
- data/test/hexapdf/test_importer.rb +17 -0
- data/test/hexapdf/test_revisions.rb +54 -41
- data/test/hexapdf/type/acro_form/test_form.rb +9 -0
- metadata +2 -2
@@ -12,26 +12,10 @@ module HexaPDF
|
|
12
12
|
def ca_certificate
|
13
13
|
@ca_certificate ||=
|
14
14
|
begin
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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::
|
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(
|
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 "
|
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
|
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
|
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
|
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 #
|
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
|
-
[
|
321
|
+
[10, 90],
|
321
322
|
[10, 80, 110, 110],
|
322
|
-
[[[
|
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
|
35
|
-
ibox = inline_box(HexaPDF::Document.new.layout.text("test is going good",
|
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(
|
38
|
-
assert_equal(
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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 "
|
54
|
-
|
55
|
-
|
56
|
-
|
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 =
|
64
|
-
box.fit_wrapped_box
|
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
|
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:
|
69
|
-
check_box(box,
|
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:
|
74
|
-
check_box(box, 100,
|
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,
|
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
|
-
|
48
|
-
|
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
|
-
[:
|
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, [
|
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,
|
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
|