Class: PNGlitch::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/pnglitch/base.rb

Overview

Base is the class that represents the interface for PNGlitch functions.

It will be initialized through PNGlitch#open and be a mainly used instance.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file, limit_of_decompressed_data_size = nil) ⇒ Base

Instanciate the class with the passed file



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/pnglitch/base.rb', line 15

def initialize file, limit_of_decompressed_data_size = nil
  path = Pathname.new file
  @head_data = StringIO.new
  @tail_data = StringIO.new
  @compressed_data = Tempfile.new 'compressed', encoding: 'ascii-8bit'
  @filtered_data = Tempfile.new 'filtered', encoding: 'ascii-8bit'
  @idat_chunk_size = nil

  @head_data.binmode
  @tail_data.binmode
  @compressed_data.binmode
  @filtered_data.binmode

  open(path, 'rb') do |io|
    idat_sizes = []
    @head_data << io.read(8) # signature
    while bytes = io.read(8)
      length, type = bytes.unpack 'Na*'
      if length > io.size - io.pos
          raise FormatError.new path.to_s
      end
      if type == 'IHDR'
        ihdr = {
          width:              io.read(4).unpack('N').first,
          height:             io.read(4).unpack('N').first,
          bit_depth:          io.read(1).unpack('C').first,
          color_type:         io.read(1).unpack('C').first,
          compression_method: io.read(1).unpack('C').first,
          filter_method:      io.read(1).unpack('C').first,
          interlace_method:   io.read(1).unpack('C').first,
        }
        @width = ihdr[:width]
        @height = ihdr[:height]
        @interlace = ihdr[:interlace_method]
        @sample_size = {0 => 1, 2 => 3, 3 => 1, 4 => 2, 6 => 4}[ihdr[:color_type]]
        io.pos -= 13
      end
      if type == 'IDAT'
        @compressed_data << io.read(length)
        idat_sizes << length
        io.pos += 4 # crc
      else
        target_io = @compressed_data.pos == 0 ? @head_data : @tail_data
        target_io << bytes
        target_io << io.read(length + 4)
      end
    end
    @idat_chunk_size = idat_sizes.first if idat_sizes.size > 1
  end
  if @compressed_data.size == 0
    raise FormatError.new path.to_s
  end
  @head_data.rewind
  @tail_data.rewind
  @compressed_data.rewind
  decompressed_size = 0
  expected_size = (1 + @width * @sample_size) * @height
  expected_size = limit_of_decompressed_data_size unless limit_of_decompressed_data_size.nil?
  z = Zlib::Inflate.new
  z.inflate(@compressed_data.read) do |chunk|
    decompressed_size += chunk.size
    # raise error when the data size goes over 2 times the usually expected size
    if decompressed_size > expected_size * 2
      z.close
      self.close
      raise DataSizeError.new path.to_s, decompressed_size, expected_size
    end
    @filtered_data << chunk
  end
  z.close
  @compressed_data.rewind
  @filtered_data.rewind
  @is_compressed_data_modified = false
end

Instance Attribute Details

#compressed_dataObject

Returns the value of attribute compressed_data.



10
11
12
# File 'lib/pnglitch/base.rb', line 10

def compressed_data
  @compressed_data
end

#filtered_dataObject

Returns the value of attribute filtered_data.



10
11
12
# File 'lib/pnglitch/base.rb', line 10

def filtered_data
  @filtered_data
end

#head_dataObject

Returns the value of attribute head_data.



10
11
12
# File 'lib/pnglitch/base.rb', line 10

def head_data
  @head_data
end

#heightObject

Returns the value of attribute height.



9
10
11
# File 'lib/pnglitch/base.rb', line 9

def height
  @height
end

#idat_chunk_sizeObject

Returns the value of attribute idat_chunk_size.



10
11
12
# File 'lib/pnglitch/base.rb', line 10

def idat_chunk_size
  @idat_chunk_size
end

#is_compressed_data_modifiedObject (readonly)

Returns the value of attribute is_compressed_data_modified.



