{
  $Id: ImagingXpm.pas 171 2009-09-02 01:34:19Z galfar $
  Vampyre Imaging Library
  by Marek Mauder
  http://imaginglib.sourceforge.net

  The contents of this file are used with permission, subject to the Mozilla
  Public License Version 1.1 (the "License"); you may not use this file except
  in compliance with the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL/MPL-1.1.html

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
  the specific language governing rights and limitations under the License.

  Alternatively, the contents of this file may be used under the terms of the
  GNU Lesser General Public License (the  "LGPL License"), in which case the
  provisions of the LGPL License are applicable instead of those above.
  If you wish to allow use of your version of this file only under the terms
  of the LGPL License and not to allow others to use your version of this file
  under the MPL, indicate your decision by deleting  the provisions above and
  replace  them with the notice and other provisions required by the LGPL
  License.  If you do not delete the provisions above, a recipient may use
  your version of this file under either the MPL or the LGPL License.

  For more information about the LGPL: http://www.gnu.org/copyleft/lesser.html
}

{ This unit contains image format loader for X Window Pixmap images. }
unit ImagingXpm;
{$I ImagingOptions.inc}

interface

uses
  SysUtils, Classes, Contnrs, ImagingTypes, Imaging, ImagingUtility,
  ImagingFormats, ImagingIO, ImagingCanvases;

type
  { Class for loading X Window Pixmap images known as XPM.
    It is ASCII-text-based format, basicaly a fragment of C code
    declaring static array. Loaded image is in ifA8R8G8B8 data format.
    Loading as well as saving is supported now. }
  TXPMFileFormat = class(TImageFileFormat)
  protected
    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
    constructor Create; override;
    function TestFormat(Handle: TImagingHandle): Boolean; override;
  end;

implementation

const
  SXPMFormatName = 'X Window Pixmap';
  SXPMMasks = '*.xpm';
  XPMSupportedFormats: TImageFormats = [ifA8R8G8B8];

