hexapdf 0.14.4 → 0.15.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/lib/hexapdf/cli/form.rb +30 -8
- data/lib/hexapdf/configuration.rb +18 -3
- data/lib/hexapdf/content/canvas.rb +1 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +16 -0
- data/lib/hexapdf/error.rb +4 -3
- data/lib/hexapdf/parser.rb +18 -6
- data/lib/hexapdf/revision.rb +16 -0
- data/lib/hexapdf/type/acro_form.rb +1 -0
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +29 -17
- data/lib/hexapdf/type/acro_form/button_field.rb +8 -4
- data/lib/hexapdf/type/acro_form/field.rb +1 -0
- data/lib/hexapdf/type/acro_form/form.rb +37 -0
- data/lib/hexapdf/type/acro_form/signature_field.rb +223 -0
- data/lib/hexapdf/type/annotation.rb +18 -9
- data/lib/hexapdf/type/annotations/widget.rb +3 -1
- data/lib/hexapdf/type/font_descriptor.rb +9 -2
- data/lib/hexapdf/type/page.rb +81 -0
- data/lib/hexapdf/utils/graphics_helpers.rb +4 -4
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/content/test_canvas.rb +21 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +27 -0
- data/test/hexapdf/test_parser.rb +23 -3
- data/test/hexapdf/test_revision.rb +21 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +21 -2
- data/test/hexapdf/type/acro_form/test_button_field.rb +13 -7
- data/test/hexapdf/type/acro_form/test_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_form.rb +46 -2
- data/test/hexapdf/type/acro_form/test_signature_field.rb +38 -0
- data/test/hexapdf/type/annotations/test_widget.rb +2 -0
- data/test/hexapdf/type/test_annotation.rb +24 -10
- data/test/hexapdf/type/test_font_descriptor.rb +7 -0
- data/test/hexapdf/type/test_page.rb +187 -49
- data/test/hexapdf/utils/test_graphics_helpers.rb +8 -0
- metadata +4 -2
@@ -47,18 +47,18 @@ module HexaPDF
|
|
47
47
|
#
|
48
48
|
# +rwidth+::
|
49
49
|
# The requested width. If +rheight+ is not specified, it is chosen so that the aspect
|
50
|
-
# ratio is maintained
|
50
|
+
# ratio is maintained. In case of +width+ begin zero, +height+ is used for the height.
|
51
51
|
#
|
52
52
|
# +rheight+::
|
53
53
|
# The requested height. If +rwidth+ is not specified, it is chosen so that the aspect
|
54
|
-
# ratio is maintained
|
54
|
+
# ratio is maintained. In case of +height+ begin zero, +width+ is used for the width.
|
55
55
|
def calculate_dimensions(width, height, rwidth: nil, rheight: nil)
|
56
56
|
if rwidth && rheight
|
57
57
|
[rwidth, rheight]
|
58
58
|
elsif rwidth
|
59
|
-
[rwidth, height * rwidth / width.to_f]
|
59
|
+
[rwidth, width == 0 ? height : height * rwidth / width.to_f]
|
60
60
|
elsif rheight
|
61
|
-
[width * rheight / height.to_f, rheight]
|
61
|
+
[height == 0 ? width : width * rheight / height.to_f, rheight]
|
62
62
|
else
|
63
63
|
[width, height]
|
64
64
|
end
|
data/lib/hexapdf/version.rb
CHANGED
@@ -831,6 +831,17 @@ describe HexaPDF::Content::Canvas do
|
|
831
831
|
[:restore_graphics_state]])
|
832
832
|
end
|
833
833
|
|
834
|
+
it "doesn't do anything if the image's width or height is zero" do
|
835
|
+
@image[:Width] = 0
|
836
|
+
@canvas.xobject(@image, at: [0, 0])
|
837
|
+
assert_operators(@page.contents, [])
|
838
|
+
|
839
|
+
@image[:Width] = 10
|
840
|
+
@image[:Height] = 0
|
841
|
+
@canvas.xobject(@image, at: [0, 0])
|
842
|
+
assert_operators(@page.contents, [])
|
843
|
+
end
|
844
|
+
|
834
845
|
it "correctly serializes the form with no options" do
|
835
846
|
@canvas.xobject(@form, at: [1, 2])
|
836
847
|
assert_operators(@page.contents, [[:save_graphics_state],
|
@@ -862,6 +873,16 @@ describe HexaPDF::Content::Canvas do
|
|
862
873
|
[:paint_xobject, [:XO1]],
|
863
874
|
[:restore_graphics_state]])
|
864
875
|
end
|
876
|
+
|
877
|
+
it "doesn't do anything if the form's width or height is zero" do
|
878
|
+
@form[:BBox] = [100, 50, 100, 200]
|
879
|
+
@canvas.xobject(@form, at: [0, 0])
|
880
|
+
assert_operators(@page.contents, [])
|
881
|
+
|
882
|
+
@form[:BBox] = [100, 50, 150, 50]
|
883
|
+
@canvas.xobject(@form, at: [0, 0])
|
884
|
+
assert_operators(@page.contents, [])
|
885
|
+
end
|
865
886
|
end
|
866
887
|
|
867
888
|
describe "character_spacing" do
|
@@ -292,4 +292,31 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
|
|
292
292
|
@handler.set_up_encryption(permissions: perms)
|
293
293
|
assert_equal([:copy_content, :modify_content], @handler.permissions.sort)
|
294
294
|
end
|
295
|
+
|
296
|
+
describe "handling of metadata streams" do
|
297
|
+
before do
|
298
|
+
@doc = HexaPDF::Document.new
|
299
|
+
@output = StringIO.new(''.b)
|
300
|
+
end
|
301
|
+
|
302
|
+
it "doesn't decrypt or encrypt a metadata stream if /EncryptMetadata is false" do
|
303
|
+
@doc.encrypt(encrypt_metadata: false)
|
304
|
+
@doc.catalog[:Metadata] = @doc.wrap({Type: :Metadata, Subtype: :XML}, stream: "HELLODATA")
|
305
|
+
@doc.write(@output)
|
306
|
+
assert_match(/stream\nHELLODATA\nendstream/, @output.string)
|
307
|
+
|
308
|
+
doc = HexaPDF::Document.new(io: @output)
|
309
|
+
assert_equal('HELLODATA', doc.catalog[:Metadata].stream)
|
310
|
+
end
|
311
|
+
|
312
|
+
it "doesn't modify decryption/encryption for metadata streams if /V is not 4 or 5" do
|
313
|
+
@doc.encrypt(encrypt_metadata: false, algorithm: :arc4)
|
314
|
+
@doc.catalog[:Metadata] = @doc.wrap({Type: :Metadata, Subtype: :XML}, stream: "HELLODATA")
|
315
|
+
@doc.write(@output)
|
316
|
+
refute_match(/stream\nHELLODATA\nendstream/, @output.string)
|
317
|
+
|
318
|
+
doc = HexaPDF::Document.new(io: @output)
|
319
|
+
assert_equal('HELLODATA', doc.catalog[:Metadata].stream)
|
320
|
+
end
|
321
|
+
end
|
295
322
|
end
|
data/test/hexapdf/test_parser.rb
CHANGED
@@ -50,7 +50,8 @@ describe HexaPDF::Parser do
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def create_parser(str)
|
53
|
-
@
|
53
|
+
@parse_io = StringIO.new(str)
|
54
|
+
@parser = HexaPDF::Parser.new(@parse_io, @document)
|
54
55
|
end
|
55
56
|
|
56
57
|
describe "parse_indirect_object" do
|
@@ -94,6 +95,12 @@ describe HexaPDF::Parser do
|
|
94
95
|
assert_equal('12', TestHelper.collector(stream.fiber))
|
95
96
|
end
|
96
97
|
|
98
|
+
it "handles keyword stream followed by space and CR LF" do
|
99
|
+
create_parser("1 0 obj<</Length 2>> stream \r\n12\nendstream endobj")
|
100
|
+
*, stream = @parser.parse_indirect_object
|
101
|
+
assert_equal('12', TestHelper.collector(stream.fiber))
|
102
|
+
end
|
103
|
+
|
97
104
|
it "handles invalid indirect object value consisting of number followed by endobj without space" do
|
98
105
|
create_parser("1 0 obj 749endobj")
|
99
106
|
object, * = @parser.parse_indirect_object
|
@@ -166,7 +173,13 @@ describe HexaPDF::Parser do
|
|
166
173
|
it "fails if keyword stream is followed by space and CR or LF instead of LF or CR/LF" do
|
167
174
|
create_parser("1 0 obj<</Length 2>> stream \n12\nendstream endobj")
|
168
175
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
169
|
-
assert_match(/
|
176
|
+
assert_match(/followed by space instead/, exp.message)
|
177
|
+
end
|
178
|
+
|
179
|
+
it "fails if keyword stream is followed by space and CR LF instead of LF or CR/LF" do
|
180
|
+
create_parser("1 0 obj<</Length 2>> stream \r\n12\nendstream endobj")
|
181
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
182
|
+
assert_match(/followed by space instead/, exp.message)
|
170
183
|
end
|
171
184
|
|
172
185
|
it "fails for numbers followed by endobj without space" do
|
@@ -511,6 +524,13 @@ describe HexaPDF::Parser do
|
|
511
524
|
assert_match(/not a cross-reference stream/, exp.message)
|
512
525
|
end
|
513
526
|
|
527
|
+
it "fails if the cross-reference stream is missing data" do
|
528
|
+
@parse_io.string[287..288] = ''
|
529
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.load_revision(212) }
|
530
|
+
assert_match(/missing data/, exp.message)
|
531
|
+
assert_equal(212, exp.pos)
|
532
|
+
end
|
533
|
+
|
514
534
|
it "fails on strict parsing if the cross-reference stream doesn't contain an entry for itself" do
|
515
535
|
@document.config['parser.on_correctable_error'] = proc { true }
|
516
536
|
create_parser("2 0 obj\n<</Type/XRef/Length 3/W [1 1 1]/Size 1>>" \
|
@@ -578,7 +598,7 @@ describe HexaPDF::Parser do
|
|
578
598
|
end
|
579
599
|
|
580
600
|
it "uses the first trailer in case of a linearized file" do
|
581
|
-
create_parser("
|
601
|
+
create_parser("1 0 obj\n<</Linearized true>>\nendobj\ntrailer <</Size 1/Prev 342>>\ntrailer <</Size 2>>")
|
582
602
|
assert_equal({Size: 1}, @parser.reconstructed_revision.trailer.value)
|
583
603
|
end
|
584
604
|
|
@@ -105,6 +105,27 @@ describe HexaPDF::Revision do
|
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
108
|
+
describe "update" do
|
109
|
+
before do
|
110
|
+
@rev.add(@obj)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "updates the object if it has the same data instance" do
|
114
|
+
x = HexaPDF::Object.new(@obj.data)
|
115
|
+
y = @rev.update(x)
|
116
|
+
assert_same(x, y)
|
117
|
+
refute_same(x, @obj)
|
118
|
+
assert_same(x, @rev.object(@ref))
|
119
|
+
end
|
120
|
+
|
121
|
+
it "doesn't update the object if it refers to a different data instance" do
|
122
|
+
x = HexaPDF::Object.new(:value, oid: 5)
|
123
|
+
assert_nil(@rev.update(x))
|
124
|
+
x.data.oid = 1
|
125
|
+
assert_nil(@rev.update(x))
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
108
129
|
describe "delete" do
|
109
130
|
before do
|
110
131
|
@rev.add(@obj)
|
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.15.4)>>
|
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.15.4)>>
|
76
76
|
endobj
|
77
77
|
2 0 obj
|
78
78
|
<</Length 10>>stream
|
@@ -748,17 +748,36 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
748
748
|
|
749
749
|
describe "font resolution in case the referenced font is not usable" do
|
750
750
|
before do
|
751
|
-
@doc.config['acro_form.fallback_font'] = ['Times', {variant: :
|
751
|
+
@doc.config['acro_form.fallback_font'] = ['Times', {variant: :italic}]
|
752
752
|
@field[:V] = 'Test'
|
753
753
|
end
|
754
754
|
|
755
755
|
it "uses the fallback font if the font is not usable" do
|
756
756
|
def (@form.default_resources.font(:F1)).font_wrapper; nil; end
|
757
757
|
@generator.create_appearances
|
758
|
-
assert_equal(:'Times-
|
758
|
+
assert_equal(:'Times-Italic', @widget[:AP][:N][:Resources][:Font][:F2][:BaseFont])
|
759
759
|
end
|
760
760
|
|
761
761
|
it "uses the fallback font if the font is not found" do
|
762
|
+
@form.default_resources[:Font].delete(:F1)
|
763
|
+
@generator.create_appearances
|
764
|
+
assert_equal(:'Times-Italic', @widget[:AP][:N][:Resources][:Font][:F1][:BaseFont])
|
765
|
+
end
|
766
|
+
|
767
|
+
it "respects a simple fallback font name" do
|
768
|
+
@doc.config['acro_form.fallback_font'] = 'Times'
|
769
|
+
@form.default_resources[:Font].delete(:F1)
|
770
|
+
@generator.create_appearances
|
771
|
+
assert_equal(:'Times-Roman', @widget[:AP][:N][:Resources][:Font][:F1][:BaseFont])
|
772
|
+
end
|
773
|
+
|
774
|
+
it "respects a fallback font callable object" do
|
775
|
+
field = @field
|
776
|
+
@doc.config['acro_form.fallback_font'] = proc do |field_arg, font_arg|
|
777
|
+
assert_same(field.data, field_arg.data)
|
778
|
+
assert_nil(font_arg)
|
779
|
+
'Times'
|
780
|
+
end
|
762
781
|
@form.default_resources[:Font].delete(:F1)
|
763
782
|
@generator.create_appearances
|
764
783
|
assert_equal(:'Times-Roman', @widget[:AP][:N][:Resources][:Font][:F1][:BaseFont])
|
@@ -225,20 +225,26 @@ describe HexaPDF::Type::AcroForm::ButtonField do
|
|
225
225
|
it "won't generate appearances if they already exist" do
|
226
226
|
widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
|
227
227
|
@field.create_appearances
|
228
|
-
yes = widget.
|
229
|
-
off = widget.
|
230
|
-
widget.appearance.normal_appearance.delete(:Off)
|
228
|
+
yes = widget.appearance_dict.normal_appearance[:Yes]
|
229
|
+
off = widget.appearance_dict.normal_appearance[:Off]
|
231
230
|
@field.create_appearances
|
232
|
-
assert_same(yes, widget.
|
233
|
-
|
231
|
+
assert_same(yes, widget.appearance_dict.normal_appearance[:Yes])
|
232
|
+
assert_same(off, widget.appearance_dict.normal_appearance[:Off])
|
233
|
+
|
234
|
+
@field.delete_widget(widget)
|
235
|
+
@field.flag(:push_button)
|
236
|
+
widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0], AP: {N: @doc.wrap({}, stream: '')})
|
237
|
+
appearance = widget.appearance_dict.normal_appearance
|
238
|
+
@field.create_appearances
|
239
|
+
assert_same(appearance, widget.appearance_dict.normal_appearance)
|
234
240
|
end
|
235
241
|
|
236
242
|
it "always generates appearances if force is true" do
|
237
243
|
widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
|
238
244
|
@field.create_appearances
|
239
|
-
yes = widget.
|
245
|
+
yes = widget.appearance_dict.normal_appearance[:Yes]
|
240
246
|
@field.create_appearances(force: true)
|
241
|
-
refute_same(yes, widget.
|
247
|
+
refute_same(yes, widget.appearance_dict.normal_appearance[:Yes])
|
242
248
|
end
|
243
249
|
|
244
250
|
it "fails for unsupported button types" do
|
@@ -200,9 +200,14 @@ describe HexaPDF::Type::AcroForm::Field do
|
|
200
200
|
|
201
201
|
it "deletes the widget if it is embedded" do
|
202
202
|
widget = @field.create_widget(@page)
|
203
|
+
@doc.revisions.current.update(widget)
|
204
|
+
assert_same(widget, @doc.object(widget))
|
205
|
+
refute_same(@field, @doc.object(@field))
|
206
|
+
|
203
207
|
@field.delete_widget(widget)
|
204
208
|
refute(@field.key?(:Subtype))
|
205
209
|
assert(@page[:Annots].empty?)
|
210
|
+
assert_same(@field, @doc.object(@field))
|
206
211
|
end
|
207
212
|
|
208
213
|
it "deletes the widget if it is not embedded" do
|
@@ -254,8 +254,8 @@ describe HexaPDF::Type::AcroForm::Form do
|
|
254
254
|
|
255
255
|
it "creates the appearances of all field widgets if necessary" do
|
256
256
|
@acro_form.create_appearances
|
257
|
-
assert(@tf.each_widget.all? {|w| w.
|
258
|
-
assert(@cb.each_widget.all? {|w| w.
|
257
|
+
assert(@tf.each_widget.all? {|w| w.appearance_dict.normal_appearance.kind_of?(HexaPDF::Stream) })
|
258
|
+
assert(@cb.each_widget.all? {|w| w.appearance_dict.normal_appearance[:Yes].kind_of?(HexaPDF::Stream) })
|
259
259
|
end
|
260
260
|
|
261
261
|
it "force the creation of appearances if force is true" do
|
@@ -268,6 +268,50 @@ describe HexaPDF::Type::AcroForm::Form do
|
|
268
268
|
end
|
269
269
|
end
|
270
270
|
|
271
|
+
describe "flatten" do
|
272
|
+
before do
|
273
|
+
@acro_form.root_fields << @doc.wrap({T: 'test'})
|
274
|
+
@tf = @acro_form.create_text_field('textfields')
|
275
|
+
@tf.set_default_appearance_string
|
276
|
+
@tf[:V] = 'Test'
|
277
|
+
@tf.create_widget(@doc.pages.add)
|
278
|
+
@cb = @acro_form.create_check_box('test.checkbox')
|
279
|
+
@cb.create_widget(@doc.pages[0])
|
280
|
+
@cb.create_widget(@doc.pages.add)
|
281
|
+
end
|
282
|
+
|
283
|
+
it "creates the missing appearances if instructed to do so" do
|
284
|
+
assert_equal(3, @acro_form.flatten(create_appearances: false).size)
|
285
|
+
assert_equal(0, @acro_form.flatten(create_appearances: true).size)
|
286
|
+
end
|
287
|
+
|
288
|
+
it "flattens the whole interactive form" do
|
289
|
+
result = @acro_form.flatten
|
290
|
+
assert(result.empty?)
|
291
|
+
assert(@tf.null?)
|
292
|
+
assert(@cb.null?)
|
293
|
+
assert(@acro_form.null?)
|
294
|
+
refute(@doc.catalog.key?(:AcroForm))
|
295
|
+
end
|
296
|
+
|
297
|
+
it "flattens the given fields" do
|
298
|
+
result = @acro_form.flatten(fields: [@cb])
|
299
|
+
assert(result.empty?)
|
300
|
+
assert(@cb.null?)
|
301
|
+
refute(@tf.null?)
|
302
|
+
refute(@acro_form.null?)
|
303
|
+
assert(@doc.catalog.key?(:AcroForm))
|
304
|
+
end
|
305
|
+
|
306
|
+
it "doesn't delete the form object if not all fields were flattened" do
|
307
|
+
@acro_form.create_appearances
|
308
|
+
@tf.delete(:AP)
|
309
|
+
result = @acro_form.flatten(create_appearances: false)
|
310
|
+
assert_equal(1, result.size)
|
311
|
+
assert(@doc.catalog.key?(:AcroForm))
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
271
315
|
describe "perform_validation" do
|
272
316
|
it "checks whether the /DR field is available when /DA is set" do
|
273
317
|
@acro_form[:DA] = 'test'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'hexapdf/document'
|
5
|
+
require 'hexapdf/type/acro_form/signature_field'
|
6
|
+
|
7
|
+
describe HexaPDF::Type::AcroForm::SignatureField::LockDictionary do
|
8
|
+
it "validates the presence of the /Fields key" do
|
9
|
+
doc = HexaPDF::Document.new
|
10
|
+
obj = HexaPDF::Type::AcroForm::SignatureField::LockDictionary.new({Action: :All}, document: doc)
|
11
|
+
assert(obj.validate)
|
12
|
+
obj[:Action] = :Include
|
13
|
+
refute(obj.validate)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe HexaPDF::Type::AcroForm::SignatureField do
|
18
|
+
before do
|
19
|
+
@doc = HexaPDF::Document.new
|
20
|
+
@field = @doc.wrap({}, type: :XXAcroFormField, subtype: :Sig)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "sets the field value" do
|
24
|
+
@field.field_value = {Empty: :True}
|
25
|
+
assert_equal({Empty: :True}, @field[:V].value)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "gets the field value" do
|
29
|
+
@field[:V] = {Empty: :True}
|
30
|
+
assert_equal({Empty: :True}, @field.field_value.value)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "validates the value of the /FT field" do
|
34
|
+
refute(@field.validate(auto_correct: false))
|
35
|
+
assert(@field.validate)
|
36
|
+
assert_equal(:Sig, @field.field_type)
|
37
|
+
end
|
38
|
+
end
|
@@ -56,6 +56,8 @@ describe HexaPDF::Type::Annotations::Widget do
|
|
56
56
|
|
57
57
|
describe "background_color" do
|
58
58
|
it "returns the current background color" do
|
59
|
+
assert_nil(@widget.background_color)
|
60
|
+
@widget[:MK] = {BG: []}
|
59
61
|
assert_nil(@widget.background_color)
|
60
62
|
@widget[:MK] = {BG: [1]}
|
61
63
|
assert_equal([1], @widget.background_color.components)
|
@@ -40,19 +40,33 @@ describe HexaPDF::Type::Annotation do
|
|
40
40
|
|
41
41
|
it "returns the appearance dictionary" do
|
42
42
|
@annot[:AP] = :yes
|
43
|
-
assert_equal(:yes, @annot.
|
43
|
+
assert_equal(:yes, @annot.appearance_dict)
|
44
44
|
end
|
45
45
|
|
46
|
-
it "
|
47
|
-
|
46
|
+
it "returns the appearance stream of the given type" do
|
47
|
+
assert_nil(@annot.appearance)
|
48
|
+
|
48
49
|
@annot[:AP] = {N: {}}
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
@annot[:AP][:N] =
|
53
|
-
|
54
|
-
|
55
|
-
|
50
|
+
assert_nil(@annot.appearance)
|
51
|
+
|
52
|
+
stream = @doc.wrap({}, stream: '')
|
53
|
+
@annot[:AP][:N] = stream
|
54
|
+
assert_nil(@annot.appearance)
|
55
|
+
|
56
|
+
stream[:BBox] = [1, 2, 3, 4]
|
57
|
+
appearance = @annot.appearance
|
58
|
+
assert_same(stream.data, appearance.data)
|
59
|
+
assert_equal(:Form, appearance[:Subtype])
|
60
|
+
|
61
|
+
@annot[:AP][:N] = {X: {}}
|
62
|
+
assert_nil(@annot.appearance)
|
63
|
+
|
64
|
+
@annot[:AS] = :X
|
65
|
+
@annot[:AP][:N][:X] = stream
|
66
|
+
assert_same(stream.data, @annot.appearance.data)
|
67
|
+
|
68
|
+
@annot[:AP][:D] = {X: stream}
|
69
|
+
assert_same(stream.data, @annot.appearance(:down).data)
|
56
70
|
end
|
57
71
|
|
58
72
|
describe "flags" do
|