hexapdf 0.12.1 → 0.14.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +130 -0
- data/examples/019-acro_form.rb +41 -4
- data/lib/hexapdf/cli/command.rb +4 -2
- data/lib/hexapdf/cli/image2pdf.rb +2 -1
- data/lib/hexapdf/cli/info.rb +51 -2
- data/lib/hexapdf/cli/inspect.rb +30 -8
- data/lib/hexapdf/cli/merge.rb +1 -1
- data/lib/hexapdf/cli/split.rb +74 -14
- data/lib/hexapdf/configuration.rb +15 -0
- data/lib/hexapdf/content/graphic_object/arc.rb +3 -3
- data/lib/hexapdf/content/parser.rb +1 -1
- data/lib/hexapdf/dictionary.rb +9 -6
- data/lib/hexapdf/dictionary_fields.rb +1 -9
- data/lib/hexapdf/document.rb +41 -16
- data/lib/hexapdf/document/files.rb +0 -1
- data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
- data/lib/hexapdf/encryption/security_handler.rb +1 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +1 -0
- data/lib/hexapdf/font/cmap.rb +1 -4
- data/lib/hexapdf/font/true_type/subsetter.rb +12 -3
- data/lib/hexapdf/font/true_type/table/head.rb +1 -0
- data/lib/hexapdf/font/true_type/table/os2.rb +2 -0
- data/lib/hexapdf/font/true_type/table/post.rb +15 -10
- data/lib/hexapdf/font_loader/from_configuration.rb +2 -2
- data/lib/hexapdf/font_loader/from_file.rb +18 -8
- data/lib/hexapdf/image_loader/png.rb +3 -2
- data/lib/hexapdf/importer.rb +3 -2
- data/lib/hexapdf/layout/line.rb +1 -1
- data/lib/hexapdf/layout/style.rb +23 -23
- data/lib/hexapdf/layout/text_layouter.rb +2 -2
- data/lib/hexapdf/layout/text_shaper.rb +3 -2
- data/lib/hexapdf/object.rb +52 -25
- data/lib/hexapdf/parser.rb +96 -4
- data/lib/hexapdf/pdf_array.rb +12 -5
- data/lib/hexapdf/revisions.rb +29 -21
- data/lib/hexapdf/serializer.rb +34 -8
- data/lib/hexapdf/task/optimize.rb +6 -4
- data/lib/hexapdf/tokenizer.rb +4 -3
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +132 -28
- data/lib/hexapdf/type/acro_form/button_field.rb +21 -13
- data/lib/hexapdf/type/acro_form/choice_field.rb +68 -14
- data/lib/hexapdf/type/acro_form/field.rb +35 -5
- data/lib/hexapdf/type/acro_form/form.rb +139 -14
- data/lib/hexapdf/type/acro_form/text_field.rb +70 -4
- data/lib/hexapdf/type/actions/uri.rb +3 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -4
- data/lib/hexapdf/type/catalog.rb +2 -2
- data/lib/hexapdf/type/cid_font.rb +1 -1
- data/lib/hexapdf/type/file_specification.rb +1 -1
- data/lib/hexapdf/type/font.rb +1 -1
- data/lib/hexapdf/type/font_simple.rb +4 -2
- data/lib/hexapdf/type/font_true_type.rb +6 -2
- data/lib/hexapdf/type/font_type0.rb +4 -4
- data/lib/hexapdf/type/form.rb +15 -2
- data/lib/hexapdf/type/image.rb +2 -2
- data/lib/hexapdf/type/page.rb +37 -13
- data/lib/hexapdf/type/page_tree_node.rb +29 -5
- data/lib/hexapdf/type/resources.rb +1 -0
- data/lib/hexapdf/type/trailer.rb +2 -3
- data/lib/hexapdf/utils/object_hash.rb +0 -1
- data/lib/hexapdf/utils/sorted_tree_node.rb +18 -15
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/common_tokenizer_tests.rb +6 -1
- data/test/hexapdf/content/graphic_object/test_arc.rb +4 -4
- data/test/hexapdf/content/test_canvas.rb +3 -3
- data/test/hexapdf/content/test_color_space.rb +1 -1
- data/test/hexapdf/encryption/test_aes.rb +4 -4
- data/test/hexapdf/encryption/test_standard_security_handler.rb +11 -11
- data/test/hexapdf/filter/test_ascii85_decode.rb +1 -1
- data/test/hexapdf/filter/test_ascii_hex_decode.rb +1 -1
- data/test/hexapdf/font/true_type/table/test_post.rb +1 -1
- data/test/hexapdf/font/true_type/test_subsetter.rb +5 -0
- data/test/hexapdf/font_loader/test_from_configuration.rb +7 -3
- data/test/hexapdf/font_loader/test_from_file.rb +7 -0
- data/test/hexapdf/layout/test_style.rb +1 -1
- data/test/hexapdf/layout/test_text_layouter.rb +12 -5
- data/test/hexapdf/test_configuration.rb +2 -2
- data/test/hexapdf/test_dictionary.rb +8 -1
- data/test/hexapdf/test_dictionary_fields.rb +2 -2
- data/test/hexapdf/test_document.rb +18 -10
- data/test/hexapdf/test_object.rb +71 -26
- data/test/hexapdf/test_parser.rb +171 -53
- data/test/hexapdf/test_pdf_array.rb +8 -1
- data/test/hexapdf/test_revisions.rb +35 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +296 -38
- data/test/hexapdf/type/acro_form/test_button_field.rb +22 -2
- data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
- data/test/hexapdf/type/acro_form/test_field.rb +39 -0
- data/test/hexapdf/type/acro_form/test_form.rb +87 -15
- data/test/hexapdf/type/acro_form/test_text_field.rb +77 -1
- data/test/hexapdf/type/test_font_simple.rb +2 -1
- data/test/hexapdf/type/test_font_true_type.rb +6 -0
- data/test/hexapdf/type/test_form.rb +26 -1
- data/test/hexapdf/type/test_page.rb +45 -7
- data/test/hexapdf/type/test_page_tree_node.rb +42 -0
- data/test/hexapdf/utils/test_bit_field.rb +2 -0
- data/test/hexapdf/utils/test_object_hash.rb +5 -0
- data/test/hexapdf/utils/test_sorted_tree_node.rb +10 -9
- data/test/test_helper.rb +2 -0
- metadata +6 -11
data/test/hexapdf/test_parser.rb
CHANGED
@@ -8,6 +8,7 @@ require 'stringio'
|
|
8
8
|
describe HexaPDF::Parser do
|
9
9
|
before do
|
10
10
|
@document = HexaPDF::Document.new
|
11
|
+
@document.config['parser.try_xref_reconstruction'] = false
|
11
12
|
@document.add(@document.wrap(10, oid: 1, gen: 0))
|
12
13
|
|
13
14
|
create_parser(<<~EOF)
|
@@ -87,6 +88,12 @@ describe HexaPDF::Parser do
|
|
87
88
|
assert_equal('12', TestHelper.collector(stream.fiber))
|
88
89
|
end
|
89
90
|
|
91
|
+
it "handles invalid indirect object value consisting of number followed by endobj without space" do
|
92
|
+
create_parser("1 0 obj 749endobj")
|
93
|
+
object, * = @parser.parse_indirect_object
|
94
|
+
assert_equal(749, object)
|
95
|
+
end
|
96
|
+
|
90
97
|
it "recovers from an invalid stream length value" do
|
91
98
|
create_parser("1 0 obj<</Length 4>> stream\n12endstream endobj")
|
92
99
|
obj, _, _, stream = @parser.parse_indirect_object
|
@@ -132,6 +139,54 @@ describe HexaPDF::Parser do
|
|
132
139
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object(0) }
|
133
140
|
assert_match(/stream.*followed by.*endstream/i, exp.message)
|
134
141
|
end
|
142
|
+
|
143
|
+
describe "with strict parsing" do
|
144
|
+
before do
|
145
|
+
@document.config['parser.on_correctable_error'] = proc { true }
|
146
|
+
end
|
147
|
+
|
148
|
+
it "fails if an empty indirect object is found" do
|
149
|
+
create_parser("1 0 obj\nendobj")
|
150
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
151
|
+
assert_match(/no indirect object value/i, exp.message)
|
152
|
+
end
|
153
|
+
|
154
|
+
it "fails if keyword stream is followed only by CR without LF" do
|
155
|
+
create_parser("1 0 obj<</Length 2>> stream\r12\nendstream endobj")
|
156
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
157
|
+
assert_match(/not CR alone/, exp.message)
|
158
|
+
end
|
159
|
+
|
160
|
+
it "fails for numbers followed by endobj without space" do
|
161
|
+
create_parser("1 0 obj 749endobj")
|
162
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
163
|
+
assert_match(/Invalid object value after 'obj'/, exp.message)
|
164
|
+
end
|
165
|
+
|
166
|
+
it "fails if the stream length value is invalid" do
|
167
|
+
create_parser("1 0 obj<</Length 4>> stream\n12endstream endobj")
|
168
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
169
|
+
assert_match(/invalid stream length/i, exp.message)
|
170
|
+
end
|
171
|
+
|
172
|
+
it "fails if the keyword endobj is mangled" do
|
173
|
+
create_parser("1 0 obj\n<< >>\nendobjd\n")
|
174
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
175
|
+
assert_match(/keyword endobj/, exp.message)
|
176
|
+
end
|
177
|
+
|
178
|
+
it "fails if the keyword endobj is missing" do
|
179
|
+
create_parser("1 0 obj\n<< >>")
|
180
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
181
|
+
assert_match(/keyword endobj/, exp.message)
|
182
|
+
end
|
183
|
+
|
184
|
+
it "fails if there is data between 'endstream' and 'endobj'" do
|
185
|
+
create_parser("1 0 obj\n<< >>\nstream\nendstream\ntest\nendobj\n")
|
186
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object(0) }
|
187
|
+
assert_match(/keyword endobj/, exp.message)
|
188
|
+
end
|
189
|
+
end
|
135
190
|
end
|
136
191
|
|
137
192
|
describe "load_object" do
|
@@ -205,7 +260,7 @@ describe HexaPDF::Parser do
|
|
205
260
|
end
|
206
261
|
|
207
262
|
it "ignores garbage at the end of the file" do
|
208
|
-
create_parser("startxref\n5\n%%EOF"
|
263
|
+
create_parser("startxref\n5\n%%EOF" << "\nhallo" * 150)
|
209
264
|
assert_equal(5, @parser.startxref_offset)
|
210
265
|
end
|
211
266
|
|
@@ -215,9 +270,9 @@ describe HexaPDF::Parser do
|
|
215
270
|
end
|
216
271
|
|
217
272
|
it "finds the startxref anywhere in file" do
|
218
|
-
create_parser("startxref\n5\n%%EOF"
|
273
|
+
create_parser("startxref\n5\n%%EOF" << "\nhallo" * 5000)
|
219
274
|
assert_equal(5, @parser.startxref_offset)
|
220
|
-
create_parser("startxref\n5\n%%EOF\n"
|
275
|
+
create_parser("startxref\n5\n%%EOF\n" << "h" * 1017)
|
221
276
|
assert_equal(5, @parser.startxref_offset)
|
222
277
|
end
|
223
278
|
|
@@ -242,6 +297,13 @@ describe HexaPDF::Parser do
|
|
242
297
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
|
243
298
|
assert_match(/missing startxref/, exp.message)
|
244
299
|
end
|
300
|
+
|
301
|
+
it "fails on strict parsing if the startxref is not in the last part of the file" do
|
302
|
+
@document.config['parser.on_correctable_error'] = proc { true }
|
303
|
+
create_parser("startxref\n5\n%%EOF" << "\nhallo" * 5000)
|
304
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
|
305
|
+
assert_match(/end-of-file marker not found/, exp.message)
|
306
|
+
end
|
245
307
|
end
|
246
308
|
|
247
309
|
describe "file_header_version" do
|
@@ -262,7 +324,7 @@ describe HexaPDF::Parser do
|
|
262
324
|
end
|
263
325
|
|
264
326
|
it "ignores junk at the beginning of the file and correctly calculates offset" do
|
265
|
-
create_parser("junk" * 200
|
327
|
+
create_parser("junk" * 200 << "\n%PDF-1.4\n")
|
266
328
|
assert_equal('1.4', @parser.file_header_version)
|
267
329
|
assert_equal(801, @parser.instance_variable_get(:@header_offset))
|
268
330
|
end
|
@@ -318,6 +380,12 @@ describe HexaPDF::Parser do
|
|
318
380
|
assert_match(/invalid cross-reference subsection/i, exp.message)
|
319
381
|
end
|
320
382
|
|
383
|
+
it "fails if a sub section entry is mangled" do
|
384
|
+
create_parser("xref\n0 2\n000a000000 00000 n\n0000000000 65535 n\ntrailer\n<<>>\n")
|
385
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(0) }
|
386
|
+
assert_match(/invalid cross-reference entry/i, exp.message)
|
387
|
+
end
|
388
|
+
|
321
389
|
it "fails if there is no trailer" do
|
322
390
|
create_parser("xref\n0 1\n0000000000 00000 n \n")
|
323
391
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(0) }
|
@@ -329,6 +397,71 @@ describe HexaPDF::Parser do
|
|
329
397
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(0) }
|
330
398
|
assert_match(/dictionary/, exp.message)
|
331
399
|
end
|
400
|
+
|
401
|
+
describe "invalid numbering of main xref section" do
|
402
|
+
it "handles the xref if the numbering is off by N" do
|
403
|
+
create_parser(" 1 0 obj 1 endobj\n" \
|
404
|
+
"xref\n1 2\n0000000000 65535 f \n0000000001 00000 n \ntrailer\n<<>>\n")
|
405
|
+
section, _trailer = @parser.parse_xref_section_and_trailer(17)
|
406
|
+
assert_equal(HexaPDF::XRefSection.in_use_entry(1, 0, 1), section[1])
|
407
|
+
end
|
408
|
+
|
409
|
+
it "fails if the first entry is not the one for oid=0" do
|
410
|
+
create_parser(" 1 0 obj 1 endobj\n" \
|
411
|
+
"xref\n1 2\n0000000000 00005 f \n0000000001 00000 n \ntrailer\n<<>>\n")
|
412
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(17) }
|
413
|
+
assert_match(/Main.*invalid numbering/i, exp.message)
|
414
|
+
|
415
|
+
create_parser(" 1 0 obj 1 endobj\n" \
|
416
|
+
"xref\n1 2\n0000000001 00000 n \n0000000001 00000 n \ntrailer\n<<>>\n")
|
417
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(17) }
|
418
|
+
assert_match(/Main.*invalid numbering/i, exp.message)
|
419
|
+
end
|
420
|
+
|
421
|
+
it "fails if the tested entry position is invalid" do
|
422
|
+
create_parser(" 1 0 obj 1 endobj\n" \
|
423
|
+
"xref\n1 2\n0000000000 65535 f \n0000000005 00000 n \ntrailer\n<<>>\n")
|
424
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(17) }
|
425
|
+
assert_match(/Main.*invalid numbering/i, exp.message)
|
426
|
+
end
|
427
|
+
|
428
|
+
it "fails if the tested entry position's oid doesn't match the corrected entry oid" do
|
429
|
+
create_parser(" 2 0 obj 1 endobj\n" \
|
430
|
+
"xref\n1 2\n0000000000 65535 f \n0000000001 00000 n \ntrailer\n<<>>\n")
|
431
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(17) }
|
432
|
+
assert_match(/Main.*invalid numbering/i, exp.message)
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
describe "with strict parsing" do
|
437
|
+
before do
|
438
|
+
@document.config['parser.on_correctable_error'] = proc { true }
|
439
|
+
end
|
440
|
+
|
441
|
+
it "fails if xref type=n with offset=0" do
|
442
|
+
create_parser("xref\n0 2\n0000000000 00000 n \n0000000000 00000 n \ntrailer\n<<>>\n")
|
443
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(0) }
|
444
|
+
assert_match(/invalid.*cross-reference entry/i, exp.message)
|
445
|
+
end
|
446
|
+
|
447
|
+
it " fails xref type=n with gen>65535" do
|
448
|
+
create_parser("xref\n0 2\n0000000000 00000 n \n0000000000 65536 n \ntrailer\n<<>>\n")
|
449
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(0) }
|
450
|
+
assert_match(/invalid.*cross-reference entry/i, exp.message)
|
451
|
+
end
|
452
|
+
|
453
|
+
it "fails if trailing second whitespace is missing" do
|
454
|
+
create_parser("xref\n0 1\n0000000000 00000 n\ntrailer\n<<>>\n")
|
455
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(0) }
|
456
|
+
assert_match(/invalid.*cross-reference entry/i, exp.message)
|
457
|
+
end
|
458
|
+
|
459
|
+
it "fails if the main cross-reference section has invalid numbering" do
|
460
|
+
create_parser("xref\n1 1\n0000000001 00000 n \ntrailer\n<<>>\n")
|
461
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(0) }
|
462
|
+
assert_match(/Main.*invalid numbering/i, exp.message)
|
463
|
+
end
|
464
|
+
end
|
332
465
|
end
|
333
466
|
|
334
467
|
describe "load_revision" do
|
@@ -348,75 +481,60 @@ describe HexaPDF::Parser do
|
|
348
481
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.load_revision(10) }
|
349
482
|
assert_match(/not a cross-reference stream/, exp.message)
|
350
483
|
end
|
351
|
-
end
|
352
484
|
|
353
|
-
|
354
|
-
before do
|
485
|
+
it "fails on strict parsing if the cross-reference stream doesn't contain an entry for itself" do
|
355
486
|
@document.config['parser.on_correctable_error'] = proc { true }
|
487
|
+
create_parser("2 0 obj\n<</Type/XRef/Length 3/W [1 1 1]/Size 1>>" \
|
488
|
+
"stream\n\x01\x0A\x00\nendstream endobj")
|
489
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.load_revision(0) }
|
490
|
+
assert_match(/entry for itself/, exp.message)
|
356
491
|
end
|
492
|
+
end
|
357
493
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
end
|
363
|
-
|
364
|
-
it "parse_xref_section_and_trailer fails if xref type=n with offset=0" do
|
365
|
-
create_parser("xref\n0 2\n0000000000 00000 n \n0000000000 00000 n \ntrailer\n<<>>\n")
|
366
|
-
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(0) }
|
367
|
-
assert_match(/invalid.*cross-reference entry/i, exp.message)
|
494
|
+
describe "reconstruct_revision" do
|
495
|
+
before do
|
496
|
+
@document.config['parser.try_xref_reconstruction'] = true
|
497
|
+
@xref = HexaPDF::XRefSection.in_use_entry(1, 0, 100)
|
368
498
|
end
|
369
499
|
|
370
|
-
it "
|
371
|
-
create_parser("
|
372
|
-
|
373
|
-
assert_match(/invalid.*cross-reference entry/i, exp.message)
|
500
|
+
it "serially parses the contents" do
|
501
|
+
create_parser("1 0 obj\n5\nendobj\n1 0 obj\n6\nendobj\ntrailer\n<</Size 1>>")
|
502
|
+
assert_equal(6, @parser.load_object(@xref).value)
|
374
503
|
end
|
375
504
|
|
376
|
-
it "
|
377
|
-
create_parser("
|
378
|
-
|
379
|
-
assert_match(/invalid.*cross-reference subsection entry/i, exp.message)
|
505
|
+
it "ignores parts where the starting line is split across lines" do
|
506
|
+
create_parser("1 0 obj\n5\nendobj\n1 0\nobj\n6\nendobj\ntrailer\n<</Size 1>>")
|
507
|
+
assert_equal(5, @parser.load_object(@xref).value)
|
380
508
|
end
|
381
509
|
|
382
|
-
it "
|
383
|
-
create_parser("1 0 obj\nendobj")
|
384
|
-
|
385
|
-
assert_match(/no indirect object value/i, exp.message)
|
510
|
+
it "handles cases where the line contains an invalid string that exceeds the read buffer" do
|
511
|
+
create_parser("(1" << "(abc" * 32188 << "\n1 0 obj\n6\nendobj\ntrailer\n<</Size 1>>")
|
512
|
+
assert_equal(6, @parser.load_object(@xref).value)
|
386
513
|
end
|
387
514
|
|
388
|
-
it "
|
389
|
-
create_parser("1
|
390
|
-
|
391
|
-
assert_match(/not CR alone/, exp.message)
|
515
|
+
it "ignores invalid objects" do
|
516
|
+
create_parser("1 x obj\n5\nendobj\n1 0 xobj\n6\nendobj\n1 0 obj 4\nendobj\ntrailer\n<</Size 1>>")
|
517
|
+
assert_equal(4, @parser.load_object(@xref).value)
|
392
518
|
end
|
393
519
|
|
394
|
-
it "
|
395
|
-
create_parser("1 0 obj
|
396
|
-
|
397
|
-
assert_match(/invalid stream length/i, exp.message)
|
520
|
+
it "ignores invalid lines" do
|
521
|
+
create_parser("1 0 obj\n5\nendobj\nhello there\n1 0 obj\n6\nendobj\ntrailer\n<</Size 1>>")
|
522
|
+
assert_equal(6, @parser.load_object(@xref).value)
|
398
523
|
end
|
399
524
|
|
400
|
-
it "
|
401
|
-
create_parser("1
|
402
|
-
|
403
|
-
assert_match(/keyword endobj/, exp.message)
|
404
|
-
create_parser("1 0 obj\n<< >>")
|
405
|
-
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
406
|
-
assert_match(/keyword endobj/, exp.message)
|
525
|
+
it "uses the last trailer" do
|
526
|
+
create_parser("trailer <</Size 1>>\ntrailer <</Size 2/Prev 342>>")
|
527
|
+
assert_equal({Size: 2}, @parser.reconstructed_revision.trailer.value)
|
407
528
|
end
|
408
529
|
|
409
|
-
it "
|
410
|
-
create_parser("1
|
411
|
-
|
412
|
-
assert_match(/keyword endobj/, exp.message)
|
530
|
+
it "uses the first trailer in case of a linearized file" do
|
531
|
+
create_parser("trailer <</Size 1/Prev 342>>\ntrailer <</Size 2>>")
|
532
|
+
assert_equal({Size: 1}, @parser.reconstructed_revision.trailer.value)
|
413
533
|
end
|
414
534
|
|
415
|
-
it "
|
416
|
-
create_parser("
|
417
|
-
|
418
|
-
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.load_revision(0) }
|
419
|
-
assert_match(/entry for itself/, exp.message)
|
535
|
+
it "fails if no valid trailer is found" do
|
536
|
+
create_parser("1 0 obj\n5\nendobj")
|
537
|
+
assert_raises(HexaPDF::MalformedPDFError) { @parser.load_object(@xref) }
|
420
538
|
end
|
421
539
|
end
|
422
540
|
end
|
@@ -107,6 +107,13 @@ describe HexaPDF::PDFArray do
|
|
107
107
|
assert_equal([1, :data, @array[2]], @array[0, 5])
|
108
108
|
end
|
109
109
|
|
110
|
+
it "allows deleting an object" do
|
111
|
+
obj = @array.value[1]
|
112
|
+
assert_same(obj, @array.delete(obj))
|
113
|
+
ref = HexaPDF::Object.new(:test, oid: 1)
|
114
|
+
assert_equal(ref, @array.delete(ref))
|
115
|
+
end
|
116
|
+
|
110
117
|
describe "slice!" do
|
111
118
|
it "allows deleting a single element" do
|
112
119
|
@array.slice!(2)
|
@@ -157,6 +164,6 @@ describe HexaPDF::PDFArray do
|
|
157
164
|
end
|
158
165
|
|
159
166
|
it "can be converted to a simple array" do
|
160
|
-
assert_equal(@array
|
167
|
+
assert_equal([1, :data, "deref", @array[3]], @array.to_ary)
|
161
168
|
end
|
162
169
|
end
|
@@ -158,4 +158,39 @@ describe HexaPDF::Revisions do
|
|
158
158
|
doc = HexaPDF::Document.new(io: io)
|
159
159
|
assert_equal(2, doc.revisions.count)
|
160
160
|
end
|
161
|
+
|
162
|
+
it "uses the reconstructed revision if errors are found when loading from an IO" do
|
163
|
+
io = StringIO.new(<<~EOF)
|
164
|
+
%PDF-1.7
|
165
|
+
1 0 obj
|
166
|
+
10
|
167
|
+
endobj
|
168
|
+
|
169
|
+
xref
|
170
|
+
0 2
|
171
|
+
0000000000 65535 f
|
172
|
+
0000000009 00000 n
|
173
|
+
trailer
|
174
|
+
<< /Size 5 >>
|
175
|
+
startxref
|
176
|
+
28
|
177
|
+
%%EOF
|
178
|
+
|
179
|
+
2 0 obj
|
180
|
+
300
|
181
|
+
endobj
|
182
|
+
|
183
|
+
xref
|
184
|
+
2 1
|
185
|
+
0000000301 00000 n
|
186
|
+
trailer
|
187
|
+
<< /Size 3 /Prev 100>>
|
188
|
+
startxref
|
189
|
+
139
|
190
|
+
%%EOF
|
191
|
+
EOF
|
192
|
+
doc = HexaPDF::Document.new(io: io)
|
193
|
+
assert_equal(2, doc.revisions.count)
|
194
|
+
assert_same(doc.revisions[0].trailer.value, doc.revisions[1].trailer.value)
|
195
|
+
end
|
161
196
|
end
|
data/test/hexapdf/test_writer.rb
CHANGED
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
|
|
40
40
|
219
|
41
41
|
%%EOF
|
42
42
|
3 0 obj
|
43
|
-
<</Producer(HexaPDF version 0.
|
43
|
+
<</Producer(HexaPDF version 0.14.1)>>
|
44
44
|
endobj
|
45
45
|
xref
|
46
46
|
3 1
|
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
|
|
72
72
|
141
|
73
73
|
%%EOF
|
74
74
|
6 0 obj
|
75
|
-
<</Producer(HexaPDF version 0.
|
75
|
+
<</Producer(HexaPDF version 0.14.1)>>
|
76
76
|
endobj
|
77
77
|
2 0 obj
|
78
78
|
<</Length 10>>stream
|
@@ -25,14 +25,6 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
25
25
|
assert_raises(HexaPDF::Error) { @generator.create_appearances }
|
26
26
|
end
|
27
27
|
|
28
|
-
it "fails for unsupported choice fields" do
|
29
|
-
@field = @doc.wrap(@field, type: :XXAcroFormField, subtype: :Ch)
|
30
|
-
@field[:FT] = :Ch
|
31
|
-
@field.initialize_as_list_box
|
32
|
-
@generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
|
33
|
-
assert_raises(HexaPDF::Error) { @generator.create_appearances }
|
34
|
-
end
|
35
|
-
|
36
28
|
it "fails for unsupported field types" do
|
37
29
|
@field[:FT] = :Unknown
|
38
30
|
assert_raises(HexaPDF::Error) { @generator.create_appearances }
|
@@ -121,6 +113,24 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
121
113
|
[:curve_to, [8.236068, 7.249543, 9.0, 8.572712, 9.0, 10.0]],
|
122
114
|
[:stroke_path], [:restore_graphics_state]])
|
123
115
|
end
|
116
|
+
|
117
|
+
it "handles the special case of a comb field" do
|
118
|
+
@field = @doc.add({FT: :Tx, MaxLen: 4}, type: :XXAcroFormField, subtype: :Tx)
|
119
|
+
@field.initialize_as_comb_text_field
|
120
|
+
@widget = @field.create_widget(@page, Rect: [0, 0, 10, 20])
|
121
|
+
@xform = @doc.add({Type: :XObject, Subtype: :Form, BBox: @widget[:Rect]})
|
122
|
+
@generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
|
123
|
+
@widget.border_style(width: 2)
|
124
|
+
execute
|
125
|
+
assert_operators(@xform.stream,
|
126
|
+
[[:save_graphics_state],
|
127
|
+
[:set_line_width, [2]],
|
128
|
+
[:append_rectangle, [1, 1, 8, 18]],
|
129
|
+
[:move_to, [2.5, 2]], [:line_to, [2.5, 20.0]],
|
130
|
+
[:move_to, [5.0, 2]], [:line_to, [5.0, 20.0]],
|
131
|
+
[:move_to, [7.5, 2]], [:line_to, [7.5, 20.0]],
|
132
|
+
[:stroke_path], [:restore_graphics_state]])
|
133
|
+
end
|
124
134
|
end
|
125
135
|
|
126
136
|
describe "draw_marker" do
|
@@ -342,7 +352,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
342
352
|
end
|
343
353
|
end
|
344
354
|
|
345
|
-
describe "text
|
355
|
+
describe "text fields" do
|
346
356
|
before do
|
347
357
|
@form.set_default_appearance_string
|
348
358
|
@field = @doc.add({FT: :Tx}, type: :XXAcroFormField, subtype: :Tx)
|
@@ -392,35 +402,58 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
392
402
|
assert_equal(@doc.acro_form.default_resources[:Font][:F1], form[:Resources][:Font][:F1])
|
393
403
|
end
|
394
404
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
@field.field_value = ''
|
401
|
-
end
|
405
|
+
it "re-uses the existing form XObject" do
|
406
|
+
@field[:V] = 'test'
|
407
|
+
@generator.create_appearances
|
408
|
+
form = @widget[:AP][:N]
|
409
|
+
form[:key] = :value
|
402
410
|
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
end
|
411
|
+
@field[:V] = 'test1'
|
412
|
+
@generator.create_appearances
|
413
|
+
assert_same(form, @widget[:AP][:N])
|
414
|
+
refute(form.key?(:key))
|
415
|
+
assert_match(/test1/, form.contents)
|
416
|
+
end
|
410
417
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
418
|
+
describe "font size calculation" do
|
419
|
+
before do
|
420
|
+
@widget[:Rect].height = 20
|
421
|
+
@widget[:Rect].width = 100
|
422
|
+
@field.field_value = ''
|
423
|
+
end
|
424
|
+
|
425
|
+
it "uses the non-zero font size" do
|
426
|
+
@field.set_default_appearance_string(font_size: 10)
|
427
|
+
@generator.create_appearances
|
428
|
+
assert_operators(@widget[:AP][:N].stream,
|
429
|
+
[:set_font_and_size, [:F1, 10]],
|
430
|
+
range: 5)
|
431
|
+
end
|
432
|
+
|
433
|
+
it "calculates the font size based on the rectangle height and border width" do
|
434
|
+
@generator.create_appearances
|
435
|
+
assert_operators(@widget[:AP][:N].stream,
|
436
|
+
[:set_font_and_size, [:F1, 12.923875]],
|
437
|
+
range: 5)
|
438
|
+
@widget.border_style(width: 2, color: :transparent)
|
439
|
+
@generator.create_appearances
|
440
|
+
assert_operators(@widget[:AP][:N].stream,
|
441
|
+
[:set_font_and_size, [:F1, 11.487889]],
|
442
|
+
range: 5)
|
443
|
+
end
|
444
|
+
|
445
|
+
it " in case of mulitline auto-sizing" do
|
446
|
+
@field.initialize_as_multiline_text_field
|
447
|
+
@field[:V] = 'a'
|
448
|
+
@field.set_default_appearance_string(font_size: 0)
|
449
|
+
@generator.create_appearances
|
450
|
+
assert_operators(@widget[:AP][:N].stream,
|
451
|
+
[:set_font_and_size, [:F1, 12]],
|
452
|
+
range: 6)
|
422
453
|
end
|
454
|
+
end
|
423
455
|
|
456
|
+
describe "single line text fields" do
|
424
457
|
describe "quadding e.g. text alignment" do
|
425
458
|
before do
|
426
459
|
@field.field_value = 'Test'
|
@@ -478,7 +511,166 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
478
511
|
[:restore_graphics_state],
|
479
512
|
[:end_marked_content]])
|
480
513
|
end
|
514
|
+
end
|
515
|
+
|
516
|
+
describe "multiline text fields" do
|
517
|
+
before do
|
518
|
+
@field.set_default_appearance_string(font_size: 10)
|
519
|
+
@field.initialize_as_multiline_text_field
|
520
|
+
@widget[:Rect].height = 30
|
521
|
+
@widget[:Rect].width = 100
|
522
|
+
end
|
523
|
+
|
524
|
+
describe "quadding e.g. text alignment" do
|
525
|
+
before do
|
526
|
+
@field[:V] = "Test\nValue"
|
527
|
+
end
|
481
528
|
|
529
|
+
it "works for left aligned text" do
|
530
|
+
@field.text_alignment(:left)
|
531
|
+
@generator.create_appearances
|
532
|
+
assert_operators(@widget[:AP][:N].stream,
|
533
|
+
[:set_text_matrix, [1, 0, 0, 1, 2, 16.195]],
|
534
|
+
range: 9)
|
535
|
+
end
|
536
|
+
|
537
|
+
it "works for right aligned text" do
|
538
|
+
@field.text_alignment(:right)
|
539
|
+
@generator.create_appearances
|
540
|
+
assert_operators(@widget[:AP][:N].stream,
|
541
|
+
[:set_text_matrix, [1, 0, 0, 1, 78.55, 16.195]],
|
542
|
+
range: 9)
|
543
|
+
end
|
544
|
+
|
545
|
+
it "works for center aligned text" do
|
546
|
+
@field.text_alignment(:center)
|
547
|
+
@generator.create_appearances
|
548
|
+
assert_operators(@widget[:AP][:N].stream,
|
549
|
+
[:set_text_matrix, [1, 0, 0, 1, 40.275, 16.195]],
|
550
|
+
range: 9)
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
it "creates the /N appearance stream according to the set string" do
|
555
|
+
@field.field_value = "Test\nValue"
|
556
|
+
@generator.create_appearances
|
557
|
+
assert_operators(@widget[:AP][:N].stream,
|
558
|
+
[[:begin_marked_content, [:Tx]],
|
559
|
+
[:save_graphics_state],
|
560
|
+
[:append_rectangle, [1, 1, 98, 28]],
|
561
|
+
[:clip_path_non_zero],
|
562
|
+
[:end_path],
|
563
|
+
[:save_graphics_state],
|
564
|
+
[:set_leading, [11.5625]],
|
565
|
+
[:set_font_and_size, [:F1, 10]],
|
566
|
+
[:begin_text],
|
567
|
+
[:set_text_matrix, [1, 0, 0, 1, 2, 16.195]],
|
568
|
+
[:show_text, ['Test']],
|
569
|
+
[:move_text_next_line],
|
570
|
+
[:show_text, ['Value']],
|
571
|
+
[:end_text],
|
572
|
+
[:restore_graphics_state],
|
573
|
+
[:restore_graphics_state],
|
574
|
+
[:end_marked_content]])
|
575
|
+
|
576
|
+
@field.field_value = "Test\nTest\nTest"
|
577
|
+
@field.set_default_appearance_string(font_size: 0)
|
578
|
+
@generator.create_appearances
|
579
|
+
assert_operators(@widget[:AP][:N].stream,
|
580
|
+
[[:begin_marked_content, [:Tx]],
|
581
|
+
[:save_graphics_state],
|
582
|
+
[:append_rectangle, [1, 1, 98, 28]],
|
583
|
+
[:clip_path_non_zero],
|
584
|
+
[:end_path],
|
585
|
+
[:save_graphics_state],
|
586
|
+
[:set_leading, [9.25]],
|
587
|
+
[:set_font_and_size, [:F1, 8]],
|
588
|
+
[:begin_text],
|
589
|
+
[:set_text_matrix, [1, 0, 0, 1, 2, 18.556]],
|
590
|
+
[:show_text, ['Test']],
|
591
|
+
[:move_text_next_line],
|
592
|
+
[:show_text, ['Test']],
|
593
|
+
[:move_text_next_line],
|
594
|
+
[:show_text, ['Test']],
|
595
|
+
[:end_text],
|
596
|
+
[:restore_graphics_state],
|
597
|
+
[:restore_graphics_state],
|
598
|
+
[:end_marked_content]],
|
599
|
+
)
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
describe "comb text fields" do
|
604
|
+
before do
|
605
|
+
@field.set_default_appearance_string(font_size: 10)
|
606
|
+
@field.initialize_as_comb_text_field
|
607
|
+
@field[:MaxLen] = 10
|
608
|
+
@widget[:Rect].height = 20
|
609
|
+
@widget[:Rect].width = 100
|
610
|
+
end
|
611
|
+
|
612
|
+
describe "quadding e.g. text alignment" do
|
613
|
+
before do
|
614
|
+
@field[:V] = 'Test'
|
615
|
+
end
|
616
|
+
|
617
|
+
it "works for left aligned text" do
|
618
|
+
@field.text_alignment(:left)
|
619
|
+
@generator.create_appearances
|
620
|
+
assert_operators(@widget[:AP][:N].stream,
|
621
|
+
[:set_text_matrix, [1, 0, 0, 1, 2.945, 6.41]],
|
622
|
+
range: 7)
|
623
|
+
end
|
624
|
+
|
625
|
+
it "works for right aligned text" do
|
626
|
+
@field.text_alignment(:right)
|
627
|
+
@generator.create_appearances
|
628
|
+
assert_operators(@widget[:AP][:N].stream,
|
629
|
+
[:set_text_matrix, [1, 0, 0, 1, 62.945, 6.41]],
|
630
|
+
range: 7)
|
631
|
+
end
|
632
|
+
|
633
|
+
it "works for center aligned text" do
|
634
|
+
@field.text_alignment(:center)
|
635
|
+
@generator.create_appearances
|
636
|
+
assert_operators(@widget[:AP][:N].stream,
|
637
|
+
[:set_text_matrix, [1, 0, 0, 1, 32.945, 6.41]],
|
638
|
+
range: 7)
|
639
|
+
end
|
640
|
+
|
641
|
+
it "handles centering like Adobe, e.g. shift left, when text cannot be completely centered" do
|
642
|
+
@field.field_value = 'Hello'
|
643
|
+
@field.text_alignment(:center)
|
644
|
+
@generator.create_appearances
|
645
|
+
assert_operators(@widget[:AP][:N].stream,
|
646
|
+
[:set_text_matrix, [1, 0, 0, 1, 22.39, 6.41]],
|
647
|
+
range: 7)
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
it "creates the /N appearance stream according to the set string" do
|
652
|
+
@field.field_value = 'Text'
|
653
|
+
@generator.create_appearances
|
654
|
+
assert_operators(@widget[:AP][:N].stream,
|
655
|
+
[[:begin_marked_content, [:Tx]],
|
656
|
+
[:save_graphics_state],
|
657
|
+
[:append_rectangle, [1, 1, 98, 18]],
|
658
|
+
[:clip_path_non_zero],
|
659
|
+
[:end_path],
|
660
|
+
[:set_font_and_size, [:F1, 10]],
|
661
|
+
[:begin_text],
|
662
|
+
[:set_text_matrix, [1, 0, 0, 1, 2.945, 6.41]],
|
663
|
+
[:show_text_with_positioning, [['T', -416.5, 'e', -472, 'x', -611, 't']]],
|
664
|
+
[:end_text],
|
665
|
+
[:restore_graphics_state],
|
666
|
+
[:end_marked_content]])
|
667
|
+
end
|
668
|
+
|
669
|
+
it "fails if the /MaxLen key is not set" do
|
670
|
+
@field.delete(:MaxLen)
|
671
|
+
@field[:V] = 't'
|
672
|
+
assert_raises(HexaPDF::Error) { @generator.create_appearances }
|
673
|
+
end
|
482
674
|
end
|
483
675
|
|
484
676
|
describe "choice fields" do
|
@@ -486,27 +678,93 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
486
678
|
@form.set_default_appearance_string
|
487
679
|
field = @doc.add({FT: :Ch}, type: :XXAcroFormField, subtype: :Ch)
|
488
680
|
field.initialize_as_combo_box
|
681
|
+
field.flag(:edit)
|
682
|
+
field.field_value = 'Test'
|
489
683
|
widget = field.create_widget(@page, Rect: [0, 0, 0, 0])
|
490
684
|
generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(widget)
|
491
685
|
generator.create_appearances
|
492
686
|
assert_kind_of(HexaPDF::Type::Form, widget[:AP][:N])
|
493
687
|
end
|
688
|
+
|
689
|
+
describe "list boxes" do
|
690
|
+
before do
|
691
|
+
@field = @doc.add({FT: :Ch}, type: :XXAcroFormField, subtype: :Ch)
|
692
|
+
@field.initialize_as_list_box
|
693
|
+
@field.flag(:multi_select)
|
694
|
+
@field.option_items = ['a', 'b', 'c']
|
695
|
+
@widget = @field.create_widget(@page, Rect: [0, 0, 90, 36])
|
696
|
+
@generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
|
697
|
+
end
|
698
|
+
|
699
|
+
it "uses a fixed font size for list box items if auto-sizing is used" do
|
700
|
+
@field.set_default_appearance_string(font_size: 0)
|
701
|
+
@generator.create_appearances
|
702
|
+
assert_operators(@widget[:AP][:N].stream,
|
703
|
+
[:set_font_and_size, [:F1, 12]],
|
704
|
+
range: 8)
|
705
|
+
end
|
706
|
+
|
707
|
+
it "uses the set values instead of the ones from /I if in conflict" do
|
708
|
+
@field[:I] = [0, 1]
|
709
|
+
@field[:V] = ['b']
|
710
|
+
@generator.create_appearances
|
711
|
+
assert_operators(@widget[:AP][:N].stream,
|
712
|
+
[[:set_device_rgb_non_stroking_color, [0.6, 0.756863, 0.854902]],
|
713
|
+
[:append_rectangle, [1, 7.25, 88, 13.875]],
|
714
|
+
[:fill_path_non_zero]],
|
715
|
+
range: 5..7)
|
716
|
+
end
|
717
|
+
|
718
|
+
it "creates the /N appearance stream" do
|
719
|
+
@field[:I] = [1, 2]
|
720
|
+
@field[:V] = ['b', 'c']
|
721
|
+
@generator.create_appearances
|
722
|
+
assert_operators(@widget[:AP][:N].stream,
|
723
|
+
[[:begin_marked_content, [:Tx]],
|
724
|
+
[:save_graphics_state],
|
725
|
+
[:append_rectangle, [1, 1, 88, 34]],
|
726
|
+
[:clip_path_non_zero], [:end_path],
|
727
|
+
[:set_device_rgb_non_stroking_color, [0.6, 0.756863, 0.854902]],
|
728
|
+
[:append_rectangle, [1, 7.25, 88, 13.875]],
|
729
|
+
[:append_rectangle, [1, -6.625, 88, 13.875]],
|
730
|
+
[:fill_path_non_zero],
|
731
|
+
[:save_graphics_state],
|
732
|
+
[:set_leading, [13.875]],
|
733
|
+
[:set_font_and_size, [:F1, 12]],
|
734
|
+
[:set_device_gray_non_stroking_color, [0.0]],
|
735
|
+
[:begin_text],
|
736
|
+
[:set_text_matrix, [1, 0, 0, 1, 2, 23.609]],
|
737
|
+
[:show_text, ["a"]],
|
738
|
+
[:move_text_next_line],
|
739
|
+
[:show_text, ["b"]],
|
740
|
+
[:end_text],
|
741
|
+
[:restore_graphics_state], [:restore_graphics_state],
|
742
|
+
[:end_marked_content]])
|
743
|
+
end
|
744
|
+
end
|
494
745
|
end
|
495
746
|
|
496
747
|
describe "font resolution in case the referenced font is not usable" do
|
497
748
|
before do
|
498
|
-
|
499
|
-
@field
|
749
|
+
@doc.config['acro_form.fallback_font'] = ['Times', {variant: :none}]
|
750
|
+
@field[:V] = 'Test'
|
500
751
|
end
|
501
752
|
|
502
|
-
it "uses the fallback font if
|
503
|
-
@
|
753
|
+
it "uses the fallback font if the font is not usable" do
|
754
|
+
def (@form.default_resources.font(:F1)).font_wrapper; nil; end
|
504
755
|
@generator.create_appearances
|
505
756
|
assert_equal(:'Times-Roman', @widget[:AP][:N][:Resources][:Font][:F2][:BaseFont])
|
506
757
|
end
|
507
758
|
|
759
|
+
it "uses the fallback font if the font is not found" do
|
760
|
+
@form.default_resources[:Font].delete(:F1)
|
761
|
+
@generator.create_appearances
|
762
|
+
assert_equal(:'Times-Roman', @widget[:AP][:N][:Resources][:Font][:F1][:BaseFont])
|
763
|
+
end
|
764
|
+
|
508
765
|
it "fails if fallback fonts are disabled" do
|
509
766
|
@doc.config['acro_form.fallback_font'] = nil
|
767
|
+
@form.default_resources[:Font].delete(:F1)
|
510
768
|
msg = assert_raises(HexaPDF::Error) { @generator.create_appearances }
|
511
769
|
assert_match(/Font.*not usable/, msg.message)
|
512
770
|
end
|