ImagingXpm.pas 15 KB

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