class AppInfo::PngUncrush

Public Class Methods

decompress(input, output) click to toggle source
# File lib/app_info/png_uncrush.rb, line 63
def self.decompress(input, output)
  new(input).decompress(output)
end
dimensions(input) click to toggle source
# File lib/app_info/png_uncrush.rb, line 67
def self.dimensions(input)
  new(input).dimensions
end
new(filename) click to toggle source
# File lib/app_info/png_uncrush.rb, line 71
def initialize(filename)
  @io = PngReader.new(File.open(filename))
  raise FormatError, 'not a png file' unless @io.png?
end

Public Instance Methods

decompress(output) click to toggle source
# File lib/app_info/png_uncrush.rb, line 80
def decompress(output)
  content = _remap(_dump_sections)
  return false unless content

  write_file(output, content)
rescue Zlib::DataError
  # perhops thi is a normal png image file
  false
end
dimensions() click to toggle source
# File lib/app_info/png_uncrush.rb, line 76
def dimensions
  _dump_sections(dimensions: true)
end

Private Instance Methods

_dump_sections(dimensions: false) click to toggle source
# File lib/app_info/png_uncrush.rb, line 92
def _dump_sections(dimensions: false)
  pos = @io.header.size
  optimized = false
  [].tap do |sections|
    while pos < @io.size
      type = @io[pos + 4, 4]
      length = @io[pos, 4].unpack1('N')
      data = @io[pos + 8, length]
      crc = @io[pos + 8 + length, 4].unpack1('N')
      pos += length + 12

      if type == 'CgBI'
        optimized = true
        next
      end

      if type == 'IHDR'
        width = data[0, 4].unpack1('N')
        height = data[4, 4].unpack1('N')
        return [width, height] if dimensions
      end

      break if type == 'IEND'

      if type == 'IDAT' && sections&.last&.first == 'IDAT'
        # Append to the previous IDAT
        sections.last[1] += length
        sections.last[2] += data
      else
        sections << [type, length, data, crc, width, height]
      end
    end
  end
end
_remap(sections) click to toggle source
# File lib/app_info/png_uncrush.rb, line 132
def _remap(sections)
  new_png = String.new(@io.header)
  sections.map do |(type, length, data, crc, width, height)|
    if type == 'IDAT'
      buff_size = width * height * 4 + height
      data = inflate(data[0, buff_size])
      # duplicate the content of old data at first to avoid creating too many string objects
      newdata = String.new(data)
      pos = 0

      (0...height).each do |_|
        newdata[pos] = data[pos, 1]
        pos += 1
        (0...width).each do |_|
          newdata[pos + 0] = data[pos + 2, 1]
          newdata[pos + 1] = data[pos + 1, 1]
          newdata[pos + 2] = data[pos + 0, 1]
          newdata[pos + 3] = data[pos + 3, 1]
          pos += 4
        end
      end

      data = deflate(newdata)
      length = data.length
      crc = Zlib.crc32(type)
      crc = Zlib.crc32(data, crc)
      crc = (crc + 0x100000000) % 0x100000000
    end

    new_png += [length].pack('N') + type + (data if length.positive?) + [crc].pack('N')
  end

  new_png
end
deflate(data) click to toggle source
# File lib/app_info/png_uncrush.rb, line 176
def deflate(data)
  Zlib::Deflate.deflate(data)
end
inflate(data) click to toggle source
# File lib/app_info/png_uncrush.rb, line 167
def inflate(data)
  # make zlib not check the header
  zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
  buf = zstream.inflate(data)
  zstream.finish
  zstream.close
  buf
end
write_file(path, content) click to toggle source
# File lib/app_info/png_uncrush.rb, line 127
def write_file(path, content)
  File.write(path, content, encoding: Encoding::BINARY)
  true
end