2
0

ImagingXpm.pas 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. {
  2. Vampyre Imaging Library
  3. by Marek Mauder
  4. https://github.com/galfar/imaginglib
  5. https://imaginglib.sourceforge.io
  6. - - - - -
  7. This Source Code Form is subject to the terms of the Mozilla Public
  8. License, v. 2.0. If a copy of the MPL was not distributed with this
  9. file, You can obtain one at https://mozilla.org/MPL/2.0.
  10. }
  11. { This unit contains image format loader for X Window Pixmap images. }
  12. unit ImagingXpm;
  13. {$I ImagingOptions.inc}
  14. interface
  15. uses
  16. SysUtils, Classes, Contnrs, ImagingTypes, Imaging, ImagingUtility,
  17. ImagingFormats, ImagingIO, ImagingCanvases;
  18. type
  19. { Class for loading X Window Pixmap images known as XPM.
  20. It is ASCII-text-based format, basically a fragment of C code
  21. declaring static array. Loaded image is in ifA8R8G8B8 data format.
  22. Loading as well as saving is supported now. }
  23. TXPMFileFormat = class(TImageFileFormat)
  24. protected
  25. procedure Define; override;
  26. function LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray;
  27. OnlyFirstLevel: Boolean): Boolean; override;
  28. function SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray;
  29. Index: LongInt): Boolean; override;
  30. procedure ConvertToSupported(var Image: TImageData;
  31. const Info: TImageFormatInfo); override;
  32. public
  33. function TestFormat(Handle: TImagingHandle): Boolean; override;
  34. end;
  35. implementation
  36. const
  37. SXPMFormatName = 'X Window Pixmap';
  38. SXPMMasks = '*.xpm';
  39. XPMSupportedFormats: TImageFormats = [ifA8R8G8B8];
  40. const
  41. SXPMId = '/* XPM */';
  42. WhiteSpaces = [#9, #10, #13, #32];
  43. const
  44. BucketCount = 257;
  45. type
  46. TColorHolder = class
  47. public
  48. Color: TColor32;
  49. end;
  50. TBucketItem = record
  51. Key: TColor32;
  52. Data: string[8];
  53. end;
  54. TBucketItemArray = array of TBucketItem;
  55. TBucket = record
  56. Count: Integer;
  57. ItemIdxStart: Integer;
  58. Items: TBucketItemArray;
  59. end;
  60. TBucketArray = array of TBucket;
  61. { Simple color-string hash table for faster than linear searches
  62. during XPM saving. }
  63. TSimpleBucketList = class
  64. private
  65. FBuckets: TBucketArray;
  66. FItemCount: Integer;
  67. FABucket, FAIndex: Integer;
  68. function GetData(AKey: TColor32): string;
  69. procedure SetData(AKey: TColor32; const AData: string);
  70. function FindItem(AKey: TColor32; out ABucket, AIndex: Integer): Boolean;
  71. public
  72. constructor Create;
  73. procedure Add(AKey: TColor32; const AData: string);
  74. function Exists(AKey: TColor32): Boolean;
  75. function EnumNext(out AData: string): TColor32;
  76. property Data[AKey: TColor32]: string read GetData write SetData; default;
  77. property ItemCount: Integer read FItemCount;
  78. end;
  79. { TSimpleBucketList }
  80. constructor TSimpleBucketList.Create;
  81. begin
  82. SetLength(FBuckets, BucketCount);
  83. end;
  84. function TSimpleBucketList.GetData(AKey: TColor32): string;
  85. var
  86. Bucket, Index: Integer;
  87. begin
  88. Result := '';
  89. if FindItem(AKey, Bucket, Index) then
  90. Result := string(FBuckets[Bucket].Items[Index].Data);
  91. end;
  92. procedure TSimpleBucketList.SetData(AKey: TColor32; const AData: string);
  93. var
  94. Bucket, Index: Integer;
  95. begin
  96. if FindItem(AKey, Bucket, Index) then
  97. FBuckets[Bucket].Items[Index].Data := ShortString(AData);
  98. end;
  99. function TSimpleBucketList.EnumNext(out AData: string): TColor32;
  100. begin
  101. // Skip empty buckets
  102. while FAIndex >= FBuckets[FABucket].Count do
  103. begin
  104. Inc(FABucket);
  105. if FABucket >= Length(FBuckets) then
  106. FABucket := 0;
  107. FAIndex := 0;
  108. end;
  109. Result := FBuckets[FABucket].Items[FAIndex].Key;
  110. AData := string(FBuckets[FABucket].Items[FAIndex].Data);
  111. Inc(FAIndex);
  112. end;
  113. function TSimpleBucketList.FindItem(AKey: TColor32; out ABucket,
  114. AIndex: Integer): Boolean;
  115. var
  116. I: Integer;
  117. Col: TColor32Rec;
  118. begin
  119. Result := False;
  120. Col := TColor32Rec(AKey);
  121. ABucket := (Col.A + 11 * Col.B + 59 * Col.R + 119 * Col.G) mod BucketCount;
  122. with FBuckets[ABucket] do
  123. for I := 0 to Count - 1 do
  124. if Items[I].Key = AKey then
  125. begin
  126. AIndex := I;
  127. Result := True;
  128. Break;
  129. end;
  130. end;
  131. procedure TSimpleBucketList.Add(AKey: TColor32; const AData: string);
  132. var
  133. Bucket, Index, Delta, Size: Integer;
  134. begin
  135. if not FindItem(AKey, Bucket, Index) then
  136. with FBuckets[Bucket] do
  137. begin
  138. Size := Length(Items);
  139. if Count = Size then
  140. begin
  141. if Size > 64 then
  142. Delta := Size div 4
  143. else
  144. Delta := 16;
  145. SetLength(Items, Size + Delta);
  146. end;
  147. with Items[Count] do
  148. begin
  149. Key := AKey;
  150. Data := ShortString(AData);
  151. end;
  152. Inc(Count);
  153. Inc(FItemCount);
  154. end;
  155. end;
  156. function TSimpleBucketList.Exists(AKey: TColor32): Boolean;
  157. var
  158. Bucket, Index: Integer;
  159. begin
  160. Result := FindItem(AKey, Bucket, Index);
  161. end;
  162. {
  163. TXPMFileFormat implementation
  164. }
  165. procedure TXPMFileFormat.Define;
  166. begin
  167. inherited;
  168. FName := SXPMFormatName;
  169. FFeatures := [ffLoad, ffSave];
  170. FSupportedFormats := XPMSupportedFormats;
  171. AddMasks(SXPMMasks);
  172. end;
  173. function TXPMFileFormat.LoadData(Handle: TImagingHandle;
  174. var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean;
  175. var
  176. Contents, PalLookup: TStringList;
  177. S: AnsiString;
  178. I, J, NumColors, Cpp, Line: Integer;
  179. procedure SkipWhiteSpace(var Line: string);
  180. begin
  181. while (Length(Line) > 0) and (AnsiChar(Line[1]) in WhiteSpaces) do
  182. Delete(Line, 1, 1);
  183. end;
  184. function ReadString(var Line: string): string;
  185. begin
  186. Result := '';
  187. SkipWhiteSpace(Line);
  188. while (Length(Line) > 0) and not (AnsiChar(Line[1]) in WhiteSpaces) do
  189. begin
  190. SetLength(Result, Length(Result) + 1);
  191. Result[Length(Result)] := Line[1];
  192. Delete(Line, 1, 1);
  193. end;
  194. end;
  195. function ReadInt(var Line: string): Integer;
  196. begin
  197. Result := StrToInt(ReadString(Line));
  198. end;
  199. function ParseHeader: Boolean;
  200. var
  201. S: string;
  202. begin
  203. S := Contents[0];
  204. try
  205. Images[0].Width := ReadInt(S);
  206. Images[0].Height := ReadInt(S);
  207. NumColors := ReadInt(S);
  208. Cpp := ReadInt(S);
  209. Line := 1;
  210. Result := True;
  211. except
  212. Result := False;
  213. end;
  214. end;
  215. function NamedToColor(const ColStr: string): TColor32;
  216. var
  217. S: string;
  218. begin
  219. S := LowerCase(ColStr);
  220. if (S = 'transparent') or (S = 'none') then
  221. Result := pcClear
  222. else if S = 'black' then
  223. Result := pcBlack
  224. else if S = 'blue' then
  225. Result := pcBlue
  226. else if S = 'green' then
  227. Result := pcGreen
  228. else if S = 'cyan' then
  229. Result := pcAqua
  230. else if S = 'red' then
  231. Result := pcRed
  232. else if S = 'magenta' then
  233. Result := pcFuchsia
  234. else if S = 'yellow' then
  235. Result := pcYellow
  236. else if S = 'white' then
  237. Result := pcWhite
  238. else if S = 'gray' then
  239. Result := pcLtGray
  240. else if S = 'dkblue' then
  241. Result := pcNavy
  242. else if S = 'dkgreen' then
  243. Result := pcGreen
  244. else if S = 'dkcyan' then
  245. Result := pcTeal
  246. else if S = 'dkred' then
  247. Result := pcMaroon
  248. else if S = 'dkmagenta' then
  249. Result := pcPurple
  250. else if S = 'dkyellow' then
  251. Result := pcOlive
  252. else if S = 'maroon' then
  253. Result := pcMaroon
  254. else if S = 'olive' then
  255. Result := pcOlive
  256. else if S = 'navy' then
  257. Result := pcNavy
  258. else if S = 'purple' then
  259. Result := pcPurple
  260. else if S = 'teal' then
  261. Result := pcTeal
  262. else if S = 'silver' then
  263. Result := pcSilver
  264. else if S = 'lime' then
  265. Result := pcLime
  266. else if S = 'fuchsia' then
  267. Result := pcFuchsia
  268. else if S = 'aqua' then
  269. Result := pcAqua
  270. else
  271. Result := pcClear;
  272. end;
  273. procedure ParsePalette;
  274. var
  275. I: Integer;
  276. S, ColType, ColStr, Code: string;
  277. Color: TColor32;
  278. Holder: TColorHolder;
  279. begin
  280. for I := 0 to NumColors - 1 do
  281. begin
  282. Holder := TColorHolder.Create;
  283. // Parse pixel code and color
  284. S := Contents[Line + I];
  285. Code := Copy(S, 1, Cpp);
  286. Delete(S, 1, Cpp);
  287. ColType := ReadString(S);
  288. ColStr := ReadString(S);
  289. // Convert color from hex number or named constant
  290. if ColStr[1] = '#' then
  291. begin
  292. Delete(ColStr, 1, 1);
  293. Color := UInt32(StrToInt('$' + Trim(ColStr))) or $FF000000;
  294. end
  295. else
  296. Color := NamedToColor(ColStr);
  297. // Store code and color in table for later lookup
  298. Holder.Color := Color;
  299. PalLookup.AddObject(Code, Holder);
  300. end;
  301. Inc(Line, NumColors);
  302. end;
  303. procedure ParsePixels;
  304. var
  305. X, Y, Idx: Integer;
  306. S, Code: string;
  307. Pix: PColor32;
  308. begin
  309. Pix := Images[0].Bits;
  310. for Y := 0 to Images[0].Height - 1 do
  311. begin
  312. S := Contents[Line + Y];
  313. for X := 0 to Images[0].Width - 1 do
  314. begin
  315. // Read code and look up color in the palette
  316. Code := Copy(S, X * Cpp + 1, Cpp);
  317. if PalLookup.Find(Code, Idx) then
  318. Pix^ := TColorHolder(PalLookup.Objects[Idx]).Color
  319. else
  320. Pix^ := pcClear;
  321. Inc(Pix);
  322. end;
  323. end;
  324. end;
  325. begin
  326. Result := False;
  327. SetLength(Images, 1);
  328. with GetIO, Images[0] do
  329. begin
  330. // Look up table for XPM palette entries
  331. PalLookup := TStringList.Create;
  332. PalLookup.Sorted := True;
  333. PalLookup.CaseSensitive := True;
  334. // Read whole file and assign it to string list
  335. Contents := TStringList.Create;
  336. SetLength(S, GetInputSize(GetIO, Handle));
  337. Read(Handle, @S[1], Length(S));
  338. Contents.Text := string(S);
  339. // Remove quotes and other stuff
  340. for I := Contents.Count - 1 downto 0 do
  341. begin
  342. J := Pos('"', Contents[I]);
  343. if J > 0 then
  344. Contents[I] := Copy(Contents[I], J + 1, LastDelimiter('"', Contents[I]) - J - 1)
  345. else
  346. Contents.Delete(I);
  347. end;
  348. // Parse header and create new image
  349. if not ParseHeader then
  350. Exit;
  351. NewImage(Width, Height, ifA8R8G8B8, Images[0]);
  352. // Read palette entries and assign colors to pixels
  353. ParsePalette;
  354. ParsePixels;
  355. Contents.Free;
  356. for I := 0 to PalLookup.Count - 1 do
  357. PalLookup.Objects[I].Free;
  358. PalLookup.Free;
  359. Result := True;
  360. end;
  361. end;
  362. function TXPMFileFormat.SaveData(Handle: TImagingHandle;
  363. const Images: TDynImageDataArray; Index: LongInt): Boolean;
  364. const
  365. ColorCharsCount = 92;
  366. ColorChars = ' .XoO+@#$%&*=-;:>,<1234567890qwertyuipasdfghjklzxcvbnmMNBVCZASDFGHJKLPIUYTREWQ!~^/()_`''][{}|';
  367. var
  368. X, Y: Integer;
  369. ImageToSave: TImageData;
  370. MustBeFreed: Boolean;
  371. StrFile: TStringList;
  372. ColTable: TSimpleBucketList;
  373. Stream: TMemoryStream;
  374. Line, Id: string;
  375. CharsPerPixel: Integer;
  376. Ptr: PColor32Rec;
  377. ColRec: TColor32Rec;
  378. procedure BuildColorTables(const Img: TImageData);
  379. var
  380. I: Integer;
  381. begin
  382. Ptr := Img.Bits;
  383. for I := 0 to Img.Width * Img.Height - 1 do
  384. begin
  385. if not ColTable.Exists(Ptr.Color) then
  386. ColTable.Add(Ptr.Color, '');
  387. Inc(Ptr);
  388. end;
  389. end;
  390. procedure MakeStrIdsForColors;
  391. var
  392. I, J, K: Integer;
  393. Id, Data: string;
  394. begin
  395. SetLength(Id, CharsPerPixel);
  396. for I := 0 to ColTable.ItemCount - 1 do
  397. begin
  398. ColRec.Color := ColTable.EnumNext(Data);
  399. K := I;
  400. for J := 0 to CharsPerPixel - 1 do
  401. begin
  402. Id[J + 1] := ColorChars[K mod ColorCharsCount + 1];
  403. K := K div ColorCharsCount;
  404. end;
  405. ColTable.Data[ColRec.Color] := Id;
  406. end;
  407. end;
  408. begin
  409. Result := False;
  410. StrFile := TStringList.Create;
  411. ColTable := TSimpleBucketList.Create;
  412. Stream := TMemoryStream.Create;
  413. if MakeCompatible(Images[Index], ImageToSave, MustBeFreed) then
  414. try
  415. // Put all unique colors of image to table
  416. BuildColorTables(ImageToSave);
  417. // Compute the character per pixel
  418. CharsPerPixel := 1;
  419. X := ColorCharsCount;
  420. while ColTable.ItemCount > X do
  421. begin
  422. X := X * ColorCharsCount;
  423. Inc(CharsPerPixel);
  424. end;
  425. // Assign char id to each color
  426. MakeStrIdsForColors;
  427. // Start writing XPM file
  428. StrFile.Add(SXPMId);
  429. StrFile.Add('static char *graphic[] = {');
  430. StrFile.Add('/* width height num_colors chars_per_pixel */');
  431. StrFile.Add(SysUtils.Format('"%d %d %d %d", ', [ImageToSave.Width,
  432. ImageToSave.Height, ColTable.ItemCount, CharsPerPixel]));
  433. StrFile.Add('/* colors */');
  434. // Write 'colors' part of XPM file
  435. for X := 0 to ColTable.ItemCount - 1 do
  436. begin
  437. ColRec.Color := ColTable.EnumNext(Id);
  438. if ColRec.A >= 128 then
  439. StrFile.Add(Format('"%s c #%.2x%.2x%.2x",', [Id, ColRec.R, ColRec.G, ColRec.B]))
  440. else
  441. StrFile.Add(Format('"%s c None",', [Id]));
  442. end;
  443. StrFile.Add('/* pixels */');
  444. // Write pixels - for each pixel of image find its char id
  445. // and append it to line
  446. Ptr := ImageToSave.Bits;
  447. for Y := 0 to ImageToSave.Height - 1 do
  448. begin
  449. Line := '';
  450. for X := 0 to ImageToSave.Width - 1 do
  451. begin
  452. Line := Line + ColTable.Data[Ptr.Color];
  453. Inc(Ptr);
  454. end;
  455. Line := '"' + Line + '"';
  456. if Y < ImageToSave.Height - 1 then
  457. Line := Line + ',';
  458. StrFile.Add(Line);
  459. end;
  460. StrFile.Add('};');
  461. // Finally save strings to stream and write stream's data to output
  462. // (we could directly write lines from list to output, but stream method
  463. // takes care of D2009+ Unicode strings).
  464. StrFile.SaveToStream(Stream);
  465. GetIO.Write(Handle, Stream.Memory, Stream.Size);
  466. Result := True;
  467. finally
  468. StrFile.Free;
  469. ColTable.Free;
  470. Stream.Free;
  471. if MustBeFreed then
  472. FreeImage(ImageToSave);
  473. end;
  474. end;
  475. procedure TXPMFileFormat.ConvertToSupported(var Image: TImageData;
  476. const Info: TImageFormatInfo);
  477. begin
  478. ConvertImage(Image, ifA8R8G8B8)
  479. end;
  480. function TXPMFileFormat.TestFormat(Handle: TImagingHandle): Boolean;
  481. var
  482. Id: array[0 .. 8] of AnsiChar;
  483. ReadCount: Integer;
  484. begin
  485. Result := False;
  486. if Handle <> nil then
  487. begin
  488. ReadCount := GetIO.Read(Handle, @Id, SizeOf(Id));
  489. GetIO.Seek(Handle, -ReadCount, smFromCurrent);
  490. Result := (Id = SXPMId) and (ReadCount = SizeOf(Id));
  491. end;
  492. end;
  493. initialization
  494. RegisterImageFileFormat(TXPMFileFormat);
  495. {
  496. File Notes:
  497. -- TODOS ----------------------------------------------------
  498. - nothing now
  499. -- 0.26.3 Changes/Bug Fixes -----------------------------------
  500. - Added XPM saving.
  501. -- 0.25.0 Changes/Bug Fixes -----------------------------------
  502. - Added XPM loading.
  503. - Unit created.
  504. }
  505. end.