hexapdf 0.44.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -47
  3. data/examples/019-acro_form.rb +5 -0
  4. data/examples/027-composer_optional_content.rb +6 -4
  5. data/examples/030-pdfa.rb +12 -11
  6. data/lib/hexapdf/cli/inspect.rb +5 -0
  7. data/lib/hexapdf/composer.rb +23 -1
  8. data/lib/hexapdf/configuration.rb +8 -0
  9. data/lib/hexapdf/content/canvas.rb +3 -3
  10. data/lib/hexapdf/content/canvas_composer.rb +1 -0
  11. data/lib/hexapdf/digital_signature/cms_handler.rb +31 -3
  12. data/lib/hexapdf/digital_signature/signing/default_handler.rb +9 -1
  13. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +5 -1
  14. data/lib/hexapdf/document/layout.rb +63 -30
  15. data/lib/hexapdf/document.rb +24 -2
  16. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  17. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  18. data/lib/hexapdf/importer.rb +15 -5
  19. data/lib/hexapdf/layout/box.rb +48 -36
  20. data/lib/hexapdf/layout/column_box.rb +3 -11
  21. data/lib/hexapdf/layout/container_box.rb +4 -4
  22. data/lib/hexapdf/layout/frame.rb +7 -6
  23. data/lib/hexapdf/layout/inline_box.rb +17 -23
  24. data/lib/hexapdf/layout/list_box.rb +27 -42
  25. data/lib/hexapdf/layout/page_style.rb +23 -16
  26. data/lib/hexapdf/layout/style.rb +5 -5
  27. data/lib/hexapdf/layout/table_box.rb +14 -10
  28. data/lib/hexapdf/layout/text_box.rb +60 -36
  29. data/lib/hexapdf/layout/text_fragment.rb +1 -1
  30. data/lib/hexapdf/layout/text_layouter.rb +7 -8
  31. data/lib/hexapdf/parser.rb +5 -1
  32. data/lib/hexapdf/rectangle.rb +4 -4
  33. data/lib/hexapdf/revisions.rb +1 -1
  34. data/lib/hexapdf/stream.rb +3 -3
  35. data/lib/hexapdf/tokenizer.rb +3 -2
  36. data/lib/hexapdf/type/acro_form/button_field.rb +2 -0
  37. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
  38. data/lib/hexapdf/type/acro_form/field.rb +8 -0
  39. data/lib/hexapdf/type/acro_form/form.rb +2 -1
  40. data/lib/hexapdf/type/acro_form/text_field.rb +2 -0
  41. data/lib/hexapdf/type/form.rb +2 -2
  42. data/lib/hexapdf/version.rb +1 -1
  43. data/test/hexapdf/content/test_canvas_composer.rb +13 -8
  44. data/test/hexapdf/digital_signature/common.rb +66 -84
  45. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +7 -0
  46. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +9 -0
  47. data/test/hexapdf/digital_signature/test_cms_handler.rb +41 -1
  48. data/test/hexapdf/digital_signature/test_handler.rb +2 -1
  49. data/test/hexapdf/document/test_layout.rb +44 -5
  50. data/test/hexapdf/layout/test_box.rb +23 -5
  51. data/test/hexapdf/layout/test_frame.rb +21 -2
  52. data/test/hexapdf/layout/test_inline_box.rb +17 -28
  53. data/test/hexapdf/layout/test_list_box.rb +8 -8
  54. data/test/hexapdf/layout/test_page_style.rb +7 -2
  55. data/test/hexapdf/layout/test_table_box.rb +8 -1
  56. data/test/hexapdf/layout/test_text_box.rb +51 -29
  57. data/test/hexapdf/layout/test_text_layouter.rb +0 -3
  58. data/test/hexapdf/test_composer.rb +14 -5
  59. data/test/hexapdf/test_document.rb +27 -0
  60. data/test/hexapdf/test_importer.rb +17 -0
  61. data/test/hexapdf/test_revisions.rb +54 -41
  62. data/test/hexapdf/test_serializer.rb +1 -0
  63. data/test/hexapdf/type/acro_form/test_form.rb +9 -0
  64. metadata +2 -2
@@ -76,6 +76,8 @@ module HexaPDF
76
76
  #
77
77
  # :no_export:: The field should *not* be exported by a submit-form action.
78
78
  #
79
+ # Also see the class description of the subclasses for additional, type specific field flags.
80
+ #
79
81
  # == Field Type Implementation Notes
