{
    This file is part of the Free Pascal run time library.
    Copyright (c) 2003 by the Free Pascal development team

    XPM reader class.

    See the file COPYING.FPC, included in this distribution,
    for details about the copyright.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

 **********************************************************************}
{$mode objfpc}{$h+}
unit FPReadXPM;

interface

uses FPImage, classes, sysutils;

type
  TFPReaderXPM = class (TFPCustomImageReader)
    private
      width, height, ncols, cpp, xhot, yhot : integer;
      xpmext : boolean;
      palette : TStringList;
      function HexToColor(s : string) : TFPColor;
      function NameToColor(s : string) : TFPColor;
      function DiminishWhiteSpace (s : string) : string;
    protected
      procedure InternalRead  (Str:TStream; Img:TFPCustomImage); override;
      function  InternalCheck (Str:TStream) : boolean; override;
    public
      constructor Create; override;
      destructor Destroy; override;
  end;

implementation

const
  WhiteSpace = ' '#8#10#13;

constructor TFPReaderXPM.create;
begin
  inherited create;
  palette := TStringList.Create;
end;

destructor TFPReaderXPM.Destroy;
begin
  Palette.Free;
  inherited destroy;
end;

function TFPReaderXPM.HexToColor(s : string) : TFPColor;
var l : integer;
  function CharConv (c : char) : longword;
  begin
    if (c >= 'A') and (c <= 'F') then
      result := ord (c) - ord('A') + 10
    else if (c >= '0') and (c <= '9') then
      result := ord (c) - ord('0')
    else
      raise exception.CreateFmt ('Wrong character (%s) in hexadecimal number', [c]);
  end;
  function convert (n : string) : word;
  var t,r: integer;
  begin
    result := 0;
    t := length(n);
    if t > 4 then
      raise exception.CreateFmt ('Too many bytes for color (%s)',[s]);
    for r := 1 to length(n) do
      result := (result shl 4) or CharConv(n[r]);
    // fill missing bits
    case t of
      1: result:=result or (result shl 4) or (result shl 8) or (result shl 12);
      2: result:=result or (result shl 8);
      3: result:=result or (result shl 12);
    end;
  end;
begin
  s := uppercase (s);
  l := length(s) div 3;
  result.red   := (Convert(copy(s,1,l)));
  result.green := (Convert(copy(s,l+1,l)));
  result.blue  :=  Convert(copy(s,l+l+1,l));
  result.alpha := AlphaOpaque;
end;

function TFPReaderXPM.NameToColor(s : string) : TFPColor;
begin
  s := lowercase (s);
  if s = 'transparent' then
    result := colTransparent
  else if s = 'none' then
    result := colTransparent
  else if s = 'black' then
    result := colBlack
  else if s = 'blue' then
    result := colBlue
  else if s = 'green' then
    result := colGreen
  else if s = 'cyan' then
    result := colCyan
  else if s = 'red' then
    result := colRed
  else if s = 'magenta' then
    result := colMagenta
  else if s = 'yellow' then
    result := colYellow
  else if s = 'white' then
    result := colWhite
  else if s = 'gray' then
    result := colGray
  else if s = 'ltgray' then
    result := colLtGray
  else if s = 'dkblue' then
    result := colDkBlue
  else if s = 'dkgreen' then
    result := colDkGreen
  else if s = 'dkcyan' then
    result := colDkCyan
  else if s = 'dkred' then
    result := colDkRed
  else if s = 'dkmagenta' then
    result := colDkMagenta
  else if s = 'dkyellow' then
    result := colDkYellow
  else if s = 'maroon' then
    result := colMaroon
  else if s = 'ltgreen' then
    result := colLtGreen
  else if s = 'olive' then
    result := colOlive
  else if s = 'navy' then
    result := colNavy
  else if s = 'purple' then
    result := colPurple
  else if s = 'teal' then
    result := colTeal
  else if s = 'silver' then
    result := colSilver
  else if s = 'lime' then
    result := colLime
  else if s = 'fuchsia' then
    result := colFuchsia
  else if s = 'aqua' then
    result := colAqua
  else
    result := colTransparent;
end;

function TFPReaderXPM.DiminishWhiteSpace (s : string) : string;
var r : integer;
    Doit : boolean;
