unit Img32.Extra; (******************************************************************************* * Author : Angus Johnson * * Version : 4.8 * * Date : 10 January 2025 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2019-2025 * * Purpose : Miscellaneous routines that don't belong in other modules. * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface {$I Img32.inc} uses SysUtils, Classes, Math, Types, Img32, Img32.Draw, Img32.Vector; type TButtonShape = (bsRound, bsSquare, bsDiamond); TButtonAttribute = (baShadow, ba3D, baEraseBeneath); TButtonAttributes = set of TButtonAttribute; procedure DrawEdge(img: TImage32; const rec: TRect; topLeftColor, bottomRightColor: TColor32; penWidth: double = 1.0); overload; procedure DrawEdge(img: TImage32; const rec: TRectD; topLeftColor, bottomRightColor: TColor32; penWidth: double = 1.0); overload; procedure DrawEdge(img: TImage32; const path: TPathD; topLeftColor, bottomRightColor: TColor32; penWidth: double = 1.0; closePath: Boolean = true); overload; //DrawShadowRect: is **much** faster than DrawShadow procedure DrawShadowRect(img: TImage32; const rec: TRect; depth: double; angle: double = angle45; color: TColor32 = $80000000); procedure DrawShadow(img: TImage32; const polygon: TPathD; fillRule: TFillRule; depth: double; angleRads: double = angle45; color: TColor32 = $80000000; cutoutInsideShadow: Boolean = false); overload; procedure DrawShadow(img: TImage32; const polygons: TPathsD; fillRule: TFillRule; depth: double; angleRads: double = angle45; color: TColor32 = $80000000; cutoutInsideShadow: Boolean = false); overload; procedure DrawGlow(img: TImage32; const polygon: TPathD; fillRule: TFillRule; color: TColor32; blurRadius: integer); overload; procedure DrawGlow(img: TImage32; const polygons: TPathsD; fillRule: TFillRule; color: TColor32; blurRadius: integer); overload; //FloodFill: If no CompareFunc is provided, FloodFill will fill whereever //adjoining pixels exactly match the starting pixel - Point(x,y). procedure FloodFill(img: TImage32; x, y: Integer; newColor: TColor32; tolerance: Byte = 0; compareFunc: TCompareFunctionEx = nil); procedure FastGaussianBlur(img: TImage32; const rec: TRect; stdDev: integer; repeats: integer = 2); overload; procedure FastGaussianBlur(img: TImage32; const rec: TRect; stdDevX, stdDevY: integer; repeats: integer); overload; procedure GaussianBlur(img: TImage32; rec: TRect; radius: Integer); //Emboss: A smaller radius is sharper. Increasing depth increases contrast. //Luminance changes grayscale balance (unless preserveColor = true) procedure Emboss(img: TImage32; radius: Integer = 1; depth: Integer = 10; luminance: Integer = 75; preserveColor: Boolean = false); //Sharpen: Radius range is 1 - 10; amount range is 1 - 50.
//see https://en.wikipedia.org/wiki/Unsharp_masking procedure Sharpen(img: TImage32; radius: Integer = 2; amount: Integer = 10); //HatchBackground: Assumes the current image is semi-transparent. procedure HatchBackground(img: TImage32; color1: TColor32 = clWhite32; color2: TColor32= $FFE8E8E8; hatchSize: Integer = 10); overload; procedure HatchBackground(img: TImage32; const rec: TRect; color1: TColor32 = clWhite32; color2: TColor32= $FFE8E8E8; hatchSize: Integer = 10); overload; procedure GridBackground(img: TImage32; majorInterval, minorInterval: integer; fillColor: TColor32 = clWhite32; majColor: TColor32 = $30000000; minColor: TColor32 = $20000000); procedure ReplaceExactColor(img: TImage32; oldColor, newColor: TColor32); //RemoveColor: Removes the specified color from the image, even from //pixels that are a blend of colors including the specified color.
//see https://stackoverflow.com/questions/9280902/ procedure RemoveColor(img: TImage32; color: TColor32); //FilterOnColor: Removes everything not nearly matching 'color' //This uses an algorithm that's very similar to the one in RemoveColor. procedure FilterOnColor(img: TImage32; color: TColor32); procedure FilterOnExactColor(img: TImage32; color: TColor32); procedure FilterOnAlpha(img: TImage32; alpha: byte; tolerance: byte); //RedEyeRemove: Removes 'red eye' from flash photo images. procedure RedEyeRemove(img: TImage32; const rect: TRect); procedure PencilEffect(img: TImage32; intensity: integer = 0); procedure TraceContours(img: TImage32; intensity: integer); procedure EraseInsidePath(img: TImage32; const path: TPathD; fillRule: TFillRule); procedure EraseInsidePaths(img: TImage32; const paths: TPathsD; fillRule: TFillRule); procedure EraseOutsidePath(img: TImage32; const path: TPathD; fillRule: TFillRule; const outsideBounds: TRect); procedure EraseOutsidePaths(img: TImage32; const paths: TPathsD; fillRule: TFillRule; const outsideBounds: TRect; rendererCache: TCustomRendererCache = nil); overload; procedure Draw3D(img: TImage32; const polygon: TPathD; fillRule: TFillRule; height, blurRadius: double; colorLt: TColor32 = $DDFFFFFF; colorDk: TColor32 = $80000000; angleRads: double = angle225); overload; procedure Draw3D(img: TImage32; const polygons: TPathsD; fillRule: TFillRule; height, blurRadius: double; colorLt: TColor32 = $DDFFFFFF; colorDk: TColor32 = $80000000; angleRads: double = angle225); overload; function RainbowColor(fraction: double; luminance: byte = 128): TColor32; function GradientColor(color1, color2: TColor32; frac: single): TColor32; function MakeDarker(color: TColor32; percent: cardinal): TColor32; function MakeLighter(color: TColor32; percent: cardinal): TColor32; function DrawButton(img: TImage32; const pt: TPointD; size: double; color: TColor32 = clNone32; buttonShape: TButtonShape = bsRound; buttonAttributes: TButtonAttributes = [baShadow, ba3D, baEraseBeneath]): TPathD; // RamerDouglasPeucker: simplifies paths, recursively removing vertices where // they deviate no more than 'epsilon' from their adjacent vertices. function RamerDouglasPeucker(const path: TPathD; epsilon: double): TPathD; overload; function RamerDouglasPeucker(const paths: TPathsD; epsilon: double): TPathsD; overload; {$IFDEF USE_OLD_SIMPLIFYPATHS} // SimplifyPath: Better than RDP when simplifying closed paths function SimplifyPath(const path: TPathD; shapeTolerance: double = 0.1; isOpenPath: Boolean = false): TPathD; function SimplifyPaths(const paths: TPathsD; shapeTolerance: double = 0.1; isOpenPath: Boolean = false): TPathsD; {$ELSE} // SimplifyPath: Better than RDP when simplifying closed paths function SimplifyPath(const path: TPathD; shapeTolerance: double = 0.1; isClosedPath: Boolean = true): TPathD; function SimplifyPaths(const paths: TPathsD; shapeTolerance: double = 0.1; isClosedPath: Boolean = true): TPathsD; {$ENDIF} // SimplifyPathEx: this is particularly useful following Vectorize() // because it also removes very short zig-zag segments function SimplifyPathEx(const path: TPathD; shapeTolerance: double): TPathD; function SimplifyPathsEx(const paths: TPathsD; shapeTolerance: double): TPathsD; // SmoothToCubicBezier and SmoothToCubicBezier2 have been deprecated in // favour of SmoothPath that's much simpler function SmoothToCubicBezier(const path: TPathD; pathIsClosed: Boolean; maxOffset: integer = 0): TPathD; overload; deprecated; function SmoothToCubicBezier(const paths: TPathsD; pathIsClosed: Boolean; maxOffset: integer = 0): TPathsD; overload; deprecated; function SmoothToCubicBezier2(const path: TPathD; pathIsClosed: Boolean; maxOffset: integer = 0): TPathD; overload; deprecated; function SmoothToCubicBezier2(const paths: TPathsD; pathIsClosed: Boolean; maxOffset: integer = 0): TPathsD; overload; deprecated; // SmoothPath - smooths a path using bicubic interpolation // tension (range -1 to 1): from least to most curve constraint function SmoothPath(const path: TPathD; isClosedPath: Boolean; tension: double = 0; shapeTolerance: double = 0.1): TPathD; function SmoothPaths(const paths: TPathsD; isClosedPath: Boolean; tension: double = 0; shapeTolerance: double = 0.1): TPathsD; function GetFloodFillMask(imgIn, imgMaskOut: TImage32; x, y: Integer; tolerance: Byte; compareFunc: TCompareFunctionEx): Boolean; procedure SymmetricCropTransparent(img: TImage32); //3 additional blend functions (see TImage32.CopyBlend) function BlendAverage(bgColor, fgColor: TColor32): TColor32; function BlendLinearBurn(bgColor, fgColor: TColor32): TColor32; function BlendColorDodge(bgColor, fgColor: TColor32): TColor32; implementation uses {$IFDEF USING_FMX} Img32.FMX, {$ENDIF} Img32.Transform; const FloodFillDefaultRGBTolerance: byte = 64; MaxBlur = 100; type PColor32Array = ^TColor32Array; TColor32Array = array [0.. maxint div SizeOf(TColor32) -1] of TColor32; PWeightedColorArray = ^TWeightedColorArray; TWeightedColorArray = array [0.. $FFFFFF] of TWeightedColor; // SimplifyPathsEx structures PVertex = ^TVertex; TVertex = record pt : TPointD; uvec : TPointD; dist : double; perpD : double; next : PVertex; prev: PVertex; end; TArrayOfVertices = array of TVertex; //------------------------------------------------------------------------------ // Miscellaneous functions //------------------------------------------------------------------------------ function Clamp(val, endVal: integer): integer; {$IFDEF INLINE} inline; {$ENDIF} begin if val < 0 then Result := 0 else if val >= endVal then Result := endVal -1 else Result := val; end; //------------------------------------------------------------------------------ function ModEx(val, endVal: integer): integer; {$IFDEF INLINE} inline; {$ENDIF} begin Result := val mod endVal; if Result < 0 then Result := endVal + Result; end; //------------------------------------------------------------------------------ function GetSymmetricCropTransparentRect(img: TImage32): TRect; var w,h, x,y, x1,y1: Integer; p1,p2: PARGB; opaquePxlFound: Boolean; begin Result := img.Bounds; w := img.Width; y1 := 0; opaquePxlFound := false; for y := 0 to (img.Height div 2) -1 do begin p1 := PARGB(img.PixelRow[y]); p2 := PARGB(img.PixelRow[img.Height - y -1]); for x := 0 to w -1 do begin if (p1.A > 0) or (p2.A > 0) then begin y1 := y; opaquePxlFound := true; break; end; inc(p1); inc(p2); end; if opaquePxlFound then break; end; // probably safeset not to resize empty images if not opaquePxlFound then Exit; if y1 > 0 then begin inc(Result.Top, y1); dec(Result.Bottom, y1); end; x1 := 0; h := RectHeight(Result); opaquePxlFound := false; for x := 0 to (w div 2) -1 do begin p1 := PARGB(@img.Pixels[Result.Top * w + x]); p2 := PARGB(@img.Pixels[Result.Top * w + (w -1) - x]); for y := 0 to h -1 do begin if (p1.A > 0) or (p2.A > 0) then begin x1 := x; opaquePxlFound := true; break; end; inc(p1, w); inc(p2, w); end; if opaquePxlFound then break; end; if not opaquePxlFound then Exit; inc(Result.Left, x1); dec(Result.Right, x1); end; //------------------------------------------------------------------------------ //SymmetricCropTransparent: after cropping, the image's midpoint //will be the same pixel as before cropping. (Important for rotating.) procedure SymmetricCropTransparent(img: TImage32); var rec: TRect; begin rec := GetSymmetricCropTransparentRect(img); if (rec.Top > 0) or (rec.Left > 0) then img.Crop(rec); end; //------------------------------------------------------------------------------ procedure DrawEdge(img: TImage32; const rec: TRect; topLeftColor, bottomRightColor: TColor32; penWidth: double = 1.0); begin DrawEdge(img, RectD(rec), topLeftColor, bottomRightColor, penWidth); end; //------------------------------------------------------------------------------ procedure DrawEdge(img: TImage32; const rec: TRectD; topLeftColor, bottomRightColor: TColor32; penWidth: double = 1.0); var p: TPathD; c: TColor32; begin if penWidth = 0 then Exit else if penWidth < 0 then begin c := topLeftColor; topLeftColor := bottomRightColor; bottomRightColor := c; penWidth := -penWidth; end; if topLeftColor <> bottomRightColor then begin with rec do begin p := Img32.Vector.MakePath([left, bottom, left, top, right, top]); DrawLine(img, p, penWidth, topLeftColor, esButt); p := Img32.Vector.MakePath([right, top, right, bottom, left, bottom]); DrawLine(img, p, penWidth, bottomRightColor, esButt); end; end else DrawLine(img, Rectangle(rec), penWidth, topLeftColor, esPolygon); end; //------------------------------------------------------------------------------ procedure DrawEdge(img: TImage32; const path: TPathD; topLeftColor, bottomRightColor: TColor32; penWidth: double = 1.0; closePath: Boolean = true); var i, highI, deg: integer; frac: double; c: TColor32; p: TPathD; const RadToDeg = 180/PI; begin if penWidth = 0 then Exit else if penWidth < 0 then begin c := topLeftColor; topLeftColor := bottomRightColor; bottomRightColor := c; penWidth := -penWidth; end; highI := high(path); if highI < 2 then Exit; p := path; if closePath and not PointsNearEqual(p[0], p[highI], 0.01) then begin AppendPoint(p, p[0]); inc(highI); end; for i := 1 to highI do begin deg := Round(GetAngle(p[i-1], p[i]) * RadToDeg); case deg of -180..-136: frac := (-deg-135)/45; -135..0 : frac := 0; 1..44 : frac := deg/45; else frac := 1; end; c := GradientColor(topLeftColor, bottomRightColor, frac); DrawLine(img, p[i-1], p[i], penWidth, c); end; end; //------------------------------------------------------------------------------ procedure FillColorHorz(img: TImage32; x, endX, y: integer; color: TColor32); var i,dx: integer; p: PColor32; begin if (x < 0) or (x >= img.Width) then Exit; if (y < 0) or (y >= img.Height) then Exit; p := img.PixelRow[y]; inc(p, x); if endX >= img.Width then endX := img.Width -1 else if endX < 0 then endX := 0; if endX < x then dx := -1 else dx := 1; for i := 0 to Abs(x-endX) do begin p^ := color; inc(p, dx); end; end; //------------------------------------------------------------------------------ procedure FillColorVert(img: TImage32; x, y, endY: integer; color: TColor32); var i, dy: integer; p: PColor32; begin if (x < 0) or (x >= img.Width) then Exit; if (y < 0) or (y >= img.Height) then Exit; p := img.PixelRow[y]; inc(p, x); if endY >= img.Height then endY := img.Height -1 else if endY < 0 then endY := 0; dy := img.Width; if endY < y then dy := -dy; for i := 0 to Abs(y - endY) do begin p^ := color; inc(p, dy); end; end; //------------------------------------------------------------------------------ procedure DrawShadowRect(img: TImage32; const rec: TRect; depth: double; angle: double = angle45; color: TColor32 = $80000000); var i,j, sX,sY: integer; l,t,r,b: integer; tmpImg: TImage32; tmpRec: TRect; xx,yy: double; ss: TPointD; c: TColor32; begin GetSinCos(angle, yy, xx); ss.X := depth * xx; ss.Y := depth * yy; sX := Abs(Round(ss.X)); sY := Abs(Round(ss.Y)); if rec.Left + ss.X < 0 then ss.X := -rec.Left else if rec.Right + ss.X > img.Width then ss.X := img.Width - rec.Right -1; if rec.Top + ss.Y < 0 then ss.Y := -rec.Top else if rec.Bottom + ss.Y > img.Height then ss.Y := img.Height -rec.Bottom -1; tmpImg := TImage32.Create(sX*3 +1, sY*3 +1); try i := sX div 2; j := sY div 2; DrawPolygon(tmpImg, Rectangle(i,j,i+sX*2,j+sY*2), frNonZero, color); FastGaussianBlur(tmpImg, tmpImg.Bounds, Round(sX/4),Round(sY/4), 1); // t-l corner if (ss.X < 0) or (ss.Y < 0) then begin tmpRec := Rect(0, 0, sX, sY); l := rec.Left; t := rec.Top; if ss.X < 0 then dec(l, sX); if ss.Y < 0 then dec(t, sY); img.Copy(tmpImg, tmpRec, Rect(l,t,l+sX,t+sY)); end; // t-r corner if (ss.X > 0) or (ss.Y < 0) then begin tmpRec := Rect(sX*2+1, 0, sX*3+1, sY); l := rec.Right; t := rec.Top; if ss.X < 0 then dec(l, sX); if ss.Y < 0 then dec(t, sY); img.Copy(tmpImg, tmpRec, Rect(l,t,l+sX,t+sY)); end; // b-l corner if (ss.X < 0) or (ss.Y > 0) then begin tmpRec := Rect(0, sY*2+1, sX, sY*3+1); l := rec.Left; t := rec.Bottom; if ss.X < 0 then dec(l, sX); if ss.Y < 0 then dec(t, sY); img.Copy(tmpImg, tmpRec, Rect(l,t,l+sX,t+sY)); end; // b-r corner if (ss.X > 0) or (ss.Y > 0) then begin tmpRec := Rect(sX*2+1, sY*2+1, sX*3+1, sY*3+1); l := rec.Right; t := rec.Bottom; if ss.X < 0 then dec(l, sX); if ss.Y < 0 then dec(t, sY); img.Copy(tmpImg, tmpRec, Rect(l,t,l+sX,t+sY)); end; // l-edge if (ss.X < 0) then begin l := rec.Left; t := rec.Top+sY; b := rec.Bottom-1; if ss.Y < 0 then begin dec(t, sY); dec(b,sY); end; for i := 1 to sX do begin c := tmpImg.Pixel[sX-i, sY+1]; FillColorVert(img, l-i, t, b, c); end; end; // t-edge if (ss.Y < 0) then begin l := rec.Left+sX; r := rec.Right-1; t := rec.Top; if ss.X < 0 then begin dec(l, sX); dec(r,sX); end; for i := 1 to sY do begin c := tmpImg.Pixel[sX+1, sY-i]; FillColorHorz(img, l, r, t-i, c); end; end; // r-edge if (ss.X > 0) then begin r := rec.Right-1; t := rec.Top+sY; b := rec.Bottom-1; if ss.Y < 0 then begin dec(t, sY); dec(b,sY); end; for i := 1 to sX do begin c := tmpImg.Pixel[sX*2+i, sY+1]; FillColorVert(img, r+i, t, b, c); end; end; // b-edge if (ss.Y > 0) then begin l := rec.Left+sX; r := rec.Right-1; b := rec.Bottom-1; if ss.X < 0 then begin dec(l, sX); dec(r,sX); end; for i := 1 to sY do begin c := tmpImg.Pixel[sX+1, sY*2+i]; FillColorHorz(img, l, r, b+i, c); end; end; finally tmpImg.Free; end; end; //------------------------------------------------------------------------------ procedure DrawShadow(img: TImage32; const polygon: TPathD; fillRule: TFillRule; depth: double; angleRads: double; color: TColor32; cutoutInsideShadow: Boolean); var polygons: TPathsD; begin setlength(polygons, 1); polygons[0] := polygon; DrawShadow(img, polygons, fillRule, depth, angleRads, color, cutoutInsideShadow); end; //------------------------------------------------------------------------------ procedure DrawShadow(img: TImage32; const polygons: TPathsD; fillRule: TFillRule; depth: double; angleRads: double; color: TColor32; cutoutInsideShadow: Boolean); var x, y: double; blurSize, w,h: integer; rec: TRect; polys, shadowPolys: TPathsD; shadowImg: TImage32; begin rec := GetBounds(polygons); if IsEmptyRect(rec) or (depth < 1) then Exit; {$IFDEF CLOCKWISE_ROTATION_WITH_NEGATIVE_ANGLES} angleRads := -angleRads; {$ENDIF} NormalizeAngle(angleRads); GetSinCos(angleRads, y, x); depth := depth * 0.5; x := depth * x; y := depth * y; blurSize := Max(1,Round(depth / 2)); Img32.Vector.InflateRect(rec, Ceil(depth*2), Ceil(depth*2)); polys := TranslatePath(polygons, -rec.Left, -rec.Top); shadowPolys := TranslatePath(polys, x, y); RectWidthHeight(rec, w, h); shadowImg := TImage32.Create(w, h); try DrawPolygon(shadowImg, shadowPolys, fillRule, color); FastGaussianBlur(shadowImg, shadowImg.Bounds, blurSize, 1); if cutoutInsideShadow then EraseInsidePaths(shadowImg, polys, fillRule); img.CopyBlend(shadowImg, shadowImg.Bounds, rec, BlendToAlphaLine); finally shadowImg.Free; end; end; //------------------------------------------------------------------------------ procedure DrawGlow(img: TImage32; const polygon: TPathD; fillRule: TFillRule; color: TColor32; blurRadius: integer); var polygons: TPathsD; begin setlength(polygons, 1); polygons[0] := polygon; DrawGlow(img, polygons, fillRule, color, blurRadius); end; //------------------------------------------------------------------------------ procedure DrawGlow(img: TImage32; const polygons: TPathsD; fillRule: TFillRule; color: TColor32; blurRadius: integer); var w,h: integer; rec: TRect; glowPolys: TPathsD; glowImg: TImage32; begin rec := GetBounds(polygons); glowPolys := TranslatePath(polygons, blurRadius -rec.Left +1, blurRadius -rec.Top +1); Img32.Vector.InflateRect(rec, blurRadius +1, blurRadius +1); RectWidthHeight(rec, w, h); glowImg := TImage32.Create(w, h); try DrawPolygon(glowImg, glowPolys, fillRule, color); FastGaussianBlur(glowImg, glowImg.Bounds, blurRadius, 2); glowImg.ScaleAlpha(4); img.CopyBlend(glowImg, glowImg.Bounds, rec, BlendToAlphaLine); finally glowImg.Free; end; end; //------------------------------------------------------------------------------ procedure Sharpen(img: TImage32; radius: Integer; amount: Integer); var i: Integer; amt: double; weightAmount: array [-255 .. 255] of Integer; bmpBlur: TImage32; pColor, pBlur: PARGB; begin if radius = 0 then Exit; amt := ClampRange(amount/10, 0.1, 5); radius := ClampRange(radius, 1, 10); for i := -255 to 255 do weightAmount[i] := Round(amt * i); bmpBlur := TImage32.Create(img); // clone self try pColor := PARGB(img.pixelBase); FastGaussianBlur(bmpBlur, bmpBlur.Bounds, radius, 2); pBlur := PARGB(bmpBlur.pixelBase); for i := 1 to img.Width * img.Height do begin if (pColor.A > 0) then begin pColor.R := ClampByte(pColor.R + weightAmount[pColor.R - pBlur.R]); pColor.G := ClampByte(pColor.G + weightAmount[pColor.G - pBlur.G]); pColor.B := ClampByte(pColor.B + weightAmount[pColor.B - pBlur.B]); end; Inc(pColor); Inc(pBlur); end; finally bmpBlur.Free; end; end; //------------------------------------------------------------------------------ procedure InternalHatchBackground(img: TImage32; const rec: TRect; color1, color2: TColor32; hatchSize: Integer = 10); var i, j, imgWidth: Integer; pc: PColor32; colors: array[boolean] of TColor32; hatch: Boolean; x: integer; begin colors[false] := color1; colors[true] := color2; imgWidth := img.Width; for i := rec.Top to rec.Bottom -1 do begin pc := @img.Pixels[i * imgWidth + rec.Left]; hatch := Odd(i div hatchSize); x := (rec.Left + 1) mod hatchSize; if x = 0 then hatch := not hatch; for j := rec.Left to rec.Right -1 do begin if pc^ = 0 then pc^ := colors[hatch] else if GetAlpha(pc^) < 255 then pc^ := BlendToOpaque(colors[hatch], pc^); inc(pc); inc(x); if x >= hatchSize then begin x := 0; hatch := not hatch; end; end; end; end; //------------------------------------------------------------------------------ procedure HatchBackground(img: TImage32; const rec: TRect; color1: TColor32 = clWhite32; color2: TColor32= $FFE8E8E8; hatchSize: Integer = 10); overload; begin if (rec.Right <= rec.Left) or (rec.Bottom - rec.Top <= 0) then Exit; img.BeginUpdate; try InternalHatchBackground(img, rec, color1, color2, hatchSize); finally img.EndUpdate; end; end; //------------------------------------------------------------------------------ procedure HatchBackground(img: TImage32; color1: TColor32; color2: TColor32; hatchSize: Integer); begin HatchBackground(img, img.Bounds, color1, color2, hatchSize); end; //------------------------------------------------------------------------------ procedure GridBackground(img: TImage32; majorInterval, minorInterval: integer; fillColor: TColor32; majColor: TColor32; minColor: TColor32); var i, x,y, w,h: integer; path: TPathD; cr: TCustomColorRenderer; begin img.Clear(fillColor); w := img.Width; h := img.Height; NewPointDArray(path, 2, True); if img.AntiAliased then cr := TColorRenderer.Create(minColor) else cr := TAliasedColorRenderer.Create(minColor); try if minorInterval > 0 then begin //cr.SetColor(minColor); x := minorInterval; path[0] := PointD(x, 0); path[1] := PointD(x, h);; for i := 1 to (w div minorInterval) do begin Img32.Draw.DrawLine(img, path, 1, cr, esSquare); path[0].X := path[0].X + minorInterval; path[1].X := path[1].X + minorInterval; end; y := minorInterval; path[0] := PointD(0, y); path[1] := PointD(w, y); for i := 1 to (h div minorInterval) do begin Img32.Draw.DrawLine(img, path, 1, cr, esSquare); path[0].Y := path[0].Y + minorInterval; path[1].Y := path[1].Y + minorInterval; end; end; if majorInterval > minorInterval then begin cr.SetColor(majColor); x := majorInterval; path[0] := PointD(x, 0); path[1] := PointD(x, h);; for i := 1 to (w div majorInterval) do begin Img32.Draw.DrawLine(img, path, 1, cr, esSquare); path[0].X := path[0].X + majorInterval; path[1].X := path[1].X + majorInterval; end; y := majorInterval; path[0] := PointD(0, y); path[1] := PointD(w, y); for i := 1 to (h div majorInterval) do begin Img32.Draw.DrawLine(img, path, 1, cr, esSquare); path[0].Y := path[0].Y + majorInterval; path[1].Y := path[1].Y + majorInterval; end; end; finally cr.Free; end; end; //------------------------------------------------------------------------------ function ColorDifference(color1, color2: TColor32): cardinal; {$IFDEF INLINE} inline; {$ENDIF} var c1: TARGB absolute color1; c2: TARGB absolute color2; begin result := Abs(c1.R - c2.R) + Abs(c1.G - c2.G) + Abs(c1.B - c2.B); result := (result * 341) shr 10; // divide by 3 end; //------------------------------------------------------------------------------ procedure ReplaceExactColor(img: TImage32; oldColor, newColor: TColor32); var color: PColor32; i: Integer; begin color := img.PixelBase; for i := 0 to img.Width * img.Height -1 do begin if color^ = oldColor then color^ := newColor; inc(color); end; end; //------------------------------------------------------------------------------ procedure RemoveColor(img: TImage32; color: TColor32); var fg: TARGB absolute color; bg: PARGB; i: Integer; Q: byte; begin if fg.A = 0 then Exit; bg := PARGB(img.PixelBase); for i := 0 to img.Width * img.Height -1 do begin if bg.A > 0 then begin // red if (bg.R > fg.R) then Q := bg.R - fg.R else if (bg.R < fg.R) then Q := DivTable[fg.R - bg.R, fg.R] else Q := 0; // green if (bg.G > fg.G) then Q := Max(Q, bg.G - fg.G) else if (bg.G < fg.G) then Q := Max(Q, DivTable[fg.G - bg.G, fg.G]); // blue if (bg.B > fg.B) then Q := Max(Q, bg.B - fg.B) else if (bg.B < fg.B) then Q := Max(Q, DivTable[fg.B - bg.B, fg.B]); // weight Q toward either fully opaque or fully translucent Q := Sigmoid[Q]; if (Q = 0) then bg.Color := clNone32 else if (Q < 255) then begin bg.A := MulTable[bg.A, Q]; bg.R := DivTable[bg.R - MulTable[not Q, fg.R], Q]; bg.G := DivTable[bg.G - MulTable[not Q, fg.G], Q]; bg.B := DivTable[bg.B - MulTable[not Q, fg.B], Q]; end; end; inc(bg); end; end; //------------------------------------------------------------------------------ procedure FilterOnColor(img: TImage32; color: TColor32); var fg: TARGB absolute color; bg: PARGB; i: Integer; Q: byte; begin if fg.A = 0 then Exit; bg := PARGB(img.PixelBase); for i := 0 to img.Width * img.Height -1 do begin if bg.A > 0 then begin // red if (bg.R > fg.R) then Q := bg.R - fg.R else if (bg.R < fg.R) then Q := DivTable[fg.R - bg.R, fg.R] else Q := 0; // green if (bg.G > fg.G) then Q := Max(Q, bg.G - fg.G) else if (bg.G < fg.G) then Q := Max(Q, DivTable[fg.G - bg.G, fg.G]); // blue if (bg.B > fg.B) then Q := Max(Q, bg.B - fg.B) else if (bg.B < fg.B) then Q := Max(Q, DivTable[fg.B - bg.B, fg.B]); // weight Q toward either fully opaque or fully translucent Q := Sigmoid[Q]; Q := MulTable[bg.A, not Q]; bg.Color := color; bg.A := Q; // note: fg.A is ignored end; inc(bg); end; end; //------------------------------------------------------------------------------ procedure FilterOnExactColor(img: TImage32; color: TColor32); var pc: PColor32; i: Integer; mask: TColor32; begin // alpha channel is ignored mask := $FFFFFF; color := color and mask; pc := img.PixelBase; for i := 0 to img.Width * img.Height -1 do begin if (pc^ and mask) <> color then pc^ := clNone32; inc(pc); end; end; //------------------------------------------------------------------------------ procedure FilterOnAlpha(img: TImage32; alpha: byte; tolerance: byte); var bg: PARGB; i: Integer; begin bg := PARGB(img.PixelBase); for i := 0 to img.Width * img.Height -1 do begin if abs(bg.A - alpha) > tolerance then bg.A := 0; inc(bg); end; end; //------------------------------------------------------------------------------ procedure RedEyeRemove(img: TImage32; const rect: TRect); var k: integer; cutout, mask: TImage32; path: TPathD; cutoutRec, rect3: TRect; radGrad: TRadialGradientRenderer; begin k := RectWidth(rect) * RectHeight(rect); if k < 120 then k := 2 else if k < 230 then k := 3 else k := 4; cutoutRec := rect; Img32.Vector.InflateRect(cutoutRec, k, k); cutout := TImage32.Create(img, cutoutRec); mask := TImage32.Create(cutout.Width, cutout.Height); radGrad := TRadialGradientRenderer.Create; try // fill behind the cutout with black also // blurring the fill to soften its edges rect3 := cutout.Bounds; Img32.Vector.InflateRect(rect3, -k, -k); path := Ellipse(rect3); DrawPolygon(mask, path, frNonZero, clBlack32); // given the very small area and small radius of the blur, the // speed improvement of BoxBlur over GaussianBlur is inconsequential. GaussianBlur(mask, mask.Bounds, k); img.CopyBlend(mask, mask.Bounds, cutoutRec, BlendToOpaque); // gradient fill to clNone32 a mask to soften cutout's edges path := Ellipse(cutoutRec); radGrad.SetParameters(rect3, clBlack32, clNone32); DrawPolygon(mask, path, frNonZero, radGrad); cutout.CopyBlend(mask, mask.Bounds, cutout.Bounds, BlendMaskLine); // now remove red from the cutout RemoveColor(cutout, clRed32); // finally replace the cutout ... img.CopyBlend(cutout, cutout.Bounds, cutoutRec, BlendToOpaque); finally mask.Free; cutout.Free; radGrad.Free; end; end; //------------------------------------------------------------------------------ procedure EraseInsidePath(img: TImage32; const path: TPathD; fillRule: TFillRule); begin if assigned(path) then ErasePolygon(img, path, fillRule); end; //------------------------------------------------------------------------------ procedure EraseInsidePaths(img: TImage32; const paths: TPathsD; fillRule: TFillRule); begin if assigned(paths) then ErasePolygon(img, paths, fillRule); end; //------------------------------------------------------------------------------ procedure EraseOutsideRect(img: TImage32; const r, outsideBounds: TRect); begin // Fill the parts, that are in outsideBounds but not in r with zeros // whole top block if r.Top > outsideBounds.Top then img.FillRect(Rect(outsideBounds.Left, outsideBounds.Top, outsideBounds.Right, r.Top - 1), 0); // whole bottom block if r.Bottom < outsideBounds.Bottom then img.FillRect(Rect(outsideBounds.Left, r.Bottom + 1, outsideBounds.Right, outsideBounds.Bottom), 0); // remaining left block if r.Left > outsideBounds.Left then img.FillRect(Rect(outsideBounds.Left, r.Top, r.Left - 1, r.Bottom), 0); // remaining right block if r.Right < outsideBounds.Right then img.FillRect(Rect(r.Right + 1, r.Top, outsideBounds.Right, r.Bottom), 0); end; //------------------------------------------------------------------------------ procedure EraseOutsidePath(img: TImage32; const path: TPathD; fillRule: TFillRule; const outsideBounds: TRect); var w, h: integer; renderer: TMaskRenderer; r: TRect; polygons: TPathsD; begin if not assigned(path) then Exit; RectWidthHeight(outsideBounds, w, h); if (w <= 0) or (h <= 0) then Exit; // We can skip the costly polygon rasterization if the path is // a rectangle if (fillRule in [frEvenOdd, frNonZero]) and IsSimpleRectanglePath(path, r) then begin EraseOutsideRect(img, r, outsideBounds); Exit; end; renderer := TMaskRenderer.Create; try SetLength(polygons, 1); polygons[0] := path; Rasterize(img, polygons, outsideBounds, fillRule, renderer); finally renderer.Free; end; end; //------------------------------------------------------------------------------ procedure EraseOutsidePaths(img: TImage32; const paths: TPathsD; fillRule: TFillRule; const outsideBounds: TRect; rendererCache: TCustomRendererCache); var w, h: integer; renderer: TMaskRenderer; r: TRect; begin if not assigned(paths) then Exit; RectWidthHeight(outsideBounds, w, h); if (w <= 0) or (h <= 0) then Exit; // We can skip the costly polygon rasterization if the path is // a rectangle. if (fillRule in [frEvenOdd, frNonZero]) and IsSimpleRectanglePath(paths, r) then begin EraseOutsideRect(img, r, outsideBounds); Exit; end; if rendererCache = nil then renderer := TMaskRenderer.Create else renderer := rendererCache.MaskRenderer; try Rasterize(img, paths, outsideBounds, fillRule, renderer); finally if rendererCache = nil then renderer.Free; end; end; //------------------------------------------------------------------------------ procedure Draw3D(img: TImage32; const polygon: TPathD; fillRule: TFillRule; height, blurRadius: double; colorLt: TColor32; colorDk: TColor32; angleRads: double); var polygons: TPathsD; begin setLength(polygons, 1); polygons[0] := polygon; Draw3D(img, polygons, fillRule, height, blurRadius, colorLt, colorDk, angleRads); end; //------------------------------------------------------------------------------ procedure Draw3D(img: TImage32; const polygons: TPathsD; fillRule: TFillRule; height, blurRadius: double; colorLt: TColor32; colorDk: TColor32; angleRads: double); var tmp: TImage32; rec: TRect; paths, paths2: TPathsD; w,h: integer; x,y: double; begin rec := GetBounds(polygons); if IsEmptyRect(rec) then Exit; {$IFDEF CLOCKWISE_ROTATION_WITH_NEGATIVE_ANGLES} angleRads := -angleRads; {$ENDIF} GetSinCos(angleRads, y, x); paths := TranslatePath(polygons, -rec.Left, -rec.Top); RectWidthHeight(rec, w, h); tmp := TImage32.Create(w, h); try if GetAlpha(colorLt) > 0 then begin tmp.Clear(colorLt); paths2 := TranslatePath(paths, -height*x, -height*y); EraseInsidePaths(tmp, paths2, fillRule); FastGaussianBlur(tmp, tmp.Bounds, Round(blurRadius), 0); EraseOutsidePaths(tmp, paths, fillRule, tmp.Bounds); img.CopyBlend(tmp, tmp.Bounds, rec, BlendToAlphaLine); end; if GetAlpha(colorDk) > 0 then begin tmp.Clear(colorDk); paths2 := TranslatePath(paths, height*x, height*y); EraseInsidePaths(tmp, paths2, fillRule); FastGaussianBlur(tmp, tmp.Bounds, Round(blurRadius), 0); EraseOutsidePaths(tmp, paths, fillRule, tmp.Bounds); img.CopyBlend(tmp, tmp.Bounds, rec, BlendToAlphaLine); end; finally tmp.Free; end; end; //------------------------------------------------------------------------------ function RainbowColor(fraction: double; luminance: byte = 128): TColor32; var hsl: THsl; begin if (fraction < 0) or (fraction > 1) then fraction := frac(fraction); hsl.hue := Round(fraction * 255); hsl.sat := 255; hsl.lum := luminance; hsl.alpha := 255; Result := HslToRgb(hsl); end; //------------------------------------------------------------------------------ function GradientColor(color1, color2: TColor32; frac: single): TColor32; var hsl1, hsl2: THsl; begin if (frac <= 0) then result := color1 else if (frac >= 1) then result := color2 else begin hsl1 := RgbToHsl(color1); hsl2 := RgbToHsl(color2); hsl1.hue := ClampByte(hsl1.hue*(1-frac) + hsl2.hue*frac); hsl1.sat := ClampByte(hsl1.sat*(1-frac) + hsl2.sat*frac); hsl1.lum := ClampByte(hsl1.lum*(1-frac) + hsl2.lum*frac); hsl1.alpha := ClampByte(hsl1.alpha*(1-frac) + hsl2.alpha*frac); Result := HslToRgb(hsl1); end; end; //------------------------------------------------------------------------------ function MakeDarker(color: TColor32; percent: cardinal): TColor32; var hsl: THsl; begin hsl := RgbToHsl(color); hsl.lum := ClampByte(hsl.lum - (percent/100 * hsl.lum)); Result := HslToRgb(hsl); end; //------------------------------------------------------------------------------ function MakeLighter(color: TColor32; percent: cardinal): TColor32; var hsl: THsl; begin hsl := RgbToHsl(color); hsl.lum := ClampByte(hsl.lum + percent/100 * (255 - hsl.lum)); Result := HslToRgb(hsl); end; //------------------------------------------------------------------------------ function DrawButton(img: TImage32; const pt: TPointD; size: double; color: TColor32; buttonShape: TButtonShape; buttonAttributes: TButtonAttributes): TPathD; var i: integer; radius: double; rec: TRectD; lightSize, lightAngle: double; begin if (size < 5) then Exit; radius := size * 0.5; lightSize := radius * 0.25; rec := RectD(pt.X -radius, pt.Y -radius, pt.X +radius, pt.Y +radius); if baEraseBeneath in buttonAttributes then img.Clear(Rect(rec)); case buttonShape of bsDiamond: begin NewPointDArray(Result, 4, True); for i := 0 to 3 do Result[i] := pt; Result[0].X := Result[0].X -radius; Result[1].Y := Result[1].Y -radius; Result[2].X := Result[2].X +radius; Result[3].Y := Result[3].Y +radius; end; bsSquare: begin Img32.Vector.InflateRect(rec, -1,-1); Result := Rectangle(rec); end; else Result := Ellipse(rec); end; lightAngle := angle225; img.BeginUpdate; try // nb: only need to cutout the inside shadow if // the pending color fill is semi-transparent if baShadow in buttonAttributes then DrawShadow(img, Result, frNonZero, lightSize *2, (lightAngle + angle180), $AA000000, GetAlpha(color) < $FE); if GetAlpha(color) > 2 then DrawPolygon(img, Result, frNonZero, color); if ba3D in buttonAttributes then Draw3D(img, Result, frNonZero, lightSize*2, Ceil(lightSize), $CCFFFFFF, $AA000000, lightAngle); DrawLine(img, Result, dpiAware1, clBlack32, esPolygon); finally img.EndUpdate; end; end; //------------------------------------------------------------------------------ function AlphaAverage(color1, color2: TColor32): cardinal; {$IFDEF INLINE} inline; {$ENDIF} var c1: TARGB absolute color1; c2: TARGB absolute color2; begin result := (c1.A + c2.A) shr 1; end; //------------------------------------------------------------------------------ function BlendAverage(bgColor, fgColor: TColor32): TColor32; var res: TARGB absolute Result; bg: TARGB absolute bgColor; fg: TARGB absolute fgColor; begin res.A := (fg.A + bg.A) shr 1; res.R := (fg.R + bg.R) shr 1; res.G := (fg.G + bg.G) shr 1; res.B := (fg.B + bg.B) shr 1; end; //------------------------------------------------------------------------------ function BlendLinearBurn(bgColor, fgColor: TColor32): TColor32; var res: TARGB absolute Result; bg: TARGB absolute bgColor; fg: TARGB absolute fgColor; begin res.A := 255; res.R := Max(0, bg.R + fg.R - 255); res.G := Max(0, bg.G + fg.G - 255); res.B := Max(0, bg.B + fg.B - 255); end; //------------------------------------------------------------------------------ function BlendColorDodge(bgColor, fgColor: TColor32): TColor32; var res: TARGB absolute Result; bg: TARGB absolute bgColor; fg: TARGB absolute fgColor; begin res.A := 255; res.R := DivTable[bg.R, not fg.R]; res.G := DivTable[bg.G, not fg.G]; res.B := DivTable[bg.B, not fg.B]; end; //------------------------------------------------------------------------------ procedure PencilEffect(img: TImage32; intensity: integer); var img2: TImage32; begin if img.IsEmpty then Exit; intensity := max(1, min(10, intensity)); img.Grayscale; img2 := TImage32.Create(img); try img2.InvertColors; FastGaussianBlur(img2, img2.Bounds, intensity, 2); img.CopyBlend(img2, img2.Bounds, img.Bounds, BlendColorDodge); finally img2.Free; end; end; //------------------------------------------------------------------------------ procedure TraceContours(img: TImage32; intensity: integer); var i,j, w,h: integer; tmp, tmp2: TArrayOfColor32; s, s2: PColor32; d: PARGB; begin w := img.Width; h := img.Height; if w * h = 0 then Exit; NewColor32Array(tmp, w * h); NewColor32Array(tmp2, w * h); s := img.PixelRow[0]; d := @tmp[0]; for j := 0 to h-1 do begin s2 := IncPColor32(s, 1); for i := 0 to w-2 do begin d.A := ColorDifference(s^, s2^); inc(s); inc(s2); inc(d); end; inc(s); inc(d); end; for j := 0 to w-1 do begin s := @tmp[j]; d := @tmp2[j]; s2 := IncPColor32(s, w); for i := 0 to h-2 do begin d.A := AlphaAverage(s^, s2^); inc(s, w); inc(s2, w); inc(d, w); end; end; img.BlockNotify; img.AssignPixelArray(tmp2, w, h); img.UnblockNotify; if intensity < 1 then Exit; if intensity > 10 then intensity := 10; // range = 1-10 img.ScaleAlpha(intensity); end; //------------------------------------------------------------------------------ // FLOODFILL - AND SUPPORT FUNCTIONS //------------------------------------------------------------------------------ type PFloodFillRec = ^TFloodFillRec; TFloodFillRec = record xLeft : Integer; xRight : Integer; y : Integer; dirY : Integer; next : PFloodFillRec; end; TFloodFillStack = class first : PFloodFillRec; maxY : integer; constructor Create(maxY: integer); destructor Destroy; override; procedure Push(xLeft, xRight,y, direction: Integer); procedure Pop(out xLeft, xRight,y, direction: Integer); function IsEmpty: Boolean; end; TFloodFillMask = class private img : TImage32; mask : TImage32; colorsRow : PColor32Array; maskRow : PColor32Array; initialColor : TColor32; compareFunc : TCompareFunctionEx; tolerance : Integer; public function Execute(imgIn, imgMaskOut: TImage32; x,y: integer; aTolerance: Byte = 0; compFunc: TCompareFunctionEx = nil): Boolean; procedure SetCurrentY(y: Integer); function IsMatch(x: Integer): Boolean; end; //------------------------------------------------------------------------------ // TFloodFillStack methods //------------------------------------------------------------------------------ constructor TFloodFillStack.Create(maxY: integer); begin self.maxY := maxY; end; //------------------------------------------------------------------------------ destructor TFloodFillStack.Destroy; var ffr: PFloodFillRec; begin while assigned(first) do begin ffr := first; first := first.next; dispose(ffr); end; end; //------------------------------------------------------------------------------ procedure TFloodFillStack.Push(xLeft, xRight, y, direction: Integer); var ffr: PFloodFillRec; begin if ((y <= 0) and (direction = -1)) or ((y >= maxY) and (direction = 1)) then Exit; new(ffr); ffr.xLeft := xLeft; ffr.xRight := xRight; ffr.y := y; ffr.dirY := direction; ffr.next := first; first := ffr; end; //------------------------------------------------------------------------------ procedure TFloodFillStack.Pop(out xLeft, xRight, y, direction: Integer); var ffr: PFloodFillRec; begin xLeft := first.xLeft; xRight := first.xRight; direction := first.dirY; y := first.y + direction; ffr := first; first := first.next; dispose(ffr); end; //------------------------------------------------------------------------------ function TFloodFillStack.IsEmpty: Boolean; begin result := not assigned(first); end; //------------------------------------------------------------------------------ // TFloodFillMask methods //------------------------------------------------------------------------------ function TFloodFillMask.Execute(imgIn, imgMaskOut: TImage32; x,y: integer; aTolerance: Byte; compFunc: TCompareFunctionEx): Boolean; var ffs : TFloodFillStack; w,h : integer; xl, xr, xr2 : Integer; maxX : Integer; dirY : Integer; begin Result := Assigned(imgIn) and Assigned(imgMaskOut) and InRange(x,0,imgIn.Width -1) and InRange(y,0,imgIn.Height -1); if not Result then Exit; w := imgIn.Width; h := imgIn.Height; // make sure the mask is the size of the image imgMaskOut.SetSize(w,h); img := imgIn; mask := imgMaskOut; compareFunc := compFunc; tolerance := aTolerance; maxX := w -1; ffs := TFloodFillStack.create(h -1); try initialColor := imgIn.Pixel[x, y]; xl := x; xr := x; SetCurrentY(y); IsMatch(x); while (xl > 0) and IsMatch(xl -1) do dec(xl); while (xr < maxX) and IsMatch(xr +1) do inc(xr); ffs.Push(xl, xr, y, -1); // down ffs.Push(xl, xr, y, 1); // up while not ffs.IsEmpty do begin ffs.Pop(xl, xr, y, dirY); SetCurrentY(y); xr2 := xl; // check left ... if IsMatch(xl) then begin while (xl > 0) and IsMatch(xl-1) do dec(xl); if xl <= xr2 -2 then ffs.Push(xl, xr2-2, y, -dirY); while (xr2 < maxX) and IsMatch(xr2+1) do inc(xr2); ffs.Push(xl, xr2, y, dirY); if xr2 >= xr +2 then ffs.Push(xr+2, xr2, y, -dirY); xl := xr2 +2; end; // check right ... while (xl <= xr) and not IsMatch(xl) do inc(xl); while (xl <= xr) do begin xr2 := xl; while (xr2 < maxX) and IsMatch(xr2+1) do inc(xr2); ffs.Push(xl, xr2, y, dirY); if xr2 >= xr +2 then begin ffs.Push(xr+2, xr2, y, -dirY); break; end; inc(xl, 2); while (xl <= xr) and not IsMatch(xl) do inc(xl); end; end; finally ffs.Free; end; end; //------------------------------------------------------------------------------ procedure TFloodFillMask.SetCurrentY(y: Integer); begin colorsRow := PColor32Array(img.PixelRow[y]); maskRow := PColor32Array(mask.PixelRow[y]); end; //------------------------------------------------------------------------------ function TFloodFillMask.IsMatch(x: Integer): Boolean; var b: Byte; begin if (maskRow[x] > 0) then result := false else begin b := compareFunc(initialColor, colorsRow[x]); result := b < tolerance; if Result then maskRow[x] := tolerance - b else maskRow[x] := 1; end; end; //------------------------------------------------------------------------------ function GetFloodFillMask(imgIn, imgMaskOut: TImage32; x, y: Integer; tolerance: Byte; compareFunc: TCompareFunctionEx): Boolean; var ffm: TFloodFillMask; begin if not Assigned(compareFunc) then compareFunc := CompareRGBEx; ffm := TFloodFillMask.Create; try Result := ffm.Execute(imgIn, imgMaskOut, x, y, tolerance, compareFunc); finally ffm.Free; end; end; //------------------------------------------------------------------------------ procedure FloodFill(img: TImage32; x, y: Integer; newColor: TColor32; tolerance: Byte; compareFunc: TCompareFunctionEx); var i: Integer; pc, pm: PColor32; mask: TImage32; begin if not assigned(compareFunc) then begin compareFunc := CompareRGBEx; if tolerance = 0 then tolerance := FloodFillDefaultRGBTolerance; end; mask := TImage32.Create; try if not GetFloodFillMask(img, mask, x, y, tolerance, compareFunc) then Exit; pc := img.PixelBase; pm := mask.PixelBase; for i := 0 to img.Width * img.Height -1 do begin if (pm^ > 1) then pc^ := newColor; inc(pm); inc(pc); end; finally mask.free; end; end; //------------------------------------------------------------------------------ // EMBOSS - AND SUPPORT FUNCTIONS //------------------------------------------------------------------------------ function IncPWeightColor(pwc: PWeightedColor; cnt: Integer): PWeightedColor; begin result := PWeightedColor(PByte(pwc) + cnt * SizeOf(TWeightedColor)); end; //------------------------------------------------------------------------------ function Intensity(color: TColor32): byte; var c: TARGB absolute color; begin Result := (c.R * 61 + c.G * 174 + c.B * 21) shr 8; end; //------------------------------------------------------------------------------ function Gray(color: TColor32): TColor32; var c: TARGB absolute color; res: TARGB absolute Result; begin res.A := c.A; res.R := Intensity(color); res.G := res.R; res.B := res.R; end; //------------------------------------------------------------------------------ procedure Emboss(img: TImage32; radius: Integer; depth: Integer; luminance: Integer; preserveColor: Boolean); var yy,xx, x,y, w,h: Integer; b: byte; kernel: array [0 .. MaxBlur, 0 .. MaxBlur] of Integer; wca: TArrayOfWeightedColor; pc0, pcf, pcb: PColor32; // pointers to pixels (forward & backward in kernel) pw0, pw: PWeightedColor; // pointers to weight customGray: TColor32; pc: PColor32; const maxDepth = 50; begin // grayscale luminance as percent where 0% is black and 100% is white //(luminance is ignored when preserveColor = true) luminance := ClampRange(luminance, 0, 100); b := luminance *255 div 100; customGray := $FF000000 + b shl 16 + b shl 8 + b; ClampRange(radius, 1, 5); inc(depth); ClampRange(depth, 2, maxDepth); kernel[0][0] := 1; for y := 1 to radius do for x := 1 to radius do kernel[y][x] := depth; w := img.Width; h := img.Height; // nb: dynamic arrays are zero-initialized (unless they're a function result) SetLength(wca, w * h); pc0 := IncPColor32(img.PixelBase, radius * w); pw0 := @wca[radius * w]; for y := radius to h -1 - radius do begin for x := radius to w -1 - radius do begin pw := IncPWeightColor(pw0, x); pcb := IncPColor32(pc0, x - 1); if preserveColor then begin pcf := IncPColor32(pc0, x); pw^.Add(pcf^, kernel[0,0]); inc(pcf); end else begin pw^.Add(customGray, kernel[0,0]); pcf := IncPColor32(pc0, x + 1); end; // parse the kernel ... for yy := 1 to radius do begin for xx := 1 to radius do begin pw^.Subtract(Gray(pcf^), kernel[yy,xx]); pw^.Add(Gray(pcb^), kernel[yy,xx]); dec(pcb); inc(pcf); end; dec(pcb, img.Width - radius); inc(pcf, img.Width - radius); end; end; inc(pc0, img.Width); inc(pw0, img.Width); end; pc := @img.Pixels[0]; pw := @wca[0]; for x := 0 to img.width * img.Height - 1 do begin pc^ := pw.Color or $FF000000; inc(pc); inc(pw); end; end; //------------------------------------------------------------------------------ // RamerDouglasPeucker - and support functions //------------------------------------------------------------------------------ procedure RDP(const path: TPathD; startIdx, endIdx: integer; epsilonSqrd: double; var flags: TArrayOfInteger); var i, idx: integer; d, maxD: double; begin idx := 0; maxD := 0; for i := startIdx +1 to endIdx -1 do begin // PerpendicularDistSqrd - avoids expensive Sqrt() d := PerpendicularDistSqrd(path[i], path[startIdx], path[endIdx]); if d <= maxD then Continue; maxD := d; idx := i; end; if maxD < epsilonSqrd then Exit; flags[idx] := 1; if idx > startIdx + 1 then RDP(path, startIdx, idx, epsilonSqrd, flags); if endIdx > idx + 1 then RDP(path, idx, endIdx, epsilonSqrd, flags); end; //------------------------------------------------------------------------------ function RamerDouglasPeucker(const path: TPathD; epsilon: double): TPathD; var i,j, len: integer; buffer: TArrayOfInteger; begin len := length(path); if len < 5 then begin result := Copy(path, 0, len); Exit; end; SetLength(buffer, len); // buffer is zero initialized buffer[0] := 1; buffer[len -1] := 1; RDP(path, 0, len -1, Sqr(epsilon), buffer); j := 0; SetLength(Result, len); for i := 0 to len -1 do if buffer[i] = 1 then begin Result[j] := path[i]; inc(j); end; SetLength(Result, j); end; //------------------------------------------------------------------------------ function RamerDouglasPeucker(const paths: TPathsD; epsilon: double): TPathsD; var i,j, len: integer; begin j := 0; len := length(paths); setLength(Result, len); for i := 0 to len -1 do begin Result[j] := RamerDouglasPeucker(paths[i], epsilon); if Result[j] <> nil then inc(j); end; setLength(Result, j); end; //------------------------------------------------------------------------------ function GetNext(current, high: integer; var flags: array of Boolean): integer; begin Result := current +1; while (Result <= high) and flags[Result] do inc(Result); if (Result <= high) then Exit; Result := 0; while (flags[Result]) do inc(Result); end; //--------------------------------------------------------------------------- function GetPrior(current, high: integer; var flags: array of Boolean): integer; begin Result := current; if (Result = 0) then Result := high else dec(Result); while (Result > 0) and flags[Result] do dec(Result); if not flags[Result] then Exit; Result := high; while flags[Result] do dec(Result); end; //--------------------------------------------------------------------------- type PSimplifyRec = ^TSimplifyRec; TSimplifyRec = record pt : TPointD; pdSqrd : double; prev : PSimplifyRec; next : PSimplifyRec; isEndPt : Boolean; end; function SimplifyPath(const path: TPathD; shapeTolerance: double; isClosedPath: Boolean): TPathD; var i, iPrev, iNext, len, minLen: integer; tolSqrd: double; srArray: array of TSimplifyRec; current, last: PSimplifyRec; begin Result := nil; len := Length(path); if not isClosedPath then minLen := 2 else minLen := 3; if len < minLen then Exit; SetLength(srArray, len); for i := 0 to len -1 do with srArray[i] do begin iPrev := ModEx(i-1, len); iNext := ModEx(i+1, len); pt := path[i]; prev := @srArray[iPrev]; next := @srArray[iNext]; pdSqrd := PerpendicularDistSqrd(path[i], path[iPrev], path[iNext]); isEndPt := not isClosedPath and ((i = 0) or (i = len -1)); end; current := @srArray[0]; last := current.prev; tolSqrd := Sqr(shapeTolerance); while current <> last do begin if not current.isEndPt and ((current.pdSqrd < tolSqrd) and (current.next.pdSqrd > current.pdSqrd)) then begin current.prev.next := current.next; current.next.prev := current.prev; last := current.prev; dec(len); if last.next = last.prev then break; last.pdSqrd := PerpendicularDistSqrd(last.pt, last.prev.pt, last.next.pt); current := last.next; current.pdSqrd := PerpendicularDistSqrd(current.pt, current.prev.pt, current.next.pt); end else current := current.next; end; if len < minLen then Exit; if not isClosedPath then current := @srArray[0]; NewPointDArray(Result, len, True); for i := 0 to len -1 do begin Result[i] := current.pt; current := current.next; end; end; //------------------------------------------------------------------------------ function SimplifyPaths(const paths: TPathsD; shapeTolerance: double; isClosedPath: Boolean): TPathsD; var i,j, len: integer; begin len := Length(paths); SetLength(Result, len); j := 0; for i := 0 to len -1 do begin result[j] := SimplifyPath(paths[i], shapeTolerance, isClosedPath); if Length(result[j]) > 0 then inc(j); end; SetLength(Result, j); end; //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- type PSimplifyExRec = ^TSimplifyExRec; TSimplifyExRec = record pt : TPointD; pdSqrd : double; segLenSq : double; prev : PSimplifyExRec; next : PSimplifyExRec; end; function DeleteCurrent(var current: PSimplifyExRec): Boolean; var next: PSimplifyExRec; begin current.prev.next := current.next; current.next.prev := current.prev; current := current.prev; next := current.next; Result := next <> current.prev; if not Result then Exit; next.pdSqrd := PerpendicularDistSqrd(next.pt, next.prev.pt, next.next.pt); current.segLenSq := DistanceSqrd(current.pt, current.next.pt); current.pdSqrd := PerpendicularDistSqrd(current.pt, current.prev.pt, current.next.pt); end; //--------------------------------------------------------------------------- function SimplifyPathEx(const path: TPathD; shapeTolerance: double): TPathD; var i, prevI, nextI, len: integer; shapeTolSqr: double; srArray: array of TSimplifyExRec; current, start: PSimplifyExRec; begin Result := nil; len := Length(path); if len < 3 then Exit; shapeTolSqr := Sqr(shapeTolerance); SetLength(srArray, len); for i := 0 to len -1 do begin prevI := i -1; nextI := i +1; if i = 0 then prevI := len -1 else if i = len -1 then nextI := 0; with srArray[i] do begin pt := path[i]; segLenSq:= DistanceSqrd(path[i], path[nextI]); pdSqrd := PerpendicularDistSqrd(path[i], path[prevI], path[nextI]); prev := @srArray[prevI]; next := @srArray[nextI]; end; end; current := @srArray[0]; start := current.prev; while current <> start do begin // Irrespective of segment length, remove vertices that deviate very little // from imaginary lines that pass through their adjacent vertices. // However, if the following vertex has an even sorter distance from its // respective imaginary line, its important to remove that vertex first. if ((current.pdSqrd < shapeTolSqr) and (current.pdSqrd < current.next.pdSqrd)) then begin dec(len); if not DeleteCurrent(current) then Break; start := current.prev; end // also remove insignificant path zig-zags else if (current.prev.segLenSq < shapeTolSqr) and (current.segLenSq < shapeTolSqr) and ((CrossProduct(current.prev.pt, current.pt, current.next.pt) > 0) <> (CrossProduct(current.pt, current.next.pt, current.next.next.pt) > 0)) then begin dec(len); if not DeleteCurrent(current) then Break; start := current.prev; end else current := current.next; end; if len < 3 then Exit; NewPointDArray(Result, len, True); for i := 0 to len -1 do begin Result[i] := current.pt; current := current.next; end; end; //------------------------------------------------------------------------------ function SimplifyPathsEx(const paths: TPathsD; shapeTolerance: double): TPathsD; var i,j, len: integer; begin len := Length(paths); SetLength(Result, len); j := 0; for i := 0 to len -1 do begin Result[j] := SimplifyPathEx(paths[i], shapeTolerance); if Length(Result[j]) > 0 then inc(j); end; SetLength(Result, len); end; //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- function DotProdVecs(const vec1, vec2: TPointD): double; {$IFDEF INLINE} inline; {$ENDIF} begin result := (vec1.X * vec2.X + vec1.Y * vec2.Y); end; //--------------------------------------------------------------------------- function SmoothToCubicBezier(const path: TPathD; pathIsClosed: Boolean; maxOffset: integer): TPathD; var i, j, len, prev: integer; vec: TPointD; pl: TArrayOfDouble; unitVecs: TPathD; d, angle, d1,d2: double; begin // SmoothToCubicBezier - returns cubic bezier control points Result := nil; len := Length(path); if len < 3 then Exit; NewPointDArray(Result, len *3 +1, True); prev := len-1; SetLength(pl, len); SetLength(unitVecs, len); pl[0] := Distance(path[prev], path[0]); unitVecs[0] := GetUnitVector(path[prev], path[0]); for i := 0 to len -1 do begin if i = prev then begin j := 0; end else begin j := i +1; pl[j] := Distance(path[i], path[j]); unitVecs[j] := GetUnitVector(path[i], path[j]); end; vec := GetAvgUnitVector(unitVecs[i], unitVecs[j]); angle := arccos(Max(-1,Min(1,(DotProdVecs(unitVecs[i], unitVecs[j]))))); d := abs(Pi-angle)/TwoPi; d1 := pl[i] * d; d2 := pl[j] * d; if maxOffset > 0 then begin d1 := Min(maxOffset, d1); d2 := Min(maxOffset, d2); end; if i = 0 then Result[len*3-1] := TranslatePoint(path[0], -vec.X * d1, -vec.Y * d1) else Result[i*3-1] := TranslatePoint(path[i], -vec.X * d1, -vec.Y * d1); Result[i*3] := path[i]; Result[i*3+1] := TranslatePoint(path[i], vec.X * d2, vec.Y * d2); end; Result[len*3] := path[0]; if pathIsClosed then Exit; Result[1] := Result[0]; dec(len); Result[len*3-1] := Result[len*3]; SetLength(Result, Len*3 +1); end; //------------------------------------------------------------------------------ function SmoothToCubicBezier(const paths: TPathsD; pathIsClosed: Boolean; maxOffset: integer = 0): TPathsD; var i, len: integer; begin len := Length(paths); SetLength(Result, len); for i := 0 to len -1 do Result[i] := SmoothToCubicBezier(paths[i], pathIsClosed, maxOffset); end; //------------------------------------------------------------------------------ function SmoothToCubicBezier2(const path: TPathD; pathIsClosed: Boolean; maxOffset: integer): TPathD; var i, j, len, prev: integer; vec: TPointD; pl: TArrayOfDouble; unitVecs: TPathD; d1,d2: double; begin // SmoothToCubicBezier2 - returns cubic bezier control points Result := nil; len := Length(path); if len < 3 then Exit; NewPointDArray(Result, len *3 +1); prev := len-1; SetLength(pl, len); SetLength(unitVecs, len); pl[0] := Distance(path[prev], path[0]); unitVecs[0] := GetUnitVector(path[prev], path[0]); for i := 0 to len -1 do begin if i = prev then begin j := 0; end else begin j := i +1; pl[j] := Distance(path[i], path[j]); unitVecs[j] := GetUnitVector(path[i], path[j]); end; vec := GetAvgUnitVector(unitVecs[i], unitVecs[j]); d1 := pl[i]/2; d2 := pl[j]/2; if maxOffset > 0 then begin d1 := Min(maxOffset, d1); d2 := Min(maxOffset, d2); end; if i = 0 then Result[len*3-1] := TranslatePoint(path[0], -vec.X * d1, -vec.Y * d1) else Result[i*3-1] := TranslatePoint(path[i], -vec.X * d1, -vec.Y * d1); Result[i*3] := path[i]; Result[i*3+1] := TranslatePoint(path[i], vec.X * d2, vec.Y * d2); end; Result[len*3] := path[0]; if pathIsClosed then Exit; Result[1] := Result[0]; dec(len); Result[len*3-1] := Result[len*3]; SetLength(Result, Len*3 +1); end; //------------------------------------------------------------------------------ function SmoothToCubicBezier2(const paths: TPathsD; pathIsClosed: Boolean; maxOffset: integer = 0): TPathsD; var i, len: integer; begin len := Length(paths); SetLength(Result, len); for i := 0 to len -1 do Result[i] := SmoothToCubicBezier2(paths[i], pathIsClosed, maxOffset); end; //------------------------------------------------------------------------------ function CubicInterpolate(v1, v2, v3, v4: double; t: double; tension: double = 0): double; var m0, m1, tt, ttt, tensionEx: double; a, b: double; begin tt := t * t; ttt := tt * t; tensionEx := (1-tension) * 0.5; m0 := (v3 - v1)*tensionEx; m1 := (v4 - v2)*tensionEx; a := 2*v2 - 2*v3 + m0 + m1; b := 3*v3 -3*v2 -2*m0 - m1; Result := a*ttt + b*tt + m0*t + v2; end; //------------------------------------------------------------------------------ procedure Append(var path: TPathD; const pt: TPointD); {$IFDEF INLINE} inline; {$ENDIF} var len: integer; begin len := Length(path); SetLengthUninit(path, len +1); path[len] := pt; end; //------------------------------------------------------------------------------ function SmoothPath(const path: TPathD; isClosedPath: Boolean; tension: double; shapeTolerance: double): TPathD; var i, j, highI, len, cnt: integer; pt: TPointD; dists: TArrayOfDouble; const maxInterval = 1.5; begin Result := nil; len := Length(path); if len < 3 then Exit; SetLength(dists, len); highI := len -1; dists[highI] := Distance(path[highI], path[0]); for i := 0 to highI-1 do dists[i] := Distance(path[i], path[i+1]); if tension > 1 then tension := 1 else if tension < -1 then tension := -1; if tension > 0.9 then begin Result := path; Exit; end; if isClosedPath then for i := 0 to highI do begin cnt := Ceil(dists[i]/maxInterval); Append(Result, path[i]); for j := 1 to cnt -1 do begin pt.X := CubicInterpolate( path[ModEx(i-1, len)].X, path[i].X, path[ModEx(i+1, len)].X, path[ModEx(i+2, len)].X, j/cnt, tension); pt.Y := CubicInterpolate( path[ModEx(i-1, len)].Y, path[i].Y, path[ModEx(i+1, len)].Y, path[ModEx(i+2, len)].Y, j/cnt, tension); Append(Result, pt); end; end else begin for i := 0 to highI -1 do begin cnt := Ceil(dists[i]/maxInterval); Append(Result, path[i]); for j := 1 to cnt -1 do begin pt.X := CubicInterpolate( path[Clamp(i-1, len)].X, path[Clamp(i, len)].X, path[Clamp(i+1, len)].X, path[Clamp(i+2, len)].X, j/cnt, tension); pt.Y := CubicInterpolate( path[Clamp(i-1, len)].Y, path[Clamp(i, len)].Y, path[Clamp(i+1, len)].Y, path[Clamp(i+2, len)].Y, j/cnt, tension); Append(Result, pt); end; end; Append(Result, path[highi]); end; Result := SimplifyPath(Result, shapeTolerance, false); end; //------------------------------------------------------------------------------ function SmoothPaths(const paths: TPathsD; isClosedPath: Boolean; tension: double = 0; shapeTolerance: double = 0.1): TPathsD; var i, len: integer; begin len := Length(paths); SetLength(Result, len); for i := 0 to len -1 do Result[i] := SmoothPath(paths[i], isClosedPath, tension, shapeTolerance); end; //------------------------------------------------------------------------------ // GaussianBlur //------------------------------------------------------------------------------ procedure GaussianBlur(img: TImage32; rec: TRect; radius: Integer); var i, w,h, highX, x,y,yy,z,startz: Integer; expConst: double; gaussTable: array [-MaxBlur .. MaxBlur] of integer; wc: TWeightedColor; wca: TArrayOfWeightedColor; wcaColor: TArrayOfColor32; row: PColor32Array; wcRow: PWeightedColorArray; imgWidth: Integer; dst, pc: PColor32; const tableConst = 1024; sigma = 3; begin Types.IntersectRect(rec, rec, img.Bounds); if IsEmptyRect(rec) or (radius < 1) then Exit else if radius > MaxBlur then radius := MaxBlur; expConst := - 1 / (Sqr(radius) * 2 * Sqr(sigma)); gaussTable[0] := Round(tableConst * Exp(expConst)); for i := 1 to radius do begin gaussTable[i] := Round(tableConst * Exp(expConst * Sqr(i))); gaussTable[-i] := gaussTable[i]; end; RectWidthHeight(rec, w, h); setLength(wca, w * h); NewColor32Array(wcaColor, w * h, True); imgWidth := img.Width; highX := imgWidth -1; for y := 0 to h -1 do begin row := PColor32Array(@img.Pixels[(y + rec.Top) * imgWidth + rec.Left]); wcRow := PWeightedColorArray(@wca[y * w]); for x := 0 to w -1 do for z := max(0, x - radius) to min(highX, x + radius) do wcRow[x].Add(row[z], gaussTable[x-z]); end; // calculate colors for x := 0 to w * h - 1 do wcaColor[x] := wca[x].Color; dst := @img.Pixels[rec.Left + rec.Top * imgWidth]; imgWidth := imgWidth * SizeOf(TColor32); // convert to byte size for x := 0 to w -1 do begin pc := dst; inc(pc, x); for y := 0 to h -1 do begin wc.Reset; startz := max(0, y - radius); yy := startz * w; for z := startz to min(h -1, y + radius) do begin wc.Add(wcaColor[x + yy], gaussTable[y-z]); inc(yy, w); end; pc^ := wc.Color; inc(PByte(pc), imgWidth); // increment by byte size end; end; end; //------------------------------------------------------------------------------ // FastGaussian blur - and support functions //------------------------------------------------------------------------------ //http://blog.ivank.net/fastest-gaussian-blur.html //https://www.peterkovesi.com/papers/FastGaussianSmoothing.pdf function BoxesForGauss(stdDev, boxCnt: integer): TArrayOfInteger; var i, wl, wu, m: integer; wIdeal, mIdeal: double; begin NewIntegerArray(Result, boxCnt, True); wIdeal := Sqrt((12*stdDev*stdDev/boxCnt)+1); // Ideal averaging filter width wl := Floor(wIdeal); if not Odd(wl) then dec(wl); mIdeal := (-3*stdDev*stdDev +0.25*boxCnt*wl*wl +boxCnt*wl +0.75*boxCnt)/(wl+1); m := Floor(mIdeal) div 2; // nb: variation on Ivan Kutskir's code. wl := (wl -1) div 2; // It's better to do this here wu := wl+1; // than later in both BoxBlurH & BoxBlurV for i := 0 to boxCnt -1 do if i < m then Result[i] := wl else Result[i] := wu; end; //------------------------------------------------------------------------------ procedure FastGaussianBlur(img: TImage32; const rec: TRect; stdDev: integer; repeats: integer); begin FastGaussianBlur(img, rec, stdDev, stdDev, repeats); end; //------------------------------------------------------------------------------ procedure BoxBlurHLine(src, dst: PColor32; srcRiOffset: nativeint; count, w: integer; dstLast: PColor32; var v: TWeightedColor); var lastColor: TColor32; val: PWeightedColor; s, d: PColor32; begin lastColor := v.Color; if count > w then count := w; w := w - count; // The Delphi compiler sometimes is really stupid with // the CPU register allocation. With this, even if no actual // code is produced, the compiler happens to make better // decisions. val := @v; s := src; d := dst; if count > 0 then begin while count > 0 do begin if val.AddSubtract(PColor32Array(s)[srcRiOffset], s^) then lastColor := val.Color; inc(s); d^ := lastColor; inc(d); dec(count); end; count := w; while count > 0 do begin d^ := lastColor; inc(d); dec(count); end; end; while PByte(d) <= PByte(dstLast) do begin if val.AddNoneSubtract(s^) then lastColor := val.Color; inc(s); d^ := lastColor; inc(d); end; end; //------------------------------------------------------------------------------ procedure BoxBlurH(const src, dst: TArrayOfColor32; w,h, stdDev: integer); var i,j, ti, li, ri, re, ovr: integer; fv, val: TWeightedColor; lastColor: TColor32; stdDevW: integer; begin ovr := Max(0, stdDev - w); for i := 0 to h -1 do begin ti := i * w; li := ti; ri := ti +stdDev; re := ti +w -1; // idx of last pixel in row fv.Reset(src[ti]); val.Reset(src[ti], stdDev +1); for j := 0 to stdDev -1 - ovr do val.Add(src[ti + j]); if ovr > 0 then val.Add(clNone32, ovr); for j := 0 to stdDev do begin if ri <= re then val.Add(src[ri]) else val.Add(src[re]); // color of last pixel in row inc(ri); val.Subtract(fv); if ti <= re then dst[ti] := val.Color; inc(ti); end; // Skip "val.Color" calculation if both for-loops are skipped anyway stdDevW := w - stdDev*2 - 1; if (ti <= re) or (stdDevW > 0) then begin if w > 4 then // prevent the call-overhead if it would be slower than the inline version BoxBlurHLine(@src[li], @dst[ti], ri - li, re - ri + 1, stdDevW, @dst[re], val) else begin lastColor := val.Color; for j := stdDevW downto 1 do begin if ri <= re then begin if val.AddSubtract(src[ri], src[li]) then lastColor := val.Color; inc(ri); inc(li); end; dst[ti] := lastColor; inc(ti); end; while ti <= re do begin if val.AddNoneSubtract(src[li]) then lastColor := val.Color; inc(li); dst[ti] := lastColor; inc(ti); end; end; end; end; end; //------------------------------------------------------------------------------ procedure BoxBlurVLine(src, dst: PColor32; srcRiOffset: nativeint; widthBytes, count, h: integer; dstLast: PColor32; var v: TWeightedColor); var lastColor: TColor32; val: PWeightedColor; s, d: PColor32; begin lastColor := v.Color; if count > h then count := h; h := h - count; // The Delphi compiler sometimes is really stupid with // the CPU register allocation. With this, even if no actual // code is produced, the compiler happens to make better // decisions. val := @v; s := src; d := dst; if count > 0 then begin while count > 0 do begin if val.AddSubtract(PColor32Array(s)[srcRiOffset], s^) then lastColor := val.Color; inc(PByte(s), widthBytes); d^ := lastColor; inc(PByte(d), widthBytes); dec(count); end; count := h; while count > 0 do begin d^ := lastColor; inc(PByte(d), widthBytes); dec(count); end; end; while PByte(d) <= PByte(dstLast) do begin if val.AddNoneSubtract(s^) then lastColor := val.Color; inc(PByte(s), widthBytes); d^ := lastColor; inc(PByte(d), widthBytes); end; end; //------------------------------------------------------------------------------ procedure BoxBlurV(const src, dst: TArrayOfColor32; w, h, stdDev: integer); var i,j, ti, li, ri, re, ovr: integer; fv, val: TWeightedColor; lastColor: TColor32; stdDevH: integer; begin ovr := Max(0, stdDev - h); for i := 0 to w -1 do begin ti := i; li := ti; ri := ti + stdDev * w; re := ti +w *(h-1); // idx of last pixel in column fv.Reset(src[ti]); val.Reset(src[ti], stdDev +1); for j := 0 to stdDev -1 -ovr do val.Add(src[ti + j *w]); if ovr > 0 then val.Add(clNone32, ovr); for j := 0 to stdDev do begin if ri <= re then val.Add(src[ri]) else val.Add(src[re]); // color of last pixel in column inc(ri, w); val.Subtract(fv); if ti <= re then dst[ti] := val.Color; inc(ti, w); end; // Skip "val.Color" calculation if both for-loops are skipped anyway stdDevH := h - stdDev*2 - 1; if (ti <= re) or (stdDevH > 0) then begin if stdDevH > 4 then // prevent the call-overhead if it would be slower than the inline version BoxBlurVLine(@src[li], @dst[ti], ri - li, w * SizeOf(TColor32), re - ri + 1, stdDevH, @dst[re], val) else begin lastColor := val.Color; for j := stdDevH downto 1 do begin if ri <= re then begin if val.AddSubtract(src[ri], src[li]) then lastColor := val.Color; inc(ri, w); inc(li, w); end; dst[ti] := lastColor; inc(ti, w); end; while ti <= re do begin if val.AddNoneSubtract(src[li]) then lastColor := val.Color; inc(li, w); dst[ti] := lastColor; inc(ti, w); end; end; end; end; end; //------------------------------------------------------------------------------ procedure FastGaussianBlur(img: TImage32; const rec: TRect; stdDevX, stdDevY: integer; repeats: integer); var i,j,len, w,h: integer; rec2: TRect; boxesH: TArrayOfInteger; boxesV: TArrayOfInteger; src, dst: TArrayOfColor32; blurFullImage: Boolean; pSrc, pDst: PColor32; begin if not Assigned(img) then Exit; Types.IntersectRect(rec2, rec, img.Bounds); if IsEmptyRect(rec2) then Exit; blurFullImage := RectsEqual(rec2, img.Bounds); RectWidthHeight(rec2, w, h); if (Min(w, h) < 2) or ((stdDevX < 1) and (stdDevY < 1)) then Exit; len := w * h; NewColor32Array(src, len, True); // content is overwritten in BoxBlurH if blurFullImage then begin // Use the img.Pixels directly instead of copying the entire image into 'dst'. // The first thing the code does is BoxBlurH({source:=}dst, {dest:=}src, ...). dst := img.Pixels; end else begin // copy a rectangular region into 'dst' NewColor32Array(dst, len, True); pSrc := img.PixelRow[rec2.Top]; inc(pSrc, rec2.Left); pDst := @dst[0]; for i := 0 to h -1 do begin Move(pSrc^, pDst^, w * SizeOf(TColor32)); inc(pSrc, img.Width); inc(pDst, w); end; end; // do the blur inc(repeats); // now represents total iterations boxesH := BoxesForGauss(stdDevX, repeats); if stdDevY = stdDevX then boxesV := boxesH else boxesV := BoxesForGauss(stdDevY, repeats); img.BeginUpdate; try for j := 0 to repeats -1 do begin BoxBlurH(dst, src, w, h, boxesH[j]); BoxBlurV(src, dst, w, h, boxesV[j]); end; if not blurFullImage then begin // copy dst array back to image rect pDst := img.PixelRow[rec2.Top]; inc(pDst, rec2.Left); pSrc := @dst[0]; for i := 0 to h -1 do begin Move(pSrc^, pDst^, w * SizeOf(TColor32)); inc(pSrc, w); inc(pDst, img.Width); end; end; finally img.EndUpdate; end; end; //------------------------------------------------------------------------------ end.