{
    $Id: fpreadpng.pp,v 1.10 2003/10/19 21:09:51 luk Exp $
    This file is part of the Free Pascal run time library.
    Copyright (c) 2003 by the Free Pascal development team

    PNG reader implementation

    See the file COPYING.FPC, included in this distribution,
    for details about the copyright.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

 **********************************************************************}
{$mode objfpc}{$h+}
unit FPReadPNG;

interface

uses
  SysUtils,Classes, FPImage, FPImgCmn, PNGComn, ZStream;

Type

  TSetPixelProc = procedure (x,y:integer; CD : TColordata) of object;
  TConvertColorProc = function (CD:TColorData) : TFPColor of object;

  TFPReaderPNG = class (TFPCustomImageReader)
    private
      Chunk : TChunk;
      FHeader : THeaderChunk;
      ZData : TMemoryStream;  // holds compressed data until all blocks are read
      Decompress : TDeCompressionStream; // decompresses the data
      FPltte : boolean;     // if palette is used
      FCountScanlines : EightLong; //Number of scanlines to process for each pass
      FScanLineLength : EightLong; //Length of scanline for each pass
      FCurrentPass : byte;
      ByteWidth : byte;          // number of bytes to read for pixel information
      BitsUsed : EightLong; // bitmasks to use to split a byte into smaller parts
      BitShift : byte;  // shift right to do of the bits extracted with BitsUsed for 1 element
      CountBitsUsed : byte;  // number of bit groups (1 pixel) per byte (when bytewidth = 1)
      //CFmt : TColorFormat; // format of the colors to convert from
      StartX,StartY, DeltaX,DeltaY, StartPass,EndPass : integer;  // number and format of passes
      FSwitchLine, FCurrentLine, FPreviousLine : pByteArray;
      FPalette : TFPPalette;
      FSetPixel : TSetPixelProc;
      FConvertColor : TConvertColorProc;
      procedure ReadChunk;
      procedure HandleData;
      procedure HandleUnknown;
      function ColorGray1 (CD:TColorData) : TFPColor;
      function ColorGray2 (CD:TColorData) : TFPColor;
      function ColorGray4 (CD:TColorData) : TFPColor;
      function ColorGray8 (CD:TColorData) : TFPColor;
      function ColorGray16 (CD:TColorData) : TFPColor;
      function ColorGrayAlpha8 (CD:TColorData) : TFPColor;
      function ColorGrayAlpha16 (CD:TColorData) : TFPColor;
      function ColorColor8 (CD:TColorData) : TFPColor;
      function ColorColor16 (CD:TColorData) : TFPColor;
      function ColorColorAlpha8 (CD:TColorData) : TFPColor;
      function ColorColorAlpha16 (CD:TColorData) : TFPColor;
    protected
      UseTransparent, EndOfFile : boolean;
      TransparentDataValue : TColorData;
      UsingBitGroup : byte;
      DataIndex : longword;
      DataBytes : TColorData;
      function CurrentLine(x:longword) : byte;
      function PrevSample (x:longword): byte;
      function PreviousLine (x:longword) : byte;
      function PrevLinePrevSample (x:longword): byte;
      procedure HandleChunk; virtual;
      procedure HandlePalette; virtual;
      procedure HandleAlpha; virtual;
      function CalcX (relX:integer) : integer;
      function CalcY (relY:integer) : integer;
      function CalcColor: TColorData;
      procedure HandleScanLine (const y : integer; const ScanLine : PByteArray); virtual;
      procedure DoDecompress; virtual;
      function  DoFilter(LineFilter:byte;index:longword; b:byte) : byte; virtual;
      procedure SetPalettePixel (x,y:integer; CD : TColordata);
      procedure SetPalColPixel (x,y:integer; CD : TColordata);
      procedure SetColorPixel (x,y:integer; CD : TColordata);
      procedure SetColorTrPixel (x,y:integer; CD : TColordata);
      function DecideSetPixel : TSetPixelProc; virtual;
      procedure InternalRead  (Str:TStream; Img:TFPCustomImage); override;
      function  InternalCheck (Str:TStream) : boolean; override;
      //property ColorFormat : TColorformat read CFmt;
      property ConvertColor : TConvertColorProc read FConvertColor;
      property CurrentPass : byte read FCurrentPass;
      property Pltte : boolean read FPltte;
      property ThePalette : TFPPalette read FPalette;
      property Header : THeaderChunk read FHeader;
      property CountScanlines : EightLong read FCountScanlines;
      property ScanLineLength : EightLong read FScanLineLength;
    public
      constructor create; override;
      destructor destroy; override;
  end;

