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.
Files changed (139) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +89 -0
  3. data/COPYING.LESSER +165 -0
  4. data/README.md +131 -0
  5. data/bin/config/pdfcop.conf.yml +236 -0
  6. data/bin/pdf2pdfa +87 -0
  7. data/bin/pdf2ruby +333 -0
  8. data/bin/pdfcop +476 -0
  9. data/bin/pdfdecompress +97 -0
  10. data/bin/pdfdecrypt +91 -0
  11. data/bin/pdfencrypt +113 -0
  12. data/bin/pdfexplode +223 -0
  13. data/bin/pdfextract +277 -0
  14. data/bin/pdfmetadata +143 -0
  15. data/bin/pdfsh +12 -0
  16. data/bin/shell/console.rb +128 -0
  17. data/bin/shell/hexdump.rb +59 -0
  18. data/bin/shell/irbrc +69 -0
  19. data/examples/README.md +34 -0
  20. data/examples/attachments/attachment.rb +38 -0
  21. data/examples/attachments/nested_document.rb +51 -0
  22. data/examples/encryption/encryption.rb +28 -0
  23. data/examples/events/events.rb +72 -0
  24. data/examples/flash/flash.rb +37 -0
  25. data/examples/flash/helloworld.swf +0 -0
  26. data/examples/forms/javascript.rb +54 -0
  27. data/examples/forms/xfa.rb +115 -0
  28. data/examples/javascript/hello_world.rb +22 -0
  29. data/examples/javascript/js_emulation.rb +54 -0
  30. data/examples/loop/goto.rb +32 -0
  31. data/examples/loop/named.rb +33 -0
  32. data/examples/signature/signature.rb +65 -0
  33. data/examples/uri/javascript.rb +56 -0
  34. data/examples/uri/open-uri.rb +21 -0
  35. data/examples/uri/submitform.rb +47 -0
  36. data/lib/origami/3d.rb +364 -0
  37. data/lib/origami/acroform.rb +321 -0
  38. data/lib/origami/actions.rb +318 -0
  39. data/lib/origami/annotations.rb +711 -0
  40. data/lib/origami/array.rb +242 -0
  41. data/lib/origami/boolean.rb +90 -0
  42. data/lib/origami/catalog.rb +418 -0
  43. data/lib/origami/collections.rb +144 -0
  44. data/lib/origami/compound.rb +161 -0
  45. data/lib/origami/destinations.rb +252 -0
  46. data/lib/origami/dictionary.rb +192 -0
  47. data/lib/origami/encryption.rb +1084 -0
  48. data/lib/origami/extensions/fdf.rb +347 -0
  49. data/lib/origami/extensions/ppklite.rb +422 -0
  50. data/lib/origami/filespec.rb +197 -0
  51. data/lib/origami/filters/ascii.rb +211 -0
  52. data/lib/origami/filters/ccitt/tables.rb +267 -0
  53. data/lib/origami/filters/ccitt.rb +357 -0
  54. data/lib/origami/filters/crypt.rb +38 -0
  55. data/lib/origami/filters/dct.rb +54 -0
  56. data/lib/origami/filters/flate.rb +69 -0
  57. data/lib/origami/filters/jbig2.rb +57 -0
  58. data/lib/origami/filters/jpx.rb +47 -0
  59. data/lib/origami/filters/lzw.rb +170 -0
  60. data/lib/origami/filters/predictors.rb +292 -0
  61. data/lib/origami/filters/runlength.rb +129 -0
  62. data/lib/origami/filters.rb +364 -0
  63. data/lib/origami/font.rb +196 -0
  64. data/lib/origami/functions.rb +79 -0
  65. data/lib/origami/graphics/colors.rb +230 -0
  66. data/lib/origami/graphics/instruction.rb +98 -0
  67. data/lib/origami/graphics/path.rb +182 -0
  68. data/lib/origami/graphics/patterns.rb +174 -0
  69. data/lib/origami/graphics/render.rb +62 -0
  70. data/lib/origami/graphics/state.rb +149 -0
  71. data/lib/origami/graphics/text.rb +225 -0
  72. data/lib/origami/graphics/xobject.rb +918 -0
  73. data/lib/origami/graphics.rb +38 -0
  74. data/lib/origami/header.rb +75 -0
  75. data/lib/origami/javascript.rb +713 -0
  76. data/lib/origami/linearization.rb +330 -0
  77. data/lib/origami/metadata.rb +172 -0
  78. data/lib/origami/name.rb +135 -0
  79. data/lib/origami/null.rb +65 -0
  80. data/lib/origami/numeric.rb +181 -0
  81. data/lib/origami/obfuscation.rb +245 -0
  82. data/lib/origami/object.rb +760 -0
  83. data/lib/origami/optionalcontent.rb +183 -0
  84. data/lib/origami/outline.rb +54 -0
  85. data/lib/origami/outputintents.rb +85 -0
  86. data/lib/origami/page.rb +722 -0
  87. data/lib/origami/parser.rb +269 -0
  88. data/lib/origami/parsers/fdf.rb +56 -0
  89. data/lib/origami/parsers/pdf/lazy.rb +176 -0
  90. data/lib/origami/parsers/pdf/linear.rb +122 -0
  91. data/lib/origami/parsers/pdf.rb +118 -0
  92. data/lib/origami/parsers/ppklite.rb +57 -0
  93. data/lib/origami/pdf.rb +1108 -0
  94. data/lib/origami/reference.rb +134 -0
  95. data/lib/origami/signature.rb +702 -0
  96. data/lib/origami/stream.rb +705 -0
  97. data/lib/origami/string.rb +444 -0
  98. data/lib/origami/template/patterns.rb +56 -0
  99. data/lib/origami/template/widgets.rb +151 -0
  100. data/lib/origami/trailer.rb +190 -0
  101. data/lib/origami/tree.rb +62 -0
  102. data/lib/origami/version.rb +23 -0
  103. data/lib/origami/webcapture.rb +100 -0
  104. data/lib/origami/xfa/config.rb +453 -0
  105. data/lib/origami/xfa/connectionset.rb +146 -0
  106. data/lib/origami/xfa/datasets.rb +49 -0
  107. data/lib/origami/xfa/localeset.rb +42 -0
  108. data/lib/origami/xfa/package.rb +59 -0
  109. data/lib/origami/xfa/pdf.rb +73 -0
  110. data/lib/origami/xfa/signature.rb +42 -0
  111. data/lib/origami/xfa/sourceset.rb +43 -0
  112. data/lib/origami/xfa/stylesheet.rb +44 -0
  113. data/lib/origami/xfa/template.rb +1691 -0
  114. data/lib/origami/xfa/xdc.rb +42 -0
  115. data/lib/origami/xfa/xfa.rb +146 -0
  116. data/lib/origami/xfa/xfdf.rb +43 -0
  117. data/lib/origami/xfa/xmpmeta.rb +43 -0
  118. data/lib/origami/xfa.rb +62 -0
  119. data/lib/origami/xreftable.rb +557 -0
  120. data/lib/origami.rb +47 -0
  121. data/test/dataset/calc.pdf +85 -0
  122. data/test/dataset/crypto.pdf +36 -0
  123. data/test/dataset/empty.pdf +49 -0
  124. data/test/test_actions.rb +27 -0
  125. data/test/test_annotations.rb +68 -0
  126. data/test/test_forms.rb +30 -0
  127. data/test/test_native_types.rb +83 -0
  128. data/test/test_object_tree.rb +33 -0
  129. data/test/test_pages.rb +60 -0
  130. data/test/test_pdf.rb +20 -0
  131. data/test/test_pdf_attachment.rb +34 -0
  132. data/test/test_pdf_create.rb +24 -0
  133. data/test/test_pdf_encrypt.rb +102 -0
  134. data/test/test_pdf_parse.rb +134 -0
  135. data/test/test_pdf_parse_lazy.rb +69 -0
  136. data/test/test_pdf_sign.rb +97 -0
  137. data/test/test_streams.rb +184 -0
  138. data/test/test_xrefs.rb +67 -0
  139. metadata +280 -0
