lazarus-ccr/components/fpexif/fpeexifreadwrite.pas
wp_xxyyzz d415062ff4 fpexif: Less hints and warnings.
git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@8125 8e941d3f-bd1b-0410-a28a-d453659cc2b4
2021-10-27 14:54:14 +00:00

1195 lines
39 KiB
ObjectPascal

{ Writer for EXIF data
Writes the TIFF part of the APP0 segment.
In a JPEG image, the header of the APP0 segment must have been written before.
}
unit fpeExifReadWrite;
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$WARN 6058 off : Call to subroutine "$1" marked as inline is not inlined}
{$INCLUDE fpexif.inc}
interface
uses
Classes, SysUtils,
fpeGlobal, fpeUtils, fpeMetadata, fpeTags, fpeExifData;
type
TTiffHeader = packed record
BOM: Array[0..1] of AnsiChar; // 'II' for little endian, 'MM' for big endian
Signature: Word; // Signature (42)
IFDOffset: DWord; // Offset where image data begin, from start of TIFF header
end;
TIFDRecord = packed record
TagID: Word;
DataType: Word;
DataCount: DWord;
DataValue: DWord;
end;
{ A note on DataCount, from the EXIF specification:
"Count - The number of values. It should be noted carefully that the count
is not the sum of the bytes. In the case of one value of SHORT (16 bits),
for example, the count is '1' even though it is 2 Bytes." }
TBasicExifReader = class(TBasicMetadataReader)
protected
FStartPosition: Int64; // Beginning of TIFF header
FBigEndian: Boolean;
function AddTag(AStream: TStream; const AIFDRecord: TIFDRecord;
const AData: TBytes; AParent: TTagID): Integer; virtual;
function FindTagDef(ATagID: TTagID): TTagDef; virtual;
function FixEndian16(AValue: Word): Word;
function FixEndian32(AValue: DWord): DWord;
// procedure ReadIFD(AStream: TStream; AGroup: TTagGroup); virtual; //overload;
procedure ReadIFD(AStream: TStream; AParent: TTagID); virtual;
end;
TExifReader = class(TBasicExifReader)
private
FThumbPosition: Int64;
FThumbSize: Integer;
FExifVersion: AnsiString;
protected
FMake: String;
FModel: String;
function AddTag(AStream: TStream; const AIFDRecord: TIFDRecord;
const AData: TBytes; AParent: TTagID): Integer; override;
function FindTagDef(ATagID: TTagID): TTagDef; override;
procedure ReadIFD(AStream: TStream; AParent: TTagID); override;
public
constructor Create(AImgInfo: TImgInfo); override;
function ReadExifHeader(AStream: TStream): Boolean;
procedure ReadFromStream(AStream: TStream; AImgFormat: TImgFormat); override;
function ReadTiffHeader(AStream: TStream; out ABigEndian: Boolean): Boolean;
property BigEndian: Boolean read FBigEndian;
end;
TMakerNoteReader = class(TBasicExifReader)
protected
FExifVersion: string;
FMake: String;
FModel: String;
FTagDefs: TTagDefList;
FDataStartPosition: Int64;
procedure GetTagDefs({%H-}AStream: TStream); virtual;
procedure Error(const AMsg: String); override;
function FindTagDef(ATagID: TTagID): TTagDef; override;
function Prepare(AStream: TStream): Boolean; virtual;
public
constructor Create(AImgInfo: TImgInfo; AStartPos: Int64;
const AMake, AModel, AExifVersion: string; ABigEndian: Boolean); reintroduce;
destructor Destroy; override;
procedure ReadFromStream(AStream: TStream; AImgFormat: TImgFormat); override;
end;
TMakerNoteReaderClass = class of TMakerNoteReader;
TExifWriter = class(TBasicMetadataWriter)
private
FBigEndian: Boolean;
FTiffHeaderPosition: Int64;
FExifSegmentStartPos: Int64;
protected
function CalcOffsetFromTiffHeader(APosition: Int64): DWord;
function CanWriteTag(ATag: TTag): Boolean;
function FixEndian16(AValue: Word): Word;
function FixEndian32(AValue: DWord): DWord;
procedure WriteExifHeader(AStream: TStream);
procedure WriteIFD(AStream: TStream; ASubIFDList: TInt64List; AParentID: TTagID);
procedure WriteSubIFDs(AStream: TStream; ASubIFDList: TInt64List);
procedure WriteTag(AStream, AValueStream: TStream; ADataStartOffset: Int64;
ATag: TTag);
procedure WriteTiffHeader(AStream: TStream);
public
constructor Create(AImgInfo: TImgInfo); override;
procedure WriteToStream(AStream: TStream; AImgFormat: TImgFormat); override;
property BigEndian: Boolean read FBigEndian write FBigEndian;
end;
procedure RegisterMakerNoteReader(AClass: TMakerNoteReaderClass; AMake, AModel: String);
function GetMakerNoteReaderClass(AMake, AModel: String): TMakerNoteReaderClass;
implementation
uses
Contnrs,
fpeStrConsts, fpeIptcReadWrite;
const
EXIF_SIGNATURE: array[0..5] of AnsiChar = ('E', 'x', 'i', 'f', #0, #0);
LITTLE_ENDIAN_BOM: array[0..1] of AnsiChar = ('I', 'I');
BIG_ENDIAN_BOM: array[0..1] of AnsiChar = ('M', 'M');
type
TReaderItem = class
public
ReaderClass: TMakerNoteReaderClass;
Make: String;
Model: String;
end;
var
RegisteredReaders: TObjectList = nil;
function GetRegisteredReader(AMake, AModel: String): Integer;
var
item: TReaderItem;
ucMake: String;
Makes: TStrings;
j: Integer;
begin
if RegisteredReaders <> nil then
begin
Makes := TStringList.Create;
try
{$IFNDEF DELPHI7}
Makes.StrictDelimiter := true;
{$ENDIF}
Makes.Delimiter := ';';
ucMake := Uppercase(AMake);
for Result:=0 to RegisteredReaders.Count-1 do begin
item := TReaderItem(RegisteredReaders[Result]);
Makes.DelimitedText := item.Make;
for j := 0 to Makes.Count-1 do begin
if pos(Uppercase(Makes[j]), ucMake) <> 0 then
if (item.Model = '') or (AModel = '') or SameText(item.Model, AModel) then
exit;
end;
end;
finally
Makes.Free;
end;
end;
Result := -1;
end;
procedure RegisterMakerNoteReader(AClass: TMakerNoteReaderClass; AMake: String;
AModel: String);
var
item: TReaderItem;
idx: Integer;
begin
if RegisteredReaders = nil then
RegisteredReaders := TObjectList.Create;
idx := GetRegisteredReader(AMake, AModel);
if idx = -1 then begin
item := TReaderItem.Create;
item.ReaderClass := AClass;
item.Make := AMake;
item.Model := AModel;
idx := RegisteredReaders.Add(item);
end else begin
item := TReaderItem(RegisteredReaders[idx]);
item.ReaderClass := AClass;
item.Make := AMake;
item.Model := AModel;
end;
end;
function GetMakerNoteReaderClass(AMake, AModel: String): TMakerNoteReaderClass;
var
idx: Integer;
begin
idx := GetRegisteredReader(AMake, AModel);
if idx = -1 then
Result := TMakerNoteReader
else
Result := TReaderItem(RegisteredReaders[idx]).ReaderClass;
end;
//==============================================================================
// TBasicExifReader
//==============================================================================
//------------------------------------------------------------------------------
// Creates a tag from the IFD record and its data, and adds it to the tag list
// of the Exif.
// AParent is the ID of the sub-IFD to which the tag will belong (ID must already
// be left-shifted by 16 bits)
//------------------------------------------------------------------------------
function TBasicExifReader.AddTag(AStream: TStream; const AIFDRecord: TIFDRecord;
const AData: TBytes; AParent: TTagID): Integer;
var
tag: TTag;
tagDef: TTagDef;
newTagDef: TTagDef;
optns: TTagOptions;
tagIDRec: TTagIDRec;
begin
Unused(AStream);
Result := -1;
// Find the definition of the tag as specified by the ifd record
tagDef := FindTagDef(AIFDRecord.TagID or AParent);
if tagDef = nil then
begin
if (AIFDRecord.DataType < 1) or (AIFDRecord.DataType > 10) then begin
Error(Format('Unknown tag $%.4x has invalid datatype (%d)', [AIFDRecord.TagID, AIFDRecord.DataType]));
exit;
end;
tagIDRec.Tag := AIFDRecord.TagID;
tagIDRec.Parent := TTagIDRec(AParent).Parent;
newTagDef := TTagDef.Create;
newTagDef.TagIDRec := tagIDRec;
newTagDef.TagType := TTagType(AIFDRecord.DataType);
newTagDef.TagClass := DefaultTagClasses[newTagDef.TagType];
newTagDef.ReadOnly := true;
tagDef := newTagDef;
end else
newTagDef := nil;
// Populate the tag
optns := [];
if FBigEndian then Include(optns, toBigEndian);
if (eoTruncateBinary in FImgInfo.ExifData.ExportOptions) then
Include(optns, toTruncateBinary);
if (eoDecodeValue in FImgInfo.ExifData.ExportOptions) then
Include(optns, toDecodeValue);
tag := tagDef.TagClass.Create(tagDef, optns);
tag.TagType := TTagType(AIFDRecord.DataType);
tag.RawData := AData;
tag.Count := AIFDRecord.DataCount; // must be after setting RawData, its calculation of Count may be wrong!
// Add the tag to the EXIF tag list
Result := FImgInfo.ExifData.AddOrReplaceTag(tag);
newTagDef.Free;
end;
//------------------------------------------------------------------------------
// Looks for the tag with specified TagID and Group. Must be overridden by
// descendant classes.
//------------------------------------------------------------------------------
function TBasicExifReader.FindTagDef(ATagID: TTagID): TTagDef;
begin
Unused(ATagID);
Result := nil;
end;
//------------------------------------------------------------------------------
// Converts a 2-byte integer from big endian byte order to system endianness.
//------------------------------------------------------------------------------
function TBasicExifReader.FixEndian16(AValue: Word): Word;
begin
if FBigEndian then
Result := BEtoN(AValue)
else
Result := LEtoN(AValue);
end;
//------------------------------------------------------------------------------
// Converts a 4-byte integer from big endian byte order to system endianness.
//------------------------------------------------------------------------------
function TBasicExifReader.FixEndian32(AValue: DWord): DWord;
begin
if FBigEndian then
Result := BEtoN(AValue)
else
Result := LEtoN(AValue);
end;
//------------------------------------------------------------------------------
// Reads the image file directory (IFD) starting at the current stream position
// and adds the found tags to the specified group
//------------------------------------------------------------------------------
procedure TBasicExifReader.ReadIFD(AStream: TStream; AParent: TTagID);
var
numRecords: Word;
i: Integer;
ifdRec: TIFDRecord;
byteCount: Integer;
{$IFDEF FPC}
data: TBytes = nil;
{$ELSE}
data: TBytes;
{$ENDIF}
n: Int64;
tagPos: Int64;
newPos: Int64;
begin
{$IFDEF FPC}
ifdRec := Default(TIFDRecord);
{$ENDIF}
// Read count of directory entries
numRecords := FixEndian16(ReadWord(AStream));
if (AParent = TAGPARENT_THUMBNAIL) and (numRecords > 10) then begin
Warning(rsMoreThumbnailTagsThanExpected);
exit;
end;
tagPos := AStream.Position;
for i:=1 to numRecords do begin
AStream.Position := tagPos;
// Read directory entry...
n := SizeOf(ifdRec);
if AStream.Read(ifdRec, n) < n then begin
Error(Format(rsReadIncompleteIFDRecord, [tagPos]));
exit;
end;
if (ifdRec.TagID = 0) and (ifdRec.DataType = 0) and (ifdRec.DataCount = 0) and (ifdRec.DataValue = 0) then
begin
// This is an empty IFD entry as found in images of the YUNEEC CGO3 camera
// see: https://www.lazarusforum.de/viewtopic.php?f=18&t=13356 and
// https://bugs.freepascal.org/view.php?id=38904
tagPos := tagPos + n;
Continue;
end;
if (ifdRec.TagID = 0) and (ifdRec.DataType = 0) then
// Unexpected end of directory (4 zero bytes), so breaking here.
// see: https://bugs.freepascal.org/view.php?id=38904
Break;
ifdRec.TagID := FixEndian16(ifdRec.TagID);
ifdRec.DataType := FixEndian16(ifdRec.DataType);
if not (ifdRec.DataType in [1..ord(High(TTagType))]) then begin
Error(Format(rsIncorrectTagType, [ifdRec.DataType, i, ifdRec.TagID, FImgInfo.Filename]));
exit;
end;
ifdRec.DataCount := FixEndian32(ifdRec.DataCount);
// ifRec.DataValue will be converted later.
byteCount := Integer(ifdRec.DataCount) * TagElementSize[ifdRec.DataType];
if byteCount > 0 then begin
SetLength(data, bytecount);
if byteCount <= 4 then
Move(ifdRec.DataValue, data[0], byteCount)
else begin
AStream.Position := FStartPosition + FixEndian32(ifdRec.DataValue);
AStream.Read(data[0], byteCount);
end;
AddTag(AStream, ifdRec, data, AParent);
if ifdRec.DataType = ord(ttIFD) then begin
newPos := FStartPosition + FixEndian32(ifdRec.DataValue);
if newPos < AStream.Size then begin
AStream.Position := newPos;
ReadIFD(AStream, ifdRec.TagID shl 16);
end;
end;
end;
tagPos := tagPos + SizeOf(TIFDRecord);
end;
AStream.Position := tagPos;
end;
//==============================================================================
// TExifReader
//==============================================================================
//------------------------------------------------------------------------------
// Constructor of the EXIF reader
//------------------------------------------------------------------------------
constructor TExifReader.Create(AImgInfo: TImgInfo);
begin
inherited;
FStartPosition := -1;
end;
//------------------------------------------------------------------------------
// Creates a tag from the specified IFD record and its data, and adds it to the
// corresponding tag list of the EXIF object.
//------------------------------------------------------------------------------
function TExifReader.AddTag(AStream: TStream; const AIFDRecord: TIFDRecord;
const AData: TBytes; AParent: TTagID): Integer;
var
p: Int64;
iptcreader: TIPTCReader;
makernotereader: TMakerNoteReader;
readerClass: TMakerNoteReaderClass;
tag: TTag;
begin
Result := inherited AddTag(AStream, AIFDRecord, AData, AParent);
if Result = -1 then
exit;
tag := FImgInfo.ExifData.TagByIndex[Result];
if (tag is TOffsetTag) then
TOffsetTag(tag).TiffHeaderOffset := FStartPosition;
// Special handling for some tags
case tag.TagID of
FULLTAG_MAKE:
FMake := tag.AsString;
FULLTAG_MODEL:
FModel := tag.AsString;
FULLTAG_THUMBSTARTOFFSET:
FThumbPosition := FStartPosition + FixEndian32(AIFDRecord.DataValue);
FULLTAG_THUMBSIZE:
FThumbSize := FixEndian32(AIFDRecord.DataValue);
FULLTAG_EXIFVERSION:
begin
SetLength(FExifVersion, Length(AData));
Move(AData[0], FExifVersion[1], Length(FExifVersion));
end;
FULLTAG_MAKERNOTE:
if FImgInfo.MetadataKinds * [mdkExif, mdkExifNoMakerNotes] = [mdkExif] then
begin
// The stream is at the end of the makernote data area --> rewind it to start
AStream.Position := AStream.Position - Length(AData);
readerClass := GetMakerNoteReaderClass(FMake, FModel);
makernotereader := readerClass.Create(FImgInfo, FStartPosition, FMake, FModel, FExifVersion, FBigEndian);
try
makernotereader.ReadFromStream(AStream, FImgFormat);
finally
makernotereader.Free;
end;
end;
FULLTAG_IPTC: // Reads the IPTC tags as used in TIFF files.
if Length(tag.RawData) <> 0 then
begin
FImgInfo.CreateIptcData;
iptcReader := TIptcReader.Create(FImgInfo);
try
iptcReader.ReadIPTCData(tag.RawData);
finally
iptcReader.Free;
end;
end;
end;
// Some tags define a subdirectory --> Read it recursively
if (tag is TSubIFDTag) then begin
p := AStream.Position;
try
AStream.Position := FStartPosition + FixEndian32(AIFDRecord.DataValue);
ReadIFD(AStream, tag.TagID shl 16);
finally
AStream.Position := p;
end;
end;
end;
function TExifReader.FindTagDef(ATagID: TTagID): TTagDef;
begin
Result := FindExifTagDef(ATagID);
end;
//------------------------------------------------------------------------------
// For JPEG files only:
// Reads the header of the APP1 jpeg segment ("EXIF segment")
// Note that the segment marker and the segment size already have been read.
// The function returns FALSE if the header is not valid.
// Call ReadFromStream immediately afterwards
//------------------------------------------------------------------------------
function TExifReader.ReadExifHeader(AStream: TStream): Boolean;
var
hdr: array[0..5] of ansichar;
begin
AStream.Read({%H-}hdr[0], SizeOf(hdr));
Result := CompareMem(@hdr[0], @EXIF_SIGNATURE[0], SizeOf(hdr));
end;
//------------------------------------------------------------------------------
// Public method for reading the IFDs of the EXIF structure.
//
// IT IS REQUIRED THAT THE METHOD IS CALLED WHEN THE STREAM IS RIGHT AFTER
// THE TIFF HEADER.
//------------------------------------------------------------------------------
procedure TExifReader.ReadFromStream(AStream: TStream; AImgFormat: TImgFormat);
begin
FThumbPosition := -1;
FThumbSize := 0;
FImgFormat := AImgFormat;
FImgInfo.ExifData.BeginReading;
try
// Read IFD0 (primary directory). This routine will recursively also read
// the thumbnail directory (IFD1) and any subdirectories.
ReadIFD(AStream, TAGPARENT_PRIMARY);
finally
FImgInfo.ExifData.EndReading;
end;
end;
//------------------------------------------------------------------------------
// Read an image file directory (IFD) from the stream.
// The directory is specified by the parameter AGroup.
//------------------------------------------------------------------------------
procedure TExifReader.ReadIFD(AStream: TStream; AParent: TTagID);
var
p: Int64;
// thumbBuff: TBytes;
begin
inherited ReadIFD(AStream, AParent);
// The primary directory has the offset to the thumbnail directory (IFD1) as
// last DWord entry
if AParent = TAGPARENT_PRIMARY then
begin
// Read the offset from the stream
p := FixEndian32(ReadDWord(AStream));
if p > 0 then begin
// Move stream to beginning of IFD1...
p := FStartPosition + p;
if p < AStream.Size then begin
AStream.Position := p;
// ... and read IFD1
ReadIFD(AStream, TAGPARENT_THUMBNAIL);
end;
end;
end;
// In case of the thumbnail directory we read the thumbnail if available.
if (AParent = TAGPARENT_THUMBNAIL) and
(FThumbPosition > -1) and (FThumbSize > 0) //and
then //(FThumbPosition < FThumbSize) then
begin
// Move stream to beginning of thumbnail...
AStream.Position := FThumbPosition;
// ... and read thumbnail from stream to EXIF
FImgInfo.ExifData.LoadThumbnailFromStream(AStream, FThumbsize, false);
end;
end;
//------------------------------------------------------------------------------
// Reads the TIFF header which is before the EXIF structure and returns the
// endianness used in this file.
// NOTE: ReadFromStream must be called immediately afterwards
//------------------------------------------------------------------------------
function TExifReader.ReadTiffHeader(AStream: TStream;
out ABigEndian: Boolean): Boolean;
var
hdr: TTiffHeader;
begin
Result := false;
// The stream is at the beginning of the TIFF header. We store this
// position because all offsets within the EXIF segment are relative to
// the beginning of the TIFF header.
FStartPosition := AStream.Position;
// Determine endianness
AStream.Read(hdr{%H-}, SizeOf(hdr));
if CompareMem(@hdr.BOM[0], @BIG_ENDIAN_BOM[0], SizeOf(BIG_ENDIAN_BOM)) then
FBigEndian := true
else
if CompareMem(@hdr.BOM[0], @LITTLE_ENDIAN_BOM[0], SizeOf(LITTLE_ENDIAN_BOM)) then
FBigEndian := false
else
exit;
ABigEndian := FBigEndian;
// Check signature byte
hdr.Signature := FixEndian16(hdr.Signature);
if hdr.Signature <> 42 then
exit;
// Determine where the first directory (IFD0) begins...
hdr.IFDOffset := FixEndian32(hdr.IFDOffset);
// ... and move stream to there.
AStream.Position := FStartPosition + hdr.IFDOffset;
Result := true;
end;
//==============================================================================
// TMakerNoteReader
//==============================================================================
constructor TMakerNoteReader.Create(AImgInfo: TImgInfo; AStartPos: Int64;
const AMake, AModel, AExifVersion: String; ABigEndian: Boolean);
begin
inherited Create(AImgInfo);
FTagDefs := TTagDefList.Create;
FStartPosition := AStartPos;
FDataStartPosition := -1;
FMake := AMake;
FModel := AModel;
FExifVersion := AExifVersion;
FBigEndian := ABigEndian;
end;
destructor TMakerNoteReader.Destroy;
begin
FTagDefs.Free;
inherited;
end;
procedure TMakerNoteReader.GetTagDefs(AStream: TStream);
begin
// to be overridden by descendants
end;
{ Since the MakerNotes are not well-defined we don't want to abort reading of
the entire file by an incorrectly interpreted MakeNote tag.
IMPORTANT: All methods calling Error() must be exited afterwards because
the faulty file structure may lead to crashes. }
procedure TMakerNoteReader.Error(const AMsg: String);
begin
Warning(AMsg);
end;
function TMakerNoteReader.FindTagDef(ATagID: TTagID): TTagDef;
var
i: Integer;
begin
if FTagDefs <> nil then
begin
for i:=0 to FTagDefs.Count-1 do begin
Result := FTagDefs[i];
if Result.TagID = ATagID then
exit;
end;
end;
Result := nil;
end;
(*
procedure TMakerNoteReader.GetTagDefs(AStream: TStream; AImgFormat: TImgFormat);
var
UCMake, {%H-}UCModel: String;
tmp, tmp2: String;
b: TBytes;
p: Integer;
streamPos: Int64;
tiffHdrPos: Int64;
ok: Boolean;
dw: DWord;
begin
UCMake := Uppercase(FMake);
UCModel := Uppercase(FModel);
{
if UCMake = 'CANON' then
BuildCanonTagDefs(FTagDefs)
else
}
if UCMake = 'SEIKO' then
BuildEpsonTagDefs(FTagDefs)
else
if UCMake = 'SANYO' then
BuildSanyoTagDefs(FTagDefs)
else
if pos('MINOLTA', UCMake) = 1 then
BuildMinoltaTagDefs(FTagDefs)
else
if UCMake = 'FUJI' then begin
FBigEndian := false;
BuildFujiTagDefs(FTagDefs)
end else
{
if pos('OLYMP', UCMake) = 1 then
//BuildOlympusTagDefs(FTagDefs) -- is done by specific Olympus reader
else
// if UCMake = 'CASIO' then
// streamPos := AStream.Position;
// if PosInStream('QVC', AStream, streamPos) <> -1 then begin
// FTagDefs := @Casio1Table;
// FNumTagDefs := Length(Casio1Table);
// end else begin
// FTagDefs := @Casio12Table;
// FNumTagDefs := Length(Casio2Table);
// end;
// }
// BuildCasio1TagDefs(FTagDefs)
//else
if UCMake = 'NIKON CORPORATION' then begin
SetLength(b, 20);
streamPos := AStream.Position;
AStream.Read(b[0], 20);
AStream.Position := streamPos;
SetLength(tmp, 6);
Move(b[0], tmp[1], 6);
if (PosInBytes('Nikon'#00#02#16#00#00'MM'#00#42#00#00#00#08, b) > -1) or
(PosInBytes('Nikon'#00#02#16#00#00'II'#42#00#08#00#00#00, b) > -1)
then
BuildNikon3TagDefs(FTagDefs)
else begin
p := Max(0, Pos(' ', FModel));
tmp2 := FModel[p+1];
if (FExifVersion > '0210') or
((FExifVersion = '') and (tmp2 = 'D') and (AImgFormat = ifTiff))
then
BuildNikon2TagDefs(FTagDefs)
else
if (tmp = 'Nikon') then
BuildNikon1TagDefs(FTagDefs)
else
BuildNikon2TagDefs(FTagDefs);
end;
end;
end;
*)
function TMakerNoteReader.Prepare(AStream: TStream): Boolean;
begin
Unused(AStream);
Result := true;
end;
procedure TMakerNoteReader.ReadFromStream(AStream: TStream; AImgFormat: TImgFormat);
begin
if FDataStartPosition = -1 then
FDataStartPosition := AStream.Position;
FImgFormat := AImgFormat;
GetTagDefs(AStream);
if FTagDefs.Count = 0 then
exit;
AStream.Position := FDataStartPosition;
if not Prepare(AStream) then
exit;
ReadIFD(AStream, TAGPARENT_MAKERNOTE);
end;
//==============================================================================
// TExifWriter
//==============================================================================
//------------------------------------------------------------------------------
// Constructor of the EXIF writer
//------------------------------------------------------------------------------
constructor TExifWriter.Create;
begin
inherited;
FExifSegmentStartPos := -1;
end;
//------------------------------------------------------------------------------
// Calculates the difference of the specified stream position to the position
// where the TIFF header starts.
//------------------------------------------------------------------------------
function TExifWriter.CalcOffsetFromTiffHeader(APosition: Int64): DWord;
begin
if APosition > FTiffHeaderPosition then
Result := DWord(APosition - FTiffHeaderPosition)
else
Error('Incorrect stream position');
end;
//------------------------------------------------------------------------------
// Returns false if the specified tag must not be written to the stream.
// This happens if the option toVolatile of the tag's Options is set.
//------------------------------------------------------------------------------
function TExifWriter.CanWriteTag(ATag: TTag): Boolean;
begin
Result := (ATag <> nil) and (not ATag.IsVolatile);
end;
//------------------------------------------------------------------------------
// Converts a 2-byte integer to BigEndian format if required
//------------------------------------------------------------------------------
function TExifWriter.FixEndian16(AValue: Word): Word;
begin
if FBigEndian then
Result := NtoBE(AValue)
else
Result := NtoLE(AValue);
end;
//------------------------------------------------------------------------------
// Converts a 4-byte integer to BigEndian format if required
//------------------------------------------------------------------------------
function TExifWriter.FixEndian32(AValue: DWord): DWord;
begin
if FBigEndian then
Result := NtoBE(AValue)
else
Result := NtoLE(AValue);
end;
//------------------------------------------------------------------------------
// Writes the Exif header needed by JPEG files.
// Call WriteToStream immediately afterwards
//------------------------------------------------------------------------------
procedure TExifWriter.WriteExifHeader(AStream: TStream);
const
SEGMENT_MARKER: array[0..1] of byte = ($FF, $E1);
SIZE: Word = 0;
begin
FExifSegmentStartPos := AStream.Position;
AStream.WriteBuffer(SEGMENT_MARKER[0], 2);
// Next two zero bytes are the size of the entire Exif segiment, they will be
// replaced when the segment is completely written. For this, we stored the
// offset to the beginning of the EXIF segment in FExifSegmentStartPos.
AStream.WriteBuffer(SIZE, 2);
AStream.WriteBuffer(EXIF_SIGNATURE[0], 6);
end;
//------------------------------------------------------------------------------
// Writes all IFD records belonging to the same directory specified by the
// TagID of the tag which defines it.
// ASubIFDList is provided to collect all stream index positions with tags
// defining a sub-IFD; these sub-IFDs will be written later in WriteSubIFDs
// Data, in general, are written in the following order
// |<--- SubIFD records --->|<--- SubIFD data --->|
// In case of thumbnail directory (IFD1):
// |<--- IFD1 records --->|<--- Thumbnail image --->|<--- IFD1 data --->|
//
// -----------------------------------------------------------------------------
procedure TExifWriter.WriteIFD(AStream: TStream; ASubIFDList: TInt64List;
AParentID: TTagID);
var
valueStream: TMemoryStream;
i: Integer;
count: Integer;
tag: TTag;
startPos: Int64;
sizeOfTagPart: DWord;
dataStartOffset: Int64;
thumbStartOffset: Int64;
offsetToIFD1: Int64;
w: Word;
dw: DWord;
begin
// Don't write MakerNote sub-tags, they are already contained in the data of
// the MAKERNOTE tag itself.
if AParentID = TAGPARENT_MAKERNOTE then
exit;
valueStream := TMemoryStream.Create;
try
// Count IFD records in this directory
count := 0;
for i:=0 to FImgInfo.ExifData.TagCount-1 do begin
tag := FImgInfo.ExifData.TagByIndex[i];
if (tag.TagID and $FFFF0000 = AParentID) and not (tag.IsVolatile) and tag.HasData then
inc(count);
end;
// The IFD begins at the current stream position...
startPos := AStream.Position;
// ... and, knowing the size of the tag part of the subdirectory, we can
// calculate where the data part of the subdirectory will begin.
// This is needed as the offset from the beginning of the TIFF header.
sizeOfTagPart := SizeOf(Word) + // count of tags in IFD as 16bit integer
count * SizeOf(TIFDRecord) + // each tag occupies an IFDRecord
SizeOf(DWord); // 32-bit offset to next IFD, or terminating zero
dataStartOffset := startPos + sizeOfTagPart - FTiffHeaderPosition;
// In case of IFD1 (Thumbnail group) the thumbnail will be written
// immediately after all tags of IFD1. This offset position must be noted
// in the tag. We calculate and store this value here for usage later.
if (AParentID = TAGPARENT_THUMBNAIL) and FImgInfo.HasThumbnail then begin
thumbStartOffset := dataStartOffset;
dataStartOffset := dataStartOffset + FImgInfo.ExifData.ThumbnailSize;
end else
thumbStartOffset := 0;
// Write IFD record count as 16-bit integer
w := FixEndian16(count);
AStream.WriteBuffer(w, SizeOf(w));
// Now write all the records in this directory
if count > 0 then begin
for i:=0 to FImgInfo.ExifData.TagCount-1 do begin
tag := FImgInfo.ExifData.TagByIndex[i];
// Skip tags which do not belong to the requested group
if (tag.TagID and $FFFF0000 <> AParentID) or tag.IsVolatile or not tag.HasData then
Continue;
// Offset to the thumbnail image
if tag.TagID = FULLTAG_THUMBSTARTOFFSET then
tag.AsInteger := DWord(thumbStartOffset)
else
// Some tags will link to subdirectories. The offset to the start of
// a subdirectory must be specified in the DataValue field of the
// written ifd record. Since it is not clear at this moment where the
// subdirectory will begin we store the offset to the ifd record in
// ASubIFDlist for later correction.
if (tag is TSubIFDTag) and (tag.TagID <> FULLTAG_MAKERNOTE)
then
ASubIFDList.Add(AStream.Position);
// Now write the tag
WriteTag(AStream, valueStream, datastartOffset, tag);
end;
end;
// The last entry of the directory is the offset to the next IFD, or 0
// if not other IFD follows at the same level. This affects only IFD0
// where IFD1 can follow if an embedded thumbnail image exists.
if (AParentID = TAGPARENT_PRIMARY) and FImgInfo.HasThumbnail then begin
offsetToIFD1 := AStream.Position + SizeOf(DWord) + valuestream.Size;
dw := CalcOffsetFromTiffHeader(offsetToIFD1);
end else
dw := 0;
dw := FixEndian32(dw);
AStream.WriteBuffer(dw, SizeOf(dw));
// Write the thumbnail
if AParentID = TAGPARENT_THUMBNAIL then
FImgInfo.ExifData.SaveThumbnailToStream(AStream);
// Copy the valuestream to the end of the tag stream (AStream)
valueStream.Seek(0, soFromBeginning);
AStream.CopyFrom(valueStream, valueStream.Size);
// Rewind the stream to its end
AStream.Seek(0, soFromEnd);
finally
valueStream.Free;
end;
end;
//------------------------------------------------------------------------------
// The integer list ASubIFDList contains all the stream positions (in AStream)
// where tags begin which link to a subdirectory.
// WriteSubIFDs will read back the TagID of the subdirectory, write the tags
// of the subdirectory and write the position where the subdirectory starts
// to the tag's DataValue field in AStream.
//------------------------------------------------------------------------------
procedure TExifWriter.WriteSubIFDs(AStream: TStream; ASubIFDList: TInt64List);
var
subIFDStartPos: Int64;
tagPos: Int64;
i: Integer;
tagid: TTagID;
rec: TIFDRecord;
offs: DWord;
begin
i := 0;
while i < ASubIFDList.Count do begin
// The current stream position is where the subdirectory tags will be
// begin. It must be written to the subdirectory tag's DataValue field.
subIFDStartPos := AStream.Position;
// Extract the ID of the tag linking to the first subdirectory in the list
// from the already written stream. Use the offset stored in ASubIFDList
// to find it.
tagPos := ASubIFDList[0];
AStream.Position := tagPos;
// Read the tag's IFD record
AStream.ReadBuffer(rec{%H-}, SizeOf(rec));
// Get the TagID of the subdirectory (note: this might be written as big-endian)
// Then get the TagGroup corresponding to this tag; this is needed when calling WriteIFD
if FBigEndian then tagid := BEToN(rec.TagID) else tagid := LEtoN(rec.TagID);
// Write the correct subdirectory start position to the IFD record
offs := CalcOffsetFromTiffHeader(subIFDStartPos);
rec.DataValue := FixEndian32(offs);
// Write the IFD record back to the stream. Don't forget to return to
// where the tag starts!
AStream.Position := tagPos;
AStream.WriteBuffer(rec, SizeOf(rec));
// Now return the stream to the end (i.e. where the subdirectory should be)
// and write the tags of the subdirectory.
AStream.Seek(0, soFromEnd);
WriteIFD(AStream, ASubIFDList, tagID shl 16);
// Delete the current SubIFDList entry because it has been handled now.
ASubIFDList.Delete(0);
end;
end;
//------------------------------------------------------------------------------
// Writes a tag and all its related elements to the stream as an IFDRecord.
//
// AStream: stream to which the tag is written
// AValueStream: Since the data of tags being longer than 4 bytes are written
// after the tag part of the streasm, but AStream has not seen all tags yet
// we temporarily write the data part into a separate "value stream".
// ADataStartOffset: Indiates the offset of the first data bytes in the
// value stream once it has been appended to the output stream (AStream).
// It is measureed from the beginning of the TIFF header.
// ATag: Tag entry to be written
//------------------------------------------------------------------------------
procedure TExifWriter.WriteTag(AStream, AValueStream: TStream;
ADataStartOffset: Int64; ATag: TTag);
var
rec: TIFDRecord;
len: Integer;
begin
if (ATag = nil) or (not CanWriteTag(ATag)) or (not ATag.HasData) then
exit;
// Calculate number of data bytes
len := ATag.Count * TagElementSize[ord(ATag.TagType)];
// Populate elements of the IFD record
rec.TagID := FixEndian16(TTagIDRec(ATag.TagID).Tag);
rec.DataType := FixEndian16(ord(ATag.TagType));
rec.DataCount := FixEndian32(ATag.Count);
if len <= 4 then begin
rec.DataValue := 0;
Move(ATag.RawData[0], rec.DataValue, len);
end else
begin
rec.DataValue := FixEndian32(DWord(ADataStartOffset + AValueStream.Position));
AValueStream.WriteBuffer(ATag.RawData[0], Length(ATag.RawData));
end;
// Write out
AStream.Write(rec, SizeOf(Rec));
end;
(*
procedure TExifWriter.WriteTag(AStream, AValueStream: TStream;
ADataStartOffset: Int64; ATag: TTagEntry);
var
rec: TIFDRecord;
rat: TExifRational;
s: ansistring;
n: DWord;
begin
rec.TagID := FixEndian16(ATag.Tag);
rec.DataType := FixEndian16(ATag.TType);
if ATag.TType = FMT_STRING then
begin
s := ATag.Raw;
if s[Length(s)] <> #0 then s := s + #0;
rec.DataCount := FixEndian32(Length(s));
if Length(s) <= 4 then begin
n := 0;
Move(s[1], n, Length(s));
rec.DataValue := n; // tag.Raw is already has the endianness needed //FixEndian32(n);
end else begin
rec.DataValue := FixEndian32(DWord(ADataStartOffset + AValueStream.Position));
AValueStream.WriteBuffer(s[1], Length(s));
end;
end else
if ATag.TType = FMT_BINARY then begin
rec.DataCount := FixEndian32(Length(ATag.Raw));
if Length(ATag.Raw) <= 4 then begin
n := 0;
Move(ATag.Raw[1], n, Length(ATag.Raw));
rec.DataValue := n; // tag.Raw is already has the endianness needed //FixEndian32(n);
// rec.DataValue := FixEndian32(n);
end else begin
rec.DataValue := FixEndian32(DWord(ADataStartOffset + AValueStream.Position));
AValueStream.WriteBuffer(ATag.Raw[1], Length(ATag.Raw));
end;
end else
if BYTES_PER_FORMAT[ATag.TType] > 4 then begin
// If the value requires mote than 4 bytes the data bytes are written to
// the ValueStream, and the DataValue field gets the offset to the begin
// of data, counted from the start of the TIFF header. Since the stream
// with all the IDFRecords is not complete at this moment we store the
// offsets to these fields in the OffsetList for correction later.
// For this reason, we do not take care of endianness here as well.
rec.DataCount := FixEndian32(Length(ATag.Raw) div BYTES_PER_FORMAT[ATag.TType]);
rec.DataValue := FixEndian32(DWord(ADataStartOffset + AValueStream.Position));
case ATag.TType of
FMT_URATIONAL, FMT_SRATIONAL:
begin
AValueStream.WriteBuffer(ATag.Raw[1], Length(ATag.Raw));
{
// Note: ATag.Raw already has the correct endianness!
rat := PExifRational(@ATag.Raw[1])^;
// rat.Numerator := FixEndian32(rat.Numerator);
// rat.Denominator := FixEndian32(rat.Denominator);
rat.Numerator := rat.Numerator;
rat.Denominator := rat.Denominator;
AValueStream.WriteBuffer(rat, SizeOf(TExifRational));
}
end;
FMT_DOUBLE:
begin
AValueStream.WriteBuffer(ATag.Raw[1], Length(ATag.Raw));
end;
end;
end else
begin
// If the size of the data field is not larger than 4 bytes
// then the data value is written to the rec.DataValue field directly.
// Note: ATag.Raw already has the correct endianness
rec.DataCount := FixEndian32(Length(ATag.Raw) div BYTES_PER_FORMAT[ATag.TType]);
rec.DataValue := 0;
Move(ATag.Raw[1], rec.DataValue, Length(ATag.Raw));
{
rec.DataValue :
case ATag.TType of
FMT_BYTE, FMT_SBYTE:
rec.DataValue := byte(ATag.Raw[1]);
FMT_USHORT, FMT_SSHORT:
rec.DataValue := PWord(@ATag.Raw[1])^;
//rec.DataValue := FixEndian32(PWord(@ATag.Raw[1])^);
FMT_ULONG, FMT_SLONG:
rec.DataValue := PDWord(@ATag.Raw[1])^;
//rec.DataValue := FixEndian32(PDWord(@ATag.Raw[1])^);
FMT_SINGLE:
Move(ATag.Raw[1], rec.DataValue, SizeOf(Single));
end;
}
end;
// Write out
AStream.Write(rec, SizeOf(Rec));
end;
*)
procedure TExifWriter.WriteTiffHeader(AStream: TStream);
var
header: TTiffHeader;
offs: DWord;
begin
if FBigEndian then
Move(BIG_ENDIAN_BOM[0], {%H-}header.BOM[0], 2)
else
Move(LITTLE_ENDIAN_BOM[0], header.BOM[0], 2);
header.Signature := FixEndian16(42); // magic number
offs := SizeOf(header);
header.IFDOffset := FixEndian32(offs); // Offset to start of IFD0, from begin of TIFF header
// Write out
AStream.WriteBuffer(header, SizeOf(header));
end;
procedure TExifWriter.WriteToStream(AStream: TStream; AImgFormat: TImgFormat);
var
subIFDList: TInt64List;
begin
FImgFormat := AImgFormat;
case FImgFormat of
ifJpeg:
WriteExifHeader(AStream);
else
Error('Image format not supported.');
end;
subIFDList := TInt64List.Create;
try
// Tiff header
FTiffHeaderPosition := AStream.Position;
WriteTiffHeader(AStream);
// Write IFD0
WriteIFD(AStream, subIFDList, TAGPARENT_PRIMARY);
// Write IFD1
if FImgInfo.HasThumbnail then
WriteIFD(AStream, subIFDList, TAGPARENT_THUMBNAIL);
// Write special subIFDs collected in subIFDList
WriteSubIFDs(AStream, subIFDList);
// If WriteToStream is called within a JPEG structure we must update the
// size of the EXIF segment.
UpdateSegmentSize(AStream, FExifSegmentStartPos);
finally
subIFDList.Free;
end;
end;
initialization
finalization
RegisteredReaders.Free;
end.