doublecmd/src/uexifreader.pas
Alexander Koblov fc7f57ff44 UPD: Exif-wdx - use non-localized field names for storage
(cherry picked from commit 46c39f0024)
2024-03-08 17:42:23 +03:00

308 lines
8.3 KiB
ObjectPascal

{
Double Commander
-------------------------------------------------------------------------
Simple exchangeable image file format reader
Copyright (C) 2016-2024 Alexander Koblov (alexx2000@mail.ru)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
}
unit uExifReader;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, StreamEx;
type
{ TTag }
TTag = packed record
ID : UInt16; // Tag number
Typ : UInt16; // Tag type
Count : UInt32; // Tag length
Offset : UInt32; // Offset / Value
end;
{ TExifReader }
TExifReader = class(TMemoryStream)
private
FOffset: Int64;
FSwap: Boolean;
protected
FMake: String;
FModel: String;
FImageWidth: UInt16;
FImageHeight: UInt16;
FOrientation: UInt16;
FDateTimeOriginal: TDateTime;
private
procedure Reset;
function ReadString(Offset, Count: Int32): String;
function ReadDateTime(Offset, Count: Int32): TDateTime;
procedure ReadTag(var ATag: TTag);
function DoImageFileDirectory: Boolean;
public
function LoadFromFile(const FileName: String): Boolean;
property Make: String read FMake;
property Model: String read FModel;
property ImageWidth: UInt16 read FImageWidth;
property ImageHeight: UInt16 read FImageHeight;
property Orientation: UInt16 read FOrientation;
property DateTimeOriginal: TDateTime read FDateTimeOriginal;
end;
const
cMake = 'Manufacturer';
cModel = 'Camera model';
cImageWidth = 'Width';
cImageHeight = 'Height';
cOrientation = 'Orientation';
cDateTimeOriginal = 'Date taken';
resourcestring
rsMake = cMake;
rsModel = cModel;
rsImageWidth = cImageWidth;
rsImageHeight = cImageHeight;
rsOrientation = cOrientation;
rsDateTimeOriginal = cDateTimeOriginal;
implementation
uses
Math, DCClassesUtf8;
{ TExifReader }
procedure TExifReader.Reset;
begin
Clear;
FImageWidth:= 0;
FImageHeight:= 0;
FOrientation:= 0;
FMake:= EmptyStr;
FModel:= EmptyStr;
FDateTimeOriginal:= 0;
end;
function TExifReader.ReadString(Offset, Count: Int32): String;
var
AOffset: Int64;
begin
if Count <= 4 then
Result:= PAnsiChar(@Offset)
else begin
AOffset:= Self.Seek(0, soCurrent);
Self.Seek(Offset + FOffset, soBeginning);
SetLength(Result, Count);
Self.ReadBuffer(Result[1], Count);
Result:= PAnsiChar(Result);
Self.Seek(AOffset, soBeginning);
end;
end;
function TExifReader.ReadDateTime(Offset, Count: Int32): TDateTime;
var
S: String;
SystemTime: TSystemTime;
begin
S:= ReadString(Offset, Count);
try
SystemTime.Millisecond:= 0;
// Data format is "YYYY:MM:DD HH:MM:SS"
SystemTime.Year:= StrToDWord(Copy(S, 1, 4));
SystemTime.Month:= StrToDWord(Copy(S, 6, 2));
SystemTime.Day:= StrToDWord(Copy(S, 9, 2));
SystemTime.Hour:= StrToDWord(Copy(S, 12, 2));
SystemTime.Minute:= StrToDWord(Copy(S, 15, 2));
SystemTime.Second:= StrToDWord(Copy(S, 18, 2));
Result:= SystemTimeToDateTime(SystemTime);
except
Result:= 0;
end;
end;
procedure TExifReader.ReadTag(var ATag: TTag);
begin
Self.ReadBuffer(ATag, SizeOf(TTag));
if FSwap = False then
begin
case ATag.Typ of
1, 6: ATag.Offset:= UInt8(ATag.Offset);
3, 8: ATag.Offset:= UInt16(ATag.Offset);
end;
end
else begin
ATag.ID:= SwapEndian(ATag.ID);
ATag.Typ:= SwapEndian(ATag.Typ);
ATag.Count:= SwapEndian(ATag.Count);
case ATag.Typ of
1, 6: ATag.Offset:= UInt8(ATag.Offset);
3, 8: ATag.Offset:= SwapEndian(UInt16(ATag.Offset));
else if (ATag.Typ <> 2) or (ATag.Count > 4) then
ATag.Offset:= SwapEndian(ATag.Offset);
end;
end;
end;
function TExifReader.DoImageFileDirectory: Boolean;
var
I: Int32;
ATag: TTag;
ACount: UInt16;
AOffset: Int32 = 0;
begin
ACount:= Self.ReadWord;
if FSwap then ACount:= SwapEndian(ACount);
for I:= 1 to ACount do
begin
ReadTag(ATag);
case ATag.ID of
$100: // Image width
begin
FImageWidth := ATag.Offset;
end;
$101: // Image height
begin
FImageHeight := ATag.Offset;
end;
$010f: // Shows manufacturer of digicam
begin
FMake:= ReadString(ATag.Offset, ATag.Count);
end;
$0110: // Shows model number of digicam
begin
FModel:= ReadString(ATag.Offset, ATag.Count);
end;
$0112: // The orientation of the camera relative to the scene
begin
FOrientation:= ATag.Offset;
end;
$8769: // Exif IFD Pointer
begin
AOffset:= ATag.Offset;
end;
end;
end;
Result:= ACount > 0;
if AOffset > 0 then
begin
Self.Seek(FOffset + AOffset, soBeginning);
ACount:= Self.ReadWord;
if FSwap then ACount:= SwapEndian(ACount);
for I:= 1 to ACount do
begin
ReadTag(ATag);
case ATag.ID of
$9003: // Date/Time of original image taken
begin
FDateTimeOriginal:= ReadDateTime(ATag.Offset, ATag.Count);
end;
// Image pixel width
$A002: if FImageWidth = 0 then FImageWidth := ATag.Offset;
// Image pixel height
$A003: if FImageHeight = 0 then FImageHeight := ATag.Offset;
end;
end;
end;
end;
function TExifReader.LoadFromFile(const FileName: String): Boolean;
const
BUFFER_SIZE = 196608;
var
P: UInt16;
ASize: UInt16;
Offset: UInt32;
AFile: TFileStreamEx;
Magic: array [0..5] of AnsiChar;
begin
Reset;
try
AFile:= TFileStreamEx.Create(FileName, fmOpenRead or fmShareDenyNone);
try
Self.SetSize(Min(AFile.Size, BUFFER_SIZE));
AFile.ReadBuffer(Self.Memory^, Self.Size);
finally
AFile.Free;
end;
except
Exit(False);
end;
try
if (Self.ReadByte <> $FF) then Exit(False);
if (Self.ReadByte <> $D8) then Exit(False);
repeat
if Self.ReadByte = $FF then
begin
case Self.ReadByte of
$E1: // Exif Marker
begin
Break;
end;
$D9: // End Of Image (EOI)
begin
Exit(False);
end;
else begin // Unknown section, skip
P:= Self.ReadWordBE;
Self.Seek(Int64(P) - 2, soCurrent);
end;
end;
end;
until False;
// Exif data size
ASize:= Self.ReadWordBE;
// Exif magic string
Self.Read(Magic, SizeOf(Magic));
if (CompareByte(Magic, 'Exif'#0#0, SizeOf(Magic)) <> 0) then
Exit(False);
FOffset:= Self.Seek(0, soCurrent);
// Byte order
case Self.ReadWord of
$4949: FSwap:= {$IF DEFINED(ENDIAN_BIG)} True {$ELSE} False {$ENDIF}; // little-endian
$4D4D: FSwap:= {$IF DEFINED(ENDIAN_LITTLE)} True {$ELSE} False {$ENDIF}; // big-endian
else Exit(False);
end;
// Magic word
P:= Self.ReadWord;
if (P <> $002A) and (P <> $2A00) then Exit(False);
// Offset to first IFD
Offset:= Self.ReadDWord;
if FSwap then Offset:= SwapEndian(Offset);
// Go to Image file directory
Self.Seek(Offset - 8, soCurrent);
Result:= DoImageFileDirectory;
except
Reset;
Result:= False;
end;
end;
end.