const
  SXPMId = '/* XPM */';
  WhiteSpaces = [#9, #10, #13, #32];

const
  BucketCount = 257;

type
  TColorHolder = class
  public
    Color: TColor32;
  end;

  TBucketItem = record
    Key: TColor32;
    Data: string[8];
  end;

  TBucketItemArray = array of TBucketItem;

  TBucket = record
    Count: Integer;
    ItemIdxStart: Integer;
    Items: TBucketItemArray;
  end;

  TBucketArray = array of TBucket;

  { Simple color-string hash table for faster than linear searches
    during XPM saving. }
  TSimpleBucketList = class
  private
    FBuckets: TBucketArray;
    FItemCount: Integer;
    FABucket, FAIndex: Integer;
    function GetData(AKey: TColor32): string;
    procedure SetData(AKey: TColor32; const AData: string);
    function FindItem(AKey: TColor32; out ABucket, AIndex: Integer): Boolean;
  public
    constructor Create;
    procedure Add(AKey: TColor32; const AData: string);
    function Exists(AKey: TColor32): Boolean;
    function EnumNext(out AData: string): TColor32;
    property Data[AKey: TColor32]: string read GetData write SetData; default;
    property ItemCount: Integer read FItemCount;
  end;

  { TSimpleBucketList }

constructor TSimpleBucketList.Create;
begin
  SetLength(FBuckets, BucketCount);
end;

function TSimpleBucketList.GetData(AKey: TColor32): string;
var
  Bucket, Index: Integer;
begin
  Result := '';
  if FindItem(AKey, Bucket, Index) then
    Result := FBuckets[Bucket].Items[Index].Data;
end;

procedure TSimpleBucketList.SetData(AKey: TColor32; const AData: string);
var
  Bucket, Index: Integer;
begin
  if FindItem(AKey, Bucket, Index) then
    FBuckets[Bucket].Items[Index].Data := AData;
end;

function TSimpleBucketList.EnumNext(out AData: string): TColor32;
begin
  // Skip empty buckets
  while FAIndex >= FBuckets[FABucket].Count do
  begin
    Inc(FABucket);
    if FABucket >= Length(FBuckets) then
      FABucket := 0;
    FAIndex := 0;
  end;

  Result := FBuckets[FABucket].Items[FAIndex].Key;
  AData := FBuckets[FABucket].Items[FAIndex].Data;
  Inc(FAIndex);
end;

function TSimpleBucketList.FindItem(AKey: TColor32; out ABucket,
  AIndex: Integer): Boolean;
var
  I: Integer;
  Col: TColor32Rec;
begin
  Result := False;
  Col := TColor32Rec(AKey);
  ABucket := (Col.A + 11 * Col.B + 59 * Col.R + 119 * Col.G) mod BucketCount;
  with FBuckets[ABucket] do
  for I := 0 to Count - 1 do
    if Items[I].Key = AKey then
    begin
      AIndex := I;
      Result := True;
      Break;
    end;
end;

procedure TSimpleBucketList.Add(AKey: TColor32; const AData: string);
var
  Bucket, Index, Delta, Size: Integer;
begin
  if not FindItem(AKey, Bucket, Index) then
  with FBuckets[Bucket] do
  begin
    Size := Length(Items);
    if Count = Size then
    begin
      if Size > 64 then
        Delta := Size div 4
      else
        Delta := 16;
      SetLength(Items, Size + Delta);
    end;

    with Items[Count] do
    begin
      Key := AKey;
      Data := AData;
    end;
    Inc(Count);
    Inc(FItemCount);
  end;
end;

function TSimpleBucketList.Exists(AKey: TColor32): Boolean;
var
  Bucket, Index: Integer;
begin
  Result := FindItem(AKey, Bucket, Index);
end;

{
  TXPMFileFormat implementation
}

constructor TXPMFileFormat.Create;
begin
  inherited Create;
  FName := SXPMFormatName;
  FCanLoad := True;
  FCanSave := True;
  FIsMultiImageFormat := False;
  FSupportedFormats := XPMSupportedFormats;

  AddMasks(SXPMMasks);
end;

function TXPMFileFormat.LoadData(Handle: TImagingHandle;
  var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean;
var
  Contents, PalLookup: TStringList;
  S: AnsiString;
  I, J, NumColors, Cpp, Line: Integer;

  procedure SkipWhiteSpace(var Line: string);
  begin
    while (Length(Line) > 0) and (Line[1] in WhiteSpaces) do
      Delete(Line, 1, 1);
  end;

  function ReadString(var Line: string): string;
  begin
    Result := '';
    SkipWhiteSpace(Line);
    while (Length(Line) > 0) and not(Line[1] in WhiteSpaces) do
    begin
      SetLength(Result, Length(Result) + 1);
      Result[Length(Result)] := Line[1];
      Delete(Line, 1, 1);
    end;
  end;

  function ReadInt(var Line: string): Integer;
  begin
    Result := StrToInt(ReadString(Line));
  end;

  function ParseHeader: Boolean;
  var
    S: string;
  begin
    S := Contents[0];
    try
      Images[0].Width := ReadInt(S);
      Images[0].Height := ReadInt(S);
      NumColors := ReadInt(S);
      Cpp := ReadInt(S);
      Line := 1;
      Result := True;
    except
      Result := False;
    end;
  end;

  function NamedToColor(const ColStr: string): TColor32;
  var
    S: string;
  begin
    S := LowerCase(ColStr);
    if (S = 'transparent') or (S = 'none') then
      Result := pcClear
    else if S = 'black' then
      Result := pcBlack
    else if S = 'blue' then
      Result := pcBlue
    else if S = 'green' then
      Result := pcGreen
    else if S = 'cyan' then
      Result := pcAqua
    else if S = 'red' then
      Result := pcRed
    else if S = 'magenta' then
      Result := pcFuchsia
    else if S = 'yellow' then
      Result := pcYellow
    else if S = 'white' then
      Result := pcWhite
    else if S = 'gray' then
      Result := pcLtGray
    else if S = 'dkblue' then
      Result := pcNavy
    else if S = 'dkgreen' then
      Result := pcGreen
    else if S = 'dkcyan' then
      Result := pcTeal
    else if S = 'dkred' then
      Result := pcMaroon
    else if S = 'dkmagenta' then
      Result := pcPurple
    else if S = 'dkyellow' then
      Result := pcOlive
    else if S = 'maroon' then
      Result := pcMaroon
    else if S = 'olive' then
      Result := pcOlive
    else if S = 'navy' then
      Result := pcNavy
    else if S = 'purple' then
      Result := pcPurple
    else if S = 'teal' then
      Result := pcTeal
    else if S = 'silver' then
      Result := pcSilver
    else if S = 'lime' then
      Result := pcLime
    else if S = 'fuchsia' then
      Result := pcFuchsia
    else if S = 'aqua' then
      Result := pcAqua
    else
      Result := pcClear;
  end;

  procedure ParsePalette;
  var
    I: Integer;
    S, ColType, ColStr, Code: string;
    Color: TColor32;
    Holder: TColorHolder;
  begin
    for I := 0 to NumColors - 1 do
    begin
      Holder := TColorHolder.Create;
      // Parse pixel code and color
      S := Contents[Line + I];
      Code := Copy(S, 1, Cpp);
      Delete(S, 1, Cpp);
      ColType := ReadString(S);
      ColStr := ReadString(S);
      // Convert color from hex number or named constant
      if ColStr[1] = '#' then
      begin
        Delete(ColStr, 1, 1);
        Color := LongWord(StrToInt('$' + Trim(ColStr))) or $FF000000;
      end
      else
        Color := NamedToColor(ColStr);
      // Store code and color in table for later lookup
      Holder.Color := Color;
      PalLookup.AddObject(Code, Holder);
    end;
    Inc(Line, NumColors);
  end;

  procedure ParsePixels;
  var
    X, Y, Idx: Integer;
    S, Code: string;
    Pix: PColor32;
  begin
    Pix := Images[0].Bits;
    for Y := 0 to Images[0].Height - 1 do
    begin
      S := Contents[Line + Y];
      for X := 0 to Images[0].Width - 1 do
      begin
        // Read code and look up color in the palette
        Code := Copy(S, X * Cpp + 1, Cpp);
        if PalLookup.Find(Code, Idx) then
          Pix^ := TColorHolder(PalLookup.Objects[Idx]).Color
        else
          Pix^ := pcClear;
        Inc(Pix);
      end;
    end;
  end;

begin
  Result := False;
  SetLength(Images, 1);
  with GetIO, Images[0] do
  begin
    // Look up table for XPM palette entries
    PalLookup := TStringList.Create;
    PalLookup.Sorted := True;
    PalLookup.CaseSensitive := True;
    // Read whole file and assign it to string list
    Contents := TStringList.Create;
    SetLength(S, GetInputSize(GetIO, Handle));
    Read(Handle, @S[1], Length(S));
    Contents.Text := S;
    // Remove quotes and other stuff
    for I := Contents.Count - 1 downto 0 do
    begin
      J := Pos('"', Contents[I]);
      if J > 0 then
        Contents[I] := Copy(Contents[I], J + 1, LastDelimiter('"', Contents[I]) - J - 1)
      else
        Contents.Delete(I);
    end;
    // Parse header and create new image
    if not ParseHeader then
      Exit;
    NewImage(Width, Height, ifA8R8G8B8, Images[0]);
    // Read palette entries and assign colors to pixels
    ParsePalette;
    ParsePixels;

    Contents.Free;
    for I := 0 to PalLookup.Count - 1 do
      PalLookup.Objects[I].Free;
    PalLookup.Free;
    Result := True;
  end;
end;

function TXPMFileFormat.SaveData(Handle: TImagingHandle;
  const Images: TDynImageDataArray; Index: LongInt): Boolean;
const
  ColorCharsCount = 92;
  ColorChars = ' .XoO+@#$%&*=-;:>,<1234567890qwertyuipasdfghjklzxcvbnmMNBVCZASDFGHJKLPIUYTREWQ!~^/()_`''][{}|';
var
  X, Y: Integer;
  ImageToSave: TImageData;
  MustBeFreed: Boolean;
  StrFile: TStringList;
  ColTable: TSimpleBucketList;
  Stream: TMemoryStream;
  Line, Id: string;
  CharsPerPixel: Integer;
  Ptr: PColor32Rec;
  ColRec: TColor32Rec;

  procedure BuildColorTables(const Img: TImageData);
  var
    I: Integer;
  begin
    Ptr := Img.Bits;
    for I := 0 to Img.Width * Img.Height - 1 do
    begin
      if not ColTable.Exists(Ptr.Color) then
        ColTable.Add(Ptr.Color, '');
      Inc(Ptr);
    end;
  end;

  procedure MakeStrIdsForColors;
  var
    I, J, K: Integer;
    Id, Data: string;
  begin
    SetLength(Id, CharsPerPixel);
    for I := 0 to ColTable.ItemCount - 1 do
    begin
      ColRec.Color := ColTable.EnumNext(Data);
      K := I;
      for J := 0 to CharsPerPixel - 1 do
      begin
        Id[J + 1] := ColorChars[K mod ColorCharsCount + 1];
        K := K div ColorCharsCount;
      end;
      ColTable.Data[ColRec.Color] := Id;
    end;
  end;

begin
  Result := False;

  StrFile := TStringList.Create;
  ColTable := TSimpleBucketList.Create;
  Stream := TMemoryStream.Create;

  if MakeCompatible(Images[Index], ImageToSave, MustBeFreed) then
  try
    // Put all unique colors of image to table
    BuildColorTables(ImageToSave);
    // Compute the character per pixel
    CharsPerPixel := 1;
    X := ColorCharsCount;
    while ColTable.ItemCount > X do
    begin
      X := X * ColorCharsCount;
      Inc(CharsPerPixel);
    end;
    // Assign char id to each color
    MakeStrIdsForColors;

    // Start writing XPM file
    StrFile.Add(SXPMId);
    StrFile.Add('static char *graphic[] = {');
    StrFile.Add('/* width height num_colors chars_per_pixel */');
    StrFile.Add(SysUtils.Format('"%d %d %d %d", ', [ImageToSave.Width,
        ImageToSave.Height, ColTable.ItemCount, CharsPerPixel]));
    StrFile.Add('/* colors */');

    // Write 'colors' part of XPM file
    for X := 0 to ColTable.ItemCount - 1 do
    begin
      ColRec.Color := ColTable.EnumNext(Id);
      if ColRec.A >= 128 then
        StrFile.Add(Format('"%s c #%.2x%.2x%.2x",', [Id, ColRec.R, ColRec.G, ColRec.B]))
      else
        StrFile.Add(Format('"%s c None",', [Id]));
    end;

    StrFile.Add('/* pixels */');

    // Write pixels - for aech pixel of image find its char id
    // and append it to line
    Ptr := ImageToSave.Bits;
    for Y := 0 to ImageToSave.Height - 1 do
    begin
      Line := '';
      for X := 0 to ImageToSave.Width - 1 do
      begin
        Line := Line + ColTable.Data[Ptr.Color];
        Inc(Ptr);
      end;
      Line := '"' + Line + '"';
      if Y < ImageToSave.Height - 1 then
        Line := Line + ',';
      StrFile.Add(Line);
    end;

    StrFile.Add('};');

    // Finally save strings to stream and write stream's data to output
    // (we could directly write lines from list to output, but stream method
    // takes care of D2009+ Unicode strings).
    StrFile.SaveToStream(Stream);
    GetIO.Write(Handle, Stream.Memory, Stream.Size);

    Result := True;
  finally
    StrFile.Free;
    ColTable.Free;
    Stream.Free;
    if MustBeFreed then
      FreeImage(ImageToSave);
  end;
end;

procedure TXPMFileFormat.ConvertToSupported(var Image: TImageData;
  const Info: TImageFormatInfo);
begin
  ConvertImage(Image, ifA8R8G8B8)
end;

function TXPMFileFormat.TestFormat(Handle: TImagingHandle): Boolean;
var
  Id: array[0 .. 8] of AnsiChar;
  ReadCount: Integer;
begin
  Result := False;
  if Handle <> nil then
  begin
    ReadCount := GetIO.Read(Handle, @Id, SizeOf(Id));
    GetIO.Seek(Handle, -ReadCount, smFromCurrent);
    Result := (Id = SXPMId) and (ReadCount = SizeOf(Id));
  end;
end;

initialization

RegisterImageFileFormat(TXPMFileFormat);

{
  File Notes:

  -- TODOS ----------------------------------------------------
  - nothing now

  -- 0.26.3 Changes/Bug Fixes -----------------------------------
  - Added XPM saving.

  -- 0.25.0 Changes/Bug Fixes -----------------------------------
  - Added XPM loading.
  - Unit created.
}

end.


