hexapdf 0.46.0 → 1.0.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -16
  3. data/lib/hexapdf/composer.rb +7 -0
  4. data/lib/hexapdf/configuration.rb +13 -0
  5. data/lib/hexapdf/content/parser.rb +3 -1
  6. data/lib/hexapdf/digital_signature/cms_handler.rb +13 -0
  7. data/lib/hexapdf/digital_signature/signature.rb +1 -1
  8. data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -0
  9. data/lib/hexapdf/document.rb +14 -3
  10. data/lib/hexapdf/encryption/standard_security_handler.rb +32 -26
  11. data/lib/hexapdf/font/cmap/writer.rb +58 -4
  12. data/lib/hexapdf/font/cmap.rb +7 -0
  13. data/lib/hexapdf/font/true_type_wrapper.rb +41 -16
  14. data/lib/hexapdf/importer.rb +1 -1
  15. data/lib/hexapdf/layout/table_box.rb +57 -10
  16. data/lib/hexapdf/layout/text_fragment.rb +2 -1
  17. data/lib/hexapdf/object.rb +1 -1
  18. data/lib/hexapdf/parser.rb +1 -1
  19. data/lib/hexapdf/reference.rb +1 -1
  20. data/lib/hexapdf/task/merge_acro_form.rb +164 -0
  21. data/lib/hexapdf/task/optimize.rb +4 -4
  22. data/lib/hexapdf/task.rb +1 -0
  23. data/lib/hexapdf/tokenizer.rb +2 -0
  24. data/lib/hexapdf/type/acro_form/appearance_generator.rb +8 -4
  25. data/lib/hexapdf/type/acro_form/form.rb +14 -24
  26. data/lib/hexapdf/type/acro_form/signature_field.rb +18 -7
  27. data/lib/hexapdf/type/acro_form/variable_text_field.rb +12 -4
  28. data/lib/hexapdf/type/actions/go_to.rb +1 -0
  29. data/lib/hexapdf/type/actions/go_to_r.rb +1 -0
  30. data/lib/hexapdf/type/actions/launch.rb +5 -1
  31. data/lib/hexapdf/type/annotation.rb +6 -1
  32. data/lib/hexapdf/type/annotations/markup_annotation.rb +14 -1
  33. data/lib/hexapdf/type/annotations/widget.rb +4 -2
  34. data/lib/hexapdf/type/catalog.rb +3 -0
  35. data/lib/hexapdf/type/cid_font.rb +4 -1
  36. data/lib/hexapdf/type/file_specification.rb +17 -14
  37. data/lib/hexapdf/type/font_descriptor.rb +4 -3
  38. data/lib/hexapdf/type/font_simple.rb +3 -1
  39. data/lib/hexapdf/type/font_true_type.rb +2 -0
  40. data/lib/hexapdf/type/font_type0.rb +1 -1
  41. data/lib/hexapdf/type/font_type1.rb +7 -0
  42. data/lib/hexapdf/type/font_type3.rb +0 -1
  43. data/lib/hexapdf/type/form.rb +5 -2
  44. data/lib/hexapdf/type/graphics_state_parameter.rb +7 -4
  45. data/lib/hexapdf/type/image.rb +8 -4
  46. data/lib/hexapdf/type/info.rb +2 -2
  47. data/lib/hexapdf/type/mark_information.rb +2 -2
  48. data/lib/hexapdf/type/optional_content_configuration.rb +1 -1
  49. data/lib/hexapdf/type/optional_content_membership.rb +1 -1
  50. data/lib/hexapdf/type/page.rb +5 -3
  51. data/lib/hexapdf/type/resources.rb +6 -6
  52. data/lib/hexapdf/type/viewer_preferences.rb +4 -3
  53. data/lib/hexapdf/version.rb +1 -1
  54. data/lib/hexapdf/writer.rb +1 -0
  55. data/test/data/standard-security-handler/bothpwd-aes-256bit-V5-R5.pdf +43 -0
  56. data/test/data/standard-security-handler/nopwd-aes-256bit-V5-R5.pdf +44 -0
  57. data/test/data/standard-security-handler/ownerpwd-aes-256bit-V5-R5.pdf +43 -0
  58. data/test/data/standard-security-handler/userpwd-aes-256bit-V5-R5.pdf +0 -0
  59. data/test/hexapdf/common_tokenizer_tests.rb +5 -0
  60. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +6 -0
  61. data/test/hexapdf/digital_signature/test_cms_handler.rb +12 -7
  62. data/test/hexapdf/digital_signature/test_signature.rb +7 -0
  63. data/test/hexapdf/digital_signature/test_signatures.rb +12 -7
  64. data/test/hexapdf/encryption/test_standard_security_handler.rb +5 -2
  65. data/test/hexapdf/font/cmap/test_writer.rb +73 -16
  66. data/test/hexapdf/font/test_true_type_wrapper.rb +17 -3
  67. data/test/hexapdf/layout/test_list_box.rb +7 -7
  68. data/test/hexapdf/layout/test_table_box.rb +52 -0
  69. data/test/hexapdf/layout/test_text_fragment.rb +3 -3
  70. data/test/hexapdf/layout/test_text_layouter.rb +4 -2
  71. data/test/hexapdf/task/test_merge_acro_form.rb +104 -0
  72. data/test/hexapdf/task/test_optimize.rb +2 -0
  73. data/test/hexapdf/test_composer.rb +8 -0
  74. data/test/hexapdf/test_document.rb +12 -3
  75. data/test/hexapdf/test_importer.rb +7 -0
  76. data/test/hexapdf/test_parser.rb +7 -0
  77. data/test/hexapdf/test_writer.rb +19 -5
  78. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +40 -23
  79. data/test/hexapdf/type/acro_form/test_form.rb +7 -8
  80. data/test/hexapdf/type/acro_form/test_signature_field.rb +3 -1
  81. data/test/hexapdf/type/acro_form/test_variable_text_field.rb +14 -1
  82. data/test/hexapdf/type/actions/test_launch.rb +6 -2
  83. data/test/hexapdf/type/annotations/test_widget.rb +4 -0
  84. data/test/hexapdf/type/test_font_type1.rb +5 -0
  85. data/test/hexapdf/type/test_form.rb +1 -1
  86. data/test/hexapdf/type/test_page.rb +7 -1
  87. metadata +8 -2
