hexapdf 0.26.2 → 0.28.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +115 -1
- data/README.md +1 -1
- data/examples/013-text_layouter_shapes.rb +8 -8
- data/examples/016-frame_automatic_box_placement.rb +3 -3
- data/examples/017-frame_text_flow.rb +3 -3
- data/examples/019-acro_form.rb +14 -3
- data/examples/020-column_box.rb +3 -3
- data/examples/023-images.rb +30 -0
- data/lib/hexapdf/cli/info.rb +5 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/cli/split.rb +8 -8
- data/lib/hexapdf/cli/watermark.rb +2 -2
- data/lib/hexapdf/configuration.rb +3 -2
- data/lib/hexapdf/content/canvas.rb +8 -3
- data/lib/hexapdf/dictionary.rb +4 -17
- data/lib/hexapdf/document/destinations.rb +42 -5
- data/lib/hexapdf/document/signatures.rb +265 -48
- data/lib/hexapdf/document.rb +6 -10
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/importer.rb +35 -27
- data/lib/hexapdf/layout/list_box.rb +1 -5
- data/lib/hexapdf/object.rb +5 -0
- data/lib/hexapdf/parser.rb +14 -0
- data/lib/hexapdf/revision.rb +15 -12
- data/lib/hexapdf/revisions.rb +7 -1
- data/lib/hexapdf/tokenizer.rb +15 -9
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/field.rb +11 -5
- data/lib/hexapdf/type/acro_form/form.rb +61 -8
- data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -0
- data/lib/hexapdf/type/catalog.rb +1 -1
- data/lib/hexapdf/type/font_true_type.rb +14 -0
- data/lib/hexapdf/type/object_stream.rb +2 -2
- data/lib/hexapdf/type/outline.rb +19 -1
- data/lib/hexapdf/type/outline_item.rb +72 -14
- data/lib/hexapdf/type/page.rb +95 -64
- data/lib/hexapdf/type/resources.rb +13 -17
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +16 -2
- data/lib/hexapdf/type/signature.rb +10 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +5 -3
- data/test/hexapdf/content/test_canvas.rb +5 -0
- data/test/hexapdf/document/test_destinations.rb +41 -0
- data/test/hexapdf/document/test_pages.rb +2 -2
- data/test/hexapdf/document/test_signatures.rb +139 -19
- data/test/hexapdf/encryption/test_aes.rb +1 -1
- data/test/hexapdf/filter/test_predictor.rb +0 -1
- data/test/hexapdf/layout/test_box.rb +2 -1
- data/test/hexapdf/layout/test_column_box.rb +1 -1
- data/test/hexapdf/layout/test_list_box.rb +1 -1
- data/test/hexapdf/test_document.rb +2 -8
- data/test/hexapdf/test_importer.rb +27 -6
- data/test/hexapdf/test_parser.rb +19 -2
- data/test/hexapdf/test_revision.rb +15 -14
- data/test/hexapdf/test_revisions.rb +63 -12
- data/test/hexapdf/test_stream.rb +1 -1
- data/test/hexapdf/test_tokenizer.rb +10 -1
- data/test/hexapdf/test_writer.rb +11 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
- data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
- data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_field.rb +4 -4
- data/test/hexapdf/type/acro_form/test_form.rb +65 -0
- data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
- data/test/hexapdf/type/signature/common.rb +54 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +21 -0
- data/test/hexapdf/type/test_catalog.rb +5 -2
- data/test/hexapdf/type/test_font_true_type.rb +20 -0
- data/test/hexapdf/type/test_object_stream.rb +2 -1
- data/test/hexapdf/type/test_outline.rb +4 -1
- data/test/hexapdf/type/test_outline_item.rb +62 -1
- data/test/hexapdf/type/test_page.rb +103 -45
- data/test/hexapdf/type/test_page_tree_node.rb +4 -2
- data/test/hexapdf/type/test_resources.rb +0 -5
- data/test/hexapdf/type/test_signature.rb +8 -0
- data/test/test_helper.rb +1 -1
- metadata +61 -4
|
@@ -131,7 +131,8 @@ describe HexaPDF::Layout::Box do
|
|
|
131
131
|
assert_equal([nil, box], box.split(150, 150, nil))
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
it "can't be split if it doesn't (completely) fit as the default implementation
|
|
134
|
+
it "can't be split if it doesn't (completely) fit as the default implementation " \
|
|
135
|
+
"knows nothing about the content" do
|
|
135
136
|
@box.style.position = :flow # make sure we would generally be splitable
|
|
136
137
|
@box.fit(90, 100, nil)
|
|
137
138
|
assert_equal([nil, @box], @box.split(150, 150, nil))
|
|
@@ -11,7 +11,7 @@ describe HexaPDF::Layout::ColumnBox do
|
|
|
11
11
|
@text_boxes = 5.times.map do
|
|
12
12
|
HexaPDF::Layout::TextBox.new(items: [inline_box] * 15, style: {position: :default})
|
|
13
13
|
end
|
|
14
|
-
draw_block = lambda do |canvas,
|
|
14
|
+
draw_block = lambda do |canvas, _box|
|
|
15
15
|
canvas.move_to(0, 0).end_path
|
|
16
16
|
end
|
|
17
17
|
@fixed_size_boxes = 15.times.map { HexaPDF::Layout::Box.new(width: 20, height: 10, &draw_block) }
|
|
@@ -222,7 +222,7 @@ describe HexaPDF::Layout::ListBox do
|
|
|
222
222
|
end
|
|
223
223
|
|
|
224
224
|
it "allows drawing custom markers" do
|
|
225
|
-
marker = lambda do |
|
|
225
|
+
marker = lambda do |_doc, _list_box, _index|
|
|
226
226
|
HexaPDF::Layout::Box.create(width: 10, height: 10) {}
|
|
227
227
|
end
|
|
228
228
|
box = create_box(children: @fixed_size_boxes[0, 1], item_type: marker)
|
|
@@ -163,14 +163,8 @@ describe HexaPDF::Document do
|
|
|
163
163
|
refute_equal(0, obj.oid)
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
-
it "
|
|
167
|
-
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
it "fails if the given object is associated with no or the destination document" do
|
|
171
|
-
assert_raises(ArgumentError) { @doc.import(HexaPDF::Object.new(5)) }
|
|
172
|
-
obj = @doc.add(5)
|
|
173
|
-
assert_raises(ArgumentError) { @doc.import(obj) }
|
|
166
|
+
it "works if the given object is not a PDF object" do
|
|
167
|
+
assert_equal(5, @doc.import(5))
|
|
174
168
|
end
|
|
175
169
|
end
|
|
176
170
|
|
|
@@ -19,6 +19,7 @@ describe HexaPDF::Importer::NullableWeakRef do
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
describe HexaPDF::Importer do
|
|
22
|
+
class TestClass < HexaPDF::Dictionary; end
|
|
22
23
|
before do
|
|
23
24
|
@source = HexaPDF::Document.new
|
|
24
25
|
obj = @source.add("test")
|
|
@@ -30,12 +31,12 @@ describe HexaPDF::Importer do
|
|
|
30
31
|
@source.pages.add
|
|
31
32
|
@source.pages.root[:Rotate] = 90
|
|
32
33
|
@dest = HexaPDF::Document.new
|
|
33
|
-
@importer = HexaPDF::Importer.for(
|
|
34
|
+
@importer = HexaPDF::Importer.for(@dest)
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
describe "::for" do
|
|
37
38
|
it "caches the importer" do
|
|
38
|
-
assert_same(@importer, HexaPDF::Importer.for(
|
|
39
|
+
assert_same(@importer, HexaPDF::Importer.for(@dest))
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
|
|
@@ -60,8 +61,14 @@ describe HexaPDF::Importer do
|
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
it "can import a direct object" do
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
assert_nil(@importer.import(nil))
|
|
65
|
+
assert_equal(5, @importer.import(5))
|
|
66
|
+
assert(@dest.object?(@importer.import({key: @obj})[:key]))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "determines the source document dynamically" do
|
|
70
|
+
obj = @importer.import(@obj.value)
|
|
71
|
+
assert_equal("test", obj[:ref].value)
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
it "copies the data of the imported objects" do
|
|
@@ -86,6 +93,19 @@ describe HexaPDF::Importer do
|
|
|
86
93
|
assert_same(hash, obj[:hash])
|
|
87
94
|
end
|
|
88
95
|
|
|
96
|
+
it "uses the class of the argument when directly importing a HexaPDF::Object" do
|
|
97
|
+
src_obj = @source.wrap(@hash, type: TestClass)
|
|
98
|
+
dest_obj = @importer.import(src_obj)
|
|
99
|
+
assert_instance_of(TestClass, dest_obj)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "uses the class of the argument when importing an already mapped HexaPDF::Object" do
|
|
103
|
+
@importer.import(@obj) # also maps @hash
|
|
104
|
+
src_obj = @source.wrap(@hash, type: TestClass)
|
|
105
|
+
dest_obj = @importer.import(src_obj)
|
|
106
|
+
assert_instance_of(TestClass, dest_obj)
|
|
107
|
+
end
|
|
108
|
+
|
|
89
109
|
it "duplicates the stream if it is a string" do
|
|
90
110
|
src_obj = @source.add({}, stream: 'data')
|
|
91
111
|
dst_obj = @importer.import(src_obj)
|
|
@@ -106,10 +126,11 @@ describe HexaPDF::Importer do
|
|
|
106
126
|
assert_equal(90, page[:Rotate])
|
|
107
127
|
end
|
|
108
128
|
|
|
109
|
-
it "
|
|
129
|
+
it "works for importing objects from different documents" do
|
|
110
130
|
other_doc = HexaPDF::Document.new
|
|
111
131
|
other_obj = other_doc.add("test")
|
|
112
|
-
|
|
132
|
+
imported = @importer.import(other_obj)
|
|
133
|
+
assert_equal("test", imported.value)
|
|
113
134
|
end
|
|
114
135
|
end
|
|
115
136
|
end
|
data/test/hexapdf/test_parser.rb
CHANGED
|
@@ -54,6 +54,23 @@ describe HexaPDF::Parser do
|
|
|
54
54
|
@parser = HexaPDF::Parser.new(@parse_io, @document)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
describe "linearized?" do
|
|
58
|
+
it "can determine whether a document is linearized" do
|
|
59
|
+
create_parser("%PDF-1.7\n%abcdefgh\n1 0 obj\n<</Linearized 1/H [2 4]/O 1/E 1/N 1/T 1>>\nendobj")
|
|
60
|
+
assert(@parser.linearized?)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "returns false if the first object is not a linearization dictionary" do
|
|
64
|
+
create_parser("%PDF-1.7\n%abcdefgh\n1 0 obj\n<</Length 2 0 R>>\nstream\nhallo\nendstream\nendobj")
|
|
65
|
+
refute(@parser.linearized?)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "returns false if there is a parse error" do
|
|
69
|
+
create_parser("%PDF-1.7\n%abcdefgh\n1 a obj thing")
|
|
70
|
+
refute(@parser.linearized?)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
57
74
|
describe "parse_indirect_object" do
|
|
58
75
|
it "reads indirect objects sequentially" do
|
|
59
76
|
object, oid, gen, stream = @parser.parse_indirect_object
|
|
@@ -108,7 +125,7 @@ describe HexaPDF::Parser do
|
|
|
108
125
|
end
|
|
109
126
|
|
|
110
127
|
it "treats indirect objects with invalid values as null objects" do
|
|
111
|
-
create_parser("1 0 obj <</test
|
|
128
|
+
create_parser("1 0 obj <</test <end)> > endobj")
|
|
112
129
|
object, * = @parser.parse_indirect_object
|
|
113
130
|
assert_nil(object)
|
|
114
131
|
end
|
|
@@ -210,7 +227,7 @@ describe HexaPDF::Parser do
|
|
|
210
227
|
end
|
|
211
228
|
|
|
212
229
|
it "fails for invalid values" do
|
|
213
|
-
create_parser("1 0 obj <</test
|
|
230
|
+
create_parser("1 0 obj <</test <end)> >endobj")
|
|
214
231
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
|
215
232
|
assert_match(/Invalid value after '1 0 obj'/, exp.message)
|
|
216
233
|
end
|
|
@@ -199,6 +199,14 @@ describe HexaPDF::Revision do
|
|
|
199
199
|
deleted = @rev.object(6)
|
|
200
200
|
@rev.delete(6)
|
|
201
201
|
assert_equal([obj, @obj, deleted], @rev.each_modified_object.to_a)
|
|
202
|
+
assert_same(obj, @rev.object(3))
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "optionally deletes the modified objects from the revision" do
|
|
206
|
+
obj = @rev.object(3)
|
|
207
|
+
obj.value = :other
|
|
208
|
+
assert_equal([obj], @rev.each_modified_object(delete: true).to_a)
|
|
209
|
+
refute_same(obj, @rev.object(3))
|
|
202
210
|
end
|
|
203
211
|
|
|
204
212
|
it "ignores object and xref streams that were deleted" do
|
|
@@ -207,6 +215,13 @@ describe HexaPDF::Revision do
|
|
|
207
215
|
assert_equal([], @rev.each_modified_object.to_a)
|
|
208
216
|
end
|
|
209
217
|
|
|
218
|
+
it "handles object and xref streams that were added appropriately depending on the 'all' arg" do
|
|
219
|
+
xref = @rev.add(HexaPDF::Dictionary.new({Type: :XRef}, oid: 8))
|
|
220
|
+
objstm = @rev.add(HexaPDF::Dictionary.new({Type: :ObjStm}, oid: 9))
|
|
221
|
+
assert_equal([], @rev.each_modified_object.to_a)
|
|
222
|
+
assert_equal([xref, objstm], @rev.each_modified_object(all: true).to_a)
|
|
223
|
+
end
|
|
224
|
+
|
|
210
225
|
it "doesn't return non-modified objects" do
|
|
211
226
|
@rev.object(2)
|
|
212
227
|
assert_equal([], @rev.each_modified_object.to_a)
|
|
@@ -230,18 +245,4 @@ describe HexaPDF::Revision do
|
|
|
230
245
|
assert_equal([], @rev.each_modified_object.to_a)
|
|
231
246
|
end
|
|
232
247
|
end
|
|
233
|
-
|
|
234
|
-
describe "reset_objects" do
|
|
235
|
-
it "deletes loaded objects" do
|
|
236
|
-
@rev.object(2)
|
|
237
|
-
@rev.reset_objects
|
|
238
|
-
assert(@rev.instance_variable_get(:@objects).oids.empty?)
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
it "deletes added objects" do
|
|
242
|
-
@rev.add(@obj)
|
|
243
|
-
@rev.reset_objects
|
|
244
|
-
assert(@rev.instance_variable_get(:@objects).oids.empty?)
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
248
|
end
|
|
@@ -273,40 +273,48 @@ describe HexaPDF::Revisions do
|
|
|
273
273
|
1 0 obj
|
|
274
274
|
10
|
|
275
275
|
endobj
|
|
276
|
+
xref
|
|
277
|
+
0 2
|
|
278
|
+
0000000000 65535 f
|
|
279
|
+
0000000009 00000 n
|
|
280
|
+
trailer
|
|
281
|
+
<< /Size 2 >>
|
|
282
|
+
startxref
|
|
283
|
+
27
|
|
284
|
+
%%EOF
|
|
276
285
|
|
|
277
286
|
2 0 obj
|
|
278
287
|
20
|
|
279
288
|
endobj
|
|
280
289
|
|
|
281
290
|
3 0 obj
|
|
282
|
-
<< /Type /XRef /Size
|
|
291
|
+
<< /Type /XRef /Size 4 /Index [2 1] /W [1 1 1] /Filter /ASCIIHexDecode /Length 6
|
|
283
292
|
>>stream
|
|
284
|
-
|
|
293
|
+
017600
|
|
285
294
|
endstream
|
|
286
295
|
endobj
|
|
287
296
|
|
|
288
297
|
xref
|
|
289
|
-
|
|
290
|
-
0000000000 65535 f
|
|
291
|
-
0000000009 00000 n
|
|
298
|
+
2 2
|
|
292
299
|
0000000000 65535 f
|
|
293
|
-
|
|
300
|
+
0000000137 00000 n
|
|
294
301
|
trailer
|
|
295
|
-
<< /Size
|
|
302
|
+
<< /Size 4 /Prev 27>>
|
|
296
303
|
startxref
|
|
297
|
-
|
|
304
|
+
260
|
|
298
305
|
%%EOF
|
|
299
306
|
|
|
300
307
|
xref
|
|
301
308
|
0 0
|
|
302
309
|
trailer
|
|
303
|
-
<< /Size
|
|
310
|
+
<< /Size 4 /Prev 260 /XRefStm 137>>
|
|
304
311
|
startxref
|
|
305
|
-
|
|
312
|
+
360
|
|
306
313
|
%%EOF
|
|
307
314
|
EOF
|
|
308
|
-
doc = HexaPDF::Document.new(io: io)
|
|
309
|
-
assert_equal(
|
|
315
|
+
doc = HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
|
|
316
|
+
assert_equal(2, doc.revisions.count)
|
|
317
|
+
assert_equal(10, doc.object(1).value)
|
|
310
318
|
assert_equal(20, doc.object(2).value)
|
|
311
319
|
end
|
|
312
320
|
|
|
@@ -348,4 +356,47 @@ describe HexaPDF::Revisions do
|
|
|
348
356
|
HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
|
|
349
357
|
end
|
|
350
358
|
end
|
|
359
|
+
|
|
360
|
+
it "merges the two revisions of a linearized PDF into one" do
|
|
361
|
+
io = StringIO.new(<<~EOF)
|
|
362
|
+
%PDF-1.2
|
|
363
|
+
5 0 obj
|
|
364
|
+
<</Linearized 1>>
|
|
365
|
+
endobj
|
|
366
|
+
xref
|
|
367
|
+
5 1
|
|
368
|
+
0000000009 00000 n
|
|
369
|
+
trailer
|
|
370
|
+
<</ID[(a)(b)]/Info 1 0 R/Root 2 0 R/Size 6/Prev 394>>
|
|
371
|
+
%
|
|
372
|
+
1 0 obj
|
|
373
|
+
<</ModDate(D:20221205233910+01'00')/Producer(HexaPDF version 0.27.0)>>
|
|
374
|
+
endobj
|
|
375
|
+
2 0 obj
|
|
376
|
+
<</Type/Catalog/Pages 3 0 R>>
|
|
377
|
+
endobj
|
|
378
|
+
3 0 obj
|
|
379
|
+
<</Type/Pages/Kids[4 0 R]/Count 1>>
|
|
380
|
+
endobj
|
|
381
|
+
4 0 obj
|
|
382
|
+
<</Type/Page/MediaBox[0 0 595 842]/Parent 3 0 R/Resources<<>>>>
|
|
383
|
+
endobj
|
|
384
|
+
xref
|
|
385
|
+
0 5
|
|
386
|
+
0000000000 65535 f
|
|
387
|
+
0000000133 00000 n
|
|
388
|
+
0000000219 00000 n
|
|
389
|
+
0000000264 00000 n
|
|
390
|
+
0000000315 00000 n
|
|
391
|
+
trailer
|
|
392
|
+
<</ID[(a)(b)]/Info 1 0 R/Root 2 0 R/Size 5>>
|
|
393
|
+
startxref
|
|
394
|
+
41
|
|
395
|
+
%%EOF
|
|
396
|
+
EOF
|
|
397
|
+
doc = HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
|
|
398
|
+
assert(doc.revisions.parser.linearized?)
|
|
399
|
+
assert_equal(1, doc.revisions.count)
|
|
400
|
+
assert_same(5, doc.revisions.current.xref_section.max_oid)
|
|
401
|
+
end
|
|
351
402
|
end
|
data/test/hexapdf/test_stream.rb
CHANGED
|
@@ -142,7 +142,7 @@ describe HexaPDF::Stream do
|
|
|
142
142
|
def encoded_data(str, encoders = [])
|
|
143
143
|
map = @document.config['filter.map']
|
|
144
144
|
tmp = feeder(str)
|
|
145
|
-
encoders.each {|e| tmp =
|
|
145
|
+
encoders.each {|e| tmp = Object.const_get(map[e]).encoder(tmp) }
|
|
146
146
|
collector(tmp)
|
|
147
147
|
end
|
|
148
148
|
|
|
@@ -13,9 +13,18 @@ describe HexaPDF::Tokenizer do
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
it "handles object references" do
|
|
16
|
-
|
|
16
|
+
#HexaPDF::Reference.new(1, 0), HexaPDF::Reference.new(1, 2), 2, -1, 'R', 0, 0, 'R', -1, 0, 'R',
|
|
17
|
+
create_tokenizer("1 0 R +2 +15 R 2 -1 R 0 0 R 0 10 R -1 0 R")
|
|
17
18
|
assert_equal(HexaPDF::Reference.new(1, 0), @tokenizer.next_token)
|
|
18
19
|
assert_equal(HexaPDF::Reference.new(2, 15), @tokenizer.next_token)
|
|
20
|
+
assert_equal(2, @tokenizer.next_token)
|
|
21
|
+
assert_equal(-1, @tokenizer.next_token)
|
|
22
|
+
assert_equal('R', @tokenizer.next_token)
|
|
23
|
+
assert_nil(@tokenizer.next_token)
|
|
24
|
+
assert_nil(@tokenizer.next_token)
|
|
25
|
+
assert_equal(-1, @tokenizer.next_token)
|
|
26
|
+
assert_equal(0, @tokenizer.next_token)
|
|
27
|
+
assert_equal('R', @tokenizer.next_token)
|
|
19
28
|
@tokenizer.pos = 0
|
|
20
29
|
assert_equal(HexaPDF::Reference.new(1, 0), @tokenizer.next_object)
|
|
21
30
|
assert_equal(HexaPDF::Reference.new(2, 15), @tokenizer.next_object)
|
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.28.0)>>
|
|
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.28.0)>>
|
|
76
76
|
endobj
|
|
77
77
|
2 0 obj
|
|
78
78
|
<</Length 10>>stream
|
|
@@ -125,6 +125,14 @@ describe HexaPDF::Writer do
|
|
|
125
125
|
refute_match(/^trailer/, output_io.string)
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
+
it "updates the PDF version using the catalog's /Version entry if necessary" do
|
|
129
|
+
doc = HexaPDF::Document.new(io: @std_input_io)
|
|
130
|
+
doc.version = '2.0'
|
|
131
|
+
output_io = StringIO.new
|
|
132
|
+
HexaPDF::Writer.write(doc, output_io, incremental: true)
|
|
133
|
+
assert_equal('2.0', HexaPDF::Document.new(io: output_io).version)
|
|
134
|
+
end
|
|
135
|
+
|
|
128
136
|
it "raises an error if the used encryption was changed" do
|
|
129
137
|
io = StringIO.new
|
|
130
138
|
doc = HexaPDF::Document.new
|
|
@@ -206,7 +214,7 @@ describe HexaPDF::Writer do
|
|
|
206
214
|
<</Type/Page/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<<>>>>
|
|
207
215
|
endobj
|
|
208
216
|
5 0 obj
|
|
209
|
-
<</Producer(HexaPDF version 0.
|
|
217
|
+
<</Producer(HexaPDF version 0.28.0)>>
|
|
210
218
|
endobj
|
|
211
219
|
4 0 obj
|
|
212
220
|
<</Root 1 0 R/Info 5 0 R/Size 6/Type/XRef/W[1 1 2]/Index[0 6]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 33>>stream
|
|
@@ -18,12 +18,6 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
|
18
18
|
@generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
it "fails for unsupported button fields" do
|
|
22
|
-
@field.flag(:push_button)
|
|
23
|
-
@generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
|
|
24
|
-
assert_raises(HexaPDF::Error) { @generator.create_appearances }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
21
|
it "fails for unsupported field types" do
|
|
28
22
|
@field[:FT] = :Unknown
|
|
29
23
|
assert_raises(HexaPDF::Error) { @generator.create_appearances }
|
|
@@ -141,8 +135,8 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
|
141
135
|
end
|
|
142
136
|
|
|
143
137
|
def execute
|
|
144
|
-
@generator.send(:draw_marker, @xform.canvas, @widget[:Rect], @widget.
|
|
145
|
-
@widget.marker_style)
|
|
138
|
+
@generator.send(:draw_marker, @xform.canvas, @widget[:Rect].width, @widget[:Rect].height,
|
|
139
|
+
@widget.border_style.width, @widget.marker_style)
|
|
146
140
|
end
|
|
147
141
|
|
|
148
142
|
it "handles the marker :circle specially for radio button widgets" do
|
|
@@ -196,6 +190,18 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
|
196
190
|
[:show_text, ["4"]],
|
|
197
191
|
[:end_text]])
|
|
198
192
|
end
|
|
193
|
+
|
|
194
|
+
it "draws the default marker if an empty string is specified as marker" do
|
|
195
|
+
@widget.marker_style(style: '', color: 0.5, size: 5)
|
|
196
|
+
execute
|
|
197
|
+
assert_operators(@xform.stream,
|
|
198
|
+
[[:set_font_and_size, [:F1, 5]],
|
|
199
|
+
[:set_device_gray_non_stroking_color, [0.5]],
|
|
200
|
+
[:begin_text],
|
|
201
|
+
[:set_text_matrix, [1, 0, 0, 1, 2.885, 8.2725]],
|
|
202
|
+
[:show_text, ["4"]],
|
|
203
|
+
[:end_text]])
|
|
204
|
+
end
|
|
199
205
|
end
|
|
200
206
|
end
|
|
201
207
|
|
|
@@ -213,7 +219,15 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
|
213
219
|
|
|
214
220
|
it "updates the widgets' /AS entry to point to the selected appearance" do
|
|
215
221
|
@generator.create_appearances
|
|
216
|
-
assert_equal(
|
|
222
|
+
assert_equal(:Off, @widget[:AS])
|
|
223
|
+
|
|
224
|
+
@field.field_value = :Yes
|
|
225
|
+
@generator.create_appearances
|
|
226
|
+
assert_equal(:Yes, @widget[:AS])
|
|
227
|
+
|
|
228
|
+
@field.delete(:V)
|
|
229
|
+
@generator.create_appearances
|
|
230
|
+
assert_equal(:Off, @widget[:AS])
|
|
217
231
|
end
|
|
218
232
|
|
|
219
233
|
it "set the print flag on the widgets" do
|
|
@@ -231,6 +245,33 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
|
231
245
|
assert_equal(12, @widget[:Rect].height)
|
|
232
246
|
end
|
|
233
247
|
|
|
248
|
+
describe "takes the rotation into account" do
|
|
249
|
+
def check_rotation(angle, width, height, matrix)
|
|
250
|
+
@widget[:MK] = {R: angle}
|
|
251
|
+
@field[:V] = :Yes
|
|
252
|
+
@generator.create_appearances
|
|
253
|
+
form = @widget[:AP][:N][@widget[:AS]]
|
|
254
|
+
assert_equal([0, 0, width, height], form[:BBox].value)
|
|
255
|
+
assert_equal(matrix, form[:Matrix].value)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it "works for 0 degrees" do
|
|
259
|
+
check_rotation(-360, @widget[:Rect].width, @widget[:Rect].height, [1, 0, 0, 1, 0, 0])
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "works for 90 degrees" do
|
|
263
|
+
check_rotation(450, @widget[:Rect].height, @widget[:Rect].width, [0, 1, -1, 0, 0, 0])
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "works for 180 degrees" do
|
|
267
|
+
check_rotation(180, @widget[:Rect].width, @widget[:Rect].height, [0, -1, -1, 0, 0, 0])
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it "works for 270 degrees" do
|
|
271
|
+
check_rotation(-90, @widget[:Rect].height, @widget[:Rect].width, [0, -1, 1, 0, 0, 0])
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
234
275
|
it "creates the needed appearance streams" do
|
|
235
276
|
@widget[:AP][:N].delete(:Off)
|
|
236
277
|
@generator.create_appearances
|
|
@@ -292,61 +333,21 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
|
292
333
|
assert_equal(:Off, @widget[:AS])
|
|
293
334
|
end
|
|
294
335
|
|
|
295
|
-
it "set the print flag on the widgets" do
|
|
296
|
-
@generator.create_appearances
|
|
297
|
-
assert(@widget.flagged?(:print))
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
it "adjusts the /Rect if width is zero" do
|
|
301
|
-
@generator.create_appearances
|
|
302
|
-
assert_equal(12, @widget[:Rect].width)
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
it "adjusts the /Rect if height is zero" do
|
|
306
|
-
@generator.create_appearances
|
|
307
|
-
assert_equal(12, @widget[:Rect].height)
|
|
308
|
-
end
|
|
309
|
-
|
|
310
336
|
it "creates the needed appearance streams" do
|
|
311
337
|
@generator.create_appearances
|
|
312
338
|
assert_equal(:XObject, @widget[:AP][:N][:Off].type)
|
|
313
339
|
assert_equal(:XObject, @widget[:AP][:N][:radio].type)
|
|
314
340
|
end
|
|
341
|
+
end
|
|
315
342
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
@
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
[:set_device_gray_non_stroking_color, [1.0]],
|
|
322
|
-
[:append_rectangle, [0, 0, 12, 12]],
|
|
323
|
-
[:fill_path_non_zero],
|
|
324
|
-
[:append_rectangle, [0.5, 0.5, 11, 11]],
|
|
325
|
-
[:stroke_path], [:restore_graphics_state]])
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
it "creates the appearance stream according to the set value" do
|
|
329
|
-
@widget.marker_style(style: :check)
|
|
330
|
-
@generator.create_appearances
|
|
331
|
-
assert_operators(@widget[:AP][:N][:radio].stream,
|
|
332
|
-
[[:save_graphics_state],
|
|
333
|
-
[:set_device_gray_non_stroking_color, [1.0]],
|
|
334
|
-
[:append_rectangle, [0, 0, 12, 12]],
|
|
335
|
-
[:fill_path_non_zero],
|
|
336
|
-
[:append_rectangle, [0.5, 0.5, 11, 11]],
|
|
337
|
-
[:stroke_path], [:restore_graphics_state],
|
|
338
|
-
|
|
339
|
-
[:save_graphics_state],
|
|
340
|
-
[:set_font_and_size, [:F1, 10]],
|
|
341
|
-
[:begin_text],
|
|
342
|
-
[:set_text_matrix, [1, 0, 0, 1, 1.77, 2.545]],
|
|
343
|
-
[:show_text, ["4"]],
|
|
344
|
-
[:end_text],
|
|
345
|
-
[:restore_graphics_state]])
|
|
343
|
+
describe "push buttons" do
|
|
344
|
+
before do
|
|
345
|
+
@field.initialize_as_push_button
|
|
346
|
+
@widget = @field.create_widget(@page, Rect: [0, 0, 0, 0])
|
|
347
|
+
@generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
|
|
346
348
|
end
|
|
347
349
|
|
|
348
|
-
it "fails
|
|
349
|
-
@widget[:AP][:N].delete(:radio)
|
|
350
|
+
it "fails because it is not implemented yet" do
|
|
350
351
|
assert_raises(HexaPDF::Error) { @generator.create_appearances }
|
|
351
352
|
end
|
|
352
353
|
end
|
|
@@ -417,6 +418,37 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
|
417
418
|
assert_match(/test1/, form.contents)
|
|
418
419
|
end
|
|
419
420
|
|
|
421
|
+
describe "takes the rotation into account" do
|
|
422
|
+
before do
|
|
423
|
+
@widget[:Rect] = [0, 0, 100, 20]
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def check_rotation(angle, width, height, matrix)
|
|
427
|
+
@widget[:MK] = {R: angle}
|
|
428
|
+
@field[:V] = 'test'
|
|
429
|
+
@generator.create_appearances
|
|
430
|
+
form = @widget[:AP][:N]
|
|
431
|
+
assert_equal([0, 0, width, height], form[:BBox].value)
|
|
432
|
+
assert_equal(matrix, form[:Matrix].value)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
it "works for 0 degrees" do
|
|
436
|
+
check_rotation(-360, @widget[:Rect].width, @widget[:Rect].height, [1, 0, 0, 1, 0, 0])
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
it "works for 90 degrees" do
|
|
440
|
+
check_rotation(450, @widget[:Rect].height, @widget[:Rect].width, [0, 1, -1, 0, 0, 0])
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
it "works for 180 degrees" do
|
|
444
|
+
check_rotation(180, @widget[:Rect].width, @widget[:Rect].height, [0, -1, -1, 0, 0, 0])
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
it "works for 270 degrees" do
|
|
448
|
+
check_rotation(-90, @widget[:Rect].height, @widget[:Rect].width, [0, -1, 1, 0, 0, 0])
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
420
452
|
describe "font size calculation" do
|
|
421
453
|
before do
|
|
422
454
|
@widget[:Rect].height = 20
|
|
@@ -496,6 +528,53 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
|
496
528
|
end
|
|
497
529
|
end
|
|
498
530
|
|
|
531
|
+
describe "Javascript action AFNumber_Format" do
|
|
532
|
+
before do
|
|
533
|
+
@field.field_value = '1234567.898765'
|
|
534
|
+
@action = {S: :JavaScript, JS: ''}
|
|
535
|
+
@field[:AA] = {F: @action}
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def assert_format(arg_string, result, range)
|
|
539
|
+
@action[:JS] = "AFNumber_Format(#{arg_string});"
|
|
540
|
+
@generator.create_appearances
|
|
541
|
+
assert_operators(@widget[:AP][:N].stream, result, range: range)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
it "respects the set number of decimals" do
|
|
545
|
+
assert_format('0, 2, 0, 0, "E", false',
|
|
546
|
+
[:show_text, ["1.234.568E"]], 9)
|
|
547
|
+
assert_format('2, 2, 0, 0, "E", false',
|
|
548
|
+
[:show_text, ["1.234.567,90E"]], 9)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
it "respects the digit separator style" do
|
|
552
|
+
["1,234,567.90", "1234567.90", "1.234.567,90", "1234567,90"].each_with_index do |result, style|
|
|
553
|
+
assert_format("2, #{style}, 0, 0, \"\", false", [:show_text, [result]], 9)
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
it "respects the negative value styling" do
|
|
558
|
+
@field.field_value = '-1234567.898'
|
|
559
|
+
["-E1234567,90", "E1234567,90", "(E1234567,90)", "(E1234567,90)"].each_with_index do |result, style|
|
|
560
|
+
assert_format("2, 3, #{style}, 0, \"E\", true",
|
|
561
|
+
[[:set_device_rgb_non_stroking_color, [style % 2, 0.0, 0.0]],
|
|
562
|
+
[:begin_text],
|
|
563
|
+
[:set_text_matrix, [1, 0, 0, 1, 2, 3.240724]],
|
|
564
|
+
[:show_text, [result]]], 6..9)
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
it "respects the specified currency string and position" do
|
|
569
|
+
assert_format('2, 3, 0, 0, " E", false', [:show_text, ["1234567,90 E"]], 9)
|
|
570
|
+
assert_format('2, 3, 0, 0, "E ", true', [:show_text, ["E 1234567,90"]], 9)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
it "does nothing to the value if the Javascript method could not be determined " do
|
|
574
|
+
assert_format('2, 3, 0, 0, " E", false, a', [:show_text, ["1234567.898765"]], 8)
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
499
578
|
it "creates the /N appearance stream according to the set string" do
|
|
500
579
|
@field.field_value = 'Text'
|
|
501
580
|
@field.set_default_appearance_string(font_color: "red")
|