Class: Origami::Stream

Inherits:
Object
  • Object
show all
Includes:
Object, StandardObject
Defined in:
lib/origami/stream.rb,
lib/origami/obfuscation.rb

Overview

Class representing a PDF Stream Object. Streams can be used to hold any kind of data, especially binary data.

Constant Summary collapse

TOKENS =

:nodoc:

[ "stream" + WHITECHARS_NORET  + "\\r?\\n", "endstream" ]
@@regexp_open =
Regexp.new(WHITESPACES + TOKENS.first)
@@regexp_close =
Regexp.new(TOKENS.last)
@@cast_fingerprints =
{}
@@defined_filters =

Actually only 5 first ones are implemented, other ones are mainly about image data processing (JPEG, JPEG2000 …)

%i[
  ASCIIHexDecode
  ASCII85Decode
  LZWDecode
  FlateDecode
  RunLengthDecode
   CCITTFaxDecode
  JBIG2Decode
  DCTDecode
  JPXDecode
   AHx
  A85
  LZW
  Fl
  RL
  CCF
  DCT
]

Constants included from StandardObject

Origami::StandardObject::DEFAULT_ATTRIBUTES

Instance Attribute Summary collapse

Attributes included from Object

#file_offset, #generation, #no, #objstm_offset, #parent

Class Method Summary collapse

Instance Method Summary collapse

Methods included from StandardObject

#do_type_check, #has_field?, included, #set_default_value, #set_default_values, #version_required

Methods included from Object

#<=>, #copy, #document, #export, #indirect?, #indirect_parent, #logicalize, #logicalize!, #native_type, #reference, #set_document, #set_indirect, skip_until_next_obj, #solve, #to_o, #type, typeof, #version_required, #xrefs

Constructor Details

#initialize(data = "", dictionary = {}) ⇒ Stream

Creates a new PDF Stream.

data

The Stream uncompressed data.

dictionary

A hash representing the Stream attributes.



87
88
89
90
91
92
93
94
95
# File 'lib/origami/stream.rb', line 87

def initialize(data = "", dictionary = {})
    super()

    set_indirect(true)

    @encoded_data = nil
    @dictionary, @data = Dictionary.new(dictionary), data
    @dictionary.parent = self
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(field, *args) ⇒ Object (private)

:nodoc:



414
415
416
417
418
419
420
421
# File 'lib/origami/stream.rb', line 414

def method_missing(field, *args) #:nodoc:
    if field.to_s[-1,1] == '='
        self[field.to_s[0..-2].to_sym] = args.first
    else
        obj = self[field];
        obj.is_a?(Reference) ? obj.solve : obj
    end
end

Instance Attribute Details

#dictionaryObject

Returns the value of attribute dictionary.



72
73
74
# File 'lib/origami/stream.rb', line 72

def dictionary
  @dictionary
end

Class Method Details

.add_type_info(typeclass, key, value) ⇒ Object

:nodoc:



164
165
166
167
168
169
170
171
172
# File 'lib/origami/stream.rb', line 164

def self.add_type_info(typeclass, key, value) #:nodoc:
    if not @@cast_fingerprints.has_key?(typeclass) and typeclass.superclass != Stream and
         @@cast_fingerprints.has_key?(typeclass.superclass)
        @@cast_fingerprints[typeclass] = @@cast_fingerprints[typeclass.superclass].dup
    end

    @@cast_fingerprints[typeclass] ||= {}
    @@cast_fingerprints[typeclass][key.to_o] = value.to_o
end

.guess_type(hash) ⇒ Object

:nodoc:



174
175
176
177
178
179
180
181
182
183
184
# File 'lib/origami/stream.rb', line 174

def self.guess_type(hash) #:nodoc:
    best_type = self

    @@cast_fingerprints.each_pair do |klass, keys|
        next unless klass < best_type

        best_type = klass if keys.all? { |k,v| hash[k] == v }
    end

    best_type
end

.native_typeObject



410
# File 'lib/origami/stream.rb', line 410

def self.native_type ; Stream end

.parse(stream, parser = nil) ⇒ Object

:nodoc:



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/origami/stream.rb', line 116