implementation



const StartPoints : array[0..7, 0..1] of word =
         ((0,0),(0,0),(4,0),(0,4),(2,0),(0,2),(1,0),(0,1));
      Delta : array[0..7,0..1] of word =
         ((1,1),(8,8),(8,8),(4,8),(4,4),(2,4),(2,2),(1,2));
      BitsUsed1Depth : EightLong = ($80,$40,$20,$10,$08,$04,$02,$01);
      BitsUsed2Depth : EightLong = ($C0,$30,$0C,$03,0,0,0,0);
      BitsUsed4Depth : EightLong = ($F0,$0F,0,0,0,0,0,0);

constructor TFPReaderPNG.create;
begin
  inherited;
  chunk.acapacity := 0;
  chunk.data := nil;
  UseTransparent := False;
end;

destructor TFPReaderPNG.destroy;
begin
  with chunk do
    if acapacity > 0 then
      freemem (data);
  inherited;
end;

procedure TFPReaderPNG.ReadChunk;

var ChunkHeader : TChunkHeader;
    readCRC : longword;
    l : longword;
begin
  TheStream.Read (ChunkHeader,sizeof(ChunkHeader));
  with chunk do
    begin
    // chunk header
    with ChunkHeader do
      begin
      alength := swap(CLength);
      ReadType := CType;
      end;
    aType := low(TChunkTypes);
    while (aType < high(TChunkTypes)) and (ChunkTypes[aType] <> ReadType) do
      inc (aType);
    if alength > MaxChunkLength then
      raise PNGImageException.Create ('Invalid chunklength');
    if alength > acapacity then
      begin
      if acapacity > 0 then
        freemem (data);
      GetMem (data, alength);
      acapacity := alength;
      end;
    l := TheStream.read (data^, alength);
    if l <> alength then
      raise PNGImageException.Create ('Chunk length exceeds stream length');
    TheStream.Read (readCRC, sizeof(ReadCRC));
    l := CalculateCRC (All1Bits, ReadType, sizeOf(ReadType));
    l := CalculateCRC (l, data^, alength);
    l := swap(l xor All1Bits);
    if ReadCRC <> l then
      raise PNGImageException.Create ('CRC check failed');
    end;
end;

procedure TFPReaderPNG.HandleData;
var OldSize : longword;
begin
  OldSize := ZData.size;
  ZData.Size := OldSize + Chunk.aLength;
  ZData.Write (chunk.Data^, chunk.aLength);
end;

procedure TFPReaderPNG.HandleAlpha;
  procedure PaletteAlpha;
    var r : integer;
        a : word;
        c : TFPColor;
    begin
      with chunk do
        begin
        if alength > longword(ThePalette.count) then
          raise PNGImageException.create ('To much alpha values for palette');
        for r := 0 to alength-1 do
          begin
          c := ThePalette[r];
          a := data^[r];
          c.alpha := (a shl 16) + a;
          ThePalette[r] := c;
          end;
        end;
    end;
  procedure TransparentGray;
    var a : word;
    begin
      move (chunk.data^[0], a, 2);
      a := swap (a);
      TransparentDataValue := a;
      UseTransparent := True;
    end;
  procedure TransparentColor;
    var d : byte;
        r,g,b : word;
        a : TColorData;
    begin
      with chunk do
        begin
        move (data^[0], r, 2);
        move (data^[2], g, 2);
        move (data^[4], b, 2);
        end;
      r := swap (r);
      g := swap (g);
      b := swap (b);
      d := header.bitdepth;
      a := (TColorData(b) shl d) shl d;
      a := a + (TColorData(g) shl d) + r;
      TransparentDataValue := a;
      UseTransparent := True;
    end;
begin
  case header.ColorType of
    3 : PaletteAlpha;
    0 : TransparentGray;
    2 : TransparentColor;
  end;
