Microsoft Windows icon file: Ruby parsing library

Microsoft Windows uses specific file format to store applications icons - ICO. This is a container that contains one or more image files (effectively, DIB parts of BMP files or full PNG files are contained inside).

File extension

ico

KS implementation details

License: CC0-1.0

This page hosts a formal specification of Microsoft Windows icon file using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Usage

Parse a local file and get structure in memory:

data = Ico.from_file("path/to/local/file.ico")

Or parse structure from a string of bytes:

bytes = "\x00\x01\x02..."
data = Ico.new(Kaitai::Struct::Stream.new(bytes))

After that, one can get various attributes from the structure by invoking getter methods like:

data.num_images # => Number of images contained in this file

Ruby source code to parse Microsoft Windows icon file

ico.rb

# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

require 'kaitai/struct/struct'

unless Gem::Version.new(Kaitai::Struct::VERSION) >= Gem::Version.new('0.7')
  raise "Incompatible Kaitai Struct Ruby API: 0.7 or later is required, but you have #{Kaitai::Struct::VERSION}"
end


##
# Microsoft Windows uses specific file format to store applications
# icons - ICO. This is a container that contains one or more image
# files (effectively, DIB parts of BMP files or full PNG files are
# contained inside).
# @see https://msdn.microsoft.com/en-us/library/ms997538.aspx Source
class Ico < Kaitai::Struct::Struct
  def initialize(_io, _parent = nil, _root = self)
    super(_io, _parent, _root)
    _read
  end

  def _read
    @magic = @_io.ensure_fixed_contents([0, 0, 1, 0].pack('C*'))
    @num_images = @_io.read_u2le
    @images = Array.new(num_images)
    (num_images).times { |i|
      @images[i] = IconDirEntry.new(@_io, self, @_root)
    }
    self
  end
  class IconDirEntry < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @width = @_io.read_u1
      @height = @_io.read_u1
      @num_colors = @_io.read_u1
      @reserved = @_io.ensure_fixed_contents([0].pack('C*'))
      @num_planes = @_io.read_u2le
      @bpp = @_io.read_u2le
      @len_img = @_io.read_u4le
      @ofs_img = @_io.read_u4le
      self
    end

    ##
    # Raw image data. Use `is_png` to determine whether this is an
    # embedded PNG file (true) or a DIB bitmap (false) and call a
    # relevant parser, if needed to parse image data further.
    def img
      return @img unless @img.nil?
      _pos = @_io.pos
      @_io.seek(ofs_img)
      @img = @_io.read_bytes(len_img)
      @_io.seek(_pos)
      @img
    end

    ##
    # Pre-reads first 8 bytes of the image to determine if it's an
    # embedded PNG file.
    def png_header
      return @png_header unless @png_header.nil?
      _pos = @_io.pos
      @_io.seek(ofs_img)
      @png_header = @_io.read_bytes(8)
      @_io.seek(_pos)
      @png_header
    end

    ##
    # True if this image is in PNG format.
    def is_png
      return @is_png unless @is_png.nil?
      @is_png = png_header == [137, 80, 78, 71, 13, 10, 26, 10].pack('C*')
      @is_png
    end

    ##
    # Width of image, px
    attr_reader :width

    ##
    # Height of image, px
    attr_reader :height

    ##
    # Number of colors in palette of the image or 0 if image has
    # no palette (i.e. RGB, RGBA, etc)
    attr_reader :num_colors
    attr_reader :reserved

    ##
    # Number of color planes
    attr_reader :num_planes

    ##
    # Bits per pixel in the image
    attr_reader :bpp

    ##
    # Size of the image data
    attr_reader :len_img

    ##
    # Absolute offset of the image data start in the file
    attr_reader :ofs_img
  end
  attr_reader :magic

  ##
  # Number of images contained in this file
  attr_reader :num_images
  attr_reader :images
end