481 lines
13 KiB
Plaintext
481 lines
13 KiB
Plaintext
{
|
|
Vampyre Imaging Library
|
|
by Marek Mauder
|
|
https://github.com/galfar/imaginglib
|
|
https://imaginglib.sourceforge.io
|
|
- - - - -
|
|
This Source Code Form is subject to the terms of the Mozilla Public
|
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
file, You can obtain one at https://mozilla.org/MPL/2.0.
|
|
}
|
|
|
|
{ This unit contains image format loader/saver for Radiance HDR/RGBE images.}
|
|
unit ImagingRadiance;
|
|
|
|
{$I ImagingOptions.inc}
|
|
|
|
interface
|
|
|
|
uses
|
|
SysUtils, Classes, Imaging, ImagingTypes, ImagingUtility;
|
|
|
|
type
|
|
{ Radiance is a suite of tools for performing lighting simulation. It's
|
|
development started in 1985 and it pioneered the concept of
|
|
high dynamic range imaging. Radiance defined an image format for storing
|
|
HDR images, now described as RGBE image format. Since it was the first
|
|
HDR image format, this format is supported by many other software packages.
|
|
|
|
Radiance image file consists of three sections: a header, resolution string,
|
|
followed by the pixel data. Each pixel is stored as 4 bytes, one byte
|
|
mantissa for each r, g, b and a shared one byte exponent.
|
|
The pixel data may be stored uncompressed or using run length encoding.
|
|
|
|
Imaging translates RGBE pixels to original float values and stores them
|
|
in ifR32G32B32F data format. It can read both compressed and uncompressed
|
|
files, and saves files as compressed.}
|
|
THdrFileFormat = class(TImageFileFormat)
|
|
protected
|
|
procedure Define; override;
|
|
function LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray;
|
|
OnlyFirstLevel: Boolean): Boolean; override;
|
|
function SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray;
|
|
Index: LongInt): Boolean; override;
|
|
procedure ConvertToSupported(var Image: TImageData;
|
|
const Info: TImageFormatInfo); override;
|
|
public
|
|
function TestFormat(Handle: TImagingHandle): Boolean; override;
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
Math, ImagingIO;
|
|
|
|
const
|
|
SHdrFormatName = 'Radiance HDR/RGBE';
|
|
SHdrMasks = '*.hdr';
|
|
HdrSupportedFormats: TImageFormats = [ifR32G32B32F];
|
|
|
|
type
|
|
TSignature = array[0..9] of AnsiChar;
|
|
THdrFormat = (hfRgb, hfXyz);
|
|
|
|
THdrHeader = record
|
|
Format: THdrFormat;
|
|
Width: Integer;
|
|
Height: Integer;
|
|
end;
|
|
|
|
TRgbe = packed record
|
|
R, G, B, E: Byte;
|
|
end;
|
|
TDynRgbeArray = array of TRgbe;
|
|
|
|
const
|
|
RadianceSignature: TSignature = '#?RADIANCE';
|
|
RgbeSignature: TSignature = '#?RGBE';
|
|
SFmtRgbeRle = '32-bit_rle_rgbe';
|
|
SFmtXyzeRle = '32-bit_rle_xyze';
|
|
|
|
resourcestring
|
|
SErrorBadHeader = 'Bad HDR/RGBE header format.';
|
|
SWrongScanLineWidth = 'Wrong scanline width.';
|
|
SXyzNotSupported = 'XYZ color space not supported.';
|
|
|
|
{ THdrFileFormat }
|
|
|
|
procedure THdrFileFormat.Define;
|
|
begin
|
|
inherited;
|
|
FName := SHdrFormatName;
|
|
FFeatures := [ffLoad, ffSave];
|
|
FSupportedFormats := HdrSupportedFormats;
|
|
|
|
AddMasks(SHdrMasks);
|
|
end;
|
|
|
|
function THdrFileFormat.LoadData(Handle: TImagingHandle;
|
|
var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean;
|
|
var
|
|
Header: THdrHeader;
|
|
IO: TIOFunctions;
|
|
|
|
function ReadHeader: Boolean;
|
|
const
|
|
CommentIds: TAnsiCharSet = ['#', '!'];
|
|
var
|
|
Line: AnsiString;
|
|
HasResolution: Boolean;
|
|
Count, Idx: Integer;
|
|
ValStr, NativeLine: string;
|
|
ValFloat: Double;
|
|
begin
|
|
Result := False;
|
|
HasResolution := False;
|
|
Count := 0;
|
|
|
|
repeat
|
|
if not ReadLine(IO, Handle, Line) then
|
|
Exit;
|
|
|
|
Inc(Count);
|
|
if Count > 16 then // Too long header for HDR
|
|
Exit;
|
|
|
|
if Length(Line) = 0 then
|
|
Continue;
|
|
if Line[1] in CommentIds then
|
|
Continue;
|
|
|
|
NativeLine := string(Line);
|
|
|
|
if StrMaskMatch(NativeLine, 'Format=*') then
|
|
begin
|
|
// Data format parsing
|
|
ValStr := Copy(NativeLine, 8, MaxInt);
|
|
if ValStr = SFmtRgbeRle then
|
|
Header.Format := hfRgb
|
|
else if ValStr = SFmtXyzeRle then
|
|
Header.Format := hfXyz
|
|
else
|
|
Exit;
|
|
end;
|
|
|
|
if StrMaskMatch(NativeLine, 'Gamma=*') then
|
|
begin
|
|
ValStr := Copy(NativeLine, 7, MaxInt);
|
|
if TryStrToFloat(ValStr, ValFloat, GetFormatSettingsForFloats) then
|
|
FMetadata.SetMetaItem(SMetaGamma, ValFloat);
|
|
end;
|
|
|
|
if StrMaskMatch(NativeLine, 'Exposure=*') then
|
|
begin
|
|
ValStr := Copy(NativeLine, 10, MaxInt);
|
|
if TryStrToFloat(ValStr, ValFloat, GetFormatSettingsForFloats) then
|
|
FMetadata.SetMetaItem(SMetaExposure, ValFloat);
|
|
end;
|
|
|
|
if StrMaskMatch(NativeLine, '?Y * ?X *') then
|
|
begin
|
|
Idx := Pos('X', NativeLine);
|
|
ValStr := SubString(NativeLine, 4, Idx - 2);
|
|
if not TryStrToInt(ValStr, Header.Height) then
|
|
Exit;
|
|
ValStr := Copy(NativeLine, Idx + 2, MaxInt);
|
|
if not TryStrToInt(ValStr, Header.Width) then
|
|
Exit;
|
|
|
|
if (NativeLine[1] = '-') then
|
|
Header.Height := -Header.Height;
|
|
if (NativeLine[Idx - 1] = '-') then
|
|
Header.Width := -Header.Width;
|
|
|
|
HasResolution := True;
|
|
end;
|
|
|
|
until HasResolution;
|
|
Result := True;
|
|
end;
|
|
|
|
procedure DecodeRgbe(const Src: TRgbe; Dest: PColor96FPRec); {$IFDEF USE_INLINE}inline;{$ENDIF}
|
|
var
|
|
Mult: Single;
|
|
begin
|
|
if Src.E > 0 then
|
|
begin
|
|
Mult := Math.Ldexp(1, Src.E - 128);
|
|
Dest.R := Src.R / 255 * Mult;
|
|
Dest.G := Src.G / 255 * Mult;
|
|
Dest.B := Src.B / 255 * Mult;
|
|
end
|
|
else
|
|
begin
|
|
Dest.R := 0;
|
|
Dest.G := 0;
|
|
Dest.B := 0;
|
|
end;
|
|
end;
|
|
|
|
procedure ReadCompressedLine(Width, Y: Integer; var DestBuffer: TDynRgbeArray);
|
|
var
|
|
Pos: Integer;
|
|
I, X, Count: Integer;
|
|
Code, Value: Byte;
|
|
LineBuff: TDynByteArray;
|
|
Rgbe: TRgbe;
|
|
Ptr: PByte;
|
|
begin
|
|
SetLength(LineBuff, Width);
|
|
IO.Read(Handle, @Rgbe, SizeOf(Rgbe));
|
|
|
|
if ((Rgbe.B shl 8) or Rgbe.E) <> Width then
|
|
RaiseImaging(SWrongScanLineWidth);
|
|
|
|
for I := 0 to 3 do
|
|
begin
|
|
Pos := 0;
|
|
while Pos < Width do
|
|
begin
|
|
IO.Read(Handle, @Code, SizeOf(Byte));
|
|
if Code > 128 then
|
|
begin
|
|
Count := Code - 128;
|
|
IO.Read(Handle, @Value, SizeOf(Byte));
|
|
FillMemoryByte(@LineBuff[Pos], Count, Value);
|
|
end
|
|
else
|
|
begin
|
|
Count := Code;
|
|
IO.Read(Handle, @LineBuff[Pos], Count * SizeOf(Byte));
|
|
end;
|
|
Inc(Pos, Count);
|
|
end;
|
|
|
|
Ptr := @PByteArray(@DestBuffer[0])[I];
|
|
for X := 0 to Width - 1 do
|
|
begin
|
|
Ptr^ := LineBuff[X];
|
|
Inc(Ptr, 4);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure ReadPixels(var Image: TImageData);
|
|
var
|
|
Y, X, SrcLineLen: Integer;
|
|
Dest: PColor96FPRec;
|
|
Compressed: Boolean;
|
|
Rgbe: TRgbe;
|
|
Buffer: TDynRgbeArray;
|
|
begin
|
|
Dest := Image.Bits;
|
|
Compressed := not ((Image.Width < 8) or (Image.Width > $7FFFF));
|
|
SrcLineLen := Image.Width * SizeOf(TRgbe);
|
|
|
|
IO.Read(Handle, @Rgbe, SizeOf(Rgbe));
|
|
IO.Seek(Handle, -SizeOf(Rgbe), smFromCurrent);
|
|
|
|
if (Rgbe.R <> 2) or (Rgbe.G <> 2) or ((Rgbe.B and 128) > 0) then
|
|
Compressed := False;
|
|
|
|
SetLength(Buffer, Image.Width);
|
|
|
|
for Y := 0 to Image.Height - 1 do
|
|
begin
|
|
if Compressed then
|
|
ReadCompressedLine(Image.Width, Y, Buffer)
|
|
else
|
|
IO.Read(Handle, @Buffer[0], SrcLineLen);
|
|
|
|
for X := 0 to Image.Width - 1 do
|
|
begin
|
|
DecodeRgbe(Buffer[X], Dest);
|
|
Inc(Dest);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
IO := GetIO;
|
|
SetLength(Images, 1);
|
|
|
|
// Read header, allocate new image and, then read and convert the pixels
|
|
if not ReadHeader then
|
|
RaiseImaging(SErrorBadHeader);
|
|
if (Header.Format = hfXyz) then
|
|
RaiseImaging(SXyzNotSupported);
|
|
|
|
NewImage(Abs(Header.Width), Abs(Header.Height), ifR32G32B32F, Images[0]);
|
|
ReadPixels(Images[0]);
|
|
|
|
// Flip/mirror the image as needed (height < 0 is default top-down)
|
|
if Header.Width < 0 then
|
|
MirrorImage(Images[0]);
|
|
if Header.Height > 0 then
|
|
FlipImage(Images[0]);
|
|
|
|
Result := True;
|
|
end;
|
|
|
|
function THdrFileFormat.SaveData(Handle: TImagingHandle;
|
|
const Images: TDynImageDataArray; Index: LongInt): Boolean;
|
|
const
|
|
LineEnd = #$0A;
|
|
SPrgComment = '#Made with Vampyre Imaging Library';
|
|
SSizeFmt = '-Y %d +X %d';
|
|
var
|
|
ImageToSave: TImageData;
|
|
MustBeFreed: Boolean;
|
|
IO: TIOFunctions;
|
|
|
|
procedure SaveHeader;
|
|
begin
|
|
WriteLine(IO, Handle, RadianceSignature, LineEnd);
|
|
WriteLine(IO, Handle, SPrgComment, LineEnd);
|
|
WriteLine(IO, Handle, 'FORMAT=' + SFmtRgbeRle, LineEnd + LineEnd);
|
|
WriteLine(IO, Handle, AnsiString(Format(SSizeFmt, [ImageToSave.Height, ImageToSave.Width])), LineEnd);
|
|
end;
|
|
|
|
procedure EncodeRgbe(const Src: TColor96FPRec; var DestR, DestG, DestB, DestE: Byte); {$IFDEF USE_INLINE}inline;{$ENDIF}
|
|
var
|
|
V, M: {$IFDEF FPC}Float{$ELSE}Extended{$ENDIF};
|
|
E: Integer;
|
|
begin
|
|
V := Src.R;
|
|
if (Src.G > V) then
|
|
V := Src.G;
|
|
if (Src.B > V) then
|
|
V := Src.B;
|
|
|
|
if V < 1e-32 then
|
|
begin
|
|
DestR := 0;
|
|
DestG := 0;
|
|
DestB := 0;
|
|
DestE := 0;
|
|
end
|
|
else
|
|
begin
|
|
Frexp(V, M, E);
|
|
V := M * 256.0 / V;
|
|
DestR := ClampToByte(Round(Src.R * V));
|
|
DestG := ClampToByte(Round(Src.G * V));
|
|
DestB := ClampToByte(Round(Src.B * V));
|
|
DestE := ClampToByte(E + 128);
|
|
end;
|
|
end;
|
|
|
|
procedure WriteRleLine(const Line: array of Byte; Width: Integer);
|
|
const
|
|
MinRunLength = 4;
|
|
var
|
|
Cur, BeginRun, RunCount, OldRunCount, NonRunCount: Integer;
|
|
Buf: array[0..1] of Byte;
|
|
begin
|
|
Cur := 0;
|
|
while Cur < Width do
|
|
begin
|
|
BeginRun := Cur;
|
|
RunCount := 0;
|
|
OldRunCount := 0;
|
|
while (RunCount < MinRunLength) and (BeginRun < Width) do
|
|
begin
|
|
Inc(BeginRun, RunCount);
|
|
OldRunCount := RunCount;
|
|
RunCount := 1;
|
|
while (BeginRun + RunCount < Width) and (RunCount < 127) and (Line[BeginRun] = Line[BeginRun + RunCount]) do
|
|
Inc(RunCount);
|
|
end;
|
|
if (OldRunCount > 1) and (OldRunCount = BeginRun - Cur) then
|
|
begin
|
|
Buf[0] := 128 + OldRunCount;
|
|
Buf[1] := Line[Cur];
|
|
IO.Write(Handle, @Buf, 2);
|
|
Cur := BeginRun;
|
|
end;
|
|
while Cur < BeginRun do
|
|
begin
|
|
NonRunCount := Min(128, BeginRun - Cur);
|
|
Buf[0] := NonRunCount;
|
|
IO.Write(Handle, @Buf, 1);
|
|
IO.Write(Handle, @Line[Cur], NonRunCount);
|
|
Inc(Cur, NonRunCount);
|
|
end;
|
|
if RunCount >= MinRunLength then
|
|
begin
|
|
Buf[0] := 128 + RunCount;
|
|
Buf[1] := Line[BeginRun];
|
|
IO.Write(Handle, @Buf, 2);
|
|
Inc(Cur, RunCount);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure SavePixels;
|
|
var
|
|
Y, X, I, Width: Integer;
|
|
SrcPtr: PColor96FPRecArray;
|
|
Components: array of array of Byte;
|
|
StartLine: array[0..3] of Byte;
|
|
begin
|
|
Width := ImageToSave.Width;
|
|
// Save using RLE, each component is compressed separately
|
|
SetLength(Components, 4, Width);
|
|
|
|
for Y := 0 to ImageToSave.Height - 1 do
|
|
begin
|
|
SrcPtr := @PColor96FPRecArray(ImageToSave.Bits)[ImageToSave.Width * Y];
|
|
|
|
// Identify line as using "new" RLE scheme (separate components)
|
|
StartLine[0] := 2;
|
|
StartLine[1] := 2;
|
|
StartLine[2] := Width shr 8;
|
|
StartLine[3] := Width and $FF;
|
|
IO.Write(Handle, @StartLine, SizeOf(StartLine));
|
|
|
|
for X := 0 to Width - 1 do
|
|
begin
|
|
EncodeRgbe(SrcPtr[X], Components[0, X], Components[1, X],
|
|
Components[2, X], Components[3, X]);
|
|
end;
|
|
|
|
for I := 0 to 3 do
|
|
WriteRleLine(Components[I], Width);
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
Result := False;
|
|
IO := GetIO;
|
|
// Makes image to save compatible with Jpeg saving capabilities
|
|
if MakeCompatible(Images[Index], ImageToSave, MustBeFreed) then
|
|
with ImageToSave do
|
|
try
|
|
// Save header
|
|
SaveHeader;
|
|
// Save uncompressed pixels
|
|
SavePixels;
|
|
Result := True;
|
|
finally
|
|
if MustBeFreed then
|
|
FreeImage(ImageToSave);
|
|
end;
|
|
end;
|
|
|
|
procedure THdrFileFormat.ConvertToSupported(var Image: TImageData;
|
|
const Info: TImageFormatInfo);
|
|
begin
|
|
ConvertImage(Image, ifR32G32B32F);
|
|
end;
|
|
|
|
function THdrFileFormat.TestFormat(Handle: TImagingHandle): Boolean;
|
|
var
|
|
FileSig: TSignature;
|
|
ReadCount: Integer;
|
|
begin
|
|
Result := False;
|
|
if Handle <> nil then
|
|
begin
|
|
ReadCount := GetIO.Read(Handle, @FileSig, SizeOf(FileSig));
|
|
GetIO.Seek(Handle, -ReadCount, smFromCurrent);
|
|
Result := (ReadCount = SizeOf(FileSig)) and
|
|
((FileSig = RadianceSignature) or CompareMem(@FileSig, @RgbeSignature, 6));
|
|
end;
|
|
end;
|
|
|
|
initialization
|
|
RegisterImageFileFormat(THdrFileFormat);
|
|
|
|
{
|
|
File Notes:
|
|
|
|
-- 0.77.1 ---------------------------------------------------
|
|
- Added RLE compression to saving.
|
|
- Added image saving.
|
|
- Unit created with initial stuff (loading only).
|
|
|
|
}
|
|
|
|
end.
|