end;

procedure TFPReaderPNG.HandlePalette;
var r : longword;
    c : TFPColor;
    t : word;
begin
  if header.colortype = 3 then
    with chunk do
      begin
      if TheImage.UsePalette then
        FPalette := TheImage.Palette
      else
        FPalette := TFPPalette.Create(0);
      c.Alpha := AlphaOpaque;
      if (aLength mod 3) > 0 then
        raise PNGImageException.Create ('Impossible length for PLTE-chunk');
      r := 0;
      ThePalette.count := 0;
      while r < alength do
        begin
        t := data^[r];
        c.red := t + (t shl 8);
        inc (r);
        t := data^[r];
        c.green := t + (t shl 8);
        inc (r);
        t := data^[r];
        c.blue := t + (t shl 8);
        inc (r);
        ThePalette.Add (c);
        end;
      end;
end;

procedure TFPReaderPNG.SetPalettePixel (x,y:integer; CD : TColordata);
begin  // both PNG and palette have palette
  TheImage.Pixels[x,y] := CD;
end;

procedure TFPReaderPNG.SetPalColPixel (x,y:integer; CD : TColordata);
begin  // PNG with palette, Img without
  TheImage.Colors[x,y] := ThePalette[CD];
end;

procedure TFPReaderPNG.SetColorPixel (x,y:integer; CD : TColordata);
var c : TFPColor;
begin  // both PNG and Img work without palette, and no transparency colordata
  // c := ConvertColor (CD,CFmt);
  c := ConvertColor (CD);
  TheImage.Colors[x,y] := c;
end;

procedure TFPReaderPNG.SetColorTrPixel (x,y:integer; CD : TColordata);
var c : TFPColor;
begin  // both PNG and Img work without palette, and there is a transparency colordata
  //c := ConvertColor (CD,CFmt);
  c := ConvertColor (CD);
  if TransparentDataValue = CD then
    c.alpha := alphaTransparent;
  TheImage.Colors[x,y] := c;
end;

function TFPReaderPNG.CurrentLine(x:longword):byte;
begin
  result := FCurrentLine^[x];
end;

function TFPReaderPNG.PrevSample (x:longword): byte;
begin
  if x < byteWidth then
    result := 0
  else
    result := FCurrentLine^[x - bytewidth];
end;

function TFPReaderPNG.PreviousLine (x:longword) : byte;
begin
  result := FPreviousline^[x];
end;

function TFPReaderPNG.PrevLinePrevSample (x:longword): byte;
begin
  if x < byteWidth then
    result := 0
  else
    result := FPreviousLine^[x - bytewidth];
end;

function TFPReaderPNG.DoFilter(LineFilter:byte;index:longword; b:byte) : byte;
var diff : byte;
  procedure FilterSub;
  begin
    diff := PrevSample(index);
  end;
  procedure FilterUp;
  begin
    diff := PreviousLine(index);
  end;
  procedure FilterAverage;
  var l, p : word;
  begin
    l := PrevSample(index);
    p := PreviousLine(index);
    diff := (l + p) div 2;
  end;
  procedure FilterPaeth;
  var dl, dp, dlp : word; // index for previous and distances for:
      l, p, lp : byte;  // r:predictor, Left, Previous, LeftPrevious
      r : integer;
  begin
    l := PrevSample(index);
    lp := PrevLinePrevSample(index);
    p := PreviousLine(index);
    r := l + p - lp;
    dl := abs (r - l);
    dlp := abs (r - lp);
    dp := abs (r - p);
    if (dl <= dp) and (dl <= dlp) then
      diff := l
    else if dp <= dlp then
      diff := p
    else
      diff := lp;
  end;
begin
  case LineFilter of
    0 : diff := 0;
    1 : FilterSub;
    2 : FilterUp;
    3 : FilterAverage;
    4 : FilterPaeth;
  end;
  result := (b + diff) mod $100;
end;

function TFPReaderPNG.DecideSetPixel : TSetPixelProc;
begin
  if Pltte then
    if TheImage.UsePalette then
      result := @SetPalettePixel
    else
      result := @SetPalColPixel
  else
    if UseTransparent then
      result := @SetColorTrPixel
    else
      result := @SetColorPixel;
