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:



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

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:



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

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:



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
163
164
# File 'lib/origami/stream.rb', line 118

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:



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

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

#[]=(key, val) ⇒ Object

:nodoc:



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

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

#cast_to(type, _parser = nil) ⇒ Object



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

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.



268
269
270
271
272
# File 'lib/origami/stream.rb', line 268

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.



279
280
281
282
# File 'lib/origami/stream.rb', line 279

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

#decode!Object

Uncompress the stream data.



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
345
346
# File 'lib/origami/stream.rb', line 306

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.



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

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:



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

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

#encode!Object

Compress the stream data.



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
384
385
# File 'lib/origami/stream.rb', line 351

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.



288
289
290
291
292
# File 'lib/origami/stream.rb', line 288

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.



298
299
300
301
# File 'lib/origami/stream.rb', line 298

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

#filtersObject

Returns an Array of Filters for this Stream.



221
222
223
# File 'lib/origami/stream.rb', line 221

def filters
    self.each_filter.to_a
end

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

Returns:



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

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

#post_buildObject



112
113
114
115
116
# File 'lib/origami/stream.rb', line 112

def post_build
    self.Length = @encoded_data.length

    super
end

#pre_buildObject



106
107
108
109
110
# File 'lib/origami/stream.rb', line 106

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.



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

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:



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

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:



261
262
263
# File 'lib/origami/stream.rb', line 261

def value #:nodoc:
    self
end