Class: Origami::Stream

Inherits:
Object
  • Object
show all
Includes:
FieldAccessor, 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|\\r|\\n)" , "endstream" ]
@@regexp_open =
Regexp.new(WHITESPACES + TOKENS.first)
@@regexp_close =
Regexp.new(TOKENS.last)
@@type_signatures =
{}
@@type_keys =
[]
@@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 FieldAccessor

#method_missing, #respond_to_missing?

Methods included from StandardObject

included, #version_required

Methods included from Object

#<=>, #copy, #document, #export, included, #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.



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

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 in the class Origami::FieldAccessor

Instance Attribute Details

#dictionaryObject

Returns the value of attribute dictionary.



74
75
76
# File 'lib/origami/stream.rb', line 74

def dictionary
  @dictionary
end

Class Method Details

.add_type_signature(key, value) ⇒ Object

:nodoc:



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

def self.add_type_signature(key, value) #:nodoc:
    key, value = key.to_o, value.to_o

    # Inherit the superclass type information.
    if not @@type_signatures.key?(self) and @@type_signatures.key?(self.superclass)
        @@type_signatures[self] = @@type_signatures[self.superclass].dup
    end

    @@type_signatures[self] ||= {}
    @@type_signatures[self][key] = value

    @@type_keys.push(key) unless @@type_keys.include?(key)
end

.guess_type(hash) ⇒ Object

:nodoc:



178
179
180
181
182
183
184
185
186
187
188
# File 'lib/origami/stream.rb', line 178

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

    @@type_signatures.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

.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)
        else
            Stream.new('', dictionary)
        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:



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

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

#[]=(key, val) ⇒ Object

:nodoc:



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

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

#cast_to(type, _parser = nil) ⇒ Object



246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/origami/stream.rb', line 246

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

    cast = type.new("", self.dictionary.copy)
    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.



266
267
268
269
270
# File 'lib/origami/stream.rb', line 266

def data
    self.decode! unless decoded?

    @data
end

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

Sets the uncompressed stream content.

str

The new uncompressed data.



277
278
279
280
# File 'lib/origami/stream.rb', line 277

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

#decode!Object

Uncompress the stream data.



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
342
343
344
# File 'lib/origami/stream.rb', line 304

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.



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/origami/stream.rb', line 193

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:



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

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

#encode!Object

Compress the stream data.



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
381
382
383
# File 'lib/origami/stream.rb', line 349

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.



286
287
288
289
290
# File 'lib/origami/stream.rb', line 286

def encoded_data
    self.encode! unless encoded?

    @encoded_data
end

#encoded_data=(str) ⇒ Object

Sets the raw compressed stream content.

str

the new raw data.



296
297
298
299
# File 'lib/origami/stream.rb', line 296

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

#filtersObject

Returns an Array of Filters for this Stream.



219
220
221
# File 'lib/origami/stream.rb', line 219

def filters
    self.each_filter.to_a
end

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

Returns:



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

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.



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/origami/stream.rb', line 227

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

    layer = filters.index(:FlateDecode) or filters.index(:LZWDecode)
    if layer.nil?
        raise InvalidStreamObjectError, 'Predictor functions can only be used with Flate or LZW filters'
    end

    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:



385
386
387
388
389
390
391
392
393
394
# File 'lib/origami/stream.rb', line 385

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:



259
260
261
# File 'lib/origami/stream.rb', line 259

def value #:nodoc:
    self
end