fpditherer.pas 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. {*****************************************************************************}
  2. {
  3. This file is part of the Free Pascal's "Free Components Library".
  4. Copyright (c) 2005 by Giulio Bernardi
  5. This file contains classes used to dither images.
  6. See the file COPYING.FPC, included in this distribution,
  7. for details about the copyright.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  11. }
  12. {*****************************************************************************}
  13. {$mode objfpc}{$h+}
  14. unit FPDitherer;
  15. interface
  16. uses sysutils, classes, fpimage, fpcolhash;
  17. type
  18. FPDithererException = class (exception);
  19. type
  20. TFPDithererProgressEvent = procedure (Sender: TObject; Stage: TFPImgProgressStage; PercentDone: Byte;
  21. const Msg: AnsiString; var Continue : Boolean) of object;
  22. type
  23. TFPBaseDitherer = class
  24. private
  25. FPalette : TFPPalette;
  26. FOnProgress : TFPDithererProgressEvent;
  27. procedure QuickSort(const l, r : integer);
  28. protected
  29. FImage : TFPCustomImage;
  30. FHashMap : TFPColorHashTable;
  31. FSorted : boolean;
  32. FUseHash : boolean;
  33. FUseAlpha : boolean;
  34. function ColorCompare(const c1, c2 : TFPColor) : shortint;
  35. function GetColorDinst(const c1, c2 : TFPColor) : integer;
  36. function SubtractColorInt(const c1, c2 : TFPColor) : int64;
  37. function SubtractColor(const c1, c2 : TFPColor) : TFPColor;
  38. procedure InternalDither(const Source : TFPCustomImage; Dest : TFPCustomImage); virtual;
  39. function FindBestColor(OrigColor : TFPColor; var PalIndex : integer) : integer; virtual;
  40. procedure Progress (Sender: TObject; Stage: TFPImgProgressStage; PercentDone: Byte; const Msg: AnsiString; var Continue : Boolean); virtual;
  41. procedure SetUseHash(Value : boolean); virtual;
  42. procedure SetSorted(Value : boolean); virtual;
  43. public
  44. property OnProgress : TFPDithererProgressEvent read FOnProgress write FOnProgress;
  45. property Palette : TFPPalette read FPalette;
  46. property PaletteSorted : boolean read FSorted write SetSorted;
  47. property UseHashMap : boolean read FUseHash write SetUseHash;
  48. property UseAlpha : boolean read FUseAlpha write FUseAlpha;
  49. procedure Dither(const Source : TFPCustomImage; Dest : TFPCustomImage);
  50. procedure SortPalette; virtual;
  51. constructor Create(ThePalette : TFPPalette); virtual;
  52. destructor Destroy; override;
  53. end;
  54. type
  55. PFPPixelReal = ^TFPPixelReal;
  56. TFPPixelReal = record { pixel in real form }
  57. a, r, g, b : real;
  58. end;
  59. PFSPixelLine = ^TFSPixelLine;
  60. TFSPixelLine = record
  61. pixels : PFPPixelReal; { a line of pixels }
  62. Next : PFSPixelLine; { next line of pixels }
  63. end;
  64. type
  65. TFPFloydSteinbergDitherer = class(TFPBaseDitherer)
  66. private
  67. Lines : PFSPixelLine;
  68. function Color2Real(const c : TFPColor) : TFPPixelReal;
  69. function Real2Color(r : TFPPixelReal) : TFPColor;
  70. procedure CreatePixelLine(var line : PFSPixelLine; const row : integer);
  71. function GetError(const c1, c2 : TFPColor) : TFPPixelReal;
  72. procedure DistributeErrors(var line : PFSPixelLine; const row : integer; Img : TFPCustomImage);
  73. procedure DeleteAllPixelLines(var line : PFSPixelLine);
  74. protected
  75. procedure InternalDither(const Source : TFPCustomImage; Dest : TFPCustomImage); override;
  76. public
  77. constructor Create(ThePalette : TFPPalette); override;
  78. end;
  79. implementation
  80. { TFPBaseDitherer }
  81. procedure TFPBaseDitherer.Dither(const Source : TFPCustomImage; Dest : TFPCustomImage);
  82. begin
  83. if FPalette.Count=0 then
  84. raise FPDithererException.Create('Palette is empty');
  85. if Source=Dest then
  86. raise FPDithererException.Create('Source and Destination images must be different');
  87. InternalDither(Source,Dest);
  88. if FUseHash then
  89. FHashMap.Clear;
  90. end;
  91. constructor TFPBaseDitherer.Create(ThePalette : TFPPalette);
  92. begin
  93. FSorted:=false;
  94. FUseAlpha:=false;
  95. FImage:=nil;
  96. FPalette:=ThePalette;
  97. FUseHash:=true;
  98. FHashMap:=TFPColorHashTable.Create;
  99. end;
  100. destructor TFPBaseDitherer.Destroy;
  101. begin
  102. if Assigned(FHashMap) then
  103. FHashMap.Free;
  104. end;
  105. procedure TFPBaseDitherer.SetUseHash(Value : boolean);
  106. begin
  107. if Value=FUseHash then exit;
  108. if Value then
  109. FHashMap:=TFPColorHashTable.Create
  110. else
  111. begin
  112. FHashMap.Free;
  113. FHashMap:=nil;
  114. end;
  115. FUseHash:=Value;
  116. end;
  117. procedure TFPBaseDitherer.SetSorted(Value : boolean);
  118. begin
  119. FSorted:=Value;
  120. end;
  121. procedure TFPBaseDitherer.Progress(Sender: TObject; Stage: TFPImgProgressStage; PercentDone: Byte; const Msg: AnsiString; var Continue : Boolean);
  122. begin
  123. if Assigned(FOnProgress) then
  124. FOnProgress(Sender,Stage,PercentDone,Msg,Continue);
  125. end;
  126. { rgb triplets are considered like a number having msb in msb(r) and lsb in lsb(b) }
  127. function TFPBaseDitherer.SubtractColorInt(const c1, c2 : TFPColor) : int64;
  128. var whole1, whole2 : int64;
  129. begin
  130. whole1:= ((c1.Red and $FF00) shl 8) or (c1.Green and $FF00) or ((c1.Blue and $FF00) shr 8);
  131. whole2:= ((c2.Red and $FF00) shl 8) or (c2.Green and $FF00) or ((c2.Blue and $FF00) shr 8);
  132. if FUseAlpha then
  133. begin
  134. whole1:=whole1 or ((c1.Alpha and $FF00) shl 16);
  135. whole2:=whole2 or ((c2.Alpha and $FF00) shl 16);
  136. end;
  137. Result:= whole1 - whole2;
  138. end;
  139. { this is more efficient than calling subtractcolorint and then extracting r g b values }
  140. function TFPBaseDitherer.GetColorDinst(const c1, c2 : TFPColor) : integer;
  141. var dinst : integer;
  142. begin
  143. dinst:=abs(((c1.Red and $FF00) shr 8) - ((c2.Red and $FF00) shr 8));
  144. dinst:=dinst+abs(((c1.Green and $FF00) shr 8) - ((c2.Green and $FF00) shr 8));
  145. dinst:=dinst+abs(((c1.Blue and $FF00) shr 8) - ((c2.Blue and $FF00) shr 8));
  146. if FUseAlpha then
  147. dinst:=dinst+abs(((c1.Alpha and $FF00) shr 8) - ((c2.Alpha and $FF00) shr 8));
  148. Result:= dinst;
  149. end;
  150. function TFPBaseDitherer.SubtractColor(const c1, c2 : TFPColor) : TFPColor;
  151. var whole : int64;
  152. begin
  153. whole:=abs(SubtractColorInt(c1,c2));
  154. if FUseALpha then
  155. Result.Alpha:=(whole and $FF000000) shr 16
  156. else
  157. Result.Alpha:=AlphaOpaque;
  158. Result.Red:=(whole and $00FF0000) shr 8;
  159. Result.Green:=(whole and $0000FF00);
  160. Result.Blue:=(whole and $000000FF) shl 8;
  161. end;
  162. function TFPBaseDitherer.ColorCompare(const c1, c2 : TFPColor) : shortint;
  163. var whole : int64;
  164. begin
  165. whole:=SubtractColorInt(c1,c2);
  166. if whole>0 then Result:=1
  167. else if whole<0 then Result:=-1
  168. else Result:=0;
  169. end;
  170. procedure TFPBaseDitherer.QuickSort(const l, r : integer);
  171. var i, j : integer;
  172. pivot, temp : TFPColor;
  173. begin
  174. if l<r then
  175. begin
  176. pivot:=FPalette[l];
  177. i:=l+1;
  178. j:=r;
  179. repeat
  180. while ((i<=r) and (ColorCompare(FPalette[i],pivot)<=0)) do
  181. inc(i);
  182. while (ColorCompare(FPalette[j],pivot)=1) do
  183. dec(j);
  184. if i<j then
  185. begin
  186. temp:=FPalette[i];
  187. FPalette[i]:=FPalette[j];
  188. FPalette[j]:=temp;
  189. end;
  190. until i > j;
  191. { don't swap if they are equal }
  192. if ColorCompare(FPalette[j],pivot)<>0 then
  193. begin
  194. Fpalette[l]:=Fpalette[j];
  195. Fpalette[j]:=pivot;
  196. end;
  197. Quicksort(l,j-1);
  198. Quicksort(i,r);
  199. end;
  200. end;
  201. procedure TFPBaseDitherer.SortPalette;
  202. begin
  203. QuickSort(0,FPalette.Count-1);
  204. FSorted:=true;
  205. end;
  206. type
  207. PBestColorData = ^TBestColorData;
  208. TBestColorData = record
  209. palindex, dinst : integer;
  210. end;
  211. function TFPBaseDitherer.FindBestColor(OrigColor : TFPColor; var PalIndex : integer) : integer;
  212. var i, curr, dinst, tmpdinst, top, bottom : integer;
  213. hashval : PBestColorData;
  214. begin
  215. dinst:=$7FFFFFFF;
  216. curr:=0;
  217. if FUseHash then { use the hashmap to improve speed }
  218. begin
  219. hashval:=FHashMap.Get(OrigColor);
  220. if hashval<>nil then
  221. begin
  222. PalIndex:=hashval^.palindex;
  223. Result:=hashval^.dinst;
  224. exit;
  225. end;
  226. end;
  227. { with a sorted palette, proceed by binary search. this is more efficient with large images or large palettes }
  228. if FSorted then
  229. begin
  230. top:=0;
  231. bottom:=FPalette.Count-1;
  232. while top<=bottom do
  233. begin
  234. i:=(bottom+top) div 2;
  235. tmpdinst:=ColorCompare(OrigColor,Fpalette[i]);
  236. if tmpdinst<0 then bottom:=i-1
  237. else if tmpdinst>0 then top:=i+1
  238. else break; { we found it }
  239. end;
  240. curr:=i;
  241. dinst:=GetColorDinst(OrigColor,Fpalette[i]);
  242. end
  243. else
  244. for i:=0 to FPalette.Count-1 do
  245. begin
  246. tmpdinst:=GetColorDinst(OrigColor,FPalette[i]);
  247. if tmpdinst<dinst then
  248. begin
  249. dinst:=tmpdinst;
  250. curr:=i;
  251. end;
  252. if tmpdinst=0 then break; { There can't be anything better, stop searching }
  253. end;
  254. if FUseHash then { if we are using a hashmap, remember this value}
  255. begin
  256. hashval:=GetMem(sizeof(TBestColorData));
  257. if hashval=nil then
  258. raise FPDithererException.Create('Out of memory');
  259. hashval^.PalIndex:=curr;
  260. hashval^.dinst:=dinst;
  261. FHashMap.Insert(OrigColor,hashval);
  262. end;
  263. PalIndex:=curr;
  264. Result:=dinst;
  265. end;
  266. procedure TFPBaseDitherer.InternalDither(const Source : TFPCustomImage; Dest : TFPCustomImage);
  267. var i,j, palindex : integer;
  268. percent : byte;
  269. percentinterval : longword;
  270. percentacc : longword;
  271. FContinue : boolean;
  272. begin
  273. FImage:=Source;
  274. percent:=0;
  275. percentinterval:=(FImage.Width*FImage.Height*4) div 100;
  276. if percentinterval=0 then percentinterval:=$FFFFFFFF;
  277. percentacc:=0;
  278. FContinue:=true;
  279. Progress (self,psStarting,0,'',FContinue);
  280. Dest.SetSize(0,0);
  281. Dest.UsePalette:=true;
  282. Dest.Palette.Clear;
  283. Dest.Palette.Merge(FPalette);
  284. Dest.SetSize(FImage.Width,FImage.Height);
  285. for j:=0 to FImage.Height-1 do
  286. for i:=0 to FImage.Width-1 do
  287. begin
  288. FindBestColor(FImage[i,j], palindex);
  289. Dest.Pixels[i,j]:=palindex;
  290. inc(percentacc,4);
  291. if percentacc>=percentinterval then
  292. begin
  293. percent:=percent+(percentacc div percentinterval);
  294. percentacc:=percentacc mod percentinterval;
  295. Progress (self,psRunning,percent,'',FContinue);
  296. if not fcontinue then exit;
  297. end;
  298. end;
  299. Progress (self,psEnding,100,'',FContinue);
  300. end;
  301. { TFPFloydSteinbergDitherer }
  302. const FSNullPixel : TFPPixelReal = (a : 0.0; r : 0.0; g : 0.0; b : 0.0);
  303. constructor TFPFloydSteinbergDitherer.Create(ThePalette : TFPPalette);
  304. begin
  305. inherited Create(ThePalette);
  306. Lines:=nil;
  307. end;
  308. function TFPFloydSteinbergDitherer.GetError(const c1, c2 : TFPColor) : TFPPixelReal;
  309. var temp : TFPPixelReal;
  310. begin
  311. if FUseAlpha then
  312. temp.a:=((c1.Alpha and $FF00) shr 8) - ((c2.Alpha and $FF00) shr 8);
  313. temp.r:=((c1.Red and $FF00) shr 8) - ((c2.Red and $FF00) shr 8);
  314. temp.g:=((c1.Green and $FF00) shr 8) - ((c2.Green and $FF00) shr 8);
  315. temp.b:=((c1.Blue and $FF00) shr 8) - ((c2.Blue and $FF00) shr 8);
  316. Result:=temp;
  317. end;
  318. function TFPFloydSteinbergDitherer.Color2Real(const c : TFPColor) : TFPPixelReal;
  319. var temp : TFPPixelReal;
  320. begin
  321. if FUseAlpha then
  322. temp.a:=((c.Alpha and $FF00) shr 8);
  323. temp.r:=((c.Red and $FF00) shr 8);
  324. temp.g:=((c.Green and $FF00) shr 8);
  325. temp.b:=((c.Blue and $FF00) shr 8);
  326. Result:=temp;
  327. end;
  328. function TFPFloydSteinbergDitherer.Real2Color(r : TFPPixelReal) : TFPColor;
  329. var temp : TFPColor;
  330. begin
  331. { adjust overflows and underflows }
  332. if r.r> 255 then r.r:=255; if r.r<0 then r.r:=0;
  333. if r.g> 255 then r.g:=255; if r.g<0 then r.g:=0;
  334. if r.b> 255 then r.b:=255; if r.b<0 then r.b:=0;
  335. if FUseAlpha then
  336. begin
  337. if r.a> 255 then r.a:=255; if r.a<0 then r.a:=0;
  338. end;
  339. temp.Red:=round(r.r);
  340. temp.Red:=(temp.Red shl 8) + temp.Red;
  341. temp.Green:=round(r.g);
  342. temp.Green:=(temp.Green shl 8) + temp.Green;
  343. temp.Blue:=round(r.b);
  344. temp.Blue:=(temp.Blue shl 8) + temp.Blue;
  345. if FUseAlpha then
  346. begin
  347. temp.Alpha:=round(r.a);
  348. temp.Alpha:=(temp.Alpha shl 8) + temp.Alpha;
  349. end
  350. else
  351. temp.Alpha:=AlphaOpaque;
  352. Result:=temp;
  353. end;
  354. procedure TFPFloydSteinbergDitherer.CreatePixelLine(var line : PFSPixelLine; const row : integer);
  355. var i : integer;
  356. begin
  357. line:=GetMem(sizeof(TFSPixelLine));
  358. if line=nil then
  359. raise FPDithererException.Create('Out of memory');
  360. line^.next:=nil;
  361. { two extra pixels so we don't have to check if the pixel is on start or end of line }
  362. getmem(line^.pixels,sizeof(TFPPixelReal)*(FImage.Width+2));
  363. if line^.pixels=nil then
  364. raise FPDithererException.Create('Out of memory');
  365. if row<FImage.Height-1 then
  366. begin
  367. line^.pixels[0]:=FSNullPixel;
  368. line^.pixels[FImage.Width+1]:=FSNullPixel;
  369. for i:=0 to FImage.Width-1 do
  370. line^.pixels[i+1]:=Color2Real(FImage[i,row]);
  371. end
  372. else
  373. for i:=0 to FImage.Width+1 do
  374. line^.pixels[i]:=FSNullPixel;
  375. end;
  376. const e716 = 0.4375;
  377. e516 = 0.3125;
  378. e316 = 0.1875;
  379. e116 = 0.0625;
  380. procedure TFPFloydSteinbergDitherer.DistributeErrors(var line : PFSPixelLine; const row : integer; Img : TFPCustomImage);
  381. var i, width : integer;
  382. palindex : integer;
  383. OldColor : TFPColor;
  384. dir : shortint;
  385. nextline : PFSPixelLine;
  386. begin
  387. width:=FImage.Width;
  388. if (row mod 2)=0 then
  389. begin
  390. dir:=1;
  391. i:=1;
  392. end
  393. else
  394. begin
  395. dir:=-1;
  396. i:=width;
  397. end;
  398. if width<1 then exit;
  399. repeat
  400. OldColor:=Real2Color(line^.pixels[i]);
  401. FindBestColor(OldColor, palindex);
  402. Img.Pixels[i-1,row]:=palindex; { we use this color for this pixel... }
  403. line^.pixels[i]:=GetError(OldColor,Palette[palindex]);
  404. { now distribute this error to the other pixels, in this way: }
  405. { note: for odd lines this is mirrored and we start from right}
  406. { 0 0 0 }
  407. { 0 X 7/16 }
  408. { 3/16 5/16 1/16 }
  409. line^.pixels[i+dir].r:=line^.pixels[i+dir].r+(line^.pixels[i].r*e716);
  410. line^.pixels[i+dir].g:=line^.pixels[i+dir].g+(line^.pixels[i].g*e716);
  411. line^.pixels[i+dir].b:=line^.pixels[i+dir].b+(line^.pixels[i].b*e716);
  412. if FUseAlpha then
  413. line^.pixels[i+dir].a:=line^.pixels[i+dir].a+(line^.pixels[i].a*e716);
  414. nextline:=line^.next;
  415. nextline^.pixels[i].r:=nextline^.pixels[i].r+(line^.pixels[i].r*e516);
  416. nextline^.pixels[i].g:=nextline^.pixels[i].g+(line^.pixels[i].g*e516);
  417. nextline^.pixels[i].b:=nextline^.pixels[i].b+(line^.pixels[i].b*e516);
  418. if FUseAlpha then
  419. nextline^.pixels[i].a:=nextline^.pixels[i].a+(line^.pixels[i].a*e516);
  420. nextline^.pixels[i+dir].r:=nextline^.pixels[i+dir].r+(line^.pixels[i].r*e116);
  421. nextline^.pixels[i+dir].g:=nextline^.pixels[i+dir].g+(line^.pixels[i].g*e116);
  422. nextline^.pixels[i+dir].b:=nextline^.pixels[i+dir].b+(line^.pixels[i].b*e116);
  423. if FUseAlpha then
  424. nextline^.pixels[i+dir].a:=nextline^.pixels[i+dir].a+(line^.pixels[i].a*e116);
  425. nextline^.pixels[i-dir].r:=nextline^.pixels[i-dir].r+(line^.pixels[i].r*e316);
  426. nextline^.pixels[i-dir].g:=nextline^.pixels[i-dir].g+(line^.pixels[i].g*e316);
  427. nextline^.pixels[i-dir].b:=nextline^.pixels[i-dir].b+(line^.pixels[i].b*e316);
  428. if FUseAlpha then
  429. nextline^.pixels[i-dir].a:=nextline^.pixels[i-dir].a+(line^.pixels[i].a*e316);
  430. i:=i+dir;
  431. until ((i<1) or (i>width));
  432. end;
  433. procedure TFPFloydSteinbergDitherer.DeleteAllPixelLines(var line : PFSPixelLine);
  434. var tmp : PFSPixelLine;
  435. begin
  436. while line<>nil do
  437. begin
  438. tmp:=line^.next;
  439. FreeMem(line^.pixels);
  440. FreeMem(line);
  441. line:=tmp;
  442. end;
  443. end;
  444. procedure TFPFloydSteinbergDitherer.InternalDither(const Source : TFPCustomImage; Dest : TFPCustomImage);
  445. var i : integer;
  446. tmpline : PFSPixelLine;
  447. percent : byte;
  448. percentinterval : longword;
  449. percentacc : longword;
  450. FContinue : boolean;
  451. begin
  452. FImage:=Source;
  453. if FImage.Height=0 then exit;
  454. Dest.SetSize(0,0);
  455. try
  456. Dest.UsePalette:=true;
  457. Dest.Palette.Clear;
  458. Dest.Palette.Merge(FPalette);
  459. Dest.SetSize(FImage.Width,FImage.Height);
  460. percent:=0;
  461. percentinterval:=(FImage.Height*4) div 100;
  462. if percentinterval=0 then percentinterval:=$FFFFFFFF;
  463. percentacc:=0;
  464. FContinue:=true;
  465. Progress (self,psStarting,0,'',FContinue);
  466. if not FContinue then exit;
  467. CreatePixelLine(Lines,0);
  468. CreatePixelLine(Lines^.next,1);
  469. for i:=0 to FImage.Height-1 do
  470. begin
  471. DistributeErrors(Lines, i, Dest);
  472. tmpline:=Lines;
  473. Lines:=Lines^.next;
  474. FreeMem(tmpline^.pixels);
  475. FreeMem(tmpline);
  476. CreatePixelLine(Lines^.next,i+2);
  477. inc(percentacc,4);
  478. if percentacc>=percentinterval then
  479. begin
  480. percent:=percent+(percentacc div percentinterval);
  481. percentacc:=percentacc mod percentinterval;
  482. Progress (self,psRunning,percent,'',FContinue);
  483. if not FContinue then exit;
  484. end;
  485. end;
  486. Progress (self,psEnding,100,'',FContinue);
  487. finally
  488. DeleteAllPixelLines(lines);
  489. end;
  490. end;
  491. end.