diff --git a/Build.bat b/Build.bat index 9c713c7..fe41aea 100644 --- a/Build.bat +++ b/Build.bat @@ -4,8 +4,8 @@ msbuild.exe "Source\SKIAShellExtensions32.dproj" /target:Clean;Build /p:Platform msbuild.exe "Source\LottieTextEditor.dproj" /target:Clean;Build /p:Platform=Win64 /p:config=release msbuild.exe "Source\LottieTextEditor.dproj" /target:Clean;Build /p:Platform=Win32 /p:config=release -call D:\ETHEA\Certificate\SignFileWithSectico.bat D:\ETHEA\LottieShellExtensions\Bin32\LottieTextEditor.exe -call D:\ETHEA\Certificate\SignFileWithSectico.bat D:\ETHEA\LottieShellExtensions\Bin64\LottieTextEditor.exe +call D:\ETHEA\Certificate\SignFileWithSectico.bat D:\ETHEA\SkiaShellExtensions\Bin32\LottieTextEditor.exe +call D:\ETHEA\Certificate\SignFileWithSectico.bat D:\ETHEA\SkiaShellExtensions\Bin64\LottieTextEditor.exe :INNO "C:\Program Files (x86)\Inno Setup 6\iscc.exe" "D:\ETHEA\SKIAShellExtensions\Setup\SKIAShellExtensions.iss" diff --git a/Ext/SVGIconImageList/Image32/source/Clipper.Core.pas b/Ext/SVGIconImageList/Image32/source/Clipper.Core.pas index 0a76571..77bc276 100644 --- a/Ext/SVGIconImageList/Image32/source/Clipper.Core.pas +++ b/Ext/SVGIconImageList/Image32/source/Clipper.Core.pas @@ -2,12 +2,12 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * -* Website : http://www.angusj.com * +* Date : 22 November 2024 * +* Website : https://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : Core Clipper Library module * * Contains structures and functions used throughout the library * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) {$I Clipper.inc} @@ -18,12 +18,15 @@ interface SysUtils, Classes, Math; type +{$IFDEF USINGZ} + ZType = Int64; // or alternatively, ZType = double +{$ENDIF} PPoint64 = ^TPoint64; TPoint64 = record X, Y: Int64; {$IFDEF USINGZ} - Z: Int64; + Z: ZType; {$ENDIF} end; @@ -31,7 +34,7 @@ TPoint64 = record TPointD = record X, Y: double; {$IFDEF USINGZ} - Z: Int64; + Z: ZType; {$ENDIF} end; @@ -121,6 +124,7 @@ TListEx = class fCount : integer; fCapacity : integer; fList : TPointerList; + fSorted : Boolean; protected function UnsafeGet(idx: integer): Pointer; // no range checking procedure UnsafeSet(idx: integer; val: Pointer); @@ -130,14 +134,16 @@ TListEx = class destructor Destroy; override; procedure Clear; virtual; function Add(item: Pointer): integer; + procedure DeleteLast; procedure Swap(idx1, idx2: integer); - procedure Sort(Compare: TListSortCompare); + procedure Sort(Compare: TListSortCompareFunc); procedure Resize(count: integer); property Count: integer read fCount; + property Sorted: Boolean read fSorted; property Item[idx: integer]: Pointer read UnsafeGet; default; end; - TClipType = (ctNone, ctIntersection, ctUnion, ctDifference, ctXor); + TClipType = (ctNoClip, ctIntersection, ctUnion, ctDifference, ctXor); TPointInPolygonResult = (pipOn, pipInside, pipOutside); @@ -154,7 +160,7 @@ function IsPositive(const path: TPath64): Boolean; overload; function IsPositive(const path: TPathD): Boolean; overload; {$IFDEF INLINING} inline; {$ENDIF} -function __Trunc(val: double): Int64; {$IFDEF INLINE} inline; {$ENDIF} +function IsCollinear(const pt1, sharedPt, pt2: TPoint64): Boolean; function CrossProduct(const pt1, pt2, pt3: TPoint64): double; overload; {$IFDEF INLINING} inline; {$ENDIF} @@ -186,11 +192,11 @@ function PointsNearEqual(const pt1, pt2: TPointD; distanceSqrd: double): Boolean {$IFDEF INLINING} inline; {$ENDIF} {$IFDEF USINGZ} -function Point64(const X, Y: Int64; Z: Int64 = 0): TPoint64; overload; +function Point64(const X, Y: Int64; Z: ZType = 0): TPoint64; overload; {$IFDEF INLINING} inline; {$ENDIF} -function Point64(const X, Y: Double; Z: Int64 = 0): TPoint64; overload; +function Point64(const X, Y: Double; Z: ZType = 0): TPoint64; overload; {$IFDEF INLINING} inline; {$ENDIF} -function PointD(const X, Y: Double; Z: Int64 = 0): TPointD; overload; +function PointD(const X, Y: Double; Z: ZType = 0): TPointD; overload; {$IFDEF INLINING} inline; {$ENDIF} {$ELSE} function Point64(const X, Y: Int64): TPoint64; overload; {$IFDEF INLINING} inline; {$ENDIF} @@ -540,6 +546,7 @@ procedure TListEx.Clear; fList := nil; fCount := 0; fCapacity := 0; + fSorted := false; end; //------------------------------------------------------------------------------ @@ -555,6 +562,13 @@ function TListEx.Add(item: Pointer): integer; fList[fCount] := item; Result := fCount; inc(fCount); + fSorted := false; +end; +//------------------------------------------------------------------------------ + +procedure TListEx.DeleteLast; +begin + dec(fCount); end; //------------------------------------------------------------------------------ @@ -611,10 +625,11 @@ procedure QuickSort(SortList: TPointerList; L, R: Integer; end; //------------------------------------------------------------------------------ -procedure TListEx.Sort(Compare: TListSortCompare); +procedure TListEx.Sort(Compare: TListSortCompareFunc); begin if fCount < 2 then Exit; QuickSort(FList, 0, fCount - 1, Compare); + fSorted := true; end; //------------------------------------------------------------------------------ @@ -654,6 +669,7 @@ procedure TListEx.Swap(idx1, idx2: integer); p := fList[idx1]; fList[idx1] := fList[idx2]; fList[idx2] := p; + fSorted := false; end; //------------------------------------------------------------------------------ @@ -829,6 +845,9 @@ function ScalePath(const path: TPath64; sx, sy: double): TPath64; begin result[i].X := Round(path[i].X * sx); result[i].Y := Round(path[i].Y * sy); +{$IFDEF USINGZ} + result[i].Z := path[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -845,10 +864,16 @@ function ScalePath(const path: TPathD; sx, sy: double): TPath64; j := 1; result[0].X := Round(path[0].X * sx); result[0].Y := Round(path[0].Y * sy); +{$IFDEF USINGZ} + result[0].Z := path[0].Z; +{$ENDIF} for i := 1 to len -1 do begin result[j].X := Round(path[i].X * sx); result[j].Y := Round(path[i].Y * sy); +{$IFDEF USINGZ} + result[j].Z := path[i].Z; +{$ENDIF} if (result[j].X <> result[j-1].X) or (result[j].Y <> result[j-1].Y) then inc(j); end; @@ -866,10 +891,16 @@ function ScalePath(const path: TPath64; scale: double): TPath64; j := 1; result[0].X := Round(path[0].X * scale); result[0].Y := Round(path[0].Y * scale); +{$IFDEF USINGZ} + result[0].Z := path[0].Z; +{$ENDIF} for i := 1 to len -1 do begin result[j].X := Round(path[i].X * scale); result[j].Y := Round(path[i].Y * scale); +{$IFDEF USINGZ} + result[j].Z := path[i].Z; +{$ENDIF} if (result[j].X <> result[j-1].X) or (result[j].Y <> result[j-1].Y) then inc(j); end; @@ -887,6 +918,9 @@ function ScalePath(const path: TPathD; scale: double): TPath64; begin result[i].X := Round(path[i].X * scale); result[i].Y := Round(path[i].Y * scale); +{$IFDEF USINGZ} + result[i].Z := path[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -926,6 +960,9 @@ function ScalePathD(const path: TPath64; sx, sy: double): TPathD; begin result[i].X := path[i].X * sx; result[i].Y := path[i].Y * sy; +{$IFDEF USINGZ} + result[i].Z := path[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -939,6 +976,9 @@ function ScalePathD(const path: TPathD; sx, sy: double): TPathD; begin result[i].X := path[i].X * sx; result[i].Y := path[i].Y * sy; +{$IFDEF USINGZ} + result[i].Z := path[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -989,6 +1029,9 @@ function ScalePathsD(const paths: TPaths64; sx, sy: double): TPathsD; begin result[i][j].X := (paths[i][j].X * sx); result[i][j].Y := (paths[i][j].Y * sy); +{$IFDEF USINGZ} + result[i][j].Z := paths[i][j].Z; +{$ENDIF} end; end; end; @@ -1008,6 +1051,9 @@ function ScalePathsD(const paths: TPathsD; sx, sy: double): TPathsD; begin result[i][j].X := paths[i][j].X * sx; result[i][j].Y := paths[i][j].Y * sy; +{$IFDEF USINGZ} + result[i][j].Z := paths[i][j].Z; +{$ENDIF} end; end; end; @@ -1103,6 +1149,9 @@ function Path64(const pathD: TPathD): TPath64; begin Result[i].X := Round(pathD[i].X); Result[i].Y := Round(pathD[i].Y); +{$IFDEF USINGZ} + Result[i].Z := pathD[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -1117,6 +1166,9 @@ function PathD(const path: TPath64): TPathD; begin Result[i].X := path[i].X; Result[i].Y := path[i].Y; +{$IFDEF USINGZ} + Result[i].Z := path[i].Z; +{$ENDIF} end; end; //------------------------------------------------------------------------------ @@ -1347,7 +1399,7 @@ function ArrayOfPathsToPaths(const ap: TArrayOfPaths): TPaths64; //------------------------------------------------------------------------------ {$IFDEF USINGZ} -function Point64(const X, Y: Int64; Z: Int64): TPoint64; +function Point64(const X, Y: Int64; Z: ZType): TPoint64; begin Result.X := X; Result.Y := Y; @@ -1355,7 +1407,7 @@ function Point64(const X, Y: Int64; Z: Int64): TPoint64; end; //------------------------------------------------------------------------------ -function Point64(const X, Y: Double; Z: Int64): TPoint64; +function Point64(const X, Y: Double; Z: ZType): TPoint64; begin Result.X := Round(X); Result.Y := Round(Y); @@ -1363,7 +1415,7 @@ function Point64(const X, Y: Double; Z: Int64): TPoint64; end; //------------------------------------------------------------------------------ -function PointD(const X, Y: Double; Z: Int64): TPointD; +function PointD(const X, Y: Double; Z: ZType): TPointD; begin Result.X := X; Result.Y := Y; @@ -1827,6 +1879,72 @@ function IsPositive(const path: TPathD): Boolean; end; //------------------------------------------------------------------------------ +function TriSign(val: Int64): integer; // returns 0, 1 or -1 +{$IFDEF INLINING} inline; {$ENDIF} +begin + if (val < 0) then Result := -1 + else if (val > 1) then Result := 1 + else Result := 0; +end; +//------------------------------------------------------------------------------ + +type + TMultiplyUInt64Result = record + lo64: UInt64; + hi64 : UInt64; + end; + +function MultiplyUInt64(a, b: UInt64): TMultiplyUInt64Result; // #834, #835 +{$IFDEF INLINING} inline; {$ENDIF} +var + x1, x2, x3: UInt64; +begin + x1 := (a and $FFFFFFFF) * (b and $FFFFFFFF); + x2 := (a shr 32) * (b and $FFFFFFFF) + (x1 shr 32); + x3 := (a and $FFFFFFFF) * (b shr 32) + (x2 and $FFFFFFFF); + Result.lo64 := ((x3 and $FFFFFFFF) shl 32) or (x1 and $FFFFFFFF); + Result.hi64 := hi(a shr 32) * (b shr 32) + (x2 shr 32) + (x3 shr 32); +end; +//------------------------------------------------------------------------------ + +function ProductsAreEqual(a, b, c, d: Int64): Boolean; +var + absA,absB,absC,absD: UInt64; + absAB, absCD : TMultiplyUInt64Result; + signAB, signCD : integer; +begin + // nb: unsigned values will be needed for CalcOverflowCarry() + absA := UInt64(Abs(a)); + absB := UInt64(Abs(b)); + absC := UInt64(Abs(c)); + absD := UInt64(Abs(d)); + + absAB := MultiplyUInt64(absA, absB); + absCD := MultiplyUInt64(absC, absD); + + // nb: it's important to differentiate 0 values here from other values + signAB := TriSign(a) * TriSign(b); + signCD := TriSign(c) * TriSign(d); + + Result := (absAB.lo64 = absCD.lo64) and + (absAB.hi64 = absCD.hi64) and (signAB = signCD); +end; +//------------------------------------------------------------------------------ + +function IsCollinear(const pt1, sharedPt, pt2: TPoint64): Boolean; +var + a,b,c,d: Int64; +begin + a := sharedPt.X - pt1.X; + b := pt2.Y - sharedPt.Y; + c := sharedPt.Y - pt1.Y; + d := pt2.X - sharedPt.X; + // When checking for collinearity with very large coordinate values + // then ProductsAreEqual is more accurate than using CrossProduct. + Result := ProductsAreEqual(a, b, c, d); +end; +//------------------------------------------------------------------------------ + function CrossProduct(const pt1, pt2, pt3: TPoint64): double; begin result := CrossProduct( @@ -1925,14 +2043,14 @@ function CleanPath(const path: TPath64): TPath64; Result := nil; len := Length(path); while (len > 2) and - (CrossProduct(path[len-2], path[len-1], path[0]) = 0) do dec(len); + (IsCollinear(path[len-2], path[len-1], path[0])) do dec(len); SetLength(Result, len); if (len < 2) then Exit; prev := path[len -1]; j := 0; for i := 0 to len -2 do begin - if CrossProduct(prev, path[i], path[i+1]) = 0 then Continue; + if IsCollinear(prev, path[i], path[i+1]) then Continue; Result[j] := path[i]; inc(j); prev := path[i]; @@ -1942,6 +2060,14 @@ function CleanPath(const path: TPath64): TPath64; end; //------------------------------------------------------------------------------ +function GetSign(const val: double): integer; {$IFDEF INLINING} inline; {$ENDIF} +begin + if val = 0 then Result := 0 + else if val < 0 then Result := -1 + else Result := 1; +end; +//------------------------------------------------------------------------------ + function SegmentsIntersect(const s1a, s1b, s2a, s2b: TPoint64; inclusive: Boolean): boolean; var @@ -1961,35 +2087,11 @@ function SegmentsIntersect(const s1a, s1b, s2a, s2b: TPoint64; (res3 <> 0) or (res4 <> 0); // ensures not collinear end else begin - result := (CrossProduct(s1a, s2a, s2b) * CrossProduct(s1b, s2a, s2b) < 0) and - (CrossProduct(s2a, s1a, s1b) * CrossProduct(s2b, s1a, s1b) < 0); - end; -end; -//------------------------------------------------------------------------------ - -function __Trunc(val: double): Int64; {$IFDEF INLINE} inline; {$ENDIF} -var - exp: integer; - i64: UInt64 absolute val; -const - shl51: UInt64 = UInt64(1) shl 51; -begin - Result := 0; - if i64 = 0 then Exit; - exp := Integer(Cardinal(i64 shr 52) and $7FF) - 1023; - //nb: when exp == 1024 then val == INF or NAN. - if exp < 0 then - Exit - else if exp > 52 then - begin - Result := ((i64 and $1FFFFFFFFFFFFF) shl (exp - 52)) or (UInt64(1) shl exp) - end else - begin - Result := ((i64 and $1FFFFFFFFFFFFF) shr (52 - exp)) or (UInt64(1) shl exp); - //the following line will round - //if (i64 and (shl51 shr (exp)) <> 0) then inc(Result); + result := (GetSign(CrossProduct(s1a, s2a, s2b)) * + GetSign(CrossProduct(s1b, s2a, s2b)) < 0) and + (GetSign(CrossProduct(s2a, s1a, s1b)) * + GetSign(CrossProduct(s2b, s1a, s1b)) < 0); end; - if val < 0 then Result := -Result; end; //------------------------------------------------------------------------------ @@ -2011,6 +2113,9 @@ function GetSegmentIntersectPt(const ln1a, ln1b, ln2a, ln2b: TPoint64; else if t >= 1.0 then ip := ln1b; ip.X := Trunc(ln1a.X + t * dx1); ip.Y := Trunc(ln1a.Y + t * dy1); +{$IFDEF USINGZ} + ip.Z := 0; +{$ENDIF} end; //------------------------------------------------------------------------------ diff --git a/Ext/SVGIconImageList/Image32/source/Clipper.Engine.pas b/Ext/SVGIconImageList/Image32/source/Clipper.Engine.pas index 26ac220..e66db76 100644 --- a/Ext/SVGIconImageList/Image32/source/Clipper.Engine.pas +++ b/Ext/SVGIconImageList/Image32/source/Clipper.Engine.pas @@ -2,11 +2,11 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * -* Website : http://www.angusj.com * +* Date : 22 November 2024 * +* Website : https://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : This is the main polygon clipping module * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -219,7 +219,7 @@ TClipperBase = class FSucceeded : Boolean; FReverseSolution : Boolean; {$IFDEF USINGZ} - fDefaultZ : Int64; + fDefaultZ : Ztype; fZCallback : TZCallback64; {$ENDIF} procedure Reset; @@ -239,7 +239,7 @@ TClipperBase = class function PopHorz(out e: PActive): Boolean; {$IFDEF INLINING} inline; {$ENDIF} function StartOpenPath(e: PActive; const pt: TPoint64): POutPt; procedure UpdateEdgeIntoAEL(var e: PActive); - function IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; + procedure IntersectEdges(e1, e2: PActive; pt: TPoint64); procedure DeleteEdges(var e: PActive); procedure DeleteFromAEL(e: PActive); procedure AdjustCurrXAndCopyToSEL(topY: Int64); @@ -282,12 +282,12 @@ TClipperBase = class function ClearSolutionOnly: Boolean; procedure ExecuteInternal(clipType: TClipType; fillRule: TFillRule; usingPolytree: Boolean); - function BuildPaths(out closedPaths, openPaths: TPaths64): Boolean; + function BuildPaths(var closedPaths, openPaths: TPaths64): Boolean; function BuildTree(polytree: TPolyPathBase; out openPaths: TPaths64): Boolean; {$IFDEF USINGZ} procedure SetZ( e1, e2: PActive; var intersectPt: TPoint64); property ZCallback : TZCallback64 read fZCallback write fZCallback; - property DefaultZ : Int64 READ fDefaultZ write fDefaultZ; + property DefaultZ : Ztype read fDefaultZ write fDefaultZ; {$ENDIF} property Succeeded : Boolean read FSucceeded; public @@ -372,8 +372,7 @@ TClipperD = class(TClipperBase) // for floating point coordinates FInvScale: double; {$IFDEF USINGZ} fZCallback : TZCallbackD; - procedure ZCB(const bot1, top1, bot2, top2: TPoint64; - var intersectPt: TPoint64); + procedure ZCB(const bot1, top1, bot2, top2: TPoint64; var intersectPt: TPoint64); procedure CheckCallback; {$ENDIF} public @@ -900,7 +899,7 @@ function PointInOpPolygon(const pt: TPoint64; op: POutPt): TPointInPolygonResult while (op2 <> op) and (op2.pt.Y > pt.Y) do op2 := op2.next; if (op2 = op) then break; - // must have touched or crossed the pt.Y horizonal + // must have touched or crossed the pt.Y horizontal // and this must happen an even number of times if (op2.pt.Y = pt.Y) then // touching the horizontal @@ -1017,6 +1016,11 @@ procedure AddPathsToVertexList(const paths: TPaths64; GetMem(v, sizeof(TVertex) * totalVerts); vertexList.Add(v); + {$IF not defined(FPC) and (CompilerVersion <= 26.0)} + // Delphi 7-XE5 have a problem with "continue" and the + // code analysis, marking "ascending" as "not initialized" + ascending := False; + {$IFEND} for i := 0 to High(paths) do begin len := Length(paths[i]); @@ -1462,7 +1466,11 @@ function XYCoordsEqual(const pt1, pt2: TPoint64): Boolean; procedure TClipperBase.SetZ(e1, e2: PActive; var intersectPt: TPoint64); begin - if not Assigned(fZCallback) then Exit; + if not Assigned(fZCallback) then + begin + intersectPt.Z := 0; + Exit; + end; // prioritize subject vertices over clip vertices // and pass the subject vertices before clip vertices in the callback @@ -1834,8 +1842,8 @@ function IsValidAelOrder(resident, newcomer: PActive): Boolean; // resident must also have just been inserted else if IsLeftBound(resident) <> newcomerIsLeft then Result := newcomerIsLeft - else if (CrossProduct(PrevPrevVertex(resident).pt, - resident.bot, resident.top) = 0) then + else if IsCollinear(PrevPrevVertex(resident).pt, + resident.bot, resident.top) then Result := true else // otherwise compare turning direction of the alternate bound @@ -2105,7 +2113,7 @@ procedure TClipperBase.CleanCollinear(outRec: POutRec); // a duplicate point OR // not preserving collinear points OR // is a 180 degree 'spike' - if (CrossProduct(op2.prev.pt, op2.pt, op2.next.pt) = 0) and + if IsCollinear(op2.prev.pt, op2.pt, op2.next.pt) and (PointsEqual(op2.pt,op2.prev.pt) or PointsEqual(op2.pt,op2.next.pt) or not FPreserveCollinear or @@ -2381,7 +2389,7 @@ procedure TClipperBase.CheckJoinLeft(e: PActive; if PerpendicDistFromLineSqrd(pt, prev.bot, prev.top) > 0.25 then Exit end else if (e.currX <> prev.currX) then Exit; - if (CrossProduct(e.top, pt, prev.top) <> 0) then Exit; + if not IsCollinear(e.top, pt, prev.top) then Exit; if (e.outrec.idx = prev.outrec.idx) then AddLocalMaxPoly(prev, e, pt) @@ -2413,7 +2421,7 @@ procedure TClipperBase.CheckJoinRight(e: PActive; end else if (e.currX <> next.currX) then Exit; - if (CrossProduct(e.top, pt, next.top) <> 0) then Exit; + if not IsCollinear(e.top, pt, next.top) then Exit; if e.outrec.idx = next.outrec.idx then AddLocalMaxPoly(e, next, pt) else if e.outrec.idx < next.outrec.idx then @@ -2551,14 +2559,12 @@ function FindEdgeWithMatchingLocMin(e: PActive): PActive; {$IFNDEF USINGZ} {$HINTS OFF} {$ENDIF} -function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; +procedure TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64); var e1WindCnt, e2WindCnt, e1WindCnt2, e2WindCnt2: Integer; e3: PActive; - op2: POutPt; + op, op2: POutPt; begin - Result := nil; - // MANAGE OPEN PATH INTERSECTIONS SEPARATELY ... if FHasOpenPaths and (IsOpen(e1) or IsOpen(e2)) then begin @@ -2583,7 +2589,7 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; // toggle contribution ... if IsHotEdge(e1) then begin - Result := AddOutPt(e1, pt); + op := AddOutPt(e1, pt); if IsFront(e1) then e1.outrec.frontE := nil else e1.outrec.backE := nil; @@ -2605,15 +2611,14 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; if e1.windDx > 0 then SetSides(e3.outrec, e1, e3) else SetSides(e3.outrec, e3, e1); - Result := e3.outrec.pts; Exit; end else - Result := StartOpenPath(e1, pt); + op := StartOpenPath(e1, pt); end else - Result := StartOpenPath(e1, pt); + op := StartOpenPath(e1, pt); {$IFDEF USINGZ} - SetZ(e1, e2, Result.pt); + SetZ(e1, e2, op.pt); {$ENDIF} Exit; end; @@ -2677,20 +2682,20 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; if not (e1WindCnt in [0,1]) or not (e2WindCnt in [0,1]) or (not IsSamePolyType(e1, e2) and (fClipType <> ctXor)) then begin - Result := AddLocalMaxPoly(e1, e2, pt); + op := AddLocalMaxPoly(e1, e2, pt); {$IFDEF USINGZ} - if Assigned(Result) then SetZ(e1, e2, Result.pt); + if Assigned(op) then SetZ(e1, e2, op.pt); {$ENDIF} end else if IsFront(e1) or (e1.outrec = e2.outrec) then begin // this 'else if' condition isn't strictly needed but - // it's sensible to split polygons that ony touch at + // it's sensible to split polygons that only touch at // a common vertex (not at common edges). - Result := AddLocalMaxPoly(e1, e2, pt); + op := AddLocalMaxPoly(e1, e2, pt); {$IFDEF USINGZ} op2 := AddLocalMinPoly(e1, e2, pt); - if Assigned(Result) then SetZ(e1, e2, Result.pt); + if Assigned(op) then SetZ(e1, e2, op.pt); SetZ(e1, e2, op2.pt); {$ELSE} AddLocalMinPoly(e1, e2, pt); @@ -2698,10 +2703,10 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; end else begin // can't treat as maxima & minima - Result := AddOutPt(e1, pt); + op := AddOutPt(e1, pt); {$IFDEF USINGZ} op2 := AddOutPt(e2, pt); - SetZ(e1, e2, Result.pt); + SetZ(e1, e2, op.pt); SetZ(e1, e2, op2.pt); {$ELSE} AddOutPt(e2, pt); @@ -2713,17 +2718,17 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; // if one or other edge is 'hot' ... else if IsHotEdge(e1) then begin - Result := AddOutPt(e1, pt); + op := AddOutPt(e1, pt); {$IFDEF USINGZ} - SetZ(e1, e2, Result.pt); + SetZ(e1, e2, op.pt); {$ENDIF} SwapOutRecs(e1, e2); end else if IsHotEdge(e2) then begin - Result := AddOutPt(e2, pt); + op := AddOutPt(e2, pt); {$IFDEF USINGZ} - SetZ(e1, e2, Result.pt); + SetZ(e1, e2, op.pt); {$ENDIF} SwapOutRecs(e1, e2); end @@ -2751,32 +2756,32 @@ function TClipperBase.IntersectEdges(e1, e2: PActive; pt: TPoint64): POutPt; if not IsSamePolyType(e1, e2) then begin - Result := AddLocalMinPoly(e1, e2, pt, false); + op := AddLocalMinPoly(e1, e2, pt, false); {$IFDEF USINGZ} - SetZ(e1, e2, Result.pt); + SetZ(e1, e2, op.pt); {$ENDIF} end else if (e1WindCnt = 1) and (e2WindCnt = 1) then begin - Result := nil; + op := nil; case FClipType of ctIntersection: if (e1WindCnt2 <= 0) or (e2WindCnt2 <= 0) then Exit - else Result := AddLocalMinPoly(e1, e2, pt, false); + else op := AddLocalMinPoly(e1, e2, pt, false); ctUnion: if (e1WindCnt2 <= 0) and (e2WindCnt2 <= 0) then - Result := AddLocalMinPoly(e1, e2, pt, false); + op := AddLocalMinPoly(e1, e2, pt, false); ctDifference: if ((GetPolyType(e1) = ptClip) and (e1WindCnt2 > 0) and (e2WindCnt2 > 0)) or ((GetPolyType(e1) = ptSubject) and (e1WindCnt2 <= 0) and (e2WindCnt2 <= 0)) then - Result := AddLocalMinPoly(e1, e2, pt, false); + op := AddLocalMinPoly(e1, e2, pt, false); else // xOr - Result := AddLocalMinPoly(e1, e2, pt, false); + op := AddLocalMinPoly(e1, e2, pt, false); end; {$IFDEF USINGZ} - if assigned(Result) then SetZ(e1, e2, Result.pt); + if assigned(op) then SetZ(e1, e2, op.pt); {$ENDIF} end; end; @@ -2840,7 +2845,7 @@ procedure TClipperBase.ExecuteInternal(clipType: TClipType; Y: Int64; e: PActive; begin - if clipType = ctNone then Exit; + if clipType = ctNoClip then Exit; FFillRule := fillRule; FClipType := clipType; Reset; @@ -3523,7 +3528,7 @@ procedure TClipperBase.DoHorizontal(horzEdge: PActive); end; if IsHotEdge(horzEdge) then begin - //nb: The outrec containining the op returned by IntersectEdges + //nb: The outrec containing the op returned by IntersectEdges //above may no longer be associated with horzEdge. FHorzSegList.Add(GetLastOp(horzEdge)); end; @@ -3665,17 +3670,15 @@ function TClipperBase.DoMaxima(e: PActive): PActive; end; //------------------------------------------------------------------------------ -function TClipperBase.BuildPaths(out closedPaths, openPaths: TPaths64): Boolean; +function TClipperBase.BuildPaths(var closedPaths, openPaths: TPaths64): Boolean; var - i, cntClosed, cntOpen: Integer; + i: Integer; + closedCnt, openCnt: integer; outRec: POutRec; begin + closedCnt := Length(closedPaths); + openCnt := Length(openPaths); try - cntClosed := 0; cntOpen := 0; - SetLength(closedPaths, FOutRecList.Count); - if FHasOpenPaths then - SetLength(openPaths, FOutRecList.Count); - i := 0; while i < FOutRecList.Count do begin @@ -3685,22 +3688,21 @@ function TClipperBase.BuildPaths(out closedPaths, openPaths: TPaths64): Boolean; if outRec.isOpen then begin + SetLength(openPaths, openCnt +1); if BuildPath(outRec.pts, FReverseSolution, - true, openPaths[cntOpen]) then - inc(cntOpen); + true, openPaths[openCnt]) then inc(openCnt); end else begin // nb: CleanCollinear can add to FOutRecList CleanCollinear(outRec); // closed paths should always return a Positive orientation // except when ReverseSolution == true + SetLength(closedPaths, closedCnt +1); if BuildPath(outRec.pts, FReverseSolution, - false, closedPaths[cntClosed]) then - inc(cntClosed); + false, closedPaths[closedCnt]) then + inc(closedCnt); end; end; - SetLength(closedPaths, cntClosed); - SetLength(openPaths, cntOpen); result := true; except result := false; diff --git a/Ext/SVGIconImageList/Image32/source/Clipper.Minkowski.pas b/Ext/SVGIconImageList/Image32/source/Clipper.Minkowski.pas index bacb3ea..d2e9705 100644 --- a/Ext/SVGIconImageList/Image32/source/Clipper.Minkowski.pas +++ b/Ext/SVGIconImageList/Image32/source/Clipper.Minkowski.pas @@ -5,7 +5,7 @@ * Date : 21 December 2023 * * Copyright : Angus Johnson 2010-2022 * * Purpose : Minkowski Addition and Difference * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) {$I Clipper.inc} diff --git a/Ext/SVGIconImageList/Image32/source/Clipper.Offset.pas b/Ext/SVGIconImageList/Image32/source/Clipper.Offset.pas index bf95bf8..e9c474b 100644 --- a/Ext/SVGIconImageList/Image32/source/Clipper.Offset.pas +++ b/Ext/SVGIconImageList/Image32/source/Clipper.Offset.pas @@ -2,11 +2,11 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 14 March 2024 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2024 * +* Date : 22 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2010-2025 * * Purpose : Path Offset (Inflate/Shrink) * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) {$I Clipper.inc} @@ -41,8 +41,6 @@ TGroup = class endType : TEndType; reversed : Boolean; lowestPathIdx : integer; - areasList : TDoubleArray; - isHoleList : BooleanArray; constructor Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType); end; @@ -72,12 +70,19 @@ TClipperOffset = class fDeltaCallback64 : TDeltaCallback64; {$IFDEF USINGZ} fZCallback64 : TZCallback64; - procedure AddPoint(x,y: double; z: Int64); overload; + procedure ZCB(const bot1, top1, bot2, top2: TPoint64; + var intersectPt: TPoint64); + procedure AddPoint(x,y: double; z: ZType); overload; + procedure AddPoint(const pt: TPoint64); overload; + {$IFDEF INLINING} inline; {$ENDIF} + procedure AddPoint(const pt: TPoint64; newZ: ZType); overload; + {$IFDEF INLINING} inline; {$ENDIF} {$ELSE} procedure AddPoint(x,y: double); overload; -{$ENDIF} + procedure AddPoint(const pt: TPoint64); overload; {$IFDEF INLINING} inline; {$ENDIF} +{$ENDIF} procedure DoSquare(j, k: Integer); procedure DoBevel(j, k: Integer); procedure DoMiter(j, k: Integer; cosA: Double); @@ -86,7 +91,7 @@ TClipperOffset = class procedure BuildNormals; procedure DoGroupOffset(group: TGroup); - procedure OffsetPolygon(isShrinking: Boolean; area_: double); + procedure OffsetPolygon; procedure OffsetOpenJoined; procedure OffsetOpenPath; function CalcSolutionCapacity: integer; @@ -137,6 +142,20 @@ implementation TwoPi : Double = 2 * PI; InvTwoPi : Double = 1/(2 * PI); +// Clipper2 approximates arcs by using series of relatively short straight +//line segments. And logically, shorter line segments will produce better arc +// approximations. But very short segments can degrade performance, usually +// with little or no discernable improvement in curve quality. Very short +// segments can even detract from curve quality, due to the effects of integer +// rounding. Since there isn't an optimal number of line segments for any given +// arc radius (that perfectly balances curve approximation with performance), +// arc tolerance is user defined. Nevertheless, when the user doesn't define +// an arc tolerance (ie leaves alone the 0 default value), the calculated +// default arc tolerance (offset_radius / 500) generally produces good (smooth) +// arc approximations without producing excessively small segment lengths. +// See also: https://www.angusj.com/clipper2/Docs/Trigonometry.htm +const arc_const = 0.002; // <-- 1/500 + //------------------------------------------------------------------------------ // Miscellaneous offset support functions //------------------------------------------------------------------------------ @@ -232,9 +251,7 @@ function UnsafeGet(List: TList; Index: Integer): Pointer; constructor TGroup.Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType); var i, len: integer; - a: double; isJoined: boolean; - pb: PBoolean; begin Self.joinType := jt; Self.endType := et; @@ -246,29 +263,13 @@ constructor TGroup.Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType); paths[i] := StripDuplicates(pathsIn[i], isJoined); reversed := false; - SetLength(isHoleList, len); - SetLength(areasList, len); if (et = etPolygon) then begin - pb := @isHoleList[0]; - for i := 0 to len -1 do - begin - a := Area(paths[i]); - pb^ := a < 0; - inc(pb); - end; - // the lowermost path must be an outer path, so if its orientation is // negative, then flag that the whole group is 'reversed' (so negate // delta etc.) as this is much more efficient than reversing every path. lowestPathIdx := GetLowestPolygonIdx(pathsIn); - reversed := (lowestPathIdx >= 0) and isHoleList[lowestPathIdx]; - if not reversed then Exit; - pb := @isHoleList[0]; - for i := 0 to len -1 do - begin - pb^ := not pb^; inc(pb); - end; + reversed := (lowestPathIdx >= 0) and (Area(pathsIn[lowestPathIdx]) < 0); end else lowestPathIdx := -1; end; @@ -357,7 +358,6 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); i,j, len, steps: Integer; r, stepsPer360, arcTol: Double; absDelta: double; - isShrinking: Boolean; rec: TRect64; pt0: TPoint64; begin @@ -378,13 +378,12 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); if (group.joinType = jtRound) or (group.endType = etRound) then begin // calculate the number of steps required to approximate a circle - // (see http://www.angusj.com/clipper2/Docs/Trigonometry.htm) + // (see https://www.angusj.com/clipper2/Docs/Trigonometry.htm) // arcTol - when arc_tolerance_ is undefined (0) then curve imprecision // will be relative to the size of the offset (delta). Obviously very //large offsets will almost always require much less precision. - arcTol := Iif(fArcTolerance > 0.01, - Min(absDelta, fArcTolerance), - Log10(2 + absDelta) * 0.25); // empirically derived + arcTol := Iif(fArcTolerance > 0.0, + Min(absDelta, fArcTolerance), absDelta * arc_const); stepsPer360 := Pi / ArcCos(1 - arcTol / absDelta); if (stepsPer360 > absDelta * Pi) then @@ -397,9 +396,6 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); for i := 0 to High(group.paths) do begin - isShrinking := (group.endType = etPolygon) and - (group.reversed = ((fGroupDelta < 0) = group.isHoleList[i])); - fInPath := group.paths[i]; fNorms := nil; len := Length(fInPath); @@ -450,7 +446,7 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); BuildNormals; if fEndType = etPolygon then - OffsetPolygon(isShrinking, group.areasList[i]) + OffsetPolygon else if fEndType = etJoined then OffsetOpenJoined else @@ -465,6 +461,7 @@ procedure TClipperOffset.BuildNormals; begin len := Length(fInPath); SetLength(fNorms, len); + if len = 0 then Exit; for i := 0 to len-2 do fNorms[i] := GetUnitNormal(fInPath[i], fInPath[i+1]); fNorms[len -1] := GetUnitNormal(fInPath[len -1], fInPath[0]); @@ -495,33 +492,26 @@ function TClipperOffset.CalcSolutionCapacity: integer; end; //------------------------------------------------------------------------------ -procedure TClipperOffset.OffsetPolygon(isShrinking: Boolean; area_: double); +procedure TClipperOffset.OffsetPolygon; var i,j: integer; begin j := high(fInPath); for i := 0 to high(fInPath) do OffsetPoint(i, j); - - // make sure that polygon areas aren't reversing which would indicate - // that the polygon has shrunk too far and that it should be discarded. - // See also - #593 & #715 - if isShrinking and (area_ <> 0) and // area = 0.0 when JoinType.Joined - ((area_ < 0) <> (Area(fOutPath) < 0)) then Exit; - UpdateSolution; end; //------------------------------------------------------------------------------ procedure TClipperOffset.OffsetOpenJoined; begin - OffsetPolygon(false, 0); + OffsetPolygon; fInPath := ReversePath(fInPath); // Rebuild normals // BuildNormals; fNorms := ReversePath(fNorms); fNorms := ShiftPath(fNorms, 1); fNorms := NegatePath(fNorms); - OffsetPolygon(true, 0); + OffsetPolygon; end; //------------------------------------------------------------------------------ @@ -647,6 +637,10 @@ procedure TClipperOffset.ExecuteInternal(delta: Double); PreserveCollinear := fPreserveCollinear; // the solution should retain the orientation of the input ReverseSolution := fReverseSolution <> pathsReversed; +{$IFDEF USINGZ} + ZCallback := ZCB; +{$ENDIF} + AddSubject(fSolution); if assigned(fSolutionTree) then Execute(ctUnion, fillRule, fSolutionTree, dummy); @@ -701,7 +695,21 @@ procedure TClipperOffset.Execute(delta: Double; polytree: TPolyTree64); //------------------------------------------------------------------------------ {$IFDEF USINGZ} -procedure TClipperOffset.AddPoint(x,y: double; z: Int64); +procedure TClipperOffset.ZCB(const bot1, top1, bot2, top2: TPoint64; + var intersectPt: TPoint64); +begin + if (bot1.Z <> 0) and + ((bot1.Z = bot2.Z) or (bot1.Z = top2.Z)) then intersectPt.Z := bot1.Z + else if (bot2.Z <> 0) and (bot2.Z = top1.Z) then intersectPt.Z := bot2.Z + else if (top1.Z <> 0) and (top1.Z = top2.Z) then intersectPt.Z := top1.Z + else if Assigned(ZCallback) then + ZCallback(bot1, top1, bot2, top2, intersectPt); +end; +{$ENDIF} +//------------------------------------------------------------------------------ + +{$IFDEF USINGZ} +procedure TClipperOffset.AddPoint(x,y: double; z: ZType); {$ELSE} procedure TClipperOffset.AddPoint(x,y: double); {$ENDIF} @@ -724,22 +732,33 @@ procedure TClipperOffset.AddPoint(x,y: double); end; //------------------------------------------------------------------------------ +{$IFDEF USINGZ} +procedure TClipperOffset.AddPoint(const pt: TPoint64; newZ: ZType); +begin + AddPoint(pt.X, pt.Y, newZ); +end; +//------------------------------------------------------------------------------ + procedure TClipperOffset.AddPoint(const pt: TPoint64); begin -{$IFDEF USINGZ} AddPoint(pt.X, pt.Y, pt.Z); +end; +//------------------------------------------------------------------------------ + {$ELSE} +procedure TClipperOffset.AddPoint(const pt: TPoint64); +begin AddPoint(pt.X, pt.Y); -{$ENDIF} end; //------------------------------------------------------------------------------ +{$ENDIF} function IntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPointD): TPointD; var m1,b1,m2,b2: double; begin result := NullPointD; - //see http://astronomy.swin.edu.au/~pbourke/geometry/lineline2d/ + //see https://paulbourke.net/geometry/pointlineplane/#i2l if (ln1B.X = ln1A.X) then begin if (ln2B.X = ln2A.X) then exit; //parallel lines @@ -911,10 +930,8 @@ procedure TClipperOffset.DoRound(j, k: Integer; angle: double); // when fDeltaCallback64 is assigned, fGroupDelta won't be constant, // so we'll need to do the following calculations for *every* vertex. absDelta := Abs(fGroupDelta); - arcTol := Iif(fArcTolerance > 0.01, - Min(absDelta, fArcTolerance), - Log10(2 + absDelta) * 0.25); // empirically derived - //http://www.angusj.com/clipper2/Docs/Trigonometry.htm + arcTol := Iif(fArcTolerance > 0.0, + Min(absDelta, fArcTolerance), absDelta * arc_const); stepsPer360 := Pi / ArcCos(1 - arcTol / absDelta); if (stepsPer360 > absDelta * Pi) then stepsPer360 := absDelta * Pi; // avoid excessive precision @@ -984,11 +1001,20 @@ procedure TClipperOffset.DoRound(j, k: Integer; angle: double); if (cosA > -0.999) and (sinA * fGroupDelta < 0) then begin // is concave + // by far the simplest way to construct concave joins, especially those + // joining very short segments, is to insert 3 points that produce negative + // regions. These regions will be removed later by the finishing union + // operation. This is also the best way to ensure that path reversals + // (ie over-shrunk paths) are removed. +{$IFDEF USINGZ} + AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGroupDelta), fInPath[j].Z); + AddPoint(fInPath[j]); // (#405, #873) + AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGroupDelta), fInPath[j].Z); +{$ELSE} AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGroupDelta)); - // this extra point is the only (simple) way to ensure that - // path reversals are fully cleaned with the trailing clipper - AddPoint(fInPath[j]); // (#405) + AddPoint(fInPath[j]); // (#405, #873) AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGroupDelta)); +{$ENDIF} end else if (cosA > 0.999) and (fJoinType <> jtRound) then begin diff --git a/Ext/SVGIconImageList/Image32/source/Clipper.RectClip.pas b/Ext/SVGIconImageList/Image32/source/Clipper.RectClip.pas index 4e2da7d..a6898ba 100644 --- a/Ext/SVGIconImageList/Image32/source/Clipper.RectClip.pas +++ b/Ext/SVGIconImageList/Image32/source/Clipper.RectClip.pas @@ -2,11 +2,11 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2024 * -* Website : http://www.angusj.com * +* Date : 5 July 2024 * +* Website : https://www.angusj.com * * Copyright : Angus Johnson 2010-2024 * * Purpose : FAST rectangular clipping * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -650,9 +650,28 @@ function TRectClip64.Execute(const paths: TPaths64): TPaths64; end; //------------------------------------------------------------------------------ +function StartLocsAreClockwise(const startLocs: TList): Boolean; +var + i,j, res: integer; +begin + res := 0; + for i := 1 to startLocs.Count -1 do + begin + j := Ord(TLocation(startLocs[i])) - Ord(TLocation(startLocs[i - 1])); + case j of + -1: dec(res); + 1: inc(res); + -3: inc(res); + 3: dec(res); + end; + end; + result := res > 0; +end; +//------------------------------------------------------------------------------ + procedure TRectClip64.ExecuteInternal(const path: TPath64); var - i,highI : integer; + i,j, highI : integer; prevPt,ip,ip2 : TPoint64; loc, prevLoc : TLocation; loc2 : TLocation; @@ -661,6 +680,7 @@ procedure TRectClip64.ExecuteInternal(const path: TPath64); crossingLoc : TLocation; prevCrossLoc : TLocation; isCw : Boolean; + startLocsCW : Boolean; begin if (Length(path) < 3) then Exit; fStartLocs.Clear; @@ -797,10 +817,12 @@ procedure TRectClip64.ExecuteInternal(const path: TPath64); begin // yep, the path does fully contain rect // so add rect to the solution + startLocsCW := StartLocsAreClockwise(fStartLocs); for i := 0 to 3 do begin - Add(fRectPath[i]); - AddToEdge(fEdges[i*2], fResults[0]); + if startLocsCW then j := i else j := 3 - i; + Add(fRectPath[j]); + AddToEdge(fEdges[j*2], fResults[0]); end; end; end; @@ -840,7 +862,7 @@ procedure TRectClip64.CheckEdges; op2 := op; repeat - if (CrossProduct(op2.prev.pt, op2.pt, op2.next.pt) = 0) then + if IsCollinear(op2.prev.pt, op2.pt, op2.next.pt) then begin if op2 = op then begin @@ -1082,7 +1104,7 @@ function TRectClip64.GetPath(resultIdx: integer): TPath64; op2 := op.next; while Assigned(op2) and (op2 <> op) do begin - if (CrossProduct(op2.prev.pt, op2.pt, op2.next.pt) = 0) then + if IsCollinear(op2.prev.pt, op2.pt, op2.next.pt) then begin op := op2.prev; op2 := DisposeOp(op2); diff --git a/Ext/SVGIconImageList/Image32/source/Clipper.inc b/Ext/SVGIconImageList/Image32/source/Clipper.inc index 5b15f92..17da40d 100644 --- a/Ext/SVGIconImageList/Image32/source/Clipper.inc +++ b/Ext/SVGIconImageList/Image32/source/Clipper.inc @@ -7,7 +7,7 @@ {.$DEFINE USINGZ} /////////////////////////////////////////////////////////////////////////////// -//COMPILER DIFINED PREPROCESSOR DIRECTIVES (ie. do not touch ;)) +//COMPILER DEFINED PREPROCESSOR DIRECTIVES (ie. do not touch ;)) /////////////////////////////////////////////////////////////////////////////// {$IFDEF FPC} diff --git a/Ext/SVGIconImageList/Image32/source/Clipper.pas b/Ext/SVGIconImageList/Image32/source/Clipper.pas index 1c36223..09a33f4 100644 --- a/Ext/SVGIconImageList/Image32/source/Clipper.pas +++ b/Ext/SVGIconImageList/Image32/source/Clipper.pas @@ -2,11 +2,11 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 21 December 2023 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2023 * +* Date : 7 May 2024 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2010-2024 * * Purpose : This module provides a simple interface to the Clipper Library * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -42,6 +42,7 @@ interface frNonZero = Clipper.Core.frNonZero; frPositive = Clipper.Core.frPositive; frNegative = Clipper.Core.frNegative; + jtBevel = Clipper.Offset.jtBevel; jtSquare = Clipper.Offset.jtSquare; jtRound = Clipper.Offset.jtRound; jtMiter = Clipper.Offset.jtMiter; @@ -51,7 +52,7 @@ interface etSquare = Clipper.Offset.etSquare; etRound = Clipper.Offset.etRound; - ctNone = Clipper.Core.ctNone; + ctNone = Clipper.Core.ctNoClip; ctIntersection = Clipper.Core.ctIntersection; ctUnion = Clipper.Core.ctUnion; ctDifference = Clipper.Core.ctDifference; @@ -753,9 +754,9 @@ function TrimCollinear(const p: TPath64; isOpenPath: Boolean = false): TPath64; if not isOpenPath then begin while (i < len -1) and - (CrossProduct(p[len -1], p[i], p[i+1]) = 0) do inc(i); + IsCollinear(p[len -1], p[i], p[i+1]) do inc(i); while (i < len -1) and - (CrossProduct(p[len -2], p[len -1], p[i]) = 0) do dec(len); + IsCollinear(p[len -2], p[len -1], p[i]) do dec(len); end; if (len - i < 3) then begin @@ -770,7 +771,7 @@ function TrimCollinear(const p: TPath64; isOpenPath: Boolean = false): TPath64; Result[0] := p[i]; j := 0; for i := i+1 to len -2 do - if CrossProduct(result[j], p[i], p[i+1]) <> 0 then + if not IsCollinear(result[j], p[i], p[i+1]) then begin inc(j); result[j] := p[i]; @@ -781,14 +782,14 @@ function TrimCollinear(const p: TPath64; isOpenPath: Boolean = false): TPath64; inc(j); result[j] := p[len-1]; end - else if CrossProduct(result[j], p[len-1], result[0]) <> 0 then + else if not IsCollinear(result[j], p[len-1], result[0]) then begin inc(j); result[j] := p[len-1]; end else begin while (j > 1) and - (CrossProduct(result[j-1], result[j], result[0]) = 0) do dec(j); + IsCollinear(result[j-1], result[j], result[0]) do dec(j); if j < 2 then j := -1; end; SetLength(Result, j +1); @@ -820,7 +821,7 @@ function DistanceSqrd(const pt1, pt2: TPoint64): double; var x1,y1,x2,y2: double; begin - // nb: older versions of Delphi don't allow explicit typcasting + // nb: older versions of Delphi don't allow explicit typecasting x1 := pt1.X; y1 := pt1.Y; x2 := pt2.X; y2 := pt2.Y; result := Sqr(x1 - x2) + Sqr(y1 - y2); diff --git a/Ext/SVGIconImageList/Image32/source/Img32.CQ.pas b/Ext/SVGIconImageList/Image32/source/Img32.CQ.pas index 8acea08..2fae705 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.CQ.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.CQ.pas @@ -2,13 +2,13 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 10 April 2024 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2024 * +* Version : 4.7 * +* Date : 6 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : Color reduction for TImage32 * * : Uses Octree Color Quantization & Floyd / Steinberg Dithering * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -39,7 +39,7 @@ TColFreqRec = record // https://web.archive.org/web/20140605161956/ // http://www.microsoft.com/msj/archive/S3F1.aspx - TReduceType = (rtSimple, rtMedianCut, rtFrequency, rtMixed); + TReduceType = (rtSimple, rtMedianCut, rtFrequency); TOctree = class; TOctNodes8 = array[0 .. 7] of TOctNode; @@ -71,11 +71,12 @@ TOctNode = class TOctree = class private - fLeaves : cardinal; + fLeafCount : cardinal; fTop : TOctNode; ColorPxlCnt : integer; fReducible8 : TOctNodes8; fReduceType : TReduceType; + procedure AddInternal(color: TColor32; var node: TOctNode); procedure Delete(var node: TOctNode); procedure Add(color: TColor32); procedure GetNodeColor(var color: TColor32); @@ -83,8 +84,7 @@ TOctree = class function ReduceOne: Boolean; protected function BasicReduce(palSize: cardinal): TArrayOfColor32; - procedure FrequencyCut(palSize: integer; var cfArr: TArrayOfColFreq); - procedure MedianCut(palSize, arrLen: integer; var cfArr: TArrayOfColFreq); + procedure MedianCut(palSize: integer; var cfArr: TArrayOfColFreq); public constructor Create; destructor Destroy; override; @@ -92,7 +92,7 @@ TOctree = class procedure BuildTree(image: TImage32); procedure ApplyPalette(image: TImage32); function GetColorFreqArray: TArrayOfColFreq; - property ColorCount: cardinal read fLeaves; + property ColorCount: cardinal read fLeafCount; // PixelCount: = Sum( leaves[ 0 .. n-1 ].Count ) // and semi-transparent pixels aren't counted property PixelCount: integer read ColorPxlCnt; @@ -101,7 +101,7 @@ TOctree = class function ReduceImage(image: TImage32; maxColors: Cardinal; useDithering: Boolean = true; reduceType: TReduceType = rtMedianCut): TArrayOfColor32; -function CreatePaletteOctree(image: TImage32; reduceType: TReduceType = rtMixed): TOctree; +function CreatePaletteOctree(image: TImage32; reduceType: TReduceType = rtMedianCut): TOctree; {$IFDEF MSWINDOWS} function CreateLogPalette(const palColors: TArrayOfColor32): TMaxLogPalette; @@ -424,6 +424,18 @@ procedure MedianCutInternal(var cfArr: TArrayOfColFreq; MedianCutInternal(cfArr, ints, idx, sizeAdjust, start, mid-1, level); MedianCutInternal(cfArr, ints, idx, sizeAdjust, mid, finish, level); end; +//------------------------------------------------------------------------------ + +procedure FrequencyCut(palSize: integer; var cfArr: TArrayOfColFreq); +var + i,len : integer; +begin + len := Length(cfArr); // total colors in octree + if palSize >= len then Exit; + PaletteSort(cfArr, 0, len -1, PalSortDescending); + for i := palSize to len -1 do + cfArr[i].freq := 0; +end; //------------------------------------------------------------------------------ // TOctNode methods @@ -562,21 +574,10 @@ procedure TOctNode.GetNodeColor(var color: TColor32); //------------------------------------------------------------------------------ function TOctNode.GetColor: TColor32; -var - argb: TARGB absolute Result; begin if palColor = UnassignedColor then - begin - argb.R := TotalR; - argb.G := TotalG; - argb.B := TotalB; - argb.A := 255; - palColor := Result; - end else - begin - Result := palColor; - end; - + palColor := Color32(255, TotalR, TotalG, TotalB); + Result := palColor; end; //------------------------------------------------------------------------------ @@ -586,7 +587,7 @@ function TOctNode.GetColor: TColor32; constructor TOctree.Create; begin fReduceType := rtMedianCut; - fLeaves := 0; + fLeafCount := 0; fTop := TOctNode.Create(0); fReducible8 := NullOctNodes8; end; @@ -613,7 +614,7 @@ procedure TOctree.Delete(var node: TOctNode); procedure TOctree.Reset; begin if Assigned(fTop) then Delete(fTop); - fLeaves := 0; + fLeafCount := 0; fTop := TOctNode.Create(0); fReducible8 := NullOctNodes8; end; @@ -683,13 +684,14 @@ function TOctree.ReduceOne: Boolean; end; node.Add(wc.Color); inc(node.Count, wc.Weight -1); - Dec(fLeaves, childCnt -1); + Dec(fLeafCount, childCnt -1); end; //------------------------------------------------------------------------------ -procedure TOctree.MedianCut(palSize, arrLen: integer; var cfArr: TArrayOfColFreq); +procedure TOctree.MedianCut(palSize: integer; var cfArr: TArrayOfColFreq); var i,j : integer; + arrLen : integer; sizeAdjust: cardinal; idxArrLen : integer; idxArr : TArrayOfInteger; @@ -699,6 +701,7 @@ procedure TOctree.MedianCut(palSize, arrLen: integer; var cfArr: TArrayOfColFreq // precondition: palSize == 2^n SetLength(idxArr, palSize +1); idxArrLen := 0; sizeAdjust := 0; + arrLen := Length(cfArr); MedianCutInternal(cfArr, idxArr, idxArrLen, sizeAdjust, 0, arrLen -1, palSize); idxArr[idxArrLen] := arrLen; @@ -722,19 +725,7 @@ procedure TOctree.MedianCut(palSize, arrLen: integer; var cfArr: TArrayOfColFreq end; //------------------------------------------------------------------------------ -procedure TOctree.FrequencyCut(palSize: integer; var cfArr: TArrayOfColFreq); -var - i,len : integer; -begin - len := Length(cfArr); // total colors in octree - if palSize >= len then Exit; - PaletteSort(cfArr, 0, len -1, PalSortDescending); - for i := palSize to len -1 do - cfArr[i].freq := 0; -end; -//------------------------------------------------------------------------------ - -procedure AddColor(octree: TOctree; color: TColor32; var node: TOctNode); +procedure TOctree.AddInternal(color: TColor32; var node: TOctNode); var idx, level: integer; child: TOctNode; @@ -749,25 +740,25 @@ procedure AddColor(octree: TOctree; color: TColor32; var node: TOctNode); if child.IsLeaf then begin child.Add(color); - Inc(octree.fLeaves); + Inc(fLeafCount); end else begin - child.Next := octree.fReducible8[level]; - octree.fReducible8[level] := child; - AddColor(octree, color, child); + child.Next := fReducible8[level]; + fReducible8[level] := child; + AddInternal(color, child); end; end else if child.IsLeaf then child.Add(color) else - AddColor(octree, color, child); + AddInternal(color, child); end; //------------------------------------------------------------------------------ procedure TOctree.Add(color: TColor32); begin inc(ColorPxlCnt); - AddColor(self, color, fTop); + AddInternal(color, fTop); end; //------------------------------------------------------------------------------ @@ -802,7 +793,7 @@ procedure TOctree.GetTreePalette(out colors: TArrayOfColor32); end; begin - SetLength(colors, fLeaves); + SetLength(colors, fLeafCount); count := 0; FillPalette(fTop); end; @@ -831,7 +822,7 @@ function TOctree.GetColorFreqArray: TArrayOfColFreq; end; begin - SetLength(Result, fLeaves); + SetLength(Result, fLeafCount); count := 0; FillPalette(fTop); end; @@ -876,7 +867,7 @@ function SortPaletteByLuminence(const pal: TArrayOfColor32): TArrayOfColor32; function TOctree.BasicReduce(palSize: cardinal): TArrayOfColor32; begin - while (fLeaves > palSize) and ReduceOne do; + while (fLeafCount > palSize) and ReduceOne do; GetTreePalette(Result); end; @@ -994,11 +985,20 @@ function CreatePaletteOctree(image: TImage32; reduceType: TReduceType): TOctree; function ReduceImage(image: TImage32; maxColors: Cardinal; useDithering: Boolean; reduceType: TReduceType): TArrayOfColor32; var - i, len : integer; - j : cardinal; - octree : TOctree; - pc : PARGB; - cfArr : TArrayOfColFreq; + i : integer; + j : Cardinal; + octree : TOctree; + pc : PARGB; + + cfArr1024 : TArrayOfColFreq; + cfaLen : integer; + + procedure BuildColorFreqArray1024; + begin + while (octree.ColorCount > 1024) and octree.ReduceOne do ; + cfArr1024 := octree.GetColorFreqArray; + end; + begin if MaxColors < 2 then MaxColors := 1 else @@ -1006,50 +1006,48 @@ function ReduceImage(image: TImage32; maxColors: Cardinal; octree := CreatePaletteOctree(image, reduceType); try - if octree.fReduceType = rtSimple then - begin - Result := octree.BasicReduce(maxColors); - octree.ApplyPalette(image); - Exit; - end; - - while (octree.ColorCount > 1024) and octree.ReduceOne do ; - cfArr := octree.GetColorFreqArray; - len := Length(cfArr); // total colors in octree case octree.fReduceType of - rtMedianCut: // mostly :) + rtSimple: + octree.BasicReduce(maxColors); + rtMedianCut: begin - octree.FrequencyCut(512, cfArr); // trim only very infrequent colors - octree.MedianCut(maxColors, Min(len, 512), cfArr); // otherwise MC + BuildColorFreqArray1024; + octree.MedianCut(maxColors, cfArr1024); end; rtFrequency: - octree.FrequencyCut(maxColors, cfArr); // trim just on frequency - rtMixed: begin - octree.FrequencyCut(maxColors * 4, cfArr); - octree.MedianCut(maxColors, Min(len, maxColors *4), cfArr); + BuildColorFreqArray1024; + FrequencyCut(maxColors, cfArr1024); // trim just on frequency end; end; - // build result palette - j := 0; - SetLength(Result, maxColors); - for i := 0 to len -1 do + // build the result palette + if octree.fReduceType = rtSimple then begin - if cfArr[i].freq > 0 then + octree.GetTreePalette(Result); + end else + begin + j := 0; + SetLength(Result, maxColors); + cfaLen := Length(cfArr1024); + for i := 0 to cfaLen -1 do begin - Result[j] := cfArr[i].color; - inc(j); - if j = maxColors then break; + if cfArr1024[i].freq > 0 then + begin + Result[j] := cfArr1024[i].color; + inc(j); + if j = maxColors then break; + end; end; - end; - Result := SortPaletteByLuminence(Result); + if j < maxColors then SetLength(Result, j); + Result := SortPaletteByLuminence(Result); - // update all (residual) nodes with its closest palette color match - for i := maxColors to len -1 do - cfArr[i].node.palColor := - GetNearestPaletteColor(cfArr[i].node.GetColor, Result); + // update all (residual) nodes with its closest palette color match + for i := maxColors to cfaLen -1 do + cfArr1024[i].node.palColor := + GetNearestPaletteColor(cfArr1024[i].node.GetColor, Result); + end; if useDithering then begin diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Clipper2.pas b/Ext/SVGIconImageList/Image32/source/Img32.Clipper2.pas index 4fa6708..eccd8dc 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Clipper2.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Clipper2.pas @@ -2,12 +2,12 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.3 * -* Date : 27 September 2022 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2022 * +* Version : 4.7 * +* Date : 6 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : Wrapper module for the Clipper library * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Draw.pas b/Ext/SVGIconImageList/Image32/source/Img32.Draw.pas index e7b89c4..6aea69c 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Draw.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Draw.pas @@ -2,30 +2,22 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 23 March 2024 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2024 * +* Version : 4.8 * +* Date : 2 February 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * * * Purpose : Polygon renderer for TImage32 * * * * License : Use, modification & distribution is subject to * * Boost Software License Ver 1 * -* http://www.boost.org/LICENSE_1_0.txt * +* https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface {$I Img32.inc} -// MemCheck may be useful for debugging (adds a minimal cost to performance) -{.$DEFINE MemCheck} - -// UseTrunc makes rendering thread safe, -// so it's generally preferred over Round and SetRoundMode(). -// See https://github.com/AngusJohnson/Image32/issues/45 -{$DEFINE UseTrunc} - uses SysUtils, Classes, Types, Math, Img32, Img32.Vector; @@ -58,6 +50,7 @@ TCustomRenderer = class {$IFDEF ABSTRACT_CLASSES} abstract {$ENDIF} fCurrLinePtr : Pointer; fPixelSize : integer; fChangeProc : TImage32ChangeProc; + fOpacity : Byte; protected procedure NotifyChange; function Initialize(imgBase: Pointer; @@ -67,32 +60,82 @@ TCustomRenderer = class {$IFDEF ABSTRACT_CLASSES} abstract {$ENDIF} // RenderProc: x & y refer to pixel coords in the destination image and // where x1 is the start (and left) and x2 is the end of the render procedure RenderProc(x1, x2, y: integer; alpha: PByte); virtual; abstract; + // RenderProcSkip: is called for every skipped line block if + // SupportsRenderProcSkip=True and the Rasterize() function skips scanlines. + procedure RenderProcSkip(const skippedRect: TRect); virtual; + // SetClipRect is called by the Rasterize() function with the + // rasterization clipRect. The default implementation does nothing. + procedure SetClipRect(const clipRect: TRect); virtual; + // If SupportsRenderProcSkip returns True the Rasterize() function + // will call RenderProcSkip() for every scanline where it didn't have + // anything to rasterize. + function SupportsRenderProcSkip: Boolean; virtual; + public + constructor Create; virtual; property ImgWidth: integer read fImgWidth; property ImgHeight: integer read fImgHeight; property ImgBase: Pointer read fImgBase; property PixelSize: integer read fPixelSize; + property Opacity: Byte read fOpacity write fOpacity; end; - TColorRenderer = class(TCustomRenderer) + TCustomColorRenderer = class(TCustomRenderer) private - fAlpha: Byte; fColor: TColor32; + protected + property Color: TColor32 read fColor write fColor; + public + procedure SetColor(value: TColor32); virtual; + end; + + TColorRenderer = class(TCustomColorRenderer) + private + fAlpha: Byte; protected procedure RenderProc(x1, x2, y: integer; alpha: PByte); override; function Initialize(targetImage: TImage32): Boolean; override; public - constructor Create(color: TColor32 = clNone32); - procedure SetColor(value: TColor32); + constructor Create(color: TColor32 = clNone32); reintroduce; + procedure SetColor(value: TColor32); override; end; - TAliasedColorRenderer = class(TCustomRenderer) - private - fColor: TColor32; + TAliasedColorRenderer = class(TCustomColorRenderer) protected function Initialize(targetImage: TImage32): Boolean; override; procedure RenderProc(x1, x2, y: integer; alpha: PByte); override; public - constructor Create(color: TColor32 = clNone32); + constructor Create(color: TColor32 = clNone32); reintroduce; + end; + + // TMaskRenderer masks all pixels inside the clipRect area + // where the alpha[]-array is zero. + TMaskRenderer = class(TCustomRenderer) + private + fClipRect: TRect; + protected + procedure SetClipRect(const clipRect: TRect); override; + procedure RenderProc(x1, x2, y: integer; alpha: PByte); override; + procedure RenderProcSkip(const skippedRect: TRect); override; + function SupportsRenderProcSkip: Boolean; override; + end; + + // TCustomRendererCache is used to not create Renderer + // objects for every DrawPolygon/DrawLine function call. The color + // of the TCustomColorRenderer will be changed by the DrawPolygon/ + // DrawLine method. + TCustomRendererCache = class(TObject) + private + fColorRenderer: TColorRenderer; + fAliasedColorRenderer: TAliasedColorRenderer; + fMaskRenderer: TMaskRenderer; + public + constructor Create; + destructor Destroy; override; + function GetColorRenderer(color: TColor32): TColorRenderer; + + property ColorRenderer: TColorRenderer read fColorRenderer; + property AliasedColorRenderer: TAliasedColorRenderer read fAliasedColorRenderer; + property MaskRenderer: TMaskRenderer read fMaskRenderer; end; TEraseRenderer = class(TCustomRenderer) @@ -101,8 +144,15 @@ TEraseRenderer = class(TCustomRenderer) end; TInverseRenderer = class(TCustomRenderer) + private + fBackImage : TImage32; + fCurrBackY : integer; + fCurrBkLinePtr : Pointer; protected + function GetSrcPixel(x, y: integer): Pointer; procedure RenderProc(x1, x2, y: integer; alpha: PByte); override; + public + constructor Create(bkImg: TImage32 = nil); reintroduce; end; TImageRenderer = class(TCustomRenderer) @@ -113,13 +163,13 @@ TImageRenderer = class(TCustomRenderer) fLastYY : integer; fMirrorY : Boolean; fBoundsProc : TBoundsProc; - function GetFirstBrushPixel(x, y: integer): PARGB; + function GetFirstBrushPixel(x, y: integer): PColor32; protected procedure RenderProc(x1, x2, y: integer; alpha: PByte); override; function Initialize(targetImage: TImage32): Boolean; override; public constructor Create(tileFillStyle: TTileFillStyle = tfsRepeat; - brushImage: TImage32 = nil); + brushImage: TImage32 = nil); reintroduce; destructor Destroy; override; procedure SetTileFillStyle(value: TTileFillStyle); property Image: TImage32 read fImage; @@ -136,7 +186,7 @@ TCustomGradientRenderer = class(TCustomRenderer) fColorsCnt : integer; procedure SetGradientFillStyle(value: TGradientFillStyle); virtual; public - constructor Create; + constructor Create; override; procedure SetParameters(startColor, endColor: TColor32; gradFillStyle: TGradientFillStyle = gfsClamp); virtual; procedure InsertColorStop(offsetFrac: double; color: TColor32); @@ -224,6 +274,11 @@ TBarycentricRenderer = class(TCustomRenderer) const line: TPathD; lineWidth: double; color: TColor32; endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto; miterLimit: double = 2); overload; + procedure DrawLine(img: TImage32; + const line: TPathD; lineWidth: double; color: TColor32; + rendererCache: TCustomRendererCache; + endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto; + miterLimit: double = 2); overload; procedure DrawLine(img: TImage32; const line: TPathD; lineWidth: double; renderer: TCustomRenderer; endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto; @@ -232,6 +287,10 @@ TBarycentricRenderer = class(TCustomRenderer) lineWidth: double; color: TColor32; endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto; miterLimit: double = 2); overload; + procedure DrawLine(img: TImage32; const lines: TPathsD; + lineWidth: double; color: TColor32; rendererCache: TCustomRendererCache; + endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto; + miterLimit: double = 2); overload; procedure DrawLine(img: TImage32; const lines: TPathsD; lineWidth: double; renderer: TCustomRenderer; endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto; @@ -245,30 +304,42 @@ TBarycentricRenderer = class(TCustomRenderer) endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); overload; procedure DrawDashedLine(img: TImage32; const line: TPathD; - dashPattern: TArrayOfInteger; patternOffset: PDouble; + dashPattern: TArrayOfDouble; patternOffset: PDouble; lineWidth: double; color: TColor32; - endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); overload; + endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto; + rendererCache: TCustomRendererCache = nil); overload; procedure DrawDashedLine(img: TImage32; const lines: TPathsD; - dashPattern: TArrayOfInteger; patternOffset: PDouble; + dashPattern: TArrayOfDouble; patternOffset: PDouble; lineWidth: double; color: TColor32; endStyle: TEndStyle; - joinStyle: TJoinStyle = jsAuto); overload; + joinStyle: TJoinStyle = jsAuto; + rendererCache: TCustomRendererCache = nil); overload; procedure DrawDashedLine(img: TImage32; const line: TPathD; - dashPattern: TArrayOfInteger; patternOffset: PDouble; + dashPattern: TArrayOfDouble; patternOffset: PDouble; lineWidth: double; renderer: TCustomRenderer; endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); overload; procedure DrawDashedLine(img: TImage32; const lines: TPathsD; - dashPattern: TArrayOfInteger; patternOffset: PDouble; + dashPattern: TArrayOfDouble; patternOffset: PDouble; lineWidth: double; renderer: TCustomRenderer; endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); overload; procedure DrawInvertedDashedLine(img: TImage32; - const line: TPathD; dashPattern: TArrayOfInteger; + const line: TPathD; dashPattern: TArrayOfDouble; patternOffset: PDouble; lineWidth: double; endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); overload; procedure DrawInvertedDashedLine(img: TImage32; - const lines: TPathsD; dashPattern: TArrayOfInteger; - patternOffset: PDouble; lineWidth: double; - endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); overload; + const lines: TPathsD; dashPattern: TArrayOfDouble; + patternOffset: PDouble; lineWidth: double; endStyle: TEndStyle; + joinStyle: TJoinStyle = jsAuto); overload; + // bkgndImg - an alternative background image + // (useful when drawing on a layered image) + procedure DrawInvertedDashedLine(img, bkgndImg: TImage32; + const line: TPathD; dashPattern: TArrayOfDouble; + patternOffset: PDouble; lineWidth: double; endStyle: TEndStyle; + joinStyle: TJoinStyle = jsAuto); overload; + procedure DrawInvertedDashedLine(img, bkgndImg: TImage32; + const lines: TPathsD; dashPattern: TArrayOfDouble; + patternOffset: PDouble; lineWidth: double; endStyle: TEndStyle; + joinStyle: TJoinStyle = jsAuto); overload; procedure DrawPolygon(img: TImage32; const polygon: TPathD; fillRule: TFillRule; color: TColor32); overload; @@ -276,6 +347,9 @@ TBarycentricRenderer = class(TCustomRenderer) fillRule: TFillRule; renderer: TCustomRenderer); overload; procedure DrawPolygon(img: TImage32; const polygons: TPathsD; fillRule: TFillRule; color: TColor32); overload; + procedure DrawPolygon(img: TImage32; const polygons: TPathsD; + fillRule: TFillRule; color: TColor32; + rendererCache: TCustomRendererCache); overload; procedure DrawPolygon(img: TImage32; const polygons: TPathsD; fillRule: TFillRule; renderer: TCustomRenderer); overload; @@ -297,6 +371,13 @@ TBarycentricRenderer = class(TCustomRenderer) // MISCELLANEOUS FUNCTIONS // ///////////////////////////////////////////////////////////////////////// + procedure EraseLine(img: TImage32; const line: TPathD; lineWidth: double; + endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto; + miterLimit: double = 2); overload; + procedure EraseLine(img: TImage32; const lines: TPathsD; lineWidth: double; + endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto; + miterLimit: double = 2); overload; + procedure ErasePolygon(img: TImage32; const polygon: TPathD; fillRule: TFillRule); overload; procedure ErasePolygon(img: TImage32; const polygons: TPathsD; @@ -310,28 +391,31 @@ TBarycentricRenderer = class(TCustomRenderer) const mask: TArrayOfByte; color: TColor32 = clBlack32); procedure Rasterize(const paths: TPathsD; - const clipRec: TRect; fillRule: TFillRule; renderer: TCustomRenderer); + const clipRec: TRect; fillRule: TFillRule; renderer: TCustomRenderer); overload; + procedure Rasterize(img: TImage32; const paths: TPathsD; + const clipRec: TRect; fillRule: TFillRule; renderer: TCustomRenderer); overload; implementation -{$IFDEF MemCheck} -resourcestring - sMemCheckError = 'Img32.Draw: Memory allocation error'; -{$ENDIF} +{$IFDEF CPUX86} +const + // Use faster Trunc for x86 code in this unit. + Trunc: function(Value: Double): Integer = __Trunc; +{$ENDIF CPUX86} type // A horizontal scanline contains any number of line fragments. A fragment // can be a number of pixels wide but it can't be more than one pixel high. - // TFragment = record - // botX, topX, dy, dydx: double; // ie x at bottom and top of scanline - // end; + PFragment = ^TFragment; + TFragment = record + botX, topX, dy, dydx: double; // ie x at bottom and top of scanline + end; TScanLine = record Y: integer; minX, maxX: integer; fragCnt: integer; - {$IFDEF MemCheck} total: integer; {$ENDIF} fragOffset: integer; end; PScanline = ^TScanline; @@ -446,16 +530,35 @@ function GetPixel(current: PARGB; delta: integer): PARGB; end; // ------------------------------------------------------------------------------ -function ReverseColors(const colors: TArrayOfGradientColor): TArrayOfGradientColor; +// Here "const" is used for optimization reasons, to skip the +// dyn-array reference counting. "const" for dyn-arrays doesn't +// prevent one from changing the array's content. +procedure ReverseColors(const colors: TArrayOfGradientColor); var - i, highI: integer; + highI: integer; + dst, src: ^TGradientColor; + // Not using a TGradientColor record for the temporary value + // allows the 64-bit compiler to use an XMM register for it. + tmpOffset: double; + tmpColor: TColor32; begin highI := High(colors); - SetLength(result, highI +1); - for i := 0 to highI do + + dst := @colors[0]; + src := @colors[highI]; + while PByte(dst) < PByte(src) do begin - result[i].color := colors[highI -i].color; - result[i].offset := 1 - colors[highI -i].offset; + tmpColor := dst.color; + tmpOffset := dst.offset; + + dst.color := src.color; + dst.offset := 1 - src.offset; + + src.color := tmpColor; + src.offset := 1 - tmpOffset; + + inc(dst); + dec(src); end; end; // ------------------------------------------------------------------------------ @@ -519,15 +622,9 @@ function MirrorQ(q, endQ: integer): integer; function MirrorD(d: double; colorCnt: integer): integer; begin dec(colorCnt); -{$IFDEF UseTrunc} // used in TSvgRadialGradientRenderer.RenderProc if Odd(Trunc(d)) then result := Trunc((1 - frac(d)) * colorCnt) else result := Trunc(frac(d) * colorCnt); -{$ELSE} - if Odd(Round(d)) then - result := Round((1 - frac(d)) * colorCnt) else - result := Round(frac(d) * colorCnt); -{$ENDIF} end; // ------------------------------------------------------------------------------ @@ -555,15 +652,9 @@ function SoftRptQ(q, endQ: integer): integer; function RepeatD(d: double; colorCnt: integer): integer; begin dec(colorCnt); -{$IFDEF UseTrunc} // used in TSvgRadialGradientRenderer.RenderProc if (d < 0) then result := Trunc((1 + frac(d)) * colorCnt) else result := Trunc(frac(d) * colorCnt); -{$ELSE} - if (d < 0) then - result := Round((1 + frac(d)) * colorCnt) else - result := Round(frac(d) * colorCnt); -{$ENDIF} end; // ------------------------------------------------------------------------------ @@ -577,12 +668,12 @@ function BlendColorUsingMask(bgColor, fgColor: TColor32; mask: Byte): TColor32; if fg.A = 0 then begin Result := bgColor; - res.A := MulBytes(res.A, not mask); + res.A := MulTable[res.A, not mask]; end else if bg.A = 0 then begin Result := fgColor; - res.A := MulBytes(res.A, mask); + res.A := MulTable[res.A, mask]; end else if (mask = 0) then Result := bgColor @@ -602,21 +693,23 @@ function BlendColorUsingMask(bgColor, fgColor: TColor32; mask: Byte): TColor32; // MakeColorGradient: using the supplied array of TGradientColor, // create an array of TColor32 of the specified length -function MakeColorGradient(const gradColors: TArrayOfGradientColor; - len: integer): TArrayOfColor32; +procedure MakeColorGradient(const gradColors: TArrayOfGradientColor; + len: integer; var result: TArrayOfColor32); var i,j, lenC: integer; - dist, offset1, offset2, step, pos: double; + dist, offset1, offset2, step, pos, reciprocalDistTimes255: double; color1, color2: TColor32; begin lenC := length(gradColors); if (len = 0) or (lenC < 2) then Exit; - SetLength(result, len); + if Length(result) <> len then // we can reuse the array + SetLength(result, len); color2 := gradColors[0].color; result[0] := color2; if len = 1 then Exit; + reciprocalDistTimes255 := 0; step := 1/(len-1); pos := step; offset2 := 0; @@ -627,9 +720,11 @@ function MakeColorGradient(const gradColors: TArrayOfGradientColor; dist := offset2 - offset1; color1 := color2; color2 := gradColors[i].color; + if dist > 0 then + reciprocalDistTimes255 := 255/dist; // 1/dist*255 while (pos <= dist) and (j < len) do begin - result[j] := BlendColorUsingMask(color1, color2, Round(pos/dist * 255)); + result[j] := BlendColorUsingMask(color1, color2, Round(pos * reciprocalDistTimes255)); inc(j); pos := pos + step; end; @@ -644,7 +739,7 @@ function MakeColorGradient(const gradColors: TArrayOfGradientColor; // ------------------------------------------------------------------------------ procedure AllocateScanlines(const polygons: TPathsD; - var scanlines: TArrayOfScanline; out fragments: PDouble; clipBottom, clipRight: integer); + const scanlines: TArrayOfScanline; var fragments: PFragment; clipBottom, clipRight: integer); var i,j, highI, highJ: integer; y1, y2: integer; @@ -656,18 +751,10 @@ procedure AllocateScanlines(const polygons: TPathsD; begin highJ := high(polygons[i]); if highJ < 2 then continue; -{$IFDEF UseTrunc} y1 := Trunc(polygons[i][highJ].Y); -{$ELSE} - y1 := Round(polygons[i][highJ].Y); -{$ENDIF} for j := 0 to highJ do begin -{$IFDEF UseTrunc} y2 := Trunc(polygons[i][j].Y); -{$ELSE} - y2 := Round(polygons[i][j].Y); -{$ENDIF} if y1 < y2 then begin // descending (but ignore edges outside the clipping range) @@ -709,10 +796,9 @@ procedure AllocateScanlines(const polygons: TPathsD; if j > 0 then begin psl.fragOffset := fragOff; - inc(fragOff, j * 4); // 4 doubles are needed for each fragment + inc(fragOff, j); end else psl.fragOffset := -1; - {$IFDEF MemCheck} psl.total := j; {$ENDIF} psl.fragCnt := 0; // reset for later psl.minX := clipRight; psl.maxX := 0; @@ -720,39 +806,38 @@ procedure AllocateScanlines(const polygons: TPathsD; dec(psl); end; // allocate fragments as a single block of memory - GetMem(fragments, fragOff * sizeOf(Double)); + GetMem(fragments, fragOff * sizeOf(TFragment)); end; // ------------------------------------------------------------------------------ procedure SplitEdgeIntoFragments(const pt1, pt2: TPointD; - const scanlines: TArrayOfScanline; fragments: PDouble; const clipRec: TRect); + const scanlines: TArrayOfScanline; fragments: PFragment; const clipRec: TRect); var x,y, dx,dy, absDx, dydx, dxdy: double; i, scanlineY, maxY, maxX: integer; psl: PScanLine; - pFrag: PDouble; + pFrag: PFragment; bot, top: TPointD; begin dy := pt1.Y - pt2.Y; - dx := pt2.X - pt1.X; - RectWidthHeight(clipRec, maxX, maxY); - absDx := abs(dx); if dy > 0 then begin // ASCENDING EDGE (+VE WINDING DIR) if dy < 0.0001 then Exit; //ignore near horizontals bot := pt1; top := pt2; - // exclude edges that are completely outside the top or bottom clip region - if (top.Y >= maxY) or (bot.Y <= 0) then Exit; end else begin // DESCENDING EDGE (-VE WINDING DIR) if dy > -0.0001 then Exit; //ignore near horizontals bot := pt2; top := pt1; - // exclude edges that are completely outside the top or bottom clip region - if (top.Y >= maxY) or (bot.Y <= 0) then Exit; end; + // exclude edges that are completely outside the top or bottom clip region + RectWidthHeight(clipRec, maxX, maxY); + if (top.Y >= maxY) or (bot.Y <= 0) then Exit; + + dx := pt2.X - pt1.X; + absDx := abs(dx); if absDx < 0.000001 then begin @@ -763,13 +848,8 @@ procedure SplitEdgeIntoFragments(const pt1, pt2: TPointD; // but still update maxX for each scanline the edge passes if bot.X > maxX then begin -{$IFDEF UseTrunc} for i := Min(maxY, Trunc(bot.Y)) downto Max(0, Trunc(top.Y)) do scanlines[i].maxX := maxX; -{$ELSE} - for i := Min(maxY, Round(bot.Y)) downto Max(0, Round(top.Y)) do - scanlines[i].maxX := maxX; -{$ENDIF} Exit; end; @@ -786,24 +866,14 @@ procedure SplitEdgeIntoFragments(const pt1, pt2: TPointD; begin if top.X >= maxX then begin -{$IFDEF UseTrunc} for i := Min(maxY, Trunc(bot.Y)) downto Max(0, Trunc(top.Y)) do scanlines[i].maxX := maxX; -{$ELSE} - for i := Min(maxY, Round(bot.Y)) downto Max(0, Round(top.Y)) do - scanlines[i].maxX := maxX; -{$ENDIF} Exit; end; // here the edge must be oriented bottom-right to top-left y := bot.Y - (bot.X - maxX) * Abs(dydx); -{$IFDEF UseTrunc} for i := Min(maxY, Trunc(bot.Y)) downto Max(0, Trunc(y)) do scanlines[i].maxX := maxX; -{$ELSE} - for i := Min(maxY, Round(bot.Y)) downto Max(0, Round(y)) do - scanlines[i].maxX := maxX; -{$ENDIF} bot.Y := y; if bot.Y <= 0 then Exit; bot.X := maxX; @@ -812,13 +882,8 @@ procedure SplitEdgeIntoFragments(const pt1, pt2: TPointD; begin // here the edge must be oriented bottom-left to top-right y := top.Y + (top.X - maxX) * Abs(dydx); -{$IFDEF UseTrunc} for i := Min(maxY, Trunc(y)) downto Max(0, Trunc(top.Y)) do scanlines[i].maxX := maxX; -{$ELSE} - for i := Min(maxY, Round(y)) downto Max(0, Round(top.Y)) do - scanlines[i].maxX := maxX; -{$ENDIF} top.Y := y; if top.Y >= maxY then Exit; top.X := maxX; @@ -837,11 +902,7 @@ procedure SplitEdgeIntoFragments(const pt1, pt2: TPointD; end; // SPLIT THE EDGE INTO MULTIPLE SCANLINE FRAGMENTS -{$IFDEF UseTrunc} scanlineY := Trunc(bot.Y); -{$ELSE} - scanlineY := Round(bot.Y); -{$ENDIF} if bot.Y = scanlineY then dec(scanlineY); // at the lower-most extent of the edge 'split' the first fragment @@ -849,61 +910,52 @@ procedure SplitEdgeIntoFragments(const pt1, pt2: TPointD; psl := @scanlines[scanlineY]; if psl.fragOffset < 0 then Exit; //a very rare event - {$IFDEF MemCheck} - if psl.fragCnt = psl.total then raise Exception.Create(sMemCheckError); - {$ENDIF} pFrag := fragments; - inc(pFrag, psl.fragOffset + psl.fragCnt * 4); + inc(pFrag, psl.fragOffset + psl.fragCnt); inc(psl.fragCnt); - pFrag^ := bot.X; inc(pFrag); + pFrag.botX := bot.X; if scanlineY <= top.Y then begin // the whole edge is within 1 scanline - pFrag^ := top.X; inc(pFrag); - pFrag^ := bot.Y - top.Y; inc(pFrag); - pFrag^ := dydx; + pFrag.topX := top.X; + pFrag.dy := bot.Y - top.Y; + pFrag.dydx := dydx; Exit; end; x := bot.X + (bot.Y - scanlineY) * dxdy; - pFrag^ := x; inc(pFrag); - pFrag^ := bot.Y - scanlineY; inc(pFrag); - pFrag^ := dydx; + pFrag.topX := x; + pFrag.dy := bot.Y - scanlineY; + pFrag.dydx := dydx; // 'split' subsequent fragments until the top fragment dec(psl); while psl.Y > top.Y do begin - {$IFDEF MemCheck} - if psl.fragCnt = psl.total then raise Exception.Create(sMemCheckError); - {$ENDIF} pFrag := fragments; - inc(pFrag, psl.fragOffset + psl.fragCnt * 4); + inc(pFrag, psl.fragOffset + psl.fragCnt); inc(psl.fragCnt); - pFrag^ := x; inc(pFrag); + pFrag.botX := x; x := x + dxdy; - pFrag^ := x; inc(pFrag); - pFrag^ := 1; inc(pFrag); - pFrag^ := dydx; + pFrag.topX := x; + pFrag.dy := 1; + pFrag.dydx := dydx; dec(psl); end; // and finally the top fragment - {$IFDEF MemCheck} - if psl.fragCnt = psl.total then raise Exception.Create(sMemCheckError); - {$ENDIF} pFrag := fragments; - inc(pFrag, psl.fragOffset + psl.fragCnt * 4); + inc(pFrag, psl.fragOffset + psl.fragCnt); inc(psl.fragCnt); - pFrag^ := x; inc(pFrag); - pFrag^ := top.X; inc(pFrag); - pFrag^ := psl.Y + 1 - top.Y; inc(pFrag); - pFrag^ := dydx; + pFrag.botX := x; + pFrag.topX := top.X; + pFrag.dy := psl.Y + 1 - top.Y; + pFrag.dydx := dydx; end; // ------------------------------------------------------------------------------ -procedure InitializeScanlines(var polygons: TPathsD; - const scanlines: TArrayOfScanline; fragments: PDouble; const clipRec: TRect); +procedure InitializeScanlines(const polygons: TPathsD; + const scanlines: TArrayOfScanline; fragments: PFragment; const clipRec: TRect); var i,j, highJ: integer; pt1, pt2: PPointD; @@ -925,21 +977,23 @@ procedure InitializeScanlines(var polygons: TPathsD; // ------------------------------------------------------------------------------ procedure ProcessScanlineFragments(var scanline: TScanLine; - fragments: PDouble; var buffer: TArrayOfDouble); + fragments: PFragment; const buffer: TArrayOfDouble); var i,j, leftXi,rightXi: integer; - fracX, yy, q, windDir: double; + fracX, yy, q{, windDir}: double; left, right, dy, dydx: double; - pd, frag: PDouble; + frag: PFragment; + pd: PDouble; begin frag := fragments; inc(frag, scanline.fragOffset); for i := 1 to scanline.fragCnt do begin - left := frag^; inc(frag); //botX - right := frag^; inc(frag); //topX - dy := frag^; inc(frag); - dydx := frag^; inc(frag); + left := frag.botX; + right := frag.topX; + dy := frag.dy; + dydx := frag.dydx; + inc(frag); // converting botX & topX to left & right simplifies code if {botX > topX} left > right then @@ -949,18 +1003,14 @@ procedure ProcessScanlineFragments(var scanline: TScanLine; right := q; end; -{$IFDEF UseTrunc} leftXi := Max(0, Trunc(left)); rightXi := Max(0, Trunc(right)); -{$ELSE} - leftXi := Max(0, Round(left)); - rightXi := Max(0, Round(right)); -{$ENDIF} - if (leftXi = rightXi) then begin - if dydx < 0 then windDir := -1.0 else windDir := 1.0; // the fragment is only one pixel wide + //if dydx < 0 then windDir := -1.0 else windDir := 1.0; + if dydx < 0 then dy := -dy; + if leftXi < scanline.minX then scanline.minX := leftXi; if rightXi > scanline.maxX then @@ -968,13 +1018,13 @@ procedure ProcessScanlineFragments(var scanline: TScanLine; pd := @buffer[leftXi]; if (left <= 0) then begin - pd^ := pd^ + dy * windDir; + pd^ := pd^ + dy {* windDir}; end else begin q := (left + right) * 0.5 - leftXi; - pd^ := pd^ + (1-q) * dy * windDir; + pd^ := pd^ + (1-q) * dy {* windDir}; inc(pd); - pd^ := pd^ + q * dy * windDir; + pd^ := pd^ + q * dy {* windDir}; end; end else begin @@ -1009,135 +1059,343 @@ procedure ProcessScanlineFragments(var scanline: TScanLine; end; // ------------------------------------------------------------------------------ -{$IFNDEF TROUNDINGMODE} -type - TRoundingMode = {$IFNDEF FPC}Math.{$ENDIF}TFPURoundingMode; +{$RANGECHECKS OFF} // negative array index is used +{ CPU register optimized implementations. Every data type must be exactly the one used. } +procedure FillByteBufferEvenOdd(byteBuffer: PByte; + windingAccum: PDouble; count: nativeint); +var + accum: double; + lastValue: integer; + start: nativeint; + buf: PByteArray; +begin + accum := 0; //winding count accumulator + lastValue := 0; + // Copy byteBuffer to a local variable, so Delphi's 32bit compiler + // can put buf into a CPU register. + buf := PByteArray(byteBuffer); + + // Use the negative offset trick to only increment "count" + // until it reaches zero. And by offsetting the arrays, "count" + // also becomes the index for those. + inc(PByte(buf), count); + inc(windingAccum, count); + count := -count; + while count < 0 do + begin + // lastValue can be used if accum doesn't change + if PInt64Array(windingAccum)[count] = 0 then + begin + start := count; + repeat + inc(count); + until (count = 0) or (PInt64Array(windingAccum)[count] <> 0); + FillChar(buf[start], count - start, Byte(lastValue)); + if count = 0 then break; + end; + + accum := accum + PDoubleArray(windingAccum)[count]; + + // EvenOdd + lastValue := Trunc(Abs(accum) * 1275) mod 2550; // mul 5 + if lastValue > 1275 then + lastValue := (2550 - lastValue) shr 2 else // div 4 + lastValue := lastValue shr 2; // div 4 + if lastValue > 255 then lastValue := 255; + + buf[count] := Byte(lastValue); + PDoubleArray(windingAccum)[count] := 0; + inc(count); // walk towards zero + end; +end; + +procedure FillByteBufferNonZero(byteBuffer: PByte; + windingAccum: PDouble; count: nativeint); +var + accum: double; + lastValue: integer; + start: nativeint; + buf: PByteArray; +begin + accum := 0; //winding count accumulator + lastValue := 0; + // Copy byteBuffer to a local variable, so Delphi's 32bit compiler + // can put buf into a CPU register. + buf := PByteArray(byteBuffer); + + // Use the negative offset trick to only increment "count" + // until it reaches zero. And by offsetting the arrays, "count" + // also becomes the index for those. + inc(PByte(buf), count); + inc(windingAccum, count); + count := -count; + while count < 0 do + begin + // lastValue can be used if accum doesn't change + if PInt64Array(windingAccum)[count] = 0 then + begin + start := count; + repeat + inc(count); + until (count = 0) or (PInt64Array(windingAccum)[count] <> 0); + FillChar(buf[start], count - start, Byte(lastValue)); + if count = 0 then break; + end; + + accum := accum + PDoubleArray(windingAccum)[count]; + + // NonZero + lastValue := Trunc(Abs(accum) * 318); + if lastValue > 255 then lastValue := 255; + + buf[count] := Byte(lastValue); + PDoubleArray(windingAccum)[count] := 0; + inc(count); // walk towards zero + end; +end; + +procedure FillByteBufferPositive(byteBuffer: PByte; + windingAccum: PDouble; count: nativeint); +var + accum: double; + lastValue: integer; + start: nativeint; + buf: PByteArray; +begin + accum := 0; //winding count accumulator + lastValue := 0; + // Copy byteBuffer to a local variable, so Delphi's 32bit compiler + // can put buf into a CPU register. + buf := PByteArray(byteBuffer); + + // Use the negative offset trick to only increment "count" + // until it reaches zero. And by offsetting the arrays, "count" + // also becomes the index for those. + inc(PByte(buf), count); + inc(windingAccum, count); + count := -count; + while count < 0 do + begin + // lastValue can be used if accum doesn't change + if PInt64Array(windingAccum)[count] = 0 then + begin + start := count; + repeat + inc(count); + until (count = 0) or (PInt64Array(windingAccum)[count] <> 0); + FillChar(buf[start], count - start, Byte(lastValue)); + if count = 0 then break; + end; + + accum := accum + PDoubleArray(windingAccum)[count]; + + // Positive + lastValue := 0; + if accum > 0.002 then + begin + lastValue := Trunc(accum * 318); + if lastValue > 255 then lastValue := 255; + end; + + buf[count] := Byte(lastValue); + PDoubleArray(windingAccum)[count] := 0; + inc(count); // walk towards zero + end; +end; + +procedure FillByteBufferNegative(byteBuffer: PByte; + windingAccum: PDouble; count: nativeint); +var + accum: double; + lastValue: integer; + start: nativeint; + buf: PByteArray; +begin + accum := 0; //winding count accumulator + lastValue := 0; + // Copy byteBuffer to a local variable, so Delphi's 32bit compiler + // can put buf into a CPU register. + buf := PByteArray(byteBuffer); + + // Use the negative offset trick to only increment "count" + // until it reaches zero. And by offsetting the arrays, "count" + // also becomes the index for those. + inc(PByte(buf), count); + inc(windingAccum, count); + count := -count; + while count < 0 do + begin + // lastValue can be used if accum doesn't change + if PInt64Array(windingAccum)[count] = 0 then + begin + start := count; + repeat + inc(count); + until (count = 0) or (PInt64Array(windingAccum)[count] <> 0); + FillChar(buf[start], count - start, Byte(lastValue)); + if count = 0 then break; + end; + + accum := accum + PDoubleArray(windingAccum)[count]; + + // Negative + lastValue := 0; + if accum < -0.002 then + begin + lastValue := Trunc(accum * -318); + if lastValue > 255 then lastValue := 255; + end; + + buf[count] := Byte(lastValue); + PDoubleArray(windingAccum)[count] := 0; + inc(count); // walk towards zero + end; +end; +{$IFDEF RANGECHECKS_ENABLED} + {$RANGECHECKS ON} {$ENDIF} procedure Rasterize(const paths: TPathsD; const clipRec: TRect; fillRule: TFillRule; renderer: TCustomRenderer); var - i,j, xli,xri, maxW, maxH, aa: integer; + i, xli,xri, maxW, maxH: integer; clipRec2: TRect; paths2: TPathsD; - accum: double; windingAccum: TArrayOfDouble; - byteBuffer: TArrayOfByte; + byteBuffer: PByteArray; scanlines: TArrayOfScanline; - fragments: PDouble; + fragments: PFragment; scanline: PScanline; -{$IFnDEF UseTrunc} - savedRoundMode: TRoundingMode; -{$ENDIF} + skippedScanlines: integer; + skipRenderer: boolean; + + // FPC generates wrong code if "count" isn't NativeInt + FillByteBuffer: procedure(byteBuffer: PByte; windingAccum: PDouble; count: nativeint); begin // See also https://nothings.org/gamedev/rasterize/ - if not assigned(renderer) then Exit; + if not assigned(paths) or not assigned(renderer) then Exit; + renderer.SetClipRect(clipRec); + skipRenderer := renderer.SupportsRenderProcSkip; + Types.IntersectRect(clipRec2, clipRec, GetBounds(paths)); - if IsEmptyRect(clipRec2) then Exit; + if IsEmptyRect(clipRec2) then + begin + if skipRenderer then renderer.RenderProcSkip(clipRec); + Exit; + end; - paths2 := TranslatePath(paths, -clipRec2.Left, -clipRec2.Top); + if (clipRec2.Left = 0) and (clipRec2.Top = 0) then + paths2 := paths + else + paths2 := TranslatePath(paths, -clipRec2.Left, -clipRec2.Top); // Delphi's Round() function is *much* faster than Trunc(), // and even a little faster than Trunc() above (except // when the FastMM4 memory manager is enabled.) fragments := nil; -{$IFnDEF UseTrunc} - savedRoundMode := SetRoundMode(rmDown); -{$ENDIF} + byteBuffer := nil; try RectWidthHeight(clipRec2, maxW, maxH); + if maxW <= 0 then Exit; + GetMem(byteBuffer, maxW); // no need for dyn. array zero initialize SetLength(scanlines, maxH +1); SetLength(windingAccum, maxW +2); AllocateScanlines(paths2, scanlines, fragments, maxH, maxW-1); InitializeScanlines(paths2, scanlines, fragments, clipRec2); - SetLength(byteBuffer, maxW); - if byteBuffer = nil then Exit; + case fillRule of + frEvenOdd: + FillByteBuffer := FillByteBufferEvenOdd; + frNonZero: + FillByteBuffer := FillByteBufferNonZero; +{$IFDEF REVERSE_ORIENTATION} + frPositive: +{$ELSE} + frNegative: +{$ENDIF} + FillByteBuffer := FillByteBufferPositive; +{$IFDEF REVERSE_ORIENTATION} + frNegative: +{$ELSE} + frPositive: +{$ENDIF} + FillByteBuffer := FillByteBufferNegative; + else + if skipRenderer then renderer.RenderProcSkip(clipRec); + Exit; + end; + + // Notify the renderer about the parts at the top + // that we didn't touch. + if skipRenderer and (clipRec2.Top > clipRec.Top) then + begin + renderer.RenderProcSkip(Rect(clipRec.Left, clipRec.Top, + clipRec.Right, clipRec2.Top - 1)); + end; + + skippedScanlines := 0; scanline := @scanlines[0]; for i := 0 to high(scanlines) do begin if scanline.fragCnt = 0 then begin inc(scanline); + if skipRenderer then inc(skippedScanlines); Continue; end; + // If we have skipped some scanlines, we must notify the renderer. + if skipRenderer and (skippedScanlines > 0) then + begin + renderer.RenderProcSkip(Rect(clipRec.Left, clipRec2.Top + i - skippedScanlines, + clipRec.Right, clipRec2.Top + i - 1)); + skippedScanlines := 0; + end; + // process each scanline to fill the winding count accumulation buffer ProcessScanlineFragments(scanline^, fragments, windingAccum); // it's faster to process only the modified sub-array of windingAccum xli := scanline.minX; xri := Min(maxW -1, scanline.maxX +1); - FillChar(byteBuffer[xli], xri - xli +1, 0); // a 25% weighting has been added to the alpha channel to minimize any // background bleed-through where polygons join with a common edge. - accum := 0; //winding count accumulator - for j := xli to xri do - begin - accum := accum + windingAccum[j]; - case fillRule of - frEvenOdd: - begin -{$IFDEF UseTrunc} - aa := Trunc(Abs(accum) * 1275) mod 2550; // *5 -{$ELSE} - aa := Round(Abs(accum) * 1275) mod 2550; // *5 -{$ENDIF} - if aa > 1275 then - byteBuffer[j] := Min(255, (2550 - aa) shr 2) else // /4 - byteBuffer[j] := Min(255, aa shr 2); // /4 - end; - frNonZero: - begin -{$IFDEF UseTrunc} - byteBuffer[j] := Min(255, Trunc(Abs(accum) * 318)); -{$ELSE} - byteBuffer[j] := Min(255, Round(Abs(accum) * 318)); -{$ENDIF} - end; - {$IFDEF REVERSE_ORIENTATION} - frPositive: - {$ELSE} - frNegative: - {$ENDIF} - begin -{$IFDEF UseTrunc} - if accum > 0.002 then - byteBuffer[j] := Min(255, Trunc(accum * 318)); -{$ELSE} - if accum > 0.002 then - byteBuffer[j] := Min(255, Round(accum * 318)); -{$ENDIF} - end; - {$IFDEF REVERSE_ORIENTATION} - frNegative: - {$ELSE} - frPositive: - {$ENDIF} - begin -{$IFDEF UseTrunc} - if accum < -0.002 then - byteBuffer[j] := Min(255, Trunc(-accum * 318)); -{$ELSE} - if accum < -0.002 then - byteBuffer[j] := Min(255, Round(-accum * 318)); -{$ENDIF} - end; - end; - end; + // FillByteBuffer overwrites every byte in byteBuffer[xli..xri] and also resets + // windingAccum[xli..xri] to 0. + FillByteBuffer(@byteBuffer[xli], @windingAccum[xli], xri - xli +1); + renderer.RenderProc(clipRec2.Left + xli, clipRec2.Left + xri, clipRec2.Top + i, @byteBuffer[xli]); - // cleanup and deallocate memory - FillChar(windingAccum[xli], (xri - xli +1) * sizeOf(Double), 0); inc(scanline); end; + + // Notify the renderer about the last skipped scanlines + if skipRenderer then + begin + clipRec2.Bottom := clipRec2.top + High(scanlines) - skippedScanlines; + if clipRec2.Bottom < clipRec.Bottom then + begin + renderer.RenderProcSkip(Rect(clipRec.Left, clipRec2.Bottom + 1, + clipRec.Right, clipRec.Bottom)); + end; + end; finally + // cleanup and deallocate memory FreeMem(fragments); -{$IFnDEF UseTrunc} - SetRoundMode(savedRoundMode); -{$ENDIF} + FreeMem(byteBuffer); + end; +end; +// ------------------------------------------------------------------------------ + +procedure Rasterize(img: TImage32; const paths: TPathsD; + const clipRec: TRect; fillRule: TFillRule; renderer: TCustomRenderer); +begin + if renderer.Initialize(img) then + begin + Rasterize(paths, clipRec, fillRule, renderer); + renderer.NotifyChange; end; end; @@ -1145,6 +1403,13 @@ procedure Rasterize(const paths: TPathsD; const clipRec: TRect; // TAbstractRenderer // ------------------------------------------------------------------------------ +constructor TCustomRenderer.Create; +begin + inherited; + fOpacity := 255; +end; +// ------------------------------------------------------------------------------ + function TCustomRenderer.Initialize(imgBase: Pointer; imgWidth, imgHeight, pixelSize: integer): Boolean; begin @@ -1154,7 +1419,7 @@ function TCustomRenderer.Initialize(imgBase: Pointer; fPixelSize := pixelSize; fCurrLinePtr := fImgBase; - fCurrY := 0; + fCurrY := -1; result := true; end; // ------------------------------------------------------------------------------ @@ -1186,6 +1451,33 @@ function TCustomRenderer.GetDstPixel(x, y: integer): Pointer; Result := fCurrLinePtr; inc(PByte(Result), x * fPixelSize); end; +// ------------------------------------------------------------------------------ + +procedure TCustomRenderer.SetClipRect(const clipRect: TRect); +begin + // default: do nothing +end; +// ------------------------------------------------------------------------------ + +procedure TCustomRenderer.RenderProcSkip(const skippedRect: TRect); +begin + // default: do nothing +end; +// ------------------------------------------------------------------------------ + +function TCustomRenderer.SupportsRenderProcSkip: Boolean; +begin + Result := False; +end; + +// ------------------------------------------------------------------------------ +// TCustomColorRenderer +// ------------------------------------------------------------------------------ + +procedure TCustomColorRenderer.SetColor(value: TColor32); +begin + fColor := value; +end; // ------------------------------------------------------------------------------ // TColorRenderer @@ -1193,6 +1485,7 @@ function TCustomRenderer.GetDstPixel(x, y: integer): Pointer; constructor TColorRenderer.Create(color: TColor32 = clNone32); begin + inherited Create; if color <> clNone32 then SetColor(color); end; // ------------------------------------------------------------------------------ @@ -1211,22 +1504,100 @@ procedure TColorRenderer.SetColor(value: TColor32); end; // ------------------------------------------------------------------------------ -procedure TColorRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); +{$RANGECHECKS OFF} // negative array index usage (Delphi 7-2007 have no pointer math) +type + // Used to reduce the number of parameters to help the compiler's + // optimizer. + TRenderProcData = record + dst: PColor32Array; + alpha: PByteArray; + end; + +function RenderProcBlendToAlpha255(count: nativeint; dstColor: TColor32; + var data: TRenderProcData): nativeint; +// CPU register optimized var - i: integer; - dst: PColor32; + a: byte; + dst: PColor32Array; + alpha: PByteArray; begin - dst := GetDstPixel(x1,y); - for i := x1 to x2 do + Result := count; + dst := data.dst; + alpha := data.alpha; + + a := alpha[Result]; + dst[Result] := dstColor; + inc(Result); + + while (Result < 0) and (alpha[Result] = a) do begin - // BlendToAlpha is marginally slower than BlendToOpaque but it's used - // here because it's universally applicable. - // Ord() is used here because very old compilers define PByte as a PChar - if Ord(alpha^) > 1 then - dst^ := BlendToAlpha(dst^, ((Ord(alpha^) * fAlpha) shr 8) shl 24 or fColor); - inc(dst); inc(alpha); + dst[Result] := dstColor; + inc(Result); + end; +end; + +procedure RenderProcBlendToAlpha(dst: PColor32Array; alpha: PByteArray; + count: nativeint; color: TColor32; alphaTable: PByteArray); +var + a: byte; + lastDst, dstColor: TColor32; + data: TRenderProcData; +begin + // Use negative offset trick. + alpha := @alpha[count]; + dst := @dst[count]; + count := -count; + + // store pointers for RenderProcBlendToAlpha255 + data.dst := dst; + data.alpha := alpha; + + while count < 0 do + begin + a := alpha[count]; + if a > 1 then + begin + a := alphaTable[a]; + dstColor := (a shl 24) or color; + + // Special handling for alpha channel 255 (copy dstColor into dst) + if a = 255 then + count := RenderProcBlendToAlpha255(count, dstColor, data) + else + begin + lastDst := dst[count]; + dstColor := BlendToAlpha(lastDst, dstColor); + + a := alpha[count]; + dst[count] := dstColor; + inc(count); + + // if we have the same dst-pixel and the same alpha channel, we can + // just copy the already calculated BlendToAlpha color. + while (count < 0) and (a = alpha[count]) and (dst[count] = lastDst) do + begin + dst[count] := dstColor; + inc(count); + end; + end; + end + else + inc(count); end; end; +{$IFDEF RANGECHECKS_ENABLED} + {$RANGECHECKS ON} +{$ENDIF} + +procedure TColorRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); +begin + // Help the compiler to get better CPU register allocation. + // Without the hidden Self parameter the compiler optimizes + // better. + RenderProcBlendToAlpha(PColor32Array(GetDstPixel(x1, y)), + PByteArray(alpha), x2 - x1 + 1, fColor, + PByteArray(@MulTable[fAlpha])); +end; // ------------------------------------------------------------------------------ // TAliasedColorRenderer @@ -1234,6 +1605,7 @@ procedure TColorRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); constructor TAliasedColorRenderer.Create(color: TColor32 = clNone32); begin + inherited Create; fColor := color; end; // ------------------------------------------------------------------------------ @@ -1250,15 +1622,138 @@ procedure TAliasedColorRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); var i: integer; dst: PColor32; + c: TColor32; begin dst := GetDstPixel(x1,y); + c := fColor; // copy fColor to local variable for i := x1 to x2 do begin - if Ord(alpha^) > 127 then dst^ := fColor; //ie no blending + if Ord(alpha^) > 127 then dst^ := c; //ie no blending inc(dst); inc(alpha); end; end; +// ------------------------------------------------------------------------------ +// TMaskRenderer +// ------------------------------------------------------------------------------ + +procedure TMaskRenderer.SetClipRect(const clipRect: TRect); +begin + fClipRect := clipRect; + // clipping to the image size + if fClipRect.Left < 0 then fClipRect.Left := 0; + if fClipRect.Top < 0 then fClipRect.Top := 0; + if fClipRect.Right > fImgWidth then fClipRect.Right := fImgWidth; + if fClipRect.Bottom > fImgHeight then fClipRect.Bottom := fImgHeight; +end; +// ------------------------------------------------------------------------------ + +procedure TMaskRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); +var + p: PColor32; + i: integer; +begin + // CopyBlend excludes ClipRect.Right/Bottom, so we also + // need to exclude it. + if (y < fClipRect.Top) or (y >= fClipRect.Bottom) then Exit; + if x2 >= fClipRect.Right then x2 := fClipRect.Right - 1; + + if x1 < fClipRect.Left then + begin + inc(alpha, fClipRect.Left - x1); + x1 := fClipRect.Left; + end; + + p := GetDstPixel(fClipRect.Left, y); + + // Clear the area before x1 (inside OutsideBounds) + FillChar(p^, (x1 - fClipRect.Left) * SizeOf(TColor32), 0); + inc(p, x1 - fClipRect.Left); + + // Fill the area between x1 and x2 + for i := x1 to x2 do + begin + if p^ <> 0 then + begin + if Ord(alpha^) = 0 then + p^ := 0 + else if Ord(alpha^) <> 255 then + p^ := BlendMask(p^, Ord(alpha^) shl 24); + end; + inc(p); + inc(alpha); + end; + + // Clear the area after x2 (inside OutsideBounds) + FillChar(p^, (fClipRect.Right - (x2 + 1)) * SizeOf(TColor32), 0); +end; +// ------------------------------------------------------------------------------ + +procedure TMaskRenderer.RenderProcSkip(const skippedRect: TRect); +var + i, h, w: integer; + p: PColor32; + r: TRect; +begin + r := skippedRect; + if r.Left < fClipRect.Left then r.Left := fClipRect.Left; + if r.Top < fClipRect.Top then r.Top := fClipRect.Top; + // CopyBlend excludes ClipRect.Right/Bottom, so we also + // need to exclude it. + if r.Right >= fClipRect.Right then r.Right := fClipRect.Right - 1; + if r.Bottom >= fClipRect.Bottom then r.Bottom := fClipRect.Bottom - 1; + + if r.Right < r.Left then Exit; + if r.Bottom < r.Top then Exit; + + w := r.Right - r.Left + 1; + h := r.Bottom - r.Top + 1; + p := GetDstPixel(r.Left, r.Top); + if w = fImgWidth then + FillChar(p^, w * h * SizeOf(TColor32), 0) + else + begin + for i := 1 to h do + begin + FillChar(p^, w * SizeOf(TColor32), 0); + inc(p, fImgWidth); + end; + end; +end; + +// ------------------------------------------------------------------------------ +function TMaskRenderer.SupportsRenderProcSkip: Boolean; +begin + Result := True; +end; + +// ------------------------------------------------------------------------------ +// TCustomRendererCache +// ------------------------------------------------------------------------------ + +constructor TCustomRendererCache.Create; +begin + inherited Create; + fColorRenderer := TColorRenderer.Create; + fAliasedColorRenderer := TAliasedColorRenderer.Create; + fMaskRenderer := TMaskRenderer.Create; +end; +// ------------------------------------------------------------------------------ + +destructor TCustomRendererCache.Destroy; +begin + fColorRenderer.Free; + fAliasedColorRenderer.Free; + fMaskRenderer.Free; +end; +// ------------------------------------------------------------------------------ + +function TCustomRendererCache.GetColorRenderer(color: TColor32): TColorRenderer; +begin + Result := fColorRenderer; + Result.SetColor(color); +end; + // ------------------------------------------------------------------------------ // TBrushImageRenderer // ------------------------------------------------------------------------------ @@ -1266,6 +1761,7 @@ procedure TAliasedColorRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); constructor TImageRenderer.Create(tileFillStyle: TTileFillStyle; brushImage: TImage32); begin + inherited Create; fImage := TImage32.Create(brushImage); SetTileFillStyle(tileFillStyle); end; @@ -1303,24 +1799,34 @@ procedure TImageRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); var i: integer; pDst: PColor32; - pBrush: PARGB; + pImg: PColor32; + opacityTable: PByteArray; begin pDst := GetDstPixel(x1,y); dec(x1, fOffset.X); dec(x2, fOffset.X); dec(y, fOffset.Y); - pBrush := GetFirstBrushPixel(x1, y); - for i := x1 to x2 do + pImg := GetFirstBrushPixel(x1, y); + if Opacity < 255 then begin - pDst^ := BlendToAlpha(pDst^, - MulBytes(pBrush.A, Ord(alpha^)) shl 24 or (pBrush.Color and $FFFFFF)); - inc(pDst); inc(alpha); - pBrush := GetPixel(fBrushPixel, fBoundsProc(i, fImage.Width)); - end; + opacityTable := PByteArray(@MulTable[Opacity]); + for i := x1 to x2 do + begin + pDst^ := BlendToAlpha3(pDst^, pImg^, opacityTable[Ord(alpha^)]); + inc(pDst); inc(alpha); + pImg := PColor32(GetPixel(fBrushPixel, fBoundsProc(i, fImage.Width))); + end; + end else + for i := x1 to x2 do + begin + pDst^ := BlendToAlpha3(pDst^, pImg^, Ord(alpha^)); + inc(pDst); inc(alpha); + pImg := PColor32(GetPixel(fBrushPixel, fBoundsProc(i, fImage.Width))); + end; end; // ------------------------------------------------------------------------------ -function TImageRenderer.GetFirstBrushPixel(x, y: integer): PARGB; +function TImageRenderer.GetFirstBrushPixel(x, y: integer): PColor32; begin if fMirrorY then y := MirrorQ(y, fImage.Height) else @@ -1331,7 +1837,7 @@ function TImageRenderer.GetFirstBrushPixel(x, y: integer): PARGB; fLastYY := y; end; x := fBoundsProc(x, fImage.Width); - result := GetPixel(fBrushPixel, x); + result := PColor32(GetPixel(fBrushPixel, x)); end; // ------------------------------------------------------------------------------ @@ -1340,6 +1846,7 @@ function TImageRenderer.GetFirstBrushPixel(x, y: integer): PARGB; constructor TCustomGradientRenderer.Create; begin + inherited Create; fBoundsProc := ClampQ; //default proc end; // ------------------------------------------------------------------------------ @@ -1365,7 +1872,7 @@ procedure TCustomGradientRenderer.SetParameters(startColor, endColor: TColor32; gradFillStyle: TGradientFillStyle = gfsClamp); begin SetGradientFillStyle(gradFillStyle); - // reset gradient colors if perviously set + // reset gradient colors if previously set SetLength(fGradientColors, 2); fGradientColors[0].offset := 0; fGradientColors[0].color := startColor; @@ -1434,7 +1941,7 @@ function TLinearGradientRenderer.Initialize(targetImage: TImage32): Boolean; // gradient > 45 degrees if (fEndPt.Y < fStartPt.Y) then begin - fGradientColors := ReverseColors(fGradientColors); + ReverseColors(fGradientColors); SwapPoints(fStartPt, fEndPt); end; fIsVert := true; @@ -1443,9 +1950,9 @@ function TLinearGradientRenderer.Initialize(targetImage: TImage32): Boolean; dxdy := dx/dy; fColorsCnt := Ceil(dy + dxdy * (fEndPt.X - fStartPt.X)); - fColors := MakeColorGradient(fGradientColors, fColorsCnt); + MakeColorGradient(fGradientColors, fColorsCnt, fColors); // get a list of perpendicular offsets for each - SetLength(fPerpendicOffsets, ImgWidth); + NewIntegerArray(fPerpendicOffsets, ImgWidth, True); // from an imaginary line that's through fStartPt and perpendicular to // the gradient line, get a list of Y offsets for each X in image width for i := 0 to ImgWidth -1 do @@ -1460,7 +1967,7 @@ function TLinearGradientRenderer.Initialize(targetImage: TImage32): Boolean; end; if (fEndPt.X < fStartPt.X) then begin - fGradientColors := ReverseColors(fGradientColors); + ReverseColors(fGradientColors); SwapPoints(fStartPt, fEndPt); end; fIsVert := false; @@ -1469,8 +1976,8 @@ function TLinearGradientRenderer.Initialize(targetImage: TImage32): Boolean; dydx := dy/dx; //perpendicular slope fColorsCnt := Ceil(dx + dydx * (fEndPt.Y - fStartPt.Y)); - fColors := MakeColorGradient(fGradientColors, fColorsCnt); - SetLength(fPerpendicOffsets, ImgHeight); + MakeColorGradient(fGradientColors, fColorsCnt, fColors); + NewIntegerArray(fPerpendicOffsets, ImgHeight, True); // from an imaginary line that's through fStartPt and perpendicular to // the gradient line, get a list of X offsets for each Y in image height for i := 0 to ImgHeight -1 do @@ -1481,27 +1988,66 @@ function TLinearGradientRenderer.Initialize(targetImage: TImage32): Boolean; procedure TLinearGradientRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); var - i, off: integer; + i, colorsCnt: integer; pDst: PColor32; - color: TARGB; + color: TColor32; + boundsProc: TBoundsProc; + offset: Integer; + colors: PColor32Array; + perpendicOffsets: PIntegerArray; + opacityTable: PByteArray; begin pDst := GetDstPixel(x1,y); - for i := x1 to x2 do + // optimize self fields access + colorsCnt := fColorsCnt; + colors := @fColors[0]; + boundsProc := fBoundsProc; + if fIsVert then + begin + perpendicOffsets := @fPerpendicOffsets[0]; // optimize self field access + if Opacity < 255 then + begin + opacityTable := PByteArray(@MulTable[Opacity]); + for i := x1 to x2 do + begin + // when fIsVert = true, fPerpendicOffsets is an array of Y for each X + color := colors[boundsProc(y - perpendicOffsets[i], colorsCnt)]; + pDst^ := BlendToAlpha3(pDst^, color, opacityTable[Ord(alpha^)]); + inc(pDst); inc(alpha); + end; + end else + begin + for i := x1 to x2 do + begin + // when fIsVert = true, fPerpendicOffsets is an array of Y for each X + color := colors[boundsProc(y - perpendicOffsets[i], colorsCnt)]; + pDst^ := BlendToAlpha3(pDst^, color, Ord(alpha^)); + inc(pDst); inc(alpha); + end; + end; + end + else begin - if fIsVert then + // when fIsVert = false, fPerpendicOffsets is an array of X for each Y + offset := fPerpendicOffsets[y]; + if Opacity < 255 then begin - // when fIsVert = true, fPerpendicOffsets is an array of Y for each X - off := fPerpendicOffsets[i]; - color.Color := fColors[fBoundsProc(y - off, fColorsCnt)]; + opacityTable := PByteArray(@MulTable[Opacity]); + for i := x1 to x2 do + begin + color := colors[boundsProc(i - offset, colorsCnt)]; + pDst^ := BlendToAlpha3(pDst^, color, opacityTable[Ord(alpha^)]); + inc(pDst); inc(alpha); + end; end else begin - // when fIsVert = false, fPerpendicOffsets is an array of X for each Y - off := fPerpendicOffsets[y]; - color.Color := fColors[fBoundsProc(i - off, fColorsCnt)]; + for i := x1 to x2 do + begin + color := colors[boundsProc(i - offset, colorsCnt)]; + pDst^ := BlendToAlpha3(pDst^, color, Ord(alpha^)); + inc(pDst); inc(alpha); + end; end; - pDst^ := BlendToAlpha(pDst^, - MulBytes(color.A, Ord(alpha^)) shl 24 or (color.Color and $FFFFFF)); - inc(pDst); inc(alpha); end; end; @@ -1513,7 +2059,7 @@ function TRadialGradientRenderer.Initialize(targetImage: TImage32): Boolean; begin result := inherited Initialize(targetImage) and (fColorsCnt > 1); if result then - fColors := MakeColorGradient(fGradientColors, fColorsCnt); + MakeColorGradient(fGradientColors, fColorsCnt, fColors); end; // ------------------------------------------------------------------------------ @@ -1551,21 +2097,30 @@ procedure TRadialGradientRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); var i: integer; dist: double; - color: TARGB; + color: TColor32; pDst: PColor32; + opacityTable: PByteArray; begin pDst := GetDstPixel(x1,y); - for i := x1 to x2 do + if Opacity < 255 then begin - dist := Hypot((y - fCenterPt.Y) *fScaleY, (i - fCenterPt.X) *fScaleX); -{$IFDEF UseTrunc} - color.Color := fColors[fBoundsProc(Trunc(dist), fColorsCnt)]; -{$ELSE} - color.Color := fColors[fBoundsProc(Round(dist), fColorsCnt)]; -{$ENDIF} - pDst^ := BlendToAlpha(pDst^, - MulBytes(color.A, Ord(alpha^)) shl 24 or (color.Color and $FFFFFF)); - inc(pDst); inc(alpha); + opacityTable := PByteArray(@MulTable[Opacity]); + for i := x1 to x2 do + begin + dist := Hypot((y - fCenterPt.Y) *fScaleY, (i - fCenterPt.X) *fScaleX); + color := fColors[fBoundsProc(Trunc(dist), fColorsCnt)]; + pDst^ := BlendToAlpha3(pDst^, color, opacityTable[Ord(alpha^)]); + inc(pDst); inc(alpha); + end; + end else + begin + for i := x1 to x2 do + begin + dist := Hypot((y - fCenterPt.Y) *fScaleY, (i - fCenterPt.X) *fScaleX); + color := fColors[fBoundsProc(Trunc(dist), fColorsCnt)]; + pDst^ := BlendToAlpha3(pDst^, color, Ord(alpha^)); + inc(pDst); inc(alpha); + end; end; end; @@ -1577,7 +2132,7 @@ function TSvgRadialGradientRenderer.Initialize(targetImage: TImage32): Boolean; begin result := inherited Initialize(targetImage) and (fColorsCnt > 1); if result then - fColors := MakeColorGradient(fGradientColors, fColorsCnt); + MakeColorGradient(fGradientColors, fColorsCnt, fColors); end; // ------------------------------------------------------------------------------ @@ -1615,10 +2170,12 @@ procedure TSvgRadialGradientRenderer.RenderProc(x1, x2, y: integer; alpha: PByte i: integer; q,qq, m,c, qa,qb,qc,qs: double; dist, dist2: double; - color: TARGB; + color: TColor32; pDst: PColor32; pt, ellipsePt: TPointD; + opacityTable: PByteArray; begin + opacityTable := PByteArray(@MulTable[Opacity]); // get the left-most pixel to render pDst := GetDstPixel(x1,y); pt.X := x1 - fCenterPt.X; pt.Y := y - fCenterPt.Y; @@ -1665,17 +2222,18 @@ procedure TSvgRadialGradientRenderer.RenderProc(x1, x2, y: integer; alpha: PByte ellipsePt.X := (-qb -qs)/(2 * qa) else ellipsePt.X := (-qb +qs)/(2 * qa); ellipsePt.Y := m * ellipsePt.X + c; - dist := Hypot(pt.X - fFocusPt.X, pt.Y - fFocusPt.Y); - dist2 := Hypot(ellipsePt.X - fFocusPt.X, ellipsePt.Y - fFocusPt.Y); + + // Use sqr'ed distances (Sqrt(a^2+b^2)/Sqrt(x^2+y^2) => Sqrt((a^2+b^2)/(x^2+y^2)) + dist := Sqr(pt.X - fFocusPt.X) + Sqr(pt.Y - fFocusPt.Y); + dist2 := Sqr(ellipsePt.X - fFocusPt.X) + Sqr(ellipsePt.Y - fFocusPt.Y); if dist2 = 0 then q := 1 else - q := dist/ dist2; + q := Sqrt(dist/dist2); end else q := 1; //shouldn't happen :) end; - color.Color := fColors[fBoundsProcD(Abs(q), fColorsCnt)]; - pDst^ := BlendToAlpha(pDst^, - MulBytes(color.A, Ord(alpha^)) shl 24 or (color.Color and $FFFFFF)); + color := fColors[fBoundsProcD(Abs(q), fColorsCnt)]; + pDst^ := BlendToAlpha3(pDst^, color, opacityTable[Ord(alpha^)]); inc(pDst); pt.X := pt.X + 1; inc(alpha); end; end; @@ -1693,9 +2251,9 @@ procedure TEraseRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); for i := x1 to x2 do begin {$IFDEF PBYTE} - dst.A := MulBytes(dst.A, not alpha^); + dst.A := MulTable[dst.A, not alpha^]; {$ELSE} - dst.A := MulBytes(dst.A, not Ord(alpha^)); + dst.A := MulTable[dst.A, not Ord(alpha^)]; {$ENDIF} inc(dst); inc(alpha); end; @@ -1705,23 +2263,74 @@ procedure TEraseRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); // TInverseRenderer // ------------------------------------------------------------------------------ +constructor TInverseRenderer.Create(bkImg: TImage32); +begin + inherited Create; + fCurrBackY := -1; + // bkImg, when assigned, is the background master image + // and fImage is very likely a transparent (layered) image + fBackImage := bkImg; +end; +// ------------------------------------------------------------------------------ + +function TInverseRenderer.GetSrcPixel(x, y: integer): Pointer; +begin + if (y <> fCurrBackY) then + begin + fCurrBackY := y; + fCurrBkLinePtr := fBackImage.PixelBase; + inc(PByte(fCurrBkLinePtr), y * fImgWidth * fPixelSize); + end; + Result := fCurrBkLinePtr; + inc(PByte(Result), x * fPixelSize); +end; +// ------------------------------------------------------------------------------ + +function IsMidColor(const color: TARGB): Boolean; +{$IFDEF INLINE} inline; {$ENDIF} +begin + // not too dark and not too light :)) + Result := Abs(color.R + color.G + color.B - 383) < 64; +end; +// ------------------------------------------------------------------------------ + procedure TInverseRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); var i: integer; - dst: PARGB; + src, dst: PARGB; c: TARGB; begin dst := PARGB(GetDstPixel(x1,y)); - for i := x1 to x2 do + if Assigned(fBackImage) then begin - c.Color := not dst.Color; - c.A := MulBytes(dst.A, Ord(alpha^)); - dst.Color := BlendToAlpha(dst.Color, c.Color); - inc(dst); inc(alpha); + src := PARGB(GetSrcPixel(x1,y)); + for i := x1 to x2 do + begin + if src.Color = 0 then c.Color := clBlack32 + else if IsMidColor(src^) then c.Color := clWhite32 + else c.Color := not src.Color; + c.A := Ord(alpha^); + dst.Color := BlendToAlpha(dst.Color, c.Color); + inc(dst); inc(src); inc(alpha); + end; + end else + begin + for i := x1 to x2 do + begin + if dst.Color = 0 then c.Color := clBlack32 + else if IsMidColor(dst^) then c.Color := clWhite32 + else c.Color := not dst.Color; + c.A := Ord(alpha^); + dst.Color := BlendToAlpha(dst.Color, c.Color); + inc(dst); inc(alpha); + end; end; end; // ------------------------------------------------------------------------------ +// TBarycentricRenderer +// ------------------------------------------------------------------------------ + procedure TBarycentricRenderer.SetParameters(const a, b, c: TPointD; c1, c2, c3: TColor32); @@ -1771,16 +2380,31 @@ procedure TBarycentricRenderer.RenderProc(x1, x2, y: integer; alpha: PByte); x: integer; p: PARGB; c: TARGB; + opacityTable: PByteArray; begin p := PARGB(fImgBase); inc(p, y * ImgWidth + x1); - for x := x1 to x2 do + if Opacity < 255 then begin - c.Color := GetColor(PointD(x, y)); - c.A := c.A * Ord(alpha^) shr 8; - p.Color := BlendToAlpha(p.Color, c.Color); - inc(p); inc(alpha); - end; + opacityTable := PByteArray(@MulTable[Opacity]); + for x := x1 to x2 do + begin + c.Color := GetColor(PointD(x, y)); + c.A := opacityTable[MulTable[c.A, Ord(alpha^)]]; + p.Color := BlendToAlpha(p.Color, c.Color); + inc(p); inc(alpha); + end + end + else + for x := x1 to x2 do + begin + c.Color := GetColor(PointD(x, y)); + c.A := MulTable[c.A, Ord(alpha^)]; + p.Color := BlendToAlpha(p.Color, c.Color); + inc(p); inc(alpha); + end + + end; // ------------------------------------------------------------------------------ @@ -1848,7 +2472,7 @@ procedure DrawLine(img: TImage32; lines: TPathsD; begin setLength(lines, 1); - setLength(lines[0], 2); + NewPointDArray(lines[0], 2, True); lines[0][0] := pt1; lines[0][1] := pt2; DrawLine(img, lines, lineWidth, color, esRound); @@ -1867,6 +2491,19 @@ procedure DrawLine(img: TImage32; const line: TPathD; lineWidth: double; end; // ------------------------------------------------------------------------------ +procedure DrawLine(img: TImage32; const line: TPathD; lineWidth: double; + color: TColor32; rendererCache: TCustomRendererCache; + endStyle: TEndStyle; joinStyle: TJoinStyle; miterLimit: double); +var + lines: TPathsD; +begin + setLength(lines, 1); + lines[0] := line; + DrawLine(img, lines, lineWidth, color, rendererCache, endStyle, joinStyle, + miterLimit); +end; +// ------------------------------------------------------------------------------ + procedure DrawLine(img: TImage32; const line: TPathD; lineWidth: double; renderer: TCustomRenderer; endStyle: TEndStyle; joinStyle: TJoinStyle; miterLimit: double); @@ -1894,28 +2531,40 @@ procedure DrawLine(img: TImage32; const lines: TPathsD; lineWidth: double; color: TColor32; endStyle: TEndStyle; joinStyle: TJoinStyle; miterLimit: double); var - lines2: TPathsD; - cr: TCustomRenderer; + cr: TCustomColorRenderer; begin if not assigned(lines) then exit; - if (lineWidth < MinStrokeWidth) then lineWidth := MinStrokeWidth; - lines2 := RoughOutline(lines, lineWidth, joinStyle, endStyle, miterLimit); if img.AntiAliased then cr := TColorRenderer.Create(color) else cr := TAliasedColorRenderer.Create(color); try - if cr.Initialize(img) then - begin - Rasterize(lines2, img.bounds, frNonZero, cr); - cr.NotifyChange; - end; + DrawLine(img, lines, lineWidth, cr, endStyle, joinStyle, miterLimit); finally cr.free; end; end; // ------------------------------------------------------------------------------ +procedure DrawLine(img: TImage32; const lines: TPathsD; + lineWidth: double; color: TColor32; rendererCache: TCustomRendererCache; + endStyle: TEndStyle; joinStyle: TJoinStyle; miterLimit: double); +var + cr: TCustomColorRenderer; +begin + if not assigned(lines) then exit; + if rendererCache = nil then + DrawLine(img, lines, lineWidth, color, endStyle, joinStyle, miterLimit) + else + begin + if img.AntiAliased then + cr := rendererCache.ColorRenderer else + cr := rendererCache.AliasedColorRenderer; + DrawLine(img, lines, lineWidth, cr, endStyle, joinStyle, miterLimit); + end; +end; +// ------------------------------------------------------------------------------ + procedure DrawLine(img: TImage32; const lines: TPathsD; lineWidth: double; renderer: TCustomRenderer; endStyle: TEndStyle; joinStyle: TJoinStyle; @@ -1926,11 +2575,7 @@ procedure DrawLine(img: TImage32; const lines: TPathsD; if (not assigned(lines)) or (not assigned(renderer)) then exit; if (lineWidth < MinStrokeWidth) then lineWidth := MinStrokeWidth; lines2 := RoughOutline(lines, lineWidth, joinStyle, endStyle, miterLimit); - if renderer.Initialize(img) then - begin - Rasterize(lines2, img.bounds, frNonZero, renderer); - renderer.NotifyChange; - end; + Rasterize(img, lines2, img.bounds, frNonZero, renderer); end; // ------------------------------------------------------------------------------ @@ -1946,11 +2591,7 @@ procedure DrawInvertedLine(img: TImage32; lines2 := RoughOutline(lines, lineWidth, joinStyle, endStyle, 2); ir := TInverseRenderer.Create; try - if ir.Initialize(img) then - begin - Rasterize(lines2, img.bounds, frNonZero, ir); - ir.NotifyChange; - end; + Rasterize(img, lines2, img.bounds, frNonZero, ir); finally ir.free; end; @@ -1958,8 +2599,9 @@ procedure DrawInvertedLine(img: TImage32; // ------------------------------------------------------------------------------ procedure DrawDashedLine(img: TImage32; const line: TPathD; - dashPattern: TArrayOfInteger; patternOffset: PDouble; lineWidth: double; - color: TColor32; endStyle: TEndStyle; joinStyle: TJoinStyle); + dashPattern: TArrayOfDouble; patternOffset: PDouble; lineWidth: double; + color: TColor32; endStyle: TEndStyle; joinStyle: TJoinStyle; + rendererCache: TCustomRendererCache); var lines: TPathsD; cr: TColorRenderer; @@ -1987,34 +2629,36 @@ procedure DrawDashedLine(img: TImage32; const line: TPathD; endStyle := esButt; end; lines := RoughOutline(lines, lineWidth, joinStyle, endStyle); - cr := TColorRenderer.Create(color); + + if rendererCache = nil then + cr := TColorRenderer.Create(color) else + cr := rendererCache.GetColorRenderer(color); try - if cr.Initialize(img) then - begin - Rasterize(lines, img.bounds, frNonZero, cr); - cr.NotifyChange; - end; + Rasterize(img, lines, img.bounds, frNonZero, cr); finally - cr.free; + if rendererCache = nil then + cr.free; end; end; // ------------------------------------------------------------------------------ procedure DrawDashedLine(img: TImage32; const lines: TPathsD; - dashPattern: TArrayOfInteger; patternOffset: PDouble; lineWidth: double; - color: TColor32; endStyle: TEndStyle; joinStyle: TJoinStyle); + dashPattern: TArrayOfDouble; patternOffset: PDouble; lineWidth: double; + color: TColor32; endStyle: TEndStyle; joinStyle: TJoinStyle; + rendererCache: TCustomRendererCache); var i: integer; begin if not assigned(lines) then exit; for i := 0 to high(lines) do DrawDashedLine(img, lines[i], - dashPattern, patternOffset, lineWidth, color, endStyle, joinStyle); + dashPattern, patternOffset, lineWidth, color, endStyle, joinStyle, + rendererCache); end; // ------------------------------------------------------------------------------ procedure DrawDashedLine(img: TImage32; const line: TPathD; - dashPattern: TArrayOfInteger; patternOffset: PDouble; lineWidth: double; + dashPattern: TArrayOfDouble; patternOffset: PDouble; lineWidth: double; renderer: TCustomRenderer; endStyle: TEndStyle; joinStyle: TJoinStyle); var i: integer; @@ -2029,16 +2673,12 @@ procedure DrawDashedLine(img: TImage32; const line: TPathD; lines := GetDashedPath(line, endStyle = esPolygon, dashPattern, patternOffset); if Length(lines) = 0 then Exit; lines := RoughOutline(lines, lineWidth, joinStyle, endStyle); - if renderer.Initialize(img) then - begin - Rasterize(lines, img.bounds, frNonZero, renderer); - renderer.NotifyChange; - end; + Rasterize(img, lines, img.bounds, frNonZero, renderer); end; // ------------------------------------------------------------------------------ procedure DrawDashedLine(img: TImage32; const lines: TPathsD; - dashPattern: TArrayOfInteger; patternOffset: PDouble; lineWidth: double; + dashPattern: TArrayOfDouble; patternOffset: PDouble; lineWidth: double; renderer: TCustomRenderer; endStyle: TEndStyle; joinStyle: TJoinStyle); var i: integer; @@ -2050,8 +2690,8 @@ procedure DrawDashedLine(img: TImage32; const lines: TPathsD; end; // ------------------------------------------------------------------------------ -procedure DrawInvertedDashedLine(img: TImage32; - const line: TPathD; dashPattern: TArrayOfInteger; +procedure DrawInvertedDashedLine(img, bkgndImg: TImage32; + const line: TPathD; dashPattern: TArrayOfDouble; patternOffset: PDouble; lineWidth: double; endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); var @@ -2059,7 +2699,14 @@ procedure DrawInvertedDashedLine(img: TImage32; lines: TPathsD; renderer: TInverseRenderer; begin - if not assigned(line) then exit; + // when using an alterate background image, + // make sure it's the same size as img ... + if Assigned(bkgndImg) and + (bkgndImg.Width <> img.Width) or + (bkgndImg.Height <> img.Height) then bkgndImg := nil; + + if not assigned(line) or img.IsEmpty then exit; + if (lineWidth < MinStrokeWidth) then lineWidth := MinStrokeWidth; for i := 0 to High(dashPattern) do @@ -2068,29 +2715,49 @@ procedure DrawInvertedDashedLine(img: TImage32; lines := GetDashedPath(line, endStyle = esPolygon, dashPattern, patternOffset); if Length(lines) = 0 then Exit; lines := RoughOutline(lines, lineWidth, joinStyle, endStyle); - renderer := TInverseRenderer.Create; + renderer := TInverseRenderer.Create(bkgndImg); try - if renderer.Initialize(img) then - begin - Rasterize(lines, img.bounds, frNonZero, renderer); - renderer.NotifyChange; - end; + Rasterize(img, lines, img.bounds, frNonZero, renderer); finally renderer.Free; end; end; // ------------------------------------------------------------------------------ +procedure DrawInvertedDashedLine(img, bkgndImg: TImage32; + const lines: TPathsD; dashPattern: TArrayOfDouble; + patternOffset: PDouble; lineWidth: double; endStyle: TEndStyle; + joinStyle: TJoinStyle = jsAuto); +var + i: integer; +begin + if not assigned(lines) then exit; + for i := 0 to high(lines) do + DrawInvertedDashedLine(img, bkgndImg, lines[i], + dashPattern, patternOffset, lineWidth, endStyle, joinStyle); +end; +// ------------------------------------------------------------------------------ + procedure DrawInvertedDashedLine(img: TImage32; - const lines: TPathsD; dashPattern: TArrayOfInteger; - patternOffset: PDouble; lineWidth: double; - endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); + const line: TPathD; dashPattern: TArrayOfDouble; + patternOffset: PDouble; lineWidth: double; endStyle: TEndStyle; + joinStyle: TJoinStyle); +begin + DrawInvertedDashedLine(img, nil, line, + dashPattern, patternOffset, lineWidth, endStyle, joinStyle); +end; +// ------------------------------------------------------------------------------ + +procedure DrawInvertedDashedLine(img: TImage32; + const lines: TPathsD; dashPattern: TArrayOfDouble; + patternOffset: PDouble; lineWidth: double; endStyle: TEndStyle; + joinStyle: TJoinStyle); var i: integer; begin if not assigned(lines) then exit; for i := 0 to high(lines) do - DrawInvertedDashedLine(img, lines[i], + DrawInvertedDashedLine(img, nil, lines[i], dashPattern, patternOffset, lineWidth, endStyle, joinStyle); end; // ------------------------------------------------------------------------------ @@ -2115,11 +2782,7 @@ procedure DrawPolygon(img: TImage32; const polygon: TPathD; if (not assigned(polygon)) or (not assigned(renderer)) then exit; setLength(polygons, 1); polygons[0] := polygon; - if renderer.Initialize(img) then - begin - Rasterize(polygons, img.Bounds, fillRule, renderer); - renderer.NotifyChange; - end; + Rasterize(img, polygons, img.Bounds, fillRule, renderer); end; // ------------------------------------------------------------------------------ @@ -2133,11 +2796,7 @@ procedure DrawPolygon(img: TImage32; const polygons: TPathsD; cr := TColorRenderer.Create(color) else cr := TAliasedColorRenderer.Create(color); try - if cr.Initialize(img) then - begin - Rasterize(polygons, img.bounds, fillRule, cr); - cr.NotifyChange; - end; + Rasterize(img, polygons, img.bounds, fillRule, cr); finally cr.free; end; @@ -2145,17 +2804,33 @@ procedure DrawPolygon(img: TImage32; const polygons: TPathsD; // ------------------------------------------------------------------------------ procedure DrawPolygon(img: TImage32; const polygons: TPathsD; - fillRule: TFillRule; renderer: TCustomRenderer); + fillRule: TFillRule; color: TColor32; + rendererCache: TCustomRendererCache); +var + cr: TCustomColorRenderer; begin - if (not assigned(polygons)) or (not assigned(renderer)) then exit; - if renderer.Initialize(img) then + if not assigned(polygons) then exit; + if rendererCache = nil then + DrawPolygon(img, polygons, fillRule, color) + else begin - Rasterize(polygons, img.bounds, fillRule, renderer); - renderer.NotifyChange; + if img.AntiAliased then + cr := rendererCache.ColorRenderer else + cr := rendererCache.AliasedColorRenderer; + cr.SetColor(color); + Rasterize(img, polygons, img.bounds, fillRule, cr); end; end; // ------------------------------------------------------------------------------ +procedure DrawPolygon(img: TImage32; const polygons: TPathsD; + fillRule: TFillRule; renderer: TCustomRenderer); +begin + if (not assigned(polygons)) or (not assigned(renderer)) then exit; + Rasterize(img, polygons, img.bounds, fillRule, renderer); +end; +// ------------------------------------------------------------------------------ + procedure DrawInvertedPolygon(img: TImage32; const polygon: TPathD; fillRule: TFillRule); var @@ -2176,11 +2851,7 @@ procedure DrawInvertedPolygon(img: TImage32; const polygons: TPathsD; if not assigned(polygons) then exit; cr := TInverseRenderer.Create; try - if cr.Initialize(img) then - begin - Rasterize(polygons, img.bounds, fillRule, cr); - cr.NotifyChange; - end; + Rasterize(img, polygons, img.bounds, fillRule, cr); finally cr.free; end; @@ -2206,19 +2877,45 @@ procedure DrawPolygon_ClearType(img: TImage32; const polygons: TPathsD; tmpPolygons := ScalePath(tmpPolygons, 3, 1); cr := TColorRenderer.Create(clBlack32); try - if cr.Initialize(tmpImg) then - Rasterize(tmpPolygons, tmpImg.bounds, fillRule, cr); + Rasterize(tmpImg, tmpPolygons, tmpImg.bounds, fillRule, cr); finally cr.Free; end; ApplyClearType(tmpImg, color, backColor); - img.CopyBlend(tmpImg, tmpImg.Bounds, rec, BlendToAlpha); + img.CopyBlend(tmpImg, tmpImg.Bounds, rec, BlendToAlphaLine); finally tmpImg.Free; end; end; // ------------------------------------------------------------------------------ +procedure EraseLine(img: TImage32; const line: TPathD; lineWidth: double; + endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto; miterLimit: double = 2); +var + lines: TPathsD; +begin + if not assigned(line) then exit; + setLength(lines, 1); + lines[0] := line; + EraseLine(img, lines, lineWidth, endStyle, joinStyle, miterLimit); +end; +// ------------------------------------------------------------------------------ + +procedure EraseLine(img: TImage32; const lines: TPathsD; lineWidth: double; + endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto; miterLimit: double = 2); +var + er: TEraseRenderer; +begin + if not assigned(lines) then exit; + er := TEraseRenderer.Create; + try + DrawLine(img, lines, lineWidth, er, endStyle, joinStyle, miterLimit); + finally + er.Free; + end; +end; +// ------------------------------------------------------------------------------ + procedure ErasePolygon(img: TImage32; const polygon: TPathD; fillRule: TFillRule); var @@ -2238,11 +2935,7 @@ procedure ErasePolygon(img: TImage32; const polygons: TPathsD; begin er := TEraseRenderer.Create; try - if er.Initialize(img) then - begin - Rasterize(polygons, img.bounds, fillRule, er); - er.NotifyChange; - end; + Rasterize(img, polygons, img.bounds, fillRule, er); finally er.Free; end; diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Extra.pas b/Ext/SVGIconImageList/Image32/source/Img32.Extra.pas index b0f5b5d..a639e12 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Extra.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Extra.pas @@ -2,12 +2,12 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 2 May 2024 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2024 * +* Version : 4.8 * +* Date : 2 February 2025 * +* Website : https://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 * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -30,7 +30,7 @@ procedure DrawEdge(img: TImage32; const path: TPathD; topLeftColor, bottomRightColor: TColor32; penWidth: double = 1.0; closePath: Boolean = true); overload; -//DrawShadowRect: is **much** faster than DrawShadow +// 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; @@ -45,8 +45,8 @@ procedure DrawGlow(img: TImage32; const polygon: TPathD; 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). +// FloodFill: If no CompareFunc is provided, FloodFill will fill wherever +// 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); @@ -57,16 +57,24 @@ procedure FastGaussianBlur(img: TImage32; 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) +// 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 +// 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. +// Hatch: This will overwrite the image and ignore any transparency +procedure Hatch(img: TImage32; color1: TColor32 = clWhite32; + color2: TColor32= $FFE8E8E8; hatchSize: Integer = 10); overload; +procedure Hatch(img: TImage32; const rec: TRect; + color1: TColor32 = clWhite32; color2: TColor32= $FFE8E8E8; + hatchSize: Integer = 10); overload; + +// HatchBackground: hatches behind the existing image, so +// it 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; @@ -77,22 +85,27 @@ procedure GridBackground(img: TImage32; majorInterval, minorInterval: integer; fillColor: TColor32 = clWhite32; majColor: TColor32 = $30000000; minColor: TColor32 = $20000000); +procedure ReplaceColor(img: TImage32; oldColor, newColor: TColor32; + channelTolerance: Byte; preserveAlpha: Boolean = false); 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/ +// 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); +procedure RemoveExactColor(img: TImage32; color: TColor32); +// RemoveAllExceptColor: Opposite of RemoveColor +procedure RemoveAllExceptColor(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); +// FilterOnColor - renamed RemoveAllExceptColor +procedure FilterOnColor(img: TImage32; color: TColor32); deprecated; +// FilterOnExactColor - renamed RemoveExactColor +procedure FilterOnExactColor(img: TImage32; color: TColor32); deprecated; -procedure FilterOnAlpha(img: TImage32; alpha: byte; tolerance: byte); +// FilterOnAlpha - simpler just to set alpha to zero below a specified alpha +// procedure FilterOnAlpha(img: TImage32; alpha: byte; tolerance: byte); -//RedEyeRemove: Removes 'red eye' from flash photo images. +// RedEyeRemove: Removes 'red eye' from flash photo images. procedure RedEyeRemove(img: TImage32; const rect: TRect); procedure PencilEffect(img: TImage32; intensity: integer = 0); @@ -107,7 +120,8 @@ procedure EraseInsidePaths(img: TImage32; procedure EraseOutsidePath(img: TImage32; const path: TPathD; fillRule: TFillRule; const outsideBounds: TRect); procedure EraseOutsidePaths(img: TImage32; const paths: TPathsD; - fillRule: TFillRule; const outsideBounds: TRect); + fillRule: TFillRule; const outsideBounds: TRect; + rendererCache: TCustomRendererCache = nil); overload; procedure Draw3D(img: TImage32; const polygon: TPathD; fillRule: TFillRule; height, blurRadius: double; @@ -149,8 +163,8 @@ function SimplifyPaths(const paths: TPathsD; shapeTolerance: double = 0.1; isClosedPath: Boolean = true): TPathsD; {$ENDIF} -// SimplifyPathEx: this is mainly useful following Vectorize() -// Also removes very short segments that zig-zag (rather than curve) +// 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; @@ -172,12 +186,9 @@ function SmoothPath(const path: TPathD; isClosedPath: Boolean; 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; +function SymmetricCropTransparent(img: TImage32): TPoint; -procedure SymmetricCropTransparent(img: TImage32); - -//3 additional blend functions (see TImage32.CopyBlend) +// Three additional blend functions (see TImage32.CopyBlend) function BlendAverage(bgColor, fgColor: TColor32): TColor32; function BlendLinearBurn(bgColor, fgColor: TColor32): TColor32; function BlendColorDodge(bgColor, fgColor: TColor32): TColor32; @@ -217,6 +228,23 @@ TVertex = record // 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; @@ -275,14 +303,15 @@ function GetSymmetricCropTransparentRect(img: TImage32): TRect; end; //------------------------------------------------------------------------------ -//SymmetricCropTransparent: after cropping, the image's midpoint -//will be the same pixel as before cropping. (Important for rotating.) -procedure SymmetricCropTransparent(img: TImage32); +// SymmetricCropTransparent: after cropping, the image's midpoint +// will be the same pixel as before cropping. (Important for rotating.) +function SymmetricCropTransparent(img: TImage32): TPoint; var rec: TRect; begin rec := GetSymmetricCropTransparentRect(img); - if (rec.Top > 0) or (rec.Left > 0) then img.Crop(rec); + Result := rec.TopLeft; + if (Result.X > 0) or (Result.Y > 0) then img.Crop(rec); end; //------------------------------------------------------------------------------ @@ -345,7 +374,7 @@ procedure DrawEdge(img: TImage32; const path: TPathD; p := path; if closePath and not PointsNearEqual(p[0], p[highI], 0.01) then begin - AppendToPath(p, p[0]); + AppendPoint(p, p[0]); inc(highI); end; for i := 1 to highI do @@ -538,7 +567,9 @@ procedure DrawShadow(img: TImage32; const polygons: TPathsD; begin rec := GetBounds(polygons); if IsEmptyRect(rec) or (depth < 1) then Exit; - if not ClockwiseRotationIsAnglePositive then angleRads := -angleRads; +{$IFDEF CLOCKWISE_ROTATION_WITH_NEGATIVE_ANGLES} + angleRads := -angleRads; +{$ENDIF} NormalizeAngle(angleRads); GetSinCos(angleRads, y, x); depth := depth * 0.5; @@ -554,7 +585,7 @@ procedure DrawShadow(img: TImage32; const polygons: TPathsD; DrawPolygon(shadowImg, shadowPolys, fillRule, color); FastGaussianBlur(shadowImg, shadowImg.Bounds, blurSize, 1); if cutoutInsideShadow then EraseInsidePaths(shadowImg, polys, fillRule); - img.CopyBlend(shadowImg, shadowImg.Bounds, rec, BlendToAlpha); + img.CopyBlend(shadowImg, shadowImg.Bounds, rec, BlendToAlphaLine); finally shadowImg.Free; end; @@ -590,7 +621,7 @@ procedure DrawGlow(img: TImage32; const polygons: TPathsD; DrawPolygon(glowImg, glowPolys, fillRule, color); FastGaussianBlur(glowImg, glowImg.Bounds, blurRadius, 2); glowImg.ScaleAlpha(4); - img.CopyBlend(glowImg, glowImg.Bounds, rec, BlendToAlpha); + img.CopyBlend(glowImg, glowImg.Bounds, rec, BlendToAlphaLine); finally glowImg.Free; end; @@ -631,31 +662,106 @@ procedure Sharpen(img: TImage32; radius: Integer; amount: Integer); end; //------------------------------------------------------------------------------ -procedure HatchBackground(img: TImage32; const rec: TRect; - color1: TColor32 = clWhite32; color2: TColor32= $FFE8E8E8; - hatchSize: Integer = 10); overload; +procedure InternalHatch(img: TImage32; const rec: TRect; + color1, color2: TColor32; hatchSize: Integer = 10); var - i,j: Integer; + 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 + pc^ := colors[hatch]; + inc(pc); inc(x); + if x >= hatchSize then + begin + x := 0; + hatch := not hatch; + end; + end; + end; +end; +//------------------------------------------------------------------------------ + +procedure Hatch(img: TImage32; color1: TColor32 = clWhite32; + color2: TColor32= $FFE8E8E8; hatchSize: Integer = 10); +begin + Hatch(img, img.Bounds, color1, color2, hatchSize); +end; +//------------------------------------------------------------------------------ + +procedure Hatch(img: TImage32; const rec: TRect; + color1: TColor32 = clWhite32; color2: TColor32= $FFE8E8E8; + hatchSize: Integer = 10); +begin + if (rec.Right <= rec.Left) or (rec.Bottom - rec.Top <= 0) then Exit; img.BeginUpdate; try - for i := rec.Top to rec.Bottom -1 do + InternalHatch(img, rec, color1, color2, hatchSize); + finally + img.EndUpdate; + 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 - pc := img.PixelRow[i]; - inc(pc, rec.Left); - hatch := Odd(i div hatchSize); - for j := rec.Left to rec.Right -1 do + 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 - if (j + 1) mod hatchSize = 0 then hatch := not hatch; - pc^ := BlendToOpaque(pc^, colors[hatch]); - inc(pc); + 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; @@ -674,47 +780,58 @@ procedure GridBackground(img: TImage32; majorInterval, minorInterval: integer; var i, x,y, w,h: integer; path: TPathD; + cr: TCustomColorRenderer; begin img.Clear(fillColor); w := img.Width; h := img.Height; - SetLength(path, 2); - if minorInterval > 0 then - begin - 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, minColor, 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, minColor, esSquare); - path[0].Y := path[0].Y + minorInterval; - path[1].Y := path[1].Y + minorInterval; - end; - end; - if majorInterval > minorInterval then - begin - x := majorInterval; - path[0] := PointD(x, 0); path[1] := PointD(x, h);; - for i := 1 to (w div majorInterval) do + NewPointDArray(path, 2, True); + + if img.AntiAliased then + cr := TColorRenderer.Create(minColor) else + cr := TAliasedColorRenderer.Create(minColor); + try + if minorInterval > 0 then begin - Img32.Draw.DrawLine(img, path, 1, majColor, esSquare); - path[0].X := path[0].X + majorInterval; - path[1].X := path[1].X + majorInterval; + 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; - y := majorInterval; - path[0] := PointD(0, y); path[1] := PointD(w, y); - for i := 1 to (h div majorInterval) do + if majorInterval > minorInterval then begin - Img32.Draw.DrawLine(img, path, 1, majColor, esSquare); - path[0].Y := path[0].Y + majorInterval; - path[1].Y := path[1].Y + majorInterval; + 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; //------------------------------------------------------------------------------ @@ -744,6 +861,40 @@ procedure ReplaceExactColor(img: TImage32; oldColor, newColor: TColor32); end; //------------------------------------------------------------------------------ +procedure ReplaceColor(img: TImage32; oldColor, newColor: TColor32; + channelTolerance: Byte; preserveAlpha: Boolean); +var + c: PARGB; + a,r,g,b: Byte; + i: Integer; +begin + c := PARGB(img.PixelBase); + a := TARGB(oldColor).A; + r := TARGB(oldColor).R; + g := TARGB(oldColor).G; + b := TARGB(oldColor).B; + + if preserveAlpha then + begin + newColor := newColor and $FFFFFF; + for i := 0 to img.Width * img.Height -1 do + begin + if Abs(c.R - r) + Abs(c.G - g) + Abs(c.B - b) <= channelTolerance then + c.Color := a or newColor; + inc(c); + end + end else + for i := 0 to img.Width * img.Height -1 do + begin + if (Abs(c.A - a) <= channelTolerance) and + (Abs(c.R - r) <= channelTolerance) and + (Abs(c.G - g) <= channelTolerance) and + (Abs(c.B - b) <= channelTolerance) then c.Color := newColor; + inc(c); + end +end; +//------------------------------------------------------------------------------ + procedure RemoveColor(img: TImage32; color: TColor32); var fg: TARGB absolute color; @@ -773,12 +924,14 @@ procedure RemoveColor(img: TImage32; color: TColor32); if (Q = 0) then bg.Color := clNone32 - else if (Q < 255) then + else if (Q = 255) then + // do nothing + else 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]; + bg.R := DivTable[ClampByte(bg.R - MulTable[not Q, fg.R]), Q]; + bg.G := DivTable[ClampByte(bg.G - MulTable[not Q, fg.G]), Q]; + bg.B := DivTable[ClampByte(bg.B - MulTable[not Q, fg.B]), Q]; end; end; inc(bg); @@ -786,7 +939,7 @@ procedure RemoveColor(img: TImage32; color: TColor32); end; //------------------------------------------------------------------------------ -procedure FilterOnColor(img: TImage32; color: TColor32); +procedure RemoveAllExceptColor(img: TImage32; color: TColor32); var fg: TARGB absolute color; bg: PARGB; @@ -800,24 +953,16 @@ procedure FilterOnColor(img: TImage32; color: TColor32); 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; - + 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]); + 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]); + 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]; @@ -831,7 +976,13 @@ procedure FilterOnColor(img: TImage32; color: TColor32); end; //------------------------------------------------------------------------------ -procedure FilterOnExactColor(img: TImage32; color: TColor32); +procedure FilterOnColor(img: TImage32; color: TColor32); +begin + RemoveAllExceptColor(img, color); +end; +//------------------------------------------------------------------------------ + +procedure RemoveExactColor(img: TImage32; color: TColor32); var pc: PColor32; i: Integer; @@ -850,20 +1001,26 @@ procedure FilterOnExactColor(img: TImage32; color: TColor32); end; //------------------------------------------------------------------------------ -procedure FilterOnAlpha(img: TImage32; alpha: byte; tolerance: byte); -var - bg: PARGB; - i: Integer; +procedure FilterOnExactColor(img: TImage32; color: TColor32); 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; + RemoveExactColor(img, color); 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; @@ -896,7 +1053,7 @@ procedure RedEyeRemove(img: TImage32; const rect: TRect); path := Ellipse(cutoutRec); radGrad.SetParameters(rect3, clBlack32, clNone32); DrawPolygon(mask, path, frNonZero, radGrad); - cutout.CopyBlend(mask, mask.Bounds, cutout.Bounds, BlendMask); + cutout.CopyBlend(mask, mask.Bounds, cutout.Bounds, BlendMaskLine); // now remove red from the cutout RemoveColor(cutout, clRed32); // finally replace the cutout ... @@ -923,42 +1080,86 @@ procedure EraseInsidePaths(img: TImage32; const paths: TPathsD; fillRule: TFillR 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 - mask: TImage32; - p: TPathD; - w,h: integer; + w, h: integer; + renderer: TMaskRenderer; + r: TRect; + polygons: TPathsD; begin if not assigned(path) then Exit; - RectWidthHeight(outsideBounds, w,h); - mask := TImage32.Create(w, h); + 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 - p := TranslatePath(path, -outsideBounds.Left, -outsideBounds.top); - DrawPolygon(mask, p, fillRule, clBlack32); - img.CopyBlend(mask, mask.Bounds, outsideBounds, BlendMask); + SetLength(polygons, 1); + polygons[0] := path; + Rasterize(img, polygons, outsideBounds, fillRule, renderer); finally - mask.Free; + renderer.Free; end; end; //------------------------------------------------------------------------------ procedure EraseOutsidePaths(img: TImage32; const paths: TPathsD; - fillRule: TFillRule; const outsideBounds: TRect); + fillRule: TFillRule; const outsideBounds: TRect; + rendererCache: TCustomRendererCache); var - mask: TImage32; - pp: TPathsD; - w,h: integer; + w, h: integer; + renderer: TMaskRenderer; + r: TRect; begin if not assigned(paths) then Exit; - RectWidthHeight(outsideBounds, w,h); - mask := TImage32.Create(w, h); + 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 - pp := TranslatePath(paths, -outsideBounds.Left, -outsideBounds.top); - DrawPolygon(mask, pp, fillRule, clBlack32); - img.CopyBlend(mask, mask.Bounds, outsideBounds, BlendMask); + Rasterize(img, paths, outsideBounds, fillRule, renderer); finally - mask.Free; + if rendererCache = nil then + renderer.Free; end; end; //------------------------------------------------------------------------------ @@ -987,7 +1188,9 @@ procedure Draw3D(img: TImage32; const polygons: TPathsD; begin rec := GetBounds(polygons); if IsEmptyRect(rec) then Exit; - if not ClockwiseRotationIsAnglePositive then angleRads := -angleRads; +{$IFDEF CLOCKWISE_ROTATION_WITH_NEGATIVE_ANGLES} + angleRads := -angleRads; +{$ENDIF} GetSinCos(angleRads, y, x); paths := TranslatePath(polygons, -rec.Left, -rec.Top); RectWidthHeight(rec, w, h); @@ -1000,7 +1203,7 @@ procedure Draw3D(img: TImage32; const polygons: TPathsD; EraseInsidePaths(tmp, paths2, fillRule); FastGaussianBlur(tmp, tmp.Bounds, Round(blurRadius), 0); EraseOutsidePaths(tmp, paths, fillRule, tmp.Bounds); - img.CopyBlend(tmp, tmp.Bounds, rec, BlendToAlpha); + img.CopyBlend(tmp, tmp.Bounds, rec, BlendToAlphaLine); end; if GetAlpha(colorDk) > 0 then begin @@ -1009,7 +1212,7 @@ procedure Draw3D(img: TImage32; const polygons: TPathsD; EraseInsidePaths(tmp, paths2, fillRule); FastGaussianBlur(tmp, tmp.Bounds, Round(blurRadius), 0); EraseOutsidePaths(tmp, paths, fillRule, tmp.Bounds); - img.CopyBlend(tmp, tmp.Bounds, rec, BlendToAlpha); + img.CopyBlend(tmp, tmp.Bounds, rec, BlendToAlphaLine); end; finally tmp.Free; @@ -1088,7 +1291,7 @@ function DrawButton(img: TImage32; const pt: TPointD; case buttonShape of bsDiamond: begin - SetLength(Result, 4); + 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; @@ -1116,7 +1319,7 @@ function DrawButton(img: TImage32; const pt: TPointD; if ba3D in buttonAttributes then Draw3D(img, Result, frNonZero, lightSize*2, Ceil(lightSize), $CCFFFFFF, $AA000000, lightAngle); - DrawLine(img, Result, dpiAware1, clBlack32, esPolygon); + DrawLine(img, Result, dpiAware1, clBlack32, esPolygon, jsButt); finally img.EndUpdate; end; @@ -1199,8 +1402,8 @@ procedure TraceContours(img: TImage32; intensity: integer); begin w := img.Width; h := img.Height; if w * h = 0 then Exit; - SetLength(tmp, w * h); - SetLength(tmp2, w * h); + NewColor32Array(tmp, w * h); + NewColor32Array(tmp2, w * h); s := img.PixelRow[0]; d := @tmp[0]; for j := 0 to h-1 do begin @@ -1222,7 +1425,11 @@ procedure TraceContours(img: TImage32; intensity: integer); inc(s, w); inc(s2, w); inc(d, w); end; end; - Move(tmp2[0], img.PixelBase^, w * h * sizeOf(TColor32)); + + img.BlockNotify; + img.AssignPixelArray(tmp2, w, h); + img.UnblockNotify; + if intensity < 1 then Exit; if intensity > 10 then intensity := 10; // range = 1-10 @@ -1661,97 +1868,64 @@ TSimplifyRec = record pdSqrd : double; prev : PSimplifyRec; next : PSimplifyRec; - isEnd : Boolean; + isEndPt : Boolean; end; function SimplifyPath(const path: TPathD; shapeTolerance: double; isClosedPath: Boolean): TPathD; var - i, highI, minLen: integer; + i, iPrev, iNext, len, minLen: integer; tolSqrd: double; srArray: array of TSimplifyRec; - first, last: PSimplifyRec; + current, last: PSimplifyRec; begin Result := nil; - highI := High(path); + len := Length(path); if not isClosedPath then minLen := 2 else minLen := 3; + if len < minLen then Exit; - if highI +1 < minLen then Exit; - - SetLength(srArray, highI +1); - with srArray[0] do - begin - pt := path[0]; - prev := @srArray[highI]; - next := @srArray[1]; - if isClosedPath then - begin - pdSqrd := PerpendicularDistSqrd(path[0], path[highI], path[1]); - isEnd := false; - end else - begin - pdSqrd := MaxDouble; - isEnd := true; - end; - end; - - with srArray[highI] do - begin - pt := path[highI]; - prev := @srArray[highI-1]; - next := @srArray[0]; - if isClosedPath then - begin - pdSqrd := PerpendicularDistSqrd(path[highI], path[highI-1], path[0]); - isEnd := false; - end else - begin - pdSqrd := MaxDouble; - isEnd := true; - end; - end; - - for i := 1 to highI -1 do + 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[i-1]; - next := @srArray[i+1]; - pdSqrd := PerpendicularDistSqrd(path[i], path[i-1], path[i+1]); - isEnd := false; + 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; - first := @srArray[0]; - last := first.prev; + current := @srArray[0]; + last := current.prev; tolSqrd := Sqr(shapeTolerance); - while first <> last do + while current <> last do begin - if first.isEnd or (first.pdSqrd > tolSqrd) or - (first.next.pdSqrd < first.pdSqrd) then + if not current.isEndPt and + ((current.pdSqrd < tolSqrd) and (current.next.pdSqrd > current.pdSqrd)) then begin - first := first.next; - end else - begin - first.prev.next := first.next; - first.next.prev := first.prev; - last := first.prev; - dec(highI); + 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); - first := last.next; - first.pdSqrd := - PerpendicularDistSqrd(first.pt, first.prev.pt, first.next.pt); - end; + 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 highI +1 < minLen then Exit; - if not isClosedPath then first := @srArray[0]; - SetLength(Result, highI +1); - for i := 0 to HighI do + + 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] := first.pt; - first := first.next; + Result[i] := current.pt; + current := current.next; end; end; //------------------------------------------------------------------------------ @@ -1778,11 +1952,11 @@ function SimplifyPaths(const paths: TPathsD; type PSimplifyExRec = ^TSimplifyExRec; TSimplifyExRec = record - pt : TPointD; - pdSqrd : double; - segLenSq: double; - prev : PSimplifyExRec; - next : PSimplifyExRec; + pt : TPointD; + pdSqrd : double; + segLenSq : double; + prev : PSimplifyExRec; + next : PSimplifyExRec; end; function DeleteCurrent(var current: PSimplifyExRec): Boolean; @@ -1796,37 +1970,36 @@ function DeleteCurrent(var current: PSimplifyExRec): Boolean; Result := next <> current.prev; if not Result then Exit; next.pdSqrd := PerpendicularDistSqrd(next.pt, next.prev.pt, next.next.pt); - next.segLenSq := DistanceSqrd(next.prev.pt, 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, highI: integer; - cp, cp2, shapeTolSqr, shapeTolSqrEx: double; + i, prevI, nextI, len: integer; + shapeTolSqr: double; srArray: array of TSimplifyExRec; current, start: PSimplifyExRec; begin Result := nil; - highI := High(path); - if highI < 2 then Exit; + len := Length(path); + if len < 3 then Exit; shapeTolSqr := Sqr(shapeTolerance); - shapeTolSqrEx := shapeTolerance * 4 +1; // may need adjusting - SetLength(srArray, highI +1); + SetLength(srArray, len); - for i := 0 to highI do + for i := 0 to len -1 do begin prevI := i -1; nextI := i +1; - if i = 0 then prevI := highI - else if i = highI then nextI := 0; + 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[prevI], path[i]); + segLenSq:= DistanceSqrd(path[i], path[nextI]); pdSqrd := PerpendicularDistSqrd(path[i], path[prevI], path[nextI]); prev := @srArray[prevI]; next := @srArray[nextI]; @@ -1838,40 +2011,33 @@ function SimplifyPathEx(const path: TPathD; shapeTolerance: double): TPathD; 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 - // nb: always remove the shorter segment first - // irrespective of segment length remove vertices that - // deviate insignificantly from their adjacent vertices. - dec(highI); + dec(len); if not DeleteCurrent(current) then Break; start := current.prev; - end else if - (current.segLenSq * shapeTolSqrEx < current.prev.segLenSq) and - (current.segLenSq * shapeTolSqrEx < current.next.segLenSq) then + 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 - cp := CrossProduct(current.prev.prev.pt, current.prev.pt, current.pt); - cp2 := CrossProduct(current.prev.pt, current.pt, current.next.pt); - if ((cp > 0) = (cp2 > 0)) then - begin - // not a zig-zag (ie avoids truncating tightly rounded corners) - current := current.next; - end else - begin - // remove insignificant zigzags - current.prev.pt := MidPoint(current.pt, current.prev.pt); - if not DeleteCurrent(current) then Break; - start := current.prev; - dec(highI); - end; + dec(len); + if not DeleteCurrent(current) then Break; + start := current.prev; end else current := current.next; end; - if highI < 2 then Exit; - SetLength(Result, highI +1); - for i := 0 to HighI do + if len < 3 then Exit; + NewPointDArray(Result, len, True); + for i := 0 to len -1 do begin Result[i] := current.pt; current := current.next; @@ -1918,7 +2084,7 @@ function SmoothToCubicBezier(const path: TPathD; len := Length(path); if len < 3 then Exit; - SetLength(Result, len *3 +1); + NewPointDArray(Result, len *3 +1, True); prev := len-1; SetLength(pl, len); SetLength(unitVecs, len); @@ -1991,7 +2157,7 @@ function SmoothToCubicBezier2(const path: TPathD; len := Length(path); if len < 3 then Exit; - SetLength(Result, len *3 +1); + NewPointDArray(Result, len *3 +1); prev := len-1; SetLength(pl, len); SetLength(unitVecs, len); @@ -2048,21 +2214,6 @@ function SmoothToCubicBezier2(const paths: TPathsD; end; //------------------------------------------------------------------------------ -function Clamp(val, endVal: integer): integer; inline; -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; inline; -begin - Result := val mod endVal; - if Result < 0 then Result := endVal + Result; -end; -//------------------------------------------------------------------------------ - function CubicInterpolate(v1, v2, v3, v4: double; t: double; tension: double = 0): double; var @@ -2080,12 +2231,13 @@ function CubicInterpolate(v1, v2, v3, v4: double; end; //------------------------------------------------------------------------------ -procedure Append(var path: TPathD; const pt: TPointD); inline; +procedure Append(var path: TPathD; const pt: TPointD); + {$IFDEF INLINE} inline; {$ENDIF} var len: integer; begin len := Length(path); - SetLength(path, len +1); + SetLengthUninit(path, len +1); path[len] := pt; end; //------------------------------------------------------------------------------ @@ -2180,43 +2332,68 @@ function SmoothPaths(const paths: TPathsD; isClosedPath: Boolean; procedure GaussianBlur(img: TImage32; rec: TRect; radius: Integer); var - i, w,h, x,y,yy,z: Integer; - gaussTable: array [-MaxBlur .. MaxBlur] of Cardinal; + 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; - for i := 0 to radius do + + expConst := - 1 / (Sqr(radius) * 2 * Sqr(sigma)); + gaussTable[0] := Round(tableConst * Exp(expConst)); + for i := 1 to radius do begin - gaussTable[i] := Sqr(Radius - i +1); + 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) * img.Width + rec.Left]); + 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(img.Width -1, x + radius) 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; - yy := max(0, y - radius) * w; - for z := max(0, y - radius) to min(h -1, y + radius) do + startz := max(0, y - radius); + yy := startz * w; + for z := startz to min(h -1, y + radius) do begin - wc.Add(wca[x + yy].Color, gaussTable[y-z]); + wc.Add(wcaColor[x + yy], gaussTable[y-z]); inc(yy, w); end; - img.Pixels[x + rec.Left + (y + rec.Top) * img.Width] := wc.Color; + pc^ := wc.Color; + inc(PByte(pc), imgWidth); // increment by byte size end; end; end; @@ -2225,15 +2402,15 @@ procedure GaussianBlur(img: TImage32; rec: TRect; radius: Integer); // FastGaussian blur - and support functions //------------------------------------------------------------------------------ -//http://blog.ivank.net/fastest-gaussian-blur.html -//https://www.peterkovesi.com/papers/FastGaussianSmoothing.pdf +// 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 - SetLength(Result, boxCnt); + 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 := @@ -2255,11 +2432,64 @@ procedure FastGaussianBlur(img: TImage32; end; //------------------------------------------------------------------------------ -procedure BoxBlurH(var src, dst: TArrayOfColor32; w,h, stdDev: integer); +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; - ce: TColor32; + lastColor: TColor32; + stdDevW: integer; begin ovr := Max(0, stdDev - w); for i := 0 to h -1 do @@ -2268,7 +2498,6 @@ procedure BoxBlurH(var src, dst: TArrayOfColor32; w,h, stdDev: integer); li := ti; ri := ti +stdDev; re := ti +w -1; // idx of last pixel in row - ce := src[re]; // color of last pixel in row fv.Reset(src[ti]); val.Reset(src[ti], stdDev +1); for j := 0 to stdDev -1 - ovr do @@ -2276,41 +2505,109 @@ procedure BoxBlurH(var src, dst: TArrayOfColor32; w,h, stdDev: integer); if ovr > 0 then val.Add(clNone32, ovr); for j := 0 to stdDev do begin - if ri > re then - val.Add(ce) else - val.Add(src[ri]); + 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; - for j := stdDev +1 to w - stdDev -1 do + + // 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 ri <= re then + 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 - val.Add(src[ri]); inc(ri); - val.Subtract(src[li]); inc(li); + 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; - dst[ti] := val.Color; inc(ti); end; - while ti <= re do + 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 - if ti > re then Break; - val.Add(clNone32); - val.Subtract(src[li]); inc(li); - dst[ti] := val.Color; - inc(ti); + 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(var src, dst: TArrayOfColor32; w, h, stdDev: integer); +procedure BoxBlurV(const src, dst: TArrayOfColor32; w, h, stdDev: integer); var i,j, ti, li, ri, re, ovr: integer; fv, val: TWeightedColor; - ce: TColor32; + lastColor: TColor32; + stdDevH: integer; begin ovr := Max(0, stdDev - h); for i := 0 to w -1 do @@ -2319,7 +2616,6 @@ procedure BoxBlurV(var src, dst: TArrayOfColor32; w, h, stdDev: integer); li := ti; ri := ti + stdDev * w; re := ti +w *(h-1); // idx of last pixel in column - ce := src[re]; // color of last pixel in column fv.Reset(src[ti]); val.Reset(src[ti], stdDev +1); for j := 0 to stdDev -1 -ovr do @@ -2327,30 +2623,47 @@ procedure BoxBlurV(var src, dst: TArrayOfColor32; w, h, stdDev: integer); if ovr > 0 then val.Add(clNone32, ovr); for j := 0 to stdDev do begin - if ri > re then - val.Add(ce) else - val.Add(src[ri]); + 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; - for j := stdDev +1 to h - stdDev -1 do + + // 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 ri <= re then + 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 - val.Add(src[ri]); inc(ri, w); - val.Subtract(src[li]); inc(li, w); + 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; - dst[ti] := val.Color; inc(ti, w); - end; - while ti <= re do - begin - val.Add(clNone32); - val.Subtract(src[li]); inc(li, w); - dst[ti] := val.Color; - inc(ti, w); end; end; end; @@ -2374,15 +2687,17 @@ procedure FastGaussianBlur(img: TImage32; RectWidthHeight(rec2, w, h); if (Min(w, h) < 2) or ((stdDevX < 1) and (stdDevY < 1)) then Exit; len := w * h; - SetLength(src, len); - SetLength(dst, len); + NewColor32Array(src, len, True); // content is overwritten in BoxBlurH if blurFullImage then begin - // copy the entire image into 'dst' - Move(img.PixelBase^, dst[0], len * SizeOf(TColor32)); - end else + // 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]; @@ -2393,25 +2708,25 @@ procedure FastGaussianBlur(img: TImage32; 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); - for j := 0 to repeats -1 do + + 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; - // copy dst array back to image rect - img.BeginUpdate; - try - if blurFullImage then - begin - Move(dst[0], img.PixelBase^, len * SizeOf(TColor32)); - end else + + if not blurFullImage then begin + // copy dst array back to image rect pDst := img.PixelRow[rec2.Top]; inc(pDst, rec2.Left); pSrc := @dst[0]; diff --git a/Ext/SVGIconImageList/Image32/source/Img32.FMX.pas b/Ext/SVGIconImageList/Image32/source/Img32.FMX.pas index a2cf921..060873f 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.FMX.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.FMX.pas @@ -2,12 +2,12 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 3 September 2023 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2023 * +* Version : 4.7 * +* Date : 6 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : Image file format support for TImage32 and FMX * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -82,6 +82,9 @@ function TImageFormat_FMX.LoadFromStream(stream: TStream; var cm: TBitmapCodecManager; surf: TBitmapSurface; +//{$IF DEFINED(ANDROID)} +// i: integer; +//{$IFEND} begin result := false; surf := TBitmapSurface.Create; @@ -95,6 +98,11 @@ function TImageFormat_FMX.LoadFromStream(stream: TStream; else Exit; img32.SetSize(surf.Width, surf.Height); Move(surf.Scanline[0]^, img32.PixelBase^, surf.Width * surf.Height * 4); +// {$IF DEFINED(ANDROID)} +// if img32.HasTransparency then +// for i := 0 to img32.Width * img32.Height -1 do +// img32.Pixels[i] := SwapRedBlue(img32.Pixels[i]); +// {$IFEND} result := true; finally cm.Free; diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.BMP.pas b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.BMP.pas index c1c0837..84bfc9b 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.BMP.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.BMP.pas @@ -2,12 +2,12 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 28 March 2024 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2024 * +* Version : 4.7 * +* Date : 6 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : BMP file format extension for TImage32 * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -729,6 +729,7 @@ procedure StreamWrite24BitImage(img32: TImage32; stream: TStream); pc: PColor32; pb: PByte; begin + //rowSize = img32.Width *3 then rounded up to a multiple of 4 rowSize := GetRowSize(24, img32.Width); delta := rowSize - (img32.Width *3); totalBytes := rowSize * img32.Height; @@ -758,7 +759,6 @@ procedure TImageFormat_BMP.SaveToStream(stream: TStream; UsesAlpha: Boolean; pals: TArrayOfColor32; tmp: TImage32; - writeValue: TTriColor32; begin //write everything except a BMP file header because some streams //(eg resource streams) don't need a file header @@ -825,10 +825,9 @@ procedure TImageFormat_BMP.SaveToStream(stream: TStream; end; 24: begin - bih.bV4V4Compression := BI_BITFIELDS; stream.Write(bih, bih.bV4Size); - writeValue := MakeBitfields; - stream.Write(writeValue, SizeOf(TTriColor32)); + // nb: BI_BITFIELDS only used in 16bpp and 32bpp formats + // See BITMAPINFOHEADER structure StreamWrite24BitImage(tmp, stream); end else diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.GIF.pas b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.GIF.pas index ea92178..bb0b2de 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.GIF.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.GIF.pas @@ -1,12 +1,12 @@ unit Img32.Fmt.GIF; (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 12 March 2023 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2023 * +* Version : 4.7 * +* Date : 6 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : GIF file format extension for TImage32 * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.JPG.pas b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.JPG.pas index 4d9a97f..9c9e291 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.JPG.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.JPG.pas @@ -2,12 +2,12 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 12 March 2023 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2023 * +* Version : 4.7 * +* Date : 6 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : JPG/JPEG file format extension for TImage32 * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -26,7 +26,7 @@ TImageFormat_JPG = class(TImageFormat) img32: TImage32; imgIndex: integer = 0): Boolean; override; //SaveToStream: compressionQuality (range: 0-100%) procedure SaveToStream(stream: TStream; - img32: TImage32; compressionQlty: integer = -1); override; + img32: TImage32; compressionQlty: integer = defaultCompression); override; class function CopyToClipboard(img32: TImage32): Boolean; override; class function CanPasteFromClipboard: Boolean; override; class function PasteFromClipboard(img32: TImage32): Boolean; override; @@ -98,8 +98,9 @@ procedure TImageFormat_JPG.SaveToStream(stream: TStream; Jpeg := TJpegImage.Create; with TJpegImageHack(jpeg) do try - if (compressionQlty >= 0) then - jpeg.CompressionQuality := Min(100, compressionQlty); + if compressionQuality = defaultCompression then + jpeg.CompressionQuality := 75 else + jpeg.CompressionQuality := Max(0, Min(100, compressionQuality)); NewImage; NewBitmap; Bitmap.Width := img32.Width; diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.PNG.pas b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.PNG.pas index 6f1bfb8..f44381d 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.PNG.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.PNG.pas @@ -2,12 +2,12 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 9 May 2023 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2023 * +* Version : 4.7 * +* Date : 6 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : PNG file format extension for TImage32 * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -25,9 +25,9 @@ TImageFormat_PNG = class(TImageFormat) class function IsValidImageStream(stream: TStream): Boolean; override; function LoadFromStream(stream: TStream; img32: TImage32; imgIndex: integer = 0): Boolean; override; - // SaveToStream: the compressionQuality parameter is ignored here + // SaveToStream: compressionQuality range is 0 .. 9 (ZLIB compression) procedure SaveToStream(stream: TStream; - img32: TImage32; compressionQuality: integer = 0); override; + img32: TImage32; compressionQuality: integer = defaultCompression); override; class function CanCopyToClipboard: Boolean; override; class function CopyToClipboard(img32: TImage32): Boolean; override; class function CanPasteFromClipboard: Boolean; override; @@ -87,7 +87,7 @@ function TImageFormat_PNG.LoadFromStream(stream: TStream; //------------------------------------------------------------------------------ procedure TImageFormat_PNG.SaveToStream(stream: TStream; - img32: TImage32; compressionQuality: integer = 0); + img32: TImage32; compressionQuality: integer); var png: TPortableNetworkGraphic; begin @@ -95,6 +95,9 @@ procedure TImageFormat_PNG.SaveToStream(stream: TStream; img32.BeginUpdate; png := TPortableNetworkGraphic.Create; try + if compressionQuality = defaultCompression then + png.CompressionLevel := 7 else + png.CompressionLevel := Max(0, Min(9, compressionQuality)); png.SetSize(img32.Width, img32.Height); png.PixelFormat := pf32bit; Move(img32.PixelBase^, png.ScanLine[0]^, img32.Width * img32.Height *4); @@ -107,16 +110,97 @@ procedure TImageFormat_PNG.SaveToStream(stream: TStream; //------------------------------------------------------------------------------ {$ELSE} +procedure CopyLineWithAlpha(dst: PARGB; srcAlpha, srcColor: PByte; Width: Integer); +type + PARGBStaticArray = ^TARGBStaticArray; + TARGBStaticArray = array[0..3] of TARGB; +var + j: Integer; +begin + j := Width; + + // Copy 4 Pixels at a time. + // Instead of ">= 4" we need ">= 5" here. Otherwise the code would + // read a byte from the last srcColor (3 bytes per pixel) that doesn't exist + while j >= 5 do + begin + // Read and mask the 4 bytes from srcColor because it has only 3 bytes per pixel + // and replace the alpha channel + PARGBStaticArray(dst)[0].Color := (PColor32(@srcColor[0])^ and $00FFFFFF) or (srcAlpha[0] shl 24); + PARGBStaticArray(dst)[1].Color := (PColor32(@srcColor[3])^ and $00FFFFFF) or (srcAlpha[1] shl 24); + PARGBStaticArray(dst)[2].Color := (PColor32(@srcColor[6])^ and $00FFFFFF) or (srcAlpha[2] shl 24); + PARGBStaticArray(dst)[3].Color := (PColor32(@srcColor[9])^ and $00FFFFFF) or (srcAlpha[3] shl 24); + + inc(srcColor, 3 * 4); + inc(srcAlpha, 4); + inc(dst, 4); + dec(j, 4); + end; + + // Copy the remaining pixels by accessing only the 3 bytes per pixel. + while j > 0 do + begin + dst.Color := {A:} (srcAlpha^ shl 24) or + {B:} (srcColor[0]) or + {G:} (srcColor[1] shl 8) or + {R:} (srcColor[2] shl 16); + inc(srcColor, 3); + inc(srcAlpha); + inc(dst); + dec(j); + end; +end; + +procedure CopyLineWithoutAlpha(dst: PARGB; srcColor: PByte; Width: Integer); +type + PARGBStaticArray = ^TARGBStaticArray; + TARGBStaticArray = array[0..3] of TARGB; +var + j: Integer; +begin + j := Width; + + // Copy 4 Pixels at a time + // Instead of ">= 4" we need ">= 5" here. Otherwise the code would + // read a byte from the last srcColor (3 bytes per pixel) that doesn't exist + while j >= 5 do + begin + // Replace the alpha channel with 255 + PARGBStaticArray(dst)[0].Color := PColor32(@srcColor[0])^ or $FF000000; + PARGBStaticArray(dst)[1].Color := PColor32(@srcColor[3])^ or $FF000000; + PARGBStaticArray(dst)[2].Color := PColor32(@srcColor[6])^ or $FF000000; + PARGBStaticArray(dst)[3].Color := PColor32(@srcColor[9])^ or $FF000000; + + inc(srcColor, 3 * 4); + inc(dst, 4); + dec(j, 4); + end; + + // Copy the remaining pixels by accessing only the 3 bytes per pixel. + while j > 0 do + begin + dst.Color := {A:} $FF000000 or + {B:} (srcColor[0]) or + {G:} (srcColor[1] shl 8) or + {R:} (srcColor[2] shl 16); + inc(srcColor, 3); + inc(dst); + dec(j); + end; +end; + function TImageFormat_PNG.LoadFromStream(stream: TStream; img32: TImage32; imgIndex: integer): Boolean; var i,j : integer; png : TPngImage; dst : PARGB; - srcAlpha : PByte; srcColor : PByte; - palentries : array[0..255] of TPaletteEntry; + palentries : array of TPaletteEntry; + palSize : integer; + palIs4Bits : Boolean; usingPal : Boolean; + palOdd : Boolean; transpColor : TColor32; begin img32.BeginUpdate; @@ -125,59 +209,72 @@ function TImageFormat_PNG.LoadFromStream(stream: TStream; png.LoadFromStream(stream); img32.SetSize(png.Width, png.Height); - //bytesPerRow := PByte(png.Scanline[1]) - PByte(png.Scanline[0]); - //usingPal := (Abs(bytesPerRow) = png.Width) and (png.Palette <> 0); usingPal := (png.Header.BitDepth <= 8) and (png.Palette <> 0); if usingPal then begin - GetPaletteEntries(png.Palette, 0, 256, palentries); - FixPalette(@palentries[0], 256); - end; - - for i := 0 to img32.Height -1 do - begin - dst := PARGB(img32.PixelRow[i]); - srcColor := png.Scanline[i]; + palSize := 256; + SetLength(palentries, palSize); + GetPaletteEntries(png.Palette, 0, 256, palentries[0]); + if (Cardinal(palentries[255]) = 0) and (Cardinal(palentries[254]) = 0) then + begin + palSize := 253; + while (palSize > 0) and (Cardinal(palentries[palSize -1]) = 0) do + dec(palSize); + end; + palIs4Bits := palSize <= 16; // each pal index uses only 4 bits + FixPalette(@palentries[0], palSize); - if usingPal then + transpColor := TColor32(png.transparentColor) or $FF000000; + for i := 0 to img32.Height -1 do begin - transpColor := TColor32(png.transparentColor) or $FF000000; + dst := PARGB(img32.PixelRow[i]); + srcColor := png.Scanline[i]; + palOdd := false; for j := 0 to img32.Width -1 do begin - dst.Color := TColor32(palentries[srcColor^]); - if dst.Color = transpColor then - dst.Color := clNone32; - inc(srcColor); + if not palIs4Bits then + begin + dst.Color := TColor32(palentries[srcColor^]); + inc(srcColor); + end + else if palOdd then + begin + dst.Color := TColor32(palentries[srcColor^ and $F]); + palOdd := false; + inc(srcColor); + end else + begin + dst.Color := TColor32(palentries[srcColor^ shr 4]); + palOdd := true; + end; + if dst.Color = transpColor then dst.Color := clNone32; inc(dst); end; - end - else if png.Transparent and - (png.Header.ColorType = COLOR_RGBALPHA) or + end; + end + + else if png.Transparent and + (png.Header.ColorType = COLOR_RGBALPHA) or (png.Header.ColorType = COLOR_GRAYSCALEALPHA) then + begin + for i := 0 to img32.Height -1 do begin - srcAlpha := PByte(png.AlphaScanline[i]); - for j := 0 to img32.Width -1 do - begin - dst.A := srcAlpha^; inc(srcAlpha); - dst.B := srcColor^; inc(srcColor); - dst.G := srcColor^; inc(srcColor); - dst.R := srcColor^; inc(srcColor); - inc(dst); - end - end else - begin - for j := 0 to img32.Width -1 do - begin - dst.A := 255; - dst.B := srcColor^; inc(srcColor); - dst.G := srcColor^; inc(srcColor); - dst.R := srcColor^; inc(srcColor); - inc(dst); - end; + dst := PARGB(img32.PixelRow[i]); + srcColor := png.Scanline[i]; + CopyLineWithAlpha(dst, PByte(png.AlphaScanline[i]), srcColor, img32.Width); end; + end else + begin + for i := 0 to img32.Height -1 do + begin + dst := PARGB(img32.PixelRow[i]); + srcColor := png.Scanline[i]; + CopyLineWithoutAlpha(dst, srcColor, img32.Width); + end; end; + finally png.Free; img32.EndUpdate; @@ -187,7 +284,7 @@ function TImageFormat_PNG.LoadFromStream(stream: TStream; //------------------------------------------------------------------------------ procedure TImageFormat_PNG.SaveToStream(stream: TStream; - img32: TImage32; compressionQuality: integer = 0); + img32: TImage32; compressionQuality: integer); var i,j: integer; png: TPngImage; @@ -196,6 +293,9 @@ procedure TImageFormat_PNG.SaveToStream(stream: TStream; begin png := TPngImage.CreateBlank(COLOR_RGBALPHA, 8, img32.Width, img32.Height); try + if compressionQuality = defaultCompression then + png.CompressionLevel := 7 else + png.CompressionLevel := Max(0, Min(9, compressionQuality)); png.CreateAlpha; for i := 0 to img32.Height -1 do begin @@ -327,7 +427,7 @@ class function TImageFormat_PNG.PasteFromClipboard(img32: TImage32): Boolean; initialization TImage32.RegisterImageFormatClass('PNG', TImageFormat_PNG, cpHigh); - CF_PNG := RegisterClipboardFormat('PNG'); + CF_PNG := RegisterClipboardFormat('PNG'); CF_IMAGEPNG := RegisterClipboardFormat('image/png'); {$IFEND} diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.QOI.pas b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.QOI.pas index 3f5363c..f88a68d 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.QOI.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.QOI.pas @@ -1,12 +1,12 @@ unit Img32.Fmt.QOI; (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 12 March 2023 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2023 * +* Version : 4.7 * +* Date : 6 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : QOI file format extension for TImage32 * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) (******************************************************************************* @@ -99,7 +99,7 @@ function SwapBytes(Value: Cardinal): Cardinal; function ReadByte(var p: PByte): Byte; {$IFDEF INLINE} inline; {$ENDIF} begin - Result := Byte(p^); //nb: Delphi 7 compatability + Result := Byte(p^); //nb: Delphi 7 compatibility inc(p); end; @@ -132,7 +132,7 @@ function TImageFormat_QOI.LoadFromStream(stream: TStream; inc(src, stream.Position); end else begin - SetLength(srcTmp, size); + NewByteArray(srcTmp, size, True); stream.Read(srcTmp[0], size); src := @srcTmp[0]; end; diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.SVG.pas b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.SVG.pas index 2166d71..02fabbe 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Fmt.SVG.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Fmt.SVG.pas @@ -2,12 +2,12 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 11 March 2024 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2024 * +* Version : 4.7 * +* Date : 6 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : SVG file format extension for TImage32 * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -377,19 +377,27 @@ function TImageFormat_SVG.LoadFromStream(stream: TStream; Result := LoadFromStream(stream); if not Result then Exit; - r := RootElement.GetViewbox; + r := RootElement.viewboxWH; img32.BeginUpdate; try - if img32.IsEmpty and not r.IsEmpty then - img32.SetSize(Round(r.Width), Round(r.Height)) + if img32.IsEmpty then + begin + with RootElement do + if Width.IsValid and Height.IsValid then + img32.SetSize( + Round(Width.GetValue(defaultSvgWidth, 0)), + Round(Height.GetValue(defaultSvgHeight, 0))) + else if not r.IsEmpty then + img32.SetSize(Round(r.Width), Round(r.Height)) + else + img32.SetSize(defaultSvgWidth, defaultSvgHeight); + end else if not r.IsEmpty then begin // scale the SVG to best fit the image dimensions sx := GetScaleForBestFit(r.Width, r.Height, img32.Width, img32.Height); img32.SetSize(Round(r.Width * sx), Round(r.Height * sx)); - end - else - img32.SetSize(defaultSvgWidth, defaultSvgHeight); + end; //draw the SVG image to fit inside the canvas DrawImage(img32, True); diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Layers.pas b/Ext/SVGIconImageList/Image32/source/Img32.Layers.pas index c91b417..ec6996d 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Layers.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Layers.pas @@ -2,12 +2,12 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 16 April 2024 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2024 * +* Version : 4.8 * +* Date : 11 Febuary 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : Layered images support * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -74,7 +74,6 @@ TLayer32 = class(TStorage) {$IFNDEF NO_STORAGE} fStreamingRec : TRectWH; {$ENDIF} - fDesignerLayer : Boolean; function GetMidPoint: TPointD; procedure SetVisible(value: Boolean); procedure SetHeight(value: double); @@ -88,11 +87,12 @@ TLayer32 = class(TStorage) function GetPrevLayerInGroup: TLayer32; function GetLayer32Parent: TLayer32; procedure SetLayer32Parent(parent: TLayer32); - procedure SetOuterMargin(value: double); procedure CreateInternal(parent: TStorage = nil; const name: string = ''); protected - UpdateInfo : TUpdateInfo; + fIsDesignLayer : Boolean; + fUpdateInfo : TUpdateInfo; procedure SetDesignerLayer(value: Boolean); + procedure SetOuterMargin(value: double); virtual; function GetUpdateNeeded: Boolean; procedure DoBeforeMerge; virtual; procedure PreMerge(hideDesigners: Boolean); virtual; @@ -111,6 +111,7 @@ TLayer32 = class(TStorage) procedure SetOpacity(value: Byte); virtual; procedure ImageChanged(Sender: TImage32); virtual; procedure UpdateLayeredImage(newLayeredImage: TLayeredImage32); + property UpdateInfo: TUpdateInfo read fUpdateInfo; property UpdateNeeded : Boolean read GetUpdateNeeded; public constructor Create(parent: TStorage = nil; const name: string = ''); overload; override; @@ -139,7 +140,6 @@ TLayer32 = class(TStorage) procedure SetSize(width, height: double); procedure Invalidate; virtual; - //procedure Invalidate(const rec: TRectD); overload; virtual; function AddChild(layerClass: TLayer32Class; const name: string = ''): TLayer32; reintroduce; virtual; @@ -153,7 +153,7 @@ TLayer32 = class(TStorage) //Portions of child layers residing outside this region will be clipped. property ClipPath: TPathsD read fClipPath write SetClipPath; procedure Offset(dx, dy: double); overload; virtual; - property IsDesignerLayer: Boolean read fDesignerLayer; + property IsDesignerLayer: Boolean read fIsDesignLayer write SetDesignerLayer; property InnerBounds: TRectD read GetInnerBounds; property InnerRect: TRectD read GetInnerRectD; property OuterBounds: TRectD read GetOuterBounds; @@ -203,16 +203,19 @@ THitTestLayer32 = class(TLayer32) //abstract class property HitTestEnabled: Boolean read GetEnabled write SetEnabled; end; - //TRotLayer32: rotation methods added + //TRotLayer32: rotating and scaling methods added //(abstract base layer for TVectorLayer32 and TRasterLayer32) TRotLayer32 = class(THitTestLayer32) private fAngle : double; + fScaleX : double; + fScaleY : double; fPivotPt : TPointD; fAutoPivot : Boolean; function GetPivotPt: TPointD; procedure SetAutoPivot(val: Boolean); procedure SetAngle(newAngle: double); + procedure Scale(sx, sy: double); virtual; protected procedure SetPivotPt(const pivot: TPointD); virtual; {$IFNDEF NO_STORAGE} @@ -222,7 +225,7 @@ TRotLayer32 = class(THitTestLayer32) public constructor Create(parent: TLayer32 = nil; const name: string = ''); override; function Rotate(angleDelta: double): Boolean; virtual; - procedure ResetAngle; + procedure Reset; procedure Offset(dx, dy: double); override; property Angle: double read fAngle write SetAngle; property PivotPt: TPointD read GetPivotPt write SetPivotPt; @@ -233,10 +236,15 @@ TRotLayer32 = class(THitTestLayer32) //or transforms Paths when bounds change TVectorLayer32 = class(TRotLayer32) private - fPaths : TPathsD; - fOnDraw : TNotifyEvent; + fPaths : TPathsD; + fIsDrawing : Boolean; + fOnDraw : TNotifyEvent; procedure RepositionAndDraw; + function GetRelativePaths: TPathsD; protected + // we need to accommodate drawing bezier splines on TVectorLayer32 where + // the drawn path goes well outside the stored control points (Paths). + //procedure SetOuterMargin(value: double); override; procedure SetPaths(const newPaths: TPathsD); virtual; procedure Draw; virtual; public @@ -244,20 +252,22 @@ TVectorLayer32 = class(TRotLayer32) procedure SetInnerBounds(const newBounds: TRectD); override; procedure Offset(dx,dy: double); override; function Rotate(angleDelta: double): Boolean; override; + procedure Scale(sx, sy: double); override; procedure UpdateHitTestMask(const vectorRegions: TPathsD); virtual; procedure UpdateHitTestMaskFromImage; + procedure AppendPoint(const pt: TPointD); + procedure AppendPath(const path: TPathD); property Paths: TPathsD read fPaths write SetPaths; + property PathsRelativeToLayer: TPathsD read GetRelativePaths; property OnDraw: TNotifyEvent read fOnDraw write fOnDraw; end; TRasterLayer32 = class(TRotLayer32) //display layer for raster images private fMasterImg : TImage32; - //fMatrix: allows combining any number of scaling & rotating ops. - fMatrix : TMatrixD; - fRotating : Boolean; - fPreScaleSize : TSize; fAutoHitTest : Boolean; + fAutoCrop : Boolean; + fCropMargins : TPoint; procedure DoAutoHitTest; protected procedure ImageChanged(Sender: TImage32); override; @@ -270,9 +280,11 @@ TRasterLayer32 = class(TRotLayer32) //display layer for raster images procedure UpdateHitTestMaskTransparent(alphaValue: Byte = 127); overload; virtual; procedure SetInnerBounds(const newBounds: TRectD); override; function Rotate(angleDelta: double): Boolean; override; + procedure Scale(sx, sy: double); override; property AutoSetHitTestMask: Boolean read fAutoHitTest write fAutoHitTest; - property MasterImage: TImage32 read fMasterImg; + property AutoCrop : Boolean read fAutoCrop write fAutoCrop; + property MasterImage : TImage32 read fMasterImg; end; TButtonDesignerLayer32 = class; @@ -443,7 +455,7 @@ function UpdateRotatingButtonGroup(rotateButton: TLayer32): double; var DefaultButtonSize: integer; - dashes: TArrayOfInteger; + dashes: TArrayOfDouble; const crDefault = 0; @@ -470,6 +482,7 @@ implementation rsUpdateRotateGroupError = 'UpdateRotateGroup - invalid group'; rsLayeredImage32Error = 'TLayeredImage32: ''root'' must be a TGroupLayer32'; rsLayer32Error = 'TLayer32 - children must also be TLayer32'; + rsVectorLayer32Error = 'TVectorLayer32 - updating Paths during draw events will cause recursion.'; //------------------------------------------------------------------------------ // TLayerNotifyImage32 @@ -580,11 +593,11 @@ procedure TLayer32.CreateInternal(parent: TStorage = nil; const name: string = ' constructor TLayer32.Create(parent: TStorage; const name: string); begin - fDesignerLayer := true; //must do this first + fIsDesignLayer := true; //must do this first if not Assigned(parent) then CreateInternal(nil, name) else if parent.InheritsFrom(TLayer32) then - //this constructor is commonly overrided in descendant layer classes + //this constructor is commonly overridden in descendant layer classes Create(TLayer32(parent), name) else begin @@ -613,9 +626,9 @@ destructor TLayer32.Destroy; begin Invalidate; rec := OuterBounds; - if not UpdateInfo.priorPosition.IsEmpty then + if not fUpdateInfo.priorPosition.IsEmpty then begin - rec := Parent.MakeAbsolute(UpdateInfo.priorPosition); + rec := Parent.MakeAbsolute(fUpdateInfo.priorPosition); with fLayeredImage do fInvalidRect := UnionRect(fInvalidRect, rec); end; @@ -629,7 +642,7 @@ destructor TLayer32.Destroy; procedure TLayer32.SetDesignerLayer(value: Boolean); begin - fDesignerLayer := value; + fIsDesignLayer := value; end; //------------------------------------------------------------------------------ @@ -654,7 +667,7 @@ procedure TLayer32.SetLayer32Parent(parent: TLayer32); function TLayer32.GetUpdateNeeded: Boolean; begin - Result := (UpdateInfo.updateMethod <> umNone); + Result := (fUpdateInfo.updateMethod <> umNone); end; //------------------------------------------------------------------------------ @@ -662,42 +675,19 @@ procedure TLayer32.Invalidate; var layer : TLayer32; begin - if (UpdateInfo.updateMethod = umSelf) then Exit; - UpdateInfo.updateMethod := umSelf; + if (fUpdateInfo.updateMethod = umSelf) then Exit; + fUpdateInfo.updateMethod := umSelf; layer := Parent; while Assigned(layer) do begin - if layer.UpdateInfo.updateMethod <> umNone then Break; - layer.UpdateInfo.updateMethod := umChild; + if layer.fUpdateInfo.updateMethod <> umNone then Break; + layer.fUpdateInfo.updateMethod := umChild; layer := layer.Parent; end; end; //------------------------------------------------------------------------------ -//procedure TLayer32.Invalidate(const rec: TRectD); -//var -// layer : TLayer32; -//begin -// if (UpdateInfo.updateMethod = umAll) or -// not Assigned(fLayeredImage) or (self = Root) then Exit; -// -// with UpdateInfo do -// begin -// updateMethod := umRegion; -// updateRegion := UnionRect(updateRegion, rec); -// end; -// -// layer := Parent; -// while Assigned(layer) do -// begin -// if layer.UpdateInfo.childUpdating then Break; -// layer.UpdateInfo.childUpdating := true; -// layer := layer.Parent; -// end; -//end; -//------------------------------------------------------------------------------ - function TLayer32.GetNextLayerInGroup: TLayer32; begin if not Assigned(Parent) or (Index = Parent.ChildCount -1) then @@ -1142,21 +1132,21 @@ procedure TLayer32.PreMerge(hideDesigners: Boolean); begin if not Visible or (hideDesigners and IsDesignerLayer) or - (UpdateInfo.updateMethod = umNone) then + (fUpdateInfo.updateMethod = umNone) then Continue; - if UpdateInfo.updateMethod = umSelf then + if fUpdateInfo.updateMethod = umSelf then begin - rec := Parent.MakeAbsolute(UpdateInfo.priorPosition); + rec := Parent.MakeAbsolute(fUpdateInfo.priorPosition); with fLayeredImage do fInvalidRect := UnionRect(fInvalidRect, rec); - UpdateInfo.priorPosition := OuterBounds; - rec := Parent.MakeAbsolute(UpdateInfo.priorPosition); + fUpdateInfo.priorPosition := OuterBounds; + rec := Parent.MakeAbsolute(fUpdateInfo.priorPosition); with fLayeredImage do fInvalidRect := UnionRect(fInvalidRect, rec); end; - // premerge children (recursion) + // premerge children DoBeforeMerge; PreMerge(hideDesigners); end; @@ -1189,6 +1179,11 @@ procedure TLayer32.Merge(hideDesigners: Boolean; updateRect: TRect); img := fMergeImage; end; + {$IF not defined(FPC) and (CompilerVersion <= 26.0)} + // Delphi 7-XE5 have a problem with "continue" and the + // code analysis, marking "childImg" as "not initialized" + childImg := nil; + {$IFEND} //merge redraw all children for i := 0 to ChildCount -1 do begin @@ -1199,7 +1194,7 @@ procedure TLayer32.Merge(hideDesigners: Boolean; updateRect: TRect); Continue; //recursive merge - if (UpdateInfo.updateMethod <> umNone) then + if (fUpdateInfo.updateMethod <> umNone) then Merge(hideDesigners, updateRect); if Assigned(fMergeImage) then @@ -1218,7 +1213,7 @@ procedure TLayer32.Merge(hideDesigners: Boolean; updateRect: TRect); //independently of the group layer's positioning if (self is TGroupLayer32) then TranslateRect(dstRect, Floor(-self.Left), Floor(-self.Top)); - Types.IntersectRect(dstRect, dstRect, self.Image.Bounds); + Types.IntersectRect(dstRect, dstRect, self.Image.Bounds); end; if IsEmptyRect(dstRect) then Continue; @@ -1254,7 +1249,7 @@ procedure TLayer32.Merge(hideDesigners: Boolean; updateRect: TRect); TranslateRect(rec2, Floor(childLayer.fOuterMargin -childLayer.Left -fOuterMargin), Floor(childLayer.fOuterMargin -childLayer.Top -fOuterMargin)); - childImg2.CopyBlend(fClipImage, rec, rec2, BlendMask); + childImg2.CopyBlend(fClipImage, rec, rec2, BlendMaskLine); end; end else childImg2 := childImg; @@ -1269,7 +1264,7 @@ procedure TLayer32.Merge(hideDesigners: Boolean; updateRect: TRect); end; end; - with UpdateInfo do + with fUpdateInfo do begin priorPosition := OuterBounds; updateMethod := umNone; @@ -1400,7 +1395,7 @@ procedure TGroupLayer32.Invalidate; procedure TGroupLayer32.PreMerge(hideDesigners: Boolean); begin inherited; - if (self <> Root) and (UpdateInfo.updateMethod <> umNone) then + if (self <> Root) and (fUpdateInfo.updateMethod <> umNone) then UpdateGroupBounds; end; //------------------------------------------------------------------------------ @@ -1490,7 +1485,7 @@ constructor TRotLayer32.Create(parent: TLayer32; const name: string); begin inherited; fAutoPivot := true; - fPivotPt := InvalidPointD; + Reset; end; //------------------------------------------------------------------------------ @@ -1498,12 +1493,18 @@ procedure TRotLayer32.SetAngle(newAngle: double); begin NormalizeAngle(newAngle); if newAngle = fAngle then Exit; - if PointsEqual(fPivotPt, InvalidPointD) then - fPivotPt := MidPoint; + if not IsValid(fPivotPt) then fPivotPt := MidPoint; Rotate(newAngle - fAngle); end; //------------------------------------------------------------------------------ +procedure TRotLayer32.Scale(sx, sy: double); +begin + if (sx > 0) then fScaleX := fScaleX * sx; + if (sy > 0) then fScaleY := fScaleY * sy; +end; +//------------------------------------------------------------------------------ + function TRotLayer32.Rotate(angleDelta: double): Boolean; begin Result := (angleDelta <> 0) and not HasChildren; @@ -1515,10 +1516,12 @@ function TRotLayer32.Rotate(angleDelta: double): Boolean; end; //------------------------------------------------------------------------------ -procedure TRotLayer32.ResetAngle; +procedure TRotLayer32.Reset; begin - fAngle := 0; fPivotPt := InvalidPointD; + fAngle := 0; + fScaleX := 1.0; + fScaleY := 1.0; end; //------------------------------------------------------------------------------ @@ -1600,12 +1603,26 @@ function TVectorLayer32.Rotate(angleDelta: double): Boolean; end; //------------------------------------------------------------------------------ +procedure TVectorLayer32.Scale(sx, sy: double); +begin + inherited; + SetInnerBounds(RectD(fLeft, fTop, fWidth * sx, fHeight * sy)); +end; +//------------------------------------------------------------------------------ + procedure TVectorLayer32.SetPaths(const newPaths: TPathsD); begin fPaths := CopyPaths(newPaths); fPivotPt := InvalidPointD; - if Assigned(fPaths) then RepositionAndDraw - else inherited SetInnerBounds(NullRectD); + if not Assigned(fPaths) then inherited SetInnerBounds(NullRectD) + else if fIsDrawing then Raise Exception.Create(rsVectorLayer32Error) + else RepositionAndDraw; +end; +//------------------------------------------------------------------------------ + +function TVectorLayer32.GetRelativePaths: TPathsD; +begin + Result := TranslatePath(fPaths, -Left + fOuterMargin, -Top + fOuterMargin); end; //------------------------------------------------------------------------------ @@ -1647,6 +1664,33 @@ procedure TVectorLayer32.Offset(dx,dy: double); end; //------------------------------------------------------------------------------ +procedure TVectorLayer32.AppendPoint(const pt: TPointD); +var + highPaths, lenPts: integer; +begin + highPaths := High(fPaths); + if highPaths < 0 then + begin + SetLength(fPaths, 1); + SetLength(fPaths[0], 1); + fPaths[0][0] := pt; + end else + begin + lenPts := Length(fPaths[highPaths]); + SetLength(fPaths[highPaths], lenPts +1); + fPaths[highPaths][lenPts] := pt; + end; + RepositionAndDraw; +end; +//------------------------------------------------------------------------------ + +procedure TVectorLayer32.AppendPath(const path: TPathD); +begin + Img32.Vector.AppendPath(fPaths, path); + RepositionAndDraw; +end; +//------------------------------------------------------------------------------ + procedure TVectorLayer32.RepositionAndDraw; var rec: TRectD; @@ -1669,7 +1713,10 @@ procedure TVectorLayer32.Draw; begin //to draw the layer, either override this event //in a descendant class or assign the OnDraw property - if Assigned(fOnDraw) then fOnDraw(self); + if not Assigned(fOnDraw) then Exit; + fIsDrawing := true; + fOnDraw(self); + fIsDrawing := false; end; //------------------------------------------------------------------------------ @@ -1698,6 +1745,7 @@ constructor TRasterLayer32.Create(parent: TLayer32; const name: string); fCursorId := crHandPoint; fAutoHitTest := true; fOuterMargin := 0; + fAutoCrop := true; end; //------------------------------------------------------------------------------ @@ -1752,28 +1800,28 @@ procedure TRasterLayer32.ImageChanged(Sender: TImage32); begin if (Sender = MasterImage) then begin + Reset; if MasterImage.IsEmpty then Exit; - MasterImage.BlockNotify; - MasterImage.CropTransparentPixels; - MasterImage.UnblockNotify; - Invalidate; - + if fAutoCrop then + begin + MasterImage.BlockNotify; + MasterImage.CropTransparentPixels; + MasterImage.UnblockNotify; + end; //reset whenever MasterImage changes - fAngle := 0; - fMatrix := IdentityMatrix; - fRotating := false; - fPreScaleSize := Size(MasterImage.Width, MasterImage.Height); - - if Image.IsEmpty and - (TLayerNotifyImage32(Image).UpdateCount = 0) then - Image.Assign(MasterImage); + if Image.IsEmpty and (TLayerNotifyImage32(Image).UpdateCount = 0) then + Image.Assign(MasterImage); + fCropMargins := NullPoint; + Invalidate; end else begin if MasterImage.IsEmpty and not Image.IsEmpty then begin Image.BlockNotify; try - Image.CropTransparentPixels; + if fAutoCrop then + fCropMargins := SymmetricCropTransparent(Image); + PositionAt(Left + fCropMargins.X, Top + fCropMargins.Y); MasterImage.Assign(Image); finally Image.UnblockNotify; @@ -1787,42 +1835,79 @@ procedure TRasterLayer32.ImageChanged(Sender: TImage32); procedure TRasterLayer32.SetInnerBounds(const newBounds: TRectD); var - newWidth, newHeight: double; - w,h: integer; + x,y, rx,ry: double; + mat: TMatrixD; + sinA, cosA, tanA: double; begin - - if fRotating and Assigned(Image) then - begin - //rotation has just ended - fRotating := false; - //update fMatrix with the new rotation angle - if (fAngle <> 0) then - MatrixRotate(fMatrix, Image.MidPoint, fAngle); - - //and since we're about to start scaling, we need - //to store the starting size, and reset the angle - fPreScaleSize := Size(Image.Width, Image.Height); - fAngle := 0; - end; - - newWidth := newBounds.Width; - newHeight := newBounds.Height; - - //make sure the image is large enough to scale safely - if not MasterImage.IsEmpty and (newWidth > 1) and (newHeight > 1) then + if not MasterImage.IsEmpty and + //the image must be large enough to scale safely + (newBounds.Width > 1) and (newBounds.Height > 1) then begin Image.BeginUpdate; try - Image.Assign(MasterImage); - //apply any prior transformations + // determine the amount of scaling in the **un-rotated** + // image that will fit 'newBounds' once the image is rotated + + // given: + // the pivot point is unimportant (final position already defined) + // rotated x = sin(angle)*y + cos(angle)*x + // rotated y = cos(angle)*y + sin(angle)*x + // let: + // X, Y : unrotated image width & height + // rX, rY : rotated image width & height + + // rX = sinA * Y + cosA * X + // X = rX / cosA - sinA/cosA * Y + // X = rX / cosA - tanA * Y + // rY = cosA*Y + sinA*X + // Y = rY/cosA - tanA * X + // X = rX / cosA - tanA * (rY / cosA - tanA * X) + // X = rX / cosA - tanA * rY/cosA + tanA*tanA * X + // X - tanA*tanA * X = rX / cosA - tanA * rY/cosA + // X * (1 - tanA*tanA) = rX / cosA - tanA * rY/cosA + // X = (rX / cosA - tanA * rY/cosA) / (1 - tanA*tanA) + // Y := (rY - sinA * x) /cosA; + + sinA := Abs(Sin(fAngle)); + cosA := Abs(Cos(fAngle)); + if sinA = 0.0 then // no rotation (or 180 deg. rotation) + begin + fScaleX := newBounds.Width / MasterImage.Width; + fScaleY := newBounds.Height / MasterImage.Height; + end + else if cosA = 0.0 then // rotated 90 or 270 degrees + begin + fScaleX := newBounds.Height / MasterImage.Width; + fScaleY := newBounds.Width / MasterImage.Height; + end else + begin + tanA := sinA/cosA; + // adjust for rotational cropping + rx := newBounds.Width + fCropMargins.X * 2; + ry := newBounds.Height + fCropMargins.Y * 2; + x := (rx /cosA - tanA * ry / cosA) / (1 - tanA*tanA); + y := (ry - sinA * x) /cosA; + + if (x <= 0) or (y <= 0) then + begin + Image.SetSize(Round(newBounds.Width), Round(newBounds.Height)); + PositionAt(newBounds.Left, newBounds.Top); + Exit; + end; + + fScaleX := x / MasterImage.Width; + fScaleY := y / MasterImage.Height; + end; + + Image.AssignSettings(MasterImage); Image.Resampler := rWeightedBilinear; - AffineTransformImage(Image, fMatrix, true); // assumes no skew - //cropping is very important with rotation - SymmetricCropTransparent(Image); - w := Ceil(newBounds.Right) - Floor(newBounds.Left); - h := Ceil(newBounds.Bottom) - Floor(newBounds.Top); - Image.Resize(w, h); //nb: stretch resizes - PositionAt(newBounds.TopLeft); + mat := IdentityMatrix; + MatrixScale(mat, fScaleX, fScaleY); + MatrixRotate(mat, fAngle); + AffineTransformImage(MasterImage, Image, mat, true); + if fAutoCrop then + fCropMargins := SymmetricCropTransparent(Image); + PositionAt(newBounds.Left, newBounds.Top); finally Image.EndUpdate; end; @@ -1834,8 +1919,8 @@ procedure TRasterLayer32.SetInnerBounds(const newBounds: TRectD); function TRasterLayer32.Rotate(angleDelta: double): Boolean; var - mat: TMatrixD; - pt, mp: TPointD; + mat : TMatrixD; + mp : TPointD; begin Result := (angleDelta <> 0) and not MasterImage.IsEmpty and @@ -1844,38 +1929,37 @@ function TRasterLayer32.Rotate(angleDelta: double): Boolean; if not Result then Exit; mp := MidPoint; - - //if not already rotating, then update fMatrix with prior scaling - if not fRotating then - begin - Assert((fPreScaleSize.cx > 0) and (fPreScaleSize.cy > 0), 'oops!'); - MatrixScale(fMatrix, - Image.Width/fPreScaleSize.cx, - Image.Height/fPreScaleSize.cy); - - fRotating := true; - if fAutoPivot then fPivotPt := mp; - end; - + mat := IdentityMatrix; + MatrixScale(mat, fScaleX, fScaleY); + MatrixRotate(mat, fAngle); RotatePoint(mp, PivotPt, angleDelta); Image.BlockNotify; try - Image.Assign(MasterImage); - mat := fMatrix; - pt := PointD(PivotPt.X - fLeft, PivotPt.Y - fTop); - MatrixRotate(mat, pt, Angle); + Image.AssignSettings(MasterImage); Image.Resampler := rWeightedBilinear; - AffineTransformImage(Image, mat, true); // assumes no skew + AffineTransformImage(MasterImage, Image, mat, true); finally Image.UnblockNotify; end; + // cropping the image significantly improves performance + if fAutoCrop then + fCropMargins := SymmetricCropTransparent(Image); + fWidth := Image.Width; fHeight := Image.Height; PositionCenteredAt(mp); DoAutoHitTest; end; +//------------------------------------------------------------------------------ + +procedure TRasterLayer32.Scale(sx, sy: double); +begin + inherited; + SetInnerBounds(RectD(fLeft, fTop, + MasterImage.Width * fSCaleX, MasterImage.Height * fSCaleY)); +end; //------------------------------------------------------------------------------ // TRotatingGroupLayer32 class @@ -1900,8 +1984,9 @@ procedure TRotatingGroupLayer32.Init(const rec: TRect; rec2: TRectD; begin //startingZeroOffset: default = 0 (ie 3 o'clock) - if not ClockwiseRotationIsAnglePositive then +{$IFDEF CLOCKWISE_ROTATION_WITH_NEGATIVE_ANGLES} startingZeroOffset := -startingZeroOffset; +{$ENDIF} fZeroOffset := startingZeroOffset; if buttonSize <= 0 then buttonSize := DefaultButtonSize; @@ -2072,7 +2157,7 @@ procedure TButtonDesignerLayer32.Draw; constructor TLayeredImage32.Create(parent: TStorage; const name: string); begin inherited; - fBackColor := clBtnFace32; + fBackColor := clNone32; fResampler := DefaultResampler; fLastUpdateType := utUndefined; @@ -2188,7 +2273,7 @@ procedure TLayeredImage32.Invalidate; function TLayeredImage32.GetRepaintNeeded: Boolean; begin - Result := Root.UpdateInfo.updateMethod <> umNone; + Result := Root.fUpdateInfo.updateMethod <> umNone; end; //------------------------------------------------------------------------------ @@ -2371,7 +2456,7 @@ function GetRectEdgeMidPoints(const rec: TRectD): TPathD; mp: TPointD; begin mp := MidPoint(rec); - SetLength(Result, 4); + NewPointDArray(Result, 4, True); Result[0] := PointD(mp.X, rec.Top); Result[1] := PointD(rec.Right, mp.Y); Result[2] := PointD(mp.X, rec.Bottom); @@ -2456,7 +2541,7 @@ function UpdateSizingButtonGroup(movedButton: TLayer32): TRect; group := TSizingGroupLayer32(movedButton.Parent); with group do begin - SetLength(path, ChildCount); + NewPointDArray(path, ChildCount, True); for i := 0 to ChildCount -1 do path[i] := Child[i].MidPoint; end; diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Panels.pas b/Ext/SVGIconImageList/Image32/source/Img32.Panels.pas index 8083609..fd614a8 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Panels.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Panels.pas @@ -2,12 +2,12 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 24 April 2024 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2024 * +* Version : 4.8 * +* Date : 2 Febuary 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : Component that displays images on a TPanel descendant * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -26,19 +26,19 @@ interface TShowScrollBtns = (ssbFocused, ssAlways, ssNever); TPanelBkgType = (pbtSolidColor, pbtChessBoard); - //TDrawImageEvent: template for TBaseImgPanel's OnDrawImage event property. - //nb: with scaling, srcRect & dstRect may have different widths +/- heights. + // TDrawImageEvent: template for TBaseImgPanel's OnDrawImage event property. + // nb: with scaling, srcRect & dstRect may have different widths +/- heights. TDrawImageEvent = procedure (Sender: TObject; dstCanvas: TCanvas; const srcRect, dstRect: TRect) of Object; TFileDropEvent = procedure (Sender: TObject; const filename: string) of Object; - //TPanelScrollbar: used internally by TBaseImgPanel and TImage32Panel + // TPanelScrollbar: used internally by TBaseImgPanel and TImage32Panel TPanelScrollbar = record - btnSize : integer; //in dst coords - btnDelta : double; //how much src moves for each px of the ScrollBar - srcOffset : integer; //offset in unscaled src coords - maxSrcOffset : double; //max offset in unscaled src coords + btnSize : integer; // in dst coords + btnDelta : double; // how much src moves for each px of the ScrollBar + srcOffset : integer; // offset in unscaled src coords + maxSrcOffset : double; // max offset in unscaled src coords MouseOver : Boolean; MouseDown : Boolean; MouseDownPos : integer; @@ -65,6 +65,7 @@ TBaseImgPanel = class(TPanel) fOnScrolling : TNotifyEvent; fOnZooming : TNotifyEvent; fOnMouseWheel : TMouseWheelEvent; + fCursor : TCursor; {$IFDEF GESTURES} fLastDistance: integer; fLastLocation: TPoint; @@ -73,6 +74,7 @@ TBaseImgPanel = class(TPanel) fBkgChBrdColor1 : TColor32; fBkgChBrdColor2 : TColor32; fBkgChBrdSize : Integer; + procedure SetCursor(cursor: TCursor); procedure UpdateOffsetDelta(resetOrigin: Boolean); function GetMinScrollBtnSize: integer; function GetDstOffset: TPoint; @@ -92,11 +94,12 @@ TBaseImgPanel = class(TPanel) procedure SetBkgChBrdColor1(value : TColor32); procedure SetBkgChBrdColor2(value : TColor32); procedure SetBkgChBrdSize(value : Integer); + function GetTabStop: Boolean; + procedure SetTabStop(tabstop: Boolean); {$IFDEF GESTURES} procedure Gesture(Sender: TObject; const EventInfo: TGestureEventInfo; var Handled: Boolean); {$ENDIF} - procedure WMSize(var Message: TWMSize); message WM_SIZE; procedure CMFontChanged(var Message: TMessage); message CM_FONTCHANGED; procedure WMEraseBkgnd(var message: TMessage); message WM_ERASEBKGND; procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE; @@ -113,6 +116,7 @@ TBaseImgPanel = class(TPanel) X, Y: Integer); override; procedure DrawToPanelCanvas(const srcRect, dstRect: TRect); virtual; procedure Paint; override; + procedure Resize; override; procedure WMKeyDown(var Message: TWMKey); message WM_KEYDOWN; procedure WMKeyUp(var Message: TWMKey); message WM_KEYUP; procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE; @@ -126,7 +130,7 @@ TBaseImgPanel = class(TPanel) function ClientToImage(const clientPt: TPoint): TPoint; function ImageToClient(const surfacePt: TPoint): TPoint; function RecenterImageAt(const imagePt: TPoint): Boolean; - //ScaleAtPoint: zooms in or out keeping 'pt' stationary relative to display + // ScaleAtPoint: zooms in or out keeping 'pt' stationary relative to display procedure ScaleAtPoint(scaleDelta: double; const pt: TPoint); property InnerClientRect: TRect read GetInnerClientRect; property InnerMargin: integer read GetInnerMargin; @@ -136,33 +140,35 @@ TBaseImgPanel = class(TPanel) property ScrollbarVert: TPanelScrollbar read fScrollbarVert write fScrollbarVert; published - //AutoCenter: centers the image when its size is less than the display size + // AutoCenter: centers the image when its size is less than the display size property AutoCenter: Boolean read fAutoCenter write SetAutoCenter; property Color: TColor read GetColor write SetColor; - //FocusedColor: colour of the border when the panel is focused + // FocusedColor: colour of the border when the panel is focused property FocusedColor: TColor read fFocusedColor write fFocusedColor; property UnFocusedColor: TColor read fUnfocusedColor write fUnfocusedColor; - //Scale: image scale (between ScaleMin and ScaleMax) if AllowZoom is enabled + // Scale: image scale (between ScaleMin and ScaleMax) if AllowZoom is enabled property Scale: double read fScale write SetScale; property ScaleMin: double read fScaleMin write SetScaleMin; property ScaleMax: double read fScaleMax write SetScaleMax; - //ShowScrollButtons: defaults to ssbFocused (ie only when Panel has focus) + // ShowScrollButtons: defaults to ssbFocused (ie only when Panel has focus) property ShowScrollButtons : TShowScrollBtns read fShowScrollBtns write SetShowScrollButtons; property AllowKeyScroll: Boolean read fAllowKeyScroll write fAllowKeyScroll; property AllowScrnScroll: Boolean read fAllowScrnScroll write fAllowScrnScroll; property AllowZoom: Boolean read fAllowZoom write SetAllowZoom; - //Hatched background option + // Hatched background option property BkgType : TPanelBkgType read fBkgType write SetBkgType; property BkgChBrdColor1 : TColor32 read fBkgChBrdColor1 write SetBkgChBrdColor1; property BkgChBrdColor2 : TColor32 read fBkgChBrdColor2 write SetBkgChBrdColor2; property BkgChBrdSize : Integer read fBkgChBrdSize write SetBkgChBrdSize; - //OnKeyDown: optional event for custom keyboard actions + // OnKeyDown: optional event for custom keyboard actions property OnKeyDown: TKeyEvent read fOnKeyDown write fOnKeyDown; property OnKeyUp: TKeyEvent read fOnKeyUp write fOnKeyUp; property OnMouseWheel: TMouseWheelEvent read FOnMouseWheel write FOnMouseWheel; property OnScrolling: TNotifyEvent read fOnScrolling write fOnScrolling; property OnZooming: TNotifyEvent read fOnZooming write fOnZooming; + property Cursor: TCursor read fCursor write SetCursor; + property TabStop: Boolean read GetTabStop write SetTabStop stored True; end; TImage32Panel = class(TBaseImgPanel) @@ -186,7 +192,7 @@ TImage32Panel = class(TBaseImgPanel) constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure ClearImage; - //CopyToImage: avoids a full redraw + // CopyToImage: avoids a full redraw procedure CopyToImage(srcImg: TImage32; const rec: TRect); function CopyToClipboard: Boolean; function PasteFromClipboard: Boolean; @@ -227,9 +233,9 @@ TNotifyImage32 = class(TImage32) //------------------------------------------------------------------------------ var - //The minimum width for scrolling buttons. If borders are too narrow - //to properly display scroll buttons then scroll buttons will be disabled. - MinBorderWidth: integer = 0; //see initialization + // The minimum width for scrolling buttons. If borders are too narrow + // to properly display scroll buttons then scroll buttons will be disabled. + MinBorderWidth: integer = 0; // see initialization const MinImageScale = 0.001; @@ -368,15 +374,15 @@ constructor TBaseImgPanel.Create(AOwner: TComponent); Height := 200; Width := 200; {$IFnDEF FPC} - {$IF COMPILERVERSION >= 17} //this is a guess + {$IF COMPILERVERSION >= 17} // this is a guess ShowCaption := false; {$IFEND} {$ENDIF} BevelWidth := 1; BorderWidth := 12; BevelInner := bvLowered; - DoubleBuffered := true; - TabStop := true; + //DoubleBuffered := true; + inherited TabStop := true; {$IFDEF GESTURES} OnGesture := Gesture; Touch.InteractiveGestures := [igPressAndTap, igZoom, igPan]; @@ -388,6 +394,7 @@ constructor TBaseImgPanel.Create(AOwner: TComponent); fAutoCenter := true; fFocusedColor := RgbColor(clActiveCaption); fUnfocusedColor := clBtnFace; + fCursor := inherited Cursor; fScale := 1.0; fScaleMin := 0.05; fScaleMax := 20; @@ -405,13 +412,6 @@ destructor TBaseImgPanel.Destroy; end; //------------------------------------------------------------------------------ -procedure TBaseImgPanel.WMSize(var Message: TWMSize); -begin - inherited; - UpdateOffsetDelta(true); -end; -//------------------------------------------------------------------------------ - function TBaseImgPanel.GetDstOffset: TPoint; begin if not fAutoCenter then @@ -427,11 +427,11 @@ function TBaseImgPanel.GetDstOffset: TPoint; function TBaseImgPanel.GetInnerMargin: integer; begin - //nb: BorderWidth is the space between outer and inner bevels + // nb: BorderWidth is the space between outer and inner bevels Result := DpiAware(BorderWidth); if BevelInner <> bvNone then inc(result, BevelWidth); if BevelOuter <> bvNone then inc(result, BevelWidth); - //BorderStyle changes the OUTSIDE of the panel so won't affect InnerMargin. + // BorderStyle changes the OUTSIDE of the panel so won't affect InnerMargin. end; //------------------------------------------------------------------------------ @@ -484,7 +484,7 @@ procedure TBaseImgPanel.ScaleToFit; h,w: integer; begin if IsEmpty then Exit; - //fScale := 1; + // fScale := 1; fScrollbarHorz.srcOffset := 0; fScrollbarVert.srcOffset := 0; rec := GetInnerClientRect; @@ -562,6 +562,18 @@ procedure TBaseImgPanel.SetColor(acolor: TColor); end; //------------------------------------------------------------------------------ +function TBaseImgPanel.GetTabStop: Boolean; +begin + Result := inherited TabStop; +end; +//------------------------------------------------------------------------------ + +procedure TBaseImgPanel.SetTabStop(tabstop: Boolean); +begin + inherited TabStop := tabstop; +end; +//------------------------------------------------------------------------------ + procedure TBaseImgPanel.SetAutoCenter(value: Boolean); begin if value = fAutoCenter then Exit; @@ -592,7 +604,7 @@ procedure TBaseImgPanel.UpdateOffsetDelta(resetOrigin: Boolean); innerClientW, innerClientH, btnMin: integer; scaledW, scaledH: double; begin - //we need to determine 2 things: + // we need to determine 2 things: // 1. scroll button size // 2. how much a 1px button move moves the scaled image if (fImageSize.cx = 0) or (fImageSize.cy = 0) then Exit; @@ -605,7 +617,7 @@ procedure TBaseImgPanel.UpdateOffsetDelta(resetOrigin: Boolean); with fScrollbarVert do begin if resetOrigin then srcOffset := 0; - if (scaledH < innerClientH + tolerance) then //no scroll button needed + if (scaledH < innerClientH + tolerance) then // no scroll button needed begin btnSize := 0; btnDelta := 0; maxSrcOffset := 0; end else @@ -618,7 +630,7 @@ procedure TBaseImgPanel.UpdateOffsetDelta(resetOrigin: Boolean); with fScrollbarHorz do begin if resetOrigin then srcOffset := 0; - if (scaledW < innerClientW + tolerance) then //no scroll button needed + if (scaledW < innerClientW + tolerance) then // no scroll button needed begin btnSize := 0; btnDelta := 0; maxSrcOffset := 0; end else @@ -705,32 +717,26 @@ function TBaseImgPanel.ImageToClient(const surfacePt: TPoint): TPoint; function TBaseImgPanel.RecenterImageAt(const imagePt: TPoint): Boolean; var - scaledW, scaledH: Double; marg, innerW, innerH: Integer; - pt1, pt2: TPoint; - q, maxOffset: double; + pt2: TPoint; + q: double; begin Result := (fScrollbarHorz.maxSrcOffset > 0) or (fScrollbarVert.maxSrcOffset = 0); if not Result then Exit; - scaledW := fImageSize.cx * fScale; - scaledH := fImageSize.cy * fScale; marg := GetInnerMargin; innerW := ClientWidth - marg*2; innerH := ClientHeight - marg*2; - pt1 := imagePt; pt2 := ClientToImage(Types.Point(marg + innerW div 2, marg + innerH div 2)); with fScrollbarHorz do begin - q := (pt1.X - pt2.X); - maxOffset := (scaledW - innerW) / fScale; - srcOffset := Round(Max(0,Min(maxOffset, q))); + q := (imagePt.X - pt2.X); + srcOffset := EnsureRange(Round(srcOffset + q), 0, Round(maxSrcOffset)); end; with fScrollbarVert do begin - q := (pt1.Y - pt2.Y); - maxOffset := (scaledH - innerH) / fScale; - srcOffset := Round(Max(0,Min(maxOffset, q))); + q := (imagePt.Y - pt2.Y); + srcOffset := EnsureRange(Round(srcOffset + q), 0, Round(maxSrcOffset)); end; Invalidate; end; @@ -774,7 +780,7 @@ procedure TBaseImgPanel.MouseMove(Shift: TShiftState; X, Y: Integer); inDrawRegion: Boolean; begin rec := GetInnerClientRect; - inDrawRegion := PtInRect(rec, Types.Point(X,Y)); + inDrawRegion := Windows.PtInRect(rec, Types.Point(X,Y)); if inDrawRegion and not (fScrollbarHorz.MouseDown or fScrollbarVert.MouseDown) then begin @@ -784,10 +790,11 @@ procedure TBaseImgPanel.MouseMove(Shift: TShiftState; X, Y: Integer); fScrollbarHorz.MouseOver := false; fScrollbarVert.MouseOver := false; end; - cursor := crDefault; + inherited cursor := fCursor; inherited; Exit; end; + if not fMouseDown or not (fAllowScrnScroll or fAllowKeyScroll) then begin @@ -799,19 +806,19 @@ procedure TBaseImgPanel.MouseMove(Shift: TShiftState; X, Y: Integer); begin if (Y < rec.Bottom) then begin - cursor := crSizeNS; + inherited cursor := crSizeNS; if not fScrollbarVert.MouseOver then Invalidate; fScrollbarVert.MouseOver := true; end else - cursor := crDefault; + inherited cursor := fCursor; end else if (Y >= rec.Bottom) and (fScrollbarHorz.btnSize > 0) then begin - Cursor := crSizeWE; + inherited cursor := crSizeWE; if not fScrollbarHorz.MouseOver then Invalidate; fScrollbarHorz.MouseOver := true; end else - cursor := crDefault; + inherited cursor := fCursor; end; Exit; end; @@ -820,7 +827,7 @@ procedure TBaseImgPanel.MouseMove(Shift: TShiftState; X, Y: Integer); if not (fAllowScrnScroll or fAllowKeyScroll) then Exit; if fScrollbarVert.MouseDown then begin - //dragging vertical scrollbar + // dragging vertical scrollbar with fScrollbarVert do begin inc(srcOffset, Round((Y - MouseDownPos) / btnDelta)); @@ -829,7 +836,7 @@ procedure TBaseImgPanel.MouseMove(Shift: TShiftState; X, Y: Integer); end else if fScrollbarHorz.MouseDown then begin - //dragging horizontal scrollbar + // dragging horizontal scrollbar with fScrollbarHorz do begin inc(srcOffset, Round((X - MouseDownPos) / btnDelta)); @@ -837,7 +844,7 @@ procedure TBaseImgPanel.MouseMove(Shift: TShiftState; X, Y: Integer); end; end else if fAllowScrnScroll then begin - //click and drag the drawing image + // click and drag the drawing image with fScrollbarVert do if btnDelta > 0 then begin dec(srcOffset, Round((Y - MouseDownPos) / fScale)); @@ -849,11 +856,10 @@ procedure TBaseImgPanel.MouseMove(Shift: TShiftState; X, Y: Integer); MouseDownPos := X; end; end else - begin - Exit; //ie exit here if NOT scrolling - end; + Exit; // ie exit here if NOT scrolling if assigned(fOnScrolling) then fOnScrolling(self); Invalidate; + inherited; end; //------------------------------------------------------------------------------ @@ -892,11 +898,11 @@ procedure TBaseImgPanel.CMFocusChanged(var Message: TMessage); procedure TBaseImgPanel.WMEraseBkgnd(var message: TMessage); begin - message.Result := 0; //ie don't bother erasing background + message.Result := 0; // ie don't bother erasing background end; //------------------------------------------------------------------------------ -type TControl = class(Controls.TControl); //access protected Color property +type TWinControl = class(Controls.TWinControl); // access protected Color property procedure TBaseImgPanel.DrawToPanelCanvas(const srcRect, dstRect: TRect); begin @@ -956,7 +962,7 @@ procedure TBaseImgPanel.Paint; backgroundPainted: Boolean; pt: TPoint; begin - //calculate un-scaled source rectangle that corresponds with dstRec + // calculate un-scaled source rectangle that corresponds with dstRec marg := GetInnerMargin; innerRec := GetInnerClientRect; dpiAwareBW := DpiAware(BorderWidth); @@ -964,7 +970,7 @@ procedure TBaseImgPanel.Paint; srcRec := dstRec; TranslateRect(srcRec, -marg, -marg); ScaleRect(srcRec, 1/fScale); - //if the scaled drawing is smaller than InnerClientRect then center it + // if the scaled drawing is smaller than InnerClientRect then center it pt := GetDstOffset; if pt.X > 0 then begin @@ -980,7 +986,7 @@ procedure TBaseImgPanel.Paint; srcRec.Top := 0; srcRec.Bottom := fImageSize.cy; end; - //calc offsets + // calc offsets with fScrollbarHorz do if (srcOffset < 0) or (btnSize = 0) then srcOffset := 0; with fScrollbarVert do @@ -990,7 +996,7 @@ procedure TBaseImgPanel.Paint; if fScrollbarHorz.srcOffset > fScrollbarHorz.maxSrcOffset then fScrollbarHorz.srcOffset := Round(fScrollbarHorz.maxSrcOffset); TranslateRect(srcRec, fScrollbarHorz.srcOffset, fScrollbarVert.srcOffset); - //paint innerRec background + // paint innerRec background backgroundPainted := ParentBackground and {$IFDEF STYLESERVICES} StyleServices.Enabled and (seClient in StyleElements) and @@ -1001,7 +1007,7 @@ procedure TBaseImgPanel.Paint; if (csDesigning in ComponentState) or not backgroundPainted then begin if ParentColor then - Canvas.Brush.Color := TControl(parent).Color else + Canvas.Brush.Color := TWinControl(parent).Color else Canvas.Brush.Color := self.Color; Canvas.FillRect(innerRec); end; @@ -1010,25 +1016,25 @@ procedure TBaseImgPanel.Paint; begin DrawChessBoard(innerRec); end; - //draw the image + // draw the image DrawToPanelCanvas(srcRec, dstRec); - //prevent recursive paints (in case Invalidate etc called in fOnDrawImage) + // prevent recursive paints (in case Invalidate etc called in fOnDrawImage) RedrawWindow(Handle, nil, 0, RDW_NOERASE or RDW_NOINTERNALPAINT or RDW_VALIDATE); - //Exit;////////////////// - //paint the outer bevel + // Exit;////////////////// + // paint the outer bevel tmpRec := ClientRect; case BevelOuter of bvLowered: DrawFrame(tmpRec, clBtnShadow, clBtnHighlight, BevelWidth); bvRaised: DrawFrame(tmpRec, clBtnHighlight, clBtnShadow, BevelWidth); end; - //paint the border + // paint the border InflateRect(tmpRec, integer(-BevelWidth), integer(-BevelWidth)); if Focused then DrawFrame(tmpRec, fFocusedColor, fFocusedColor, dpiAwareBW) else DrawFrame(tmpRec, fUnfocusedColor, fUnfocusedColor, dpiAwareBW); InflateRect(tmpRec, integer(-dpiAwareBW), integer(-dpiAwareBW)); - //paint the inner bevel + // paint the inner bevel case BevelInner of bvLowered: DrawFrame(tmpRec, clBtnShadow, clBtnHighlight, BevelWidth); bvRaised: DrawFrame(tmpRec, clBtnHighlight, clBtnShadow, BevelWidth); @@ -1039,7 +1045,7 @@ procedure TBaseImgPanel.Paint; (Focused and (fShowScrollBtns = ssbFocused))) then begin btnMin := GetMinScrollBtnSize; - //draw vertical scrollbar + // draw vertical scrollbar with fScrollbarVert do if (btnSize > 0) then begin @@ -1052,7 +1058,7 @@ procedure TBaseImgPanel.Paint; else Canvas.Brush.Color := MakeDarker(Color, 20); DrawScrollButton(tmpRec); end; - //draw horizontal scrollbar + // draw horizontal scrollbar with fScrollbarHorz do if (btnSize > 0) then begin @@ -1069,6 +1075,13 @@ procedure TBaseImgPanel.Paint; end; //------------------------------------------------------------------------------ +procedure TBaseImgPanel.Resize; +begin + UpdateOffsetDelta(true); + inherited; +end; +//------------------------------------------------------------------------------ + {$IFDEF GESTURES} procedure TBaseImgPanel.Gesture(Sender: TObject; const EventInfo: TGestureEventInfo; var Handled: Boolean); @@ -1151,6 +1164,14 @@ procedure TBaseImgPanel.WMMouseHWheel(var Message: TCMMouseWheel); end; //------------------------------------------------------------------------------ +procedure TBaseImgPanel.SetCursor(cursor: TCursor); +begin + if cursor = inherited Cursor then Exit; + inherited Cursor := cursor; + fCursor := cursor; +end; +//------------------------------------------------------------------------------ + function TBaseImgPanel.DoMouseHWheel(Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint): Boolean; begin @@ -1194,7 +1215,7 @@ procedure TBaseImgPanel.WMKeyDown(var Message: TWMKey); if ssCtrl in shiftState then begin if not fAllowZoom then Exit; - //zoom in and out with CTRL+UP and CTRL+DOWN respectively + // zoom in and out with CTRL+UP and CTRL+DOWN respectively midPoint := Types.Point(ClientWidth div 2, ClientHeight div 2); case Message.CharCode of VK_UP: ScaleAtPoint(1.1, midPoint); @@ -1204,9 +1225,9 @@ procedure TBaseImgPanel.WMKeyDown(var Message: TWMKey); end else begin if not fAllowKeyScroll then Exit; - //otherwise scroll the image with the arrow keys + // otherwise scroll the image with the arrow keys if ssShift in shiftState then - mul := 5 else //ie scrolls 5 times faster with Shift key down + mul := 5 else // ie scrolls 5 times faster with Shift key down mul := 1; case Message.CharCode of VK_LEFT: @@ -1271,6 +1292,7 @@ constructor TImage32Panel.Create(AOwner: TComponent); fImage.SetSize(200,200); fAllowCopy := true; fAllowPaste := true; + DoubleBuffered := true; end; //------------------------------------------------------------------------------ diff --git a/Ext/SVGIconImageList/Image32/source/Img32.Resamplers.pas b/Ext/SVGIconImageList/Image32/source/Img32.Resamplers.pas index d708832..606bed1 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.Resamplers.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.Resamplers.pas @@ -2,12 +2,12 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 2 May 2024 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2024 * +* Version : 4.8 * +* Date : 10 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * Purpose : For image transformations (scaling, rotating etc.) * -* License : http://www.boost.org/LICENSE_1_0.txt * +* License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -17,6 +17,10 @@ interface uses SysUtils, Classes, Math, Img32; +// Premultiplies the alpha channel into the color channels from pSrc and stores +// it into pDst. pSrc and pDst can be the same pointer. +procedure PremultiplyAlpha(pSrc, pDst: PARGB; count: nativeint); overload; + // BoxDownSampling: As the name implies, is only intended for image // down-sampling (ie shrinking images) where it performs a little better // than other resamplers which tend toward pixelation. Nevertheless, this @@ -26,6 +30,14 @@ interface procedure BoxDownSampling(Image: TImage32; scale: double); overload; procedure BoxDownSampling(Image: TImage32; scaleX, scaleY: double); overload; procedure BoxDownSampling(Image: TImage32; newWidth, newHeight: Integer); overload; +procedure BoxDownSampling(Image, TargetImage: TImage32; scale: double); overload; +procedure BoxDownSampling(Image, TargetImage: TImage32; scaleX, scaleY: double); overload; +procedure BoxDownSampling(Image, TargetImage: TImage32; newWidth, newHeight: Integer); overload; + +procedure NearestNeighborResize(Image: TImage32; newWidth, newHeight: Integer); overload; +procedure NearestNeighborResize(Image, TargetImage: TImage32; newWidth, newHeight: Integer); overload; +procedure ResamplerResize(Image: TImage32; newWidth, newHeight: Integer); overload; +procedure ResamplerResize(Image, TargetImage: TImage32; newWidth, newHeight: Integer); overload; // The following general purpose resamplers are registered below: // function NearestResampler(img: TImage32; x, y: double): TColor32; @@ -63,7 +75,7 @@ function BilinearResample(img: TImage32; x, y: double): TColor32; var iw, ih: integer; xx, yy, xR, yB: integer; - weight: Cardinal; + weight: integer; pixels: TArrayOfColor32; weightedColor: TWeightedColor; xf, yf: double; @@ -167,7 +179,7 @@ function WeightedBilinearResample(img: TImage32; x, y: double): TColor32; var iw, ih: integer; xx, yy, xR, yB: integer; - weight: Cardinal; + weight: integer; pixels: TArrayOfColor32; weightedColor: TWeightedColor; xf, yf: double; @@ -374,7 +386,7 @@ function CubicInterpolate(aclr: PColor32; // let m2 = slope at pixel_c (using slope of pixel_d - pixel_b) // then t(0) = aa(0^3) + bb(0^2) + cc(0) + dd = dd // then t(1) = aa(1^3) + bb(1^2) + cc(1) + dd = aa + bb + cc + dd - // differentiating parametic equation at t'(0) and t'(1) ... + // differentiating parametric equation at t'(0) and t'(1) ... // t'(0) = m0 = 3*aa(0^2) + 2*bb(0) + cc = cc // t'(1) = m1 = 3*aa(1^2) + 2*bb(1) + cc = 3*aa + 2*bb + cc // t(0) = dd ::EQ1 @@ -509,6 +521,109 @@ function BicubicResample(img: TImage32; x, y: double): TColor32; Result := CubicInterpolate(@c[0], yFrac, bceY); end; +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ + +{$RANGECHECKS OFF} // negative index usage for Delphi 7-2007 +procedure PremultiplyAlpha(pSrc, pDst: PARGB; count: nativeint); +var + a: byte; + tab: PByteArray; + c: TColor32; + s, d: PColor32Array; +begin + if count = 0 then exit; + + // Use negative index trick + inc(pSrc, count); + inc(pDst, count); + count := -count; + + // This function is optimized with the assumption that if a pixel has a certain + // alpha channel, then the probability that the following pixels have the same + // alpha channel, is very high. + + c := PColor32Array(pSrc)[count]; + a := c shr 24; + while True do + begin + case a of + 0: // Special handling for 0 => color becomes black + begin + // Win32: Load stack variable into CPU register + s := PColor32Array(pSrc); + d := PColor32Array(pDst); + while True do + begin + d[count] := 0; + inc(count); + if count = 0 then exit; + c := s[count]; + a := c shr 24; + if a <> 0 then break; + end; + end; + + 255: // Special handling for 255 => no color change + begin + // Win32: Load stack variable into CPU register + s := PColor32Array(pSrc); + d := PColor32Array(pDst); + if s = d then // if source=dest, we can skip writing to d + begin + while True do + begin + //d[count] := c; // skip the write + inc(count); + if count = 0 then exit; + c := s[count]; + a := c shr 24; + if a <> 255 then break; + end; + end + else + begin + while True do + begin + d[count] := c; + inc(count); + if count = 0 then exit; + c := s[count]; + a := c shr 24; + if a <> 255 then break; + end; + end; + end; + + else + // Premultiply the alpha channel + + // Win32: Load stack variable into CPU register + s := PColor32Array(pSrc); + // Win32: This line "breaks" Delphi's register allocator + //d := PColor32Array(pDst); + while True do + begin + tab := @MulTable[a]; + c := (c and $FF000000) or + (tab[Byte(c shr 16)] shl 16) or + (tab[Byte(c shr 8)] shl 8) or + (tab[Byte(c )] ); + //d[count] := c; + PColor32Array(pDst)[count] := c; + inc(count); + if count = 0 then exit; + c := s[count]; + a := c shr 24; + if (a = 0) or (a = 255) then break; + end; + end; + end; +end; +{$IFDEF RANGECHECKS_ENABLED} +{$RANGECHECKS ON} +{$ENDIF RANGECHECKS_ENABLED} + //------------------------------------------------------------------------------ // BoxDownSampling and related functions //------------------------------------------------------------------------------ @@ -579,33 +694,51 @@ function GetWeightedColor(const srcBits: TArrayOfColor32; procedure BoxDownSampling(Image: TImage32; scaleX, scaleY: double); begin - BoxDownSampling(Image, + BoxDownSampling(Image, Image, scaleX, scaleY); +end; +//------------------------------------------------------------------------------ + +procedure BoxDownSampling(Image: TImage32; scale: double); +begin + BoxDownSampling(Image, Image, scale); +end; +//------------------------------------------------------------------------------ + +procedure BoxDownSampling(Image: TImage32; newWidth, newHeight: Integer); +begin + BoxDownSampling(Image, Image, newWidth, newHeight); +end; +//------------------------------------------------------------------------------ + +procedure BoxDownSampling(Image, TargetImage: TImage32; scaleX, scaleY: double); +begin + BoxDownSampling(Image, TargetImage, Max(1, Integer(Round(Image.Width * scaleX))), Max(1, Integer(Round(Image.Height * scaleY)))); end; //------------------------------------------------------------------------------ -procedure BoxDownSampling(Image: TImage32; scale: double); +procedure BoxDownSampling(Image, TargetImage: TImage32; scale: double); begin - BoxDownSampling(Image, + BoxDownSampling(Image, TargetImage, Max(1, Integer(Round(Image.Width * scale))), Max(1, Integer(Round(Image.Height * scale)))); end; //------------------------------------------------------------------------------ -procedure BoxDownSampling(Image: TImage32; newWidth, newHeight: Integer); +procedure BoxDownSampling(Image, TargetImage: TImage32; newWidth, newHeight: Integer); var x,y, x256,y256,xx256,yy256: Integer; sx,sy: double; tmp: TArrayOfColor32; pc: PColor32; - scaledX: array of Integer; + scaledX: TArrayOfInteger; begin sx := Image.Width/newWidth * 256; sy := Image.Height/newHeight * 256; - SetLength(tmp, newWidth * newHeight); + NewColor32Array(tmp, newWidth * newHeight, True); - SetLength(scaledX, newWidth +1); //+1 for fractional overrun + NewIntegerArray(scaledX, newWidth, True); for x := 0 to newWidth -1 do scaledX[x] := Round((x+1) * sx); @@ -626,11 +759,75 @@ procedure BoxDownSampling(Image: TImage32; newWidth, newHeight: Integer); y256 := yy256; end; - Image.BeginUpdate; - Image.SetSize(newWidth, newHeight); - Move(tmp[0], Image.Pixels[0], newWidth * newHeight * SizeOf(TColor32)); - Image.EndUpdate; + TargetImage.AssignPixelArray(tmp, newWidth, newHeight); +end; +//------------------------------------------------------------------------------ + +procedure NearestNeighborResize(Image: TImage32; newWidth, newHeight: Integer); +begin + NearestNeighborResize(Image, Image, newWidth, newHeight); +end; +//------------------------------------------------------------------------------ + +procedure NearestNeighborResize(Image, TargetImage: TImage32; newWidth, newHeight: Integer); +var + x, y, offset: Integer; + scaledXi, scaledYiOffset: TArrayOfInteger; + tmp: TArrayOfColor32; + pc: PColor32; + pixels: TArrayOfColor32; +begin + //this NearestNeighbor code is slightly more efficient than + //the more general purpose one in Img32.Resamplers + + if (newWidth = Image.Width) and (newHeight = Image.Height) then + begin + if TargetImage <> Image then TargetImage.Assign(Image); + Exit; + end; + NewColor32Array(tmp, newWidth * newHeight, True); + + //get scaled X & Y values once only (storing them in lookup arrays) ... + NewIntegerArray(scaledXi, newWidth, True); + for x := 0 to newWidth -1 do + scaledXi[x] := (x * Image.Width) div newWidth; + NewIntegerArray(scaledYiOffset, newHeight, True); + SetLength(scaledYiOffset, newHeight); + for y := 0 to newHeight -1 do + //scaledYiOffset[y] := Round(y * Image.Height / newHeight) * Image.Width; + scaledYiOffset[y] := ((y * Image.Height) div newHeight) * Image.Width; + + pc := @tmp[0]; + pixels := Image.Pixels; + for y := 0 to newHeight - 1 do + begin + offset := scaledYiOffset[y]; + for x := 0 to newWidth - 1 do + begin + pc^ := pixels[scaledXi[x] + offset]; + inc(pc); + end; + end; + + TargetImage.AssignPixelArray(tmp, newWidth, newHeight); +end; +//------------------------------------------------------------------------------ + +procedure ResamplerResize(Image: TImage32; newWidth, newHeight: Integer); +begin + ResamplerResize(Image, Image, newWidth, newHeight); end; +//------------------------------------------------------------------------------ + +procedure ResamplerResize(Image, TargetImage: TImage32; newWidth, newHeight: Integer); +var + mat: TMatrixD; +begin + mat := IdentityMatrix; + MatrixScale(mat, newWidth/Image.Width, newHeight/Image.Height); + AffineTransformImage(Image, TargetImage, mat); +end; + //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ diff --git a/Ext/SVGIconImageList/Image32/source/Img32.SVG.Core.pas b/Ext/SVGIconImageList/Image32/source/Img32.SVG.Core.pas index afe6b31..520c7ac 100644 --- a/Ext/SVGIconImageList/Image32/source/Img32.SVG.Core.pas +++ b/Ext/SVGIconImageList/Image32/source/Img32.SVG.Core.pas @@ -2,16 +2,16 @@ (******************************************************************************* * Author : Angus Johnson * -* Version : 4.4 * -* Date : 13 March 2024 * -* Website : http://www.angusj.com * -* Copyright : Angus Johnson 2019-2024 * +* Version : 4.7 * +* Date : 12 January 2025 * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2019-2025 * * * * Purpose : Essential structures and functions to read SVG files * * * * License : Use, modification & distribution is subject to * * Boost Software License Ver 1 * -* http://www.boost.org/LICENSE_1_0.txt * +* https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************) interface @@ -19,7 +19,7 @@ interface {$I Img32.inc} uses - SysUtils, Classes, Types, Math, + SysUtils, Classes, Types, Math, StrUtils, {$IFDEF XPLAT_GENERICS} Generics.Collections, Generics.Defaults,{$ENDIF} Img32, Img32.Vector, Img32.Text, Img32.Transform; @@ -76,6 +76,7 @@ interface procedure Init; function GetRectD(const relSize: TRectD; assumeRelValBelow: Double): TRectD; overload; function GetRectD(relSize: double; assumeRelValBelow: Double): TRectD; overload; + function GetRectD(relSizeX, relSizeY: double; assumeRelValBelow: Double): TRectD; overload; function GetRectWH(const relSize: TRectD; assumeRelValBelow: Double): TRectWH; function IsValid: Boolean; function IsEmpty: Boolean; @@ -94,36 +95,49 @@ interface TSvgItalicSyle = (sfsUndefined, sfsNone, sfsItalic); TFontDecoration = (fdUndefined, fdNone, fdUnderline, fdStrikeThrough); TSvgTextAlign = (staUndefined, staLeft, staCenter, staRight, staJustify); + TSpacesInText = (sitUndefined, sitIgnore, sitPreserve); + + UTF8Strings = array of UTF8String; TSVGFontInfo = record - family : TTtfFontFamily; - size : double; - spacing : double; - textLength : double; - italic : TSvgItalicSyle; - weight : Integer; - align : TSvgTextAlign; - decoration : TFontDecoration; - baseShift : TValue; + family : TFontFamily; + familyNames : UTF8Strings; + size : double; + spacing : double; + spacesInText : TSpacesInText; + textLength : double; + italic : TSvgItalicSyle; + weight : Integer; + align : TSvgTextAlign; + decoration : TFontDecoration; + baseShift : TValue; end; ////////////////////////////////////////////////////////////////////// - // TClassStylesList: custom TStringList that stores ansistring objects + // TClassStylesList: Map that stores CSS selectors with their styles ////////////////////////////////////////////////////////////////////// - PAnsStringiRec = ^TAnsiStringRec; //used internally by TClassStylesList - TAnsiStringRec = record - ansi : UTF8String; + PClassStyleListItem = ^TClassStyleListItem; + TClassStyleListItem = record //used internally by TClassStylesList + Hash : Cardinal; + Next : Integer; + Name : UTF8String; + Style : UTF8String; end; TClassStylesList = class private - fList : TStringList; + FNameHash: Cardinal; + FItems: array of TClassStyleListItem; + FBuckets: TArrayOfInteger; + FCount: Integer; + FMod: Cardinal; + procedure Grow(NewCapacity: Integer = -1); + function FindItemIndex(const Name: UTF8String): Integer; public - constructor Create; - destructor Destroy; override; - function AddAppendStyle(const classname: string; const ansi: UTF8String): integer; - function GetStyle(const classname: UTF8String): UTF8String; + procedure Preallocate(AdditionalItemCount: Integer); + procedure AddAppendStyle(const Name, Style: UTF8String); + function GetStyle(const Name: UTF8String): UTF8String; procedure Clear; end; @@ -165,8 +179,9 @@ TXmlEl = class //base element class procedure Clear; virtual; function ParseHeader(var c: PUTF8Char; endC: PUTF8Char): Boolean; virtual; function ParseContent(var c: PUTF8Char; endC: PUTF8Char): Boolean; virtual; - function ParseAttribName(var c: PUTF8Char; endC: PUTF8Char; attrib: PSvgAttrib): Boolean; - function ParseAttribValue(var c: PUTF8Char; endC: PUTF8Char; attrib: PSvgAttrib): Boolean; + class function ParseAttribName(c, endC: PUTF8Char; attrib: PSvgAttrib): PUTF8Char; {$IFDEF CLASS_STATIC}static;{$ENDIF} + class function ParseAttribValue(c, endC: PUTF8Char; attrib: PSvgAttrib): PUTF8Char; {$IFDEF CLASS_STATIC}static;{$ENDIF} + class function ParseAttribNameAndValue(c, endC: PUTF8Char; attrib: PSvgAttrib): PUTF8Char; {$IFDEF CLASS_STATIC}static;{$ENDIF} function ParseAttributes(var c: PUTF8Char; endC: PUTF8Char): Boolean; virtual; procedure ParseStyleAttribute(const style: UTF8String); property Attrib[index: integer]: PSvgAttrib read GetAttrib; @@ -175,13 +190,13 @@ TXmlEl = class //base element class TDocTypeEl = class(TXmlEl) private - procedure SkipWord(var c, endC: PUTF8Char); + function SkipWord(c, endC: PUTF8Char): PUTF8Char; function ParseEntities(var c, endC: PUTF8Char): Boolean; public function ParseAttributes(var c: PUTF8Char; endC: PUTF8Char): Boolean; override; end; - TSvgTreeEl = class(TXmlEl) + TSvgXmlEl = class(TXmlEl) public constructor Create(owner: TSvgParser); override; procedure Clear; override; @@ -193,10 +208,10 @@ TSvgParser = class svgStream : TMemoryStream; procedure ParseUtf8Stream; public - classStyles :TClassStylesList; + classStyles : TClassStylesList; xmlHeader : TXmlEl; docType : TDocTypeEl; - svgTree : TSvgTreeEl; + svgTree : TSvgXmlEl; constructor Create; destructor Destroy; override; procedure Clear; @@ -213,22 +228,37 @@ TSvgParser = class //general parsing functions ////////////////////////////////////////// function ParseNextWord(var c: PUTF8Char; endC: PUTF8Char; out word: UTF8String): Boolean; - function ParseNextWordEx(var c: PUTF8Char; endC: PUTF8Char; - out word: UTF8String): Boolean; + function ParseNextWordHash(var c: PUTF8Char; endC: PUTF8Char; + out hash: cardinal): Boolean; overload; + function ParseNextWordHash(c, endC: PUTF8Char): cardinal; overload; + function ParseNextWordExHash(var c: PUTF8Char; endC: PUTF8Char; + out hash: cardinal): Boolean; function ParseNextNum(var c: PUTF8Char; endC: PUTF8Char; skipComma: Boolean; out val: double): Boolean; function ParseNextNumEx(var c: PUTF8Char; endC: PUTF8Char; skipComma: Boolean; out val: double; out unitType: TUnitType): Boolean; - function GetHash(const name: UTF8String): cardinal; + function GetHash(c: PUTF8Char; len: nativeint): cardinal; overload; + function GetHash(const name: UTF8String): cardinal; overload; {$IFDEF INLINE} inline; {$ENDIF} function GetHashCaseSensitive(name: PUTF8Char; nameLen: integer): cardinal; function ExtractRef(const href: UTF8String): UTF8String; function IsNumPending(var c: PUTF8Char; endC: PUTF8Char; ignoreComma: Boolean): Boolean; function UTF8StringToColor32(const value: UTF8String; var color: TColor32): Boolean; - function MakeDashArray(const dblArray: TArrayOfDouble; scale: double): TArrayOfInteger; + function ScaleDashArray(const dblArray: TArrayOfDouble; scale: double): TArrayOfDouble; function Match(c: PUTF8Char; const compare: UTF8String): Boolean; overload; function Match(const compare1, compare2: UTF8String): Boolean; overload; - function ToUTF8String(var c: PUTF8Char; endC: PUTF8Char): UTF8String; + function PosEx(const subStr: utf8String; const text: Utf8String; startIdx: integer = 1): integer; + procedure ToUTF8String(c, endC: PUTF8Char; var S: UTF8String; + spacesInText: TSpacesInText = sitUndefined); + function TrimMultiSpacesUtf8(const text: Utf8String): Utf8String; + function TrimMultiSpacesUnicode(const text: UnicodeString): UnicodeString; + function ConvertNewlines(const s: UTF8String): UTF8String; overload; + function ConvertNewlines(const s: UnicodeString): UnicodeString; overload; + function StripNewlines(const s: UTF8String): UTF8String; overload; + function StripNewlines(const s: UnicodeString): UnicodeString; overload; + procedure ToAsciiLowerUTF8String(c, endC: PUTF8Char; var S: UTF8String); + procedure ToTrimmedUTF8String(c, endC: PUTF8Char; var S: UTF8String); + function IsSameUTF8String(const S1, S2: UTF8String): Boolean; //special parsing functions ////////////////////////////////////////// procedure ParseStyleElementContent(const value: UTF8String; stylesList: TClassStylesList); @@ -241,7 +271,11 @@ TSvgParser = class function ClampRange(val, min, max: double): double; function SkipBlanks(var c: PUTF8Char; endC: PUTF8Char): Boolean; - function SkipBlanksAndComma(var current: PUTF8Char; currentEnd: PUTF8Char): Boolean; + function SkipBlanksEx(c: PUTF8Char; endC: PUTF8Char): PUTF8Char; {$IFDEF INLINE} inline; {$ENDIF} + function SkipBlanksAndComma(c, endC: PUTF8Char): PUTF8Char; {$IFDEF INLINE} inline; {$ENDIF} + + function GetCommaSeparatedArray(const str: UTF8String): UTF8Strings; + function TrimQuotes(const str: UTF8String): UTF8String; procedure ConvertUnicodeToUtf8(memStream: TMemoryStream); @@ -252,9 +286,9 @@ TSvgParser = class type TSetOfUTF8Char = set of UTF8Char; - UTF8Strings = array of UTF8String; -function CharInSet(chr: UTF8Char; chrs: TSetOfUTF8Char): Boolean; +function CharInSet(chr: UTF8Char; const chrs: TSetOfUTF8Char): Boolean; +function DecodeUtf8ToUnicode(const utf8: UTF8String): UnicodeString; const clInvalid = $00010001; @@ -263,27 +297,52 @@ function CharInSet(chr: UTF8Char; chrs: TSetOfUTF8Char): Boolean; quote = ''''; dquote = '"'; space = #32; + comma = ','; SvgDecimalSeparator = '.'; //do not localize {$I Img32.SVG.HashConsts.inc} var - LowerCaseTable : array[#0..#255] of UTF8Char; - ColorConstList : TStringList; + LowerCaseTable : array[#0..#$FF] of UTF8Char; implementation +//------------------------------------------------------------------------------ +// Color Constant HashMap +//------------------------------------------------------------------------------ type + PColorConst = ^TColorConst; TColorConst = record - ColorName : string; - ColorValue: Cardinal; + ColorName : UTF8String; + ColorValue: TColor32; end; - TColorObj = class - cc: TColorConst; + PPColorConstMapItem = ^PColorConstMapItem; + PColorConstMapItem = ^TColorConstMapItem; + TColorConstMapItem = record + Hash: Cardinal; + Next: PColorConstMapItem; + Data: PColorConst; end; + PColorConstMapItemArray = ^TColorConstMapItemArray; + TColorConstMapItemArray = array[0..MaxInt div SizeOf(TColorConstMapItem) - 1] of TColorConstMapItem; + + TColorConstList = class(TObject) + private + FItems: array of TColorConstMapItem; + FBuckets: array of PColorConstMapItem; + FCount: Integer; + FMod: Cardinal; + public + constructor Create(Colors: PColorConst; Count: Integer); + function GetColorValue(const ColorName: UTF8String; var Color: TColor32): Boolean; + end; + +var + ColorConstList : TColorConstList; + const buffSize = 8; @@ -371,6 +430,24 @@ function Base64Decode(const str: PAnsiChar; len: integer; memStream: TMemoryStre // Miscellaneous functions ... //------------------------------------------------------------------------------ +function NewSvgAttrib(): PSvgAttrib; {$IFDEF INLINE} inline; {$ENDIF} +begin + // New(Result) uses RTTI to initialize the UTF8String fields to nil. + // By allocating zero'ed memory we can achieve that much faster. + Result := AllocMem(SizeOf(TSvgAttrib)); +end; +//------------------------------------------------------------------------------ + +procedure DisposeSvgAttrib(attrib: PSvgAttrib); {$IFDEF INLINE} inline; {$ENDIF} +begin + // Dispose(Result) uses RTTI to set the UTF8String fields to nil. + // By clearing them outself we can achieve that much faster. + attrib.name := ''; + attrib.value := ''; + FreeMem(attrib); +end; +//------------------------------------------------------------------------------ + function GetScale(src, dst: double): double; begin Result := dst / src; @@ -389,6 +466,7 @@ function GetScaleForBestFit(srcW, srcH, dstW, dstH: double): double; Result := 1 else Result := sx; end; +//------------------------------------------------------------------------------ function ClampRange(val, min, max: double): double; {$IFDEF INLINE} inline; {$ENDIF} @@ -399,7 +477,96 @@ function ClampRange(val, min, max: double): double; end; //------------------------------------------------------------------------------ -function CharInSet(chr: UTF8Char; chrs: TSetOfUTF8Char): Boolean; +function IsSameAsciiUTF8String(const S1, S2: UTF8String): Boolean; +var + Len: Integer; + I: Integer; + Ch1, Ch2: UTF8Char; +begin + Len := Length(S1); + Result := Len = Length(S2); + if Result then + begin + Result := False; + I := 1; + while True do + begin + if I > Len then + Break; + + Ch1 := S1[I]; + Ch2 := S2[I]; + if Ch1 = Ch2 then + begin + Inc(I); + Continue; + end; + + case Ch1 of + 'A'..'Z', 'a'..'z': + ch1 := UTF8Char(Ord(ch1) xor $20); // toggle upper/lower + end; + + if Ch1 <> Ch2 then + Exit; + Inc(I); + end; + Result := True; + end; +end; +//------------------------------------------------------------------------------ + +function IsSameUTF8StringSlow(const S1, S2: UTF8String): Boolean; +begin + Result := AnsiSameText(string(S1), string(S2)); +end; +//------------------------------------------------------------------------------ + +function IsSameUTF8String(const S1, S2: UTF8String): Boolean; +var + Len: Integer; + I: Integer; + Ch1, Ch2: UTF8Char; +begin + Len := Length(S1); + Result := Len = Length(S2); + if Result then + begin + Result := False; + I := 1; + Ch1 := #0; + Ch2 := #0; + while True do + begin + if I > Len then + Break; + + Ch1 := S1[I]; + Ch2 := S2[I]; + if Ch1 = Ch2 then + begin + Inc(I); + Continue; + end; + + case Ch1 of + 'A'..'Z', 'a'..'z': + ch1 := UTF8Char(Ord(ch1) xor $20); // toggle upper/lower + end; + + if Ch1 <> Ch2 then + Break; + Inc(I); + end; + if Ch1 = Ch2 then + Result := True + else if (Ord(Ch1) or Ord(Ch2)) and $80 <> 0 then // we found non-matching, non-ASCII characters + Result := IsSameUTF8StringSlow(S1, S2); + end; +end; +//------------------------------------------------------------------------------ + +function CharInSet(chr: UTF8Char; const chrs: TSetOfUTF8Char): Boolean; begin Result := chr in chrs; end; @@ -412,8 +579,7 @@ function Match(c: PUTF8Char; const compare: UTF8String): Boolean; Result := false; for i := 1 to Length(compare) do begin - if LowerCaseTable[c^] <> compare[i] then Exit; - inc(c); + if LowerCaseTable[c[i - 1]] <> compare[i] then Exit; end; Result := true; end; @@ -430,8 +596,7 @@ function Match(const compare1, compare2: UTF8String): Boolean; c1 := @compare1[1]; c2 := @compare2[1]; for i := 1 to len do begin - if LowerCaseTable[c1^] <> LowerCaseTable[c2^] then Exit; - inc(c1); inc(c2); + if LowerCaseTable[c1[i - 1]] <> LowerCaseTable[c2[i - 1]] then Exit; end; Result := true; end; @@ -444,23 +609,70 @@ function Split(const str: UTF8String): UTF8Strings; spcCnt := 0; i := 1; len := Length(str); - while (len > 0) and (str[len] <= #32) do dec(len); - while (i <= len) and (str[i] <= #32) do inc(i); + while (len > 0) and (str[len] <= space) do dec(len); + while (i <= len) and (str[i] <= space) do inc(i); for j := i + 1 to len do - if (str[j] <= #32) and (str[j -1] > #32) then inc(spcCnt); + if (str[j] <= space) and (str[j -1] > space) then inc(spcCnt); SetLength(Result, spcCnt +1); for k := 0 to spcCnt do begin j := i; - while (j <= len) and (str[j] > #32) do inc(j); + while (j <= len) and (str[j] > space) do inc(j); SetLength(Result[k], j -i); - Move(str[i], Result[k][1], j -i); - while (j <= len) and (str[j] <= #32) do inc(j); + if j > i then + Move(str[i], Result[k][1], j -i); + while (j <= len) and (str[j] <= space) do inc(j); i := j; end; end; //------------------------------------------------------------------------------ +function TrimQuotes(const str: UTF8String): UTF8String; +var + i, len: integer; + savedQuote: UTF8Char; +begin + len := Length(str); + i := 1; + while (i < len) and (str[i] <= space) do inc(i); + if (i < len) and (str[i] in [quote, dquote]) then + begin + savedQuote := str[i]; + inc(i); + while (len > i) and (str[len] <= space) do dec(len); + if (len = i) or (str[len] <> savedQuote) then + Result := str else // oops! + Result := Copy(str, i, len - i); + end + else + Result := str +end; +//------------------------------------------------------------------------------ + +function GetCommaSeparatedArray(const str: UTF8String): UTF8Strings; +var + i,j,k, cnt, len: integer; +begin + // precondition: commas CANNOT be embedded + len := Length(str); + cnt := 1; + for i := 1 to len do + if (str[i] = comma) then inc(cnt); + SetLength(Result, cnt); + j := 0; + k := 1; + for i := 1 to len do + begin + if (str[i] <> comma) then Continue; + Result[j] := TrimQuotes(Copy(str, k, i-k)); + inc(j); + k := i + 1; + end; + if len >= k then + Result[j] := TrimQuotes(Copy(str, k, len-k +1)); +end; +//------------------------------------------------------------------------------ + function GetXmlEncoding(memory: Pointer; len: integer): TSvgEncoding; var p, p1: PUTF8Char; @@ -483,141 +695,252 @@ function GetXmlEncoding(memory: Pointer; len: integer): TSvgEncoding; //------------------------------------------------------------------------------ function SkipBlanks(var c: PUTF8Char; endC: PUTF8Char): Boolean; +var + cc: PUTF8Char; +begin + cc := c; + if (cc < endC) and (cc^ <= space) then + begin + inc(cc); + while (cc < endC) and (cc^ <= space) do inc(cc); + c := cc; + end; + Result := (cc < endC); +end; +//------------------------------------------------------------------------------ + +function SkipBlanksEx(c: PUTF8Char; endC: PUTF8Char): PUTF8Char; begin while (c < endC) and (c^ <= space) do inc(c); - Result := (c < endC); + Result := c; end; //------------------------------------------------------------------------------ -function SkipBlanksAndComma(var current: PUTF8Char; currentEnd: PUTF8Char): Boolean; +function SkipBlanksAndComma(c, endC: PUTF8Char): PUTF8Char; begin - Result := SkipBlanks(current, currentEnd); - if not Result or (current^ <> ',') then Exit; - inc(current); - Result := SkipBlanks(current, currentEnd); + Result := SkipBlanksEx(c, endC); + if (Result >= endC) or (Result^ <> ',') then Exit; + Result := SkipBlanksEx(Result + 1, endC); end; //------------------------------------------------------------------------------ -function SkipStyleBlanks(var c: PUTF8Char; endC: PUTF8Char): Boolean; +function SkipStyleBlanks(c, endC: PUTF8Char): PUTF8Char; var inComment: Boolean; + ch: UTF8Char; begin //style content may include multi-line comment blocks inComment := false; while (c < endC) do begin + ch := c^; if inComment then begin - if (c^ = '*') and ((c +1)^ = '/') then + if (ch = '*') and ((c +1)^ = '/') then begin inComment := false; inc(c); end; end - else if (c^ > space) then + else if (ch > space) then begin - inComment := (c^ = '/') and ((c +1)^ = '*'); + inComment := (ch = '/') and ((c +1)^ = '*'); if not inComment then break; + inc(c); end; inc(c); end; - Result := (c < endC); + Result := c; +end; +//------------------------------------------------------------------------------ + +function IsDigit(c: UTF8Char): Boolean; {$IFDEF INLINE} inline; {$ENDIF} +begin + case c of + '0'..'9': Result := True; + else Result := False; + end; +end; +//------------------------------------------------------------------------------ + +function IsQuoteChar(c: UTF8Char): Boolean; {$IFDEF INLINE} inline; {$ENDIF} +begin + Result := (c = quote) or (c = dquote); end; //------------------------------------------------------------------------------ function IsAlpha(c: UTF8Char): Boolean; {$IFDEF INLINE} inline; {$ENDIF} begin - Result := CharInSet(c, ['A'..'Z','a'..'z']); + case c of + 'A'..'Z', 'a'..'z': Result := True; + else Result := False; + end; end; //------------------------------------------------------------------------------ -function ParseStyleNameLen(var c: PUTF8Char; endC: PUTF8Char): integer; +function ParseStyleNameLen(c, endC: PUTF8Char): PUTF8Char; var c2: PUTF8Char; -const - validNonFirstChars = ['0'..'9','A'..'Z','a'..'z','-']; begin - Result := 0; + Result := c; //nb: style names may start with a hyphen - if (c^ = '-') then - begin - if not IsAlpha((c+1)^) then Exit; - end - else if not IsAlpha(c^) then Exit; + c2 := Result; + if (c2^ = '-') then inc(c2); + if not IsAlpha(c2^) then Exit; - c2 := c; inc(c); - while (c < endC) and CharInSet(c^, validNonFirstChars) do inc(c); - Result := c - c2; + Result := c2 + 1; + while Result < endC do + begin + case Result^ of + '0'..'9', 'A'..'Z', 'a'..'z', '-': inc(Result); + else break; + end; + end; end; //------------------------------------------------------------------------------ function ParseNextWord(var c: PUTF8Char; endC: PUTF8Char; out word: UTF8String): Boolean; +var + c2, cc: PUTF8Char; +begin + cc := SkipBlanksAndComma(c, endC); + if cc >= endC then + begin + c := cc; + Result := False; + Exit; + end; + + c2 := cc; + while cc < endC do + begin + case cc^ of + 'A'..'Z', 'a'..'z': inc(cc); + else break; + end; + end; + c := cc; + ToUTF8String(c2, cc, word); + Result := True; +end; +//------------------------------------------------------------------------------ + +function ParseNextWordHash(var c: PUTF8Char; endC: PUTF8Char; out hash: cardinal): Boolean; +var + c2, cc: PUTF8Char; +begin + cc := SkipBlanksAndComma(c, endC); + if cc >= endC then + begin + c := cc; + hash := 0; + Result := False; + Exit; + end; + + c2 := cc; + while cc < endC do + begin + case cc^ of + 'A'..'Z', 'a'..'z': inc(cc); + else break; + end; + end; + c := cc; + hash := GetHash(c2, cc - c2); + Result := True; +end; +//------------------------------------------------------------------------------ + +function ParseNextWordHash(c, endC: PUTF8Char): cardinal; var c2: PUTF8Char; begin - Result := SkipBlanksAndComma(c, endC); - if not Result then Exit; + c := SkipBlanksAndComma(c, endC); + if c >= endC then + begin + Result := 0; + Exit; + end; + c2 := c; - while (c < endC) and - (LowerCaseTable[c^] >= 'a') and (LowerCaseTable[c^] <= 'z') do - inc(c); - word := ToUTF8String(c2, c); + while c < endC do + begin + case c^ of + 'A'..'Z', 'a'..'z': inc(c); + else break; + end; + end; + Result := GetHash(c2, c - c2); end; //------------------------------------------------------------------------------ -function ParseNextWordEx(var c: PUTF8Char; endC: PUTF8Char; - out word: UTF8String): Boolean; +function ParseNextWordExHash(var c: PUTF8Char; endC: PUTF8Char; + out hash: cardinal): Boolean; var - isQuoted: Boolean; - c2: PUTF8Char; + c2, cc: PUTF8Char; begin - Result := SkipBlanksAndComma(c, endC); - if not Result then Exit; - isQuoted := (c^) = quote; - if isQuoted then + cc := SkipBlanksAndComma(c, endC); + if cc >= endC then + begin + c := cc; + hash := 0; + Result := False; + Exit; + end; + + if cc^ = quote then begin inc(c); - c2 := c; - while (c < endC) and (c^ <> quote) do inc(c); - word := ToUTF8String(c2, c); - inc(c); + c2 := cc; + while (cc < endC) and (cc^ <> quote) do inc(cc); + hash := GetHash(c2, cc - c2); + inc(cc); end else begin - Result := CharInSet(LowerCaseTable[c^], ['A'..'Z', 'a'..'z']); - if not Result then Exit; - c2 := c; - inc(c); - while (c < endC) and - CharInSet(LowerCaseTable[c^], ['A'..'Z', 'a'..'z', '-', '_']) do inc(c); - word := ToUTF8String(c2, c); + if not IsAlpha(cc^) then + begin + hash := 0; + Result := False; + Exit; + end; + c2 := cc; + inc(cc); + while cc < endC do + case cc^ of + 'A'..'Z', 'a'..'z', '-', '_': inc(cc); + else break; + end; + hash := GetHash(c2, cc - c2); end; + c := cc; + Result := True; end; //------------------------------------------------------------------------------ -function ParseNameLength(var c: PUTF8Char; endC: PUTF8Char): integer; overload; -var - c2: PUTF8Char; -const - validNonFirstChars = ['0'..'9','A'..'Z','a'..'z','_',':','-']; +function ParseNameLength(c: PUTF8Char; endC: PUTF8Char): PUTF8Char; begin - c2 := c; inc(c); - while (c < endC) and CharInSet(c^, validNonFirstChars) do inc(c); - Result := c - c2; + while c < endC do + begin + case c^ of + '0'..'9', 'A'..'Z', 'a'..'z', '_', ':', '-': inc(c); + else break; + end; + end; + Result := c; end; //------------------------------------------------------------------------------ {$OVERFLOWCHECKS OFF} -function GetHash(const name: UTF8String): cardinal; +function GetHash(c: PUTF8Char; len: nativeint): cardinal; var i: integer; - c: PUTF8Char; begin //https://en.wikipedia.org/wiki/Jenkins_hash_function - c := PUTF8Char(name); Result := 0; if c = nil then Exit; - for i := 1 to Length(name) do + for i := 1 to len do begin Result := (Result + Ord(LowerCaseTable[c^])); Result := Result + (Result shl 10); @@ -628,7 +951,16 @@ function GetHash(const name: UTF8String): cardinal; Result := Result xor (Result shr 11); Result := Result + (Result shl 15); end; -{$OVERFLOWCHECKS ON} +{$IFDEF OVERFLOWCHECKS_ENABLED} + {$OVERFLOWCHECKS ON} +{$ENDIF} +//------------------------------------------------------------------------------ + +function GetHash(const name: UTF8String): cardinal; +begin + // skip function call by directly casting it to Pointer + Result := GetHash(PUTF8Char(Pointer(name)), Length(name)); +end; //------------------------------------------------------------------------------ {$OVERFLOWCHECKS OFF} @@ -648,231 +980,529 @@ function GetHashCaseSensitive(name: PUTF8Char; nameLen: integer): cardinal; Result := Result xor (Result shr 11); Result := Result + (Result shl 15); end; -{$OVERFLOWCHECKS ON} +{$IFDEF OVERFLOWCHECKS_ENABLED} + {$OVERFLOWCHECKS ON} +{$ENDIF} //------------------------------------------------------------------------------ function ParseNextWordHashed(var c: PUTF8Char; endC: PUTF8Char): cardinal; var c2: PUTF8Char; - name: UTF8String; + len: integer; begin c2 := c; - ParseNameLength(c, endC); - name := ToUTF8String(c2, c); - if name = '' then Result := 0 - else Result := GetHash(name); + c := ParseNameLength(c2, endC); + len := c - c2; + if len <= 0 then Result := 0 + else Result := GetHash(c2, len); +end; +//------------------------------------------------------------------------------ + +function ParseExpDigits(c, endC: PUTF8Char; out val: Integer): PUTF8Char; {$IFDEF INLINE} inline; {$ENDIF} +var + v32: Cardinal; + Digit: Integer; +begin + Result := c; + v32 := 0; + while Result < endC do + begin + Digit := Integer(Ord(Result^)) - Ord('0'); + if Cardinal(Digit) >= 10 then break; + {$IFDEF FPC} // Something Delphi can optimize but FPC can't (yet?) + v32 := (v32 shl 3) + (v32 shl 1) + Cardinal(Digit); // Delphi's code is even better than this + {$ELSE} + v32 := v32 * 10 + Cardinal(Digit); + {$ENDIF FPC} + inc(Result); + end; + val := v32; +end; +//------------------------------------------------------------------------------ + +function ParseDigitsToDouble(c, endC: PUTF8Char; out val: double): PUTF8Char; +var + v32: Cardinal; + v64: Int64; + Digit: Integer; + blockEndC: PUTF8Char; +begin + // skip leading zeros + while (c < endC) and (c^ = '0') do inc(c); + + // Use Int32 first as it is fast for 64bit and 32bit CPUs + Result := c; + v32 := 0; + + blockEndC := c + 9; // log10(2^31) = 9.33 + if blockEndC > endC then + blockEndC := endC; + while Result < blockEndC do + begin + Digit := Integer(Ord(Result^)) - Ord('0'); + if Cardinal(Digit) >= 10 then break; + {$IFDEF FPC} // Something Delphi can optimize but FPC can't (yet?) + v32 := (v32 shl 3) + (v32 shl 1) + Cardinal(Digit); + {$ELSE} + v32 := v32 * 10 + Cardinal(Digit); + {$ENDIF FPC} + inc(Result); + end; + + if (Result < endC) and (Result >= blockEndC) then + begin + v64 := v32; + + blockEndC := c + 18; // log10(2^63) = 18.96 + if blockEndC > endC then + blockEndC := endC; + while Result < blockEndC do + begin + Digit := Integer(Ord(Result^)) - Ord('0'); + if Cardinal(Digit) >= 10 then break; + {$IF (SizeOf(Pointer) = 4) or defined(FPC)} // neither Delphi 32bit nor FPC can optimize this + v64 := (v64 shl 3) + (v64 shl 1) + Cardinal(Digit); + {$ELSE} + v64 := v64 * 10 + Cardinal(Digit); + {$IFEND} + inc(Result); + end; + + val := v64; + // Use Double for the remaining digits and loose precision (we are beyond 16 digits anyway) + if (Result < endC) and (Result >= blockEndC) then + begin + while Result < endC do + begin + Digit := Integer(Ord(Result^)) - Ord('0'); + if Cardinal(Digit) >= 10 then break; + val := val * 10 + Digit; + inc(Result); + end; + end; + end + else + val := v32; end; //------------------------------------------------------------------------------ function ParseNextNumEx(var c: PUTF8Char; endC: PUTF8Char; skipComma: Boolean; out val: double; out unitType: TUnitType): Boolean; +const + Power10: array[0..18] of Double = ( + 1E0, 1E1, 1E2, 1E3, 1E4, 1E5, 1E6, 1E7, 1E8, 1E9, + 1E10, 1E11, 1E12, 1E13, 1E14, 1E15, 1E16, 1E17, 1E18 + ); + Power10Reciprocal: array[0..18] of Double = ( + 1/1E0, 1/1E1, 1/1E2, 1/1E3, 1/1E4, 1/1E5, 1/1E6, 1/1E7, 1/1E8, 1/1E9, + 1/1E10, 1/1E11, 1/1E12, 1/1E13, 1/1E14, 1/1E15, 1/1E16, 1/1E17, 1/1E18 + ); var - decPos,exp: integer; + exp: integer; isNeg, expIsNeg: Boolean; - start: PUTF8Char; + start, decStart, cc: PUTF8Char; + decimals: Double; begin Result := false; unitType := utNumber; + cc := c; + //skip white space +/- single comma if skipComma then begin - while (c < endC) and (c^ <= space) do inc(c); - if (c^ = ',') then inc(c); + while (cc < endC) and (cc^ <= space) do inc(cc); + if (cc^ = ',') then inc(cc); + end; + while (cc < endC) and (cc^ <= space) do inc(cc); + if (cc = endC) then + begin + c := cc; + Exit; end; - while (c < endC) and (c^ <= space) do inc(c); - if (c = endC) then Exit; - decPos := -1; exp := Invalid; expIsNeg := false; - isNeg := c^ = '-'; - if isNeg then inc(c); + exp := Invalid; expIsNeg := false; + isNeg := cc^ = '-'; + if isNeg then inc(cc); - val := 0; - start := c; - while c < endC do + start := cc; + + // Use fast parsing + cc := ParseDigitsToDouble(cc, endC, val); + if cc < endC then begin - if Ord(c^) = Ord(SvgDecimalSeparator) then + // Decimals + if Ord(cc^) = Ord(SvgDecimalSeparator) then begin - if decPos >= 0 then break; - decPos := 0; - end - else if (LowerCaseTable[c^] = 'e') and - (CharInSet((c+1)^, ['-','0'..'9'])) then - begin - if (c +1)^ = '-' then expIsNeg := true; - inc(c); - exp := 0; - end - else if (c^ < '0') or (c^ > '9') then - break - else if IsValid(exp) then - begin - exp := exp * 10 + (Ord(c^) - Ord('0')) - end else + inc(cc); + decStart := cc; + cc := ParseDigitsToDouble(cc, endC, decimals); + if cc > decStart then + begin + if cc - decStart <= 18 then + val := val + (decimals * Power10Reciprocal[(cc - decStart)]) + else + val := val + (decimals * Power(10, -(cc - decStart))) + end; + end; + + // Exponent + if (cc < endC) and ((cc^ = 'e') or (cc^ = 'E')) then begin - val := val *10 + Ord(c^) - Ord('0'); - if decPos >= 0 then inc(decPos); + case (cc+1)^ of + '-', '0'..'9': + begin + inc(cc); + if cc^ = '-' then + begin + expIsNeg := true; + inc(cc); + end; + cc := ParseExpDigits(cc, endC, exp); + end; + end; end; - inc(c); end; - Result := c > start; - if not Result then Exit; + Result := cc > start; + if not Result then + begin + c := cc; + Exit; + end; - if decPos > 0 then val := val * Power(10, -decPos); if isNeg then val := -val; if IsValid(exp) then begin - if expIsNeg then - val := val * Power(10, -exp) else - val := val * Power(10, exp); + if exp <= 18 then + begin + if expIsNeg then + val := val * Power10Reciprocal[exp] else + val := val * Power10[exp]; + end + else + begin + if expIsNeg then + val := val * Power(10, -exp) else + val := val * Power(10, exp); + end; end; //https://oreillymedia.github.io/Using_SVG/guide/units.html - case c^ of + case cc^ of '%': begin - inc(c); + inc(cc); unitType := utPercent; end; 'c': //convert cm to pixels - if ((c+1)^ = 'm') then + if ((cc+1)^ = 'm') then begin - inc(c, 2); + inc(cc, 2); unitType := utCm; end; 'd': //ignore deg - if ((c+1)^ = 'e') and ((c+2)^ = 'g') then + if ((cc+1)^ = 'e') and ((cc+2)^ = 'g') then begin - inc(c, 3); + inc(cc, 3); unitType := utDegree; end; 'e': //convert cm to pixels - if ((c+1)^ = 'm') then + if ((cc+1)^ = 'm') then begin - inc(c, 2); + inc(cc, 2); unitType := utEm; end - else if ((c+1)^ = 'x') then + else if ((cc+1)^ = 'x') then begin - inc(c, 2); + inc(cc, 2); unitType := utEx; end; 'i': //convert inchs to pixels - if ((c+1)^ = 'n') then + if ((cc+1)^ = 'n') then begin - inc(c, 2); + inc(cc, 2); unitType := utInch; end; 'm': //convert mm to pixels - if ((c+1)^ = 'm') then + if ((cc+1)^ = 'm') then begin - inc(c, 2); + inc(cc, 2); unitType := utMm; end; 'p': - case (c+1)^ of + case (cc+1)^ of 'c': begin - inc(c, 2); + inc(cc, 2); unitType := utPica; end; 't': begin - inc(c, 2); + inc(cc, 2); unitType := utPt; end; 'x': begin - inc(c, 2); + inc(cc, 2); unitType := utPixel; end; end; 'r': //convert radian angles to degrees - if Match(c, 'rad') then + if Match(cc, 'rad') then begin - inc(c, 3); + inc(cc, 3); unitType := utRadian; end; end; + c := cc; +end; +//------------------------------------------------------------------------------ + +function ParseNextNum(var c: PUTF8Char; endC: PUTF8Char; + skipComma: Boolean; out val: double): Boolean; +var + tmp: TValue; +begin + tmp.Init; + Result := ParseNextNumEx(c, endC, skipComma, tmp.rawVal, tmp.unitType); + val := tmp.GetValue(1, 1); +end; +//------------------------------------------------------------------------------ + +function ExtractRef(const href: UTF8String): UTF8String; {$IFDEF INLINE} inline; {$ENDIF} +var + c, c2, endC: PUTF8Char; +begin + c := PUTF8Char(href); + endC := c + Length(href); + if Match(c, 'url(') then + begin + inc(c, 4); + dec(endC); // avoid trailing ')' + end; + if c^ = '#' then inc(c); + c2 := c; + while (c < endC) and (c^ <> ')') do inc(c); + ToUTF8String(c2, c, Result); +end; +//------------------------------------------------------------------------------ + +function ParseNextChar(var c: PUTF8Char; endC: PUTF8Char): UTF8Char; +var + cc: PUTF8Char; +begin + cc := SkipBlanksEx(c, endC); + if cc >= endC then + Result := #0 + else + begin + Result := cc^; + c := cc + 1; + end; +end; +//------------------------------------------------------------------------------ + +procedure ToTrimmedUTF8String(c, endC: PUTF8Char; var S: UTF8String); +var + len: integer; +begin + // trim left + while (c < endC) and (c^ <= space) do Inc(c); + // trim right + while (endC > c) and (endC[-1] <= space) do Dec(endC); + + len := endC - c; + SetLength(S, len); + if len = 0 then Exit; + Move(c^, PUTF8Char(S)^, len * SizeOf(UTF8Char)); +end; +//------------------------------------------------------------------------------ + +function PosEx(const subStr: UTF8String; const text: Utf8String; startIdx: integer): integer; +var + i, maxI, len, subStrLen: integer; +begin + len := Length(Text); + subStrLen := Length(subStr); + maxI := len - subStrLen +1; + for i := Max(1, startIdx) to maxI do + begin + if (text[i] <> subStr[1]) or + not CompareMem(@text[i], @subStr[1], subStrLen) then Continue; + Result := i; + Exit; + end; + Result := 0; end; //------------------------------------------------------------------------------ -function ParseNextNum(var c: PUTF8Char; endC: PUTF8Char; - skipComma: Boolean; out val: double): Boolean; +function ReversePosEx(utf8: utf8Char; + const text: Utf8String; startIdx: integer): integer; overload; +{$IFDEF INLINE} inline; {$ENDIF} +begin + Result := Max(0, Min(Length(text), startidx)); + while (Result > 0) and (text[Result] <> utf8) do Dec(Result); +end; +//------------------------------------------------------------------------------ + +function TrimMultiSpacesUtf8(const text: Utf8String): Utf8String; var - tmp: TValue; + i, len: integer; begin - tmp.Init; - Result := ParseNextNumEx(c, endC, skipComma, tmp.rawVal, tmp.unitType); - val := tmp.GetValue(1, 1); + Result := text; + len := Length(Result); + for i := 1 to len do + if Result[i] < #32 then Result[i] := #32; + i := ReversePosEx(space, Result, len); + while i > 1 do + begin + Dec(i); + while (i > 0) and (Result[i] = space) do + begin + Delete(Result, i, 1); + Dec(i); + end; + i := ReversePosEx(space, Result, i); + end; end; //------------------------------------------------------------------------------ -function ExtractRef(const href: UTF8String): UTF8String; {$IFDEF INLINE} inline; {$ENDIF} +function ReversePosEx(c: WideChar; + const text: UnicodeString; startIdx: integer): integer; overload; +{$IFDEF INLINE} inline; {$ENDIF} +begin + Result := Max(0, Min(Length(text), startidx)); + while (Result > 0) and (text[Result] <> c) do Dec(Result); +end; +//------------------------------------------------------------------------------ + +function TrimMultiSpacesUnicode(const text: UnicodeString): UnicodeString; var - c, c2, endC: PUTF8Char; + i, len: integer; begin - c := PUTF8Char(href); - endC := c + Length(href); - if Match(c, 'url(') then + Result := text; + len := Length(Result); + for i := 1 to len do + if Result[i] < #32 then Result[i] := #32; + i := ReversePosEx(space, Result, len); + while i > 1 do begin - inc(c, 4); - dec(endC); // avoid trailing ')' + Dec(i); + while (i > 0) and (Result[i] = space) do + begin + Delete(Result, i, 1); + Dec(i); + end; + i := ReversePosEx(space, Result, i); end; - if c^ = '#' then inc(c); - c2 := c; - while (c < endC) and (c^ <> ')') do inc(c); - Result := ToUTF8String(c2, c); end; //------------------------------------------------------------------------------ -function ParseNextChar(var c: PUTF8Char; endC: PUTF8Char): UTF8Char; +function StripNewlines(const s: UTF8String): UTF8String; +var + i: integer; begin - Result := #0; - if not SkipBlanks(c, endC) then Exit; - Result := c^; - inc(c); + Result := s; + i := Length(Result); + while i > 0 do + begin + if Result[i] < space then Delete(Result, i, 1); + Dec(i); + end; end; //------------------------------------------------------------------------------ -function ParseQuoteChar(var c: PUTF8Char; endC: PUTF8Char): UTF8Char; +function StripNewlines(const s: UnicodeString): UnicodeString; +var + i: integer; begin - if SkipBlanks(c, endC) and (c^ in [quote, dquote]) then + Result := s; + i := Length(Result); + while i > 0 do begin - Result := c^; - inc(c); - end else - Result := #0; + if Result[i] < space then Delete(Result, i, 1); + Dec(i); + end; end; //------------------------------------------------------------------------------ -function AllTrim(var name: UTF8String): Boolean; +function ConvertNewlines(const s: UTF8String): UTF8String; overload; var - i, len: integer; + i: integer; begin - len := Length(name); - i := 0; - while (len > 0) and (name[1] <= space) do + Result := s; + i := Length(Result); + while i > 0 do begin - inc(i); dec(len); + if Result[i] < space then + begin + if Result[i] = #10 then + Result[i] := space else + Delete(Result, i, 1); + end; + Dec(i); + end; +end; +//------------------------------------------------------------------------------ + +function ConvertNewlines(const s: UnicodeString): UnicodeString; overload; +var + i: integer; +begin + Result := s; + i := Length(Result); + while i > 0 do + begin + if Result[i] < space then + begin + if Result[i] = #10 then + Result[i] := space else + Delete(Result, i, 1); + end; + Dec(i); end; - if i > 0 then Delete(name, 1, i); - Result := len > 0; - if not Result then Exit; - while name[len] <= space do dec(len); - SetLength(name, len); end; //------------------------------------------------------------------------------ -function ToUTF8String(var c: PUTF8Char; endC: PUTF8Char): UTF8String; +procedure ToUTF8String(c, endC: PUTF8Char; + var S: UTF8String; spacesInText: TSpacesInText); var len: integer; begin len := endC - c; - SetLength(Result, len); + SetLength(S, len); + if len = 0 then Exit; + Move(c^, PUTF8Char(S)^, len * SizeOf(UTF8Char)); + if spacesInText <> sitPreserve then + S := TrimMultiSpacesUtf8(S); + S := ConvertNewlines(S); +end; +//------------------------------------------------------------------------------ + +procedure ToAsciiLowerUTF8String(c, endC: PUTF8Char; var S: UTF8String); +// Reads a UTF8String and converts all upper case 'A'..'Z' to lower case 'a'..'z' +var + len: integer; + p: PUTF8Char; + ch: UTF8Char; +begin + len := endC - c; + SetLength(S, len); if len = 0 then Exit; - Move(c^, Result[1], len * SizeOf(UTF8Char)); - c := endC; + + // Use a pointer arithmetic trick to run forward by using a negative index + p := PUTF8Char(S) + len; + len := -len; + while len < 0 do + begin + ch := endC[len]; + case ch of + 'A'..'Z': + ch := UTF8Char(Byte(ch) or $20); + end; + p[len] := ch; + inc(len); + end; end; //------------------------------------------------------------------------------ @@ -880,13 +1510,11 @@ function IsKnownEntity(owner: TSvgParser; var c: PUTF8Char; endC: PUTF8Char; out entity: PSvgAttrib): boolean; var c2, c3: PUTF8Char; - entityName: UTF8String; begin inc(c); //skip ampersand. c2 := c; c3 := c; - ParseNameLength(c3, endC); - entityName := ToUTF8String(c2, c3); - entity := owner.FindEntity(GetHash(entityName)); + c3 := ParseNameLength(c3, endC); + entity := owner.FindEntity(GetHash(c2, c3 - c2)); Result := (c3^ = ';') and Assigned(entity); //nb: increments 'c' only if the entity is found. if Result then c := c3 +1 else dec(c); @@ -905,7 +1533,7 @@ function ParseQuotedString(var c: PUTF8Char; endC: PUTF8Char; while (c < endC) and (c^ <> quote) do inc(c); Result := (c < endC); if not Result then Exit; - quotStr := ToUTF8String(c2, c); + ToUTF8String(c2, c, quotStr); inc(c); end; //------------------------------------------------------------------------------ @@ -929,7 +1557,7 @@ function IsNumPending(var c: PUTF8Char; c2 := c; if (c2^ = '-') then inc(c2); if (c2^ = SvgDecimalSeparator) then inc(c2); - Result := (c2 < endC) and (c2^ >= '0') and (c2^ <= '9'); + Result := (c2 < endC) and IsDigit(c2^); end; //------------------------------------------------------------------------------ @@ -1004,7 +1632,7 @@ function ParseTransform(const transform: UTF8String): TMatrixD; MatrixSkew(mat, 0, DegToRad(values[0])); end; end; - Result := MatrixMultiply(Result, mat); + MatrixMultiply2(mat, Result); end; end; //------------------------------------------------------------------------------ @@ -1026,12 +1654,12 @@ procedure GetSvgFontInfo(const value: UTF8String; var fontInfo: TSVGFontInfo); begin hash := ParseNextWordHashed(c, endC); case hash of - hSans_045_Serif : fontInfo.family := ttfSansSerif; - hSerif : fontInfo.family := ttfSerif; - hMonospace : fontInfo.family := ttfMonospace; + hSans_045_Serif : fontInfo.family := tfSansSerif; + hSerif : fontInfo.family := tfSerif; + hMonospace : fontInfo.family := tfMonospace; hBold : fontInfo.weight := 600; hItalic : fontInfo.italic := sfsItalic; - hNormal : + hNormal : begin fontInfo.weight := 400; fontInfo.italic := sfsNone; @@ -1051,6 +1679,7 @@ function HtmlDecode(const html: UTF8String): UTF8String; var val, len: integer; c,ce,endC: PUTF8Char; + ch: UTF8Char; begin len := Length(html); SetLength(Result, len*3); @@ -1082,14 +1711,15 @@ function HtmlDecode(const html: UTF8String): UTF8String; inc(c, 3); while c < ce do begin - if (c^ >= 'a') and (c^ <= 'f') then - val := val * 16 + Ord(c^) - 87 - else if (c^ >= 'A') and (c^ <= 'F') then - val := val * 16 + Ord(c^) - 55 - else if (c^ >= '0') and (c^ <= '9') then - val := val * 16 + Ord(c^) - 48 + ch := c^; + case ch of + 'a'..'f': + val := val * 16 + Ord(ch) - 87; + 'A'..'F': + val := val * 16 + Ord(ch) - 55; + '0'..'9': + val := val * 16 + Ord(ch) - 48; else - begin val := -1; break; end; @@ -1210,16 +1840,16 @@ function UTF8StringToColor32(const value: UTF8String; var color: TColor32): Bool if mus[i] = utPercent then vals[i] := vals[i] * 255 / 100; - if ParseNextNumEx(c, endC, true, vals[3], mus[3]) then + if (c < endC) and (c^ <> ')') and ParseNextNumEx(c, endC, true, vals[3], mus[3]) then alpha := 255 else //stops further alpha adjustment vals[3] := 255; if ParseNextChar(c, endC) <> ')' then Exit; for i := 0 to 3 do if IsFraction(vals[i]) then vals[i] := vals[i] * 255; - color := ClampByte(Round(vals[3])) shl 24 + - ClampByte(Round(vals[0])) shl 16 + - ClampByte(Round(vals[1])) shl 8 + - ClampByte(Round(vals[2])); + color := ClampByte(Integer(Round(vals[3]))) shl 24 + + ClampByte(Integer(Round(vals[0]))) shl 16 + + ClampByte(Integer(Round(vals[1]))) shl 8 + + ClampByte(Integer(Round(vals[2]))); end else if (c^ = '#') then //#RRGGBB or #RGB begin @@ -1281,9 +1911,8 @@ function UTF8StringToColor32(const value: UTF8String; var color: TColor32): Bool color := clr; end else //color name lookup begin - i := ColorConstList.IndexOf(string(value)); - if i < 0 then Exit; - color := TColorObj(ColorConstList.Objects[i]).cc.ColorValue; + if not ColorConstList.GetColorValue(value, color) then + Exit; end; //and in case the opacity has been set before the color @@ -1296,28 +1925,21 @@ function UTF8StringToColor32(const value: UTF8String; var color: TColor32): Bool end; //------------------------------------------------------------------------------ -function MakeDashArray(const dblArray: TArrayOfDouble; scale: double): TArrayOfInteger; +function ScaleDashArray(const dblArray: TArrayOfDouble; scale: double): TArrayOfDouble; var i, len: integer; - dist: double; begin - dist := 0; len := Length(dblArray); SetLength(Result, len); + if len = 0 then Exit; + for i := 0 to len -1 do - begin - Result[i] := Ceil(dblArray[i] * scale); - dist := Result[i] + dist; - end; + Result[i] := dblArray[i] * scale; - if dist = 0 then - begin - Result := nil; - end - else if Odd(len) then + if Odd(len) then begin SetLength(Result, len *2); - Move(Result[0], Result[len], len * SizeOf(integer)); + Move(Result[0], Result[len], len * SizeOf(double)); end; end; //------------------------------------------------------------------------------ @@ -1334,9 +1956,9 @@ procedure ParseStyleElementContent(const value: UTF8String; stylesList: TClassStylesList); var len, cap: integer; - names: array of string; + names: array of UTF8String; - procedure AddName(const name: string); + procedure AddName(const name: UTF8String); begin if len = cap then begin @@ -1362,40 +1984,52 @@ procedure ParseStyleElementContent(const value: UTF8String; c := @value[1]; endC := c + Length(value); - SkipBlanks(c, endC); + c := SkipBlanksEx(c, endC); + if c >= endC then Exit; + if Match(c, '= endC then Break; + + case c^ of + SvgDecimalSeparator, '#', 'A'..'Z', 'a'..'z': ; + else break; + end; + //get one or more class names for each pending style c2 := c; - ParseNameLength(c, endC); - aclassName := ToUTF8String(c2, c); + c := ParseNameLength(c, endC); + ToAsciiLowerUTF8String(c2, c, aclassName); - AddName(Lowercase(String(aclassName))); - if PeekNextChar(c, endC) = ',' then + AddName(aclassName); + + c := SkipStyleBlanks(c, endC); + if (c < endC) and (c^ = ',') then begin inc(c); Continue; end; if len = 0 then break; - SetLength(names, len); //ie no more comma separated names //now get the style - if PeekNextChar(c, endC) <> '{' then Break; + if (c >= endC) or (c^ <> '{') then Break; inc(c); c2 := c; while (c < endC) and (c^ <> '}') do inc(c); if (c = endC) then break; - aStyle := ToUTF8String(c2, c); - - //finally, for each class name add (or append) this style - for i := 0 to High(names) do - stylesList.AddAppendStyle(names[i], aStyle); - names := nil; - len := 0; cap := 0; + ToTrimmedUTF8String(c2, c, aStyle); + if aStyle <> '' then + begin + stylesList.Preallocate(len); + //finally, for each class name add (or append) this style + for i := 0 to len - 1 do + stylesList.AddAppendStyle(names[i], aStyle); + end; + // Reset the used names array length, so we can reuse it to reduce the amount of SetLength calls + len := 0; inc(c); end; end; @@ -1432,7 +2066,7 @@ procedure TXmlEl.Clear; i: integer; begin for i := 0 to attribs.Count -1 do - Dispose(PSvgAttrib(attribs[i])); + DisposeSvgAttrib(PSvgAttrib(attribs.List[i])); attribs.Clear; for i := 0 to childs.Count -1 do @@ -1441,69 +2075,73 @@ procedure TXmlEl.Clear; end; //------------------------------------------------------------------------------ -function TagNameToLower(const tagName: UTF8String): UTF8String; -var - i: integer; -begin - Result := tagName; - for i := 1 to Length(Result) do - if (Result[i] >= 'A') and (Result[i] <= 'Z') then - Result[i] := AnsiChar(Ord(Result[i]) + 32); -end; -//------------------------------------------------------------------------------ - function TXmlEl.ParseHeader(var c: PUTF8Char; endC: PUTF8Char): Boolean; var style: UTF8String; c2: PUTF8Char; begin - SkipBlanks(c, endC); - c2 := c;; - ParseNameLength(c, endC); - name := TagNameToLower(ToUTF8String(c2, c)); + c2 := SkipBlanksEx(c, endC); + c := ParseNameLength(c2, endC); + ToAsciiLowerUTF8String(c2, c, name); //load the class's style (ie undotted style) if found. style := owner.classStyles.GetStyle(name); if style <> '' then ParseStyleAttribute(style); - Result := ParseAttributes(c, endC); end; //------------------------------------------------------------------------------ -function TXmlEl.ParseAttribName(var c: PUTF8Char; - endC: PUTF8Char; attrib: PSvgAttrib): Boolean; -var - c2: PUTF8Char; - //attribName: UTF8String; +class function TXmlEl.ParseAttribName(c: PUTF8Char; + endC: PUTF8Char; attrib: PSvgAttrib): PUTF8Char; begin - Result := SkipBlanks(c, endC); - if not Result then Exit; - c2 := c; - ParseNameLength(c, endC); - attrib.Name := ToUTF8String(c2, c); + Result := SkipBlanksEx(c, endC); + if Result >= endC then Exit; + c := Result; + Result := ParseNameLength(Result, endC); + ToUTF8String(c, Result, attrib.Name); attrib.hash := GetHash(attrib.Name); end; //------------------------------------------------------------------------------ -function TXmlEl.ParseAttribValue(var c: PUTF8Char; - endC: PUTF8Char; attrib: PSvgAttrib): Boolean; +class function TXmlEl.ParseAttribValue(c, endC: PUTF8Char; + attrib: PSvgAttrib): PUTF8Char; +// Parse: [Whitespaces] "=" [Whitespaces] ("'" | "\"") ("'" | "\"") var - quoteChar : UTF8Char; - c2, c3: PUTF8Char; + quoteChar: UTF8Char; + c2: PUTF8Char; begin - Result := ParseNextChar(c, endC) = '='; - if not Result then Exit; - quoteChar := ParseQuoteChar(c, endC); - if quoteChar = #0 then Exit; - //trim leading and trailing spaces - while (c < endC) and (c^ <= space) do inc(c); - c2 := c; - while (c < endC) and (c^ <> quoteChar) do inc(c); - c3 := c; - while (c3 > c2) and ((c3 -1)^ <= space) do - dec(c3); - attrib.value := ToUTF8String(c2, c3); - inc(c); //skip end quote + Result := endC; + + // ParseNextChar: + c := SkipBlanksEx(c, endC); + if (c >= endC) or (c^ <> '=') then Exit; + inc(c); // '=' parsed + + // ParseQuoteChar: + c := SkipBlanksEx(c, endC); + if c >= endC then Exit; + quoteChar := c^; + if not (quoteChar in [quote, dquote]) then Exit; + inc(c); // quote parsed + + //trim leading and trailing spaces in the actual value + c := SkipBlanksEx(c, endC); + // find value end + Result := c; + while (Result < endC) and (Result^ <> quoteChar) do inc(Result); + c2 := Result; + while (c2 > c) and ((c2 -1)^ <= space) do dec(c2); + + ToUTF8String(c, c2, attrib.value, sitPreserve); + inc(Result); //skip end quote +end; +//------------------------------------------------------------------------------ + +class function TXmlEl.ParseAttribNameAndValue(c, endC: PUTF8Char; attrib: PSvgAttrib): PUTF8Char; +begin + Result := ParseAttribName(c, endC, attrib); + if (Result < endC) then + Result := ParseAttribValue(Result, endC, attrib); end; //------------------------------------------------------------------------------ @@ -1519,32 +2157,38 @@ function TXmlEl.ParseAttributes(var c: PUTF8Char; endC: PUTF8Char): Boolean; while SkipBlanks(c, endC) do begin - if CharInSet(c^, ['/', '?', '>']) then - begin - if (c^ <> '>') then - begin - inc(c); - if (c^ <> '>') then Exit; //error - selfClosed := true; - end; - inc(c); - Result := true; - break; - end - else if (c^ = 'x') and Match(c, 'xml:') then - begin - inc(c, 4); //ignore xml: prefixes + case c^ of + '/', '?': + begin + inc(c); + if (c^ <> '>') then Exit; //error + selfClosed := true; + inc(c); + Result := true; + break; + end; + '>': + begin + inc(c); + Result := true; + break; + end; + 'x': + if Match(c, 'xml:') then + begin + inc(c, 4); //ignore xml: prefixes + end; end; - New(attrib); - if not ParseAttribName(c, endC, attrib) or - not ParseAttribValue(c, endC, attrib) then + attrib := NewSvgAttrib(); + c := ParseAttribNameAndValue(c, endC, attrib); + if c >= endC then begin - Dispose(attrib); + DisposeSvgAttrib(attrib); Exit; end; - attribs.Add(attrib); + attribs.Add(attrib); case attrib.hash of hId : idAttrib := attrib; hClass : classAttrib := attrib; @@ -1588,31 +2232,38 @@ procedure TXmlEl.ParseStyleAttribute(const style: UTF8String); attrib: PSvgAttrib; begin //there are 4 ways to load styles (in ascending precedence) - - //1. a class element style (called during element contruction) + //1. a class element style (called during element construction) //2. a non-element class style (called via a class attribute) //3. an inline style (called via a style attribute) //4. an id specific class style c := PUTF8Char(style); endC := c + Length(style); - while SkipStyleBlanks(c, endC) do + while True do begin + c := SkipStyleBlanks(c, endC); + if c >= endC then Break; + c2 := c; - ParseStyleNameLen(c, endC); - styleName := ToUTF8String(c2, c); + c := ParseStyleNameLen(c, endC); + ToUTF8String(c2, c, styleName); if styleName = '' then Break; - if (ParseNextChar(c, endC) <> ':') or //syntax check - not SkipBlanks(c,endC) then Break; + // ParseNextChar + c := SkipStyleBlanks(c, endC); + if (c >= endC) or (c^ <> ':') then Break; //syntax check + inc(c); + + c := SkipBlanksEx(c, endC); + if c >= endC then Break; c2 := c; inc(c); while (c < endC) and (c^ <> ';') do inc(c); - styleVal := ToUTF8String(c2, c); - AllTrim(styleVal); + ToTrimmedUTF8String(c2, c, styleVal); inc(c); - new(attrib); + attrib := NewSvgAttrib(); attrib.name := styleName; attrib.value := styleVal; attrib.hash := GetHash(attrib.name); @@ -1633,14 +2284,28 @@ function TXmlEl.GetAttrib(index: integer): PSvgAttrib; end; //------------------------------------------------------------------------------ +function IsTextAreaTbreak(var c: PUTF8Char; endC: PUTF8Char): Boolean; +const + // https://www.w3.org/TR/SVGTiny12/text.html#tbreakElement + tbreak: PUTF8Char = ''; +begin + Result := (c + 9 < endC) and CompareMem(c, tbreak, 9); + if Result then inc(c, 8); +end; +//------------------------------------------------------------------------------ + function TXmlEl.ParseContent(var c: PUTF8Char; endC: PUTF8Char): Boolean; var - child: TSvgTreeEl; - entity: PSvgAttrib; - c2, tmpC, tmpEndC: PUTF8Char; + child : TSvgXmlEl; + entity : PSvgAttrib; + c2, cc : PUTF8Char; + tmpC, tmpEndC : PUTF8Char; begin Result := false; - while SkipBlanks(c, endC) do + // note: don't trim spaces at the start of text content. + // Text space trimming will be done later IF and when required. + while (hash = hText) or (hash = hTSpan) or + (hash = hTextArea) or SkipBlanks(c, endC) do begin if (c^ = '<') then begin @@ -1648,49 +2313,53 @@ function TXmlEl.ParseContent(var c: PUTF8Char; endC: PUTF8Char): Boolean; case c^ of '!': begin - if Match(c, '!--') then //start comment + cc := c; + if Match(cc, '!--') then //start comment begin - inc(c, 3); - while (c < endC) and ((c^ <> '-') or - not Match(c, '-->')) do inc(c); //end comment - inc(c, 3); + inc(cc, 3); + while (cc < endC) and ((cc^ <> '-') or + not Match(cc, '-->')) do inc(cc); //end comment + inc(cc, 3); end else begin //it's very likely ']') or not Match(c, ']]>')) do - inc(c); - text := ToUTF8String(c2, c); - inc(c, 3); + while (cc < endC) and ((cc^ <> ']') or not Match(cc, ']]>')) do + inc(cc); + ToUTF8String(c2, cc, text); + inc(cc, 3); if (hash = hStyle) then ParseStyleElementContent(text, owner.classStyles); end else begin - while (c < endC) and (c^ <> '<') do inc(c); - text := ToUTF8String(c2, c); + while (cc < endC) and (cc^ <> '<') do inc(cc); + ToUTF8String(c2, cc, text); end; end; + c := cc; end; '/', '?': begin //element closing tag - inc(c); - if Match(c, name) then + cc := c; + inc(cc); + if Match(cc, name) then begin - inc(c, Length(name)); + inc(cc, Length(name)); //very rarely there's a space before '>' - SkipBlanks(c, endC); - Result := c^ = '>'; - inc(c); + cc := SkipBlanksEx(cc, endC); + Result := cc^ = '>'; + inc(cc); end; + c := cc; Exit; end; else begin //starting a new element - child := TSvgTreeEl.Create(owner); + child := TSvgXmlEl.Create(owner); childs.Add(child); if not child.ParseHeader(c, endC) then break; if not child.selfClosed then @@ -1710,28 +2379,34 @@ function TXmlEl.ParseContent(var c: PUTF8Char; endC: PUTF8Char): Boolean; end else if (hash = hTSpan) or (hash = hText) or (hash = hTextPath) then begin - //text content: and because text can be mixed with one or more - // elements we need to create sub-elements for each text block. - //And elements can even have sub-elements. - tmpC := c; - //preserve a leading space - if (tmpC -1)^ = space then dec(tmpC); - while (c < endC) and (c^ <> '<') do inc(c); - if (hash = hTextPath) then - begin - text := ToUTF8String(tmpC, c); - end else - begin - child := TSvgTreeEl.Create(owner); - childs.Add(child); - child.text := ToUTF8String(tmpC, c); - end; + // assume this is text content, and because text can also be mixed + // with any number of nested elements, always put text + // content inside a pseudo 'self closed' element + cc := c; + while (cc < endC) and (cc^ <> '<') do inc(cc); + child := TSvgXmlEl.Create(owner); + child.name := 'tspan'; + child.hash := GetHash('tspan'); + child.selfClosed := true; ////////////////////// :))) + childs.Add(child); + ToUTF8String(c, cc, child.text, sitPreserve); + c := cc; + end + else if (hash = hTextArea) then + begin + // also assume this is text content, but don't create + // pseudo elements inside