end;

function TFPReaderPNG.CalcX (relX:integer) : integer;
begin
  result := StartX + (relX * deltaX);
end;

function TFPReaderPNG.CalcY (relY:integer) : integer;
begin
  result := StartY + (relY * deltaY);
end;

function TFPReaderPNG.CalcColor: TColorData;
var cd : longword;
    r : word;
    b : byte;
begin
  if UsingBitGroup = 0 then
    begin
    Databytes := 0;
    if Header.BitDepth = 16 then
      begin
      r := 1;
      while (r < ByteWidth) do
        begin
        b := FCurrentLine^[Dataindex+r];
        FCurrentLine^[Dataindex+r] := FCurrentLine^[Dataindex+r-1];
        FCurrentLine^[Dataindex+r-1] := b;
        inc (r,2);
        end;
      end;
    move (FCurrentLine^[DataIndex], Databytes, bytewidth);
    inc (DataIndex,bytewidth);
    end;
  if bytewidth = 1 then
    begin
    cd := (Databytes and BitsUsed[UsingBitGroup]);
    result := cd shr ((CountBitsUsed-UsingBitGroup-1) * BitShift);
    inc (UsingBitgroup);
    if UsingBitGroup >= CountBitsUsed then
      UsingBitGroup := 0;
    end
  else
    result := Databytes;
end;

procedure TFPReaderPNG.HandleScanLine (const y : integer; const ScanLine : PByteArray);
var x, rx : integer;
    c : TColorData;
begin
  UsingBitGroup := 0;
  DataIndex := 0;
  for rx := 0 to ScanlineLength[CurrentPass]-1 do
    begin
    X := CalcX(rx);
    c := CalcColor;
    FSetPixel (x,y,c);
    end
end;

function TFPReaderPNG.ColorGray1 (CD:TColorDAta) : TFPColor;
begin
  if CD = 0 then
    result := colBlack
  else
    result := colWhite;
end;

function TFPReaderPNG.ColorGray2 (CD:TColorDAta) : TFPColor;
var c : word;
begin
  c := CD and 3;
  c := c + (c shl 2);
  c := c + (c shl 4);
  c := c + (c shl 8);
  with result do
    begin
    red := c;
    green := c;
    blue := c;
    alpha := alphaOpaque;
    end;
end;

function TFPReaderPNG.ColorGray4 (CD:TColorDAta) : TFPColor;
var c : word;
begin
  c := CD and $F;
  c := c + (c shl 4);
  c := c + (c shl 8);
  with result do
    begin
    red := c;
    green := c;
    blue := c;
    alpha := alphaOpaque;
    end;
end;

function TFPReaderPNG.ColorGray8 (CD:TColorDAta) : TFPColor;
var c : word;
begin
  c := CD and $FF;
  c := c + (c shl 8);
  with result do
    begin
    red := c;
    green := c;
    blue := c;
    alpha := alphaOpaque;
    end;
end;

function TFPReaderPNG.ColorGray16 (CD:TColorDAta) : TFPColor;
var c : word;
begin
  c := CD and $FFFF;
  with result do
    begin
    red := c;
    green := c;
    blue := c;
    alpha := alphaOpaque;
    end;
end;

function TFPReaderPNG.ColorGrayAlpha8 (CD:TColorData) : TFPColor;
var c : word;
begin
  c := CD and $FF00;
  c := c + (c shr 8);
  with result do
    begin
    red := c;
    green := c;
    blue := c;
    c := CD and $FF;
    alpha := c + (c shl 8);
    end;
end;

function TFPReaderPNG.ColorGrayAlpha16 (CD:TColorData) : TFPColor;
var c : word;
begin
  c := (CD and qword($FFFF0000)) shr 16;
  with result do
    begin
    red := c;
    green := c;
    blue := c;
    alpha := CD and $FFFF;
    end;
end;

function TFPReaderPNG.ColorColor8 (CD:TColorData) : TFPColor;
var c : word;
begin
  with result do
    begin
    c := CD and $FF;
    red := c + (c shl 8);
    c := CD and $FF00;
    green := c + (c shr 8);
    c := (CD and $FF0000) shr 8;
    blue := c + (c shr 8);
    alpha := alphaOpaque;
    end;
