Fantom

 

//
// Copyright (c) 2017, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   20 Jun 2017  Matthew Giannini  Creation
//

using concurrent

**
** Decodes a PNG file into an `Image`
**
@NoDoc @Js class PngDecoder
{

//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////

  ** Creates a PNG decoder for the given stream. The stream will
  ** not be closed after decoding.
  new make(InStream in)
  {
    this.in = in
  }

//////////////////////////////////////////////////////////////////////////
// Identity
//////////////////////////////////////////////////////////////////////////

  ** PNG magic number
  static const Int magic := 0x89_50_4e_47_0d_0a_1a_0a

  ** PNG mime type
  static const MimeType mime := MimeType("image/png")

  ** Returns true if Buf starts with `magic` number.
  ** The buf is not modified.
  static Bool isPng(Buf buf) { magic == buf[0..<8].readS8 }

//////////////////////////////////////////////////////////////////////////
// Decode
//////////////////////////////////////////////////////////////////////////

  Image decode()
  {
    // verify magic
    if (magic != in.readS8) throw IOErr("Missing magic")
    data := Buf()
    while (true)
    {
      len  := in.readU4
      type := in.readChars(4)
      data  = in.readBufFully(data.clear, len)
      crc  := in.readU4
      switch (type)
      {
        case "IHDR": readImageHeader(data)
        case "PLTE": readPalette(data)
        case "IDAT": readImageData(data)
        case "tRNS": readTransparency(data)
        case "IEND": break
      }
    }
    return toImage
  }

  private PngImage toImage()
  {
    PngImage {
      it.mime = PngDecoder.mime
      it.size = Size(width, height)
      it.props = [
        "colorType":       this.colorType,
        "colorSpace":      this.colorSpace,
        "colorSpaceBits":  isIndexedColor ? 8 : bitDepth,
        "interlaceMethod": this.interlaceMethod,
        "palette":         this.palette.flip,
        "transparency":    this.transparency.flip,
        "imgData":         this.imgData.flip,
      ].toImmutable
    }
  }

  private Void readImageHeader(Buf data)
  {
    this.width  = data.readU4
    if (width <= 0) throw IOErr("Invalid width: $width")

    this.height = data.readU4
    if (height <= 0) throw IOErr("Invalid height: $height")

    this.bitDepth  = data.readU1
    this.colorType = data.readU1

    compressionMethod := data.readU1
    if (compressionMethod != 0) throw IOErr("Invalid compression method: $compressionMethod")

    filterMethod := data.readU1
    if (filterMethod != 0) throw IOErr("Invalid filter method: $filterMethod")

    this.interlaceMethod = data.readU1
    if (interlaceMethod > 1) throw IOErr("Invalid interlace method: $interlaceMethod")
  }

  private Void readPalette(Buf data)
  {
    if (data.size % 3 != 0) throw IOErr("Invalid palette data size: ${data.size}")
    palette.writeBuf(data)
  }

  private Void readImageData(Buf data)
  {
    imgData.writeBuf(data)
  }

  private Void readTransparency(Buf data)
  {
    transparency.writeBuf(data)
    if (colorType == 3)
      transparency.fill(255, palette.size - transparency.size)
  }

//////////////////////////////////////////////////////////////////////////
// Util
//////////////////////////////////////////////////////////////////////////

  ** Is there a palette index
  private Bool isIndexedColor() { colorType == 3 }

  private Str colorSpace()
  {
    switch (colorType)
    {
      case 0: return "Gray"
      case 2: return "RGB"
      case 3: return "RGB"  // palette index
      case 4: return "Gray" // with alpha
      case 6: return "RGB"  // with alpha
    }
    throw IOErr("Invalid color type: $colorType")
  }

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  private InStream in
  private Str:Obj props := [:]

  // IHDR
  private Int? width
  private Int? height
  private Int? bitDepth
  private Int? colorType
  private Int? interlaceMethod

  // PLTE
  private Buf palette := Buf()

  // tRNS
  ** simple transparency alpha channel
  private Buf transparency := Buf()

  // IDAT
  ** Concatenation of all IDAT *compressed* data chunks.
  private Buf imgData := Buf()
}

**************************************************************************
** PngImage
**************************************************************************

@NoDoc @Js const class PngImage : Image
{
  new make(|This| f) : super(f) { }

  ** Does the image have an alpha channel
  Bool hasAlpha() { colorType == 4 || colorType == 6 }

  ** Does the image have a palette index
  Bool hasPalette() { palette.size > 0 }

  ** Does the image have simple transparency alpha channel
  Bool hasTransparency() { transparency.size > 0 }

  ** Color type code
  Int colorType() { props["colorType"] }

  ** Number of color components
  Int colors() {
    c := (colorType == 2 || colorType == 6) ? 3 : 1
    return hasAlpha ? c + 1 : c
  }

  ** Number of bits in a pixel
  Int pixelBits() { colors * ((Int)props["colorSpaceBits"]) }

  ** The palette index. The Buf is immutable.
  Buf palette() { props["palette"] }

  ** The simple transparency alpha channel. The Buf is immutable.
  Buf transparency() { props["transparency"] }

  ** Raw image data. The Buf is immutable.
  Buf imgData() { props["imgData"] }

  ** Get decompressed pixels. The Buf is immutable.
  Buf pixels()
  {
    if (pixelsRef.val != null) return pixelsRef.val->seek(0)

    data := Zip.deflateInStream(imgData.in).readAllBuf
    pixelBytes  := pixelBits / 8
    scanLineLen := pixelBytes * size.w.toInt
    numPixels   := scanLineLen * size.h.toInt
    pixels      := Buf(numPixels)
    row         := 0
    while (data.more)
    {
      filter := data.read
      (0..<scanLineLen).each |i|
      {
        // None
        if (0 == filter) return pixels.write(data.read)

        byte  := data.read
        col   := (i - (i % pixelBytes)) / pixelBytes
        left  := i < pixelBytes ? 0 : pixels[pixels.size - pixelBytes]
        upper := row == 0 ? 0 : pixels[((row - 1) * scanLineLen) + (col * pixelBytes) + (i % pixelBytes)]
        upperLeft := row == 0 || col == 0 ? 0 : pixels[((row - 1) * scanLineLen) + ((col - 1) * pixelBytes) + (i % pixelBytes)]

        Int? val := null
        switch (filter)
        {
          case 1: // Sub
            val = byte + left
          case 2: // Up
            val = upper + byte
          case 3: // Avg
            val = (byte + ((left + upper).toFloat / 2f).floor.toInt)
          case 4: // Paeth
            p  := left + upper - upperLeft
            pa := (p - left).abs
            pb := (p - upper).abs
            pc := (p - upperLeft).abs

            paeth := upperLeft
            if (pa <= pb && pa <= pc)
              paeth = left
            else if (pb <= pc)
              paeth = upper

            val = byte + paeth
        }
        pixels.write(val % 256)
      }
      ++row
    }

    pixelsRef.val = pixels.flip.toImmutable
    return pixelsRef.val
  }
  private const AtomicRef pixelsRef := AtomicRef(null)
}