origamindee 3.0.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 +7 -0
- data/CHANGELOG.md +89 -0
- data/COPYING.LESSER +165 -0
- data/README.md +131 -0
- data/bin/config/pdfcop.conf.yml +236 -0
- data/bin/pdf2pdfa +87 -0
- data/bin/pdf2ruby +333 -0
- data/bin/pdfcop +476 -0
- data/bin/pdfdecompress +97 -0
- data/bin/pdfdecrypt +91 -0
- data/bin/pdfencrypt +113 -0
- data/bin/pdfexplode +223 -0
- data/bin/pdfextract +277 -0
- data/bin/pdfmetadata +143 -0
- data/bin/pdfsh +12 -0
- data/bin/shell/console.rb +128 -0
- data/bin/shell/hexdump.rb +59 -0
- data/bin/shell/irbrc +69 -0
- data/examples/README.md +34 -0
- data/examples/attachments/attachment.rb +38 -0
- data/examples/attachments/nested_document.rb +51 -0
- data/examples/encryption/encryption.rb +28 -0
- data/examples/events/events.rb +72 -0
- data/examples/flash/flash.rb +37 -0
- data/examples/flash/helloworld.swf +0 -0
- data/examples/forms/javascript.rb +54 -0
- data/examples/forms/xfa.rb +115 -0
- data/examples/javascript/hello_world.rb +22 -0
- data/examples/javascript/js_emulation.rb +54 -0
- data/examples/loop/goto.rb +32 -0
- data/examples/loop/named.rb +33 -0
- data/examples/signature/signature.rb +65 -0
- data/examples/uri/javascript.rb +56 -0
- data/examples/uri/open-uri.rb +21 -0
- data/examples/uri/submitform.rb +47 -0
- data/lib/origami/3d.rb +364 -0
- data/lib/origami/acroform.rb +321 -0
- data/lib/origami/actions.rb +318 -0
- data/lib/origami/annotations.rb +711 -0
- data/lib/origami/array.rb +242 -0
- data/lib/origami/boolean.rb +90 -0
- data/lib/origami/catalog.rb +418 -0
- data/lib/origami/collections.rb +144 -0
- data/lib/origami/compound.rb +161 -0
- data/lib/origami/destinations.rb +252 -0
- data/lib/origami/dictionary.rb +192 -0
- data/lib/origami/encryption.rb +1084 -0
- data/lib/origami/extensions/fdf.rb +347 -0
- data/lib/origami/extensions/ppklite.rb +422 -0
- data/lib/origami/filespec.rb +197 -0
- data/lib/origami/filters/ascii.rb +211 -0
- data/lib/origami/filters/ccitt/tables.rb +267 -0
- data/lib/origami/filters/ccitt.rb +357 -0
- data/lib/origami/filters/crypt.rb +38 -0
- data/lib/origami/filters/dct.rb +54 -0
- data/lib/origami/filters/flate.rb +69 -0
- data/lib/origami/filters/jbig2.rb +57 -0
- data/lib/origami/filters/jpx.rb +47 -0
- data/lib/origami/filters/lzw.rb +170 -0
- data/lib/origami/filters/predictors.rb +292 -0
- data/lib/origami/filters/runlength.rb +129 -0
- data/lib/origami/filters.rb +364 -0
- data/lib/origami/font.rb +196 -0
- data/lib/origami/functions.rb +79 -0
- data/lib/origami/graphics/colors.rb +230 -0
- data/lib/origami/graphics/instruction.rb +98 -0
- data/lib/origami/graphics/path.rb +182 -0
- data/lib/origami/graphics/patterns.rb +174 -0
- data/lib/origami/graphics/render.rb +62 -0
- data/lib/origami/graphics/state.rb +149 -0
- data/lib/origami/graphics/text.rb +225 -0
- data/lib/origami/graphics/xobject.rb +918 -0
- data/lib/origami/graphics.rb +38 -0
- data/lib/origami/header.rb +75 -0
- data/lib/origami/javascript.rb +713 -0
- data/lib/origami/linearization.rb +330 -0
- data/lib/origami/metadata.rb +172 -0
- data/lib/origami/name.rb +135 -0
- data/lib/origami/null.rb +65 -0
- data/lib/origami/numeric.rb +181 -0
- data/lib/origami/obfuscation.rb +245 -0
- data/lib/origami/object.rb +760 -0
- data/lib/origami/optionalcontent.rb +183 -0
- data/lib/origami/outline.rb +54 -0
- data/lib/origami/outputintents.rb +85 -0
- data/lib/origami/page.rb +722 -0
- data/lib/origami/parser.rb +269 -0
- data/lib/origami/parsers/fdf.rb +56 -0
- data/lib/origami/parsers/pdf/lazy.rb +176 -0
- data/lib/origami/parsers/pdf/linear.rb +122 -0
- data/lib/origami/parsers/pdf.rb +118 -0
- data/lib/origami/parsers/ppklite.rb +57 -0
- data/lib/origami/pdf.rb +1108 -0
- data/lib/origami/reference.rb +134 -0
- data/lib/origami/signature.rb +702 -0
- data/lib/origami/stream.rb +705 -0
- data/lib/origami/string.rb +444 -0
- data/lib/origami/template/patterns.rb +56 -0
- data/lib/origami/template/widgets.rb +151 -0
- data/lib/origami/trailer.rb +190 -0
- data/lib/origami/tree.rb +62 -0
- data/lib/origami/version.rb +23 -0
- data/lib/origami/webcapture.rb +100 -0
- data/lib/origami/xfa/config.rb +453 -0
- data/lib/origami/xfa/connectionset.rb +146 -0
- data/lib/origami/xfa/datasets.rb +49 -0
- data/lib/origami/xfa/localeset.rb +42 -0
- data/lib/origami/xfa/package.rb +59 -0
- data/lib/origami/xfa/pdf.rb +73 -0
- data/lib/origami/xfa/signature.rb +42 -0
- data/lib/origami/xfa/sourceset.rb +43 -0
- data/lib/origami/xfa/stylesheet.rb +44 -0
- data/lib/origami/xfa/template.rb +1691 -0
- data/lib/origami/xfa/xdc.rb +42 -0
- data/lib/origami/xfa/xfa.rb +146 -0
- data/lib/origami/xfa/xfdf.rb +43 -0
- data/lib/origami/xfa/xmpmeta.rb +43 -0
- data/lib/origami/xfa.rb +62 -0
- data/lib/origami/xreftable.rb +557 -0
- data/lib/origami.rb +47 -0
- data/test/dataset/calc.pdf +85 -0
- data/test/dataset/crypto.pdf +36 -0
- data/test/dataset/empty.pdf +49 -0
- data/test/test_actions.rb +27 -0
- data/test/test_annotations.rb +68 -0
- data/test/test_forms.rb +30 -0
- data/test/test_native_types.rb +83 -0
- data/test/test_object_tree.rb +33 -0
- data/test/test_pages.rb +60 -0
- data/test/test_pdf.rb +20 -0
- data/test/test_pdf_attachment.rb +34 -0
- data/test/test_pdf_create.rb +24 -0
- data/test/test_pdf_encrypt.rb +102 -0
- data/test/test_pdf_parse.rb +134 -0
- data/test/test_pdf_parse_lazy.rb +69 -0
- data/test/test_pdf_sign.rb +97 -0
- data/test/test_streams.rb +184 -0
- data/test/test_xrefs.rb +67 -0
- metadata +280 -0
@@ -0,0 +1,918 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
This file is part of Origami, PDF manipulation framework for Ruby
|
4
|
+
Copyright (C) 2016 Guillaume Delugré.
|
5
|
+
|
6
|
+
Origami is free software: you can redistribute it and/or modify
|
7
|
+
it under the terms of the GNU Lesser General Public License as published by
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
9
|
+
(at your option) any later version.
|
10
|
+
|
11
|
+
Origami is distributed in the hope that it will be useful,
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
GNU Lesser General Public License for more details.
|
15
|
+
|
16
|
+
You should have received a copy of the GNU Lesser General Public License
|
17
|
+
along with Origami. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
=end
|
20
|
+
|
21
|
+
module Origami
|
22
|
+
|
23
|
+
#
|
24
|
+
# A class representing a Stream containing the contents of a Page.
|
25
|
+
#
|
26
|
+
class ContentStream < Stream
|
27
|
+
|
28
|
+
DEFAULT_SIZE = 12
|
29
|
+
DEFAULT_FONT = :F1
|
30
|
+
DEFAULT_LEADING = 20
|
31
|
+
DEFAULT_STROKE_COLOR = Graphics::Color::GrayScale.new(0.0)
|
32
|
+
DEFAULT_FILL_COLOR = Graphics::Color::GrayScale.new(1.0)
|
33
|
+
DEFAULT_LINECAP = Graphics::LineCapStyle::BUTT_CAP
|
34
|
+
DEFAULT_LINEJOIN = Graphics::LineJoinStyle::MITER_JOIN
|
35
|
+
DEFAULT_DASHPATTERN = Graphics::DashPattern.new([], 0)
|
36
|
+
DEFAULT_LINEWIDTH = 1.0
|
37
|
+
|
38
|
+
attr_accessor :canvas
|
39
|
+
|
40
|
+
def initialize(data = "", dictionary = {})
|
41
|
+
super
|
42
|
+
|
43
|
+
@instructions = nil
|
44
|
+
@canvas = Graphics::DummyCanvas.new
|
45
|
+
end
|
46
|
+
|
47
|
+
def render(engine)
|
48
|
+
load!
|
49
|
+
|
50
|
+
@instructions.each do |instruction|
|
51
|
+
instruction.render(engine)
|
52
|
+
end
|
53
|
+
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def pre_build #:nodoc:
|
58
|
+
unless @instructions.nil?
|
59
|
+
if @canvas.gs.text_state.is_in_text_object?
|
60
|
+
@instructions << PDF::Instruction.new('ET').render(@canvas)
|
61
|
+
end
|
62
|
+
|
63
|
+
@data = @instructions.join
|
64
|
+
end
|
65
|
+
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
def instructions
|
70
|
+
load!
|
71
|
+
|
72
|
+
@instructions
|
73
|
+
end
|
74
|
+
|
75
|
+
def draw_image(name, attr = {})
|
76
|
+
load!
|
77
|
+
|
78
|
+
x, y = attr[:x], attr[:y]
|
79
|
+
|
80
|
+
@instructions << PDF::Instruction.new('q')
|
81
|
+
@instructions << PDF::Instruction.new('cm', (attr[:w] || 300), 0, 0, (attr[:h] || 300), x, y)
|
82
|
+
@instructions << PDF::Instruction.new('Do', name)
|
83
|
+
@instructions << PDF::Instruction.new('Q')
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Draw a straight line from the point at coord _from_, to the point at coord _to_.
|
88
|
+
#
|
89
|
+
def draw_line(from, to, attr = {})
|
90
|
+
draw_polygon([from, to], attr)
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Draw a polygon from a array of coordinates.
|
95
|
+
#
|
96
|
+
def draw_polygon(coords = [], attr = {})
|
97
|
+
load!
|
98
|
+
|
99
|
+
stroke_color = attr.fetch(:stroke_color, DEFAULT_STROKE_COLOR)
|
100
|
+
fill_color = attr.fetch(:fill_color, DEFAULT_FILL_COLOR)
|
101
|
+
line_cap = attr.fetch(:line_cap, DEFAULT_LINECAP)
|
102
|
+
line_join = attr.fetch(:line_join, DEFAULT_LINEJOIN)
|
103
|
+
line_width = attr.fetch(:line_width, DEFAULT_LINEWIDTH)
|
104
|
+
dash_pattern = attr.fetch(:dash, DEFAULT_DASHPATTERN)
|
105
|
+
|
106
|
+
stroke = attr[:stroke].nil? ? true : attr[:stroke]
|
107
|
+
fill = attr[:fill].nil? ? false : attr[:fill]
|
108
|
+
|
109
|
+
stroke = true if fill == false and stroke == false
|
110
|
+
|
111
|
+
set_fill_color(fill_color) if fill
|
112
|
+
set_stroke_color(stroke_color) if stroke
|
113
|
+
set_line_width(line_width)
|
114
|
+
set_line_cap(line_cap)
|
115
|
+
set_line_join(line_join)
|
116
|
+
set_dash_pattern(dash_pattern)
|
117
|
+
|
118
|
+
if @canvas.gs.text_state.is_in_text_object?
|
119
|
+
@instructions << PDF::Instruction.new('ET').render(@canvas)
|
120
|
+
end
|
121
|
+
|
122
|
+
unless coords.size < 1
|
123
|
+
x,y = coords.slice!(0)
|
124
|
+
@instructions << PDF::Instruction.new('m',x,y).render(@canvas)
|
125
|
+
|
126
|
+
coords.each do |px,py|
|
127
|
+
@instructions << PDF::Instruction.new('l',px,py).render(@canvas)
|
128
|
+
end
|
129
|
+
|
130
|
+
@instructions << (i =
|
131
|
+
if stroke and not fill
|
132
|
+
PDF::Instruction.new('s')
|
133
|
+
elsif fill and not stroke
|
134
|
+
PDF::Instruction.new('f')
|
135
|
+
elsif fill and stroke
|
136
|
+
PDF::Instruction.new('b')
|
137
|
+
end
|
138
|
+
)
|
139
|
+
|
140
|
+
i.render(@canvas)
|
141
|
+
end
|
142
|
+
|
143
|
+
self
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# Draw a rectangle at position (_x_,_y_) with defined _width_ and _height_.
|
148
|
+
#
|
149
|
+
def draw_rectangle(x, y, width, height, attr = {})
|
150
|
+
load!
|
151
|
+
|
152
|
+
stroke_color = attr.fetch(:stroke_color, DEFAULT_STROKE_COLOR)
|
153
|
+
fill_color = attr.fetch(:fill_color, DEFAULT_FILL_COLOR)
|
154
|
+
line_cap = attr.fetch(:line_cap, DEFAULT_LINECAP)
|
155
|
+
line_join = attr.fetch(:line_join, DEFAULT_LINEJOIN)
|
156
|
+
line_width = attr.fetch(:line_width, DEFAULT_LINEWIDTH)
|
157
|
+
dash_pattern = attr.fetch(:dash, DEFAULT_DASHPATTERN)
|
158
|
+
|
159
|
+
stroke = attr[:stroke].nil? ? true : attr[:stroke]
|
160
|
+
fill = attr[:fill].nil? ? false : attr[:fill]
|
161
|
+
|
162
|
+
stroke = true if fill == false and stroke == false
|
163
|
+
|
164
|
+
set_fill_color(fill_color) if fill
|
165
|
+
set_stroke_color(stroke_color) if stroke
|
166
|
+
set_line_width(line_width)
|
167
|
+
set_line_cap(line_cap)
|
168
|
+
set_line_join(line_join)
|
169
|
+
set_dash_pattern(dash_pattern)
|
170
|
+
|
171
|
+
if @canvas.gs.text_state.is_in_text_object?
|
172
|
+
@instructions << PDF::Instruction.new('ET').render(@canvas)
|
173
|
+
end
|
174
|
+
|
175
|
+
@instructions << PDF::Instruction.new('re', x,y,width,height).render(@canvas)
|
176
|
+
|
177
|
+
@instructions << (i =
|
178
|
+
if stroke and not fill
|
179
|
+
PDF::Instruction.new('S')
|
180
|
+
elsif fill and not stroke
|
181
|
+
PDF::Instruction.new('f')
|
182
|
+
elsif fill and stroke
|
183
|
+
PDF::Instruction.new('B')
|
184
|
+
end
|
185
|
+
)
|
186
|
+
|
187
|
+
i.render(@canvas)
|
188
|
+
|
189
|
+
self
|
190
|
+
end
|
191
|
+
|
192
|
+
#
|
193
|
+
# Adds text to the content stream with custom formatting attributes.
|
194
|
+
# _text_:: Text to write.
|
195
|
+
# _attr_:: Formatting attributes.
|
196
|
+
#
|
197
|
+
def write(text, attr = {})
|
198
|
+
load!
|
199
|
+
|
200
|
+
x, y = attr[:x], attr[:y]
|
201
|
+
font = attr.fetch(:font, DEFAULT_FONT)
|
202
|
+
size = attr.fetch(:size, DEFAULT_SIZE)
|
203
|
+
leading = attr.fetch(:leading, DEFAULT_LEADING)
|
204
|
+
color = attr.fetch(:color, attr.fetch(:fill_color, DEFAULT_STROKE_COLOR))
|
205
|
+
stroke_color = attr.fetch(:stroke_color, DEFAULT_STROKE_COLOR)
|
206
|
+
line_width = attr.fetch(:line_width, DEFAULT_LINEWIDTH)
|
207
|
+
word_spacing = attr.fetch(:word_spacing, @canvas.gs.text_state.word_spacing)
|
208
|
+
char_spacing = attr.fetch(:char_spacing, @canvas.gs.text_state.char_spacing)
|
209
|
+
scale = attr.fetch(:scale, @canvas.gs.text_state.scaling)
|
210
|
+
rise = attr.fetch(:rise, @canvas.gs.text_state.text_rise)
|
211
|
+
rendering = attr.fetch(:rendering, @canvas.gs.text_state.rendering_mode)
|
212
|
+
|
213
|
+
@instructions << PDF::Instruction.new('ET').render(@canvas) if (x or y) and @canvas.gs.text_state.is_in_text_object?
|
214
|
+
|
215
|
+
unless @canvas.gs.text_state.is_in_text_object?
|
216
|
+
@instructions << PDF::Instruction.new('BT').render(@canvas)
|
217
|
+
end
|
218
|
+
|
219
|
+
set_text_font(font, size)
|
220
|
+
set_text_pos(x, y) if x or y
|
221
|
+
set_text_leading(leading)
|
222
|
+
set_text_rendering(rendering)
|
223
|
+
set_text_rise(rise)
|
224
|
+
set_text_scale(scale)
|
225
|
+
set_text_word_spacing(word_spacing)
|
226
|
+
set_text_char_spacing(char_spacing)
|
227
|
+
set_fill_color(color)
|
228
|
+
set_stroke_color(stroke_color)
|
229
|
+
set_line_width(line_width)
|
230
|
+
|
231
|
+
write_text_block(text)
|
232
|
+
|
233
|
+
self
|
234
|
+
end
|
235
|
+
|
236
|
+
def paint_shading(shade)
|
237
|
+
load!
|
238
|
+
|
239
|
+
@instructions << PDF::Instruction.new('sh', shade).render(@canvas)
|
240
|
+
|
241
|
+
self
|
242
|
+
end
|
243
|
+
|
244
|
+
def set_text_font(fontname, size)
|
245
|
+
load!
|
246
|
+
|
247
|
+
if fontname != @canvas.gs.text_state.font or size != @canvas.gs.text_state.font_size
|
248
|
+
@instructions << PDF::Instruction.new('Tf', fontname, size).render(@canvas)
|
249
|
+
end
|
250
|
+
|
251
|
+
self
|
252
|
+
end
|
253
|
+
|
254
|
+
def set_text_pos(tx,ty)
|
255
|
+
load!
|
256
|
+
|
257
|
+
@instructions << PDF::Instruction.new('Td', tx, ty).render(@canvas)
|
258
|
+
|
259
|
+
self
|
260
|
+
end
|
261
|
+
|
262
|
+
def set_text_leading(leading)
|
263
|
+
load!
|
264
|
+
|
265
|
+
if leading != @canvas.gs.text_state.leading
|
266
|
+
@instructions << PDF::Instruction.new('TL', leading).render(@canvas)
|
267
|
+
end
|
268
|
+
|
269
|
+
self
|
270
|
+
end
|
271
|
+
|
272
|
+
def set_text_rendering(rendering)
|
273
|
+
load!
|
274
|
+
|
275
|
+
if rendering != @canvas.gs.text_state.rendering_mode
|
276
|
+
@instructions << PDF::Instruction.new('Tr', rendering).render(@canvas)
|
277
|
+
end
|
278
|
+
|
279
|
+
self
|
280
|
+
end
|
281
|
+
|
282
|
+
def set_text_rise(rise)
|
283
|
+
load!
|
284
|
+
|
285
|
+
if rise != @canvas.gs.text_state.text_rise
|
286
|
+
@instructions << PDF::Instruction.new('Ts', rise).render(@canvas)
|
287
|
+
end
|
288
|
+
|
289
|
+
self
|
290
|
+
end
|
291
|
+
|
292
|
+
def set_text_scale(scaling)
|
293
|
+
load!
|
294
|
+
|
295
|
+
if scaling != @canvas.gs.text_state.scaling
|
296
|
+
@instructions << PDF::Instruction.new('Tz', scaling).render(@canvas)
|
297
|
+
end
|
298
|
+
|
299
|
+
self
|
300
|
+
end
|
301
|
+
|
302
|
+
def set_text_word_spacing(word_spacing)
|
303
|
+
load!
|
304
|
+
|
305
|
+
if word_spacing != @canvas.gs.text_state.word_spacing
|
306
|
+
@instructions << PDF::Instruction.new('Tw', word_spacing).render(@canvas)
|
307
|
+
end
|
308
|
+
|
309
|
+
self
|
310
|
+
end
|
311
|
+
|
312
|
+
def set_text_char_spacing(char_spacing)
|
313
|
+
load!
|
314
|
+
|
315
|
+
if char_spacing != @canvas.gs.text_state.char_spacing
|
316
|
+
@instructions << PDF::Instruction.new('Tc', char_spacing).render(@canvas)
|
317
|
+
end
|
318
|
+
|
319
|
+
self
|
320
|
+
end
|
321
|
+
|
322
|
+
def set_fill_color(color)
|
323
|
+
load!
|
324
|
+
|
325
|
+
@instructions << ( i =
|
326
|
+
if (color.respond_to? :r and color.respond_to? :g and color.respond_to? :b) or (color.is_a?(::Array) and color.size == 3)
|
327
|
+
r = (color.respond_to?(:r) ? color.r : color[0]).to_f / 255
|
328
|
+
g = (color.respond_to?(:g) ? color.g : color[1]).to_f / 255
|
329
|
+
b = (color.respond_to?(:b) ? color.b : color[2]).to_f / 255
|
330
|
+
PDF::Instruction.new('rg', r, g, b) if @canvas.gs.nonstroking_color != [r,g,b]
|
331
|
+
|
332
|
+
elsif (color.respond_to? :c and color.respond_to? :m and color.respond_to? :y and color.respond_to? :k) or (color.is_a?(::Array) and color.size == 4)
|
333
|
+
c = (color.respond_to?(:c) ? color.c : color[0]).to_f
|
334
|
+
m = (color.respond_to?(:m) ? color.m : color[1]).to_f
|
335
|
+
y = (color.respond_to?(:y) ? color.y : color[2]).to_f
|
336
|
+
k = (color.respond_to?(:k) ? color.k : color[3]).to_f
|
337
|
+
PDF::Instruction.new('k', c, m, y, k) if @canvas.gs.nonstroking_color != [c,m,y,k]
|
338
|
+
|
339
|
+
elsif color.respond_to?(:g) or (0.0..1.0).include?(color)
|
340
|
+
g = color.respond_to?(:g) ? color.g : color
|
341
|
+
PDF::Instruction.new('g', g) if @canvas.gs.nonstroking_color != [ g ]
|
342
|
+
|
343
|
+
else
|
344
|
+
raise TypeError, "Invalid color : #{color}"
|
345
|
+
end
|
346
|
+
)
|
347
|
+
|
348
|
+
i.render(@canvas) if i
|
349
|
+
self
|
350
|
+
end
|
351
|
+
|
352
|
+
def set_stroke_color(color)
|
353
|
+
load!
|
354
|
+
|
355
|
+
@instructions << ( i =
|
356
|
+
if (color.respond_to? :r and color.respond_to? :g and color.respond_to? :b) or (color.is_a?(::Array) and color.size == 3)
|
357
|
+
r = (color.respond_to?(:r) ? color.r : color[0]).to_f / 255
|
358
|
+
g = (color.respond_to?(:g) ? color.g : color[1]).to_f / 255
|
359
|
+
b = (color.respond_to?(:b) ? color.b : color[2]).to_f / 255
|
360
|
+
PDF::Instruction.new('RG', r, g, b) if @canvas.gs.stroking_color != [r,g,b]
|
361
|
+
|
362
|
+
elsif (color.respond_to? :c and color.respond_to? :m and color.respond_to? :y and color.respond_to? :k) or (color.is_a?(::Array) and color.size == 4)
|
363
|
+
c = (color.respond_to?(:c) ? color.c : color[0]).to_f
|
364
|
+
m = (color.respond_to?(:m) ? color.m : color[1]).to_f
|
365
|
+
y = (color.respond_to?(:y) ? color.y : color[2]).to_f
|
366
|
+
k = (color.respond_to?(:k) ? color.k : color[3]).to_f
|
367
|
+
PDF::Instruction.new('K', c, m, y, k) if @canvas.gs.stroking_color != [c,m,y,k]
|
368
|
+
|
369
|
+
elsif color.respond_to?(:g) or (0.0..1.0).include?(color)
|
370
|
+
g = color.respond_to?(:g) ? color.g : color
|
371
|
+
PDF::Instruction.new('G', g) if @canvas.gs.stroking_color != [ g ]
|
372
|
+
|
373
|
+
else
|
374
|
+
raise TypeError, "Invalid color : #{color}"
|
375
|
+
end
|
376
|
+
)
|
377
|
+
|
378
|
+
i.render(@canvas) if i
|
379
|
+
self
|
380
|
+
end
|
381
|
+
|
382
|
+
def set_dash_pattern(pattern)
|
383
|
+
load!
|
384
|
+
|
385
|
+
unless @canvas.gs.dash_pattern.eql? pattern
|
386
|
+
@instructions << PDF::Instruction.new('d', pattern.array, pattern.phase).render(@canvas)
|
387
|
+
end
|
388
|
+
|
389
|
+
self
|
390
|
+
end
|
391
|
+
|
392
|
+
def set_line_width(width)
|
393
|
+
load!
|
394
|
+
|
395
|
+
if @canvas.gs.line_width != width
|
396
|
+
@instructions << PDF::Instruction.new('w', width).render(@canvas)
|
397
|
+
end
|
398
|
+
|
399
|
+
self
|
400
|
+
end
|
401
|
+
|
402
|
+
def set_line_cap(cap)
|
403
|
+
load!
|
404
|
+
|
405
|
+
if @canvas.gs.line_cap != cap
|
406
|
+
@instructions << PDF::Instruction.new('J', cap).render(@canvas)
|
407
|
+
end
|
408
|
+
|
409
|
+
self
|
410
|
+
end
|
411
|
+
|
412
|
+
def set_line_join(join)
|
413
|
+
load!
|
414
|
+
|
415
|
+
if @canvas.gs.line_join != join
|
416
|
+
@instructions << PDF::Instruction.new('j', join).render(@canvas)
|
417
|
+
end
|
418
|
+
|
419
|
+
self
|
420
|
+
end
|
421
|
+
|
422
|
+
private
|
423
|
+
|
424
|
+
def load!
|
425
|
+
return unless @instructions.nil?
|
426
|
+
|
427
|
+
decode!
|
428
|
+
|
429
|
+
code = StringScanner.new self.data
|
430
|
+
@instructions = []
|
431
|
+
|
432
|
+
until code.eos?
|
433
|
+
insn = PDF::Instruction.parse(code)
|
434
|
+
@instructions << insn if insn
|
435
|
+
end
|
436
|
+
|
437
|
+
self
|
438
|
+
end
|
439
|
+
|
440
|
+
def write_text_block(text)
|
441
|
+
lines = text.split("\n").map!{|line| line.to_s}
|
442
|
+
|
443
|
+
@instructions << PDF::Instruction.new('Tj', lines.slice!(0)).render(@canvas)
|
444
|
+
lines.each do |line|
|
445
|
+
@instructions << PDF::Instruction.new("'", line).render(@canvas)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end #class ContentStream
|
449
|
+
|
450
|
+
class Page < Dictionary
|
451
|
+
|
452
|
+
def render(engine) #:nodoc:
|
453
|
+
contents = self.Contents
|
454
|
+
contents = [ contents ] unless contents.is_a? Array
|
455
|
+
|
456
|
+
contents.each do |stream|
|
457
|
+
stream = stream.cast_to(ContentStream) unless stream.is_a? ContentStream
|
458
|
+
|
459
|
+
stream.render(engine)
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# TODO :nodoc:
|
464
|
+
def draw_image
|
465
|
+
raise NotImplementedError
|
466
|
+
end
|
467
|
+
|
468
|
+
# See ContentStream#draw_line.
|
469
|
+
def draw_line(from, to, attr = {})
|
470
|
+
last_content_stream.draw_line(from, to, attr); self
|
471
|
+
end
|
472
|
+
|
473
|
+
# See ContentStream#draw_polygon.
|
474
|
+
def draw_polygon(coords = [], attr = {})
|
475
|
+
last_content_stream.draw_polygon(coords, attr); self
|
476
|
+
end
|
477
|
+
|
478
|
+
# See ContentStream#draw_rectangle.
|
479
|
+
def draw_rectangle(x, y, width, height, attr = {})
|
480
|
+
last_content_stream.draw_rectangle(x, y, width, height, attr); self
|
481
|
+
end
|
482
|
+
|
483
|
+
# See ContentStream#write.
|
484
|
+
def write(text, attr = {})
|
485
|
+
last_content_stream.write(text, attr); self
|
486
|
+
end
|
487
|
+
|
488
|
+
# TODO :nodoc:
|
489
|
+
def paint_shading(shade)
|
490
|
+
last_content_stream.paint_shading(shade)
|
491
|
+
end
|
492
|
+
|
493
|
+
# TODO :nodoc:
|
494
|
+
def set_text_font(_font, _size)
|
495
|
+
raise NotImplementedError
|
496
|
+
end
|
497
|
+
|
498
|
+
# See ContentStream#set_text_pos.
|
499
|
+
def set_text_pos(tx, ty)
|
500
|
+
last_content_stream.set_text_pos(tx, ty); self
|
501
|
+
end
|
502
|
+
|
503
|
+
# See ContentStream#set_text_leading.
|
504
|
+
def set_text_leading(leading)
|
505
|
+
last_content_stream.set_text_leading(leading); self
|
506
|
+
end
|
507
|
+
|
508
|
+
# See ContentStream#set_text_rendering.
|
509
|
+
def set_text_rendering(rendering)
|
510
|
+
last_content_stream.set_text_rendering(rendering); self
|
511
|
+
end
|
512
|
+
|
513
|
+
# See ContentStream#set_text_rise.
|
514
|
+
def set_text_rise(rise)
|
515
|
+
last_content_stream.set_text_rise(rise); self
|
516
|
+
end
|
517
|
+
|
518
|
+
# See ContentStream#set_text_scale.
|
519
|
+
def set_text_scale(scaling)
|
520
|
+
last_content_stream.set_text_scale(scaling); self
|
521
|
+
end
|
522
|
+
|
523
|
+
# See ContentStream#set_text_word_spacing.
|
524
|
+
def set_text_word_spacing(word_spacing)
|
525
|
+
last_content_stream.set_text_word_spacing(word_spacing); self
|
526
|
+
end
|
527
|
+
|
528
|
+
# See ContentStream#set_text_char_spacing.
|
529
|
+
def set_text_char_spacing(char_spacing)
|
530
|
+
last_content_stream.set_text_char_spacing(char_spacing); self
|
531
|
+
end
|
532
|
+
|
533
|
+
# See ContentStream#set_fill_color.
|
534
|
+
def set_fill_color(color)
|
535
|
+
last_content_stream.set_fill_color(color); self
|
536
|
+
end
|
537
|
+
|
538
|
+
# See ContentStream#set_stroke_color.
|
539
|
+
def set_stroke_color(color)
|
540
|
+
last_content_stream.set_stroke_color(color); self
|
541
|
+
end
|
542
|
+
|
543
|
+
# See ContentStream#set_dash_pattern.
|
544
|
+
def set_dash_pattern(pattern)
|
545
|
+
last_content_stream.set_dash_pattern(pattern); self
|
546
|
+
end
|
547
|
+
|
548
|
+
# See ContentStream#set_line_width.
|
549
|
+
def set_line_width(width)
|
550
|
+
last_content_stream.set_line_width(width); self
|
551
|
+
end
|
552
|
+
|
553
|
+
# See ContentStream#set_line_cap.
|
554
|
+
def set_line_cap(cap)
|
555
|
+
last_content_stream.set_line_cap(cap); self
|
556
|
+
end
|
557
|
+
|
558
|
+
# See ContentStream#set_line_join.
|
559
|
+
def set_line_join(join)
|
560
|
+
last_content_stream.set_line_join(join); self
|
561
|
+
end
|
562
|
+
|
563
|
+
private
|
564
|
+
|
565
|
+
def last_content_stream #:nodoc:
|
566
|
+
streams = self.content_streams
|
567
|
+
if streams.empty?
|
568
|
+
self.Contents = ContentStream.new
|
569
|
+
else
|
570
|
+
streams.last
|
571
|
+
end
|
572
|
+
end
|
573
|
+
end # class Page
|
574
|
+
|
575
|
+
module Graphics
|
576
|
+
|
577
|
+
module XObject
|
578
|
+
def self.included(receiver)
|
579
|
+
receiver.field :Type, :Type => Name, :Default => :XObject
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
class FormXObject < ContentStream
|
584
|
+
include XObject
|
585
|
+
include ResourcesHolder
|
586
|
+
|
587
|
+
class Group < Dictionary
|
588
|
+
include StandardObject
|
589
|
+
|
590
|
+
module Type
|
591
|
+
TRANSPARENCY = :Transparency
|
592
|
+
end
|
593
|
+
|
594
|
+
field :Type, :Type => Name, :Default => :Group
|
595
|
+
field :S, :Type => Name, :Default => Type::TRANSPARENCY, :Required => true
|
596
|
+
end
|
597
|
+
|
598
|
+
class Reference < Dictionary
|
599
|
+
include StandardObject
|
600
|
+
|
601
|
+
field :F, :Type => FileSpec, :Required => true
|
602
|
+
field :Page, :Type => [ Integer, String ], :Required => true
|
603
|
+
field :ID, :Type => Array.of(String, length: 2)
|
604
|
+
end
|
605
|
+
|
606
|
+
field :Subtype, :Type => Name, :Default => :Form, :Required => true
|
607
|
+
field :FormType, :Type => Integer, :Default => 1
|
608
|
+
field :BBox, :Type => Rectangle, :Required => true
|
609
|
+
field :Matrix, :Type => Array.of(Number, length: 6), :Default => [1, 0, 0, 1, 0, 0]
|
610
|
+
field :Resources, :Type => Resources, :Version => "1.2"
|
611
|
+
field :Group, :Type => Group, :Version => "1.4"
|
612
|
+
field :Ref, :Type => Reference, :Version => "1.4"
|
613
|
+
field :Metadata, :Type => MetadataStream, :Version => "1.4"
|
614
|
+
field :PieceInfo, :Type => Dictionary, :Version => "1.3"
|
615
|
+
field :LastModified, :Type => String, :Version => "1.3"
|
616
|
+
field :StructParent, :Type => Integer, :Version => "1.3"
|
617
|
+
field :StructParents, :Type => Integer, :Version => "1.3"
|
618
|
+
field :OPI, :Type => Dictionary, :Version => "1.2"
|
619
|
+
field :OC, :Type => Dictionary, :Version => "1.5"
|
620
|
+
field :Name, :Type => Name
|
621
|
+
field :Measure, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3
|
622
|
+
field :PtData, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3
|
623
|
+
|
624
|
+
def pre_build
|
625
|
+
self.Resources = Resources.new.pre_build unless self.key?(:Resources)
|
626
|
+
|
627
|
+
super
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
class ImageXObject < Stream
|
632
|
+
include XObject
|
633
|
+
|
634
|
+
field :Subtype, :Type => Name, :Default => :Image, :Required => true
|
635
|
+
field :Width, :Type => Integer, :Required => true
|
636
|
+
field :Height, :Type => Integer, :Required => true
|
637
|
+
field :ColorSpace, :Type => [ Name, Array ]
|
638
|
+
field :BitsPerComponent, :Type => Integer
|
639
|
+
field :Intent, :Type => Name, :Version => "1.1"
|
640
|
+
field :ImageMask, :Type => Boolean, :Default => false
|
641
|
+
field :Mask, :Type => [ ImageXObject, Array.of(Integer) ], :Version => "1.3"
|
642
|
+
field :Decode, :Type => Array.of(Number)
|
643
|
+
field :Interpolate, :Type => Boolean, :Default => false
|
644
|
+
field :Alternates, :Type => Array, :Version => "1.3"
|
645
|
+
field :SMask, :Type => ImageXObject, :Version => "1.4"
|
646
|
+
field :SMaskInData, :Type => Integer, :Default => 0, :Version => "1.5"
|
647
|
+
field :Name, :Type => Name
|
648
|
+
field :StructParent, :Type => Integer, :Version => "1.3"
|
649
|
+
field :ID, :Type => String, :Version => "1.3"
|
650
|
+
field :OPI, :Type => Dictionary, :Version => "1.2"
|
651
|
+
field :Matte, :Type => Array.of(Number), :Version => "1.4" # Used in Soft-Mask images.
|
652
|
+
field :Metadata, :Type => MetadataStream, :Version => "1.4"
|
653
|
+
field :OC, :Type => Dictionary, :Version => "1.5"
|
654
|
+
field :Measure, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3
|
655
|
+
field :PtData, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3
|
656
|
+
|
657
|
+
def self.from_image_file(path, format = nil)
|
658
|
+
if path.respond_to?(:read)
|
659
|
+
data = path.read
|
660
|
+
else
|
661
|
+
data = File.binread(File.expand_path(path))
|
662
|
+
format ||= File.extname(path)[1..-1]
|
663
|
+
end
|
664
|
+
|
665
|
+
image = ImageXObject.new
|
666
|
+
|
667
|
+
raise ArgumentError, "Missing file format" if format.nil?
|
668
|
+
case format.downcase
|
669
|
+
when 'jpg', 'jpeg', 'jpe', 'jif', 'jfif', 'jfi'
|
670
|
+
image.setFilter :DCTDecode
|
671
|
+
image.encoded_data = data
|
672
|
+
|
673
|
+
when 'jp2','jpx','j2k','jpf','jpm','mj2'
|
674
|
+
image.setFilter :JPXDecode
|
675
|
+
image.encoded_data = data
|
676
|
+
|
677
|
+
when '.b2', 'jbig', 'jbig2'
|
678
|
+
image.setFilter :JBIG2Decode
|
679
|
+
image.encoded_data = data
|
680
|
+
else
|
681
|
+
raise NotImplementedError, "Unknown file format: '#{format}'"
|
682
|
+
end
|
683
|
+
|
684
|
+
image
|
685
|
+
end
|
686
|
+
|
687
|
+
#
|
688
|
+
# Converts an ImageXObject stream into an image file data.
|
689
|
+
# Output format depends on the stream encoding:
|
690
|
+
# * JPEG for DCTDecode
|
691
|
+
# * JPEG2000 for JPXDecode
|
692
|
+
# * JBIG2 for JBIG2Decode
|
693
|
+
# * PNG for everything else
|
694
|
+
#
|
695
|
+
# Returns an array of the form [ _format_, _data_ ]
|
696
|
+
#
|
697
|
+
def to_image_file
|
698
|
+
encoding = self.Filter
|
699
|
+
encoding = encoding[0] if encoding.is_a? ::Array
|
700
|
+
|
701
|
+
case (encoding && encoding.value)
|
702
|
+
when :DCTDecode then return [ 'jpg', self.data ]
|
703
|
+
when :JBIG2Decode then return [ 'jbig2', self.data ]
|
704
|
+
when :JPXDecode then return [ 'jp2', self.data ]
|
705
|
+
end
|
706
|
+
|
707
|
+
# Assume PNG data.
|
708
|
+
|
709
|
+
raise InvalidColorError, "No colorspace specified" unless self.ColorSpace
|
710
|
+
|
711
|
+
case cs = self.ColorSpace.value
|
712
|
+
when Color::Space::DEVICE_GRAY
|
713
|
+
color_type = 0
|
714
|
+
components = 1
|
715
|
+
when Color::Space::DEVICE_RGB
|
716
|
+
color_type = 2
|
717
|
+
components = 3
|
718
|
+
when ::Array
|
719
|
+
cs_type = cs[0]
|
720
|
+
case cs_type
|
721
|
+
when :Indexed
|
722
|
+
color_type = 3
|
723
|
+
components = 3
|
724
|
+
cs_base = cs[1]
|
725
|
+
lookup = cs[3]
|
726
|
+
|
727
|
+
when :ICCBased
|
728
|
+
icc_profile = cs[1]
|
729
|
+
raise InvalidColorError,
|
730
|
+
"Invalid ICC Profile parameter" unless icc_profile.is_a?(Stream)
|
731
|
+
|
732
|
+
case icc_profile.N
|
733
|
+
when 1
|
734
|
+
color_type = 0
|
735
|
+
components = 1
|
736
|
+
when 3
|
737
|
+
color_type = 2
|
738
|
+
components = 3
|
739
|
+
else
|
740
|
+
raise InvalidColorError,
|
741
|
+
"Invalid number of components in ICC profile: #{icc_profile.N}"
|
742
|
+
end
|
743
|
+
else
|
744
|
+
raise InvalidColorError, "Unsupported color space: #{self.ColorSpace}"
|
745
|
+
end
|
746
|
+
else
|
747
|
+
raise InvalidColorError, "Unsupported color space: #{self.ColorSpace}"
|
748
|
+
end
|
749
|
+
|
750
|
+
bpc = self.BitsPerComponent || 8
|
751
|
+
w, h = self.Width, self.Height
|
752
|
+
pixels = self.data
|
753
|
+
|
754
|
+
hdr = [137, 80, 78, 71, 13, 10, 26, 10].pack('C*')
|
755
|
+
chunks = []
|
756
|
+
|
757
|
+
chunks <<
|
758
|
+
[
|
759
|
+
'IHDR',
|
760
|
+
[
|
761
|
+
w, h,
|
762
|
+
bpc, color_type, 0, 0, 0
|
763
|
+
].pack("N2C5")
|
764
|
+
]
|
765
|
+
|
766
|
+
|
767
|
+
if self.Intents
|
768
|
+
intents =
|
769
|
+
case self.Intents.value
|
770
|
+
when Intents::PERCEPTUAL then 0
|
771
|
+
when Intents::RELATIVE then 1
|
772
|
+
when Intents::SATURATION then 2
|
773
|
+
when Intents::ABSOLUTE then 3
|
774
|
+
else
|
775
|
+
3
|
776
|
+
end
|
777
|
+
|
778
|
+
chunks <<
|
779
|
+
[
|
780
|
+
'sRGB',
|
781
|
+
[ intents ].pack('C')
|
782
|
+
]
|
783
|
+
|
784
|
+
chunks << [ 'gAMA', [ 45455 ].pack("N") ]
|
785
|
+
chunks <<
|
786
|
+
[
|
787
|
+
'cHRM',
|
788
|
+
[
|
789
|
+
31270,
|
790
|
+
32900,
|
791
|
+
64000,
|
792
|
+
33000,
|
793
|
+
30000,
|
794
|
+
60000,
|
795
|
+
15000,
|
796
|
+
6000
|
797
|
+
].pack("N8")
|
798
|
+
]
|
799
|
+
end
|
800
|
+
|
801
|
+
if color_type == 3
|
802
|
+
lookup =
|
803
|
+
case lookup
|
804
|
+
when Stream then lookup.data
|
805
|
+
when String then lookup.value
|
806
|
+
else
|
807
|
+
raise InvalidColorError, "Invalid indexed palette table"
|
808
|
+
end
|
809
|
+
|
810
|
+
raise InvalidColorError, "Invalid base color space" unless cs_base
|
811
|
+
palette = ""
|
812
|
+
|
813
|
+
case cs_base
|
814
|
+
when Color::Space::DEVICE_GRAY
|
815
|
+
lookup.each_byte do |g|
|
816
|
+
palette << Color.gray_to_rgb(g).pack("C3")
|
817
|
+
end
|
818
|
+
when Color::Space::DEVICE_RGB
|
819
|
+
palette << lookup[0, (lookup.size / 3) * 3]
|
820
|
+
|
821
|
+
when Color::Space::DEVICE_CMYK
|
822
|
+
(lookup.size / 4).times do |i|
|
823
|
+
cmyk = lookup[i * 4, 4].unpack("C4").map!{|c| c.to_f / 255}
|
824
|
+
palette << Color.cmyk_to_rgb(*cmyk).map!{|c| (c * 255).to_i}.pack("C3")
|
825
|
+
end
|
826
|
+
when ::Array
|
827
|
+
|
828
|
+
case cs_base[0]
|
829
|
+
when :ICCBased
|
830
|
+
icc_profile = cs_base[1]
|
831
|
+
raise InvalidColorError,
|
832
|
+
"Invalid ICC Profile parameter" unless icc_profile.is_a?(Stream)
|
833
|
+
|
834
|
+
case icc_profile.N
|
835
|
+
when 1
|
836
|
+
lookup.each_byte do |g|
|
837
|
+
palette << Color.gray_to_rgb(g).pack("C3")
|
838
|
+
end
|
839
|
+
when 3
|
840
|
+
palette << lookup[0, (lookup.size / 3) * 3]
|
841
|
+
else
|
842
|
+
raise InvalidColorError,
|
843
|
+
"Invalid number of components in ICC profile: #{icc_profile.N}"
|
844
|
+
end
|
845
|
+
else
|
846
|
+
raise InvalidColorError, "Unsupported color space: #{cs_base}"
|
847
|
+
end
|
848
|
+
else
|
849
|
+
raise InvalidColorError, "Unsupported color space: #{cs_base}"
|
850
|
+
end
|
851
|
+
|
852
|
+
if icc_profile
|
853
|
+
chunks <<
|
854
|
+
[
|
855
|
+
'iCCP',
|
856
|
+
'ICC Profile' + "\x00\x00" + Zlib::Deflate.deflate(icc_profile.data, Zlib::BEST_COMPRESSION)
|
857
|
+
]
|
858
|
+
end
|
859
|
+
|
860
|
+
chunks <<
|
861
|
+
[
|
862
|
+
'PLTE',
|
863
|
+
palette
|
864
|
+
]
|
865
|
+
|
866
|
+
bpr = w
|
867
|
+
|
868
|
+
else # color_type != 3
|
869
|
+
if icc_profile
|
870
|
+
chunks <<
|
871
|
+
[
|
872
|
+
'iCCP',
|
873
|
+
'ICC Profile' + "\x00\x00" + Zlib::Deflate.deflate(icc_profile.data, Zlib::BEST_COMPRESSION)
|
874
|
+
]
|
875
|
+
end
|
876
|
+
|
877
|
+
bpr = (bpc >> 3) * components * w
|
878
|
+
end
|
879
|
+
|
880
|
+
nrows = pixels.size / bpr
|
881
|
+
nrows.times do |irow|
|
882
|
+
pixels.insert(irow * bpr + irow, "\x00")
|
883
|
+
end
|
884
|
+
|
885
|
+
chunks <<
|
886
|
+
[
|
887
|
+
'IDAT',
|
888
|
+
Zlib::Deflate.deflate(pixels, Zlib::BEST_COMPRESSION)
|
889
|
+
]
|
890
|
+
|
891
|
+
if self.Metadata.is_a?(Stream)
|
892
|
+
chunks <<
|
893
|
+
[
|
894
|
+
'tEXt',
|
895
|
+
"XML:com.adobe.xmp" + "\x00" + self.Metadata.data
|
896
|
+
]
|
897
|
+
end
|
898
|
+
|
899
|
+
chunks << [ 'IEND', '' ]
|
900
|
+
|
901
|
+
[ 'png',
|
902
|
+
hdr + chunks.map!{ |chk|
|
903
|
+
[ chk[1].size, chk[0], chk[1], Zlib.crc32(chk[0] + chk[1]) ].pack("NA4A*N")
|
904
|
+
}.join
|
905
|
+
]
|
906
|
+
end
|
907
|
+
end
|
908
|
+
|
909
|
+
class ReferenceDictionary < Dictionary
|
910
|
+
include StandardObject
|
911
|
+
|
912
|
+
field :F, :Type => Dictionary, :Required => true
|
913
|
+
field :Page, :Type => [Integer, String], :Required => true
|
914
|
+
field :ID, :Tyoe => Array
|
915
|
+
end
|
916
|
+
end
|
917
|
+
|
918
|
+
end
|