9
10
11
# File 'lib/pnglitch/base.rb', line 9

def is_compressed_data_modified
  @is_compressed_data_modified
end

#sample_sizeObject (readonly)

Returns the value of attribute sample_size.



9
10
11
# File 'lib/pnglitch/base.rb', line 9

def sample_size
  @sample_size
end

#tail_dataObject

Returns the value of attribute tail_data.



10
11
12
# File 'lib/pnglitch/base.rb', line 10

def tail_data
  @tail_data
end

#widthObject

Returns the value of attribute width.



9
10
11
# File 'lib/pnglitch/base.rb', line 9

def width
  @width
end

Instance Method Details

#apply_filters(prev_filters = nil, filter_codecs = nil) ⇒ Object

(Re-)computes the filtering methods on each scanline.



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/pnglitch/base.rb', line 200

def apply_filters prev_filters = nil, filter_codecs = nil
  prev_filters = filter_types if prev_filters.nil?
  filter_codecs = [] if filter_codecs.nil?
  current_filters = []
  prev = nil
  line_sizes = []
  scanline_positions.push(@filtered_data.size).inject do |m, n|
    line_sizes << n - m - 1
    n
  end
  wrap_with_rewind(@filtered_data) do
    # decode all scanlines
    prev_filters.each_with_index do |type, i|
      byte = @filtered_data.read 1
      current_filters << byte.unpack('C').first
      line_size = line_sizes[i]
      line = @filtered_data.read line_size
      filter = Filter.new type, @sample_size
      if filter_codecs[i] && filter_codecs[i][:decoder]
        filter.decoder = filter_codecs[i][:decoder]
      end
      if !prev.nil? && @interlace_pass_count.include?(i + 1)  # make sure prev to be nil if interlace pass is changed
        prev = nil
      end
      decoded = filter.decode line, prev
      @filtered_data.pos -= line_size
      @filtered_data << decoded
      prev = decoded
    end
    # encode all
    filter_codecs.reverse!
    line_sizes.reverse!
    data_amount = @filtered_data.pos # should be eof
    ref = data_amount
    current_filters.reverse_each.with_index do |type, i|
      line_size = line_sizes[i]
      ref -= line_size + 1
      @filtered_data.pos = ref + 1
      line = @filtered_data.read line_size
      prev = nil
      if !line_sizes[i + 1].nil?
        @filtered_data.pos = ref - line_size
        prev = @filtered_data.read line_size
      end
      # make sure prev to be nil if interlace pass is changed
      if @interlace_pass_count.include?(current_filters.size - i)
        prev = nil
      end
      filter = Filter.new type, @sample_size
      if filter_codecs[i] && filter_codecs[i][:encoder]
        filter.encoder = filter_codecs[i][:encoder]
      end
      encoded = filter.encode line, prev
      @filtered_data.pos = ref + 1
      @filtered_data << encoded
    end
  end
end

#change_all_filters(filter_type) ⇒ Object

Changes filter type values to passed filter_type in all scanlines



400
401
402
403
404
405
406
# File 'lib/pnglitch/base.rb', line 400

def change_all_filters filter_type
  each_scanline do |line|
    line.change_filter filter_type
  end
  compress
  self
end

#closeObject

Explicit file close.

It will close tempfiles that used internally.



95
96
97
98
99
# File 'lib/pnglitch/base.rb', line 95

def close
  @compressed_data.close
  @filtered_data.close
  self
end

#compress(level = Zlib::DEFAULT_COMPRESSION, window_bits = Zlib::MAX_WBITS, mem_level = Zlib::DEF_MEM_LEVEL, strategy = Zlib::DEFAULT_STRATEGY) ⇒ Object

Re-compress the filtered data.

All arguments are for Zlib. See the document of Zlib::Deflate.new for more detail.



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/pnglitch/base.rb', line 264