begin
  Doit := true;
  result := '';
  for r := 1 to length(s) do
    if pos(s[r],WhiteSpace)>0 then
      begin
      if DoIt then
        result := result + ' ';
      DoIt := false;
      end
    else
      begin
      DoIt := True;
      result := result + s[r];
      end;
end;

procedure TFPReaderXPM.InternalRead  (Str:TStream; Img:TFPCustomImage);
var l : TStringList;

  procedure TakeInteger (var s : string; var i : integer);
  var r : integer;
  begin
    r := pos (' ', s);
    if r = 0 then
      begin
      i := StrToInt(s);
      s := '';
      end
    else
      begin
      i := StrToInt(copy(s,1,r-1));
      delete (s, 1, r);
      end;
  end;

  procedure ParseFirstLine;
  var s : string;
  begin
    s := l[0];
    // diminish all whitespace to 1 blank
    s := DiminishWhiteSpace (trim(s));
    Takeinteger (s, width);
    Takeinteger (s, height);
    Takeinteger (s, ncols);
    Takeinteger (s, cpp);
    if s <> '' then
      begin
      Takeinteger (s, xhot);
      Takeinteger (s, yhot);
      xpmext := (comparetext(s, 'XPMEXT') = 0);
      if (s <> '') and not xpmext then
        Raise Exception.Create ('Wrong word for XPMEXT tag');
      end;
  end;

  procedure AddPalette (const code:string;const Acolor:TFPColor);
  var r : integer;
  begin
    r := Palette.Add(code);
    img.palette.Color[r] := Acolor;
  end;

  procedure AddToPalette(s : string);
  var code : string;
      c : TFPColor;
       p : integer;
  begin
    code := copy(s,1,cpp);
    s := trim(diminishWhiteSpace (copy(s,cpp+1,maxint)));
    // Search for c-key in the color values
    if s[1] = 'c' then
      delete (s, 1, 2)
    else
      begin
      p := pos (' c ',s);
      if p = 0 then
        s := ''
      else
        delete (s, 1, p+2);
      end;
    // c color value is first word, remove the rest of the line
    p := pos(' ', s);
    if p > 0 then
      delete (s, p, maxint);
    // check if exists
    if s = '' then
      raise exception.Create ('Only c-key is used for colors');
    // convert #hexadecimal value to integer and place in palette
    if s[1] = '#' then
      c := HexToColor(copy(s,2,maxint))
    else
      c := NameToColor(s);
    AddPalette(code,c);
  end;

  procedure ReadPalette;
  var r : integer;
  begin
    Palette.Clear;
    Img.Palette.Count := ncols;
    for r := 1 to ncols do
      AddToPalette (l[r]);
  end;

  procedure ReadLine (const s : string; imgindex : integer);
  var color, r, p : integer;
      code : string;
  begin
    p := 1;
    for r := 1 to width do
      begin
      code := copy(s, p, cpp);
      inc(p,cpp);
      for color := 0 to Palette.Count-1 do
        { Can't use indexof, as compare must be case sensitive }
        if code = Palette[color] then begin
          img.pixels[r-1,imgindex] := color;
          Break;
        end;
      end;
  end;

  procedure ReadData;
  var r : integer;
  begin
    for r := 1 to height do
      ReadLine (l[ncols+r], r-1);
  end;

var p, r : integer;
begin
  l := TStringList.Create;
  try
    l.LoadFromStream (Str);
    for r := l.count-1 downto 0 do
      begin
      p := pos ('"', l[r]);
      if p > 0 then
        l[r] := copy(l[r], p+1, lastdelimiter('"',l[r])-p-1)
      else
        l.delete(r);
      end;
    ParseFirstLine;
    Img.SetSize (width, height);
    ReadPalette;
    ReadData;
  finally
    l.Free;
  end;
end;

function  TFPReaderXPM.InternalCheck (Str:TStream) : boolean;
var s : string[9];
    l : integer;
begin
  try
    l := str.Read (s[1],9);
    s[0] := char(l);
    if l <> 9 then
      result := False
    else
      result := (s = '/* XPM */');
  except
    result := false;
  end;
end;

initialization
  ImageHandlers.RegisterImageReader ('XPM Format', 'xpm', TFPReaderXPM);
end.
