lazarus-ccr/components/fpexif/fpemetadata.pas
wp_xxyyzz 660f4a7db7 fpexif: Undo r7416
git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@7778 8e941d3f-bd1b-0410-a28a-d453659cc2b4
2020-10-14 19:05:30 +00:00

778 lines
22 KiB
ObjectPascal

unit fpeMetadata;
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I fpexif.inc}
interface
uses
Classes, SysUtils,
{$IFDEF FPC}
LazUTF8,
{$ENDIF}
fpeGlobal,
fpeExifData, fpeIptcData;
type
TImgInfo = class;
{ TBasicMetadataReaderWriter }
TBasicMetadataReaderWriter = class
protected
FImgInfo: TImgInfo;
FImgFormat: TImgFormat;
procedure Warning(const AMsg: String);
public
constructor Create(AImgInfo: TImgInfo); virtual;
end;
{ TBasicMetadataReader }
TBasicMetadataReader = class(TBasicMetadataReaderWriter)
protected
procedure Error(const AMsg: String); virtual;
public
procedure ReadFromStream(AStream: TStream; AImgFormat: TImgFormat); virtual;
end;
{ TBasicMetadataWriter }
TBasicMetadataWriter = class(TBasicMetadataReaderWriter)
protected
procedure Error(const AMsg: String); virtual;
procedure UpdateSegmentSize(AStream: TStream; ASegmentStartPos: Int64);
public
procedure WriteToStream(AStream: TStream; AImgFormat: TImgFormat); virtual;
end;
{ TImgInfo }
TImgInfo = class
private
FFileName: String;
FFileDate: TDateTime;
FFileSize: Int64;
FImgFormat: TImgFormat;
FImgWidth: Integer;
FImgHeight: Integer;
FWarnings: TStrings;
FMetadataKinds: TMetadataKinds;
FJFIFSegment: TBytes;
FJFXXThumbnail: TBytes;
FWriteJFIFandEXIF: Boolean;
FComment: String;
private
FExifData: TExifData;
FIptcData: TIptcData;
function GetComment: String;
function GetWarnings: String;
procedure SetComment(const AValue: String);
protected
procedure Error(const AMsg: String);
function ExtractImgFormat(AStream: TStream): TImgFormat;
procedure MergeToJpegStream(AInputStream, AOutputStream: TStream);
procedure ReadJpeg(AStream: TStream);
procedure ReadTiff(AStream: TStream);
procedure StoreFileInfo(const AFileName: String);
procedure WriteJpeg(AStream: TStream);
public
constructor Create;
destructor Destroy; override;
procedure LoadFromFile(const AFileName: String);
procedure LoadFromStream(AStream: TStream);
procedure Save;
procedure SaveToFile(const AFileName: String; AImgFile: String = '');
procedure SaveThumbnailToStream(AStream: TStream);
function CreateExifData(ABigEndian: Boolean = false): TExifData;
function CreateIptcData: TIptcData;
function HasComment: Boolean;
function HasExif: Boolean;
function HasIptc: Boolean;
function HasThumbnail: Boolean;
function HasWarnings: boolean;
{ Comment stored in the Jpeg COM segment }
property Comment: String read GetComment write SetComment;
{ Name of the file processed }
property FileName: String read FFileName;
{ Date when the file was created }
property FileDate: TDateTime read FFileDate;
{ Size of the file in bytes }
property FileSize: Int64 read FFileSize;
{ Image format, jpeg or tiff }
property ImgFormat: TImgFormat read FImgFormat;
{ Image width }
property ImgWidth: Integer read FImgWidth;
{ Image height }
property ImgHeight: Integer read FImgHeight;
{ Selects which kind of metadata will be loaded }
property MetadataKinds: TMetadataKinds read FMetadataKinds write FMetadataKinds default mdkAll;
{ Warning message - NOTE: Reading of warnings is erasing the warnings list! }
property Warnings: String read GetWarnings;
{ Write both JFIF and EXIF data if available. Normally they are mutually exclusive. }
property WriteJFIFandEXIF: Boolean read FWriteJFIFandEXIF write FWriteJFIFandEXIF;
property ExifData: TExifData read FExifData;
property IptcData: TIptcData read FIptcData; // to do: rename to IptcData
end;
implementation
uses
Variants,
fpeStrConsts, fpeUtils, fpeExifReadWrite, fpeIptcReadWrite;
type
TJpegJFIFSegment = packed record
Identifier: packed array[0..4] of AnsiChar; // 'JFIF'#0
JFIFVersion: packed array[0..1] of Byte; // 01 02
DensityUnit: Byte; // 0: aspect ratio, 1: inches, 2: cm
XDensity: Word;
YDensity: Word;
ThumbnailWidth: Byte; // Pixel count of thumbnail width...
ThumbnailHeight: Byte; // ... and height
end;
PJpegJFIFSegment = ^TJpegJFIFSegment;
TJpegJFXXSegment = packed record
Identifier: packed array[0..4] of AnsiChar; // 'JFXX'#0
ThumbnailFormat: byte; // 10: JPEG, 11: 1 byte-per-pixel palettized, 12: 3 byte-per-pixel RGB
end;
// ThumbnailData are following
PJpegJFXXSegment = ^TJpegJFXXSegment;
TJpegSOF0Segment = packed record
DataPrecision: Byte;
ImageHeight: Word;
ImageWidth: Word;
// and more..., not needed here.
end;
PJpegSOF0Segment = ^TJpegSOF0Segment;
const
{ JPEG markers consist of one or more $FF bytes, followed by a marker code
byte (which is not an FF). Here are the marker codes needed by fpExif: }
M_SOF0 = $C0; // Start Of Frame 0
M_SOI = $D8; // Start Of Image (beginning of datastream)
M_EOI = $D9; // End Of Image (end of datastream)
M_SOS = $DA; // Start Of Scan (begins compressed data)
M_JFIF = $E0; // Jfif marker 224
M_EXIF = $E1; // Exif marker 225
M_IPTC = $ED; // IPTC - Photoshop 237
M_COM = $FE; // Comment 254
//==============================================================================
// TBasicMetaDataWriter
//==============================================================================
constructor TBasicMetadataReaderWriter.Create(AImgInfo: TImgInfo);
begin
FImgInfo := AImgInfo;
end;
procedure TBasicMetadataReaderWriter.Warning(const AMsg: String);
begin
FImgInfo.FWarnings.Add(AMsg);
end;
//==============================================================================
// TBasicMetaDataReader
//==============================================================================
procedure TBasicMetadataReader.Error(const AMsg: String);
begin
raise EFpExifReader.Create(AMsg);
end;
procedure TBasicMetadataReader.ReadFromStream(AStream: TStream;
AImgFormat: TImgFormat);
begin
Assert(AStream <> nil);
FImgFormat := AImgFormat;
end;
//==============================================================================
// TBasicMetaDataWriter
//==============================================================================
procedure TBasicMetadataWriter.Error(const AMsg: String);
begin
raise EFpExifWriter.Create(AMsg);
end;
procedure TBasicMetadataWriter.UpdateSegmentSize(AStream: TStream;
ASegmentStartPos: Int64);
var
startPos: Int64;
segmentSize: Word;
w: Word;
begin
// If the metadata structure is part of a jpeg file (e.g.) then the start
// position of the corresponding metadata segment has been stored in
// ASegmentStartPos. In other cases ASegmentStartPos is -1.
// This means: if ASegmentStartPos is > -1 then the segment size must be
// written to the segment start position.
if (ASegmentStartPos < 0) then
exit;
// From the current stream position (at the end) and the position where
// the segment size must be written, we calculate the size of the segment
startPos := ASegmentStartPos + SizeOf(word);
segmentSize := AStream.Position - startPos;
// Move the stream to where the segment size must be written...
AStream.Position := startPos;
// ... and write the segment size.
w := BEToN(segmentSize);
AStream.WriteBuffer(w, SizeOf(w));
// Rewind stream to the end
AStream.Seek(0, soFromEnd);
end;
procedure TBasicMetadataWriter.WriteToStream(AStream: TStream;
AImgFormat: TImgFormat);
begin
Assert(AStream <> nil);
FImgFormat := AImgFormat;
end;
//==============================================================================
// TImgInfo
//==============================================================================
constructor TImgInfo.Create;
begin
FMetadataKinds := mdkAll;
FWarnings := TStringList.Create;
end;
destructor TImgInfo.Destroy;
begin
FWarnings.Free;
FExifData.Free;
FIptcData.Free;
inherited;
end;
function TImgInfo.CreateExifData(ABigEndian: Boolean = false): TExifData;
begin
FWarnings.Clear;
FExifData.Free;
FExifData := TExifData.Create(ABigEndian);
Result := FExifData;
end;
function TImgInfo.CreateIptcData: TIptcData;
begin
FWarnings.Clear;
FIptcData.Free;
FIptcData := TIptcData.Create;
Result := FIptcData;
end;
procedure TImgInfo.Error(const AMsg: String);
begin
raise EFpExif.Create(AMsg);
end;
function TImgInfo.ExtractImgFormat(AStream: TStream): TImgFormat;
var
p: Int64;
hdr: array[0..SizeOf(TTiffHeader)-1] of byte;
tiffHdr: TTiffHeader absolute hdr;
begin
p := AStream.Position;
try
AStream.Read({%H-}hdr[0], SizeOf(hdr));
// Test for jpeg signature
if (hdr[0] = $FF) and (hdr[1] = $D8) then begin
Result := ifJpeg;
exit;
end;
// Test for TIFF header
if (tiffHdr.BOM[0]='I') and (tiffHdr.BOM[1]='I') and (LEtoN(tiffHdr.Signature) = 42)
then begin
Result := ifTiff;
exit;
end;
if (tiffHdr.BOM[0]='M') and (tiffHdr.BOM[1]='M') and (BEtoN(tiffHdr.signature) = 42)
then begin
Result := ifTiff;
exit;
end;
Result := ifUnknown;
finally
AStream.Position := p;
end;
end;
function TImgInfo.GetComment: String;
begin
Result := FComment;
end;
function TImgInfo.GetWarnings: String;
begin
Result := FWarnings.Text;
FWarnings.Clear;
end;
function TImgInfo.HasComment: Boolean;
begin
Result := FComment <> '';
end;
function TImgInfo.HasExif: Boolean;
begin
Result := (FExifData <> nil) and (FExifData.TagCount > 0);
end;
function TImgInfo.HasIptc: Boolean;
begin
Result := (FIptcData <> nil) and (FIptcData.TagCount > 0);
end;
function TImgInfo.HasThumbnail: boolean;
begin
Result := ((FExifData <> nil) and FExifData.HasThumbnail)
or (Length(FJFXXThumbnail) > 0);
end;
function TImgInfo.HasWarnings: boolean;
begin
Result := FWarnings.Count > 0;
end;
procedure TImgInfo.LoadFromFile(const AFileName: String);
var
stream: TStream;
begin
if not FileExists(AFileName) then
Error(Format(rsFileNotFoundError, [AFileName]));
FWarnings.Clear;
StoreFileInfo(AFileName);
stream := TFileStream.Create(AFileName, fmOpenRead or fmShareDenyNone);
try
LoadFromStream(stream);
finally
stream.Free;
end;
end;
procedure TImgInfo.LoadFromStream(AStream: TStream);
begin
FWarnings.Clear;
FImgFormat := ExtractImgFormat(AStream);
if FImgFormat = ifUnknown then
Error(rsUnknownImageFormat);
case FImgFormat of
ifJpeg:
ReadJpeg(AStream);
ifTiff:
ReadTiff(AStream);
else
Error('TImgInfo.LoadFromStream: ' + rsImageFormatNotSupported);
end;
end;
{ Reads the image data from AInputstream and replaces the meta data segments
by those of TImgInfo }
procedure TImgInfo.MergeToJpegStream(AInputStream, AOutputStream: TStream);
type
TSegmentHeader = packed record
Key: byte;
Marker: byte;
Size: Word;
end;
const
SOI_MARKER: array[0..1] of byte = ($FF, $D8);
var
header: TSegmentHeader;
n, count: Int64;
savedPos: Int64;
jfif: TJpegJFIFSegment;
begin
// Write the header segment and all metadata segments stored in TImgInfo
// to the beginning of the stream
AOutputStream.Position := 0;
WriteJpeg(AOutputStream);
AInputStream.Position := 0;
// Now write copy all other segments.
AInputStream.Position := 0;
while AInputStream.Position < AInputStream.Size do begin
savedPos := AInputStream.Position; // just for debugging
n := AInputStream.Read(header{%H-}, SizeOf(header));
if n <> Sizeof(header) then
Error(rsIncompleteJpegSegmentHeader);
if header.Key <> $FF then
Error(rsJpegSegmentMarkerExpected);
header.Size := BEToN(header.Size);
// Save stream position before segment size value.
savedPos := AInputStream.Position - 2;
case header.Marker of
M_SOI:
header.Size := 0;
M_JFIF, M_EXIF, M_IPTC, M_COM: // these segments were already written by WriteJpeg
;
M_SOS:
begin
// this is the last segment before compressed data which don't have a marker
// --> just copy the rest of the file
count := AInputStream.Size - savedPos;
AInputStream.Position := savedPos;
AOutputStream.WriteBuffer(header, 2);
n := AOutputStream.CopyFrom(AInputStream, count);
if n <> count then
Error(rsJpegCompressedDataWriting);
break;
end;
else
AInputStream.Position := AInputStream.Position - 4; // go back to where the segment begins
n := AOutputStream.CopyFrom(AInputStream, Int64(header.Size) + 2);
if n <> Int64(header.Size) + 2 then
Error(rsJpegReadWriteErrorInSegment);
end;
AInputStream.Position := savedPos + header.Size;
end;
end;
procedure TImgInfo.ReadJpeg(AStream: TStream);
const
sJFIF: String[5] = 'JFIF'#0;
sJFXX: String[5] = 'JFXX'#0;
var
marker: Byte;
size: Word;
streamsize: Int64;
p: Int64;
buf: TBytes;
reader: TBasicMetadataReader;
bigEndian: Boolean;
hdr: TBytes;
hasJFIF: Boolean;
{$IFNDEF FPC}
sa: ansistring;
{$ENDIF}
begin
p := AStream.Position;
streamsize := AStream.Size;
if not ((ReadByte(AStream) = $FF) and (ReadByte(AStream) = M_SOI)) then
exit;
while p < streamsize do begin
// The basic structure of the jpeg segments is
// $FF ..... identifier (sometimes repeated)
// marker .. segment identifier (1 byte)
// size .... size of the segment in bytes (2 bytes), including size field
// data .... data of the segment, (size)-2 bytes.
repeat
marker := ReadByte(AStream);
until marker <> $FF;
size := BEtoN(ReadWord(AStream)) - 2;
p := AStream.Position;
case marker of
M_EXIF:
if FMetaDataKinds * [mdkExif, mdkExifNoMakerNotes] <> [] then begin
reader := TExifReader.Create(self);
try
if not TExifReader(reader).ReadExifHeader(AStream) then
exit;
if not TExifReader(reader).ReadTiffHeader(AStream, bigEndian) then
exit;
FExifData := CreateExifData(bigEndian);
try
reader.ReadFromStream(AStream, ifJpeg);
except
FreeAndNil(FExifData);
raise;
end;
finally
reader.Free;
end;
end;
M_IPTC:
if (mdkIPTC in FMetadataKinds) then begin
reader := TIptcReader.Create(self);
try
FIptcData := CreateIptcData;
try
reader.ReadFromStream(AStream, ifJpeg);
except
FreeAndNil(FIptcData);
raise;
end;
finally
reader.Free;
end;
end;
M_COM:
if (mdkComment in FMetadataKinds) and (size > 0) then
begin
// JFIF comment is encoded as UTF8 according to
// http://mail.kde.org/pipermail/digikam-devel/2006-May/005000.html
{$IFDEF FPC}
SetLength(FComment, size);
AStream.Read(FComment[1], size);
{$ELSE}
SetLength(sa, size);
AStream.Read(sa[1], size);
{$IFDEF UNITCODE}
FComment := UTF8Decode(sa);
{$ELSE}
FComment := Utf8ToAnsi(sa);
{$ENDIF}
{$ENDIF}
end;
M_JFIF:
begin
SetLength(hdr, size);
AStream.Read(hdr[0], size);
with PJpegJFIFSegment(@hdr[0])^ do begin
if CompareMem(@Identifier[0], @sJFIF[1], Length(sJFIF)) then
begin
// JFIF APP0 marker segment
SetLength(FJFIFSegment, size);
Move(hdr[0], FJFIFSegment[0], size);
if (JFIFVersion[0] <> 1) then
exit;
end else
if CompareMem(@Identifier[0], @sJFXX[1], Length(sJFXX)) then
begin
// JFXX extension APP0 marker segment (optional)
// alternative location of a thumbnail image:
// https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format#JFIF_extension_APP0_marker_segment
{
// --- not supported at the moment.
SetLength(FJFXXHeaderSegment, size);
Move(hdr[0], FJFXXHeaderSegment[0], size);
}
// --- not working... not a valid jpeg structure.
SetLength(FJFXXThumbnail, size - SizeOf(TJpegJFXXSegment)); //(AStream.Position - p));
Move(hdr[SizeOf(TJpegJFXXSegment)], FJFXXThumbnail[0], Length(FJFXXThumbnail));
end;
end;
hasJFIF := true;
end;
M_SOF0:
begin
SetLength(buf, size);
AStream.Read(buf[0], size);
with PJpegSOF0Segment(@buf[0])^ do begin
FImgHeight := BEtoN(ImageHeight);
FImgWidth := BEtoN(ImageWidth);
end;
SetLength(buf, 0);
end;
M_EOI, M_SOS:
break;
end;
AStream.Position := p + size;
end;
// Force writing of JFIF if it coexists with EXIF.
FWriteJFIFandEXIF := hasJFIF and HasExif;
end;
procedure TImgInfo.ReadTiff(AStream: TStream);
var
reader: TExifReader;
bigEndian: Boolean;
begin
reader := TExifReader.Create(self);
try
if not TExifReader(reader).ReadTiffHeader(AStream, bigEndian) then
exit;
FExifData := CreateExifData(bigEndian);
try
reader.ReadFromStream(AStream, ifTiff);
except
FreeAndNil(FExifData);
raise;
end;
finally
reader.Free;
end;
end;
procedure TImgInfo.Save;
begin
SaveToFile(FFileName);
end;
procedure TImgInfo.SaveThumbnailToStream(AStream: TStream);
begin
if (FExifData <> nil) and ExifData.HasThumbnail then
FExifData.SaveThumbnailToStream(AStream)
else
if Length(FJFXXThumbnail) > 0 then
AStream.Write(FJFXXThumbnail[0], Length(FJFXXThumbnail));
end;
procedure TImgInfo.SaveToFile(const AFileName: String; AImgFile: String = '');
var
ms: TMemoryStream;
srcStream: TFileStream;
begin
if (AImgFile = '') then
AImgFile := FFileName;
if AImgFile = '' then
Error(rsImageDataFileNotSpecified);
if not FileExists(AImgFile) then
Error(Format(rsImageDataFileNotExisting, [AImgFile]));
FWarnings.Clear;
ms := TMemoryStream.Create;
try
srcstream := TFileStream.Create(AImgFile, fmOpenRead + fmShareDenyNone);
try
if FImgFormat = ifUnknown then begin
FimgFormat := ExtractImgFormat(srcstream);
if FImgFormat = ifUnknown then
Error(rsCannotSaveToUnknownFileFormat);
end;
case FImgFormat of
ifJpeg: MergeToJpegStream(srcstream, ms);
ifTiff: Error(Format(rsWritingNotImplemented, ['TIFF']));
else Error(rsImageFormatNotSupported);
end;
finally
// Destroy the srcStream before saving the memorystream to file to prevent
// an error if AImgFile = AFileName
srcStream.Free;
end;
ms.SaveToFile(AFileName)
finally
ms.Free;
end;
end;
procedure TImgInfo.SetComment(const AValue: String);
begin
FComment := AValue;
end;
procedure TImgInfo.StoreFileInfo(const AFileName: String);
var
rec: TSearchRec;
res: word;
begin
res := FindFirst(AFilename, faAnyFile, rec);
if res = 0 then
begin
FFilename := AFilename;
FFileDate := FileDateToDateTime(rec.Time);
FFileSize := rec.Size;
end;
FindClose(rec);
end;
{ Writes all metadata-related segments to a stream. Note image data must be
written separately. }
procedure TImgInfo.WriteJpeg(AStream: TStream);
const
SOI_MARKER: array[0..1] of byte = ($FF, $D8);
COM_MARKER: array[0..1] of byte = ($FF, $FE);
JFIF_MARKER: array[0..1] of byte = ($FF, $E0);
JFIF_ID: ansistring = 'JFIF'#0;
var
jfifSegment: TJpegJFIFSegment;
writer: TBasicMetadataWriter;
{$IFNDEF FPC}
sa: ansistring;
{$ENDIF}
begin
// Write Start-of-image segment (SOI)
AStream.WriteBuffer(SOI_MARKER, SizeOf(SOI_MARKER));
// No EXIF or JFIF requested: write APP0 segment
if (not HasExif) or
(FMetaDataKinds * [mdkExif, mdkExifNoMakerNotes] = []) or
FWriteJFIFandEXIF then
begin
// No Exif, no JFIF --> write a default APP0 segment
if Length(FJFIFSegment) = 0 then
begin
Move(JFIF_ID[1], {%H-}jfifSegment.Identifier[0], Length(JFIF_ID));
jfifSegment.JFIFVersion[0] := 1;
jfifSegment.JFIFVersion[1] := 2;
jfifSegment.DensityUnit := 1; // inch
jfifSegment.XDensity := NtoBE(72); // 72 ppi
jfifSegment.YDensity := NtoBE(72);
jfifSegment.ThumbnailWidth := 0; // no thumbnail in APP0 segment
jfifSegment.ThumbnailHeight := 0;
AStream.WriteBuffer(JFIF_MARKER, SizeOf(JFIF_MARKER));
WriteWord(AStream, NtoBE(Word(SizeOf(jfifSegment) + 2)));
AStream.WriteBuffer(jfifSegment, SizeOf(jfifSegment));
end
// No Exif, but JFIF --> write the JFIF segment of the file
else begin
AStream.WriteBuffer(JFIF_MARKER, SizeOf(JFIF_MARKER));
WriteWord(AStream, NToBE(Word(LengtH(FJFIFSegment) + 2)));
AStream.WriteBuffer(FJFIFSegment[0], Length(FJFIFSegment));
end;
end;
// Exif --> Write APP1 segment
if HasExif then
begin
writer := TExifWriter.Create(Self);
try
TExifWriter(writer).BigEndian:= FExifData.BigEndian;
writer.WriteToStream(AStream, ifJpeg);
finally
writer.Free;
end;
end;
// Write IPTCSegment (APP13)
if (mdkIPTC in FMetadataKinds) and HasIPTC then begin
writer := TIptcWriter.Create(Self);
try
TIptcWriter(writer).WriteToStream(AStream, ifJpeg);
finally
writer.Free;
end;
end;
// Write comment segment
if (mdkComment in FMetadataKinds) and HasComment then begin
// JFIF Comment is encoded as utf8
// according to http://mail.kde.org/pipermail/digikam-devel/2006-May/005000.html
AStream.WriteBuffer(COM_MARKER, SizeOf(COM_MARKER));
{$IFDEF FPC}
WriteWord(AStream, NtoBE(Word(Length(FComment) + 2)));
AStream.WriteBuffer(FComment[1], Length(FComment));
{$ELSE}
{$IFDEF UNICODE}
sa := UTF8Encode(FComment);
{$ELSE}
sa := AnsiToUTF8(FComment);
{$ENDIF}
WriteWord(AStream, NtoBE(Word(Length(sa) + 2)));
AStream.WriteBuffer(sa[1], Length(sa));
{$ENDIF}
end;
end;
end.