fpc/fcl/image/fpreadpng.pp
luk bfa55a332c * made PNG read/Write a bit faster
* removed color conversion routines (except gray)
* memory image with size 0,0 trew an exception
* creation of a TMemoryImage of size 0,0 will have no color in palette
2003-10-19 21:09:50 +00:00

835 lines
22 KiB
ObjectPascal

{
$Id$
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.