@@ -0,0 +1,43 @@
1
+ %PDF-1.7
2
+ %����
3
+ 1 0 obj
4
+ << /Extensions << /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> >> /Pages 3 0 R /Type /Catalog >>
5
+ endobj
6
+ 2 0 obj
7
+ << /ModDate <aa1d1637459ea17c7fc9709f0c0c7b64761ff74e905b3765f33425776aa3010163cd5e898cfa7c5fc16159359375c323> >>
8
+ endobj
9
+ 3 0 obj
10
+ << /Count 1 /Kids [ 4 0 R ] /MediaBox [ 0 0 612 446 ] /Type /Pages >>
11
+ endobj
12
+ 4 0 obj
13
+ << /Contents 5 0 R /Parent 3 0 R /Resources 6 0 R /Type /Page >>
14
+ endobj
15
+ 5 0 obj
16
+ << /Length 80 /Filter /FlateDecode >>
17
+ stream
18
+ J<~S�)��9�M�$S�z��g�j���r�.R�"PO���_���X���^�GP�&z�g�*$�2SΈ�z�Kl��5ĩendstream
19
+ endobj
20
+ 6 0 obj
21
+ << /Font << /F1 7 0 R >> /ProcSet [ /PDF /Text ] >>
22
+ endobj
23
+ 7 0 obj
24
+ << /BaseFont /Helvetica /Name /F1 /Subtype /Type1 /Type /Font >>
25
+ endobj
26
+ 8 0 obj
27
+ << /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >> /Filter /Standard /Length 256 /O <c60934c0925e8d3ceebd0609102d9407dcf22d7fb3d87d030fce633af6d2ed99c1c3382bee5e8afc0d55ff8bca441d48> /OE <3fa2e3ecf34ddcb38b44af372a43268dda32111f58dc79da74d960b8fa206ead> /P -4 /Perms <b6c6ab65529f0a3322f03909e8a5547a> /R 5 /StmF /StdCF /StrF /StdCF /U <18fbd94777c28531c495a3116d273de9f8f3ed338b31c07687ca0ba06812842843765a66484d098194bcef0f9ecaaace> /UE <496a81bbb3207dfd12ddca4c16b60a411c6a18e638205824295e09afde826b4a> /V 5 >>
28
+ endobj
29
+ xref
30
+ 0 9
31
+ 0000000000 65535 f
32
+ 0000000015 00000 n
33
+ 0000000130 00000 n
34
+ 0000000259 00000 n
35
+ 0000000344 00000 n
36
+ 0000000424 00000 n
37
+ 0000000574 00000 n
38
+ 0000000641 00000 n
39
+ 0000000721 00000 n
40
+ trailer << /Info 2 0 R /Root 1 0 R /Size 9 /ID [<6790ffa610024e78369114311fc0df96><ed6c0810cb0d19599ac62042a0487749>] /Encrypt 8 0 R >>
41
+ startxref
42
+ 1268
43
+ %%EOF
@@ -0,0 +1,44 @@
1
+ %PDF-1.7
2
+ %����
3
+ 1 0 obj
4
+ << /Extensions << /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> >> /Pages 3 0 R /Type /Catalog >>
5
+ endobj
6
+ 2 0 obj
7
+ << /ModDate <0a09febf75d913e5eff6197f52f28b31321a43f49e0735a25a93dd77b1a2701e73ae05be0803747ca030ee4917544cbb> >>
8
+ endobj
9
+ 3 0 obj
10
+ << /Count 1 /Kids [ 4 0 R ] /MediaBox [ 0 0 612 446 ] /Type /Pages >>
11
+ endobj
12
+ 4 0 obj
13
+ << /Contents 5 0 R /Parent 3 0 R /Resources 6 0 R /Type /Page >>
14
+ endobj
15
+ 5 0 obj
16
+ << /Length 80 /Filter /FlateDecode >>
17
+ stream
18
+ \��sy%1����a��
19
+ �/���F�.�L:pb���C�` ��D�5 �� �کU"�ʇ�3�Fy/�9�<V��ڦ;�?5�S�8endstream
20
+ endobj
21
+ 6 0 obj
22
+ << /Font << /F1 7 0 R >> /ProcSet [ /PDF /Text ] >>
23
+ endobj
24
+ 7 0 obj
25
+ << /BaseFont /Helvetica /Name /F1 /Subtype /Type1 /Type /Font >>
26
+ endobj
27
+ 8 0 obj
28
+ << /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >> /Filter /Standard /Length 256 /O <8034f99b1eff9e91054d7ee490155e22f65170f607d6a614236b089e602517d9a22abe7c1ea53f52dbc9ae8d9701a065> /OE <bdc5c4b46519af7b4041b7341f70e4d8b1c6087dc12d3f16f060313f18e73386> /P -4 /Perms <64028d6bceb0d927477ecf45e6be7f3f> /R 5 /StmF /StdCF /StrF /StdCF /U <5db7b7b5bd8bbe9fac28477756a930cb079e1dcbdcd3b50c1817d3de4ce5184b5189848832df0043f8e1fe19ff696a36> /UE <dc3fed7b473148238ba0689409edd3d80c91bd26802003c152594aa27a50a81e> /V 5 >>
29
+ endobj
30
+ xref
31
+ 0 9
32
+ 0000000000 65535 f
33
+ 0000000015 00000 n
34
+ 0000000130 00000 n
35
+ 0000000259 00000 n
36
+ 0000000344 00000 n
37
+ 0000000424 00000 n
38
+ 0000000574 00000 n
39
+ 0000000641 00000 n
40
+ 0000000721 00000 n
41
+ trailer << /Info 2 0 R /Root 1 0 R /Size 9 /ID [<6790ffa610024e78369114311fc0df96><da8e0d03302398724f68ef24a831285e>] /Encrypt 8 0 R >>
42
+ startxref
43
+ 1268
44
+ %%EOF
@@ -0,0 +1,43 @@
1
+ %PDF-1.7
2
+ %����
3
+ 1 0 obj
4
+ << /Extensions << /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> >> /Pages 3 0 R /Type /Catalog >>
5
+ endobj
6
+ 2 0 obj
7
+ << /ModDate <d2aa32b8d87666bd29df499a424d2e6d23c0b475ebb67598733b5f06429470a37062d3d6a32fc9603301c5eb99a20605> >>
8
+ endobj
9
+ 3 0 obj
10
+ << /Count 1 /Kids [ 4 0 R ] /MediaBox [ 0 0 612 446 ] /Type /Pages >>
11
+ endobj
12
+ 4 0 obj
13
+ << /Contents 5 0 R /Parent 3 0 R /Resources 6 0 R /Type /Page >>
14
+ endobj
15
+ 5 0 obj
16
+ << /Length 80 /Filter /FlateDecode >>
17
+ stream
18
+ j��D3�(���"�ڛ�������xvY$B\4^Y����$��
19
+ endobj
20
+ 6 0 obj
21
+ << /Font << /F1 7 0 R >> /ProcSet [ /PDF /Text ] >>
22
+ endobj
23
+ 7 0 obj
24
+ << /BaseFont /Helvetica /Name /F1 /Subtype /Type1 /Type /Font >>
25
+ endobj
26
+ 8 0 obj
27
+ << /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >> /Filter /Standard /Length 256 /O <95d20f6277a6955bf4bda243c2144e94889bd5fa4225a4cf4e0d496fa1ffa1d991e1a37d46e4afe9e2dfc207eba9ec53> /OE <432a7d086b4338f1020bbc5847e6dd4f49ce586ab2c5de0f5301450e45e3bb3e> /P -4 /Perms <7df865105074d365f2ce50407e8b6dc8> /R 5 /StmF /StdCF /StrF /StdCF /U <930a631e8b2b95ff6b024e9bde92cb73c5c43f0106ec4fb1c336e49608c0740d87395c6ea79b99ee07eeae5ffbacd031> /UE <3f7eb6b9a897049bfca85a5ae71470eca3dfedbe9101e8532b217e4d95bcf51a> /V 5 >>
28
+ endobj
29
+ xref
30
+ 0 9
31
+ 0000000000 65535 f
32
+ 0000000015 00000 n
33
+ 0000000130 00000 n
34
+ 0000000259 00000 n
35
+ 0000000344 00000 n
36
+ 0000000424 00000 n
37
+ 0000000574 00000 n
38
+ 0000000641 00000 n
39
+ 0000000721 00000 n
40
+ trailer << /Info 2 0 R /Root 1 0 R /Size 9 /ID [<6790ffa610024e78369114311fc0df96><dd2e6bd65a9735c0bef37d88d5291ff1>] /Encrypt 8 0 R >>
41
+ startxref
42
+ 1268
43
+ %%EOF
@@ -104,6 +104,11 @@ module CommonTokenizerTests
104
104
  assert_raises(HexaPDF::MalformedPDFError) { @tokenizer.next_token }
105
105
  end
106
106
 
107
+ it "next_token: fails on a closing parenthesis that is not part of a literal string" do
108
+ create_tokenizer(" )")
109
+ assert_raises(HexaPDF::MalformedPDFError) { @tokenizer.next_token }
110
+ end
111
+
107
112
  it "next_token: fails on a missing greater than sign in a hex string" do
108
113
  create_tokenizer("<ABCD")
109
114
  assert_raises(HexaPDF::MalformedPDFError) { @tokenizer.next_token }
@@ -157,6 +157,12 @@ describe HexaPDF::DigitalSignature::Signing::DefaultHandler do
157
157
  assert_same(@obj, @doc.catalog[:Perms][:DocMDP])
158
158
  end
159
159
 
160
+ it "updates the document version if :pades signing is used" do
161
+ @handler.signature_type = :pades
162
+ @handler.finalize_objects(@field, @obj)
163
+ assert_equal('2.0', @doc.version)
164
+ end
165
+
160
166
  it "fails if DocMDP should be set but there is already a signature" do
161
167
  @handler.doc_mdp_permissions = :no_changes
162
168
  2.times do
@@ -62,7 +62,7 @@ describe HexaPDF::DigitalSignature::CMSHandler do
62
62
  @dict.contents = @pkcs7.to_der
63
63
  @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
64
64
  result = @handler.verify(@store)
65
- assert_equal(2, result.messages.size)
65
+ assert_equal(3, result.messages.size)
66
66
  assert_equal(:error, result.messages.first.type)
67
67
  assert_match(/Exactly one signer needed/, result.messages.first.content)
68
68
  end
@@ -100,13 +100,13 @@ describe HexaPDF::DigitalSignature::CMSHandler do
100
100
 
101
101
  it "verifies the signature itself" do
102
102
  result = @handler.verify(@store)
103
- assert_equal(:info, result.messages.last.type)
104
- assert_match(/Signature valid/, result.messages.last.content)
103
+ assert_equal(:info, result.messages[-2].type)
104
+ assert_match(/Signature valid/, result.messages[-2].content)
105
105
 
106
106
  @dict.signed_data = 'other data'
107
107
  result = @handler.verify(@store)
108
- assert_equal(:error, result.messages.last.type)
109
- assert_match(/Signature verification failed/, result.messages.last.content)
108
+ assert_equal(:error, result.messages[-2].type)
109
+ assert_match(/Signature verification failed/, result.messages[-2].content)
110
110
  end
111
111
 
112
112
  it "verifies a timestamp signature" do
@@ -125,8 +125,13 @@ describe HexaPDF::DigitalSignature::CMSHandler do
125
125
  @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
126
126
 
127
127
  result = @handler.verify(@store)
128
- assert_equal(:info, result.messages.last.type)
129
- assert_match(/Signature valid/, result.messages.last.content)
128
+ assert_equal(:info, result.messages[-2].type)
129
+ assert_match(/Signature valid/, result.messages[-2].content)
130
+ end
131
+
132
+ it "provides information on the certificate chain" do
133
+ result = @handler.verify(@store)
134
+ assert_match(/RSA signer -> HexaPDF Test Root CA/, result.messages.last.content)
130
135
  end
131
136
  end
132
137
 
@@ -111,6 +111,13 @@ describe HexaPDF::DigitalSignature::Signature do
111
111
  assert_equal((MINIMAL_PDF[0, 400] << MINIMAL_PDF[500, 333]).b, @sig.signed_data)
112
112
  end
113
113
 
114
+ it "works for invalid offsets" do
115
+ doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
116
+ @sig.document = doc
117
+ @sig[:ByteRange] = [0, 400, 9000, 333]
118
+ assert_equal(MINIMAL_PDF[0, 400], @sig.signed_data)
119
+ end
120
+
114
121
  it "fails if the document isn't associated with an existing PDF file" do
115
122
  assert_raises(HexaPDF::Error) { @sig.signed_data }
116
123
  end
@@ -16,13 +16,13 @@ describe HexaPDF::DigitalSignature::Signatures do
16
16
 
17
17
  it "iterates over all signature dictionaries" do
18
18
  assert_equal([], @doc.signatures.to_a)
19
- @sig1.field_value = :sig1
20
- @sig2.field_value = :sig2
21
- assert_equal([:sig1, :sig2], @doc.signatures.to_a)
19
+ @sig1.field_value = {k: :sig1}
20
+ @sig2.field_value = {k: :sig2}
21
+ assert_equal([{k: :sig1}, {k: :sig2}], @doc.signatures.to_a)
22
22
  end
23
23
 
24
24
  it "returns the number of signature dictionaries" do
25
- @sig1.field_value = :sig1
25
+ @sig1.field_value = {k: :sig1}
26
26
  assert_equal(1, @doc.signatures.count)
27
27
  end
28
28
 
@@ -70,7 +70,8 @@ describe HexaPDF::DigitalSignature::Signatures do
70
70
  end
71
71
  @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
72
72
  sig = @doc.signatures.first
73
- assert_equal([0, 925, 925 + sig[:Contents].size * 2 + 2, 2501], sig[:ByteRange].value)
73
+ assert_equal([0, 925, 925 + sig[:Contents].size * 2 + 2, 2455 + HexaPDF::VERSION.length],
74
+ sig[:ByteRange].value)
74
75
  assert_equal(:sig, sig[:key])
75
76
  assert_equal(:sig_field, @doc.acro_form.each_field.first[:key])
76
77
  assert(sig.key?(:Contents))
@@ -132,14 +133,18 @@ describe HexaPDF::DigitalSignature::Signatures do
132
133
  it "handles different xref section types correctly when determing the offsets" do
133
134
  @doc.delete(7)
134
135
  sig = @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
135
- assert_equal([0, 1036, 1036 + sig[:Contents].size * 2 + 2, 2483], sig[:ByteRange].value)
136
+ l1 = 1030 + HexaPDF::VERSION.length
137
+ assert_equal([0, l1, l1 + sig[:Contents].size * 2 + 2, 2437 + HexaPDF::VERSION.length],
138
+ sig[:ByteRange].value)
136
139
  end
137
140
 
138
141
  it "works if the signature object is the last object of the xref section" do
139
142
  field = @doc.acro_form(create: true).create_signature_field('Signature2')
140
143
  field.create_widget(@doc.pages[0], Rect: [0, 0, 0, 0])
141
144
  sig = @doc.signatures.add(@io, @handler, signature: field, write_options: {update_fields: false})
142
- assert_equal([0, 3143, 3143 + sig[:Contents].size * 2 + 2, 380], sig[:ByteRange].value)
145
+ l1 = 3097 + HexaPDF::VERSION.length
146
+ assert_equal([0, l1, l1 + sig[:Contents].size * 2 + 2, 374 + HexaPDF::VERSION.length],
147
+ sig[:ByteRange].value)
143
148
  end
144
149
 
145
150
  it "allows writing to a file in addition to writing to an IO" do
@@ -228,11 +228,14 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
228
228
  end
229
229
 
230
230
  it "fails if the /R value is incorrect" do
231
+ HexaPDF::Encryption::StandardEncryptionDictionary.field(:R).allowed_values << 7
231
232
  exp = assert_raises(HexaPDF::UnsupportedEncryptionError) do
232
- @handler.set_up_decryption({Filter: :Standard, V: 2, R: 5, O: 't' * 32, U: 't' * 32, P: 0,
233
+ @handler.set_up_decryption({Filter: :Standard, V: 2, R: 7, O: 't' * 32, U: 't' * 32, P: 0,
233
234
  Length: 128})
234
235
  end
235
- assert_match(/Invalid \/R value 5/i, exp.message)
236
+ assert_match(/Invalid \/R value 7/i, exp.message)
237
+ ensure
238
+ HexaPDF::Encryption::StandardEncryptionDictionary.field(:R).allowed_values.pop
236
239
  end
237
240
 
238
241
  it "fails if the supplied password is invalid" do
@@ -5,7 +5,7 @@ require 'hexapdf/font/cmap/writer'
5
5
 
6
6
  describe HexaPDF::Font::CMap::Writer do
7
7
  before do
8
- @cmap_data = <<~EOF
8
+ @to_unicode_cmap_data = <<~EOF
9
9
  /CIDInit /ProcSet findresource begin
10
10
  12 dict begin
11
11
  begincmap
@@ -32,35 +32,92 @@ describe HexaPDF::Font::CMap::Writer do
32
32
  end
33
33
  end
34
34
  EOF
35
- @mapping = []
36
- 0x00.upto(0x5e) {|i| @mapping << [i, 0x20 + i] }
37
- @mapping << [0x60, 0x90]
38
- 0x1379.upto(0x137B) {|i| @mapping << [i, 0x90FE + i - 0x1379] }
39
- @mapping << [0x3A51, 0x2003E]
35
+ @cid_cmap_data = <<~EOF
36
+ %!PS-Adobe-3.0 Resource-CMap
37
+ %%DocumentNeededResources: ProcSet (CIDInit)
38
+ %%IncludeResource: ProcSet (CIDInit)
39
+ %%BeginResource: CMap (Custom)
40
+ %%Title: (Custom Adobe Identity 0)
41
+ %%Version: 1
42
+ /CIDInit /ProcSet findresource begin
43
+ 12 dict begin
44
+ begincmap
45
+ /CIDSystemInfo 3 dict dup begin
46
+ /Registry (Adobe) def
47
+ /Ordering (Identity) def
48
+ /Supplement 0 def
49
+ end def
50
+ /CMapName /Custom def
51
+ /CMapType 1 def
52
+ /CMapVersion 1 def
53
+ /WMode 0 def
54
+ 1 begincodespacerange
55
+ <0000> <FFFF>
56
+ endcodespacerange
57
+ 1 begincidchar
58
+ <0060> 144
59
+ endcidchar
60
+ 1 begincidrange
61
+ <0000><005E> 32
62
+ endcidrange
63
+ endcmap
64
+ CMapName currentdict /CMap defineresource pop
65
+ end
66
+ end
67
+ %%EndResource
68
+ %%EOF
69
+ EOF
70
+
71
+ @to_unicode_mapping = []
72
+ @cid_mapping = []
73
+ 0x00.upto(0x5e) do |i|
74
+ @to_unicode_mapping << [i, 0x20 + i]
75
+ @cid_mapping << [i, 0x20 + i]
76
+ end
77
+ @to_unicode_mapping << [0x60, 0x90]
78
+ @cid_mapping << [0x60, 0x90]
79
+ 0x1379.upto(0x137B) do |i|
80
+ @to_unicode_mapping << [i, 0x90FE + i - 0x1379]
81
+ end
82
+ @to_unicode_mapping << [0x3A51, 0x2003E]
40
83
  end
41
84
 
42
85
  describe "create_to_unicode_cmap" do
43
86
  it "creates a correct CMap file" do
44
- assert_equal(@cmap_data, HexaPDF::Font::CMap.create_to_unicode_cmap(@mapping))
87
+ assert_equal(@to_unicode_cmap_data,
88
+ HexaPDF::Font::CMap.create_to_unicode_cmap(@to_unicode_mapping))
45
89
  end
46
90
 
47
91
  it "works if the last item is a range" do
48
- @mapping.pop
49
- @cmap_data.sub!(/2 beginbfchar/, '1 beginbfchar')
50
- @cmap_data.sub!(/<3A51><d840dc3e>\n/, '')
51
- assert_equal(@cmap_data, HexaPDF::Font::CMap.create_to_unicode_cmap(@mapping))
92
+ @to_unicode_mapping.pop
93
+ @to_unicode_cmap_data.sub!(/2 beginbfchar/, '1 beginbfchar')
94
+ @to_unicode_cmap_data.sub!(/<3A51><d840dc3e>\n/, '')
95
+ assert_equal(@to_unicode_cmap_data,
96
+ HexaPDF::Font::CMap.create_to_unicode_cmap(@to_unicode_mapping))
52
97
  end
53
98
 
54
99
  it "works with only ranges" do
55
- @mapping.delete_at(-1)
56
- @mapping.delete_at(0x5f)
57
- @cmap_data.sub!(/\n2 beginbfchar.*endbfchar/m, '')
58
- assert_equal(@cmap_data, HexaPDF::Font::CMap.create_to_unicode_cmap(@mapping))
100
+ @to_unicode_mapping.delete_at(-1)
101
+ @to_unicode_mapping.delete_at(0x5f)
102
+ @to_unicode_cmap_data.sub!(/\n2 beginbfchar.*endbfchar/m, '')
103
+ assert_equal(@to_unicode_cmap_data,
104
+ HexaPDF::Font::CMap.create_to_unicode_cmap(@to_unicode_mapping))
59
105
  end
60
106
 
61
107
  it "returns an empty CMap if the mapping is empty" do
62
- assert_equal(@cmap_data.sub(/\d+ beginbfchar.*endbfrange/m, ''),
108
+ assert_equal(@to_unicode_cmap_data.sub(/\d+ beginbfchar.*endbfrange/m, ''),
63
109
  HexaPDF::Font::CMap.create_to_unicode_cmap([]))
64
110
  end
65
111
  end
112
+
113
+ describe "create_cid_cmap" do
114
+ it "creates a correct CMap file" do
115
+ assert_equal(@cid_cmap_data, HexaPDF::Font::CMap.create_cid_cmap(@cid_mapping))
116
+ end
117
+
118
+ it "returns an empty CMap if the mapping is empty" do
119
+ assert_equal(@cid_cmap_data.sub(/\d+ begincidchar.*endcidrange/m, ''),
120
+ HexaPDF::Font::CMap.create_cid_cmap([]))
121
+ end
122
+ end
66
123
  end
@@ -71,6 +71,12 @@ describe HexaPDF::Font::TrueTypeWrapper do
71
71
  glyph.inspect)
72
72
  end
73
73
 
74
+ it "caches glyphs based on the id and string" do
75
+ glyph = @font_wrapper.glyph(17)
76
+ assert_same(glyph, @font_wrapper.glyph(17))
77
+ refute_same(glyph, @font_wrapper.glyph(17, "1"))
78
+ end
79
+
74
80
  it "invokes font.on_missing_glyph for missing glyphs" do
75
81
  glyph = @font_wrapper.glyph(9999)
76
82
  assert_kind_of(HexaPDF::Font::InvalidGlyph, glyph)
@@ -99,14 +105,18 @@ describe HexaPDF::Font::TrueTypeWrapper do
99
105
  assert_equal([1].pack('n'), code)
100
106
  code = @font_wrapper.encode(@font_wrapper.glyph(10))
101
107
  assert_equal([2].pack('n'), code)
108
+ code = @font_wrapper.encode(@font_wrapper.glyph(10, "o"))
109
+ assert_equal([3].pack('n'), code)
102
110
  end
103
111
 
104
112
  it "returns the encoded glyph ID for fonts that are not subset" do
105
113
  @font_wrapper = HexaPDF::Font::TrueTypeWrapper.new(@doc, @font, subset: false)
106
114
  code = @font_wrapper.encode(@font_wrapper.glyph(3))
107
- assert_equal([3].pack('n'), code)
115
+ assert_equal([1].pack('n'), code)
108
116
  code = @font_wrapper.encode(@font_wrapper.glyph(10))
109
- assert_equal([10].pack('n'), code)
117
+ assert_equal([2].pack('n'), code)
118
+ code = @font_wrapper.encode(@font_wrapper.glyph(10, "o"))
119
+ assert_equal([3].pack('n'), code)
110
120
  end
111
121
 
112
122
  it "raises an error if an InvalidGlyph is encoded" do
@@ -180,14 +190,18 @@ describe HexaPDF::Font::TrueTypeWrapper do
180
190
  it "with fonts that are not subset (only differences to other case)" do
181
191
  @font_wrapper = HexaPDF::Font::TrueTypeWrapper.new(@doc, @font, subset: false)
182
192
  @font_wrapper.encode(@font_wrapper.glyph(3))
193
+ @font_wrapper.encode(@font_wrapper.glyph(3, "-"))
183
194
  glyph = @font_wrapper.decode_utf8('H').first
184
195
  @font_wrapper.encode(glyph)
185
196
  @doc.dispatch_message(:complete_objects)
186
197
 
187
198
  dict = @font_wrapper.pdf_object
188
199
 
189
- assert_equal(HexaPDF::Font::CMap.create_to_unicode_cmap([[3, ' '.ord], [glyph.id, 'H'.ord]]),
200
+ assert_equal(HexaPDF::Font::CMap.create_to_unicode_cmap([[1, ' '.ord], [2, '-'.ord],
201
+ [3, 'H'.ord]]),
190
202
  dict[:ToUnicode].stream)
203
+ assert_equal(HexaPDF::Font::CMap.create_cid_cmap([[1, 3], [2, 3], [3, glyph.id]]),
204
+ dict[:Encoding].stream)
191
205
  assert_equal([glyph.id, [glyph.width]], dict[:DescendantFonts][0][:W].value)
192
206
  end
193
207
  end
@@ -185,7 +185,7 @@ describe HexaPDF::Layout::ListBox do
185
185
  [:set_font_and_size, [:F1, 11]],
186
186
  [:set_device_gray_non_stroking_color, [0.5]],
187
187
  [:begin_text],
188
- [:set_text_matrix, [1, 0, 0, 1, 1.15, 92.487]],
188
+ [:move_text, [1.15, 92.487]],
189
189
  [:show_text, ["\x95".b]],
190
190
  [:end_text],
191
191
  [:restore_graphics_state],
@@ -197,7 +197,7 @@ describe HexaPDF::Layout::ListBox do
197
197
  [:set_font_and_size, [:F1, 11]],
198
198
  [:set_device_gray_non_stroking_color, [0.5]],
199
199
  [:begin_text],
200
- [:set_text_matrix, [1, 0, 0, 1, 1.15, 82.487]],
200
+ [:move_text, [1.15, 82.487]],
201
201
  [:show_text, ["\x95".b]],
202
202
  [:end_text],
203
203
  [:restore_graphics_state],
@@ -219,7 +219,7 @@ describe HexaPDF::Layout::ListBox do
219
219
  [:set_text_rise, [-6.111111]],
220
220
  [:set_device_gray_non_stroking_color, [0.5]],
221
221
  [:begin_text],
222
- [:set_text_matrix, [1, 0, 0, 1, 0.1985, 100]],
222
+ [:move_text, [0.1985, 100]],
223
223
  [:show_text, ["m".b]],
224
224
  [:end_text],
225
225
  [:restore_graphics_state],
@@ -241,7 +241,7 @@ describe HexaPDF::Layout::ListBox do
241
241
  [:set_text_rise, [-6.111111]],
242
242
  [:set_device_gray_non_stroking_color, [0.5]],
243
243
  [:begin_text],
244
- [:set_text_matrix, [1, 0, 0, 1, 0.8145, 100]],
244
+ [:move_text, [0.8145, 100]],
245
245
  [:show_text, ["n".b]],
246
246
  [:end_text],
247
247
  [:restore_graphics_state],
@@ -263,7 +263,7 @@ describe HexaPDF::Layout::ListBox do
263
263
  [:set_font_and_size, [:F1, 11]],
264
264
  [:set_device_gray_non_stroking_color, [0.5]],
265
265
  [:begin_text],
266
- [:set_text_matrix, [1, 0, 0, 1, 6.75, 92.487]],
266
+ [:move_text, [6.75, 92.487]],
267
267
  [:show_text, ["1.".b]],
268
268
  [:end_text],
269
269
  [:restore_graphics_state],
@@ -275,7 +275,7 @@ describe HexaPDF::Layout::ListBox do
275
275
  [:set_font_and_size, [:F1, 11]],
276
276
  [:set_device_gray_non_stroking_color, [0.5]],
277
277
  [:begin_text],
278
- [:set_text_matrix, [1, 0, 0, 1, 6.75, 82.487]],
278
+ [:move_text, [6.75, 82.487]],
279
279
  [:show_text, ["2.".b]],
280
280
  [:end_text],
281
281
  [:restore_graphics_state],
@@ -314,7 +314,7 @@ describe HexaPDF::Layout::ListBox do
314
314
  [:save_graphics_state],
315
315
  [:set_font_and_size, [:F1, 10]],
316
316
  [:begin_text],
317
- [:set_text_matrix, [1, 0, 0, 1, 1.5, 93.17]],
317
+ [:move_text, [1.5, 93.17]],
318
318
  [:show_text, ["\x95".b]],
319
319
  [:end_text],
320
320
  [:restore_graphics_state],
@@ -511,6 +511,58 @@ describe HexaPDF::Layout::TableBox do
511
511
  [0, 66, 39.75, 22], [39.75, 66, 39.75, 22], [79.5, 44, 39.75, 44], [119.25, 66, 39.75, 22]])
512
512
  end
513
513
 
514
+ describe "row spans" do
515
+ # ----------
516
+ # | a | b |
517
+ # | a | |
518
+ # | a |----|
519
+ # | a | c |
520
+ # | a | |
521
+ # ----------
522
+ it "works if content of a row span cell is larger than the rows" do
523
+ cells = [[{row_span: 2, content: @fixed_size_boxes[0..2]}, @fixed_size_boxes[3]],
524
+ [@fixed_size_boxes[4]]]
525
+ check_box(create_box(cells: cells, cell_style: {padding: 0, border: {width: 0}}),
526
+ :success, 160, 30,
527
+ [[0, 0, 80, 30], [80, 0, 80, 15], [0, 0, 80, 30], [80, 15, 80, 15]])
528
+ end
529
+
530
+ # ----------
531
+ # | a | b |
532
+ # | |----|
533
+ # | | c |
534
+ # ----------
535
+ it "works if content of a row span cell is smaller than the rows" do
536
+ cells = [[{row_span: 2, content: @fixed_size_boxes[0]}, @fixed_size_boxes[3]],
537
+ [@fixed_size_boxes[4]]]
538
+ check_box(create_box(cells: cells, cell_style: {padding: 0, border: {width: 0}}),
539
+ :success, 160, 20,
540
+ [[0, 0, 80, 20], [80, 0, 80, 10], [0, 0, 80, 20], [80, 10, 80, 10]])
541
+ end
542
+
543
+ # -----------------
544
+ # | a | b | c | d |
545
+ # | a | b |---| d |
546
+ # | a | b | e | |
547
+ # | a | | e | |
548
+ # --------| e | |
549
+ # | f | g | e | |
550
+ # ----------------
551
+ it "works if multiple, possibly overlapping row spans are involved" do
552
+ cells = [[{row_span: 2, content: @fixed_size_boxes[0..2]},
553
+ {row_span: 2, content: @fixed_size_boxes[3..4]},
554
+ @fixed_size_boxes[5],
555
+ {row_span: 3, content: @fixed_size_boxes[6..7]}],
556
+ [{row_span: 2, content: @fixed_size_boxes[8, 3]}],
557
+ [@fixed_size_boxes[11], @fixed_size_boxes[12]]]
558
+ check_box(create_box(cells: cells, cell_style: {padding: 0, border: {width: 0}}),
559
+ :success, 160, 40,
560
+ [[0, 0, 40, 30], [40, 0, 40, 30], [80, 0, 40, 10], [120, 0, 40, 40],
561
+ [0, 0, 40, 30], [40, 0, 40, 30], [80, 10, 40, 30], [120, 0, 40, 40],
562
+ [0, 30, 40, 10], [40, 30, 40, 10], [80, 10, 40, 30], [120, 0, 40, 40]])
563
+ end
564
+ end
565
+
514
566
  it "fits a table with header rows" do
515
567
  result = [[0, 0, 80, 10], [80, 0, 80, 10], [0, 10, 80, 10], [80, 10, 80, 10]]
516
568
  header = lambda {|_| [@fixed_size_boxes[10, 2], @fixed_size_boxes[12, 2]] }
@@ -139,7 +139,7 @@ describe HexaPDF::Layout::TextFragment do
139
139
  [:set_text_rise, [2]],
140
140
  *middle,
141
141
  [:begin_text],
142
- [:set_text_matrix, [1, 0, 0, 1, 10, 15]],
142
+ [:move_text, [10, 15]],
143
143
  [:show_text, ['!']],
144
144
  *back,
145
145
  ].compact
@@ -156,7 +156,7 @@ describe HexaPDF::Layout::TextFragment do
156
156
  @canvas = @doc.pages.add.canvas
157
157
  @fragment.draw(@canvas, 10, 15, ignore_text_properties: true)
158
158
  assert_operators(@canvas.contents, [[:begin_text],
159
- [:set_text_matrix, [1, 0, 0, 1, 10, 15]]])
159
+ [:move_text, [10, 15]]])
160
160
  end
161
161
 
162
162
  describe "uses an appropriate text position setter" do
@@ -188,7 +188,7 @@ describe HexaPDF::Layout::TextFragment do
188
188
  it "horizontal and vertical movement" do
189
189
  @fragment.draw(@canvas, 10, 10, ignore_text_properties: true)
190
190
  assert_operators(@canvas.contents, [[:begin_text],
191
- [:set_text_matrix, [1, 0, 0, 1, 10, 10]]])
191
+ [:move_text, [10, 10]]])
192
192
  end
193
193
  end
194
194
 
@@ -743,10 +743,12 @@ describe HexaPDF::Layout::TextLayouter do
743
743
  result = processor.recorded_ops
744
744
  leading = (result.select {|name, _| name == :set_leading } || [0]).map(&:last).flatten.first
745
745
  pos = [0, 0]
746
- result.select! {|name, _| name == :set_text_matrix || name == :move_text_next_line }.
747
- map! do |name, ops|
746
+ result.select! do |name, _|
747
+ name == :set_text_matrix || name == :move_text || name == :move_text_next_line
748
+ end.map! do |name, ops|
748
749
  case name
749
750
  when :set_text_matrix then pos = ops[-2, 2]
751
+ when :move_text then pos = ops
750
752
  when :move_text_next_line then pos[1] -= leading
751
753
  end
752
754
  pos.dup