hexapdf 0.46.0 → 1.0.0

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