def self.parse(stream, parser = nil) #:nodoc:
    dictionary = Dictionary.parse(stream, parser)
    return dictionary if not stream.skip(@@regexp_open)

    length = dictionary[:Length]
    if not length.is_a?(Integer)
        raw_data = stream.scan_until(@@regexp_close)
        if raw_data.nil?
            raise InvalidStreamObjectError,
                    "Stream shall end with a 'endstream' statement"
        end
    else
        length = length.value
        raw_data = stream.peek(length)
        stream.pos += length

        if not ( unmatched = stream.scan_until(@@regexp_close) )
            raise InvalidStreamObjectError,
                "Stream shall end with a 'endstream' statement"
        end

        raw_data << unmatched
    end

    stm =
        if Origami::OPTIONS[:enable_type_guessing]
            self.guess_type(dictionary).new('', dictionary.to_h)
        else
            Stream.new('', dictionary.to_h)
        end

    raw_data.chomp!(TOKENS.last)

    if raw_data[-1,1] == "\n"
        if raw_data[-2,1] == "\r"
            raw_data = raw_data[0, raw_data.size - 2]
        else
            raw_data = raw_data[0, raw_data.size - 1]
        end
    end
    #raw_data.chomp! if length.is_a?(Integer) and length < raw_data.length

    stm.encoded_data = raw_data
    stm.file_offset = dictionary.file_offset

    stm
end

Instance Method Details

#[](key) ⇒ Object

:nodoc:



393
394
395
# File 'lib/origami/stream.rb', line 393

def [](key) #:nodoc:
    @dictionary[key]
end

#[]=(key, val) ⇒ Object

:nodoc:



397
398
399
# File 'lib/origami/stream.rb', line 397

def []=(key,val) #:nodoc:
    @dictionary[key] = val
end

#cast_to(type, _parser = nil) ⇒ Object



243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/origami/stream.rb', line 243

def cast_to(type, _parser = nil)
    super(type)

    cast = type.new("", self.dictionary.to_h)
    cast.encoded_data = self.encoded_data.dup
    cast.no, cast.generation = self.no, self.generation
    cast.set_indirect(true)
    cast.set_document(self.document)
    cast.file_offset = self.file_offset

    cast
end

#dataObject Also known as: decoded_data

Returns the uncompressed stream content.



263
264
265
266
267
# File 'lib/origami/stream.rb', line 263

def data
    self.decode! unless self.decoded?

    @data
end

#data=(str) ⇒ Object Also known as: decoded_data=

Sets the uncompressed stream content.

str

The new uncompressed data.



274
275
276
277
# File 'lib/origami/stream.rb', line 274

def data=(str)
    @encoded_data = nil
    @data = str
end

#decode!Object

Uncompress the stream data.



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/origami/stream.rb', line 301

def decode!
    self.decrypt! if self.is_a?(Encryption::EncryptedStream)
    return if decoded?

    filters = self.filters

    if filters.empty?
        @data = @encoded_data.dup
        return
    end

    unless filters.all?{|filter| filter.is_a?(Name)}
        raise InvalidStreamObjectError, "Invalid Filter type parameter"
    end

    dparams = decode_params

    @data = @encoded_data.dup
    @data.freeze

    filters.each_with_index do |filter, layer|
        params = dparams[layer].is_a?(Dictionary) ? dparams[layer] : {}

        # Handle Crypt filters.
        if filter == :Crypt
            raise Filter::Error, "Crypt filter must be the first filter" unless layer.zero?

            # Skip the Crypt filter.
            next
        end

        begin
            @data = decode_data(@data, filter, params)
        rescue Filter::Error => error
            @data = error.decoded_data if error.decoded_data
            raise
        end
    end

    self
end

#each_filterObject

Iterates over each Filter in the Stream.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/origami/stream.rb', line 189

def each_filter
    filters = self.Filter

    return enum_for(__method__) do
        case filters
        when NilClass then 0
        when Array then filters.length
        else
            1
        end
    end unless block_given?

    return if filters.nil?

    if filters.is_a?(Array)
        filters.each do |filter| yield(filter) end
    else
        yield(filters)
    end

    self