def compress(
  level = Zlib::DEFAULT_COMPRESSION,
  window_bits = Zlib::MAX_WBITS,
  mem_level = Zlib::DEF_MEM_LEVEL,
  strategy = Zlib::DEFAULT_STRATEGY
)
  wrap_with_rewind(@compressed_data, @filtered_data) do
    z = Zlib::Deflate.new level, window_bits, mem_level, strategy
    until @filtered_data.eof? do
      buffer_size = 2 ** 16
      flush = Zlib::NO_FLUSH
      flush = Zlib::FINISH if @filtered_data.size - @filtered_data.pos < buffer_size
      @compressed_data << z.deflate(@filtered_data.read(buffer_size), flush)
    end
    z.finish
    z.close
    truncate_io @compressed_data
  end
  @is_compressed_data_modified = false
  self
end

#each_scanlineObject

Process each scanline.

It takes a block with a parameter. The parameter must be an instance of PNGlitch::Scanline and it provides ways to edit the filter type and the data of the scanlines. Normally it iterates the number of the PNG image height.

Here is some examples:

pnglitch.each_scanline do |line|
  line.gsub!(/\w/, '0') # replace all alphabetical chars in data
end

pnglicth.each_scanline do |line|
  line.change_filter 3  # change all filter to 3, data will get re-filtering (it won't be a glitch)
end

pnglicth.each_scanline do |line|
  line.graft 3          # change all filter to 3 and data remains (it will be a glitch)
end

See PNGlitch::Scanline for more details.

This method is safer than glitch but will be a little bit slow.


Please note that each_scanline will apply the filters after the loop. It means a following example doesn’t work as expected.

pnglicth.each_scanline do |line|
  line.change_filter 3
  line.gsub! /\d/, 'x'  # wants to glitch after changing filters.
end

To glitch after applying the new filter types, it should be called separately like:

pnglicth.each_scanline do |line|
  line.change_filter 3
end
pnglicth.each_scanline do |line|
  line.gsub! /\d/, 'x'
end


330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/pnglitch/base.rb', line 330

def each_scanline # :yield: scanline
  return enum_for :each_scanline unless block_given?
  prev_filters = self.filter_types
  is_refilter_needed = false
  filter_codecs = []
  wrap_with_rewind(@filtered_data) do
    at = 0
    scanline_positions.push(@filtered_data.size).inject do |pos, delimit|
      scanline = Scanline.new @filtered_data, pos, (delimit - pos - 1), at
      yield scanline
      if fabricate_scanline(scanline, prev_filters, filter_codecs)
        is_refilter_needed = true
      end
      at += 1
      delimit
    end
  end
  apply_filters(prev_filters, filter_codecs) if is_refilter_needed
  compress
  self
end

#fabricate_scanline(scanline, prev_filters, filter_codecs) ⇒ Object

:nodoc:



382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/pnglitch/base.rb', line 382

def fabricate_scanline scanline, prev_filters, filter_codecs # :nodoc:
  at = scanline.index
  is_refilter_needed = false
  unless scanline.prev_filter_type.nil?
    is_refilter_needed = true
  else
    prev_filters[at] = scanline.filter_type
  end
  codec = filter_codecs[at] = scanline.filter_codec
  if !codec[:encoder].nil? || !codec[:decoder].nil?
    is_refilter_needed = true
  end
  is_refilter_needed
end

#filter_typesObject

Returns an array of each scanline’s filter type value.



104
105
106
107
108
109
110
111
112
113
114
# File 'lib/pnglitch/base.rb', line 104

def filter_types
  types = []
  wrap_with_rewind(@filtered_data) do
    scanline_positions.each do |pos|
      @filtered_data.pos = pos
      byte = @filtered_data.read 1
      types << byte.unpack('C').first
    end
  end
  types
end

#glitch(&block) ⇒ Object

Manipulates the filtered (decompressed) data as String.

To set a glitched result, return the modified value in the block.

Example:

p = PNGlitch.open 'path/to/your/image.png'
p.glitch do |data|
  data.gsub /\d/, 'x'
end
p.save 'path/to/broken/image.png'
p.close