end;

function TFPReaderPNG.ColorColor16 (CD:TColorData) : TFPColor;
var c : qword;
begin
  with result do
    begin
    red := CD and $FFFF;
    c := qword($FFFF0000);
    green := (CD and c) shr 16;
    c := c shl 16;
    blue := (CD and c) shr 32;
    alpha := alphaOpaque;
    end;
end;

function TFPReaderPNG.ColorColorAlpha8 (CD:TColorData) : TFPColor;
var c : qword;
begin
  with result do
    begin
    c := CD and $FF;
    red := c + (c shl 8);
    c := CD and $FF00;
    green := c + (c shr 8);
    c := (CD and $FF0000) shr 8;
    blue := c + (c shr 8);
    c := (CD and qword($FF000000)) shr 16;
    alpha := c + (c shr 8);
    end;
end;

function TFPReaderPNG.ColorColorAlpha16 (CD:TColorData) : TFPColor;
var c : qword;
begin
  with result do
    begin
    red := CD and $FFFF;
    c := qword($FFFF0000);
    green := (CD and c) shr 16;
    c := c shl 16;
    blue := (CD and c) shr 32;
    c := c shl 16;
    alpha := (CD and c) shr 48;
    end;
end;

procedure TFPReaderPNG.DoDecompress;

  procedure initVars;
  var r,d : integer;
  begin
    with Header do
      begin
      if interlace=0 then
        begin
        StartPass := 0;
        EndPass := 0;
        CountScanlines[0] := Height;
        ScanLineLength[0] := Width;
        end
      else
        begin
        StartPass := 1;
        EndPass := 7;
        for r := 1 to 7 do
          begin
          d := Height div delta[r,1];
          if (height mod delta[r,1]) > startpoints[r,1] then
            inc (d);
          CountScanLines[r] := d;
          d := width div delta[r,0];
          if (width mod delta[r,0]) > startpoints[r,0] then
            inc (d);
          ScanLineLength[r] := d;
          end;
        end;
      Fpltte := (ColorType = 3);
      case colortype of
        0 : case Bitdepth of
              1  : begin
                   FConvertColor := @ColorGray1; //CFmt := cfMono;
                   ByteWidth := 1;
                   end;
              2  : begin
                   FConvertColor := @ColorGray2; //CFmt := cfGray2;
                   ByteWidth := 1;
                   end;
              4  : begin
                   FConvertColor := @ColorGray4; //CFmt := cfGray4;
                   ByteWidth := 1;
                   end;
              8  : begin
                   FConvertColor := @ColorGray8; //CFmt := cfGray8;
                   ByteWidth := 1;
                   end;
              16 : begin
                   FConvertColor := @ColorGray16; //CFmt := cfGray16;
                   ByteWidth := 2;
                   end;
            end;
        2 : if BitDepth = 8 then
              begin
              FConvertColor := @ColorColor8; //CFmt := cfBGR24
              ByteWidth := 3;
              end
            else
              begin
              FConvertColor := @ColorColor16; //CFmt := cfBGR48;
              ByteWidth := 6;
              end;
        3 : if BitDepth = 16 then
              ByteWidth := 2
            else
              ByteWidth := 1;
        4 : if BitDepth = 8 then
              begin
              FConvertColor := @ColorGrayAlpha8; //CFmt := cfGrayA16
              ByteWidth := 2;
              end
            else
              begin
              FConvertColor := @ColorGrayAlpha16; //CFmt := cfGrayA32;
              ByteWidth := 4;
              end;
        6 : if BitDepth = 8 then
              begin
              FConvertColor := @ColorColorAlpha8; //CFmt := cfABGR32
              ByteWidth := 4;
              end
            else
              begin
              FConvertColor := @ColorColorAlpha16; //CFmt := cfABGR64;
              ByteWidth := 8;
              end;
      end;
      //ByteWidth := BytesNeeded[CFmt];
      case BitDepth of
        1 : begin
            CountBitsUsed := 8;
            BitShift := 1;
            BitsUsed := BitsUsed1Depth;
            end;
        2 : begin
            CountBitsUsed := 4;
            BitShift := 2;
            BitsUsed := BitsUsed2Depth;
            end;
        4 : begin
            CountBitsUsed := 2;
            BitShift := 4;
            BitsUsed := BitsUsed4Depth;
            end;
        8 : begin
            CountBitsUsed := 1;
            BitShift := 0;
            BitsUsed[0] := $FF;
            end;
        end;
      end;
  end;

  procedure Decode;
  var y, rp, ry, rx, l : integer;
      lf : byte;
  begin
    FSetPixel := DecideSetPixel;
    for rp := StartPass to EndPass do
      begin
      FCurrentPass := rp;
      StartX := StartPoints[rp,0];
      StartY := StartPoints[rp,1];
      DeltaX := Delta[rp,0];
      DeltaY := Delta[rp,1];
      if bytewidth = 1 then
        begin
        l := (ScanLineLength[rp] div CountBitsUsed);
        if (ScanLineLength[rp] mod CountBitsUsed) > 0 then
          inc (l);
        end
      else
        l := ScanLineLength[rp]*ByteWidth;
      GetMem (FPreviousLine, l);
      GetMem (FCurrentLine, l);
      fillchar (FCurrentLine^,l,0);
      try
        for ry := 0 to CountScanlines[rp]-1 do
          begin
          FSwitchLine := FCurrentLine;
          FCurrentLine := FPreviousLine;
          FPreviousLine := FSwitchLine;
          Y := CalcY(ry);
          Decompress.Read (lf, sizeof(lf));
          Decompress.Read (FCurrentLine^, l);
          if lf <> 0 then  // Do nothing when there is no filter used
            for rx := 0 to l-1 do
              FCurrentLine^[rx] := DoFilter (lf, rx, FCurrentLine^[rx]);
          HandleScanLine (y, FCurrentLine);
          end;
      finally
        freemem (FPreviousLine);
        freemem (FCurrentLine);
      end;
      end;
  end;