end

#each_key(&b) ⇒ Object

:nodoc:



401
402
403
# File 'lib/origami/stream.rb', line 401

def each_key(&b) #:nodoc:
    @dictionary.each_key(&b)
end

#encode!Object

Compress the stream data.



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/origami/stream.rb', line 346

def encode!
    return if encoded?
    filters = self.filters

    if filters.empty?
        @encoded_data = @data.dup
        return
    end

    unless filters.all?{|filter| filter.is_a?(Name)}
        raise InvalidStreamObjectError, "Invalid Filter type parameter"
    end

    dparams = decode_params

    @encoded_data = @data.dup
    (filters.length - 1).downto(0) do |layer|
        params = dparams[layer].is_a?(Dictionary) ? dparams[layer] : {}
        filter = filters[layer]

        # Handle Crypt filters.
        if filter == :Crypt
            raise Filter::Error, "Crypt filter must be the first filter" unless layer.zero?

            # Skip the Crypt filter.
            next
        end

        @encoded_data = encode_data(@encoded_data, filter, params)
    end

    self.Length = @encoded_data.length

    self
end

#encoded_dataObject

Returns the raw compressed stream content.



283
284
285
286
287
# File 'lib/origami/stream.rb', line 283

def encoded_data
    self.encode! unless self.encoded?

    @encoded_data
end

#encoded_data=(str) ⇒ Object

Sets the raw compressed stream content.

str

the new raw data.



293
294
295
296
# File 'lib/origami/stream.rb', line 293

def encoded_data=(str)
    @encoded_data = str
    @data = nil
end

#filtersObject

Returns an Array of Filters for this Stream.



215
216
217
# File 'lib/origami/stream.rb', line 215

def filters
    self.each_filter.to_a
end

#key?(name) ⇒ Boolean Also known as: has_key?

Returns:



405
406
407
# File 'lib/origami/stream.rb', line 405

def key?(name)
    @dictionary.key?(name)
end

#post_buildObject



110
111
112
113
114
# File 'lib/origami/stream.rb', line 110

def post_build
    self.Length = @encoded_data.length

    super
end

#pre_buildObject



104
105
106
107
108
# File 'lib/origami/stream.rb', line 104

def pre_build
    encode!

    super
end

#set_predictor(predictor, colors: 1, bitspercomponent: 8, columns: 1) ⇒ Object

Set predictor type for the current Stream. Applies only for LZW and FlateDecode filters.



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/origami/stream.rb', line 223

def set_predictor(predictor, colors: 1, bitspercomponent: 8, columns: 1)
    filters = self.filters

    unless filters.include?(:FlateDecode) or filters.include?(:LZWDecode)
        raise InvalidStreamObjectError, 'Predictor functions can only be used with Flate or LZW filters'
    end

    layer = filters.index(:FlateDecode) or filters.index(:LZWDecode)

    params = Filter::LZW::DecodeParms.new
    params[:Predictor] = predictor
    params[:Colors] = colors if colors != 1
    params[:BitsPerComponent] = bitspercomponent if bitspercomponent != 8
    params[:Columns] = columns if columns != 1

    set_decode_params(layer, params)

    self
end

#to_obfuscated_strObject



220
221
222
223
224
225
226
227
228
229
# File 'lib/origami/obfuscation.rb', line 220

def to_obfuscated_str
    content = ""

    content << @dictionary.to_obfuscated_str
    content << "stream" + EOL
    content << self.encoded_data
    content << EOL << TOKENS.last

    super(content)
end

#to_s(indent: 1, tab: "\t") ⇒ Object

:nodoc:



382
383
384
385
386
387
388
389
390
391
# File 'lib/origami/stream.rb', line 382

def to_s(indent: 1, tab: "\t") #:nodoc:
    content = ""

    content << @dictionary.to_s(indent: indent, tab: tab)
    content << "stream" + EOL
    content << self.encoded_data
    content << EOL << TOKENS.last

    super(content)
end

#valueObject

:nodoc:



256
257
258
# File 'lib/origami/stream.rb', line 256

def value #:nodoc:
    self
end