80
82
  #
81
83
  # If an AcroForm field type adds additional inheritable dictionary fields, it has to set the
@@ -124,6 +126,8 @@ module HexaPDF
124
126
  #
125
127
  # Returns an array of flag names representing the set bit flags.
126
128
  #
129
+ # See the class description for a list of available flags.
130
+ #
127
131
 
128
132
  ##
129
133
  # :method: flagged?
@@ -133,6 +137,8 @@ module HexaPDF
133
137
  # Returns +true+ if the given flag is set. The argument can either be the flag name or the
134
138
  # bit index.
135
139
  #
140
+ # See the class description for a list of available flags.
141
+ #
136
142
 
137
143
  ##
138
144
  # :method: flag
@@ -142,6 +148,8 @@ module HexaPDF
142
148
  # Sets the given flags, given as flag names or bit indices. If +clear_existing+ is +true+,
143
149
  # all prior flags will be cleared.
144
150
  #
151
+ # See the class description for a list of available flags.
152
+ #
145
153
  bit_field(:flags, {read_only: 0, required: 1, no_export: 2},
146
154
  lister: "flags", getter: "flagged?", setter: "flag", unsetter: "unflag",
147
155
  value_getter: "self[:Ff]", value_setter: "self[:Ff]")
@@ -388,13 +388,14 @@ module HexaPDF
388
388
  page_annots = page[:Annots].to_a - to_delete
389
389
  page[:Annots].value.replace(page_annots)
390
390
  end
391
- to_delete.each {|widget| document.delete(widget) }
392
391
 
393
392
  if field[:Parent]
394
393
  field[:Parent][:Kids].delete(field)
395
394
  else
396
395
  self[:Fields].delete(field)
397
396
  end
397
+
398
+ to_delete.each {|widget| document.delete(widget) }
398
399
  document.delete(field)
399
400
  end
400
401
 
@@ -50,6 +50,8 @@ module HexaPDF
50
50
  #
51
51
  # == Type Specific Field Flags
52
52
  #
53
+ # See the class description for Field for the general field flags.
54
+ #
53
55
  # :multiline:: If set, the text field may contain multiple lines.
54
56
  #
55
57
  # :password:: The field is a password field. This changes the behaviour of the PDF reader
@@ -159,8 +159,8 @@ module HexaPDF
159
159
  # retained without the need for parsing its contents.
160
160
  #
161
161
  # If the bounding box of the form XObject doesn't have its origin at (0, 0), the canvas origin
162
- # is translated into the bottom left corner so that this detail doesn't matter when using the
163
- # canvas. This means that the canvas' origin is always at the bottom left corner of the
162
+ # is translated into the bottom-left corner so that this detail doesn't matter when using the
163
+ # canvas. This means that the canvas' origin is always at the bottom-left corner of the
164
164
  # bounding box.
165
165
  #
166
166
  # *Note* that a canvas can only be retrieved for initially empty form XObjects!
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.44.0'
40
+ VERSION = '0.46.0'
41
41
 
42
42
  end
@@ -47,21 +47,20 @@ describe HexaPDF::Content::CanvasComposer do
47
47
  end
48
48
 
49
49
  it "splits the box if possible" do
50
- @composer.draw_box(create_box(width: 400, style: {position: :float}))
51
- box = create_box(width: 400, height: 100)
52
- box.define_singleton_method(:split) do |*|
53
- [box, HexaPDF::Layout::Box.new(height: 100) {}]
54
- end
50
+ @composer.draw_box(create_box(width: 300, height: 300, style: {position: :float}))
51
+ box = create_box(style: {mask_mode: :box})
52
+ box.define_singleton_method(:fit_content) {|*| fit_result.overflow! }
53
+ box.define_singleton_method(:split_content) { [box, HexaPDF::Layout::Box.new(height: 100) {}] }
55
54
  @composer.draw_box(box)
56
55
  assert_operators(@composer.canvas.contents,
57
56
  [[:save_graphics_state],
58
- [:concatenate_matrix, [1, 0, 0, 1, 0, 0]],
57
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 541.889764]],
59
58
  [:restore_graphics_state],
60
59
  [:save_graphics_state],
61
- [:concatenate_matrix, [1, 0, 0, 1, 400, 741.889764]],
60
+ [:concatenate_matrix, [1, 0, 0, 1, 300, 0]],
62
61
  [:restore_graphics_state],
63
62
  [:save_graphics_state],