This operation has the potential to damage filter type bytes. The damage will be a cause of glitching but some viewer applications might deny to process those results. To be polite to the filter types, use each_scanline instead.

Since this method sets the decompressed data into String, it may use a massive amount of memory. To decrease the memory usage, treat the data as IO through glitch_as_io instead.



137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/pnglitch/base.rb', line 137

def glitch &block   # :yield: data
  warn_if_compressed_data_modified

  wrap_with_rewind(@filtered_data) do
    result = yield @filtered_data.read
    @filtered_data.rewind
    @filtered_data << result
    truncate_io @filtered_data
  end
  compress
  self
end

#glitch_after_compress(&block) ⇒ Object

Manipulates the after-compressed data as String.

To set a glitched result, return the modified value in the block.

Once the compressed data is glitched, PNGlitch will warn about modifications to filtered (decompressed) data because this method does not decompress the glitched compressed data again. It means that calling glitch after glitch_after_compress will make the result overwritten and forgotten.

This operation will often destroy PNG image completely.



175
176
177
178
179
180
181
182
183
184
# File 'lib/pnglitch/base.rb', line 175

def glitch_after_compress &block   # :yield: data
  wrap_with_rewind(@compressed_data) do
    result = yield @compressed_data.read
    @compressed_data.rewind
    @compressed_data << result
    truncate_io @compressed_data
  end
  @is_compressed_data_modified = true
  self
end

#glitch_after_compress_as_io(&block) ⇒ Object

Manipulates the after-compressed data as IO.



189
190
191
192
193
194
195
# File 'lib/pnglitch/base.rb', line 189

def glitch_after_compress_as_io &block # :yield: data
  wrap_with_rewind(@compressed_data) do
    yield @compressed_data
  end
  @is_compressed_data_modified = true
  self
end

#glitch_as_io(&block) ⇒ Object

Manipulates the filtered (decompressed) data as IO.



153
154
155
156
157
158
159
160
161
# File 'lib/pnglitch/base.rb', line 153

def glitch_as_io &block # :yield: data
  warn_if_compressed_data_modified

  wrap_with_rewind(@filtered_data) do
    yield @filtered_data
  end
  compress
  self
end

#interlaced?Boolean

Checks if it is interlaced.

Returns:

  • (Boolean)


411
412
413
# File 'lib/pnglitch/base.rb', line 411

def interlaced?
  @interlace == 1
end

#save(file) ⇒ Object Also known as: output

Save to the file.



458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/pnglitch/base.rb', line 458

def save file
  wrap_with_rewind(@head_data, @tail_data, @compressed_data) do
    open(file, 'wb') do |io|
      io << @head_data.read
      chunk_size = @idat_chunk_size || @compressed_data.size
      type = 'IDAT'
      until @compressed_data.eof? do
        data = @compressed_data.read(chunk_size)
        io << [data.size].pack('N')
        io << type
        io << data
        io << [Zlib.crc32(data, Zlib.crc32(type))].pack('N')
      end
      io << @tail_data.read
    end
  end
  self
end

#scanline_at(index_or_range) ⇒ Object

Access particular scanline(s) at passed index_or_range.

It returns a single Scanline or an array of Scanline.



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/pnglitch/base.rb', line 357

def scanline_at index_or_range
  base = self
  prev_filters = self.filter_types
  filter_codecs = Array.new(prev_filters.size)
  scanlines = []
  index_or_range = self.filter_types.size - 1 if index_or_range == -1
  range = index_or_range.is_a?(Range) ? index_or_range : [index_or_range]

  at = 0
  scanline_positions.push(@filtered_data.size).inject do |pos, delimit|
    if range.include? at
      s = Scanline.new(@filtered_data, pos, (delimit - pos - 1), at) do |scanline|
        if base.fabricate_scanline(scanline, prev_filters, filter_codecs)
          base.apply_filters(prev_filters, filter_codecs)
        end
        base.compress
      end
      scanlines << s
    end
    at += 1
    delimit
  end
  scanlines.size <= 1 ? scanlines.first : scanlines
end