Portable Network Graphics Parser

lua-users home
wiki

This is a simple example of parsing portable network graphics (PNG) files [1][2] using pure Lua code (no C code like libpng [3] or zlib [4][5]). In its current form, it does not fully decode PNG files but rather only the top-level structure, including metadata. A full implementation would need to implement zlib [4] too (see ModuleCompressDeflateLua). Extend if you wish.

-- pngparse.lua
-- Simple example of parsing the main sections of a PNG file.
--
-- This is mostly just an example.  Not intended to be complete,
-- robust, modular, or well tested.
--
-- (c) 2008 David Manura. Licensed under the same terms as Lua (MIT license).


-- Unpack 32-bit unsigned integer (most-significant-byte, MSB, first)
-- from byte string.
local function unpack_msb_uint32(s)
  local a,b,c,d = s:byte(1,#s)
  local num = (((a*256) + b) * 256 + c) * 256 + d
  return num
end

-- Read 32-bit unsigned integer (most-significant-byte, MSB, first) from file.
local function read_msb_uint32(fh)
  return unpack_msb_uint32(fh:read(4))
end

-- Read unsigned byte (integer) from file
local function read_byte(fh)
  return fh:read(1):byte()
end


local function parse_zlib(fh, len)
  local byte1 = read_byte(fh)
  local byte2 = read_byte(fh)

  local compression_method = byte1 % 16
  local compression_info = math.floor(byte1 / 16)

  local fcheck = byte2 % 32
  local fdict = math.floor(byte2 / 32) % 1
  local flevel = math.floor(byte2 / 64)

  print("compression_method=", compression_method)
  print("compression_info=", compression_info)
  print("fcheck=", fcheck)
  print("fdict=", fdict)
  print("flevel=", flevel)

  fh:read(len - 6)
  print("(deflate data not displayed)")

  local checksum = read_msb_uint32(fh)
  print("checksum=", checksum)
end

local function parse_IHDR(fh, len)
  assert(len == 13, 'format error')
  local width = read_msb_uint32(fh)
  local height = read_msb_uint32(fh)
  local bit_depth = read_byte(fh)
  local color_type = read_byte(fh)
  local compression_method = read_byte(fh)
  local filter_method = read_byte(fh)
  local interlace_method = read_byte(fh)

  print("width=", width)
  print("height=", height)
  print("bit_depth=", bit_depth)
  print("color_type=", color_type)
  print("compression_method=", compression_method)
  print("filter_method=", filter_method)
  print("interlace_method=", interlace_method)

  return compression_method
end

local function parse_sRGB(fh, len)
  assert(len == 1, 'format error')
  local rendering_intent = read_byte(fh)
  print("rendering_intent=", rendering_intent)
end

local function parse_gAMA(fh, len)
  assert(len == 4, 'format error')
  local rendering_intent = read_msb_uint32(fh)
  print("rendering_intent=", rendering_intent)
end

local function parse_cHRM(fh, len)
  assert(len == 32, 'format error')

  local white_x = read_msb_uint32(fh)
  local white_y = read_msb_uint32(fh)
  local red_x = read_msb_uint32(fh)
  local red_y = read_msb_uint32(fh)
  local green_x = read_msb_uint32(fh)
  local green_y = read_msb_uint32(fh)
  local blue_x = read_msb_uint32(fh)
  local blue_y = read_msb_uint32(fh)
  print('white_x=', white_x)
  print('white_y=', white_y)
  print('red_x=', red_x)
  print('red_y=', red_y)
  print('green_x=', green_x)
  print('green_y=', green_y)
  print('blue_x=', blue_x)
  print('blue_y=', blue_y)
end

local function parse_IDAT(fh, len, compression_method)
  if compression_method == 0 then
    -- fh:read(len)
    parse_zlib(fh, len)
  else
    print('(unrecognized compression method)')
  end  
end

local function parse_png(fh)
  -- parse PNG header
  local bytes = fh:read(8)
  local expect = "\137\080\078\071\013\010\026\010"
  if bytes ~= expect then
    error 'not a PNG file'
  end

  -- parse chunks
  local compression_method
  while 1 do
    local len = read_msb_uint32(fh)
    local stype = fh:read(4)
    print("chunk:", "type=", stype, "len=", len)

    if stype == 'IHDR' then
      compression_method = parse_IHDR(fh, len)
    elseif stype == 'sRGB' then
      parse_sRGB(fh, len)
    elseif stype == 'gAMA' then
      parse_gAMA(fh, len)
    elseif stype == 'cHRM' then
      parse_cHRM(fh, len)
    elseif stype == 'IDAT' then
      parse_IDAT(fh, len, compression_method)
    else
      local data = fh:read(len)
      print("data=", len == 0 and "(empty)" or "(not displayed)")
    end

    local crc = read_msb_uint32(fh)
    print("crc=", crc)

    if stype == 'IEND' then
      break
    end
  end
end

local filename = arg[1]

if not filename then
  io.stderr:write("usage: lua pngparse.lua <filename>")
  os.exit(1)
end

local fh = assert(io.open(filename, 'rb'))
parse_png(fh)

Example output:

$ lua pngparse.lua myfile.png
chunk:  type=   IHDR    len=    13
width=  676
height= 647
bit_depth=      8
color_type=     2
compression_method=     0
filter_method=  0
interlace_method=       0
crc=    3625766246
chunk:  type=   sRGB    len=    1
rendering_intent=       0
crc=    2932743401
chunk:  type=   gAMA    len=    4
rendering_intent=       45455
crc=    201089285
chunk:  type=   cHRM    len=    32
white_x=        31270
white_y=        32900
red_x=  64000
red_y=  33000
green_x=        30000
green_y=        60000
blue_x= 15000
blue_y= 6000
crc=    2629456188
chunk:  type=   IDAT    len=    60975
compression_method=     8
compression_info=       7
fcheck= 30
fdict=  0
flevel= 1
(deflate data not displayed)
checksum=       2411816191
crc=    1804717936
chunk:  type=   IEND    len=    0
data=   (empty)
crc=    2923585666

--DavidManura


RecentChanges · preferences
edit · history
Last edited May 2, 2009 2:32 am GMT (diff)