64
- [:concatenate_matrix, [1, 0, 0, 1, 400, 641.889764]],
63
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 441.889764]],
65
64
  [:restore_graphics_state]])
66
65
  end
67
66
 
@@ -77,6 +76,12 @@ describe HexaPDF::Content::CanvasComposer do
77
76
  [:restore_graphics_state]])
78
77
  end
79
78
 
79
+ it "handles truncated boxes correctly" do
80
+ box = create_box(height: 400, style: {overflow: :truncate})
81
+ box.define_singleton_method(:fit_content) {|*| fit_result.overflow! }
82
+ assert_same(box, @composer.draw_box(box))
83
+ end
84
+
80
85
  it "returns the last drawn box" do
81
86
  box = create_box(height: 400)
82
87
  assert_same(box, @composer.draw_box(box))
@@ -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
@@ -141,6 +146,40 @@ describe HexaPDF::Document::Layout do
141
146
  end
142
147
  end
143
148
 
149
+ describe "styles" do
150
+ it "returns the existing styles" do
151
+ @layout.style(:test, font_size: 20)
152
+ assert_equal([:base, :test], @layout.styles.keys)
153
+ end
154
+
155
+ it "sets multiple styles at once" do
156
+ styles = @layout.styles(
157
+ test: {font_size: 20},
158
+ test2: {font_size: 30},
159
+ )
160
+ assert_same(styles, @layout.styles)
161
+ assert_equal([:base, :test, :test2], @layout.styles.keys)
162
+ end
163
+ end
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
+
144
183
  describe "inline_box" do
145
184
  it "takes a box as argument" do
146
185
  box = HexaPDF::Layout::Box.create(width: 10, height: 10)
@@ -40,6 +40,9 @@ describe HexaPDF::Layout::Box do
40
40
  @frame = Object.new
41
41
  def @frame.x; 0; end
42
42
  def @frame.y; 100; end
43
+ def @frame.bottom; 40; end
44
+ def @frame.width; 150; end
45
+ def @frame.height; 150; end
43
46
  end
44
47
 
45
48
  def create_box(**args, &block)
@@ -130,6 +133,14 @@ describe HexaPDF::Layout::Box do
130
133
  assert_equal(100, box.height)
131
134
  end
132
135
 
136
+ it "use the frame's width and its remaining height for position=:flow boxes" do
137
+ box = create_box(style: {position: :flow})
138
+ box.define_singleton_method(:supports_position_flow?) { true }
139
+ assert(box.fit(100, 100, @frame).success?)
140
+ assert_equal(150, box.width)
141
+ assert_equal(60, box.height)
142
+ end
143
+
133
144
  it "uses float comparison" do
134
145
  box = create_box(width: 50.0000002, height: 49.9999996)
135
146
  assert(box.fit(50, 50, @frame).success?)
@@ -137,29 +148,36 @@ describe HexaPDF::Layout::Box do
137
148
  assert_equal(49.9999996, box.height)
138
149
  end
139
150
 
140
- 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
141
159
  box = create_box(width: 101)
142
160
  assert(box.fit(100, 100, @frame).failure?)
143
161
  end
144
162
 
145
- 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
146
164
  box = create_box(height: 101)
147
165
  assert(box.fit(100, 100, @frame).failure?)
148
166
  end
149
167
 
150
- 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
151
169
  box = create_box(height: 100)
152
170
  box.style.padding = [0, 100]
153
171
  assert(box.fit(150, 150, @frame).failure?)
154
172
  end
155
173
 
156
- 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
157
175
  box = create_box(width: 100)
158
176
  box.style.padding = [100, 0]
159
177
  assert(box.fit(150, 150, @frame).failure?)
160
178
  end
161
179
 
162
- 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
163
181
  box = create_box
164
182
  box.define_singleton_method(:fit_content) do |_aw, _ah, _frame|
165
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
@@ -423,6 +433,15 @@ describe HexaPDF::Layout::Frame do
423
433
  box = HexaPDF::Layout::Box.create
424
434
  refute(@frame.fit(box).success?)
425
435
  end
436
+
437
+ it "doesn't do post-fitting tasks if fitting is a failure" do
438
+ box = HexaPDF::Layout::Box.create(width: 400)
439
+ result = @frame.fit(box)
440
+ assert(result.failure?)
441
+ assert_nil(result.x)
442
+ assert_nil(result.y)
443
+ assert_nil(result.mask)
444
+ end
426
445
  end
427
446
 
428
447
  describe "split" 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