unit fpeUtils; {$IFDEF FPC} {$mode ObjFPC}{$H+} //{$MODE DELPHI} {$ENDIF} {$I fpExif.inc} interface uses Classes, SysUtils, {$IFDEF FPC} fgl, {$ELSE} Windows, {$IFNDEF dExifNoJpeg}Graphics, jpeg,{$ENDIF} {$ENDIF} fpeGlobal; type {$IFDEF FPC} // {$IF FPC_FULLVERSION < 30002} TStringArray = array of string; // {$ENDIF} TInt64List = specialize TFPGList; {$ELSE} TInt64List = class(TList) private function GetItem(AIndex: Integer): Int64; procedure SetItem(AIndex: Integer; AValue: Int64); public destructor Destroy; override; function Add(AValue: Int64): Integer; procedure Clear; override; property Items[AIndex: Integer]: Int64 read GetItem write SetItem; default; end; TStringArray = array of string; {$ENDIF} // Big endian/little endian utilities function BEtoN(const AValue: WideString): WideString; overload; function LEtoN(const AValue: WideString): WideString; overload; function NtoBE(const AValue: WideString): WideString; overload; function NtoLE(const AValue: WideString): WideString; overload; {$IFNDEF FPC} function NtoBE(const AValue: Word): Word; overload; function NtoBE(const AValue: DWord): DWord; overload; function BEtoN(const AValue: Word): Word; overload; function BEtoN(const AValue: DWord): DWord; overload; function NtoLE(const AValue: Word): Word; overload; function NtoLE(const AValue: DWord): DWord; overload; function LEtoN(const AValue: Word): Word; overload; function LEtoN(const AValue: DWord): DWord; overload; {$ENDIF} // Delphi7 compatible stream access function ReadByte(AStream: TStream): Byte; function ReadWord(AStream: TStream): Word; function ReadDWord(AStream: TStream): DWord; procedure WriteByte(AStream: TStream; AData: Byte); procedure WriteWord(AStream: TStream; AData: Word); procedure WriteDWord(AStream: TStream; AData: DWord); // GPS utils { //function ExtractGPSPosition(const AValue: String; // out ADeg, AMin, ASec: Double): Boolean; } procedure SplitGps(AValue: Double; out ADegs, AMins, ASecs: Double); overload; procedure SplitGps(AValue: Double; out ADegs, AMins: Double); overload; function TryStrToGps(const AValue: String; out ADeg: Double): Boolean; { function GPSToStr(ACoord: Extended; ACoordType: TGpsCoordType; AGpsFormat: TGpsFormat = gf_DMS_Short; ADecs: Integer = 0): String; function StrToGPS(s: String): Extended; } // String utils function CountChar(AChar: Char; const AText: String): Integer; function FirstWord(const AText: String): String; function InsertSpaces(ACamelCaseText: String): String; function LettersOnly(const AText: String): String; function LookupValue(const AKey, ALookupTbl: String; ACompareFunc: TLookupCompareFunc): String; function LookupKey(const AValue, ALookupTbl: String; ACompareFunc: TLookupCompareFunc): String; function NumericOnly(const AText: String): String; function Split(AText: String; ASeparator: String = #9): TStringArray; {$IFNDEF FPC} {$IFNDEF UNICODE} function UTF8ToAnsi(const S: Ansistring): string; {$ENDIF} {$ENDIF} // Math utils function FloatToRational(Value, Precision: Double): TExifRational; function TryStrToRational(const AStr: String; out AValue: TExifRational): Boolean; function StrToRational(const AStr: String): TExifRational; //function GCD(a, b: integer): integer; // Image utils function JPEGImageSize(AStream: TStream; out AWidth, AHeight: Integer): Boolean; procedure JPEGScaleImage(ASrcStream, ADestStream: TStream; ADestSize: Integer = DEFAULT_THUMBNAIL_SIZE); // Buffer utils function PosInBytes(AText: ansistring; ABuffer: TBytes): Integer; // Date/time utils function LocalTimeZoneStr: String; function IPTCDateStrToDate(AValue: String): TDateTime; function IPTCTimeStrToTime(AValue: String): TDateTime; { For silencing the compiler... } procedure Unused(const A1); overload; procedure Unused(const A1, A2); overload; procedure Unused(const A1, A2, A3); overload; implementation uses {$IFDEF FPC} fpreadjpeg, fpwritejpeg, fpimage, fpcanvas, fpimgcanv, {$ELSE} // EncdDecd, {$ENDIF} Math, DateUtils, fpeStrConsts; {$IFNDEF FPC} //------------------------------------------------------------------------------ // Helper class: TInt64List - a list for 64-bit integers //------------------------------------------------------------------------------ type TInt64 = record Value: Int64; end; PInt64 = ^TInt64; destructor TInt64List.Destroy; begin Clear; inherited; end; procedure TInt64List.Clear; var i: Integer; P: PInt64; begin for i:=0 to Count-1 do begin P := inherited Items[i]; Dispose(P); end; inherited Clear; end; function TInt64List.Add(AValue: Int64): Integer; var P: PInt64; begin New(P); P^.Value := AValue; Result := inherited Add(P); end; function TInt64List.GetItem(AIndex: Integer): Int64; begin Result := PInt64(inherited Items[AIndex])^.Value; end; procedure TInt64List.SetItem(AIndex: Integer; AValue: Int64); var p: PInt64; begin p := inherited Items[AIndex]; p^.Value := AValue; end; {$IFNDEF UNICODE} function UTF8ToWideString(const S: AnsiString): WideString; var BufSize: Integer; begin Result := ''; if Length(S) = 0 then Exit; BufSize := MultiByteToWideChar(CP_UTF8, 0, PAnsiChar(S), Length(S), nil, 0); SetLength(result, BufSize); MultiByteToWideChar(CP_UTF8, 0, PANsiChar(S), Length(S), PWideChar(Result), BufSize); end; function UTF8ToAnsi(const S: Ansistring): string; begin Result := UTF8ToWideString(S); end; {$ENDIF} function SwapEndian(const AValue: Word): Word; overload; begin Result := Word((AValue shr 8) or (AValue shl 8)); end; function SwapEndian(const AValue: DWord): DWord; overload; begin Result := ((AValue shl 8) and $FF00FF00) or ((AValue shr 8) and $00FF00FF); Result := (Result shl 16) or (Result shr 16); end; function BEtoN(const AValue: Word): Word; begin {$IFDEF ENDIAN_BIG} Result := AValue; {$ELSE} Result := SwapEndian(AValue); {$ENDIF} end; function BEtoN(const AValue: DWord): DWord; begin {$IFDEF ENDIAN_BIG} Result := AValue; {$ELSE} Result := SwapEndian(AValue); {$ENDIF} end; function NtoBE(const AValue: Word): Word; begin {$IFDEF ENDIAN_BIG} Result := AValue; {$ELSE} Result := SwapEndian(AValue); {$ENDIF} end; function NtoBE(const AValue: DWord): DWord; begin {$IFDEF ENDIAN_BIG} Result := AValue; {$ELSE} Result := SwapEndian(AValue); {$ENDIF} end; function LEtoN(const AValue: Word): Word; begin {$IFDEF ENDIAN_BIG} Result := SwapEndian(AValue); {$ELSE} Result := AValue; {$ENDIF} end; function LEtoN(const AValue: DWord): DWord; begin {$IFDEF ENDIAN_BIG} Result := SwapEndian(AValue); {$ELSE} Result := AValue; {$ENDIF} end; function NtoLE(const AValue: Word): Word; begin {$IFDEF ENDIAN_BIG} Result := SwapEndian(AValue); {$ELSE} Result := AValue; {$ENDIF} end; function NtoLE(const AValue: DWord): DWord; begin {$IFDEF ENDIAN_BIG} Result := SwapEndian(AValue); {$ELSE} Result := AValue; {$ENDIF} end; {$ENDIF} function BEtoN(const AValue: WideString): WideString; {$IFNDEF ENDIAN_BIG} var i: Integer; {$ENDIF} begin {$IFDEF ENDIAN_BIG} Result := AValue; {$ELSE} SetLength(Result, Length(AValue)); for i:=1 to Length(AValue) do Result[i] := WideChar(BEToN(PDWord(@AValue[i])^)); {$ENDIF} end; function LEtoN(const AValue: WideString): WideString; {$IFDEF ENDIAN_BIG} var i: Integer; {$ENDIF} begin {$IFDEF ENDIAN_BIG} SetLength(Result, Length(AValue)); for i:=1 to Length(AValue) do Result[i] := WideChar(LEToN(PDWord(@AValue[i])^)); {$ELSE} Result := AValue; {$ENDIF} end; function NtoBE(const AValue: WideString): WideString; var i: Integer; begin {$IFDEF ENDIAN_BIG} Result := AValue; {$ELSE} SetLength(Result, Length(AValue)); for i:=1 to Length(AValue) do Result[i] := WideChar(NtoBE(PDWord(@AValue[i])^)); {$ENDIF} end; function NtoLE(const AValue: WideString): WideString; {$IFDEF ENDIAN_BIG} var i: Integer; {$ENDIF} begin {$IFDEF ENDIAN_BIG} SetLength(Result, Length(AValue)); for i:=1 to Length(AValue) do Result[i] := WideChar(NtoLE(PDWord(@AValue[i])^)); {$ELSE} Result := AValue; {$ENDIF} end; { A simple Delphi-7 compatible way of reading a byte from a stream } function ReadByte(AStream: TStream): Byte; begin AStream.Read(Result{%H-}, 1); end; { A simple Delphi-7 compatible way of reading two bytes from a stream } function ReadWord(AStream: TStream): Word; begin AStream.Read(Result{%H-}, 2); end; { A simple Delphi-7 compatible way of reading four bytes from a stream } function ReadDWord(AStream: TStream): DWord; begin AStream.Read(Result{%H-}, 4); end; { A simple Delphi-7 compatible way of writing a byte to a stream } procedure WriteByte(AStream: TStream; AData: Byte); begin AStream.Write(AData, 1); end; { A simple Delphi-7 compatible way of writing two bytex to a stream } procedure WriteWord(AStream: TStream; AData: Word); begin AStream.Write(AData, 2); end; { A simple Delphi-7 compatible way of writing four bytes to a stream } procedure WriteDWord(AStream: TStream; AData: DWord); begin AStream.Write(AData, 4); end; //============================================================================== // GPS Utilities //============================================================================== (* function ExtractGPSPosition(const AValue: String; out ADeg, AMin, ASec: Double): Boolean; const NUMERIC_CHARS = ['0'..'9', '.', ',']; //, '-', '+']; var p, p0: PChar; n: Integer; s: String; res: Integer; begin Result := false; ADeg := NaN; AMin := NaN; ASec := NaN; if AValue = '' then exit; // skip leading non-numeric characters p := @AValue[1]; while (p <> nil) and not (p^ in NUMERIC_CHARS) do inc(p); // extract first value: degrees p0 := p; n := 0; while (p <> nil) and (p^ in NUMERIC_CHARS) do begin if p^ = ',' then p^ := '.'; inc(p); inc(n); end; SetLength(s, n); Move(p0^, s[1], n*SizeOf(Char)); val(s, ADeg, res); if res <> 0 then exit; // skip non-numeric characters between degrees and minutes while (p <> nil) and not (p^ in NUMERIC_CHARS) do inc(p); // extract second value: minutes p0 := p; n := 0; while (p <> nil) and (p^ in NUMERIC_CHARS) do begin if p^ = ',' then p^ := '.'; inc(p); inc(n); end; SetLength(s, n); Move(p0^, s[1], n*SizeOf(Char)); val(s, AMin, res); if res <> 0 then exit; // skip non-numeric characters between minutes and seconds while (p <> nil) and not (p^ in NUMERIC_CHARS) do inc(p); // extract third value: seconds p0 := p; n := 0; while (p <> nil) and (p^ in NUMERIC_CHARS) do begin if p^ = ',' then p^ := '.'; inc(p); inc(n); end; SetLength(s, n); Move(p0^, s[1], n*SizeOf(Char)); val(s, ASec, res); if res <> 0 then exit; Result := (AMin >= 0) and (AMin < 60) and (ASec >= 0) and (ASec < 60); end; *) procedure SplitGps(AValue: Double; out ADegs, AMins, ASecs: Double); begin SplitGps(AValue, ADegs, AMins); ASecs := frac(AMins) * 60; AMins := trunc(AMins); end; procedure SplitGps(AValue: Double; out ADegs, AMins: Double); begin AValue := abs(AValue); AMins := frac(AValue) * 60; ADegs := trunc(AValue); end; { Combines up to three parts a GPS coordinate string (degrees, minutes, seconds) to a floating-point degree value. The parts are separated by non-numeric characters: three parts ---> d m s ---> d and m must be integer, s can be float two parts ---> d m ---> d must be integer, s can be float one part ---> d ---> d can be float Each part can exhibit a unit identifier, such as °, ', or ". BUT: they are ignored. This means that an input string 50°30" results in the output value 50.5 although the second part is marked as seconds, not minutes! } function TryStrToGps(const AValue: String; out ADeg: Double): Boolean; const NUMERIC_CHARS = ['0'..'9', '.', ',', '-', '+']; var mins, secs: Double; i, j, len: Integer; n: Integer; s: String; res: Integer; begin Result := false; ADeg := NaN; mins := 0; secs := 0; if AValue = '' then exit; // skip leading non-numeric characters len := Length(AValue); i := 1; while (i <= len) and not (AValue[i] in NUMERIC_CHARS) do inc(i); // extract first value: degrees SetLength(s, len); j := 1; n := 0; while (i <= len) and (AValue[i] in NUMERIC_CHARS) do begin if AValue[i] = ',' then s[j] := '.' else s[j] := AValue[i]; inc(i); inc(j); inc(n); end; if n > 0 then begin SetLength(s, n); val(s, ADeg, res); if res <> 0 then exit; end; // skip non-numeric characters between degrees and minutes while (i <= len) and not (AValue[i] in NUMERIC_CHARS) do inc(i); // extract second value: minutes SetLength(s, len); j := 1; n := 0; while (i <= len) and (AValue[i] in NUMERIC_CHARS) do begin if AValue[i] = ',' then s[j] := '.' else s[j] := AValue[i]; inc(i); inc(j); inc(n); end; if n > 0 then begin SetLength(s, n); val(s, mins, res); if (res <> 0) or (mins < 0) then exit; end; // skip non-numeric characters between minutes and seconds while (i <= len) and not (AValue[i] in NUMERIC_CHARS) do inc(i); // extract third value: seconds SetLength(s, len); j := 1; n := 0; while (i <= len) and (AValue[i] in NUMERIC_CHARS) do begin if AValue[i] = ',' then s[j] := '.' else s[j] := AValue[i]; inc(i); inc(j); inc(n); end; if n > 0 then begin SetLength(s, n); val(s, secs, res); if (res <> 0) or (secs < 0) then exit; end; // If the string contains seconds then minutes and deegrees must be integers if (secs <> 0) and ((frac(ADeg) > 0) or (frac(mins) > 0)) then exit; // If the string does not contain seconds then degrees must be integer. if (secs = 0) and (mins <> 0) and (frac(ADeg) > 0) then exit; // If the string contains minutes, but no seconds, then the degrees must be integer. Result := (mins >= 0) and (mins < 60) and (secs >= 0) and (secs < 60); // A similar check should be made for the degrees range, but since this is // different for latitude and longitude the check is skipped here. if Result then ADeg := abs(ADeg) + mins / 60 + secs / 3600; end; (* { Converts a GPS coordinate (extended data type) to a string } function GPSToStr(ACoord: Extended; ACoordType: TGpsCoordType; AGpsFormat: TGpsFormat = gf_DMS_Short; ADecs: Integer = 0): String; const {$IFDEF FPC} DEG_SYMBOL: string = '°'; {$ELSE} DEG_SYMBOL: ansistring = #176; // Delphi 7 wants the degree symbol in ANSI, newer versions will convert // it to a widechar automatically. {$ENDIF} RefStr: array[TGpsCoordType] of String[2] = ('NS', 'EW'); var idegs, imins: Integer; floatval: Extended; sgn: String; begin if IsNaN(ACoord) then begin Result := ''; exit; end; sgn := RefStr[ACoordType][1 + ord(ACoord < 0)]; ACoord := abs(ACoord); case AGpsFormat of gf_DD, gf_DD_Short : case AGpsFormat of gf_DD: Result := Format('%.*f degrees', [ADecs, ACoord], fpExifFmtSettings); gf_DD_Short: Result := Format('%.*f%s', [ADecs, ACoord, DEG_SYMBOL], fpExifFmtSettings); end; gf_DM, gf_DM_Short: begin idegs := trunc(ACoord); floatVal := frac(ACoord) * 60; case AGpsFormat of gf_DM: Result := Format('%d degrees %.*f minutes', [idegs, ADecs, floatVal], fpExifFmtSettings); gf_DM_Short: Result := Format('%d%s %.*f''', [idegs, DEG_SYMBOL, ADecs, floatVal], fpExifFmtSettings); end; end; gf_DMS, gf_DMS_Short: begin idegs := trunc(ACoord); imins := trunc(frac(ACoord)*60); floatVal := frac(frac(ACoord)*60)*60; // seconds case AGpsFormat of gf_DMS: Result := Format('%d degrees %d minutes %.*f seconds', [idegs, imins, ADecs, floatVal], fpExifFmtSettings); gf_DMS_Short: Result := Format('%d%s %d'' %.*f"', [idegs, DEG_SYMBOL, imins, ADecs, floatVal], fpExifFmtSettings); end; end; end; Result := Result + ' ' + sgn; end; { Converts a string to a GPS extended number. The input string s must be formatted as dd° mm' ss[.zzz]" E|W. Decimal places of seconds are optional. Instead of seconds, the string can also contain a fractional part for minutes, e.g. dd° m.mmmmmm', or for degress: d.ddddd° E|W means: either E or W. } function StrToGPS(s: String): Extended; var ds, ms, ss: String; i: Integer; tmp: String; degs, mins, secs: Extended; res: Integer; scannedPart: Integer; // 0=degrees, 1=minutes, 2=seconds isFloat: Array[-1..2] of Boolean; sgn: Integer; begin if s = '' then begin Result := NaN; exit; end; i := 1; tmp := ''; scannedPart := 0; isFloat[0] := false; isFloat[1] := false; isFloat[2] := false; degs := 0; mins := 0; secs := 0; sgn := +1; while i <= Length(s) do begin case s[i] of '0'..'9': tmp := tmp + s[i]; '.', ',': begin tmp := tmp + '.'; isFloat[scannedPart] := true; end; ' ': if scannedPart = 0 then begin // in degrees par val(tmp, degs, res); if res > 0 then raise EFpExif.Create('No numeric data in gps coordinate.'); tmp := ''; scannedPart := 1; end; '''': if not isFloat[0] then begin // ignore minutes and seconds if degrees are floats val(tmp, mins, res); if res > 0 then raise EFpExif.Create('No numeric data in gps coordinate.'); tmp := ''; scannedPart := 2; end; '"': // ignore seconds of degrees or minutes are floating point values if not (isFloat[0] or isFloat[1]) then begin val(tmp, secs, res); if res > 0 then raise EFpExif.Create('No numerical data in gps coordinate.'); tmp := ''; scannedPart := -1; end; 'W', 'w', 'S', 's': sgn := -1; end; inc(i); end; Result := (degs + mins/60 + secs/3600) * sgn; end; *) //============================================================================== // Image file utilities //============================================================================== { Extracts the width and height of a JPEG image from its data without loading it into a TJpegImage. Returns false if the stream does not contain a jpeg image. } function JPEGImageSize(AStream: TStream; out AWidth, AHeight: Integer): Boolean; type TJPGHeader = array[0..1] of Byte; //FFD8 = StartOfImage (SOI) TJPGRecord = packed record Marker: Byte; RecType: Byte; RecSize: Word; end; var n: integer; hdr: TJPGHeader; rec: TJPGRecord; p: Int64; savedPos: Int64; begin Result := false; AWidth := 0; AHeight := 0; savedPos := AStream.Position; try // Check for SOI (start of image) record n := AStream.Read(hdr{%H-}, SizeOf(hdr)); if (n < SizeOf(hdr)) or (hdr[0] <> $FF) or (hdr[1] <> $D8) then exit; rec.Marker := $FF; while (AStream.Position < AStream.Size) and (rec.Marker = $FF) do begin if AStream.Read(rec, SizeOf(rec)) < SizeOf(rec) then exit; rec.RecSize := BEToN(rec.RecSize); p := AStream.Position - 2; case rec.RecType of $C0..$C3: if (rec.RecSize >= 4) then // Start of frame markers begin AStream.Seek(1, soFromCurrent); // Skip "bits per sample" AHeight := BEToN(ReadWord(AStream)); AWidth := BEToN(ReadWord(AStream)); Result := true; exit; end; $D9: // end of image; break; end; AStream.Position := p + rec.RecSize; end; finally AStream.Position := savedPos; end; end; procedure JPEGScaleImage(ASrcStream, ADestStream: TStream; ADestSize: Integer = DEFAULT_THUMBNAIL_SIZE); {$IFDEF FPC} var srcImage, destImage: TFPCustomImage; destCanvas: TFPImageCanvas; reader: TFPCustomImageReader; writer: TFPCustomImageWriter; w, h: Integer; f: Double; begin srcImage := TFPMemoryImage.Create(10, 10); reader := TFPReaderJPEG.Create; srcImage.LoadFromStream(ASrcStream, reader); reader.Free; w := srcImage.Width; h := srcImage.Height; if w > h then f := ADestSize / w else f := ADestSize / h; destImage := TFPMemoryImage.Create(round(w*f), round(h*f)); destCanvas := TFPImageCanvas.Create(destImage); destCanvas.StretchDraw(0, 0, destImage.Width, destImage.Height, srcImage); writer := TFPWriterJPEG.Create; destImage.SaveToStream(ADestStream, writer); writer.Free; end; {$ELSE} {$IFNDEF dExifNoJpeg} var jpeg: TJPegImage; bmp: TBitmap; w, h: Integer; f: Double; begin jpeg := TJpegImage.Create; try jpeg.LoadfromStream(ASrcStream); w := jpeg.Width; h := jpeg.Height; if w > h then f := ADestSize / w else f := ADestSize / h; bmp := TBitmap.Create; bmp.PixelFormat := pf24bit; bmp.Width := round(w * f); bmp.Height := round(h * f); bmp.Canvas.StretchDraw(Rect(0, 0, bmp.Width, bmp.Height), jpeg); jpeg.Free; jpeg := TJpegImage.Create; jpeg.Assign(bmp); jpeg.SaveToStream(ADestStream); finally jpeg.Free; bmp.Free; end; end; {$ELSE} begin // CreateThumb will not work in delphi if dExifNoJpeg is defined. end; {$ENDIF} {$ENDIF} { Formatting callbacks } (* Function GpsPosn(InStr: String): String; const {$IFDEF FPC} DEGREES: string = '°'; {$ELSE} DEGREES: ansistring = #176; {$ENDIF} var p, sl: integer; s: string; gDegree, gMin, gSec: double; begin sl := length(fpExifDataSep); Result := instr; // if error return input string p := Pos(fpExifDataSep, instr); s := copy(InStr, 1, p-1); // get first irrational number gDegree := CvtRational(s); // degrees InStr := copy(InStr, p+sl, 64); p := Pos(fpExifDataSep, instr); s := copy(InStr, 1, p-1); // get second irrational number gMin := CvtRational(s); // minutes InStr := copy(InStr, p+sl, 64); gSec := CvtRational(InStr); // seconds if gSec = 0 then // camera encoded as decimal minutes begin gSec := ((gMin - trunc(gMin))*100); // seconds as a fraction of degrees gSec := gSec * 0.6; // convert to seconds gMin := trunc(gMin); // minutes is whole portion end; // Ok we'll send the result back as Degrees with // Decimal Minutes. Alternatively send back as Degree // Minutes, Seconds or Decimal Degrees. case GpsFormat of gf_DD: Result := Format('%1.4f Decimal Degrees', [gDegree + (gMin + gSec/60)/60], fpExifFmtSettings); gf_DD_Short: Result := Format('%1.4f%s', [gDegree + (gmin + gSec/60)/60, DEGREES], fpExifFmtSettings); gf_DM: Result := Format('%0.0f Degrees %1.2f Minutes',[gDegree, gMin + gsec/60], fpExifFmtSettings); gf_DM_Short: Result := Format('%0.0f%s %1.2f''', [gDegree, DEGREES, gMin + gsec/60], fpExifFmtSettings); gf_DMS: Result := Format('%0.0f Degrees %0.0f Minutes %0.2f Seconds', [gDegree, gMin, gSec], fpExifFmtSettings); gf_DMS_Short: Result := Format('%0.0f%s %0.0f'' %0.2f"', [gDegree, DEGREES, gMin, gSec], fpExifFmtSettings); end; end; function GpsAltitude(InStr: string): String; var gAltitude: double; begin Result := InStr; // if error return input string gAltitude := CvtRational(InStr); // meters/multiplier, e.g.. 110/10 Result := Format('%1.2f m', [gAltitude]); end; *) { function GpsVersionID(AText: String): String; var i: Integer; sep: Char; begin Result := ''; sep := ','; for i:=1 to Length(fpExifDataSep) do if fpExifDataSep[i] <> ' ' then begin sep := char(fpExifDataSep[i]); break; end; for i:=1 to Length(AText) do begin if AText[i] = sep then Result := Result + '.' else if AText[i] <> ' ' then Result := Result + AText[i]; end; end; function CompCfgCallback(AText: String): String; var i, ti: Integer; begin Result := ''; for i := 1 to 4 do if i <= Length(AText) then begin ti := integer(AText[i]); case ti of // 0: Result := Result + '-'; 1: Result := Result + 'Y'; 2: Result := Result + 'Cb'; 3: Result := Result + 'Cr'; 4: Result := Result + 'R'; 5: Result := Result + 'G'; 6: Result := Result + 'B'; end; end; end; } //============================================================================== // String utilities //============================================================================== { Counts how often the specified character is contained within a string } function CountChar(AChar: Char; const AText: String): Integer; var i: Integer; begin Result := 0; for i:=1 to Length(AText) do if (AChar = AText[i]) then inc(Result); end; function FirstWord(const AText: String): String; var i: Integer; begin Result := ''; for i:=1 to Length(AText) do if AText[i] in ['a'..'z', 'A'..'Z', '0'..'9'] then Result := Result + AText[i] else exit; end; { Inserts spaces into a camel-case text, i.e. 'ShutterSpeed' --> 'Shutter Speed'} function InsertSpaces(ACamelCaseText: String): String; function IsUpper(ch: char): boolean; begin Result := ((ch >= 'A') and (ch <= 'Z')) or (ch = #0) or (ch = '/'); end; var i: integer; len: Integer; ch, nextch, prevch: char; s: String; begin len := Length(ACamelCaseText); if len < 3 then begin Result := ACamelCaseText; exit; end; s := ACamelCaseText[1]; prevch := ACamelCaseText[1]; for i := 2 to len do begin ch := ACamelCaseText[i]; if i < len then nextch := ACamelCaseText[i+1] else nextch := #0; if IsUpper(ch) and (not IsUpper(prevch) or not IsUpper(nextch)) and (ch <> ' ') and (prevch <> ' ') and (nextch <> ' ') then s := s + ' ' + ch else s := s + ch; prevch := ch; end; Result := s; end; { Removes all non-alpha characters ('a'..'z', 'A'..'Z') from a string } function LettersOnly(const AText: String): string; var i: Integer; begin Result := ''; for i:=1 to Length(AText) do if AText[i] in ['a'..'z', 'A'..'Z'] then Result := Result + AText[i]; end; //============================================================================== // Lookup //============================================================================== type TLookupMode = (lmKey, lmValue); function LookupHelper(const ASearchStr, ALookupTbl: String; ACompareFunc: TLookupCompareFunc; AMode: TLookupMode; out AResultStr: String): Boolean; var i: Integer; key, val: String; inKey: Boolean; begin Result := false; if ALookupTbl = '' then exit; key := ''; inKey := true; for i:=1 to Length(ALookupTbl) do begin if ALookupTbl[i] = fpExifLookupKeySep then begin inKey := false; val := ''; end else if (ALookupTbl[i] = fpExifLookupSep) then begin case AMode of lmKey: if ACompareFunc(key, ASearchStr) then begin Result := true; AResultStr := val; exit; end; lmValue: if ACompareFunc(val, ASearchStr) then begin Result := true; AResultStr := key; exit; end; end; inKey := true; key := ''; end else if inKey then key := key + ALookupTbl[i] else val := val + ALookupTbl[i]; end; case AMode of lmKey: if ACompareFunc(key, ASearchStr) then begin Result := true; AResultStr := val; end; lmValue: if ACompareFunc(val, ASearchStr) then begin Result := true; AResultStr := key; end; end; end; function LookupValue(const AKey, ALookupTbl: String; ACompareFunc: TLookupCompareFunc): String; var found: Boolean; begin found := LookupHelper(AKey, ALookupTbl, ACompareFunc, lmKey, Result); if not found then Result := AKey; end; function LookupKey(const AValue, ALookupTbl: String; ACompareFunc: TLookupCompareFunc): String; var found: Boolean; begin found := LookupHelper(AValue, ALookupTbl, ACompareFunc, lmValue, Result); if not found then Result := ''; end; function NumericOnly(const AText: String): String; var i: Integer; begin Result := ''; for i:=1 to Length(AText) do if AText[i] in ['0'..'9'] then Result := Result + AText[i]; end; function Split(AText: String; ASeparator: String = #9): TStringArray; const BLOCK_SIZE = 20; var i, j, k, n, len: Integer; s: String; found: Boolean; begin Assert(ASeparator <> ''); if AText = '' then begin SetLength(Result, 0); exit; end; // AText := AText + ASeparator; len := Length(AText); SetLength(Result, BLOCK_SIZE); i := 1; n := 0; s := ''; while (i <= len) do begin if AText[i] = ASeparator[1] then begin j := i; k := 1; found := true; while (i <= len) and (k <= Length(ASeparator)) do begin if ASeparator[k] <> AText[i] then begin found := false; break; end; inc(k); inc(i); end; if found then begin Result[n] := s; inc(n); if n mod BLOCK_SIZE = 0 then SetLength(Result, Length(Result) + BLOCK_SIZE); s := ''; Continue; end else i := j; end else s := s + AText[i]; inc(i); end; (* if (AText[i] = ASeparator) or (i = len) then begin Result[n] := Copy(AText, j, i-j); inc(n); if n mod BLOCK_SIZE = 0 then SetLength(Result, Length(Result) + BLOCK_SIZE); j := i+1; end; inc(i); end; *) Result[n] := s; inc(n); SetLength(Result, n); end; //============================================================================== // Float to fraction converstion // // These routines are adapted from unit Fractions by Bart Boersma // https://sourceforge.net/p/lazarus-ccr/svn/HEAD/tree/components/fractions/ //============================================================================== const MaxInt32 = High(Int32); MinInt32 = Low(Int32); function InRange32(Value: Double): Boolean; {$IFDEF FPC}inline;{$ENDIF} begin Result := not ((Value > MaxInt32) or (Value < MinInt32)); end; procedure CheckRange(Value: Double); begin if not InRange32(Value) then raise ERangeError.Create(rsRangeCheckError); end; procedure AdjustPrecision(var Precision: Double; Value: Double); const MaxPrec: Double = 1.0 / MaxInt32; begin Precision := Abs(Precision); if ((Abs(Value) / Precision) > 1E15) then Precision := Abs(Value) / 1E16; if (Precision < MaxPrec) then Precision := MaxPrec; end; function IsBorderlineValue(Value: Double; out F: TExifRational): Boolean; const MaxPrec: Double = 1.0 / MaxInt32; ZeroBoundary: Double = 0.5 / MaxInt32; begin if (Abs(Value) <= MaxPrec) then begin Result := True; if (Abs(Value) < ZeroBoundary) then begin F.Numerator := 0; F.Denominator := 1; end else begin if (Value < 0) then F.Numerator := -1 else F.Numerator := 1; F.Denominator := MaxInt32; end; end else Result := False; end; // Uses method of continued fractions function FloatToRational(Value, Precision: Double): TExifRational; var H1, H2, K1, K2, A, NewA, tmp: Int32; B, diff, test: Double; PendingOverFlow, Found: Boolean; begin if IsNaN(Value) then begin Result.Numerator := 1; Result.Denominator := 0; exit; end; CheckRange(Value); AdjustPrecision(Precision, Value); //Borderline cases if IsBorderlineValue(Value, Result) then Exit; H1 := 1; H2 := 0; K1 := 0; K2 := 1; b := Value; NewA := Round(Floor(b)); repeat A := NewA; tmp := H1; H1 := (a * H1) + H2; H2 := tmp; tmp := K1; K1 := (a * K1) + K2; K2 := tmp; test := H1 / K1; diff := Abs(test - Value); Found := (diff < Precision); if not Found then begin if (Abs(B-A) < 1E-30) then B := 1E30 //happens when H1/K2 exactly matches Value else B := 1 / (B - A); PendingOverFlow := (((Double(B) * H1) + H2) > MaxInt32) or (((Double(B) * K1) + K2) > MaxInt32) or (B > MaxInt32); if not PendingOverFlow then NewA := Round(Floor(B)); end; until Found or PendingOverFlow; Result.Numerator := H1; Result.Denominator := K1; end; function TryStrToRational(const AStr: String; out AValue: TExifRational): Boolean; var p: Integer; snum, sdenom: String; begin Result := false; if AStr = '' then exit; p := pos('/', AStr); if p = 0 then begin snum := AStr; sdenom := '1'; end else begin snum := trim(Copy(AStr, 1, p-1)); sdenom := trim(Copy(AStr, p+1, MaxInt)); end; if (snum = '') or (sdenom = '') then exit; Result := TryStrToInt(snum, AValue.Numerator) and TryStrToInt(sdenom, AValue.Denominator); end; function StrToRational(const AStr: String): TExifRational; begin if not TryStrToRational(AStr, Result) then begin Result.Numerator := 1; Result.Denominator := 0; end; end; function GCD(a, b: integer): integer; begin if (a = 0) then Result := abs(b) else if (b = 0) then Result := abs(a) else if (b mod a) = 0 then Result := a else Result := GCD(b, a mod b); end; //============================================================================== // Buffer utilities //============================================================================== function PosInBytes(AText: AnsiString; ABuffer: TBytes): Integer; var i, j: Integer; found: Boolean; begin if (AText = '') or (ABuffer = nil) then begin Result := -1; exit; end; for i:= 0 to High(ABuffer) do if ABuffer[i] = ord(AText[1]) then begin found := true; for j := 2 to Length(AText) do if ABuffer[i+j-1] <> ord(AText[j]) then begin found := false; break; end; if found then begin Result := i; exit; end; end; Result := -1; end; //============================================================================== // Date/time utilities //============================================================================== {$IFNDEF FPC} function GetLocalTimeOffset: LongInt; var TZoneInfo: TTimeZoneInformation; begin GetTimeZoneInformation(TZoneInfo); Result := TZoneInfo.Bias; end; {$ENDIF} function LocalTimeZoneStr: string; var bias: Integer; h, m: Integer; begin bias := GetLocalTimeOffset; if bias >= 0 then Result := '+' else Result := '-'; bias := Abs(bias); h := bias div 60; m := bias mod 60; Result := Result + Format('%.2d%.2d', [h, m]); end; function IPTCDateStrToDate(AValue: String): TDateTime; var yr, mon, day: Integer; begin Result := 0; if (Length(AValue) >= 8) and TryStrToInt(Copy(AValue, 1, 4), yr) and TryStrToInt(Copy(AValue, 5, 2), mon) and (mon >= 1) and (mon <= 12) and TryStrToInt(Copy(AValue, 7, 2), day) and (day >= 1) and (day <= DaysInAMonth(yr, mon)) then Result := EncodeDate(yr, mon, day); end; function IPTCTimeStrToTime(AValue: String): TDateTime; var hr, mn, sc: Integer; begin Result := 0; if (Length(AValue) >= 6) and TryStrToInt(Copy(AValue, 1, 2), hr) and (hr >= 0) and (hr < 24) and TryStrToInt(Copy(AValue, 3, 2), mn) and (mn >= 0) and (mn < 60) and TryStrToInt(Copy(AValue, 5, 2), sc) and (sc >= 0) and (sc < 60) then Result := EncodeTime(hr, mn, sc, 0) end; //============================================================================== // Silence compiler warnings due to unused parameters // (code adapted from TAChart) //============================================================================== {$IFDEF FPC} {$PUSH}{$HINTS OFF} {$ENDIF} procedure Unused(const A1); begin end; procedure Unused(const A1, A2); begin end; procedure Unused(const A1, A2, A3); begin end; {$IFDEF FPC} {$POP} {$ENDIF} end.