{ 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 Windows Bitmap images. } unit ImagingBitmap; {$I ImagingOptions.inc} interface uses ImagingTypes, Imaging, ImagingUtility, ImagingFormats, ImagingIO; type { Class for loading and saving Windows Bitmap images. It can load/save 8bit indexed, 16, 24, 32 bit RGB or ARGB images with or without RLE compression. It can also load 1/4 bit indexed images and OS2 bitmaps.} TBitmapFileFormat = class(TImageFileFormat) protected FUseRLE: LongBool; 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; published { Controls that RLE compression is used during saving. Accessible trough ImagingBitmapRLE option.} property UseRLE: LongBool read FUseRLE write FUseRLE; end; implementation const SBitmapFormatName = 'Windows Bitmap Image'; SBitmapMasks = '*.bmp,*.dib'; BitmapSupportedFormats: TImageFormats = [ifIndex8, ifA1R5G5B5, ifA4R4G4B4, ifR5G6B5, ifR8G8B8, ifA8R8G8B8, ifX1R5G5B5, ifX4R4G4B4, ifX8R8G8B8]; BitmapDefaultRLE = True; const { Bitmap file identifier 'BM'.} BMMagic: Word = 19778; { Constants for the TBitmapInfoHeader.Compression field.} BI_RGB = 0; BI_RLE8 = 1; BI_RLE4 = 2; BI_BITFIELDS = 3; V3InfoHeaderSize = 40; V4InfoHeaderSize = 108; type { File Header for Windows/OS2 bitmap file.} TBitmapFileHeader = packed record ID: Word; // Is always 19778 : 'BM' Size: UInt32; // File size Reserved1: Word; Reserved2: Word; Offset: UInt32; // Offset from start pos to beginning of image bits end; { Info Header for Windows bitmap file version 4.} TBitmapInfoHeader = packed record Size: UInt32; Width: Int32; Height: Int32; Planes: Word; BitCount: Word; Compression: UInt32; SizeImage: UInt32; XPelsPerMeter: Int32; YPelsPerMeter: Int32; ClrUsed: UInt32; ClrImportant: UInt32; RedMask: UInt32; GreenMask: UInt32; BlueMask: UInt32; AlphaMask: UInt32; CSType: UInt32; EndPoints: array[0..8] of UInt32; GammaRed: UInt32; GammaGreen: UInt32; GammaBlue: UInt32; end; { Info Header for OS2 bitmaps.} TBitmapCoreHeader = packed record Size: UInt32; Width: Word; Height: Word; Planes: Word; BitCount: Word; end; { Used in RLE encoding and decoding.} TRLEOpcode = packed record Count: Byte; Command: Byte; end; PRLEOpcode = ^TRLEOpcode; { TBitmapFileFormat class implementation } procedure TBitmapFileFormat.Define; begin inherited; FName := SBitmapFormatName; FFeatures := [ffLoad, ffSave]; FSupportedFormats := BitmapSupportedFormats; FUseRLE := BitmapDefaultRLE; AddMasks(SBitmapMasks); RegisterOption(ImagingBitmapRLE, @FUseRLE); end; function TBitmapFileFormat.LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean; var BF: TBitmapFileHeader; BI: TBitmapInfoHeader; BC: TBitmapCoreHeader; IsOS2: Boolean; PalRGB: PPalette24; I, FPalSize, AlignedSize, StartPos, HeaderSize, AlignedWidthBytes, WidthBytes: LongInt; Info: TImageFormatInfo; Data: Pointer; procedure LoadRGB; var I: LongInt; LineBuffer: PByte; begin with Images[0], GetIO do begin // If BI.Height is < 0 then image data are stored non-flipped // but default in windows is flipped so if Height is positive we must // flip it if BI.BitCount < 8 then begin // For 1 and 4 bit images load aligned data, they will be converted to // 8 bit and unaligned later GetMem(Data, AlignedSize); if BI.Height < 0 then Read(Handle, Data, AlignedSize) else for I := Height - 1 downto 0 do Read(Handle, @PByteArray(Data)[I * AlignedWidthBytes], AlignedWidthBytes); end else begin // Images with pixels of size >= 1 Byte are read line by line and // copied to image bits without padding bytes GetMem(LineBuffer, AlignedWidthBytes); try if BI.Height < 0 then for I := 0 to Height - 1 do begin Read(Handle, LineBuffer, AlignedWidthBytes); Move(LineBuffer^, PByteArray(Bits)[I * WidthBytes], WidthBytes); end else for I := Height - 1 downto 0 do begin Read(Handle, LineBuffer, AlignedWidthBytes); Move(LineBuffer^, PByteArray(Bits)[I * WidthBytes], WidthBytes); end; finally FreeMemNil(LineBuffer); end; end; end; end; procedure LoadRLE4; var RLESrc: PByteArray; Row, Col, WriteRow, I: Integer; SrcPos: UInt32; DeltaX, DeltaY, Low, High: Byte; Pixels: PByteArray; OpCode: TRLEOpcode; NegHeightBitmap: Boolean; begin GetMem(RLESrc, BI.SizeImage); GetIO.Read(Handle, RLESrc, BI.SizeImage); with Images[0] do try Low := 0; Pixels := Bits; SrcPos := 0; NegHeightBitmap := BI.Height < 0; Row := 0; // Current row in dest image Col := 0; // Current column in dest image // Row in dest image where actual writing will be done WriteRow := Iff(NegHeightBitmap, Row, Height - 1 - Row); while (Row < Height) and (SrcPos < BI.SizeImage) do begin // Read RLE op-code OpCode := PRLEOpcode(@RLESrc[SrcPos])^; Inc(SrcPos, SizeOf(OpCode)); if OpCode.Count = 0 then begin // A byte Count of zero means that this is a special // instruction. case OpCode.Command of 0: begin // Move to next row Inc(Row); WriteRow := Iff(NegHeightBitmap, Row, Height - 1 - Row); Col := 0; end ; 1: Break; // Image is finished 2: begin // Move to a new relative position DeltaX := RLESrc[SrcPos]; DeltaY := RLESrc[SrcPos + 1]; Inc(SrcPos, 2); Inc(Col, DeltaX); Inc(Row, DeltaY); end else // Do not read data after EOF if SrcPos + OpCode.Command > BI.SizeImage then OpCode.Command := BI.SizeImage - SrcPos; // Take padding bytes and nibbles into account if Col + OpCode.Command > Width then OpCode.Command := Width - Col; // Store absolute data. Command code is the // number of absolute bytes to store for I := 0 to OpCode.Command - 1 do begin if (I and 1) = 0 then begin High := RLESrc[SrcPos] shr 4; Low := RLESrc[SrcPos] and $F; Pixels[WriteRow * Width + Col] := High; Inc(SrcPos); end else Pixels[WriteRow * Width + Col] := Low; Inc(Col); end; // Odd number of bytes is followed by a pad byte if (OpCode.Command mod 4) in [1, 2] then Inc(SrcPos); end; end else begin // Take padding bytes and nibbles into account if Col + OpCode.Count > Width then OpCode.Count := Width - Col; // Store a run of the same color value for I := 0 to OpCode.Count - 1 do begin if (I and 1) = 0 then Pixels[WriteRow * Width + Col] := OpCode.Command shr 4 else Pixels[WriteRow * Width + Col] := OpCode.Command and $F; Inc(Col); end; end; end; finally FreeMem(RLESrc); end; end; procedure LoadRLE8; var RLESrc: PByteArray; SrcCount, Row, Col, WriteRow: Integer; SrcPos: UInt32; DeltaX, DeltaY: Byte; Pixels: PByteArray; OpCode: TRLEOpcode; NegHeightBitmap: Boolean; begin GetMem(RLESrc, BI.SizeImage); GetIO.Read(Handle, RLESrc, BI.SizeImage); with Images[0] do try Pixels := Bits; SrcPos := 0; NegHeightBitmap := BI.Height < 0; Row := 0; // Current row in dest image Col := 0; // Current column in dest image // Row in dest image where actual writing will be done WriteRow := Iff(NegHeightBitmap, Row, Height - 1 - Row); while (Row < Height) and (SrcPos < BI.SizeImage) do begin // Read RLE op-code OpCode := PRLEOpcode(@RLESrc[SrcPos])^; Inc(SrcPos, SizeOf(OpCode)); if OpCode.Count = 0 then begin // A byte Count of zero means that this is a special // instruction. case OpCode.Command of 0: begin // Move to next row Inc(Row); WriteRow := Iff(NegHeightBitmap, Row, Height - 1 - Row); Col := 0; end ; 1: Break; // Image is finished 2: begin // Move to a new relative position DeltaX := RLESrc[SrcPos]; DeltaY := RLESrc[SrcPos + 1]; Inc(SrcPos, 2); Inc(Col, DeltaX); Inc(Row, DeltaY); end else SrcCount := OpCode.Command; // Do not read data after EOF if SrcPos + OpCode.Command > BI.SizeImage then OpCode.Command := BI.SizeImage - SrcPos; // Take padding bytes into account if Col + OpCode.Command > Width then OpCode.Command := Width - Col; // Store absolute data. Command code is the // number of absolute bytes to store Move(RLESrc[SrcPos], Pixels[WriteRow * Width + Col], OpCode.Command); Inc(SrcPos, SrcCount); Inc(Col, OpCode.Command); // Odd number of bytes is followed by a pad byte if (SrcCount mod 2) = 1 then Inc(SrcPos); end; end else begin // Take padding bytes into account if Col + OpCode.Count > Width then OpCode.Count := Width - Col; // Store a run of the same color value. Count is number of bytes to store FillChar(Pixels [WriteRow * Width + Col], OpCode.Count, OpCode.Command); Inc(Col, OpCode.Count); end; end; finally FreeMem(RLESrc); end; end; begin Data := nil; SetLength(Images, 1); with GetIO, Images[0] do try FillChar(BI, SizeOf(BI), 0); StartPos := Tell(Handle); Read(Handle, @BF, SizeOf(BF)); Read(Handle, @BI.Size, SizeOf(BI.Size)); IsOS2 := BI.Size = SizeOf(TBitmapCoreHeader); // Bitmap Info reading if IsOS2 then begin // OS/2 type bitmap, reads info header without 4 already read bytes Read(Handle, @PByteArray(@BC)[SizeOf(BI.Size)], SizeOf(TBitmapCoreHeader) - SizeOf(BI.Size)); with BI do begin ClrUsed := 0; Compression := BI_RGB; BitCount := BC.BitCount; Height := BC.Height; Width := BC.Width; end; end else begin // Windows type bitmap HeaderSize := Min(BI.Size - SizeOf(BI.Size), SizeOf(BI) - SizeOf(BI.Size)); // do not read more than size of BI! Read(Handle, @PByteArray(@BI)[SizeOf(BI.Size)], HeaderSize); // SizeImage can be 0 for BI_RGB images, but it is here because of: // I saved 8bit bitmap in Paint Shop Pro 8 as OS2 RLE compressed. // It wrote strange 64 Byte Info header with SizeImage set to 0 // Some progs were able to open it, some were not. if BI.SizeImage = 0 then BI.SizeImage := BF.Size - BF.Offset; end; // Bit mask reading. Only read it if there is V3 header, V4 header has // masks loaded already (only masks for RGB in V3). if (BI.Compression = BI_BITFIELDS) and (BI.Size = V3InfoHeaderSize) then Read(Handle, @BI.RedMask, SizeOf(BI.RedMask) * 3); case BI.BitCount of 1, 4, 8: Format := ifIndex8; 16: if BI.RedMask = $0F00 then // Set XRGB4 or ARGB4 according to value of alpha mask Format := IffFormat(BI.AlphaMask = 0, ifX4R4G4B4, ifA4R4G4B4) else if BI.RedMask = $F800 then Format := ifR5G6B5 else // R5G5B5 is default 16bit format (with Compression = BI_RGB or masks). // We set it to A1.. and later there is a check if there are any alpha values // and if not it is changed to X1R5G5B5 Format := ifA1R5G5B5; 24: Format := ifR8G8B8; 32: Format := ifA8R8G8B8; // As with R5G5B5 there is alpha check later end; NewImage(BI.Width, Abs(BI.Height), Format, Images[0]); Info := GetFormatInfo(Format); WidthBytes := Width * Info.BytesPerPixel; AlignedWidthBytes := (((Width * BI.BitCount) + 31) shr 5) * 4; AlignedSize := Height * LongInt(AlignedWidthBytes); // Palette settings and reading if BI.BitCount <= 8 then begin // Seek to the beginning of palette Seek(Handle, StartPos + SizeOf(TBitmapFileHeader) + LongInt(BI.Size), smFromBeginning); if IsOS2 then begin // OS/2 type FPalSize := 1 shl BI.BitCount; GetMem(PalRGB, FPalSize * SizeOf(TColor24Rec)); try Read(Handle, PalRGB, FPalSize * SizeOf(TColor24Rec)); for I := 0 to FPalSize - 1 do with PalRGB[I] do begin Palette[I].R := R; Palette[I].G := G; Palette[I].B := B; end; finally FreeMemNil(PalRGB); end; end else begin // Windows type FPalSize := BI.ClrUsed; if FPalSize = 0 then FPalSize := 1 shl BI.BitCount; Read(Handle, Palette, FPalSize * SizeOf(TColor32Rec)); end; for I := 0 to Info.PaletteEntries - 1 do Palette[I].A := $FF; end; // Seek to the beginning of image bits Seek(Handle, StartPos + LongInt(BF.Offset), smFromBeginning); case BI.Compression of BI_RGB: LoadRGB; BI_RLE4: LoadRLE4; BI_RLE8: LoadRLE8; BI_BITFIELDS: LoadRGB; end; if BI.AlphaMask = 0 then begin // Alpha mask is not stored in file (V3) or not defined. // Check alpha channels of loaded images if they might contain them. if Format = ifA1R5G5B5 then begin // Check if there is alpha channel present in A1R5GB5 images, if it is not // change format to X1R5G5B5 if not Has16BitImageAlpha(Width * Height, Bits) then Format := ifX1R5G5B5; end else if Format = ifA8R8G8B8 then begin // Check if there is alpha channel present in A8R8G8B8 images, if it is not // change format to X8R8G8B8 if not Has32BitImageAlpha(Width * Height, Bits) then Format := ifX8R8G8B8; end; end; if BI.BitCount < 8 then begin // 1 and 4 bpp images are supported only for loading which is now // so we now convert them to 8bpp (and unalign scanlines). case BI.BitCount of 1: Convert1To8(Data, Bits, Width, Height, AlignedWidthBytes, False); 4: begin // RLE4 bitmaps are translated to 8bit during RLE decoding if BI.Compression <> BI_RLE4 then Convert4To8(Data, Bits, Width, Height, AlignedWidthBytes, False); end; end; // Enlarge palette ReallocMem(Palette, Info.PaletteEntries * SizeOf(TColor32Rec)); end; Result := True; finally FreeMemNil(Data); end; end; function TBitmapFileFormat.SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray; Index: LongInt): Boolean; var StartPos, EndPos, I, Pad, PadSize, WidthBytes: LongInt; BF: TBitmapFileHeader; BI: TBitmapInfoHeader; Info: TImageFormatInfo; ImageToSave: TImageData; MustBeFreed: Boolean; procedure SaveRLE8; const BufferSize = 8 * 1024; var X, Y, I, SrcPos: LongInt; DiffCount, SameCount: Byte; Pixels: PByteArray; Buffer: array[0..BufferSize - 1] of Byte; BufferPos: LongInt; procedure WriteByte(ByteToWrite: Byte); begin if BufferPos = BufferSize then begin // Flush buffer if necessary GetIO.Write(Handle, @Buffer, BufferPos); BufferPos := 0; end; Buffer[BufferPos] := ByteToWrite; Inc(BufferPos); end; begin BufferPos := 0; with GetIO, ImageToSave do begin for Y := Height - 1 downto 0 do begin X := 0; SrcPos := 0; Pixels := @PByteArray(Bits)[Y * Width]; while X < Width do begin SameCount := 1; DiffCount := 0; // Determine run length while X + SameCount < Width do begin // If we reach max run length or byte with different value // we end this run if (SameCount = 255) or (Pixels[SrcPos + SameCount] <> Pixels[SrcPos]) then Break; Inc(SameCount); end; if SameCount = 1 then begin // If there are not some bytes with the same value we // compute how many different bytes are there while X + DiffCount < Width do begin // Stop diff byte counting if there two bytes with the same value // or DiffCount is too big if (DiffCount = 255) or (Pixels[SrcPos + DiffCount + 1] = Pixels[SrcPos + DiffCount]) then Break; Inc(DiffCount); end; end; // Now store absolute data (direct copy image->file) or // store RLE code only (number of repeats + byte to be repeated) if DiffCount > 2 then begin // Save 'Absolute Data' (0 + number of bytes) but only // if number is >2 because (0+1) and (0+2) are other special commands WriteByte(0); WriteByte(DiffCount); // Write absolute data to buffer for I := 0 to DiffCount - 1 do WriteByte(Pixels[SrcPos + I]); Inc(X, DiffCount); Inc(SrcPos, DiffCount); // Odd number of bytes must be padded if (DiffCount mod 2) = 1 then WriteByte(0); end else begin // Save number of repeats and byte that should be repeated WriteByte(SameCount); WriteByte(Pixels[SrcPos]); Inc(X, SameCount); Inc(SrcPos, SameCount); end; end; // Save 'End Of Line' command WriteByte(0); WriteByte(0); end; // Save 'End Of Bitmap' command WriteByte(0); WriteByte(1); // Flush buffer GetIO.Write(Handle, @Buffer, BufferPos); end; end; begin Result := False; if MakeCompatible(Images[Index], ImageToSave, MustBeFreed) then with GetIO, ImageToSave do try Info := GetFormatInfo(Format); StartPos := Tell(Handle); FillChar(BF, SizeOf(BF), 0); FillChar(BI, SizeOf(BI), 0); // Other fields will be filled later - we don't know all values now BF.ID := BMMagic; Write(Handle, @BF, SizeOf(BF)); if Info.HasAlphaChannel and (Info.BytesPerPixel = 2){V4 temp hack} then // Save images with alpha in V4 format BI.Size := V4InfoHeaderSize else // Save images without alpha in V3 format - for better compatibility BI.Size := V3InfoHeaderSize; BI.Width := Width; BI.Height := Height; BI.Planes := 1; BI.BitCount := Info.BytesPerPixel * 8; BI.XPelsPerMeter := 2835; // 72 dpi BI.YPelsPerMeter := 2835; // 72 dpi // Set compression if (Info.BytesPerPixel = 1) and FUseRLE then BI.Compression := BI_RLE8 else if (Info.HasAlphaChannel or ((BI.BitCount = 16) and (Format <> ifX1R5G5B5))) and (Info.BytesPerPixel = 2){V4 temp hack} then BI.Compression := BI_BITFIELDS else BI.Compression := BI_RGB; // Write header (first time) Write(Handle, @BI, BI.Size); // Write mask info if BI.Compression = BI_BITFIELDS then begin if BI.BitCount = 16 then with Info.PixelFormat^ do begin BI.RedMask := RBitMask; BI.GreenMask := GBitMask; BI.BlueMask := BBitMask; BI.AlphaMask := ABitMask; end else begin // Set masks for A8R8G8B8 BI.RedMask := $00FF0000; BI.GreenMask := $0000FF00; BI.BlueMask := $000000FF; BI.AlphaMask := $FF000000; end; // If V3 header is used RGB masks must be written to file separately. // V4 header has embedded masks (V4 is default for formats with alpha). if BI.Size = V3InfoHeaderSize then Write(Handle, @BI.RedMask, SizeOf(BI.RedMask) * 3); end; // Write palette if Palette <> nil then Write(Handle, Palette, Info.PaletteEntries * SizeOf(TColor32Rec)); BF.Offset := Tell(Handle) - StartPos; if BI.Compression <> BI_RLE8 then begin // Save uncompressed data, scanlines must be filled with pad bytes // to be multiples of 4, save as bottom-up (Windows native) bitmap Pad := 0; WidthBytes := Width * Info.BytesPerPixel; PadSize := ((Width * BI.BitCount + 31) div 32) * 4 - WidthBytes; for I := Height - 1 downto 0 do begin Write(Handle, @PByteArray(Bits)[I * WidthBytes], WidthBytes); if PadSize > 0 then Write(Handle, @Pad, PadSize); end; end else begin // Save data with RLE8 compression SaveRLE8; end; EndPos := Tell(Handle); Seek(Handle, StartPos, smFromBeginning); // Rewrite header with new values BF.Size := EndPos - StartPos; BI.SizeImage := BF.Size - BF.Offset; Write(Handle, @BF, SizeOf(BF)); Write(Handle, @BI, BI.Size); Seek(Handle, EndPos, smFromBeginning); Result := True; finally if MustBeFreed then FreeImage(ImageToSave); end; end; procedure TBitmapFileFormat.ConvertToSupported(var Image: TImageData; const Info: TImageFormatInfo); var ConvFormat: TImageFormat; begin if Info.IsFloatingPoint then // Convert FP image to RGB/ARGB according to presence of alpha channel ConvFormat := IffFormat(Info.HasAlphaChannel, ifA8R8G8B8, ifR8G8B8) else if Info.HasGrayChannel or Info.IsIndexed then // Convert all grayscale and indexed images to Index8 unless they have alpha // (preserve it) ConvFormat := IffFormat(Info.HasAlphaChannel, ifA8R8G8B8, ifIndex8) else if Info.HasAlphaChannel then // Convert images with alpha channel to A8R8G8B8 ConvFormat := ifA8R8G8B8 else if Info.UsePixelFormat then // Convert 16bit RGB images (no alpha) to X1R5G5B5 ConvFormat := ifX1R5G5B5 else // Convert all other formats to R8G8B8 ConvFormat := ifR8G8B8; ConvertImage(Image, ConvFormat); end; function TBitmapFileFormat.TestFormat(Handle: TImagingHandle): Boolean; var Hdr: TBitmapFileHeader; ReadCount: LongInt; begin Result := False; if Handle <> nil then with GetIO do begin ReadCount := Read(Handle, @Hdr, SizeOf(Hdr)); Seek(Handle, -ReadCount, smFromCurrent); Result := (Hdr.ID = BMMagic) and (ReadCount = SizeOf(Hdr)); end; end; initialization RegisterImageFileFormat(TBitmapFileFormat); { File Notes: -- TODOS ---------------------------------------------------- - nothing now - Add option to choose to save V3 or V4 headers. -- 0.25.0 Changes/Bug Fixes --------------------------------- - Fixed problem with indexed BMP loading - some pal entries could end up with alpha=0. -- 0.23 Changes/Bug Fixes ----------------------------------- - Now saves bitmaps as bottom-up for better compatibility (mainly Lazarus' TImage!). - Fixed crash when loading bitmaps with headers larger than V4. - Temp hacks to disable V4 headers for 32bit images (compatibility with other soft). -- 0.21 Changes/Bug Fixes ----------------------------------- - Removed temporary data allocation for image with aligned scanlines. They are now directly written to output so memory requirements are much lower now. - Now uses and recognizes BITMAPINFOHEADERV4 when loading/saving. Mainly for formats with alpha channels. - Added ifR5G6B5 to supported formats, changed converting to supported formats little bit. - Rewritten SaveRLE8 nested procedure. Old code was long and mysterious - new is short and much more readable. - MakeCompatible method moved to base class, put ConvertToSupported here. GetSupportedFormats removed, it is now set in constructor. - Rewritten LoadRLE4 and LoadRLE8 nested procedures. Should be less buggy an more readable (load inspired by Colosseum Builders' code). - Made public properties for options registered to SetOption/GetOption functions. - Added alpha check to 32b bitmap loading too (teh same as in 16b bitmap loading). - Moved Convert1To8 and Convert4To8 to ImagingFormats - Changed extensions to filename masks. - Changed SaveData, LoadData, and MakeCompatible methods according to changes in base class in Imaging unit. -- 0.19 Changes/Bug Fixes ----------------------------------- - fixed wrong const that caused A4R4G4B4 BMPs to load as A1R5G5B5 - fixed the bug that caused 8bit RLE compressed bitmaps to load as whole black -- 0.17 Changes/Bug Fixes ----------------------------------- - 16 bit images are usually without alpha but some has alpha channel and there is no indication of it - so I have added a check: if all pixels of image are with alpha = 0 image is treated as X1R5G5B5 otherwise as A1R5G5B5 -- 0.13 Changes/Bug Fixes ----------------------------------- - when loading 1/4 bit images with dword aligned dimensions there was ugly memory rewriting bug causing image corruption } end.