begin
  InitVars;
  DeCode;
end;

procedure TFPReaderPNG.HandleChunk;
begin
  case chunk.AType of
    ctIHDR : raise PNGImageException.Create ('Second IHDR chunk found');
    ctPLTE : HandlePalette;
    ctIDAT : HandleData;
    ctIEND : EndOfFile := True;
    cttRNS : HandleAlpha;
    else HandleUnknown;
  end;
end;

procedure TFPReaderPNG.HandleUnknown;
begin
  if (chunk.readtype[0] in ['A'..'Z']) then
    raise PNGImageException.Create('Critical chunk '+chunk.readtype+' not recognized');
end;

procedure TFPReaderPNG.InternalRead (Str:TStream; Img:TFPCustomImage);
begin
  if Str<>TheStream then
    writeln('WARNING: TFPReaderPNG.InternalRead Str<>TheStream');
  with Header do
    Img.SetSize (Width, Height);
  ZData := TMemoryStream.Create;
  try
    EndOfFile := false;
    while not EndOfFile do
      begin
      ReadChunk;
      HandleChunk;
      end;
    Decompress := TDecompressionStream.Create (ZData);
    try
      Decompress.position := 0;
      DoDecompress;
    finally
      Decompress.Free;
    end;
  finally
    ZData.Free;
    if not img.UsePalette and assigned(FPalette) then
      begin
      FPalette.Free;
      end;
  end;
end;

function  TFPReaderPNG.InternalCheck (Str:TStream) : boolean;
var SigCheck : array[0..7] of byte;
    r : integer;
begin
  try
    // Check Signature
    Str.Read(SigCheck, SizeOf(SigCheck));
    for r := 0 to 7 do
    begin
      If SigCheck[r] <> Signature[r] then
        raise PNGImageException.Create('This is not PNG-data');
    end;
    // Check IHDR
    ReadChunk;
    move (chunk.data^, FHeader, sizeof(Header));
    with header do
      begin
      Width := swap(width);
      height := swap (height);
      result := (width > 0) and (height > 0) and (compression = 0)
                and (filter = 0) and (Interlace in [0,1]);
      end;
  except
    result := false;
  end;
end;

initialization
  ImageHandlers.RegisterImageReader ('Portable Network Graphics', 'png', TFPReaderPNG);
end.

