{ This file is part of the Free Pascal run time library. Copyright (c) 2012 by the Free Pascal development team Tiff writer for fpImage. 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. ********************************************************************** Working: Grayscale 8,16bit (optional alpha), RGB 8,16bit (optional alpha), Orientation, multiple images, pages thumbnail Compression: deflate ToDo: Compression: LZW, packbits, jpeg, ... Planar ColorMap separate mask fillorder - not needed by baseline tiff reader bigtiff 64bit offsets endian - currently using system endianess orientation with rotation } unit FPWriteTiff; {$mode objfpc}{$H+} interface uses Math, Classes, SysUtils, zbase, zdeflate, FPimage, FPTiffCmn; type { TTiffWriterEntry } TTiffWriterEntry = class public Tag: Word; EntryType: Word; Count: DWord; Data: Pointer; DataPos: DWord; Bytes: DWord; destructor Destroy; override; end; TTiffWriterChunk = record Data: Pointer; Bytes: DWord; end; PTiffWriterChunk = ^TTiffWriterChunk; { TTiffWriterChunkOffsets } TTiffWriterChunkOffsets = class(TTiffWriterEntry) public Chunks: PTiffWriterChunk; ChunkByteCounts: TTiffWriterEntry; constructor Create(ChunkType: TTiffChunkType); destructor Destroy; override; procedure SetCount(NewCount: DWord); end; { TFPWriterTiff } TFPWriterTiff = class(TFPCustomImageWriter) private FSaveCMYKAsRGB: boolean; fStartPos: Int64; FEntries: TFPList; // list of TFPList of TTiffWriterEntry fStream: TStream; fPosition: DWord; procedure ClearEntries; procedure WriteTiff; procedure WriteHeader; procedure WriteIFDs; procedure WriteEntry(Entry: TTiffWriterEntry); procedure WriteData; procedure WriteEntryData(Entry: TTiffWriterEntry); procedure WriteBuf(var Buf; Count: DWord); procedure WriteWord(w: Word); procedure WriteDWord(d: DWord); protected procedure InternalWrite(Stream: TStream; Img: TFPCustomImage); override; procedure AddEntryString(Tag: word; const s: string); procedure AddEntryShort(Tag: word; Value: Word); procedure AddEntryLong(Tag: word; Value: DWord); procedure AddEntryShortOrLong(Tag: word; Value: DWord); procedure AddEntryRational(Tag: word; const Value: TTiffRational); procedure AddEntry(Tag: Word; EntryType: Word; EntryCount: DWord; Data: Pointer; Bytes: DWord; CopyData: boolean = true); procedure AddEntry(Entry: TTiffWriterEntry); procedure TiffError(Msg: string); procedure EncodeDeflate(var Buffer: Pointer; var Count: DWord); public constructor Create; override; destructor Destroy; override; procedure Clear; procedure AddImage(Img: TFPCustomImage); procedure SaveToStream(Stream: TStream); property SaveCMYKAsRGB: boolean read FSaveCMYKAsRGB write FSaveCMYKAsRGB; end; function CompareTiffWriteEntries(Entry1, Entry2: Pointer): integer; function CompressDeflate(InputData: PByte; InputCount: cardinal; out Compressed: PByte; var CompressedCount: cardinal; ErrorMsg: PAnsiString = nil): boolean; implementation function CompareTiffWriteEntries(Entry1, Entry2: Pointer): integer; begin Result:=integer(TTiffWriterEntry(Entry1).Tag)-integer(TTiffWriterEntry(Entry2).Tag); end; function CompressDeflate(InputData: PByte; InputCount: cardinal; out Compressed: PByte; var CompressedCount: cardinal; ErrorMsg: PAnsiString ): boolean; var stream : z_stream; err : integer; begin Result:=false; //writeln('CompressDeflate START'); Compressed:=nil; if InputCount=0 then begin CompressedCount:=0; exit(true); end; err := deflateInit(stream{%H-}, Z_DEFAULT_COMPRESSION); if err <> Z_OK then begin if ErrorMsg<>nil then ErrorMsg^:='deflateInit failed'; exit; end; // set input = InputData data stream.avail_in := InputCount; stream.next_in := InputData; // set output = compressed data if CompressedCount=0 then CompressedCount:=InputCount; GetMem(Compressed,CompressedCount); stream.avail_out := CompressedCount; stream.next_out := Compressed; err := deflate(stream, Z_NO_FLUSH); if err<>Z_OK then begin if ErrorMsg<>nil then ErrorMsg^:='deflate failed'; exit; end; while TRUE do begin //writeln('run: total_in=',stream.total_in,' avail_in=',stream.avail_in,' total_out=',stream.total_out,' avail_out=',stream.avail_out); if (stream.avail_out=0) then begin // need more space if CompressedCount<128 then CompressedCount:=CompressedCount+128 else if CompressedCount>High(CompressedCount)-1024 then begin if ErrorMsg<>nil then ErrorMsg^:='deflate compression failed, because not enough space'; exit; end else CompressedCount:=CompressedCount+1024; ReAllocMem(Compressed,CompressedCount); stream.next_out:=Compressed+stream.total_out; stream.avail_out:=CompressedCount-stream.total_out; end; err := deflate(stream, Z_FINISH); if err = Z_STREAM_END then break; if err<>Z_OK then begin if ErrorMsg<>nil then ErrorMsg^:='deflate finish failed'; exit; end; end; //writeln('compressed: total_in=',stream.total_in,' total_out=',stream.total_out); CompressedCount:=stream.total_out; ReAllocMem(Compressed,CompressedCount); err := deflateEnd(stream); if err<>Z_OK then begin if ErrorMsg<>nil then ErrorMsg^:='deflateEnd failed'; exit; end; Result:=true; end; { TFPWriterTiff } procedure TFPWriterTiff.WriteWord(w: Word); begin if fStream<>nil then fStream.WriteWord(w); inc(fPosition,2); end; procedure TFPWriterTiff.WriteDWord(d: DWord); begin if fStream<>nil then fStream.WriteDWord(d); inc(fPosition,4); end; procedure TFPWriterTiff.ClearEntries; var i: Integer; List: TFPList; j: Integer; begin for i:=FEntries.Count-1 downto 0 do begin List:=TFPList(FEntries[i]); for j:=List.Count-1 downto 0 do TObject(List[j]).Free; List.Free; end; FEntries.Clear; end; procedure TFPWriterTiff.WriteTiff; begin {$IFDEF FPC_Debug_Image} writeln('TFPWriterTiff.WriteTiff fStream=',fStream<>nil); {$ENDIF} fPosition:=0; WriteHeader; WriteIFDs; WriteData; end; procedure TFPWriterTiff.WriteHeader; var EndianMark: String; begin EndianMark:={$IFDEF FPC_BIG_ENDIAN}'MM'{$ELSE}'II'{$ENDIF}; WriteBuf(EndianMark[1],2); WriteWord(42); WriteDWord(8); end; procedure TFPWriterTiff.WriteIFDs; var i: Integer; List: TFPList; j: Integer; Entry: TTiffWriterEntry; NextIFDPos: DWord; begin for i:=0 to FEntries.Count-1 do begin List:=TFPList(FEntries[i]); // write count {$IFDEF FPC_Debug_Image} writeln('TFPWriterTiff.WriteIFDs List=',i,' Count=',List.Count); {$ENDIF} WriteWord(List.Count); // write array of entries for j:=0 to List.Count-1 do begin Entry:=TTiffWriterEntry(List[j]); WriteEntry(Entry); end; // write position of next IFD if i0 then begin BitsPerSample[SamplesPerPixel]:=AlphaBits; inc(SamplesPerPixel); end; ImgWidth:=Img.Width; ImgHeight:=Img.Height; Compression:=IFD.Compression; case Compression of TiffCompressionNone, TiffCompressionDeflateZLib: ; else {$ifdef FPC_DEBUG_IMAGE} writeln('TFPWriterTiff.AddImage unsupported compression '+TiffCompressionName(Compression)+', using deflate instead.'); {$endif} Compression:=TiffCompressionDeflateZLib; end; if IFD.Orientation in [1..4] then begin OrientedWidth:=ImgWidth; OrientedHeight:=ImgHeight; end else begin // rotated OrientedWidth:=ImgHeight; OrientedHeight:=ImgWidth; end; {$IFDEF FPC_Debug_Image} writeln('TFPWriterTiff.AddImage PhotoMetricInterpretation=',IFD.PhotoMetricInterpretation); writeln('TFPWriterTiff.AddImage ImageWidth=',ImgWidth,' ImageHeight=',ImgHeight); writeln('TFPWriterTiff.AddImage Orientation=',IFD.Orientation); writeln('TFPWriterTiff.AddImage ResolutionUnit=',IFD.ResolutionUnit); writeln('TFPWriterTiff.AddImage XResolution=',TiffRationalToStr(IFD.XResolution)); writeln('TFPWriterTiff.AddImage YResolution=',TiffRationalToStr(IFD.YResolution)); writeln('TFPWriterTiff.AddImage GrayBits=',GrayBits,' RedBits=',RedBits,' GreenBits=',GreenBits,' BlueBits=',BlueBits,' AlphaBits=',AlphaBits); writeln('TFPWriterTiff.AddImage Compression=',TiffCompressionName(Compression)); writeln('TFPWriterTiff.AddImage Page=',IFD.PageNumber,'/',IFD.PageCount); {$ENDIF} // required meta entries AddEntryShortOrLong(256,ImgWidth); AddEntryShortOrLong(257,ImgHeight); AddEntryShort(259,Compression); AddEntryShort(262,IFD.PhotoMetricInterpretation); AddEntryShort(274,IFD.Orientation); AddEntryShort(296,IFD.ResolutionUnit); AddEntryRational(282,IFD.XResolution); AddEntryRational(283,IFD.YResolution); if AlphaBits>0 then begin // ExtraSamples AddEntryShort(338,2);// 2=unassociated alpha end; // BitsPerSample (required) AddEntry(258,3,SamplesPerPixel,@BitsPerSample[0],SamplesPerPixel*2); AddEntryShort(277,SamplesPerPixel); // BitsPerPixel, BytesPerLine BitsPerPixel:=0; for i:=0 to SamplesPerPixel-1 do inc(BitsPerPixel,BitsPerSample[i]); BytesPerLine:=(BitsPerPixel*OrientedWidth+7) div 8; // optional entries NewSubFileType:=0; if IFD.ImageIsThumbNail then inc(NewSubFileType,1); if IFD.ImageIsPage then inc(NewSubFileType,2); if IFD.ImageIsMask then inc(NewSubFileType,4); if NewSubFileType>0 then AddEntryLong(254,NewSubFileType); if IFD.DocumentName<>'' then AddEntryString(269,IFD.DocumentName); if IFD.ImageDescription<>'' then AddEntryString(270,IFD.ImageDescription); if IFD.Make_ScannerManufacturer<>'' then AddEntryString(271,IFD.Make_ScannerManufacturer); if IFD.Model_Scanner<>'' then AddEntryString(272,IFD.Model_Scanner); if IFD.Software<>'' then AddEntryString(305,IFD.Software); if IFD.DateAndTime<>'' then AddEntryString(306,IFD.DateAndTime); if IFD.Artist<>'' then AddEntryString(315,IFD.Artist); if IFD.HostComputer<>'' then AddEntryString(316,IFD.HostComputer); if IFD.PageCount>0 then begin Shorts[0]:=IFD.PageNumber; Shorts[1]:=IFD.PageCount; AddEntry(297,3,2,@Shorts[0],2*SizeOf(Word)); end; if IFD.PageName<>'' then AddEntryString(285,IFD.PageName); if IFD.Copyright<>'' then AddEntryString(33432,IFD.Copyright); // chunks ChunkType:=tctStrip; if IFD.TileWidth>0 then begin AddEntryShortOrLong(322,IFD.TileWidth); AddEntryShortOrLong(323,IFD.TileLength); ChunkType:=tctTile; end else begin // RowsPerStrip (required) if OrientedWidth=0 then IFD.RowsPerStrip:=8 else IFD.RowsPerStrip:=8192 div BytesPerLine; if IFD.RowsPerStrip<1 then IFD.RowsPerStrip:=1; {$IFDEF FPC_Debug_Image} writeln('TFPWriterTiff.AddImage BitsPerPixel=',BitsPerPixel,' OrientedWidth=',OrientedWidth,' BytesPerLine=',BytesPerLine,' RowsPerStrip=',IFD.RowsPerStrip); {$ENDIF} AddEntryShortOrLong(278,IFD.RowsPerStrip); end; // tags for Offsets and ByteCounts ChunkOffsets:=TTiffWriterChunkOffsets.Create(ChunkType); AddEntry(ChunkOffsets); AddEntry(ChunkOffsets.ChunkByteCounts); if (OrientedHeight>0) and (OrientedWidth>0) then begin if ChunkType=tctTile then begin TilesAcross:=(OrientedWidth+IFD.TileWidth{%H-}-1) div IFD.TileWidth; TilesDown:=(OrientedHeight+IFD.TileLength{%H-}-1) div IFD.TileLength; ChunkCount:=TilesAcross*TilesDown; {$IFDEF FPC_Debug_Image} writeln('TFPWriterTiff.AddImage BitsPerPixel=',BitsPerPixel,' OrientedWidth=',OrientedWidth,' OrientedHeight=',OrientedHeight,' TileWidth=',IFD.TileWidth,' TileLength=',IFD.TileLength,' TilesAcross=',TilesAcross,' TilesDown=',TilesDown,' ChunkCount=',ChunkCount); {$ENDIF} end else begin ChunkCount:=(OrientedHeight+IFD.RowsPerStrip{%H-}-1) div IFD.RowsPerStrip; end; ChunkOffsets.SetCount(ChunkCount); // create chunks for ChunkIndex:=0 to ChunkCount-1 do begin if ChunkType=tctTile then begin ChunkLeft:=(ChunkIndex mod TilesAcross)*IFD.TileWidth; ChunkTop:=(ChunkIndex div TilesAcross)*IFD.TileLength; ChunkWidth:=Min(IFD.TileWidth,OrientedWidth-ChunkLeft); ChunkHeight:=Min(IFD.TileLength,OrientedHeight-ChunkTop); // boundary tiles are padded to a full tile // the padding is filled with 0 and compression will get rid of it ChunkBytesPerLine:=(BitsPerPixel*IFD.TileWidth+7) div 8; ChunkBytes:=ChunkBytesPerLine*IFD.TileLength; end else begin ChunkLeft:=0; ChunkTop:=IFD.RowsPerStrip*ChunkIndex; ChunkWidth:=OrientedWidth; ChunkHeight:=Min(IFD.RowsPerStrip,OrientedHeight-ChunkTop); ChunkBytesPerLine:=BytesPerLine; ChunkBytes:=ChunkBytesPerLine*ChunkHeight; end; GetMem(Chunk,ChunkBytes); FillByte(Chunk^,ChunkBytes,0); // fill unused bytes with 0 to help compression // Orientation if IFD.Orientation in [1..4] then begin x:=ChunkLeft; y:=ChunkTop; case IFD.Orientation of 1: begin dx:=1; dy:=1; end;// 0,0 is left, top 2: begin x:=OrientedWidth-x-1; dx:=-1; dy:=1; end;// 0,0 is right, top 3: begin x:=OrientedWidth-x-1; dx:=-1; y:=OrientedHeight-y-1; dy:=-1; end;// 0,0 is right, bottom 4: begin dx:=1; y:=OrientedHeight-y-1; dy:=-1; end;// 0,0 is left, bottom end; end else begin // rotated x:=ChunkTop; y:=ChunkLeft; case IFD.Orientation of 5: begin dx:=1; dy:=1; end;// 0,0 is top, left (rotated) 6: begin dx:=1; y:=OrientedWidth-y-1; dy:=-1; end;// 0,0 is top, right (rotated) 7: begin x:=OrientedHeight-x-1; dx:=-1; y:=OrientedWidth-y-1; dy:=-1; end;// 0,0 is bottom, right (rotated) 8: begin x:=OrientedHeight-x-1; dx:=-1; dy:=1; end;// 0,0 is bottom, left (rotated) end; end; //writeln('TFPWriterTiff.AddImage Chunk=',ChunkIndex,'/',ChunkCount,' ChunkBytes=',ChunkBytes,' ChunkRect=',ChunkLeft,',',ChunkTop,',',ChunkWidth,'x',ChunkHeight,' x=',x,' y=',y,' dx=',dx,' dy=',dy); sx:=x; // save start x for cy:=0 to ChunkHeight-1 do begin x:=sx; Run:=Chunk+cy*ChunkBytesPerLine; for cx:=0 to ChunkWidth-1 do begin Col:=Img.Colors[x,y]; case IFD.PhotoMetricInterpretation of 0,1: begin // grayscale Value:=(DWord(Col.red)+Col.green+Col.blue) div 3; if IFD.PhotoMetricInterpretation=0 then Value:=$ffff-Value;// 0 is white if GrayBits=8 then begin Run^:=Value shr 8; inc(Run); end else if GrayBits=16 then begin PWord(Run)^:=Value; inc(Run,2); end; if AlphaBits=8 then begin Run^:=Col.alpha shr 8; inc(Run); end else if AlphaBits=16 then begin PWord(Run)^:=Col.alpha; inc(Run,2); end; end; 2: begin // RGB if RedBits=8 then begin Run^:=Col.red shr 8; inc(Run); end else if RedBits=16 then begin PWord(Run)^:=Col.red; inc(Run,2); end; if GreenBits=8 then begin Run^:=Col.green shr 8; inc(Run); end else if GreenBits=16 then begin PWord(Run)^:=Col.green; inc(Run,2); end; if BlueBits=8 then begin Run^:=Col.blue shr 8; inc(Run); end else if BlueBits=16 then begin PWord(Run)^:=Col.blue; inc(Run,2); end; if AlphaBits=8 then begin Run^:=Col.alpha shr 8; inc(Run); end else if AlphaBits=16 then begin PWord(Run)^:=Col.alpha; inc(Run,2); end; end; end; // next x inc(x,dx); end; // next y inc(y,dy); end; // compress case Compression of TiffCompressionDeflateZLib: EncodeDeflate(Chunk,ChunkBytes); end; ChunkOffsets.Chunks[ChunkIndex].Data:=Chunk; ChunkOffsets.Chunks[ChunkIndex].Bytes:=ChunkBytes; // next chunk end; // created chunks end; CurEntries.Sort(@CompareTiffWriteEntries); finally IFD.Free; end; end; procedure TFPWriterTiff.SaveToStream(Stream: TStream); begin fStartPos:=Stream.Position; // simulate write to compute offsets fStream:=nil; WriteTiff; // write to stream fStream:=Stream; WriteTiff; fStream:=nil; end; procedure TFPWriterTiff.InternalWrite(Stream: TStream; Img: TFPCustomImage); begin AddImage(Img); SaveToStream(Stream); end; procedure TFPWriterTiff.AddEntryString(Tag: word; const s: string); begin if s<>'' then AddEntry(Tag,2,length(s)+1,@s[1],length(s)+1) else AddEntry(Tag,2,0,nil,0); end; procedure TFPWriterTiff.AddEntryShort(Tag: word; Value: Word); begin AddEntry(Tag,3,1,@Value,2); end; procedure TFPWriterTiff.AddEntryLong(Tag: word; Value: DWord); begin AddEntry(Tag,4,1,@Value,4); end; procedure TFPWriterTiff.AddEntryShortOrLong(Tag: word; Value: DWord); begin if Value<=High(Word) then AddEntryShort(Tag,Value) else AddEntryLong(Tag,Value); end; procedure TFPWriterTiff.AddEntryRational(Tag: word; const Value: TTiffRational ); begin AddEntry(Tag,5,1,@Value,8); end; procedure TFPWriterTiff.AddEntry(Tag: Word; EntryType: Word; EntryCount: DWord; Data: Pointer; Bytes: DWord; CopyData: boolean); var Entry: TTiffWriterEntry; begin Entry:=TTiffWriterEntry.Create; Entry.Tag:=Tag; Entry.EntryType:=EntryType; Entry.Count:=EntryCount; if CopyData then begin if Bytes>0 then begin GetMem(Entry.Data,Bytes); System.Move(Data^,Entry.Data^,Bytes); end else begin Entry.Data:=nil; end; end else Entry.Data:=Data; Entry.Bytes:=Bytes; AddEntry(Entry); end; procedure TFPWriterTiff.AddEntry(Entry: TTiffWriterEntry); var List: TFPList; begin List:=TFPList(FEntries[FEntries.Count-1]); List.Add(Entry); end; procedure TFPWriterTiff.TiffError(Msg: string); begin raise Exception.Create('TFPWriterTiff.TiffError: '+Msg); end; procedure TFPWriterTiff.EncodeDeflate(var Buffer: Pointer; var Count: DWord); var NewBuffer: PByte; NewCount: cardinal; ErrorMsg: String; begin ErrorMsg:=''; NewBuffer:=nil; try NewCount:=Count; if not CompressDeflate(Buffer,Count,NewBuffer,NewCount,@ErrorMsg) then TiffError(ErrorMsg); FreeMem(Buffer); Buffer:=NewBuffer; Count:=NewCount; NewBuffer:=nil; finally ReAllocMem(NewBuffer,0); end; end; constructor TFPWriterTiff.Create; begin inherited Create; FEntries:=TFPList.Create; FSaveCMYKAsRGB:=true; end; destructor TFPWriterTiff.Destroy; begin Clear; FreeAndNil(FEntries); inherited Destroy; end; procedure TFPWriterTiff.Clear; begin ClearEntries; end; { TTiffWriterEntry } destructor TTiffWriterEntry.Destroy; begin ReAllocMem(Data,0); inherited Destroy; end; { TTiffWriterChunkOffsets } constructor TTiffWriterChunkOffsets.Create(ChunkType: TTiffChunkType); begin EntryType:=4; // long ChunkByteCounts:=TTiffWriterEntry.Create; ChunkByteCounts.EntryType:=4; // long if ChunkType=tctTile then begin Tag:=324; // TileOffsets ChunkByteCounts.Tag:=325; // TileByteCounts end else begin Tag:=273; // StripOffsets ChunkByteCounts.Tag:=279; // StripByteCounts end; end; destructor TTiffWriterChunkOffsets.Destroy; var i: Integer; begin if Chunks<>nil then begin for i:=0 to Count-1 do ReAllocMem(Chunks[i].Data,0); ReAllocMem(Chunks,0); end; inherited Destroy; end; procedure TTiffWriterChunkOffsets.SetCount(NewCount: DWord); var Size: DWord; begin {$IFDEF FPC_Debug_Image} writeln('TTiffWriteStripOffsets.SetCount OldCount=',Count,' NewCount=',NewCount); {$ENDIF} Count:=NewCount; Size:=Count*SizeOf(TTiffWriterChunk); ReAllocMem(Chunks,Size); if Size>0 then FillByte(Chunks^,Size,0); Size:=Count*SizeOf(DWord); // Offsets ReAllocMem(Data,Size); if Size>0 then FillByte(Data^,Size,0); Bytes:=Size; // ByteCounts ReAllocMem(ChunkByteCounts.Data,Size); if Size>0 then FillByte(ChunkByteCounts.Data^,Size,0); ChunkByteCounts.Count:=Count; ChunkByteCounts.Bytes:=Size; end; initialization if ImageHandlers.ImageWriter[TiffHandlerName]=nil then ImageHandlers.RegisterImageWriter (TiffHandlerName, 'tif;tiff', TFPWriterTiff); end.