hexapdf 0.44.0 → 0.46.0

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