@@ -0,0 +1,705 @@
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
+ require 'strscan'
22
+
23
+ module Origami
24
+
25
+ class InvalidStreamObjectError < InvalidObjectError #:nodoc:
26
+ end
27
+
28
+ # Forward declaration.
29
+ class FileSpec < Dictionary; end
30
+
31
+ #
32
+ # Class representing a PDF Stream Object.
33
+ # Streams can be used to hold any kind of data, especially binary data.
34
+ #
35
+ class Stream
36
+ include Origami::Object
37
+ include StandardObject
38
+ include FieldAccessor
39
+ include Enumerable
40
+ extend TypeGuessing
41
+
42
+ TOKENS = [ "stream" + WHITECHARS_NORET + "(\\r\\n|\\r|\\n)" , "endstream" ] #:nodoc:
43
+
44
+ @@regexp_open = Regexp.new(WHITESPACES + TOKENS.first)
45
+ @@regexp_close = Regexp.new(TOKENS.last)
46
+
47
+ #
48
+ # Actually only 5 first ones are implemented,
49
+ # other ones are mainly about image data processing (JPEG, JPEG2000 ...)
50
+ #
51
+ DEFINED_FILTERS = %i[
52
+ ASCIIHexDecode
53
+ ASCII85Decode
54
+ LZWDecode
55
+ FlateDecode
56
+ RunLengthDecode
57
+
58
+ CCITTFaxDecode
59
+ JBIG2Decode
60
+ DCTDecode
61
+ JPXDecode
62
+
63
+ AHx
64
+ A85
65
+ LZW
66
+ Fl
67
+ RL
68
+ CCF
69
+ DCT
70
+ ]
71
+
72
+ attr_reader :dictionary
73
+
74
+ field :Length, :Type => Integer, :Required => true
75
+ field :Filter, :Type => [ Name, Array.of(Name) ]
76
+ field :DecodeParms, :Type => [ Dictionary, Array.of(Dictionary) ]
77
+ field :F, :Type => FileSpec, :Version => "1.2"
78
+ field :FFilter, :Type => [ Name, Array.of(Name) ], :Version => "1.2"
79
+ field :FDecodeParms, :Type => [ Dictionary, Array.of(Dictionary) ], :Version => "1.2"
80
+ field :DL, :Type => Integer, :Version => "1.5"
81
+
82
+ #
83
+ # Creates a new PDF Stream.
84
+ # _data_:: The Stream uncompressed data.
85
+ # _dictionary_:: A hash representing the Stream attributes.
86
+ #
87
+ def initialize(data = "", dictionary = {})
88
+ super()
89
+
90
+ set_indirect(true)
91
+
92
+ @encoded_data = nil
93
+ @dictionary, @data = Dictionary.new(dictionary), data
94
+ @dictionary.parent = self
95
+ end
96
+
97
+ def dictionary=(dict)
98
+ @dictionary = dict
99
+ @dictionary.parent = self
100
+ end
101
+
102
+ def pre_build
103
+ encode!
104
+
105
+ super
106
+ end
107
+
108
+ def post_build
109
+ self.Length = @encoded_data.length
110
+
111
+ super
112
+ end
113
+
114
+ def self.parse(stream, parser = nil) #:nodoc:
115
+ scanner = Parser.init_scanner(stream)
116
+ dictionary = Dictionary.parse(scanner, parser)
117
+ return dictionary if not scanner.skip(@@regexp_open)
118
+
119
+ length = dictionary[:Length]
120
+ if not length.is_a?(Integer)
121
+ raw_data = scanner.scan_until(@@regexp_close)
122
+ if raw_data.nil?
123
+ raise InvalidStreamObjectError,
124
+ "Stream shall end with a 'endstream' statement"
125
+ end
126
+ else
127
+ length = length.value
128
+ raw_data = scanner.peek(length)
129
+ scanner.pos += length
130
+
131
+ if not ( unmatched = scanner.scan_until(@@regexp_close) )
132
+ raise InvalidStreamObjectError,
133
+ "Stream shall end with a 'endstream' statement"
134
+ end
135
+
136
+ raw_data << unmatched
137
+ end
138
+
139
+ stm =
140
+ if Origami::OPTIONS[:enable_type_guessing]
141
+ self.guess_type(dictionary).new('', dictionary)
142
+ else
143
+ Stream.new('', dictionary)
144
+ end
145
+
146
+ raw_data.chomp!(TOKENS.last)
147
+
148
+ if raw_data[-1,1] == "\n"
149
+ if raw_data[-2,1] == "\r"
150
+ raw_data = raw_data[0, raw_data.size - 2]
151
+ else
152
+ raw_data = raw_data[0, raw_data.size - 1]
153
+ end
154
+ end
155
+ #raw_data.chomp! if length.is_a?(Integer) and length < raw_data.length
156
+
157
+ stm.encoded_data = raw_data
158
+ stm.file_offset = dictionary.file_offset
159
+
160
+ stm
161
+ end
162
+
163
+ #
164
+ # Iterates over each Filter in the Stream.
165
+ #
166
+ def each_filter
167
+ filters = self.Filter
168
+
169
+ return enum_for(__method__) do
170
+ case filters
171
+ when NilClass then 0
172
+ when Array then filters.length
173
+ else
174
+ 1
175
+ end
176
+ end unless block_given?
177
+
178
+ return if filters.nil?
179
+
180
+ if filters.is_a?(Array)
181
+ filters.each do |filter| yield(filter) end
182
+ else
183
+ yield(filters)
184
+ end
185
+
186
+ self
187
+ end
188
+
189
+ #
190
+ # Returns an Array of Filters for this Stream.
191
+ #
192
+ def filters
193
+ self.each_filter.to_a
194
+ end
195
+
196
+ #
197
+ # Set predictor type for the current Stream.
198
+ # Applies only for LZW and FlateDecode filters.
199
+ #
200
+ def set_predictor(predictor, colors: 1, bitspercomponent: 8, columns: 1)
201
+ filters = self.filters
202
+
203
+ layer = filters.index(:FlateDecode) or filters.index(:LZWDecode)
204
+ if layer.nil?
205
+ raise InvalidStreamObjectError, 'Predictor functions can only be used with Flate or LZW filters'
206
+ end
207
+
208
+ params = Filter::LZW::DecodeParms.new
209
+ params[:Predictor] = predictor
210
+ params[:Colors] = colors if colors != 1
211
+ params[:BitsPerComponent] = bitspercomponent if bitspercomponent != 8
212
+ params[:Columns] = columns if columns != 1
213
+
214
+ set_decode_params(layer, params)
215
+
216
+ self
217
+ end
218
+
219
+ def cast_to(type, _parser = nil)
220
+ assert_cast_type(type)
221
+
222
+ cast = type.new("", self.dictionary.copy)
223
+ cast.encoded_data = self.encoded_data.dup
224
+ cast.file_offset = self.file_offset
225
+
226
+ transfer_attributes(cast)
227
+ end
228
+
229
+ def value #:nodoc:
230
+ self
231
+ end
232
+
233
+ #
234
+ # Returns the uncompressed stream content.
235
+ #
236
+ def data
237
+ self.decode! unless decoded?
238
+
239
+ @data
240
+ end
241
+ alias decoded_data data
242
+
243
+ #
244
+ # Sets the uncompressed stream content.
245
+ # _str_:: The new uncompressed data.
246
+ #
247
+ def data=(str)
248
+ @encoded_data = nil
249
+ @data = str
250
+ end
251
+ alias decoded_data= data=
252
+
253
+ #
254
+ # Returns the raw compressed stream content.
255
+ #
256
+ def encoded_data
257
+ self.encode! unless encoded?
258
+
259
+ @encoded_data
260
+ end
261
+
262
+ #
263
+ # Sets the raw compressed stream content.
264
+ # _str_:: the new raw data.
265
+ #
266
+ def encoded_data=(str)
267
+ @encoded_data = str
268
+ @data = nil
269
+ end
270
+
271
+ #
272
+ # Uncompress the stream data.
273
+ #
274
+ def decode!
275
+ self.decrypt! if self.is_a?(Encryption::EncryptedStream)
276
+ return if decoded?
277
+
278
+ filters = self.filters
279
+ dparams = decode_params
280
+
281
+ @data = @encoded_data.dup
282
+ @data.freeze
283
+
284
+ filters.each_with_index do |filter, layer|
285
+ params = dparams[layer].is_a?(Dictionary) ? dparams[layer] : {}
286
+
287
+ # Handle Crypt filters.
288
+ if filter == :Crypt
289
+ raise Filter::Error, "Crypt filter must be the first filter" unless layer.zero?
290
+
291
+ # Skip the Crypt filter.
292
+ next
293
+ end
294
+
295
+ begin
296
+ @data = decode_data(@data, filter, params)
297
+ rescue Filter::Error => error
298
+ @data = error.decoded_data
299
+ raise
300
+ end
301
+ end
302
+
303
+ self
304
+ end
305
+
306
+ #
307
+ # Compress the stream data.
308
+ #
309
+ def encode!
310
+ return if encoded?
311
+
312
+ filters = self.filters
313
+ dparams = decode_params
314
+
315
+ @encoded_data = @data.dup
316
+ (filters.length - 1).downto(0) do |layer|
317
+ params = dparams[layer].is_a?(Dictionary) ? dparams[layer] : {}
318
+ filter = filters[layer]
319
+
320
+ # Handle Crypt filters.
321
+ if filter == :Crypt
322
+ raise Filter::Error, "Crypt filter must be the first filter" unless layer.zero?
323
+
324
+ # Skip the Crypt filter.
325
+ next
326
+ end
327
+
328
+ @encoded_data = encode_data(@encoded_data, filter, params)
329
+ end
330
+
331
+ self.Length = @encoded_data.length
332
+
333
+ self
334
+ end
335
+
336
+ def to_s(indent: 1, tab: "\t", eol: $/) #:nodoc:
337
+ content = ""
338
+
339
+ content << @dictionary.to_s(indent: indent, tab: tab)
340
+ content << "stream" + eol
341
+ content << self.encoded_data
342
+ content << eol << TOKENS.last
343
+
344
+ super(content, eol: eol)
345
+ end
346
+
347
+ def [](key) #:nodoc:
348
+ @dictionary[key]
349
+ end
350
+
351
+ def []=(key, val) #:nodoc:
352
+ @dictionary[key] = val
353
+ end
354
+
355
+ def each_key(&b) #:nodoc:
356
+ @dictionary.each_key(&b)
357
+ end
358
+
359
+ def each_pair(&b) #:nodoc
360
+ @dictionary.each_pair(&b)
361
+ end
362
+ alias each each_pair
363
+
364
+ def key?(name)
365
+ @dictionary.key?(name)
366
+ end
367
+ alias has_key? key?
368
+
369
+ def keys
370
+ @dictionary.keys
371
+ end
372
+
373
+ private
374
+
375
+ def decoded? #:nodoc:
376
+ not @data.nil?
377
+ end
378
+
379
+ def encoded? #:nodoc:
380
+ not @encoded_data.nil?
381
+ end
382
+
383
+ def each_decode_params
384
+ params = self.DecodeParms
385
+
386
+ return enum_for(__method__) do
387
+ case params
388
+ when NilClass then 0
389
+ when Array then params.length
390
+ else
391
+ 1
392
+ end
393
+ end unless block_given?
394
+
395
+ return if params.nil?
396
+
397
+ if params.is_a?(Array)
398
+ params.each do |param| yield(param) end
399
+ else
400
+ yield(params)
401
+ end
402
+
403
+ self
404
+ end
405
+
406
+ def decode_params
407
+ each_decode_params.to_a
408
+ end
409
+
410
+ def set_decode_params(layer, params) #:nodoc:
411
+ dparms = self.DecodeParms
412
+ unless dparms.is_a? ::Array
413
+ @dictionary[:DecodeParms] = dparms = []
414
+ end
415
+
416
+ if layer > dparms.length - 1
417
+ dparms.concat(::Array.new(layer - dparms.length + 1, Null.new))
418
+ end
419
+
420
+ dparms[layer] = params
421
+ @dictionary[:DecodeParms] = dparms.first if dparms.length == 1
422
+
423
+ self
424
+ end
425
+
426
+ def decode_data(data, filter, params) #:nodoc:
427
+ filter_module(filter).decode(data, params)
428
+ end
429
+
430
+ def encode_data(data, filter, params) #:nodoc:
431
+ mod = filter_module(filter)
432
+
433
+ encoded = mod.encode(data, params)
434
+
435
+ if %i[ASCIIHexDecode ASCII85Decode AHx A85].include?(filter.value)
436
+ encoded << mod::EOD
437
+ end
438
+
439
+ encoded
440
+ end
441
+
442
+ def filter_module(name)
443
+ unless name.is_a?(Name)
444
+ raise InvalidObjectStreamObjectError, "Filter has invalid type #{name.type}"
445
+ end
446
+
447
+ unless DEFINED_FILTERS.include?(name.value)
448
+ raise InvalidStreamObjectError, "Invalid filter : #{name}"
449
+ end
450
+
451
+ Filter.const_get(name.value.to_s.sub(/Decode$/, ""))
452
+ end
453
+ end
454
+
455
+ #
456
+ # Class representing an external Stream.
457
+ #
458
+ class ExternalStream < Stream
459
+
460
+ def initialize(filespec, hash = {})
461
+ hash[:F] = filespec
462
+ super('', hash)
463
+ end
464
+ end
465
+
466
+ class InvalidObjectStreamObjectError < InvalidStreamObjectError #:nodoc:
467
+ end
468
+
469
+ #
470
+ # Class representing a Stream containing other Objects.
471
+ #
472
+ class ObjectStream < Stream
473
+ include Enumerable
474
+
475
+ NUM = 0 #:nodoc:
476
+ OBJ = 1 #:nodoc:
477
+
478
+ field :Type, :Type => Name, :Default => :ObjStm, :Required => true, :Version => "1.5"
479
+ field :N, :Type => Integer, :Required => true
480
+ field :First, :Type => Integer, :Required => true
481
+ field :Extends, :Type => ObjectStream
482
+
483
+ #
484
+ # Creates a new Object Stream.
485
+ # _dictionary_:: A hash of attributes to set to the Stream.
486
+ # _raw_data_:: The Stream data.
487
+ #
488
+ def initialize(raw_data = "", dictionary = {})
489
+ super
490
+
491
+ @objects = nil
492
+ end
493
+
494
+ def pre_build #:nodoc:
495
+ load!
496
+
497
+ prolog = ""
498
+ data = ""
499
+ objoff = 0
500
+ @objects.to_a.sort.each do |num,obj|
501
+
502
+ obj.set_indirect(false)
503
+ obj.objstm_offset = objoff
504
+
505
+ prolog << "#{num} #{objoff} "
506
+ objdata = "#{obj} "
507
+
508
+ objoff += objdata.size
509
+ data << objdata
510
+ obj.set_indirect(true)
511
+ obj.no = num
512
+ end
513
+
514
+ self.data = prolog + data
515
+
516
+ @dictionary[:N] = @objects.size
517
+ @dictionary[:First] = prolog.size
518
+
519
+ super
520
+ end
521
+
522
+ #
523
+ # Adds a new Object to this Stream.
524
+ # _object_:: The Object to append.
525
+ #
526
+ def <<(object)
527
+ unless object.generation == 0
528
+ raise InvalidObjectError, "Cannot store an object with generation > 0 in an ObjectStream"
529
+ end
530
+
531
+ if object.is_a?(Stream)
532
+ raise InvalidObjectError, "Cannot store a Stream in an ObjectStream"
533
+ end
534
+
535
+ # We must have an associated document to generate new object numbers.
536
+ if @document.nil?
537
+ raise InvalidObjectError, "The ObjectStream must be added to a document before inserting objects"
538
+ end
539
+
540
+ # The object already belongs to a document.
541
+ unless object.document.nil?
542
+ object = import_object_from_document(object)
543
+ end
544
+
545
+ load!
546
+
547
+ object.no, object.generation = @document.allocate_new_object_number if object.no == 0
548
+ store_object(object)
549
+
550
+ Reference.new(object.no, 0)
551
+ end
552
+ alias insert <<
553
+
554
+ #
555
+ # Deletes Object _no_.
556
+ #
557
+ def delete(no)
558
+ load!
559
+
560
+ @objects.delete(no)
561
+ end
562
+
563
+ #
564
+ # Returns the index of Object _no_.
565
+ #
566
+ def index(no)
567
+ @objects.to_a.sort.index { |num, _| num == no }
568
+ end
569
+
570
+ #
571
+ # Returns a given decompressed object contained in the Stream.
572
+ # _no_:: The Object number.
573
+ #
574
+ def extract(no)
575
+ load!
576
+
577
+ @objects[no]
578
+ end
579
+
580
+ #
581
+ # Returns a given decompressed object by index.
582
+ # _index_:: The Object index in the ObjectStream.
583
+ #
584
+ def extract_by_index(index)
585
+ load!
586
+
587
+ raise TypeError, "index must be an integer" unless index.is_a?(::Integer)
588
+ raise IndexError, "index #{index} out of range" if index < 0 or index >= @objects.size
589
+
590
+ @objects.to_a.sort[index][1]
591
+ end
592
+
593
+ #
594
+ # Returns whether a specific object is contained in this stream.
595
+ # _no_:: The Object number.
596
+ #
597
+ def include?(no)
598
+ load!
599
+
600
+ @objects.include?(no)
601
+ end
602
+
603
+ #
604
+ # Iterates over each object in the stream.
605
+ #
606
+ def each(&b)
607
+ load!
608
+
609
+ @objects.values.each(&b)
610
+ end
611
+ alias each_object each
612
+
613
+ #
614
+ # Returns the number of objects contained in the stream.
615
+ #
616
+ def length
617
+ raise InvalidObjectStreamObjectError, "Invalid number of objects" unless self.N.is_a?(Integer)
618
+
619
+ self.N.to_i
620
+ end
621
+
622
+ #
623
+ # Returns the array of inner objects.
624
+ #
625
+ def objects
626
+ load!
627
+
628
+ @objects.values
629
+ end
630
+
631
+ private
632
+
633
+ #
634
+ # Preprocess the object in case it already belongs to a document.
635
+ # If the document is the same as the current object stream, remove the duplicate object from our document.
636
+ # If the object comes from another document, use the export method to create a version without references.
637
+ #
638
+ def import_object_from_document(object)
639
+ obj_doc = object.document
640
+
641
+ # Remove the previous instance if the object is indirect to avoid duplicates.
642
+ if obj_doc.equal?(@document)
643
+ @document.delete_object(object.reference) if object.indirect?
644
+
645
+ # Otherwise, create a exported version of the object.
646
+ else
647
+ object = object.export
648
+ end
649
+
650
+ object
651
+ end
652
+
653
+ def store_object(object) #:nodoc:
654
+ object.set_indirect(true) # all stored objects are indirect.
655
+ object.parent = self # set this stream as the parent.
656
+ object.set_document(@document) # inherit document information.
657
+
658
+ @objects[object.no] = object
659
+ end
660
+
661
+ def load! #:nodoc:
662
+ return unless @objects.nil?
663
+
664
+ decode!
665
+
666
+ @objects = {}
667
+ return if @data.empty?
668
+
669
+ data = StringScanner.new(@data)
670
+ nums = []
671
+ offsets = []
672
+ first_offset = first_object_offset
673
+
674
+ self.length.times do
675
+ nums << Integer.parse(data).to_i
676
+ offsets << Integer.parse(data).to_i
677
+ end
678
+
679
+ self.length.times do |i|
680
+ unless (0...@data.size).cover?(first_object_offset + offsets[i]) and offsets[i] >= 0
681
+ raise InvalidObjectStreamObjectError, "Invalid offset '#{offsets[i]} for object #{nums[i]}"
682
+ end
683
+
684
+ data.pos = first_offset + offsets[i]
685
+ type = Object.typeof(data)
686
+ raise InvalidObjectStreamObjectError,
687
+ "Bad embedded object format in object stream" if type.nil?
688
+
689
+ embeddedobj = type.parse(data)
690
+ embeddedobj.no = nums[i] # object number
691
+ embeddedobj.objstm_offset = offsets[i]
692
+
693
+ store_object(embeddedobj)
694
+ end
695
+ end
696
+
697
+ def first_object_offset #:nodoc:
698
+ raise InvalidObjectStreamObjectError, "Invalid First offset" unless self.First.is_a?(Integer)
699
+ raise InvalidObjectStreamObjectError, "Negative object offset" if self.First < 0
700
+
701
+ return self.First.to_i
702
+ end
703
+ end
704
+
705
+ end