prawn 2.0.2 → 2.1.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/data/images/blend_modes_bottom_layer.jpg +0 -0
- data/data/images/blend_modes_top_layer.jpg +0 -0
- data/data/images/indexed_transparency.png +0 -0
- data/data/images/indexed_transparency_alpha.dat +0 -0
- data/data/images/indexed_transparency_color.dat +0 -0
- data/lib/prawn.rb +2 -1
- data/lib/prawn/document.rb +1 -0
- data/lib/prawn/document/internals.rb +10 -2
- data/lib/prawn/font.rb +14 -1
- data/lib/prawn/graphics.rb +2 -0
- data/lib/prawn/graphics/blend_mode.rb +64 -0
- data/lib/prawn/graphics/patterns.rb +52 -16
- data/lib/prawn/graphics/transformation.rb +3 -0
- data/lib/prawn/images/png.rb +43 -5
- data/lib/prawn/text/formatted/arranger.rb +25 -17
- data/lib/prawn/text/formatted/line_wrap.rb +2 -3
- data/lib/prawn/transformation_stack.rb +42 -0
- data/lib/prawn/version.rb +1 -1
- data/manual/graphics/blend_mode.rb +49 -0
- data/manual/graphics/graphics.rb +1 -0
- data/manual/graphics/soft_masks.rb +1 -1
- data/prawn.gemspec +4 -5
- data/spec/acceptance/png_spec.rb +35 -0
- data/spec/blend_mode_spec.rb +71 -0
- data/spec/document_spec.rb +72 -76
- data/spec/font_spec.rb +11 -11
- data/spec/formatted_text_arranger_spec.rb +178 -149
- data/spec/formatted_text_box_spec.rb +23 -23
- data/spec/graphics_spec.rb +67 -28
- data/spec/image_handler_spec.rb +7 -7
- data/spec/images_spec.rb +1 -1
- data/spec/png_spec.rb +26 -4
- data/spec/repeater_spec.rb +9 -9
- data/spec/spec_helper.rb +1 -4
- data/spec/text_at_spec.rb +2 -2
- data/spec/text_box_spec.rb +20 -16
- data/spec/text_spec.rb +8 -14
- data/spec/transformation_stack_spec.rb +63 -0
- data/spec/view_spec.rb +10 -10
- metadata +27 -31
- data/data/images/pal_bk.png +0 -0
- data/spec/acceptance/png.rb +0 -24
- data/spec/extensions/mocha.rb +0 -45
data/spec/font_spec.rb
CHANGED
@@ -265,11 +265,11 @@ describe "Document#page_fonts" do
|
|
265
265
|
end
|
266
266
|
|
267
267
|
def page_should_include_font(font)
|
268
|
-
expect(page_includes_font?(font)).to
|
268
|
+
expect(page_includes_font?(font)).to eq true
|
269
269
|
end
|
270
270
|
|
271
271
|
def page_should_not_include_font(font)
|
272
|
-
expect(page_includes_font?(font)).to
|
272
|
+
expect(page_includes_font?(font)).to eq false
|
273
273
|
end
|
274
274
|
end
|
275
275
|
|
@@ -313,13 +313,13 @@ describe "AFM fonts" do
|
|
313
313
|
it "should not modify the original string when normalize_encoding() is used" do
|
314
314
|
original = "Foo"
|
315
315
|
normalized = @times.normalize_encoding(original)
|
316
|
-
expect(original.equal?(normalized)).to
|
316
|
+
expect(original.equal?(normalized)).to eq false
|
317
317
|
end
|
318
318
|
|
319
319
|
it "should modify the original string when normalize_encoding!() is used" do
|
320
320
|
original = "Foo"
|
321
321
|
normalized = @times.normalize_encoding!(original)
|
322
|
-
expect(original.equal?(normalized)).to
|
322
|
+
expect(original.equal?(normalized)).to eq true
|
323
323
|
end
|
324
324
|
end
|
325
325
|
|
@@ -335,25 +335,25 @@ describe "#glyph_present" do
|
|
335
335
|
|
336
336
|
it "should return true when present in an AFM font" do
|
337
337
|
font = @pdf.find_font("Helvetica")
|
338
|
-
expect(font.glyph_present?("H")).to
|
338
|
+
expect(font.glyph_present?("H")).to eq true
|
339
339
|
end
|
340
340
|
|
341
341
|
it "should return false when absent in an AFM font" do
|
342
342
|
font = @pdf.find_font("Helvetica")
|
343
|
-
expect(font.glyph_present?("再")).to
|
343
|
+
expect(font.glyph_present?("再")).to eq false
|
344
344
|
end
|
345
345
|
|
346
346
|
it "should return true when present in a TTF font" do
|
347
347
|
font = @pdf.find_font("#{Prawn::DATADIR}/fonts/DejaVuSans.ttf")
|
348
|
-
expect(font.glyph_present?("H")).to
|
348
|
+
expect(font.glyph_present?("H")).to eq true
|
349
349
|
end
|
350
350
|
|
351
351
|
it "should return false when absent in a TTF font" do
|
352
352
|
font = @pdf.find_font("#{Prawn::DATADIR}/fonts/DejaVuSans.ttf")
|
353
|
-
expect(font.glyph_present?("再")).to
|
353
|
+
expect(font.glyph_present?("再")).to eq false
|
354
354
|
|
355
355
|
font = @pdf.find_font("#{Prawn::DATADIR}/fonts/gkai00mp.ttf")
|
356
|
-
expect(font.glyph_present?("€")).to
|
356
|
+
expect(font.glyph_present?("€")).to eq false
|
357
357
|
end
|
358
358
|
end
|
359
359
|
|
@@ -410,13 +410,13 @@ describe "TTF fonts" do
|
|
410
410
|
it "should not modify the original string when normalize_encoding() is used" do
|
411
411
|
original = "Foo"
|
412
412
|
normalized = @font.normalize_encoding(original)
|
413
|
-
expect(original.equal?(normalized)).to
|
413
|
+
expect(original.equal?(normalized)).to eq false
|
414
414
|
end
|
415
415
|
|
416
416
|
it "should modify the original string when normalize_encoding!() is used" do
|
417
417
|
original = "Foo"
|
418
418
|
normalized = @font.normalize_encoding!(original)
|
419
|
-
expect(original.equal?(normalized)).to
|
419
|
+
expect(original.equal?(normalized)).to eq true
|
420
420
|
end
|
421
421
|
end
|
422
422
|
end
|
@@ -2,167 +2,196 @@
|
|
2
2
|
|
3
3
|
require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
|
4
4
|
|
5
|
-
describe
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
arranger.format_array = array
|
38
|
-
arranger.preview_next_string
|
39
|
-
expect(arranger.consumed).to eq([])
|
40
|
-
end
|
41
|
-
it "should not consumed array" do
|
42
|
-
create_pdf
|
43
|
-
arranger = Prawn::Text::Formatted::Arranger.new(@pdf)
|
44
|
-
array = [{ :text => "hello" }]
|
45
|
-
arranger.format_array = array
|
46
|
-
expect(arranger.preview_next_string).to eq("hello")
|
47
|
-
end
|
48
|
-
end
|
49
|
-
describe "Core::Text::Formatted::Arranger#next_string" do
|
50
|
-
before(:each) do
|
51
|
-
create_pdf
|
52
|
-
@arranger = Prawn::Text::Formatted::Arranger.new(@pdf)
|
53
|
-
array = [{ :text => "hello " },
|
54
|
-
{ :text => "world how ", :styles => [:bold] },
|
55
|
-
{ :text => "are", :styles => [:bold, :italic] },
|
56
|
-
{ :text => " you?" }]
|
57
|
-
@arranger.format_array = array
|
58
|
-
end
|
59
|
-
it "should raise_error an error if called after a line was finalized and" \
|
60
|
-
" before a new line was initialized" do
|
61
|
-
@arranger.finalize_line
|
62
|
-
expect do
|
63
|
-
@arranger.next_string
|
64
|
-
end.to raise_error(RuntimeError)
|
65
|
-
end
|
66
|
-
it "should populate consumed array" do
|
67
|
-
while string = @arranger.next_string
|
5
|
+
describe Prawn::Text::Formatted::Arranger do
|
6
|
+
let(:document) { create_pdf }
|
7
|
+
subject { Prawn::Text::Formatted::Arranger.new document }
|
8
|
+
|
9
|
+
describe '#format_array' do
|
10
|
+
it 'populates the unconsumed array' do
|
11
|
+
array = [
|
12
|
+
{ text: 'hello ' },
|
13
|
+
{ text: 'world how ', styles: [:bold] },
|
14
|
+
{ text: 'are', styles: [:bold, :italic] },
|
15
|
+
{ text: ' you?' }
|
16
|
+
]
|
17
|
+
|
18
|
+
subject.format_array = array
|
19
|
+
|
20
|
+
expect(subject.unconsumed[0]).to eq(text: 'hello ')
|
21
|
+
expect(subject.unconsumed[1]).to eq(text: 'world how ', styles: [:bold])
|
22
|
+
expect(subject.unconsumed[2]).to eq(text: 'are', styles: [:bold, :italic])
|
23
|
+
expect(subject.unconsumed[3]).to eq(text: ' you?')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'splits newlins into their own elements' do
|
27
|
+
array = [
|
28
|
+
{ text: "\nhello\nworld" }
|
29
|
+
]
|
30
|
+
|
31
|
+
subject.format_array = array
|
32
|
+
|
33
|
+
expect(subject.unconsumed[0]).to eq(text: "\n")
|
34
|
+
expect(subject.unconsumed[1]).to eq(text: "hello")
|
35
|
+
expect(subject.unconsumed[2]).to eq(text: "\n")
|
36
|
+
expect(subject.unconsumed[3]).to eq(text: "world")
|
68
37
|
end
|
69
|
-
expect(@arranger.consumed[0]).to eq(:text => "hello ")
|
70
|
-
expect(@arranger.consumed[1]).to eq(:text => "world how ",
|
71
|
-
:styles => [:bold])
|
72
|
-
expect(@arranger.consumed[2]).to eq(:text => "are",
|
73
|
-
:styles => [:bold, :italic])
|
74
|
-
expect(@arranger.consumed[3]).to eq(:text => " you?")
|
75
38
|
end
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
39
|
+
|
40
|
+
describe '#preview_next_string' do
|
41
|
+
context 'with a formatted array' do
|
42
|
+
let(:array) { [{ text: 'hello' }] }
|
43
|
+
|
44
|
+
before do
|
45
|
+
subject.format_array = array
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'does not populate the consumed array' do
|
49
|
+
subject.preview_next_string
|
50
|
+
expect(subject.consumed).to eq([])
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'returns the text of the next unconsumed hash' do
|
54
|
+
expect(subject.preview_next_string).to eq("hello")
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'returns nil if there is no more unconsumed text' do
|
58
|
+
subject.next_string
|
59
|
+
expect(subject.preview_next_string).to be_nil
|
95
60
|
end
|
96
|
-
counter += 1
|
97
61
|
end
|
98
62
|
end
|
99
|
-
end
|
100
63
|
|
101
|
-
describe
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
64
|
+
describe '#next_string' do
|
65
|
+
let(:array) {
|
66
|
+
[
|
67
|
+
{ text: 'hello ' },
|
68
|
+
{ text: 'world how ', styles: [:bold] },
|
69
|
+
{ text: 'are', styles: [:bold, :italic] },
|
70
|
+
{ text: ' you?' }
|
71
|
+
]
|
72
|
+
}
|
73
|
+
|
74
|
+
before do
|
75
|
+
subject.format_array = array
|
111
76
|
end
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
it "should return the consumed fragments in order of consumption" \
|
117
|
-
" and update" do
|
118
|
-
create_pdf
|
119
|
-
arranger = Prawn::Text::Formatted::Arranger.new(@pdf)
|
120
|
-
array = [{ :text => "hello " },
|
121
|
-
{ :text => "world how ", :styles => [:bold] },
|
122
|
-
{ :text => "are", :styles => [:bold, :italic] },
|
123
|
-
{ :text => " you?" }]
|
124
|
-
arranger.format_array = array
|
125
|
-
while string = arranger.next_string
|
77
|
+
|
78
|
+
it 'raises RuntimeError if called after a line was finalized' do
|
79
|
+
subject.finalize_line
|
80
|
+
expect { subject.next_string }.to raise_error(RuntimeError)
|
126
81
|
end
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
array = [{ :text => "hello\nworld\n\n\nhow are you?" },
|
137
|
-
{ :text => "\n" },
|
138
|
-
{ :text => "\n" },
|
139
|
-
{ :text => "\n" },
|
140
|
-
{ :text => "" },
|
141
|
-
{ :text => "fine, thanks." },
|
142
|
-
{ :text => "" },
|
143
|
-
{ :text => "\n" },
|
144
|
-
{ :text => "" }]
|
145
|
-
arranger.format_array = array
|
146
|
-
while string = arranger.next_string
|
82
|
+
|
83
|
+
it 'populates the conumed array' do
|
84
|
+
while string = subject.next_string
|
85
|
+
end
|
86
|
+
|
87
|
+
expect(subject.consumed[0]).to eq(text: 'hello ')
|
88
|
+
expect(subject.consumed[1]).to eq(text: 'world how ', styles: [:bold])
|
89
|
+
expect(subject.consumed[2]).to eq(text: 'are', styles: [:bold, :italic])
|
90
|
+
expect(subject.consumed[3]).to eq(text: ' you?')
|
147
91
|
end
|
148
|
-
|
149
|
-
|
150
|
-
|
92
|
+
|
93
|
+
it 'populates the current_format_state array' do
|
94
|
+
string = subject.next_string
|
95
|
+
expect(subject.current_format_state).to eq({})
|
96
|
+
|
97
|
+
string = subject.next_string
|
98
|
+
expect(subject.current_format_state).to eq(:styles => [:bold])
|
99
|
+
|
100
|
+
string = subject.next_string
|
101
|
+
expect(subject.current_format_state).to eq(:styles => [:bold, :italic])
|
102
|
+
|
103
|
+
string = subject.next_string
|
104
|
+
expect(subject.current_format_state).to eq({})
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'returns the text of the newly consumed hash' do
|
108
|
+
expect(subject.next_string).to eq('hello ')
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'returns nil when there are no more unconsumed hashes' do
|
112
|
+
4.times do
|
113
|
+
subject.next_string
|
114
|
+
end
|
115
|
+
|
116
|
+
expect(subject.next_string).to be_nil
|
151
117
|
end
|
152
118
|
end
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
119
|
+
|
120
|
+
describe '#retrieve_fragment' do
|
121
|
+
context 'with a formatted array whos text is an empty string' do
|
122
|
+
let(:array) {
|
123
|
+
[
|
124
|
+
{ text: "hello\nworld\n\n\nhow are you?" },
|
125
|
+
{ text: "\n" },
|
126
|
+
{ text: "\n" },
|
127
|
+
{ text: "\n" },
|
128
|
+
{ text: "" },
|
129
|
+
{ text: "fine, thanks." },
|
130
|
+
{ text: "" },
|
131
|
+
{ text: "\n" },
|
132
|
+
{ text: "" }
|
133
|
+
]
|
134
|
+
}
|
135
|
+
|
136
|
+
before do
|
137
|
+
subject.format_array = array
|
138
|
+
|
139
|
+
while string = subject.next_string
|
140
|
+
end
|
141
|
+
|
142
|
+
subject.finalize_line
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'never returns a fragment whose text is an empty string' do
|
146
|
+
while fragment = subject.retrieve_fragment
|
147
|
+
expect(fragment.text).not_to be_empty
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'with formatted array' do
|
153
|
+
let(:array) {
|
154
|
+
[
|
155
|
+
{ text: 'hello ' },
|
156
|
+
{ text: 'world how ', styles: [:bold] },
|
157
|
+
{ text: 'are', styles: [:bold, :italic] },
|
158
|
+
{ text: ' you?' }
|
159
|
+
]
|
160
|
+
}
|
161
|
+
|
162
|
+
before do
|
163
|
+
subject.format_array = array
|
164
|
+
end
|
165
|
+
|
166
|
+
context 'after all strings have been consumed' do
|
167
|
+
before do
|
168
|
+
while string = subject.next_string
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'should raise RuntimeError an error if not finalized' do
|
173
|
+
expect { subject.retrieve_fragment }.to raise_error(RuntimeError)
|
174
|
+
end
|
175
|
+
|
176
|
+
context 'and finalized' do
|
177
|
+
before do
|
178
|
+
subject.finalize_line
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'returns the consumed fragments in order of consumption' do
|
182
|
+
expect(subject.retrieve_fragment.text).to eq("hello ")
|
183
|
+
expect(subject.retrieve_fragment.text).to eq("world how ")
|
184
|
+
expect(subject.retrieve_fragment.text).to eq("are")
|
185
|
+
expect(subject.retrieve_fragment.text).to eq(" you?")
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'does not alter the current font style' do
|
189
|
+
subject.retrieve_fragment
|
190
|
+
expect(subject.current_format_state[:styles]).to be_nil
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
162
194
|
end
|
163
|
-
arranger.finalize_line
|
164
|
-
arranger.retrieve_fragment
|
165
|
-
expect(arranger.current_format_state[:styles]).to be_nil
|
166
195
|
end
|
167
196
|
end
|
168
197
|
|
@@ -225,7 +225,7 @@ describe "Text::Formatted::Box" do
|
|
225
225
|
:document => @pdf,
|
226
226
|
:fallback_fonts => [])
|
227
227
|
|
228
|
-
box.
|
228
|
+
expect(box).to_not receive(:process_fallback_fonts)
|
229
229
|
box.render
|
230
230
|
end
|
231
231
|
|
@@ -236,7 +236,7 @@ describe "Text::Formatted::Box" do
|
|
236
236
|
|
237
237
|
@pdf.font("Kai")
|
238
238
|
|
239
|
-
box.
|
239
|
+
expect(box).to_not receive(:process_fallback_fonts)
|
240
240
|
box.render
|
241
241
|
end
|
242
242
|
end
|
@@ -339,10 +339,10 @@ describe "Text::Formatted::Box#render" do
|
|
339
339
|
it "should be able to perform fragment callbacks" do
|
340
340
|
create_pdf
|
341
341
|
callback_object = TestFragmentCallback.new("something", 7, :document => @pdf)
|
342
|
-
callback_object.
|
342
|
+
expect(callback_object).to receive(:render_behind).with(
|
343
343
|
kind_of(Prawn::Text::Formatted::Fragment)
|
344
344
|
)
|
345
|
-
callback_object.
|
345
|
+
expect(callback_object).to receive(:render_in_front).with(
|
346
346
|
kind_of(Prawn::Text::Formatted::Fragment)
|
347
347
|
)
|
348
348
|
array = [{ :text => "hello world " },
|
@@ -355,18 +355,18 @@ describe "Text::Formatted::Box#render" do
|
|
355
355
|
create_pdf
|
356
356
|
|
357
357
|
callback_object = TestFragmentCallback.new("something", 7, :document => @pdf)
|
358
|
-
callback_object.
|
358
|
+
expect(callback_object).to receive(:render_behind).with(
|
359
359
|
kind_of(Prawn::Text::Formatted::Fragment)
|
360
360
|
)
|
361
|
-
callback_object.
|
361
|
+
expect(callback_object).to receive(:render_in_front).with(
|
362
362
|
kind_of(Prawn::Text::Formatted::Fragment)
|
363
363
|
)
|
364
364
|
|
365
365
|
callback_object2 = TestFragmentCallback.new("something else", 14, :document => @pdf)
|
366
|
-
callback_object2.
|
366
|
+
expect(callback_object2).to receive(:render_behind).with(
|
367
367
|
kind_of(Prawn::Text::Formatted::Fragment)
|
368
368
|
)
|
369
|
-
callback_object2.
|
369
|
+
expect(callback_object2).to receive(:render_in_front).with(
|
370
370
|
kind_of(Prawn::Text::Formatted::Fragment)
|
371
371
|
)
|
372
372
|
|
@@ -485,12 +485,12 @@ describe "Text::Formatted::Box#render" do
|
|
485
485
|
end
|
486
486
|
it "should be able to add URL links" do
|
487
487
|
create_pdf
|
488
|
-
@pdf.
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
488
|
+
expect(@pdf).to receive(:link_annotation).with(kind_of(Array), :Border => [0, 0, 0],
|
489
|
+
:A => {
|
490
|
+
:Type => :Action,
|
491
|
+
:S => :URI,
|
492
|
+
:URI => "http://example.com"
|
493
|
+
})
|
494
494
|
array = [{ :text => "click " },
|
495
495
|
{ :text => "here", :link => "http://example.com" },
|
496
496
|
{ :text => " to visit" }]
|
@@ -499,8 +499,8 @@ describe "Text::Formatted::Box#render" do
|
|
499
499
|
end
|
500
500
|
it "should be able to add destination links" do
|
501
501
|
create_pdf
|
502
|
-
@pdf.
|
503
|
-
|
502
|
+
expect(@pdf).to receive(:link_annotation).with(kind_of(Array), :Border => [0, 0, 0],
|
503
|
+
:Dest => "ToC")
|
504
504
|
array = [{ :text => "Go to the " },
|
505
505
|
{ :text => "Table of Contents", :anchor => "ToC" }]
|
506
506
|
text_box = Prawn::Text::Formatted::Box.new(array, :document => @pdf)
|
@@ -508,13 +508,13 @@ describe "Text::Formatted::Box#render" do
|
|
508
508
|
end
|
509
509
|
it "should be able to add local actions" do
|
510
510
|
create_pdf
|
511
|
-
@pdf.
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
511
|
+
expect(@pdf).to receive(:link_annotation).with(kind_of(Array), :Border => [0, 0, 0],
|
512
|
+
:A => {
|
513
|
+
:Type => :Action,
|
514
|
+
:S => :Launch,
|
515
|
+
:F => "../example.pdf",
|
516
|
+
:NewWindow => true
|
517
|
+
})
|
518
518
|
array = [{ :text => "click " },
|
519
519
|
{ :text => "here", :local => "../example.pdf" },
|
520
520
|
{ :text => " to open a local file" }]
|