Răsfoiți Sursa

ADD: Image32 graphic library

Alexander Koblov 2 ani în urmă
părinte
comite
7c91787361

+ 99 - 0
components/Image32/Image32.lpk

@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <Package Version="5">
+    <Name Value="Image32"/>
+    <Author Value="Angus Johnson"/>
+    <CompilerOptions>
+      <Version Value="11"/>
+      <SearchPaths>
+        <IncludeFiles Value="source"/>
+        <OtherUnitFiles Value="source"/>
+        <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+      </SearchPaths>
+    </CompilerOptions>
+    <Description Value="Image32 is a comprehensive 2D graphics library written entirely in Delphi Pascal, and without dependencies on other libraries. It provides an extensive range of image manipulation and drawing functions that includes text rendering through native parsing of truetype font files."/>
+    <License Value="Boost Software License - Version 1.0"/>
+    <Version Major="4" Minor="3"/>
+    <Files>
+      <Item>
+        <Filename Value="source/Img32.Draw.pas"/>
+        <UnitName Value="Img32.Draw"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.Extra.pas"/>
+        <UnitName Value="Img32.Extra"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.Fmt.SVG.pas"/>
+        <UnitName Value="Img32.Fmt.SVG"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.inc"/>
+        <Type Value="Include"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.pas"/>
+        <UnitName Value="Img32"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.Resamplers.pas"/>
+        <UnitName Value="Img32.Resamplers"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.SVG.Core.pas"/>
+        <UnitName Value="Img32.SVG.Core"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.SVG.HashConsts.inc"/>
+        <Type Value="Include"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.SVG.HtmlColorConsts.inc"/>
+        <Type Value="Include"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.SVG.HtmlHashConsts.inc"/>
+        <Type Value="Include"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.SVG.HtmlValues.inc"/>
+        <Type Value="Include"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.SVG.Path.pas"/>
+        <UnitName Value="Img32.SVG.Path"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.SVG.Reader.pas"/>
+        <UnitName Value="Img32.SVG.Reader"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.Text.pas"/>
+        <UnitName Value="Img32.Text"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.Transform.pas"/>
+        <UnitName Value="Img32.Transform"/>
+      </Item>
+      <Item>
+        <Filename Value="source/Img32.Vector.pas"/>
+        <UnitName Value="Img32.Vector"/>
+      </Item>
+    </Files>
+    <RequiredPkgs>
+      <Item>
+        <PackageName Value="LCL"/>
+      </Item>
+      <Item>
+        <PackageName Value="FCL"/>
+      </Item>
+    </RequiredPkgs>
+    <UsageOptions>
+      <UnitPath Value="$(PkgOutDir)"/>
+    </UsageOptions>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+  </Package>
+</CONFIG>

+ 17 - 0
components/Image32/Image32.pas

@@ -0,0 +1,17 @@
+{ This file was automatically created by Lazarus. Do not edit!
+  This source is only used to compile and install the package.
+ }
+
+unit Image32;
+
+{$warn 5023 off : no warning about unused units}
+interface
+
+uses
+  Img32.Draw, Img32.Extra, Img32.Fmt.SVG, Img32, Img32.Resamplers, 
+  Img32.SVG.Core, Img32.SVG.Path, Img32.SVG.Reader, Img32.Text, 
+  Img32.Transform, Img32.Vector;
+
+implementation
+
+end.

+ 23 - 0
components/Image32/LICENSE.txt

@@ -0,0 +1,23 @@
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.

+ 9 - 0
components/Image32/README.md

@@ -0,0 +1,9 @@
+# Image32
+
+A 2D graphics library written in Delphi Pascal
+
+https://github.com/AngusJohnson/Image32
+
+Version: 4.3+ (2022/10/16)
+
+Author: Angus Johnson

+ 2162 - 0
components/Image32/source/Img32.Draw.pas

@@ -0,0 +1,2162 @@
+unit Img32.Draw;
+
+(*******************************************************************************
+* Author    :  Angus Johnson                                                   *
+* Version   :  4.3                                                             *
+* Date      :  27 September 2022                                               *
+* Website   :  http://www.angusj.com                                           *
+* Copyright :  Angus Johnson 2019-2021                                         *
+*                                                                              *
+* 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                            *
+*******************************************************************************)
+
+interface
+
+{$I Img32.inc}
+
+{.$DEFINE MemCheck} //for debugging only (adds a minimal cost to performance)
+
+uses
+  SysUtils, Classes, Types, Math, Img32, Img32.Vector;
+
+type
+  TFillRule = Img32.Vector.TFillRule;
+
+  //TGradientColor: used internally by both
+  //TLinearGradientRenderer and TRadialGradientRenderer
+  TGradientColor = record
+    offset: double;
+    color: TColor32;
+  end;
+  TArrayOfGradientColor = array of TGradientColor;
+
+  TGradientFillStyle = (gfsClamp, gfsMirror, gfsRepeat);
+
+  //TBoundsProc: Function template for TCustomRenderer.
+  TBoundsProc = function(dist, colorsCnt: integer): integer;
+  TBoundsProcD = function(dist: double; colorsCnt: integer): integer;
+
+  TImage32ChangeProc = procedure of object;
+
+  //TCustomRenderer: can accommodate pixels of any size
+  TCustomRenderer = class {$IFDEF ABSTRACT_CLASSES} abstract {$ENDIF}
+  private
+    fImgWidth    : integer;
+    fImgHeight   : integer;
+    fImgBase     : Pointer;
+    fCurrY       : integer;
+    fCurrLinePtr : Pointer;
+    fPixelSize   : integer;
+    fChangeProc  : TImage32ChangeProc;
+  protected
+    procedure NotifyChange;
+    function Initialize(imgBase: Pointer;
+      imgWidth, imgHeight, pixelSize: integer): Boolean; overload; virtual;
+    function Initialize(targetImage: TImage32): Boolean; overload; virtual;
+    function GetDstPixel(x,y: integer): Pointer;
+    //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;
+    property ImgWidth: integer read fImgWidth;
+    property ImgHeight: integer read fImgHeight;
+    property ImgBase: Pointer read fImgBase;
+    property PixelSize: integer read fPixelSize;
+  end;
+
+  TColorRenderer = class(TCustomRenderer)
+  private
+    fAlpha: Byte;
+    fColor: TColor32;
+  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);
+  end;
+
+  TAliasedColorRenderer = class(TCustomRenderer)
+  private
+    fColor: TColor32;
+  protected
+    function Initialize(targetImage: TImage32): Boolean; override;
+    procedure RenderProc(x1, x2, y: integer; alpha: PByte); override;
+  public
+    constructor Create(color: TColor32 = clNone32);
+  end;
+
+  TEraseRenderer = class(TCustomRenderer)
+  protected
+    procedure RenderProc(x1, x2, y: integer; alpha: PByte); override;
+  end;
+
+  TInverseRenderer = class(TCustomRenderer)
+  protected
+    procedure RenderProc(x1, x2, y: integer; alpha: PByte); override;
+  end;
+
+  TImageRenderer = class(TCustomRenderer)
+  private
+    fImage        : TImage32;
+    fOffset       : TPoint;
+    fBrushPixel   :  PARGB;
+    fLastYY       : integer;
+    fMirrorY      : Boolean;
+    fBoundsProc   : TBoundsProc;
+    function GetFirstBrushPixel(x, y: integer): PARGB;
+  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);
+    destructor Destroy; override;
+    procedure SetTileFillStyle(value: TTileFillStyle);
+    property Image: TImage32 read fImage;
+    property Offset: TPoint read fOffset write fOffset;
+  end;
+
+  //TCustomGradientRenderer is also an abstract class
+  TCustomGradientRenderer = class(TCustomRenderer)
+  private
+    fBoundsProc      : TBoundsProc;
+    fGradientColors  : TArrayOfGradientColor;
+  protected
+    fColors          : TArrayOfColor32;
+    fColorsCnt       : integer;
+    procedure SetGradientFillStyle(value: TGradientFillStyle); virtual;
+  public
+    constructor Create;
+    procedure SetParameters(startColor, endColor: TColor32;
+      gradFillStyle: TGradientFillStyle = gfsClamp); virtual;
+    procedure InsertColorStop(offsetFrac: double; color: TColor32);
+    procedure Clear;
+  end;
+
+  TLinearGradientRenderer = class(TCustomGradientRenderer)
+  private
+    fStartPt         : TPointD;
+    fEndPt           : TPointD;
+    fPerpendicOffsets: TArrayOfInteger;
+    fIsVert          : Boolean;
+  protected
+    procedure RenderProc(x1, x2, y: integer; alpha: PByte); override;
+    function Initialize(targetImage: TImage32): Boolean; override;
+  public
+    procedure SetParameters(const startPt, endPt: TPointD;
+      startColor, endColor: TColor32;
+      gradFillStyle: TGradientFillStyle = gfsClamp); reintroduce;
+  end;
+
+  TRadialGradientRenderer = class(TCustomGradientRenderer)
+  private
+    fCenterPt       : TPointD;
+    fScaleX         : double;
+    fScaleY         : double;
+    fColors         : TArrayOfColor32;
+  protected
+    procedure RenderProc(x1, x2, y: integer; alpha: PByte); override;
+    function Initialize(targetImage: TImage32): Boolean; override;
+  public
+    procedure SetParameters(const focalRect: TRect;
+      innerColor, outerColor: TColor32;
+      gradientFillStyle: TGradientFillStyle = gfsClamp); reintroduce;
+  end;
+
+  TSvgRadialGradientRenderer = class(TCustomGradientRenderer)
+  private
+    fA, fB          : double;
+    fAA, fBB        : double;
+    fCenterPt       : TPointD;
+    fFocusPt        : TPointD;
+    fBoundsProcD    : TBoundsProcD;
+  protected
+    procedure RenderProc(x1, x2, y: integer; alpha: PByte); override;
+    function Initialize(targetImage: TImage32): Boolean; override;
+  public
+    procedure SetParameters(const ellipseRect: TRect;
+      const focus: TPoint; innerColor, outerColor: TColor32;
+      gradientFillStyle: TGradientFillStyle = gfsClamp); reintroduce;
+  end;
+
+  //Barycentric rendering colorizes inside triangles
+  TBarycentricRenderer = class(TCustomRenderer)
+  private
+    a: TPointD;
+    c1, c2, c3: TARGB;
+    v0, v1: TPointD;
+    d00, d01, d11, invDenom: double;
+    function GetColor(const pt: TPointD): TColor32;
+  protected
+    procedure RenderProc(x1, x2, y: integer; alpha: PByte); override;
+  public
+    procedure SetParameters(const a, b, c: TPointD; c1, c2, c3: TColor32);
+  end;
+
+  ///////////////////////////////////////////////////////////////////////////
+  // DRAWING FUNCTIONS
+  ///////////////////////////////////////////////////////////////////////////
+
+  procedure DrawPoint(img: TImage32; const pt: TPointD;
+    radius: double; color: TColor32); overload;
+  procedure DrawPoint(img: TImage32; const pt: TPointD;
+    radius: double; renderer: TCustomRenderer); overload;
+  procedure DrawPoint(img: TImage32; const points: TPathD;
+    radius: double; color: TColor32); overload;
+  procedure DrawPoint(img: TImage32; const paths: TPathsD;
+    radius: double; color: TColor32); overload;
+
+  procedure DrawLine(img: TImage32;
+    const pt1, pt2: TPointD; lineWidth: double; color: TColor32); overload;
+  procedure DrawLine(img: TImage32;
+    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; renderer: TCustomRenderer;
+    endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto;
+    miterLimit: double = 2); overload;
+  procedure DrawLine(img: TImage32; const lines: TPathsD;
+    lineWidth: double; color: TColor32;
+    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;
+    miterLimit: double = 2); overload;
+
+   procedure DrawInvertedLine(img: TImage32;
+     const line: TPathD; lineWidth: double;
+     endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); overload;
+   procedure DrawInvertedLine(img: TImage32;
+     const lines: TPathsD; lineWidth: double;
+     endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); overload;
+
+  procedure DrawDashedLine(img: TImage32; const line: TPathD;
+    dashPattern: TArrayOfInteger; patternOffset: PDouble;
+    lineWidth: double; color: TColor32;
+    endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); overload;
+  procedure DrawDashedLine(img: TImage32; const lines: TPathsD;
+    dashPattern: TArrayOfInteger; patternOffset: PDouble;
+    lineWidth: double; color: TColor32; endStyle: TEndStyle;
+    joinStyle: TJoinStyle = jsAuto); overload;
+  procedure DrawDashedLine(img: TImage32; const line: TPathD;
+    dashPattern: TArrayOfInteger; patternOffset: PDouble;
+    lineWidth: double; renderer: TCustomRenderer; endStyle: TEndStyle;
+    joinStyle: TJoinStyle = jsAuto); overload;
+  procedure DrawDashedLine(img: TImage32; const lines: TPathsD;
+    dashPattern: TArrayOfInteger; patternOffset: PDouble;
+    lineWidth: double; renderer: TCustomRenderer;
+    endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto); overload;
+
+  procedure DrawInvertedDashedLine(img: TImage32;
+    const line: TPathD; dashPattern: TArrayOfInteger;
+    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;
+
+  procedure DrawPolygon(img: TImage32; const polygon: TPathD;
+    fillRule: TFillRule; color: TColor32); overload;
+  procedure DrawPolygon(img: TImage32; const polygon: TPathD;
+    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; renderer: TCustomRenderer); overload;
+
+  // 'Clear Type' text rendering is quite useful for low resolution
+  // displays (96 ppi). However it's of little to no benefit on higher
+  // resolution displays and becomes unnecessary overhead. See also:
+  // https://en.wikipedia.org/wiki/Subpixel_rendering
+  // https://www.grc.com/ctwhat.htm
+  // https://www.grc.com/cttech.htm
+  procedure DrawPolygon_ClearType(img: TImage32; const polygons: TPathsD;
+    fillRule: TFillRule; color: TColor32; backColor: TColor32 = clWhite32);
+
+  ///////////////////////////////////////////////////////////////////////////
+  // MISCELLANEOUS FUNCTIONS
+  ///////////////////////////////////////////////////////////////////////////
+
+  procedure ErasePolygon(img: TImage32; const polygon: TPathD;
+    fillRule: TFillRule); overload;
+  procedure ErasePolygon(img: TImage32; const polygons: TPathsD;
+    fillRule: TFillRule); overload;
+
+  //Both DrawBoolMask and DrawAlphaMask require
+  //'mask' length to equal 'img' width * height
+  procedure DrawBoolMask(img: TImage32;
+    const mask: TArrayOfByte; color: TColor32 = clBlack32);
+  procedure DrawAlphaMask(img: TImage32;
+    const mask: TArrayOfByte; color: TColor32 = clBlack32);
+
+  procedure Rasterize(const paths: TPathsD;
+    const clipRec: TRect; fillRule: TFillRule; renderer: TCustomRenderer);
+
+implementation
+
+{$IFDEF MemCheck}
+resourcestring
+  sMemCheckError = 'Img32.Draw: Memory allocation error';
+{$ENDIF}
+
+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;
+  end;
+  TFragmentArray = array[0 .. (Maxint div SizeOf(TFragment)) -1] of TFragment;
+  PFragments = ^TFragmentArray;
+  PFragment = ^TFragment;
+
+  TScanLine = record
+    Y: integer;
+    minX, maxX: integer;
+    fragCnt: integer;
+    {$IFDEF MemCheck} total: integer; {$ENDIF}
+    fragments: PFragments;
+  end;
+  PScanline = ^TScanline;
+  TArrayOfScanline = array of TScanline;
+
+//------------------------------------------------------------------------------
+// ApplyClearType (see DrawPolygon_ClearType below)
+//------------------------------------------------------------------------------
+
+type
+  PArgbs = ^TArgbs;
+  TArgbs = array [0.. (Maxint div SizeOf(TARGB)) -1] of TARGB;
+
+procedure ApplyClearType(img: TImage32; textColor: TColor32 = clBlack32;
+  bkColor: TColor32 = clWhite32);
+const
+  centerWeighting = 5; //0 <= centerWeighting <= 25
+var
+  h, w: integer;
+  src, dst: PARGB;
+  srcArr: PArgbs;
+  fgColor: TARGB absolute textColor;
+  bgColor: TARGB absolute bkColor;
+  diff_R, diff_G, diff_B: integer;
+  bg8_R, bg8_G, bg8_B: integer;
+  rowBuffer: TArrayOfARGB;
+  primeTbl, nearTbl, FarTbl: PByteArray;
+begin
+  // Precondition: the background to text drawn onto 'img' must be transparent
+
+  // 85 + (2 * 57) + (2 * 28) == 255
+  primeTbl := PByteArray(@MulTable[85 + centerWeighting *2]);
+  nearTbl  := PByteArray(@MulTable[57]);
+  farTbl   := PByteArray(@MulTable[28 - centerWeighting]);
+  SetLength(rowBuffer, img.Width +4);
+
+  for h := 0 to img.Height -1 do
+  begin
+    //each row of the image is copied into a temporary buffer ...
+    //noting that while 'dst' (img.Pixels) is initially the source
+    //it will later be destination (during image compression).
+    dst := PARGB(@img.Pixels[h * img.Width]);
+    src := PARGB(@rowBuffer[2]);
+    Move(dst^, src^, img.Width * SizeOf(TColor32));
+    srcArr := PArgbs(rowBuffer);
+
+    //using this buffer compress the image ...
+    w := 2;
+    while w < img.Width do
+    begin
+      dst.R := primeTbl[srcArr[w].A] +
+        nearTbl[srcArr[w-1].A] + farTbl[srcArr[w-2].A] +
+        nearTbl[srcArr[w+1].A] + farTbl[srcArr[w+2].A];
+      inc(w);
+      dst.G := primeTbl[srcArr[w].A] +
+        nearTbl[srcArr[w-1].A] + farTbl[srcArr[w-2].A] +
+        nearTbl[srcArr[w+1].A] + farTbl[srcArr[w+2].A];
+      inc(w);
+      dst.B := primeTbl[srcArr[w].A] +
+        nearTbl[srcArr[w-1].A] + farTbl[srcArr[w-2].A] +
+        nearTbl[srcArr[w+1].A] + farTbl[srcArr[w+2].A];
+      inc(w);
+      dst.A := 255;
+      inc(dst);
+    end;
+  end;
+
+  //Following compression the right 2/3 of the image is redundant
+   img.Crop(Types.Rect(0,0, img.Width div 3, img.Height));
+
+  //currently text is white and the background is black
+  //so blend in the text and background colors ...
+  diff_R := fgColor.R - bgColor.R;
+  diff_G := fgColor.G - bgColor.G;
+  diff_B := fgColor.B - bgColor.B;
+  bg8_R := bgColor.R shl 8;
+  bg8_G := bgColor.G shl 8;
+  bg8_B := bgColor.B shl 8;
+  dst := PARGB(img.PixelBase);
+  for h := 0 to img.Width * img.Height -1 do
+  begin
+    if dst.R = 0 then
+      dst.Color := bkColor
+    else
+    begin
+      //blend front (text) and background colors ...
+      dst.R := (bg8_R + diff_R * dst.R) shr 8;
+      dst.G := (bg8_G + diff_G * dst.G) shr 8;
+      dst.B := (bg8_B + diff_B * dst.B) shr 8;
+    end;
+    inc(dst);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// Other miscellaneous functions
+//------------------------------------------------------------------------------
+
+////__Trunc: An efficient Trunc() algorithm (ie rounds toward zero)
+//function __Trunc(val: double): integer; {$IFDEF INLINE} inline; {$ENDIF}
+//var
+//  exp: integer;
+//  i64: UInt64 absolute val;
+//begin
+//  //https://en.wikipedia.org/wiki/Double-precision_floating-point_format
+//  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;
+//  Result := ((i64 and $1FFFFFFFFFFFFF) shr (52-exp)) or (1 shl exp);
+//  if val < 0 then Result := -Result;
+//end;
+//------------------------------------------------------------------------------
+
+function ClampByte(val: double): byte; {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  if val < 0 then result := 0
+  else if val > 255 then result := 255
+  else result := Round(val);
+end;
+//------------------------------------------------------------------------------
+
+function GetPixel(current: PARGB; delta: integer): PARGB;
+{$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result := current;
+  inc(Result, delta);
+end;
+//------------------------------------------------------------------------------
+
+function ReverseColors(const colors: TArrayOfGradientColor): TArrayOfGradientColor;
+var
+  i, highI: integer;
+begin
+  highI := High(colors);
+  SetLength(result, highI +1);
+  for i := 0 to highI do
+  begin
+    result[i].color := colors[highI -i].color;
+    result[i].offset := 1 - colors[highI -i].offset;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure SwapColors(var color1, color2: TColor32);
+var
+  c: TColor32;
+begin
+  c := color1;
+  color1 := color2;
+  color2 := c;
+end;
+//------------------------------------------------------------------------------
+
+procedure SwapPoints(var point1, point2: TPoint); overload;
+var
+  pt: TPoint;
+begin
+  pt := point1;
+  point1 := point2;
+  point2 := pt;
+end;
+//------------------------------------------------------------------------------
+
+procedure SwapPoints(var point1, point2: TPointD); overload;
+var
+  pt: TPointD;
+begin
+  pt := point1;
+  point1 := point2;
+  point2 := pt;
+end;
+//------------------------------------------------------------------------------
+
+function ClampQ(q, endQ: integer): integer;
+begin
+  if q < 0 then result := 0
+  else if q >= endQ then result := endQ -1
+  else result := q;
+end;
+//------------------------------------------------------------------------------
+
+function ClampD(d: double; colorCnt: integer): integer;
+begin
+  dec(colorCnt);
+  if d < 0 then result := 0
+  else if d >= 1 then result := colorCnt
+  else result := Round(d * colorCnt);
+end;
+//------------------------------------------------------------------------------
+
+function MirrorQ(q, endQ: integer): integer;
+begin
+  result := q mod endQ;
+  if (result < 0) then result := -result;
+  if Odd(q div endQ) then
+    result := (endQ -1) - result;
+end;
+//------------------------------------------------------------------------------
+
+function MirrorD(d: double; colorCnt: integer): integer;
+begin
+  dec(colorCnt);
+  if Odd(Round(d)) then
+    result := Round((1 - frac(d)) * colorCnt) else
+    result := Round(frac(d)  * colorCnt);
+end;
+//------------------------------------------------------------------------------
+
+function RepeatQ(q, endQ: integer): integer;
+begin
+  if (q < 0) or (q >= endQ) then
+  begin
+    endQ := Abs(endQ);
+    result := q mod endQ;
+    if result < 0 then inc(result, endQ);
+  end
+  else result := q;
+end;
+//------------------------------------------------------------------------------
+
+function SoftRptQ(q, endQ: integer): integer;
+begin
+  if (q < 0) then
+    result := endQ + (q mod endQ) else
+    result := (q mod endQ);
+  if result = 0 then result := endQ div 2;
+end;
+//------------------------------------------------------------------------------
+
+function RepeatD(d: double; colorCnt: integer): integer;
+begin
+  dec(colorCnt);
+  if (d < 0) then
+    result := Round((1 + frac(d)) * colorCnt) else
+    result := Round(frac(d)  * colorCnt);
+end;
+//------------------------------------------------------------------------------
+
+function BlendColorUsingMask(bgColor, fgColor: TColor32; mask: Byte): TColor32;
+var
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute fgColor;
+  res: TARGB absolute Result;
+  R, invR: PByteArray;
+begin
+  if fg.A = 0 then
+  begin
+    Result := bgColor;
+    res.A := MulBytes(res.A, not mask);
+  end
+  else if bg.A = 0 then
+  begin
+    Result := fgColor;
+    res.A := MulBytes(res.A, mask);
+  end
+  else if (mask = 0) then
+    Result := bgColor
+  else if (mask = 255) then
+    Result := fgColor
+  else
+  begin
+    R    := PByteArray(@MulTable[mask]);
+    InvR := PByteArray(@MulTable[not mask]);
+    res.A := R[fg.A] + InvR[bg.A];
+    res.R := R[fg.R] + InvR[bg.R];
+    res.G := R[fg.G] + InvR[bg.G];
+    res.B := R[fg.B] + InvR[bg.B];
+  end;
+end;
+//------------------------------------------------------------------------------
+
+//MakeColorGradient: using the supplied array of TGradientColor,
+//create an array of TColor32 of the specified length
+function MakeColorGradient(const gradColors: TArrayOfGradientColor;
+  len: integer): TArrayOfColor32;
+var
+  i,j, lenC: integer;
+  dist, offset1, offset2, step, pos: double;
+  color1, color2: TColor32;
+begin
+  lenC := length(gradColors);
+  if (len = 0) or (lenC < 2) then Exit;
+  SetLength(result, len);
+
+  color2 := gradColors[0].color;
+  result[0] := color2;
+  if len = 1 then Exit;
+
+  step := 1/(len-1);
+  pos := step;
+  offset2 := 0;
+  i := 1; j := 1;
+  repeat
+    offset1 := offset2;
+    offset2 := gradColors[i].offset;
+    dist := offset2 - offset1;
+    color1 := color2;
+    color2 := gradColors[i].color;
+    while (pos <= dist) and (j < len) do
+    begin
+      result[j] := BlendColorUsingMask(color1, color2, Round(pos/dist * 255));
+      inc(j);
+      pos := pos + step;
+    end;
+    pos := pos - dist;
+    inc(i);
+  until i = lenC;
+  if j < len then result[j] := result[j-1];
+end;
+
+//------------------------------------------------------------------------------
+// Rasterize() support functions
+//------------------------------------------------------------------------------
+
+procedure AllocateScanlines(const polygons: TPathsD;
+  var scanlines: TArrayOfScanline; clipBottom, clipRight: integer);
+var
+  i,j, highI, highJ: integer;
+  y1, y2: integer;
+  psl: PScanline;
+begin
+  //first count how often each edge intersects with each horizontal scanline
+  for i := 0 to high(polygons) do
+  begin
+    highJ := high(polygons[i]);
+    if highJ < 2 then continue;
+    y1 := Round(polygons[i][highJ].Y);
+    for j := 0 to highJ do
+    begin
+      y2 := Round(polygons[i][j].Y);
+      if y1 < y2 then
+      begin
+        //descending (but ignore edges outside the clipping range)
+        if (y2 >= 0) and (y1 <= clipBottom) then
+        begin
+          if (y1 > 0) and (y1 <= clipBottom)  then
+            dec(scanlines[y1 -1].fragCnt);
+          if y2 >= clipBottom then
+            inc(scanlines[clipBottom].fragCnt) else
+            inc(scanlines[y2].fragCnt);
+        end;
+      end else
+      begin
+        //ascending (but ignore edges outside the clipping range)
+        if (y1 >= 0) and (y2 <= clipBottom) then
+        begin
+          if (y2 > 0) then
+            dec(scanlines[y2 -1].fragCnt);
+          if y1 >= clipBottom then
+            inc(scanlines[clipBottom].fragCnt) else
+            inc(scanlines[y1].fragCnt);
+        end;
+      end;
+      y1 := y2;
+    end;
+  end;
+
+  //convert 'count' accumulators into real counts and allocate storage
+  j := 0;
+  highI := high(scanlines);
+  psl := @scanlines[highI];
+
+  //'fragments' is a pointer and not a dynamic array because
+  //dynamic arrays are zero initialized (hence slower than GetMem).
+  for i := highI downto 0 do
+  begin
+    inc(j, psl.fragCnt); //nb: psl.fragCnt may be < 0 here!
+    if j > 0 then
+      GetMem(psl.fragments, j * SizeOf(TFragment));
+    {$IFDEF MemCheck} psl.total := j; {$ENDIF}
+    psl.fragCnt := 0; //reset for later
+    psl.minX := clipRight;
+    psl.maxX := 0;
+    psl.Y := i;
+    dec(psl);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure SplitEdgeIntoFragments(const pt1, pt2: TPointD;
+  const scanlines: TArrayOfScanline; const clipRec: TRect);
+var
+  x,y, dx,dy, absDx, dydx, dxdy: double;
+  i, scanlineY, maxY, maxX: integer;
+  psl: PScanLine;
+  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;
+
+  if absDx < 0.000001 then
+  begin
+    //VERTICAL EDGE
+    top.X := bot.X; //this circumvents v. rare rounding issues.
+
+    //exclude vertical edges that are outside the right clip region
+    //but still update maxX for each scanline the edge passes
+    if bot.X > maxX then
+    begin
+      for i := Min(maxY, Round(bot.Y)) downto Max(0, Round(top.Y)) do
+        scanlines[i].maxX := maxX;
+      Exit;
+    end;
+
+    dxdy := 0;
+    if dy > 0 then dydx := 1 else dydx := -1;
+  end else
+  begin
+    dxdy := dx/dy;
+    dydx := dy/absDx;
+  end;
+
+  //TRIM EDGES THAT CROSS CLIPPING BOUNDARIES (EXCEPT THE LEFT BOUNDARY)
+  if bot.X >= maxX then
+  begin
+    if top.X >= maxX then
+    begin
+      for i := Min(maxY, Round(bot.Y)) downto Max(0, Round(top.Y)) do
+        scanlines[i].maxX := maxX;
+      Exit;
+    end;
+    //here the edge must be oriented bottom-right to top-left
+    y := bot.Y - (bot.X - maxX) * Abs(dydx);
+    for i := Min(maxY, Round(bot.Y)) downto Max(0, Round(y)) do
+      scanlines[i].maxX := maxX;
+    bot.Y := y;
+    if bot.Y <= 0 then Exit;
+    bot.X := maxX;
+  end
+  else if top.X > maxX then
+  begin
+    //here the edge must be oriented bottom-left to top-right
+    y := top.Y + (top.X - maxX) * Abs(dydx);
+    for i := Min(maxY, Round(y)) downto Max(0, Round(top.Y)) do
+      scanlines[i].maxX := maxX;
+    top.Y := y;
+    if top.Y >= maxY then Exit;
+    top.X := maxX;
+  end;
+  if bot.Y > maxY then
+  begin
+    bot.X := bot.X + dxdy * (bot.Y - maxY);
+    if (bot.X > maxX) then Exit;        //nb: no clipping on the left
+    bot.Y := maxY;
+  end;
+  if top.Y < 0 then
+  begin
+    top.X := top.X + (dxdy * top.Y);
+    if (top.X > maxX) then Exit;        //nb: no clipping on the left
+    top.Y := 0;
+  end;
+
+  //SPLIT THE EDGE INTO MULTIPLE SCANLINE FRAGMENTS
+  scanlineY := Round(bot.Y);
+  if bot.Y = scanlineY then dec(scanlineY);
+
+  //at the lower-most extent of the edge 'split' the first fragment
+  if scanlineY < 0 then Exit;
+
+  psl := @scanlines[scanlineY];
+  if not assigned(psl.fragments) then Exit; //a very rare event
+  {$IFDEF MemCheck}
+  if psl.fragCnt = psl.total then raise Exception.Create(sMemCheckError);
+  {$ENDIF}
+
+  pFrag := @psl.fragments[psl.fragCnt];
+  inc(psl.fragCnt);
+
+  pFrag.botX := bot.X;
+  if scanlineY <= top.Y then
+  begin
+    //the whole edge is within 1 scanline
+    pFrag.topX := top.X;
+    pFrag.dy := bot.Y - top.Y;
+    pFrag.dydx := dydx;
+    Exit;
+  end;
+
+  x := bot.X + (bot.Y - scanlineY) * dxdy;
+  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 := @psl.fragments[psl.fragCnt];
+    inc(psl.fragCnt);
+    pFrag.botX := x;
+    x := x + dxdy;
+    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 := @psl.fragments[psl.fragCnt];
+  inc(psl.fragCnt);
+  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; const clipRec: TRect);
+var
+  i,j, highJ: integer;
+  pt1, pt2: PPointD;
+begin
+ for i := 0 to high(polygons) do
+  begin
+    highJ := high(polygons[i]);
+    if highJ < 2 then continue;
+    pt1 := @polygons[i][highJ];
+    pt2 := @polygons[i][0];
+    for j := 0 to highJ do
+    begin
+      SplitEdgeIntoFragments(pt1^, pt2^, scanlines, clipRec);
+      pt1 := pt2;
+      inc(pt2);
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure ProcessScanlineFragments(var scanline: TScanLine;
+  var buffer: TArrayOfDouble);
+var
+  i,j, leftXi,rightXi: integer;
+  fracX, yy, q, windDir: double;
+  pd: PDouble;
+  frag: PFragment;
+begin
+  frag := @scanline.fragments[0];
+  for i := 1 to scanline.fragCnt do
+  begin
+    if frag.botX > frag.topX then
+    begin
+      //just swapping botX and topX simplifies code
+      q := frag.botX;
+      frag.botX := frag.topX;
+      frag.topX  := q;
+    end;
+
+    leftXi := Max(0, Round(frag.botX));
+    rightXi := Max(0, Round(frag.topX));
+
+    if (leftXi = rightXi) then
+    begin
+      if frag.dydx < 0 then windDir := -1.0 else windDir := 1.0;
+      //the fragment is only one pixel wide
+      if leftXi < scanline.minX then
+        scanline.minX := leftXi;
+      if rightXi > scanline.maxX then
+        scanline.maxX := rightXi;
+      pd := @buffer[leftXi];
+      if (frag.botX <= 0) then
+      begin
+        pd^ := pd^ + frag.dy * windDir;
+      end else
+      begin
+        q := (frag.botX + frag.topX) * 0.5 - leftXi;
+        pd^ := pd^ + (1-q) * frag.dy * windDir;
+        inc(pd);
+        pd^ := pd^ + q * frag.dy * windDir;
+      end;
+    end else
+    begin
+      if leftXi < scanline.minX then
+        scanline.minX := leftXi;
+      if rightXi > scanline.maxX then
+        scanline.maxX := rightXi;
+      pd := @buffer[leftXi];
+      //left pixel
+      fracX := leftXi + 1 - frag.botX;
+      yy := frag.dydx * fracX;
+      q := fracX * yy * 0.5;
+      pd^ := pd^ + q;
+      q :=  yy - q;
+      inc(pd);
+      //middle pixels
+      for j := leftXi +1 to rightXi -1 do
+      begin
+        pd^ := pd^ + q + frag.dydx * 0.5;
+        q := frag.dydx * 0.5;
+        inc(pd);
+      end;
+      //right pixel
+      fracX := frag.topX - rightXi;
+      yy :=  fracX * frag.dydx;
+      pd^ := pd^ + q + (1 - fracX * 0.5) * yy;
+      inc(pd);
+      //overflow
+      pd^ := pd^ + fracX * 0.5 * yy;
+    end;
+    inc(frag);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+{$IFNDEF TROUNDINGMODE}
+type
+  TRoundingMode = {$IFNDEF FPC}Math.{$ENDIF}TFPURoundingMode;
+{$ENDIF}
+
+procedure Rasterize(const paths: TPathsD; const clipRec: TRect;
+  fillRule: TFillRule; renderer: TCustomRenderer);
+var
+  i,j, xli,xri, maxW, maxH, aa: integer;
+  clipRec2: TRect;
+  paths2: TPathsD;
+  accum: double;
+  windingAccum: TArrayOfDouble;
+  byteBuffer: TArrayOfByte;
+  scanlines: TArrayOfScanline;
+  scanline: PScanline;
+  savedRoundMode: TRoundingMode;
+begin
+  //See also https://nothings.org/gamedev/rasterize/
+  if not assigned(renderer) then Exit;
+  Types.IntersectRect(clipRec2, clipRec, GetBounds(paths));
+  if IsEmptyRect(clipRec2) then Exit;
+
+  paths2 := OffsetPath(paths, -clipRec2.Left, -clipRec2.Top);
+
+  //Delphi's Round() function is *much* faster than its Trunc function, and
+  //it's even a little faster than the __Trunc function above (except when
+  //the FastMM4 memory manager is enabled.)
+  savedRoundMode := SetRoundMode(rmDown);
+
+  RectWidthHeight(clipRec2, maxW, maxH);
+  SetLength(scanlines, maxH +1);
+  SetLength(windingAccum, maxW +2);
+  AllocateScanlines(paths2, scanlines, maxH, maxW-1);
+  InitializeScanlines(paths2, scanlines, clipRec2);
+  SetLength(byteBuffer, maxW);
+  if byteBuffer = nil then Exit;
+
+  scanline := @scanlines[0];
+  for i := 0 to high(scanlines) do
+  begin
+    if scanline.fragCnt = 0 then
+    begin
+      FreeMem(scanline.fragments);
+      inc(scanline);
+      Continue;
+    end;
+
+    //process each scanline to fill the winding count accumulation buffer
+    ProcessScanlineFragments(scanline^, 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
+            aa := Round(Abs(accum) * 1275) mod 2550;              // *5
+            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
+            byteBuffer[j] := Min(255, Round(Abs(accum) * 318));
+          end;
+{$IFDEF REVERSE_ORIENTATION}
+        frPositive:
+{$ELSE}
+        frNegative:
+{$ENDIF}
+          begin
+            if accum > 0.002 then
+              byteBuffer[j] := Min(255, Round(accum * 318));
+          end;
+{$IFDEF REVERSE_ORIENTATION}
+        frNegative:
+{$ELSE}
+        frPositive:
+{$ENDIF}
+          begin
+            if accum < -0.002 then
+              byteBuffer[j] := Min(255, Round(-accum * 318));
+          end;
+      end;
+    end;
+    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);
+    FreeMem(scanline.fragments);
+    inc(scanline);
+  end;
+  SetRoundMode(savedRoundMode);
+end;
+
+//------------------------------------------------------------------------------
+// TAbstractRenderer
+//------------------------------------------------------------------------------
+
+function TCustomRenderer.Initialize(imgBase: Pointer;
+  imgWidth, imgHeight, pixelSize: integer): Boolean;
+begin
+  fImgBase := imgBase;
+  fImgWidth := ImgWidth;
+  fImgHeight := ImgHeight;
+  fPixelSize := pixelSize;
+
+  fCurrLinePtr := fImgBase;
+  fCurrY       := 0;
+  result       := true;
+end;
+//------------------------------------------------------------------------------
+
+procedure TCustomRenderer.NotifyChange;
+begin
+  if assigned(fChangeProc) then fChangeProc;
+end;
+//------------------------------------------------------------------------------
+
+type THackedImage32 = class(TImage32); //exposes protected Changed method.
+
+function TCustomRenderer.Initialize(targetImage: TImage32): Boolean;
+begin
+  fChangeProc := THackedImage32(targetImage).Changed;
+  with targetImage do
+    result := Initialize(PixelBase, Width, Height, SizeOf(TColor32));
+end;
+//------------------------------------------------------------------------------
+
+function TCustomRenderer.GetDstPixel(x, y: integer): Pointer;
+begin
+  if (y <> fCurrY) then
+  begin
+    fCurrY := y;
+    fCurrLinePtr := fImgBase;
+    inc(PByte(fCurrLinePtr), fCurrY * fImgWidth * fPixelSize);
+  end;
+  Result := fCurrLinePtr;
+  inc(PByte(Result), x * fPixelSize);
+end;
+
+//------------------------------------------------------------------------------
+// TColorRenderer
+//------------------------------------------------------------------------------
+
+constructor TColorRenderer.Create(color: TColor32 = clNone32);
+begin
+  if color <> clNone32 then SetColor(color);
+end;
+//------------------------------------------------------------------------------
+
+function TColorRenderer.Initialize(targetImage: TImage32): Boolean;
+begin
+  //there's no point rendering if the color is fully transparent
+  result := (fAlpha > 0) and inherited Initialize(targetImage);
+end;
+//------------------------------------------------------------------------------
+
+procedure TColorRenderer.SetColor(value: TColor32);
+begin
+  fColor := value and $FFFFFF;
+  fAlpha := GetAlpha(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure TColorRenderer.RenderProc(x1, x2, y: integer; alpha: PByte);
+var
+  i: integer;
+  dst: PColor32;
+begin
+  dst := GetDstPixel(x1,y);
+  for i := x1 to x2 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);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TAliasedColorRenderer
+//------------------------------------------------------------------------------
+
+constructor TAliasedColorRenderer.Create(color: TColor32 = clNone32);
+begin
+  fColor := color;
+end;
+//------------------------------------------------------------------------------
+
+function TAliasedColorRenderer.Initialize(targetImage: TImage32): Boolean;
+begin
+  //there's no point rendering if the color is fully transparent
+  result := (GetAlpha(fColor) > 0) and
+    inherited Initialize(targetImage);
+end;
+//------------------------------------------------------------------------------
+
+procedure TAliasedColorRenderer.RenderProc(x1, x2, y: integer; alpha: PByte);
+var
+  i: integer;
+  dst: PColor32;
+begin
+  dst := GetDstPixel(x1,y);
+  for i := x1 to x2 do
+  begin
+    if Ord(alpha^) > 127 then dst^ := fColor; //ie no blending
+    inc(dst); inc(alpha);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TBrushImageRenderer
+//------------------------------------------------------------------------------
+
+constructor TImageRenderer.Create(tileFillStyle: TTileFillStyle;
+  brushImage: TImage32);
+begin
+  fImage := TImage32.Create(brushImage);
+  SetTileFillStyle(tileFillStyle);
+end;
+//------------------------------------------------------------------------------
+
+destructor TImageRenderer.Destroy;
+begin
+  fImage.Free;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImageRenderer.SetTileFillStyle(value: TTileFillStyle);
+begin
+  case value of
+    tfsRepeat: fBoundsProc := RepeatQ;
+    tfsMirrorHorz: fBoundsProc := MirrorQ;
+    tfsMirrorVert: fBoundsProc := RepeatQ;
+    tfsRotate180 : fBoundsProc := MirrorQ;
+  end;
+  fMirrorY := value in [tfsMirrorVert, tfsRotate180];
+end;
+//------------------------------------------------------------------------------
+
+function TImageRenderer.Initialize(targetImage: TImage32): Boolean;
+begin
+  result := inherited Initialize(targetImage) and (not fImage.IsEmpty);
+  if not result then Exit;
+  fLastYY := 0;
+  fBrushPixel := PARGB(fImage.PixelBase);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImageRenderer.RenderProc(x1, x2, y: integer; alpha: PByte);
+var
+  i: integer;
+  pDst: PColor32;
+  pBrush: PARGB;
+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
+  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;
+end;
+//------------------------------------------------------------------------------
+
+function TImageRenderer.GetFirstBrushPixel(x, y: integer): PARGB;
+begin
+  if fMirrorY then
+    y := MirrorQ(y, fImage.Height) else
+    y := RepeatQ(y, fImage.Height);
+  if y <> fLastYY then
+  begin
+    fBrushPixel := PARGB(fImage.PixelRow[y]);
+    fLastYY := y;
+  end;
+  x := fBoundsProc(x, fImage.Width);
+  result := GetPixel(fBrushPixel, x);
+end;
+
+//------------------------------------------------------------------------------
+// TGradientRenderer
+//------------------------------------------------------------------------------
+
+constructor TCustomGradientRenderer.Create;
+begin
+  fBoundsProc := ClampQ; //default proc
+end;
+//------------------------------------------------------------------------------
+
+procedure TCustomGradientRenderer.Clear;
+begin
+  fGradientColors := nil;
+  fColors := nil;
+end;
+//------------------------------------------------------------------------------
+
+procedure TCustomGradientRenderer.SetGradientFillStyle(value: TGradientFillStyle);
+begin
+  case value of
+    gfsClamp: fBoundsProc := ClampQ;
+    gfsMirror: fBoundsProc := MirrorQ;
+    else fBoundsProc := RepeatQ;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TCustomGradientRenderer.SetParameters(startColor, endColor: TColor32;
+  gradFillStyle: TGradientFillStyle = gfsClamp);
+begin
+  SetGradientFillStyle(gradFillStyle);
+  //reset gradient colors if perviously set
+  SetLength(fGradientColors, 2);
+  fGradientColors[0].offset := 0;
+  fGradientColors[0].color := startColor;
+  fGradientColors[1].offset := 1;
+  fGradientColors[1].color := endColor;
+end;
+//------------------------------------------------------------------------------
+
+procedure TCustomGradientRenderer.InsertColorStop(offsetFrac: double; color: TColor32);
+var
+  i, len: integer;
+  gradColor: TGradientColor;
+begin
+  len := Length(fGradientColors);
+  //colorstops can only be inserted after calling SetParameters
+  if len = 0 then Exit;
+
+  if offsetFrac < 0 then offsetFrac := 0
+  else if offsetFrac > 1 then offsetFrac := 1;
+
+  if offsetFrac = 0 then
+  begin
+    fGradientColors[0].color := color;
+    Exit;
+  end
+  else if offsetFrac = 1 then
+  begin
+    fGradientColors[len -1].color := color;
+    Exit;
+  end;
+  gradColor.offset := offsetFrac;
+  gradColor.color  := color;
+
+  i := 1;
+  while (i < len-1) and
+    (fGradientColors[i].offset <= offsetFrac) do inc(i);
+  SetLength(fGradientColors, len +1);
+  Move(fGradientColors[i],
+    fGradientColors[i+1], (len -i) * SizeOf(TGradientColor));
+  fGradientColors[i] := gradColor;
+end;
+
+//------------------------------------------------------------------------------
+// TLinearGradientRenderer
+//------------------------------------------------------------------------------
+
+procedure TLinearGradientRenderer.SetParameters(const startPt, endPt: TPointD;
+  startColor, endColor: TColor32; gradFillStyle: TGradientFillStyle);
+begin
+  inherited SetParameters(startColor, endColor, gradFillStyle);
+  fStartPt := startPt;
+  fEndPt := endPt;
+end;
+//------------------------------------------------------------------------------
+
+function TLinearGradientRenderer.Initialize(targetImage: TImage32): Boolean;
+var
+  i: integer;
+  dx,dy, dxdy,dydx: double;
+begin
+  result := inherited Initialize(targetImage) and assigned(fGradientColors);
+  if not result then Exit;
+
+  if abs(fEndPt.Y - fStartPt.Y) > abs(fEndPt.X - fStartPt.X) then
+  begin
+    //gradient > 45 degrees
+    if (fEndPt.Y < fStartPt.Y) then
+    begin
+      fGradientColors := ReverseColors(fGradientColors);
+      SwapPoints(fStartPt, fEndPt);
+    end;
+    fIsVert := true;
+    dx := (fEndPt.X - fStartPt.X);
+    dy := (fEndPt.Y - fStartPt.Y);
+    dxdy := dx/dy;
+
+    fColorsCnt := Ceil(dy + dxdy * (fEndPt.X - fStartPt.X));
+    fColors := MakeColorGradient(fGradientColors, fColorsCnt);
+    //get a list of perpendicular offsets for each
+    SetLength(fPerpendicOffsets, ImgWidth);
+    //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
+      fPerpendicOffsets[i] := Round(dxdy * (fStartPt.X - i) + fStartPt.Y);
+  end
+  else //gradient <= 45 degrees
+  begin
+    if (fEndPt.X = fStartPt.X) then
+    begin
+      Result := false;
+      Exit;
+    end;
+    if (fEndPt.X < fStartPt.X) then
+    begin
+      fGradientColors := ReverseColors(fGradientColors);
+      SwapPoints(fStartPt, fEndPt);
+    end;
+    fIsVert := false;
+    dx := (fEndPt.X - fStartPt.X);
+    dy := (fEndPt.Y - fStartPt.Y);
+    dydx := dy/dx; //perpendicular slope
+
+    fColorsCnt := Ceil(dx + dydx * (fEndPt.Y - fStartPt.Y));
+    fColors := MakeColorGradient(fGradientColors, fColorsCnt);
+    SetLength(fPerpendicOffsets, ImgHeight);
+    //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
+      fPerpendicOffsets[i] := Round(dydx * (fStartPt.Y - i) + fStartPt.X);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TLinearGradientRenderer.RenderProc(x1, x2, y: integer; alpha: PByte);
+var
+  i, off: integer;
+  pDst: PColor32;
+  color: TARGB;
+begin
+  pDst := GetDstPixel(x1,y);
+  for i := x1 to x2 do
+  begin
+    if fIsVert then
+    begin
+      //when fIsVert = true, fPerpendicOffsets is an array of Y for each X
+      off := fPerpendicOffsets[i];
+      color.Color := fColors[fBoundsProc(y - off, fColorsCnt)];
+    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)];
+    end;
+    pDst^ := BlendToAlpha(pDst^,
+      MulBytes(color.A, Ord(alpha^)) shl 24 or (color.Color and $FFFFFF));
+    inc(pDst); inc(alpha);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TRadialGradientRenderer
+//------------------------------------------------------------------------------
+
+function TRadialGradientRenderer.Initialize(targetImage: TImage32): Boolean;
+begin
+  result := inherited Initialize(targetImage) and (fColorsCnt > 1);
+  if result then
+    fColors := MakeColorGradient(fGradientColors, fColorsCnt);
+end;
+//------------------------------------------------------------------------------
+
+procedure TRadialGradientRenderer.SetParameters(const focalRect: TRect;
+  innerColor, outerColor: TColor32;
+  gradientFillStyle: TGradientFillStyle);
+var
+  w,h: integer;
+  radX,radY: double;
+begin
+  inherited SetParameters(innerColor, outerColor, gradientFillStyle);
+  fColorsCnt := 0;
+  if IsEmptyRect(focalRect) then Exit;
+
+  fCenterPt.X  := (focalRect.Left + focalRect.Right) * 0.5;
+  fCenterPt.Y  := (focalRect.Top + focalRect.Bottom) * 0.5;
+  RectWidthHeight(focalRect, w, h);
+  radX    :=  w * 0.5;
+  radY    :=  h * 0.5;
+  if radX >= radY then
+  begin
+    fScaleX     := 1;
+    fScaleY     := radX/radY;
+    fColorsCnt := Ceil(radX) +1;
+  end else
+  begin
+    fScaleX     := radY/radX;
+    fScaleY     := 1;
+    fColorsCnt := Ceil(radY) +1;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TRadialGradientRenderer.RenderProc(x1, x2, y: integer; alpha: PByte);
+var
+  i: integer;
+  dist: double;
+  color: TARGB;
+  pDst: PColor32;
+begin
+  pDst := GetDstPixel(x1,y);
+  for i := x1 to x2 do
+  begin
+    dist := Hypot((y - fCenterPt.Y) *fScaleY, (i - fCenterPt.X) *fScaleX);
+    color.Color := fColors[fBoundsProc(Round(dist), fColorsCnt)];
+    pDst^ := BlendToAlpha(pDst^,
+      MulBytes(color.A, Ord(alpha^)) shl 24 or (color.Color and $FFFFFF));
+    inc(pDst); inc(alpha);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgRadialGradientRenderer
+//------------------------------------------------------------------------------
+
+function TSvgRadialGradientRenderer.Initialize(targetImage: TImage32): Boolean;
+begin
+  result := inherited Initialize(targetImage) and (fColorsCnt > 1);
+  if result then
+    fColors := MakeColorGradient(fGradientColors, fColorsCnt);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgRadialGradientRenderer.SetParameters(const ellipseRect: TRect;
+  const focus: TPoint; innerColor, outerColor: TColor32;
+  gradientFillStyle: TGradientFillStyle = gfsClamp);
+var
+  w, h  : integer;
+begin
+  inherited SetParameters(innerColor, outerColor);
+  case gradientFillStyle of
+    gfsMirror: fBoundsProcD := MirrorD;
+    gfsRepeat: fBoundsProcD := RepeatD;
+    else fBoundsProcD := ClampD;
+  end;
+
+  fColorsCnt := 0;
+  if IsEmptyRect(ellipseRect) then Exit;
+
+  fCenterPt  := RectD(ellipseRect).MidPoint;
+  RectWidthHeight(ellipseRect, w, h);
+  fA    := w * 0.5;
+  fB    := h * 0.5;
+
+  fFocusPt.X := focus.X - fCenterPt.X;
+  fFocusPt.Y := focus.Y - fCenterPt.Y;
+  fColorsCnt := Ceil(Hypot(fA*2, fB*2)) +1;
+  fAA := fA * fA;
+  fBB := fB * fB;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgRadialGradientRenderer.RenderProc(x1, x2, y: integer; alpha: PByte);
+var
+  i: integer;
+  q,m,c, qa,qb,qc,qs: double;
+  dist, dist2: double;
+  color: TARGB;
+  pDst: PColor32;
+  pt, ellipsePt: TPointD;
+begin
+  //get the left-most pixel to render
+  pDst := GetDstPixel(x1,y);
+  pt.X := x1 - fCenterPt.X; pt.Y := y - fCenterPt.Y;
+  for i := x1 to x2 do
+  begin
+    //equation of ellipse = (x*x)/aa + (y*y)/bb = 1
+    //equation of line = y = mx + c;
+    if (pt.X = fFocusPt.X) then //vertical line
+    begin
+      //let x = pt.X, then y*y = b*b(1 - Sqr(pt.X)/aa)
+      q := Sqrt(fBB*(1 - Sqr(pt.X)/fAA));
+      ellipsePt.X := pt.X;
+      if pt.Y >= fFocusPt.Y then
+        ellipsePt.Y := q else
+        ellipsePt.Y := -q;
+      dist := abs(pt.Y - fFocusPt.Y);
+      dist2 := abs(ellipsePt.Y - fFocusPt.Y);
+      if dist2 = 0 then
+        q := 1 else
+        q := dist/ dist2;
+    end else
+    begin
+      //using simultaneous equations and substitution
+      //given y = mx + c
+      m := (pt.Y - fFocusPt.Y)/(pt.X - fFocusPt.X);
+      c := pt.Y - m * pt.X;
+      //given (x*x)/aa + (y*y)/bb = 1
+      //(x*x)/aa*bb + (y*y) = bb
+      //bb/aa *(x*x) + Sqr(m*x +c) = bb
+      //bb/aa *(x*x) + (m*m)*(x*x) + 2*m*x*c +c*c = b*b
+      //(bb/aa +(m*m)) *(x*x) + 2*m*c*(x) + (c*c) - bb = 0
+      //solving quadratic equation
+      qa := (fBB/fAA +(m*m));
+      qb := 2*m*c;
+      qc := (c*c) - fBB;
+      qs := (qb*qb) - 4*qa*qc;
+      if qs >= 0 then
+      begin
+        qs := Sqrt(qs);
+        if pt.X <= fFocusPt.X then
+          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);
+        if dist2 = 0 then
+          q := 1 else
+          q := 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));
+    inc(pDst); pt.X := pt.X + 1; inc(alpha);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TEraseRenderer
+//------------------------------------------------------------------------------
+
+procedure TEraseRenderer.RenderProc(x1, x2, y: integer; alpha: PByte);
+var
+  i: integer;
+  dst: PARGB;
+begin
+  dst := PARGB(GetDstPixel(x1,y));
+  for i := x1 to x2 do
+  begin
+    {$IFDEF PBYTE}
+    dst.A := MulBytes(dst.A, not alpha^);
+    {$ELSE}
+    dst.A := MulBytes(dst.A, not Ord(alpha^));
+    {$ENDIF}
+    inc(dst); inc(alpha);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TInverseRenderer
+//------------------------------------------------------------------------------
+
+procedure TInverseRenderer.RenderProc(x1, x2, y: integer; alpha: PByte);
+var
+  i: integer;
+  dst: PARGB;
+  c: TARGB;
+begin
+  dst := PARGB(GetDstPixel(x1,y));
+  for i := x1 to x2 do
+  begin
+    c.Color := not dst.Color;
+    c.A := MulBytes(dst.A, Ord(alpha^));
+    dst.Color := BlendToAlpha(dst.Color, c.Color);
+    inc(dst); inc(alpha);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+
+procedure TBarycentricRenderer.SetParameters(const a, b, c: TPointD;
+  c1, c2, c3: TColor32);
+begin
+
+  self.a := a;
+  self.c1.Color := c1;
+  self.c2.Color := c2;
+  self.c3.Color := c3;
+
+  v0.X := b.X - a.X;
+  v0.Y := b.Y - a.Y;
+  v1.X := c.X - a.X;
+  v1.Y := c.Y - a.Y;
+  d00 := (v0.X * v0.X + v0.Y * v0.Y);
+  d01 := (v0.X * v1.X + v0.Y * v1.Y);
+  d11 := (v1.X * v1.X + v1.Y * v1.Y);
+  invDenom := 1/(d00 * d11 - d01 * d01);
+end;
+//------------------------------------------------------------------------------
+
+function TBarycentricRenderer.GetColor(const pt: TPointD): TColor32;
+var
+  v2: TPointD;
+  d20, d21, v, w, u: Double;
+  res: TARGB absolute Result;
+begin
+  Result := 0;
+  v2.X := pt.X - a.X;
+  v2.Y := pt.Y - a.Y;
+  d20 := (v2.X * v0.X + v2.Y * v0.Y);
+  d21 := (v2.X * v1.X + v2.Y * v1.Y);
+
+  v := (d11 * d20 - d01 * d21) * invDenom;
+  w := (d00 * d21 - d01 * d20) * invDenom;
+  u := 1.0 - v - w;
+
+  Res.A := ClampByte(c1.A * u + c2.A * v + c3.A * w);
+  Res.R := ClampByte(c1.R * u + c2.R * v + c3.R * w);
+  Res.G := ClampByte(c1.G * u + c2.G * v + c3.G * w);
+  Res.B := ClampByte(c1.B * u + c2.B * v + c3.B * w);
+end;
+//------------------------------------------------------------------------------
+
+procedure TBarycentricRenderer.RenderProc(x1, x2, y: integer; alpha: PByte);
+var
+  x: integer;
+  p: PARGB;
+  c: TARGB;
+begin
+  p := PARGB(fImgBase);
+  inc(p, y * ImgWidth + x1);
+  for x := x1 to x2 do
+  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;
+end;
+
+//------------------------------------------------------------------------------
+// Draw functions
+//------------------------------------------------------------------------------
+
+procedure DrawPoint(img: TImage32;
+  const pt: TPointD; radius: double; color: TColor32);
+var
+  path: TPathD;
+begin
+  if radius <= 1 then
+    path := Rectangle(pt.X-radius, pt.Y-radius, pt.X+radius, pt.Y+radius) else
+    path := Ellipse(RectD(pt.X-radius, pt.Y-radius, pt.X+radius, pt.Y+radius));
+  DrawPolygon(img, path, frEvenOdd, color);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawPoint(img: TImage32; const pt: TPointD;
+  radius: double; renderer: TCustomRenderer);
+var
+  path: TPathD;
+begin
+  path := Ellipse(RectD(pt.X -radius, pt.Y -radius, pt.X +radius, pt.Y +radius));
+  DrawPolygon(img, path, frEvenOdd, renderer);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawPoint(img: TImage32; const points: TPathD;
+  radius: double; color: TColor32);
+var
+  i: integer;
+begin
+  for i := 0 to high(points) do
+    DrawPoint(img, points[i], radius, color);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawPoint(img: TImage32; const paths: TPathsD;
+  radius: double; color: TColor32);
+var
+  i: integer;
+begin
+  for i := 0 to high(paths) do
+    DrawPoint(img, paths[i], radius, color);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawLine(img: TImage32;
+  const pt1, pt2: TPointD; lineWidth: double; color: TColor32);
+var
+  lines: TPathsD;
+begin
+  setLength(lines, 1);
+  setLength(lines[0], 2);
+  lines[0][0] := pt1;
+  lines[0][1] := pt2;
+  DrawLine(img, lines, lineWidth, color, esRound);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawLine(img: TImage32; const line: TPathD; lineWidth: double;
+  color: TColor32; endStyle: TEndStyle; joinStyle: TJoinStyle;
+  miterLimit: double);
+var
+  lines: TPathsD;
+begin
+  setLength(lines, 1);
+  lines[0] := line;
+  DrawLine(img, lines, lineWidth, color, endStyle, joinStyle, miterLimit);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawLine(img: TImage32; const line: TPathD; lineWidth: double;
+  renderer: TCustomRenderer; endStyle: TEndStyle; joinStyle: TJoinStyle;
+  miterLimit: double);
+var
+  lines: TPathsD;
+begin
+  setLength(lines, 1);
+  lines[0] := line;
+  DrawLine(img, lines, lineWidth, renderer, endStyle, joinStyle, miterLimit);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawInvertedLine(img: TImage32; const line: TPathD;
+lineWidth: double; endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto);
+var
+  lines: TPathsD;
+begin
+  setLength(lines, 1);
+  lines[0] := line;
+  DrawInvertedLine(img, lines, lineWidth, endStyle, joinStyle);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawLine(img: TImage32; const lines: TPathsD;
+  lineWidth: double; color: TColor32;
+  endStyle: TEndStyle; joinStyle: TJoinStyle; miterLimit: double);
+var
+  lines2: TPathsD;
+  cr: TCustomRenderer;
+begin
+  if not assigned(lines) then exit;
+  if (lineWidth < MinStrokeWidth) then lineWidth := MinStrokeWidth;
+  lines2 := Outline(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;
+  finally
+    cr.free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawLine(img: TImage32; const lines: TPathsD;
+  lineWidth: double; renderer: TCustomRenderer;
+  endStyle: TEndStyle; joinStyle: TJoinStyle;
+  miterLimit: double);
+var
+  lines2: TPathsD;
+begin
+  if (not assigned(lines)) or (not assigned(renderer)) then exit;
+  if (lineWidth < MinStrokeWidth) then lineWidth := MinStrokeWidth;
+  lines2 := Outline(lines, lineWidth, joinStyle, endStyle, miterLimit);
+  if renderer.Initialize(img) then
+  begin
+    Rasterize(lines2, img.bounds, frNonZero, renderer);
+    renderer.NotifyChange;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawInvertedLine(img: TImage32;
+  const lines: TPathsD; lineWidth: double;
+  endStyle: TEndStyle; joinStyle: TJoinStyle = jsAuto);
+var
+  lines2: TPathsD;
+  ir: TInverseRenderer;
+begin
+  if not assigned(lines) then exit;
+  if (lineWidth < MinStrokeWidth) then lineWidth := MinStrokeWidth;
+  lines2 := Outline(lines, lineWidth, joinStyle, endStyle, 2);
+  ir := TInverseRenderer.Create;
+  try
+    if ir.Initialize(img) then
+    begin
+      Rasterize(lines2, img.bounds, frNonZero, ir);
+      ir.NotifyChange;
+    end;
+  finally
+    ir.free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawDashedLine(img: TImage32; const line: TPathD;
+  dashPattern: TArrayOfInteger; patternOffset: PDouble; lineWidth: double;
+  color: TColor32; endStyle: TEndStyle; joinStyle: TJoinStyle);
+var
+  lines: TPathsD;
+  cr: TColorRenderer;
+  i: integer;
+begin
+  if (lineWidth < MinStrokeWidth) then lineWidth := MinStrokeWidth;
+  if not assigned(line) then exit;
+
+  for i := 0 to High(dashPattern) do
+    if dashPattern[i] <= 0 then dashPattern[i] := 1;
+
+  lines := GetDashedPath(line, endStyle = esPolygon, dashPattern, patternOffset);
+  if Length(lines) = 0 then Exit;
+  case joinStyle of
+    jsSquare, jsMiter:
+      endStyle := esSquare;
+    jsRound:
+      endStyle := esRound;
+    else
+      endStyle := esButt;
+  end;
+  lines := Outline(lines, lineWidth, joinStyle, endStyle);
+  cr := TColorRenderer.Create(color);
+  try
+    if cr.Initialize(img) then
+    begin
+      Rasterize(lines, img.bounds, frNonZero, cr);
+      cr.NotifyChange;
+    end;
+  finally
+    cr.free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawDashedLine(img: TImage32; const lines: TPathsD;
+  dashPattern: TArrayOfInteger; patternOffset: PDouble; lineWidth: double;
+  color: TColor32; endStyle: TEndStyle; joinStyle: TJoinStyle);
+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);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawDashedLine(img: TImage32; const line: TPathD;
+  dashPattern: TArrayOfInteger; patternOffset: PDouble; lineWidth: double;
+  renderer: TCustomRenderer; endStyle: TEndStyle; joinStyle: TJoinStyle);
+var
+  i: integer;
+  lines: TPathsD;
+begin
+  if (not assigned(line)) or (not assigned(renderer)) then exit;
+  if (lineWidth < MinStrokeWidth) then lineWidth := MinStrokeWidth;
+
+  for i := 0 to High(dashPattern) do
+    if dashPattern[i] <= 0 then dashPattern[i] := 1;
+
+  lines := GetDashedPath(line, endStyle = esPolygon, dashPattern, patternOffset);
+  if Length(lines) = 0 then Exit;
+  lines := Outline(lines, lineWidth, joinStyle, endStyle);
+  if renderer.Initialize(img) then
+  begin
+    Rasterize(lines, img.bounds, frNonZero, renderer);
+    renderer.NotifyChange;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawDashedLine(img: TImage32; const lines: TPathsD;
+  dashPattern: TArrayOfInteger; patternOffset: PDouble; lineWidth: double;
+  renderer: TCustomRenderer; endStyle: TEndStyle; joinStyle: TJoinStyle);
+var
+  i: integer;
+begin
+  if not assigned(lines) then exit;
+  for i := 0 to high(lines) do
+    DrawDashedLine(img, lines[i],
+      dashPattern, patternOffset, lineWidth, renderer, endStyle, joinStyle);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawInvertedDashedLine(img: TImage32;
+  const line: TPathD; dashPattern: TArrayOfInteger;
+  patternOffset: PDouble; lineWidth: double; endStyle: TEndStyle;
+  joinStyle: TJoinStyle = jsAuto);
+var
+  i: integer;
+  lines: TPathsD;
+  renderer: TInverseRenderer;
+begin
+  if not assigned(line) then exit;
+  if (lineWidth < MinStrokeWidth) then lineWidth := MinStrokeWidth;
+
+  for i := 0 to High(dashPattern) do
+    if dashPattern[i] <= 0 then dashPattern[i] := 1;
+
+  lines := GetDashedPath(line, endStyle = esPolygon, dashPattern, patternOffset);
+  if Length(lines) = 0 then Exit;
+  lines := Outline(lines, lineWidth, joinStyle, endStyle);
+  renderer := TInverseRenderer.Create;
+  try
+    if renderer.Initialize(img) then
+    begin
+      Rasterize(lines, img.bounds, frNonZero, renderer);
+      renderer.NotifyChange;
+    end;
+  finally
+    renderer.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawInvertedDashedLine(img: TImage32;
+  const lines: TPathsD; dashPattern: TArrayOfInteger;
+  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, lines[i],
+      dashPattern, patternOffset, lineWidth, endStyle, joinStyle);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawPolygon(img: TImage32; const polygon: TPathD;
+  fillRule: TFillRule; color: TColor32);
+var
+  polygons: TPathsD;
+begin
+  if not assigned(polygon) then exit;
+  setLength(polygons, 1);
+  polygons[0] := polygon;
+  DrawPolygon(img, polygons, fillRule, color);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawPolygon(img: TImage32; const polygon: TPathD;
+  fillRule: TFillRule; renderer: TCustomRenderer);
+var
+  polygons: TPathsD;
+begin
+  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;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawPolygon(img: TImage32; const polygons: TPathsD;
+  fillRule: TFillRule; color: TColor32);
+var
+  cr: TCustomRenderer;
+begin
+  if not assigned(polygons) then exit;
+  if img.AntiAliased then
+    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;
+  finally
+    cr.free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawPolygon(img: TImage32; const polygons: TPathsD;
+  fillRule: TFillRule; renderer: TCustomRenderer);
+begin
+  if (not assigned(polygons)) or (not assigned(renderer)) then exit;
+  if renderer.Initialize(img) then
+  begin
+    Rasterize(polygons, img.bounds, fillRule, renderer);
+    renderer.NotifyChange;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawPolygon_ClearType(img: TImage32; const polygons: TPathsD;
+  fillRule: TFillRule; color: TColor32; backColor: TColor32);
+var
+  w, h: integer;
+  tmpImg: TImage32;
+  rec: TRect;
+  tmpPolygons: TPathsD;
+  cr: TColorRenderer;
+begin
+  if not assigned(polygons) then exit;
+
+  rec := GetBounds(polygons);
+  RectWidthHeight(rec, w, h);
+  tmpImg := TImage32.Create(w *3, h);
+  try
+    tmpPolygons := OffsetPath(polygons, -rec.Left, -rec.Top);
+    tmpPolygons := ScalePath(tmpPolygons, 3, 1);
+    cr := TColorRenderer.Create(clBlack32);
+    try
+      if cr.Initialize(tmpImg) then
+        Rasterize(tmpPolygons, tmpImg.bounds, fillRule, cr);
+    finally
+      cr.Free;
+    end;
+    ApplyClearType(tmpImg, color, backColor);
+    img.CopyBlend(tmpImg, tmpImg.Bounds, rec, BlendToAlpha);
+  finally
+    tmpImg.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure ErasePolygon(img: TImage32; const polygon: TPathD;
+  fillRule: TFillRule);
+var
+  polygons: TPathsD;
+begin
+  if not assigned(polygon) then exit;
+  setLength(polygons, 1);
+  polygons[0] := polygon;
+  ErasePolygon(img, polygons, fillRule);
+end;
+//------------------------------------------------------------------------------
+
+procedure ErasePolygon(img: TImage32; const polygons: TPathsD;
+  fillRule: TFillRule);
+var
+  er: TEraseRenderer;
+begin
+  er := TEraseRenderer.Create;
+  try
+    if er.Initialize(img) then
+    begin
+      Rasterize(polygons, img.bounds, fillRule, er);
+      er.NotifyChange;
+    end;
+  finally
+    er.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawBoolMask(img: TImage32; const mask: TArrayOfByte; color: TColor32);
+var
+  i, len: integer;
+  pc: PColor32;
+  pb: PByte;
+begin
+  len := Length(mask);
+  if (len = 0) or (len <> img.Width * img.Height) then Exit;
+  pc := img.PixelBase;
+  pb := @mask[0];
+  for i := 0 to len -1 do
+  begin
+    {$IFDEF PBYTE}
+    if pb^ > 0 then
+    {$ELSE}
+    if pb^ > #0 then
+    {$ENDIF}
+      pc^ := color else
+      pc^ := clNone32;
+    inc(pc); inc(pb);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawAlphaMask(img: TImage32; const mask: TArrayOfByte; color: TColor32);
+var
+  i, len: integer;
+  pc: PColor32;
+  pb: PByte;
+begin
+  len := Length(mask);
+  if (len = 0) or (len <> img.Width * img.Height) then Exit;
+  color := color and $FFFFFF; //strip alpha value
+  pc := img.PixelBase;
+  pb := @mask[0];
+  for i := 0 to len -1 do
+  begin
+    {$IFDEF PBYTE}
+    if pb^ > 0 then
+      pc^ := color or pb^ shl 24 else
+      pc^ := clNone32;
+    {$ELSE}
+    if pb^ > #0 then
+      pc^ := color or Ord(pb^) shl 24 else
+      pc^ := clNone32;
+    {$ENDIF}
+    inc(pc); inc(pb);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+end.

+ 3070 - 0
components/Image32/source/Img32.Extra.pas

@@ -0,0 +1,3070 @@
+unit Img32.Extra;
+
+(*******************************************************************************
+* Author    :  Angus Johnson                                                   *
+* Version   :  4.3                                                             *
+* Date      :  27 September 2022                                               *
+* Website   :  http://www.angusj.com                                           *
+* Copyright :  Angus Johnson 2019-2021                                         *
+*                                                                              *
+* Purpose   :  Miscellaneous routines for TImage32 that                        *
+*           :  don't obviously belong in other modules.                        *
+*                                                                              *
+* License   :  Use, modification & distribution is subject to                  *
+*              Boost Software License Ver 1                                    *
+*              http://www.boost.org/LICENSE_1_0.txt                            *
+*******************************************************************************)
+
+interface
+{$I Img32.inc}
+
+uses
+  SysUtils, Classes, Math, Types,
+  Img32, Img32.Draw, Img32.Vector;
+
+type
+  TButtonShape = (bsRound, bsSquare, bsDiamond);
+  TButtonAttribute = (baShadow, ba3D, baEraseBeneath);
+  TButtonAttributes = set of TButtonAttribute;
+
+type
+  PPt = ^TPt;
+  TPt = record
+    pt   : TPointD;
+    vec  : TPointD;
+    len  : double;
+    next : PPt;
+    prev : PPt;
+  end;
+
+  TFitCurveContainer = class
+  private
+    ppts      : PPt;
+    solution  : TPathD;
+    tolSqrd   : double;
+    function Count(first, last: PPt): integer;
+    function AddPt(const pt: TPointD): PPt;
+    procedure Clear;
+    function ComputeLeftTangent(p: PPt): TPointD;
+    function ComputeRightTangent(p: PPt): TPointD;
+    function ComputeCenterTangent(p: PPt): TPointD;
+    function ChordLengthParameterize(
+      first: PPt; cnt: integer): TArrayOfDouble;
+    function GenerateBezier(first, last: PPt; cnt: integer;
+      const u: TArrayOfDouble; const firstTan, lastTan: TPointD): TPathD;
+    function Reparameterize(first: PPt; cnt: integer;
+      const u: TArrayOfDouble; const bezier: TPathD): TArrayOfDouble;
+    function NewtonRaphsonRootFind(const q: TPathD;
+      const pt: TPointD; u: double): double;
+    function ComputeMaxErrorSqrd(first, last: PPt;
+      const bezier: TPathD; const u: TArrayOfDouble;
+      out SplitPoint: PPt): double;
+    function FitCubic(first, last: PPt;
+      firstTan, lastTan: TPointD): Boolean;
+    procedure AppendSolution(const bezier: TPathD);
+  public
+    function FitCurve(const path: TPathD; closed: Boolean;
+      tolerance: double; minSegLength: double): TPathD;
+  end;
+
+procedure DrawEdge(img: TImage32; const rec: TRect;
+  topLeftColor, bottomRightColor: TColor32; penWidth: double = 1.0); overload;
+procedure DrawEdge(img: TImage32; const rec: TRectD;
+  topLeftColor, bottomRightColor: TColor32; penWidth: double = 1.0); overload;
+procedure DrawEdge(img: TImage32; const path: TPathD;
+  topLeftColor, bottomRightColor: TColor32;
+  penWidth: double = 1.0; closePath: Boolean = true); overload;
+
+//DrawShadowRect: is **much** faster than DrawShadow
+procedure DrawShadowRect(img: TImage32; const rec: TRect; depth: double;
+  angle: double = angle45; color: TColor32 = $80000000);
+procedure DrawShadow(img: TImage32; const polygon: TPathD;
+  fillRule: TFillRule; depth: double; angleRads: double = angle45;
+  color: TColor32 = $80000000; cutoutInsideShadow: Boolean = false); overload;
+procedure DrawShadow(img: TImage32; const polygons: TPathsD;
+  fillRule: TFillRule; depth: double; angleRads: double = angle45;
+  color: TColor32 = $80000000; cutoutInsideShadow: Boolean = false); overload;
+
+procedure DrawGlow(img: TImage32; const polygon: TPathD;
+  fillRule: TFillRule; color: TColor32; blurRadius: integer); overload;
+procedure DrawGlow(img: TImage32; const polygons: TPathsD;
+  fillRule: TFillRule; color: TColor32; blurRadius: integer); overload;
+
+procedure TileImage(img: TImage32; const rec: TRect; tile: TImage32); overload;
+procedure TileImage(img: TImage32; const rec: TRect; tile: TImage32; const tileRec: TRect); overload;
+
+//FloodFill: If no CompareFunc is provided, FloodFill will fill whereever
+//adjoining pixels exactly match the starting pixel - Point(x,y).
+procedure FloodFill(img: TImage32; x, y: Integer; newColor: TColor32;
+  tolerance: Byte = 0; compareFunc: TCompareFunctionEx = nil);
+
+procedure FastGaussianBlur(img: TImage32;
+  const rec: TRect; stdDev: integer; repeats: integer); overload;
+procedure FastGaussianBlur(img: TImage32;
+  const rec: TRect; stdDevX, stdDevY: integer; repeats: integer); overload;
+
+procedure GaussianBlur(img: TImage32; rec: TRect; radius: Integer);
+
+//Emboss: A smaller radius is sharper. Increasing depth increases contrast.
+//Luminance changes grayscale balance (unless preserveColor = true)
+procedure Emboss(img: TImage32; radius: Integer = 1; depth: Integer = 10;
+  luminance: Integer = 75; preserveColor: Boolean = false);
+
+//Sharpen: Radius range is 1 - 10; amount range is 1 - 50.<br>
+//see https://en.wikipedia.org/wiki/Unsharp_masking
+procedure Sharpen(img: TImage32; radius: Integer = 2; amount: Integer = 10);
+
+//HatchBackground: Assumes the current image is semi-transparent.
+procedure HatchBackground(img: TImage32; color1: TColor32 = clWhite32;
+  color2: TColor32= $FFE8E8E8; hatchSize: Integer = 10); overload;
+procedure HatchBackground(img: TImage32; const rec: TRect;
+  color1: TColor32 = clWhite32; color2: TColor32= $FFE8E8E8;
+  hatchSize: Integer = 10); overload;
+
+procedure GridBackground(img: TImage32; majorInterval, minorInterval: integer;
+  fillColor: TColor32 = clWhite32;
+  majColor: TColor32 = $30000000; minColor: TColor32 = $20000000);
+
+procedure ReplaceColor(img: TImage32; oldColor, newColor: TColor32);
+
+//EraseColor: Removes the specified color from the image, even from
+//pixels that are a blend of colors including the specified color.<br>
+//see https://stackoverflow.com/questions/9280902/
+procedure EraseColor(img: TImage32; color: TColor32);
+
+//RedEyeRemove: Removes 'red eye' from flash photo images.
+procedure RedEyeRemove(img: TImage32; const rect: TRect);
+
+procedure PencilEffect(img: TImage32; intensity: integer = 0);
+
+procedure TraceContours(img: TImage32; intensity: integer);
+
+procedure EraseInsidePath(img: TImage32;
+  const path: TPathD; fillRule: TFillRule);
+procedure EraseInsidePaths(img: TImage32;
+  const paths: TPathsD; fillRule: TFillRule);
+
+procedure EraseOutsidePath(img: TImage32; const path: TPathD;
+  fillRule: TFillRule; const outsideBounds: TRect);
+procedure EraseOutsidePaths(img: TImage32; const paths: TPathsD;
+  fillRule: TFillRule; const outsideBounds: TRect);
+
+procedure Draw3D(img: TImage32; const polygon: TPathD;
+  fillRule: TFillRule; height, blurRadius: double;
+  colorLt: TColor32 = $DDFFFFFF; colorDk: TColor32 = $80000000;
+  angleRads: double = angle225); overload;
+procedure Draw3D(img: TImage32; const polygons: TPathsD;
+  fillRule: TFillRule; height, blurRadius: double;
+  colorLt: TColor32 = $DDFFFFFF; colorDk: TColor32 = $80000000;
+  angleRads: double = angle225); overload;
+
+function RainbowColor(fraction: double): TColor32;
+function GradientColor(color1, color2: TColor32; frac: single): TColor32;
+function MakeDarker(color: TColor32; percent: cardinal): TColor32;
+function MakeLighter(color: TColor32; percent: cardinal): TColor32;
+
+function DrawButton(img: TImage32; const pt: TPointD;
+  size: double; color: TColor32 = clNone32;
+  buttonShape: TButtonShape = bsRound;
+  buttonAttributes: TButtonAttributes = [baShadow, ba3D, baEraseBeneath]): TPathD;
+
+//Vectorize: convert an image into polygon vectors
+function Vectorize(img: TImage32; compareColor: TColor32;
+  compareFunc: TCompareFunction; colorTolerance: Integer;
+  roundingTolerance: integer = 2): TPathsD;
+
+function VectorizeMask(const mask: TArrayOfByte; maskWidth: integer): TPathsD;
+
+// RamerDouglasPeucker: simplifies paths, recursively removing vertices where
+// they deviate no more than 'epsilon' from their adjacent vertices.
+function RamerDouglasPeucker(const path: TPathD;
+  epsilon: double): TPathD; overload;
+function RamerDouglasPeucker(const paths: TPathsD;
+  epsilon: double): TPathsD; overload;
+
+// SmoothToCubicBezier - produces a series of cubic bezier control points.
+// This function is very useful in the following combination:
+// RamerDouglasPeucker(), SmoothToCubicBezier(), FlattenCBezier().
+function SmoothToCubicBezier(const path: TPathD;
+  pathIsClosed: Boolean; maxOffset: integer = 0): TPathD;
+
+//InterpolatePoints: smooths a simple line chart.
+//Points should be left to right and equidistant along the X axis
+function InterpolatePoints(const points: TPathD; tension: integer = 0): TPathD;
+
+function GetFloodFillMask(imgIn, imgMaskOut: TImage32; x, y: Integer;
+  tolerance: Byte; compareFunc: TCompareFunctionEx): Boolean;
+
+procedure SymmetricCropTransparent(img: TImage32);
+
+//3 additional blend functions (see TImage32.CopyBlend)
+function BlendAverage(bgColor, fgColor: TColor32): TColor32;
+function BlendLinearBurn(bgColor, fgColor: TColor32): TColor32;
+function BlendColorDodge(bgColor, fgColor: TColor32): TColor32;
+
+//CurveFit: this function is based on -
+//"An Algorithm for Automatically Fitting Digitized Curves"
+//by Philip J. Schneider in "Graphics Gems", Academic Press, 1990
+//Smooths out many very closely positioned points
+//tolerance range: 1..10 where 10 == max tolerance.
+
+function CurveFit(const path: TPathD; closed: Boolean;
+  tolerance: double; minSegLength: double = 2): TPathD; overload;
+function CurveFit(const paths: TPathsD; closed: Boolean;
+  tolerance: double; minSegLength: double = 2): TPathsD; overload;
+
+implementation
+
+uses
+  {$IFNDEF MSWINDOWS}
+  {$IFNDEF FPC}
+  Img32.FMX,
+  {$ENDIF}
+  {$ENDIF}
+  Img32.Transform;
+
+const
+  FloodFillDefaultRGBTolerance: byte = 64;
+  MaxBlur = 100;
+
+type
+  PColor32Array = ^TColor32Array;
+  TColor32Array = array [0.. maxint div SizeOf(TColor32) -1] of TColor32;
+  PWeightedColorArray = ^TWeightedColorArray;
+  TWeightedColorArray = array [0.. $FFFFFF] of TWeightedColor;
+
+//------------------------------------------------------------------------------
+// Miscellaneous functions
+//------------------------------------------------------------------------------
+
+function GetSymmetricCropTransparentRect(img: TImage32): TRect;
+var
+  w,h, x,y, x1,y1: Integer;
+  p1,p2: PARGB;
+  opaquePxlFound: Boolean;
+begin
+  Result := img.Bounds;
+  w := img.Width;
+  y1 := 0;
+  opaquePxlFound := false;
+  for y := 0 to (img.Height div 2) -1 do
+  begin
+    p1 := PARGB(img.PixelRow[y]);
+    p2 := PARGB(img.PixelRow[img.Height - y -1]);
+    for x := 0 to w -1 do
+    begin
+      if (p1.A > 0) or (p2.A > 0) then
+      begin
+        y1 := y;
+        opaquePxlFound := true;
+        break;
+      end;
+      inc(p1); inc(p2);
+    end;
+    if opaquePxlFound then break;
+  end;
+  // probably safeset not to resize empty images
+  if not opaquePxlFound then Exit;
+  if y1 > 0 then
+  begin
+    inc(Result.Top, y1);
+    dec(Result.Bottom, y1);
+  end;
+  x1 := 0;
+  h := RectHeight(Result);
+  opaquePxlFound := false;
+  for x := 0 to (w div 2) -1 do
+  begin
+    p1 := PARGB(@img.Pixels[Result.Top * w + x]);
+    p2 := PARGB(@img.Pixels[Result.Top * w + (w -1) - x]);
+    for y := 0 to h -1 do
+    begin
+      if (p1.A > 0) or (p2.A > 0) then
+      begin
+        x1 := x;
+        opaquePxlFound := true;
+        break;
+      end;
+      inc(p1, w); inc(p2, w);
+    end;
+    if opaquePxlFound then break;
+  end;
+  if not opaquePxlFound then Exit;
+  inc(Result.Left, x1);
+  dec(Result.Right, x1);
+end;
+//------------------------------------------------------------------------------
+
+//SymmetricCropTransparent: after cropping, the image's midpoint
+//will be the same pixel as before cropping. (Important for rotating.)
+procedure SymmetricCropTransparent(img: TImage32);
+var
+  rec: TRect;
+begin
+  rec := GetSymmetricCropTransparentRect(img);
+  if (rec.Top > 0) or (rec.Left > 0) then img.Crop(rec);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawEdge(img: TImage32; const rec: TRect;
+  topLeftColor, bottomRightColor: TColor32; penWidth: double = 1.0);
+begin
+  DrawEdge(img, RectD(rec), topLeftColor, bottomRightColor, penWidth);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawEdge(img: TImage32; const rec: TRectD;
+  topLeftColor, bottomRightColor: TColor32; penWidth: double = 1.0);
+var
+  p: TPathD;
+  c: TColor32;
+begin
+  if penWidth = 0 then Exit
+  else if penWidth < 0 then
+  begin
+    c := topLeftColor;
+    topLeftColor := bottomRightColor;
+    bottomRightColor := c;
+    penWidth := -penWidth;
+  end;
+  if topLeftColor <> bottomRightColor then
+  begin
+    with rec do
+    begin
+      p := Img32.Vector.MakePath([left, bottom, left, top, right, top]);
+      DrawLine(img, p, penWidth, topLeftColor, esButt);
+      p := Img32.Vector.MakePath([right, top, right, bottom, left, bottom]);
+      DrawLine(img, p, penWidth, bottomRightColor, esButt);
+    end;
+  end else
+    DrawLine(img, Rectangle(rec), penWidth, topLeftColor, esPolygon);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawEdge(img: TImage32; const path: TPathD;
+  topLeftColor, bottomRightColor: TColor32;
+  penWidth: double = 1.0; closePath: Boolean = true);
+var
+  i, highI, deg: integer;
+  frac: double;
+  c: TColor32;
+  p: TPathD;
+const
+  RadToDeg = 180/PI;
+begin
+  if penWidth = 0 then Exit
+  else if penWidth < 0 then
+  begin
+    c := topLeftColor;
+    topLeftColor := bottomRightColor;
+    bottomRightColor := c;
+    penWidth := -penWidth;
+  end;
+  highI := high(path);
+  if highI < 2 then Exit;
+  p := path;
+  if closePath and not PointsNearEqual(p[0], p[highI], 0.01) then
+  begin
+    AppendPath(p, p[0]);
+    inc(highI);
+  end;
+  for i := 1 to highI do
+  begin
+    deg := Round(GetAngle(p[i-1], p[i]) * RadToDeg);
+    case deg of
+      -180..-136: frac := (-deg-135)/45;
+      -135..0   : frac := 0;
+      1..44     : frac := deg/45;
+      else        frac := 1;
+    end;
+    c := GradientColor(topLeftColor, bottomRightColor, frac);
+    DrawLine(img, p[i-1], p[i], penWidth, c);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure FillColorHorz(img: TImage32; x, endX, y: integer; color: TColor32);
+var
+  i,dx: integer;
+  p: PColor32;
+begin
+  if (x < 0) or (x >= img.Width) then Exit;
+  if (y < 0) or (y >= img.Height) then Exit;
+  p := img.PixelRow[y]; inc(p, x);
+  if endX >= img.Width then endX := img.Width -1
+  else if endX < 0 then endX := 0;
+  if endX < x then dx := -1 else dx := 1;
+  for i := 0 to Abs(x-endX) do
+  begin
+    p^ := color;
+    inc(p, dx);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure FillColorVert(img: TImage32; x, y, endY: integer; color: TColor32);
+var
+  i, dy: integer;
+  p: PColor32;
+begin
+  if (x < 0) or (x >= img.Width) then Exit;
+  if (y < 0) or (y >= img.Height) then Exit;
+  p := img.PixelRow[y]; inc(p, x);
+  if endY >= img.Height then
+    endY := img.Height -1 else if endY < 0 then endY := 0;
+  dy := img.Width;
+  if endY < y then dy := -dy;
+  for i := 0 to Abs(y - endY) do
+  begin
+    p^ := color;
+    inc(p, dy);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawShadowRect(img: TImage32; const rec: TRect; depth: double;
+  angle: double = angle45; color: TColor32 = $80000000);
+var
+  i,j, sX,sY: integer;
+  l,t,r,b: integer;
+  tmpImg: TImage32;
+  tmpRec: TRect;
+  xx,yy: double;
+  ss: TPointD;
+  c: TColor32;
+begin
+  GetSinCos(angle, yy, xx);
+  ss.X := depth * xx;
+  ss.Y := depth * yy;
+  sX := Abs(Round(ss.X));
+  sY := Abs(Round(ss.Y));
+  if rec.Left + ss.X < 0 then ss.X := -rec.Left
+  else if rec.Right + ss.X > img.Width then ss.X := img.Width - rec.Right -1;
+  if rec.Top + ss.Y < 0 then ss.Y := -rec.Top
+  else if rec.Bottom + ss.Y > img.Height then ss.Y := img.Height -rec.Bottom -1;
+  tmpImg  := TImage32.Create(sX*3 +1, sY*3 +1);
+  try
+    i := sX div 2; j := sY div 2;
+    DrawPolygon(tmpImg, Rectangle(i,j,i+sX*2,j+sY*2), frNonZero, color);
+    FastGaussianBlur(tmpImg, tmpImg.Bounds, Round(sX/4),Round(sY/4), 1);
+    // t-l corner
+    if (ss.X < 0) or (ss.Y < 0) then
+    begin
+      tmpRec := Rect(0, 0, sX, sY);
+      l := rec.Left; t := rec.Top;
+      if ss.X < 0 then dec(l, sX);
+      if ss.Y < 0 then dec(t, sY);
+      img.Copy(tmpImg, tmpRec, Rect(l,t,l+sX,t+sY));
+    end;
+    // t-r corner
+    if (ss.X > 0) or (ss.Y < 0) then
+    begin
+      tmpRec := Rect(sX*2+1, 0, sX*3+1, sY);
+      l := rec.Right; t := rec.Top;
+      if ss.X < 0 then dec(l, sX);
+      if ss.Y < 0 then dec(t, sY);
+      img.Copy(tmpImg, tmpRec, Rect(l,t,l+sX,t+sY));
+    end;
+    // b-l corner
+    if (ss.X < 0) or (ss.Y > 0) then
+    begin
+      tmpRec := Rect(0, sY*2+1, sX, sY*3+1);
+      l := rec.Left; t := rec.Bottom;
+      if ss.X < 0 then dec(l, sX);
+      if ss.Y < 0 then dec(t, sY);
+      img.Copy(tmpImg, tmpRec, Rect(l,t,l+sX,t+sY));
+    end;
+    // b-r corner
+    if (ss.X > 0) or (ss.Y > 0) then
+    begin
+      tmpRec := Rect(sX*2+1, sY*2+1, sX*3+1, sY*3+1);
+      l := rec.Right; t := rec.Bottom;
+      if ss.X < 0 then dec(l, sX);
+      if ss.Y < 0 then dec(t, sY);
+      img.Copy(tmpImg, tmpRec, Rect(l,t,l+sX,t+sY));
+    end;
+    // l-edge
+    if (ss.X < 0) then
+    begin
+      l := rec.Left; t := rec.Top+sY; b := rec.Bottom-1;
+      if ss.Y < 0 then begin dec(t, sY); dec(b,sY); end;
+      for i := 1 to sX do
+      begin
+        c := tmpImg.Pixel[sX-i, sY+1];
+        FillColorVert(img, l-i, t, b, c);
+      end;
+    end;
+    // t-edge
+    if (ss.Y < 0) then
+    begin
+      l := rec.Left+sX; r := rec.Right-1; t := rec.Top;
+      if ss.X < 0 then begin dec(l, sX); dec(r,sX); end;
+      for i := 1 to sY do
+      begin
+        c := tmpImg.Pixel[sX+1, sY-i];
+        FillColorHorz(img, l, r, t-i, c);
+      end;
+    end;
+    // r-edge
+    if (ss.X > 0) then
+    begin
+      r := rec.Right-1; t := rec.Top+sY; b := rec.Bottom-1;
+      if ss.Y < 0 then begin dec(t, sY); dec(b,sY); end;
+      for i := 1 to sX do
+      begin
+        c := tmpImg.Pixel[sX*2+i, sY+1];
+        FillColorVert(img, r+i, t, b, c);
+      end;
+    end;
+    // b-edge
+    if (ss.Y > 0) then
+    begin
+      l := rec.Left+sX; r := rec.Right-1; b := rec.Bottom-1;
+      if ss.X < 0 then begin dec(l, sX); dec(r,sX); end;
+      for i := 1 to sY do
+      begin
+        c := tmpImg.Pixel[sX+1, sY*2+i];
+        FillColorHorz(img, l, r, b+i, c);
+      end;
+    end;
+  finally
+    tmpImg.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawShadow(img: TImage32; const polygon: TPathD;
+  fillRule: TFillRule; depth: double; angleRads: double;
+  color: TColor32; cutoutInsideShadow: Boolean);
+var
+  polygons: TPathsD;
+begin
+  setlength(polygons, 1);
+  polygons[0] := polygon;
+  DrawShadow(img, polygons, fillRule, depth,
+    angleRads, color, cutoutInsideShadow);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawShadow(img: TImage32; const polygons: TPathsD;
+  fillRule: TFillRule; depth: double; angleRads: double;
+  color: TColor32; cutoutInsideShadow: Boolean);
+var
+  x, y: double;
+  blurSize, w,h: integer;
+  rec: TRect;
+  polys, shadowPolys: TPathsD;
+  shadowImg: TImage32;
+begin
+  rec := GetBounds(polygons);
+  if IsEmptyRect(rec) or (depth < 1) then Exit;
+  if not ClockwiseRotationIsAnglePositive then angleRads := -angleRads;
+  NormalizeAngle(angleRads);
+  GetSinCos(angleRads, y, x);
+  depth := depth * 0.5;
+  x := depth * x;
+  y := depth * y;
+  blurSize := Max(1,Round(depth / 2));
+  Img32.Vector.InflateRect(rec, Ceil(depth*2), Ceil(depth*2));
+  polys := OffsetPath(polygons, -rec.Left, -rec.Top);
+  shadowPolys := OffsetPath(polys, x, y);
+  RectWidthHeight(rec, w, h);
+  shadowImg := TImage32.Create(w, h);
+  try
+    DrawPolygon(shadowImg, shadowPolys, fillRule, color);
+    FastGaussianBlur(shadowImg, shadowImg.Bounds, blurSize, 1);
+    if cutoutInsideShadow then EraseInsidePaths(shadowImg, polys, fillRule);
+    img.CopyBlend(shadowImg, shadowImg.Bounds, rec, BlendToAlpha);
+  finally
+    shadowImg.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawGlow(img: TImage32; const polygon: TPathD;
+  fillRule: TFillRule; color: TColor32; blurRadius: integer);
+var
+  polygons: TPathsD;
+begin
+  setlength(polygons, 1);
+  polygons[0] := polygon;
+  DrawGlow(img, polygons, fillRule, color, blurRadius);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawGlow(img: TImage32; const polygons: TPathsD;
+  fillRule: TFillRule; color: TColor32; blurRadius: integer);
+var
+  w,h: integer;
+  rec: TRect;
+  glowPolys: TPathsD;
+  glowImg: TImage32;
+begin
+  rec := GetBounds(polygons);
+  glowPolys := OffsetPath(polygons,
+    blurRadius -rec.Left +1, blurRadius -rec.Top +1);
+  Img32.Vector.InflateRect(rec, blurRadius +1, blurRadius +1);
+  RectWidthHeight(rec, w, h);
+  glowImg := TImage32.Create(w, h);
+  try
+    DrawPolygon(glowImg, glowPolys, fillRule, color);
+    FastGaussianBlur(glowImg, glowImg.Bounds, blurRadius, 2);
+    glowImg.ScaleAlpha(4);
+    img.CopyBlend(glowImg, glowImg.Bounds, rec, BlendToAlpha);
+  finally
+    glowImg.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TileImage(img: TImage32; const rec: TRect; tile: TImage32);
+begin
+  TileImage(img, rec, tile, tile.Bounds);
+end;
+//------------------------------------------------------------------------------
+
+procedure TileImage(img: TImage32;
+  const rec: TRect; tile: TImage32; const tileRec: TRect);
+var
+  i, dstW, dstH, srcW, srcH, cnt: integer;
+  dstRec, srcRec: TRect;
+begin
+  if tile.IsEmpty or IsEmptyRect(tileRec) then Exit;
+  RectWidthHeight(rec, dstW,dstH);
+  RectWidthHeight(tileRec, srcW, srcH);
+  cnt := Ceil(dstW / srcW);
+  dstRec := Img32.Vector.Rect(rec.Left, rec.Top,
+    rec.Left + srcW, rec.Top + srcH);
+  for i := 1 to cnt do
+  begin
+    img.Copy(tile, tileRec, dstRec);
+    Types.OffsetRect(dstRec, srcW, 0);
+  end;
+  cnt := Ceil(dstH / srcH) -1;
+  srcRec := Img32.Vector.Rect(rec.Left, rec.Top,
+    rec.Right, rec.Top + srcH);
+  dstRec := srcRec;
+  for i := 1 to cnt do
+  begin
+    Types.OffsetRect(dstRec, 0, srcH);
+    img.Copy(img, srcRec, dstRec);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Sharpen(img: TImage32; radius: Integer; amount: Integer);
+var
+  i: Integer;
+  amt: double;
+  weightAmount: array [-255 .. 255] of Integer;
+  bmpBlur: TImage32;
+  pColor, pBlur: PARGB;
+begin
+  if radius = 0 then Exit;
+  amt := ClampRange(amount/10, 0.1, 5);
+  radius := ClampRange(radius, 1, 10);
+  for i := -255 to 255 do
+    weightAmount[i] := Round(amt * i);
+  bmpBlur := TImage32.Create(img); // clone self
+  try
+    pColor := PARGB(img.pixelBase);
+    FastGaussianBlur(bmpBlur, bmpBlur.Bounds, radius, 2);
+    pBlur := PARGB(bmpBlur.pixelBase);
+    for i := 1 to img.Width * img.Height do
+    begin
+      if (pColor.A > 0) then
+      begin
+        pColor.R := ClampByte(pColor.R  + weightAmount[pColor.R - pBlur.R]);
+        pColor.G := ClampByte(pColor.G  + weightAmount[pColor.G - pBlur.G]);
+        pColor.B := ClampByte(pColor.B  + weightAmount[pColor.B - pBlur.B]);
+      end;
+      Inc(pColor); Inc(pBlur);
+    end;
+  finally
+    bmpBlur.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure HatchBackground(img: TImage32; const rec: TRect;
+  color1: TColor32 = clWhite32; color2: TColor32= $FFE8E8E8;
+  hatchSize: Integer = 10); overload;
+var
+  i,j: Integer;
+  pc: PColor32;
+  colors: array[boolean] of TColor32;
+  hatch: Boolean;
+begin
+  colors[false] := color1;
+  colors[true] := color2;
+  img.BeginUpdate;
+  try
+    for i := rec.Top to rec.Bottom -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
+      begin
+        if (j + 1) mod hatchSize = 0 then hatch := not hatch;
+        pc^ := BlendToOpaque(pc^, colors[hatch]);
+       inc(pc);
+      end;
+    end;
+  finally
+    img.EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure HatchBackground(img: TImage32;
+  color1: TColor32; color2: TColor32; hatchSize: Integer);
+begin
+  HatchBackground(img, img.Bounds, color1, color2, hatchSize);
+end;
+//------------------------------------------------------------------------------
+
+procedure GridBackground(img: TImage32; majorInterval, minorInterval: integer;
+  fillColor: TColor32; majColor: TColor32; minColor: TColor32);
+var
+  i, x,y, w,h: integer;
+  path: TPathD;
+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
+    begin
+      Img32.Draw.DrawLine(img, path, 1, majColor, 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, majColor, esSquare);
+      path[0].Y := path[0].Y + majorInterval;
+      path[1].Y := path[1].Y + majorInterval;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function ColorDifference(color1, color2: TColor32): cardinal;
+  {$IFDEF INLINE} inline; {$ENDIF}
+var
+  c1: TARGB absolute color1;
+  c2: TARGB absolute color2;
+begin
+  result := Abs(c1.R - c2.R) + Abs(c1.G - c2.G) + Abs(c1.B - c2.B);
+  result := (result * 341) shr 10; // divide by 3
+end;
+//------------------------------------------------------------------------------
+
+procedure ReplaceColor(img: TImage32; oldColor, newColor: TColor32);
+var
+  color: PColor32;
+  i: Integer;
+begin
+  color := img.PixelBase;
+  for i := 0 to img.Width * img.Height -1 do
+  begin
+    if color^ = oldColor then color^ := newColor;
+    inc(color);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure EraseColor(img: TImage32; color: TColor32);
+var
+  fg: TARGB absolute color;
+  bg: PARGB;
+  i: Integer;
+  Q: byte;
+begin
+  if fg.A = 0 then Exit;
+  bg := PARGB(img.PixelBase);
+  for i := 0 to img.Width * img.Height -1 do
+  begin
+    if bg.A > 0 then
+    begin
+      if (bg.R > fg.R) then Q := DivTable[bg.R - fg.R, not fg.R]
+      else if (bg.R < fg.R) then Q := DivTable[fg.R - bg.R, fg.R]
+      else Q := 0;
+      if (bg.G > fg.G) then Q := Max(Q, DivTable[bg.G - fg.G, not fg.G])
+      else if (bg.G < fg.G) then Q := Max(Q, DivTable[fg.G - bg.G, fg.G]);
+      if (bg.B > fg.B) then Q := Max(Q, DivTable[bg.B - fg.B, not fg.B])
+      else if (bg.B < fg.B) then Q := Max(Q, DivTable[fg.B - bg.B, fg.B]);
+      if (Q > 0) then
+      begin
+        bg.A := MulTable[bg.A, Q];
+        bg.R := DivTable[bg.R - MulTable[fg.R, not Q], Q];
+        bg.G := DivTable[bg.G - MulTable[fg.G, not Q], Q];
+        bg.B := DivTable[bg.B - MulTable[fg.B, not Q], Q];
+      end else
+        bg.Color := clNone32;
+    end;
+    inc(bg);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure RedEyeRemove(img: TImage32; const rect: TRect);
+var
+  k: integer;
+  cutout, mask: TImage32;
+  path: TPathD;
+  cutoutRec, rect3: TRect;
+  radGrad: TRadialGradientRenderer;
+begin
+  k := RectWidth(rect) * RectHeight(rect);
+  if k < 120 then k := 2
+  else if k < 230 then k := 3
+  else k := 4;
+  cutoutRec := rect;
+  Img32.Vector.InflateRect(cutoutRec, k, k);
+  cutout  := TImage32.Create(img, cutoutRec);
+  mask    := TImage32.Create(cutout.Width, cutout.Height);
+  radGrad := TRadialGradientRenderer.Create;
+  try
+    // fill behind the cutout with black also
+    // blurring the fill to soften its edges
+    rect3 := cutout.Bounds;
+    Img32.Vector.InflateRect(rect3, -k, -k);
+    path := Ellipse(rect3);
+    DrawPolygon(mask, path, frNonZero, clBlack32);
+    // given the very small area and small radius of the blur, the
+    // speed improvement of BoxBlur over GaussianBlur is inconsequential.
+    GaussianBlur(mask, mask.Bounds, k);
+    img.CopyBlend(mask, mask.Bounds, cutoutRec, BlendToOpaque);
+    // gradient fill to clNone32 a mask to soften cutout's edges
+    path := Ellipse(cutoutRec);
+    radGrad.SetParameters(rect3, clBlack32, clNone32);
+    DrawPolygon(mask, path, frNonZero, radGrad);
+    cutout.CopyBlend(mask, mask.Bounds, cutout.Bounds, BlendMask);
+    // now remove red from the cutout
+    EraseColor(cutout, clRed32);
+    // finally replace the cutout ...
+    img.CopyBlend(cutout, cutout.Bounds, cutoutRec, BlendToOpaque);
+  finally
+    mask.Free;
+    cutout.Free;
+    radGrad.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure EraseInsidePath(img: TImage32; const path: TPathD; fillRule: TFillRule);
+begin
+  if assigned(path) then
+    ErasePolygon(img, path, fillRule);
+end;
+//------------------------------------------------------------------------------
+
+procedure EraseInsidePaths(img: TImage32; const paths: TPathsD; fillRule: TFillRule);
+begin
+  if assigned(paths) then
+    ErasePolygon(img, paths, fillRule);
+end;
+//------------------------------------------------------------------------------
+
+procedure EraseOutsidePath(img: TImage32; const path: TPathD;
+  fillRule: TFillRule; const outsideBounds: TRect);
+var
+  mask: TImage32;
+  p: TPathD;
+  w,h: integer;
+begin
+  if not assigned(path) then Exit;
+  RectWidthHeight(outsideBounds, w,h);
+  mask := TImage32.Create(w, h);
+  try
+    p := OffsetPath(path, -outsideBounds.Left, -outsideBounds.top);
+    DrawPolygon(mask, p, fillRule, clBlack32);
+    img.CopyBlend(mask, mask.Bounds, outsideBounds, BlendMask);
+  finally
+    mask.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure EraseOutsidePaths(img: TImage32; const paths: TPathsD;
+  fillRule: TFillRule; const outsideBounds: TRect);
+var
+  mask: TImage32;
+  pp: TPathsD;
+  w,h: integer;
+begin
+  if not assigned(paths) then Exit;
+  RectWidthHeight(outsideBounds, w,h);
+  mask := TImage32.Create(w, h);
+  try
+    pp := OffsetPath(paths, -outsideBounds.Left, -outsideBounds.top);
+    DrawPolygon(mask, pp, fillRule, clBlack32);
+    img.CopyBlend(mask, mask.Bounds, outsideBounds, BlendMask);
+  finally
+    mask.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Draw3D(img: TImage32; const polygon: TPathD;
+  fillRule: TFillRule; height, blurRadius: double;
+  colorLt: TColor32; colorDk: TColor32; angleRads: double);
+var
+  polygons: TPathsD;
+begin
+  setLength(polygons, 1);
+  polygons[0] := polygon;
+  Draw3D(img, polygons, fillRule, height, blurRadius, colorLt, colorDk, angleRads);
+end;
+//------------------------------------------------------------------------------
+
+procedure Draw3D(img: TImage32; const polygons: TPathsD;
+  fillRule: TFillRule; height, blurRadius: double;
+  colorLt: TColor32; colorDk: TColor32; angleRads: double);
+var
+  tmp: TImage32;
+  rec: TRect;
+  paths, paths2: TPathsD;
+  w,h: integer;
+  x,y: double;
+begin
+  rec := GetBounds(polygons);
+  if IsEmptyRect(rec) then Exit;
+  if not ClockwiseRotationIsAnglePositive then angleRads := -angleRads;
+  GetSinCos(angleRads, y, x);
+  paths := OffsetPath(polygons, -rec.Left, -rec.Top);
+  RectWidthHeight(rec, w, h);
+  tmp := TImage32.Create(w, h);
+  try
+    if GetAlpha(colorLt) > 0 then
+    begin
+      tmp.Clear(colorLt);
+      paths2 := OffsetPath(paths, -height*x, -height*y);
+      EraseInsidePaths(tmp, paths2, fillRule);
+      FastGaussianBlur(tmp, tmp.Bounds, Round(blurRadius), 0);
+      EraseOutsidePaths(tmp, paths, fillRule, tmp.Bounds);
+      img.CopyBlend(tmp, tmp.Bounds, rec, BlendToAlpha);
+    end;
+    if GetAlpha(colorDk) > 0 then
+    begin
+      tmp.Clear(colorDk);
+      paths2 := OffsetPath(paths, height*x, height*y);
+      EraseInsidePaths(tmp, paths2, fillRule);
+      FastGaussianBlur(tmp, tmp.Bounds, Round(blurRadius), 0);
+      EraseOutsidePaths(tmp, paths, fillRule, tmp.Bounds);
+      img.CopyBlend(tmp, tmp.Bounds, rec, BlendToAlpha);
+    end;
+  finally
+    tmp.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function RainbowColor(fraction: double): TColor32;
+var
+  hsl: THsl;
+begin
+  if (fraction > 0) and (fraction < 1) then
+  begin
+    hsl.hue := Round(fraction * 255);
+    hsl.sat := 255;
+    hsl.lum := 255;
+    hsl.alpha := 255;
+    Result := HslToRgb(hsl);
+  end else
+    result := clRed32
+end;
+//------------------------------------------------------------------------------
+
+function GradientColor(color1, color2: TColor32; frac: single): TColor32;
+var
+  hsl1, hsl2: THsl;
+begin
+  if (frac <= 0) then result := color1
+  else if (frac >= 1) then result := color2
+  else
+  begin
+    hsl1 := RgbToHsl(color1); hsl2 := RgbToHsl(color2);
+    hsl1.hue := ClampByte(hsl1.hue*(1-frac) + hsl2.hue*frac);
+    hsl1.sat := ClampByte(hsl1.sat*(1-frac) + hsl2.sat*frac);
+    hsl1.lum := ClampByte(hsl1.lum*(1-frac) + hsl2.lum*frac);
+    hsl1.alpha := ClampByte(hsl1.alpha*(1-frac) + hsl2.alpha*frac);
+    Result := HslToRgb(hsl1);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function MakeDarker(color: TColor32; percent: cardinal): TColor32;
+var
+  hsl: THsl;
+begin
+  hsl := RgbToHsl(color);
+  hsl.lum := ClampByte(hsl.lum - (percent/100 * hsl.lum));
+  Result := HslToRgb(hsl);
+end;
+//------------------------------------------------------------------------------
+
+function MakeLighter(color: TColor32; percent: cardinal): TColor32;
+var
+  hsl: THsl;
+begin
+  hsl := RgbToHsl(color);
+  hsl.lum := ClampByte(hsl.lum + percent/100 * (255 - hsl.lum));
+  Result := HslToRgb(hsl);
+end;
+//------------------------------------------------------------------------------
+
+function DrawButton(img: TImage32; const pt: TPointD;
+  size: double; color: TColor32; buttonShape: TButtonShape;
+  buttonAttributes: TButtonAttributes): TPathD;
+var
+  i: integer;
+  radius: double;
+  rec: TRectD;
+  lightSize, lightAngle: double;
+begin
+  if (size < 5) then Exit;
+  radius := size * 0.5;
+  lightSize := radius * 0.25;
+  rec := RectD(pt.X -radius, pt.Y -radius, pt.X +radius, pt.Y +radius);
+  if baEraseBeneath in buttonAttributes then
+    img.Clear(Rect(rec));
+  case buttonShape of
+    bsDiamond:
+      begin
+        SetLength(Result, 4);
+        for i := 0 to 3 do Result[i] := pt;
+        Result[0].X := Result[0].X -radius;
+        Result[1].Y := Result[1].Y -radius;
+        Result[2].X := Result[2].X +radius;
+        Result[3].Y := Result[3].Y +radius;
+      end;
+    bsSquare:
+      begin
+        Img32.Vector.InflateRect(rec, -1,-1);
+        Result := Rectangle(rec);
+      end;
+    else
+      Result := Ellipse(rec);
+  end;
+  lightAngle := angle225;
+  img.BeginUpdate;
+  try
+    // nb: only need to cutout the inside shadow if
+    // the pending color fill is semi-transparent
+    if baShadow in buttonAttributes then
+      DrawShadow(img, Result, frNonZero, lightSize *2,
+        (lightAngle + angle180), $AA000000, GetAlpha(color) < $FE);
+    if GetAlpha(color) > 2 then
+      DrawPolygon(img, Result, frNonZero, color);
+    if ba3D in buttonAttributes then
+      Draw3D(img, Result, frNonZero, lightSize*2,
+        Ceil(lightSize), $CCFFFFFF, $AA000000, lightAngle);
+    DrawLine(img, Result, dpiAware1, clBlack32, esPolygon);
+  finally
+    img.EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function AlphaAverage(color1, color2: TColor32): cardinal;
+  {$IFDEF INLINE} inline; {$ENDIF}
+var
+  c1: TARGB absolute color1;
+  c2: TARGB absolute color2;
+begin
+  result := (c1.A + c2.A) shr 1;
+end;
+//------------------------------------------------------------------------------
+
+function BlendAverage(bgColor, fgColor: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute fgColor;
+begin
+  res.A := (fg.A + bg.A) shr 1;
+  res.R := (fg.R + bg.R) shr 1;
+  res.G := (fg.G + bg.G) shr 1;
+  res.B := (fg.B + bg.B) shr 1;
+end;
+//------------------------------------------------------------------------------
+
+function BlendLinearBurn(bgColor, fgColor: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute fgColor;
+begin
+  res.A := 255;
+  res.R := Max(0, bg.R + fg.R - 255);
+  res.G := Max(0, bg.G + fg.G - 255);
+  res.B := Max(0, bg.B + fg.B - 255);
+end;
+//------------------------------------------------------------------------------
+
+function BlendColorDodge(bgColor, fgColor: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute fgColor;
+begin
+  res.A := 255;
+  res.R := DivTable[bg.R, not fg.R];
+  res.G := DivTable[bg.G, not fg.G];
+  res.B := DivTable[bg.B, not fg.B];
+end;
+//------------------------------------------------------------------------------
+
+procedure PencilEffect(img: TImage32; intensity: integer);
+var
+  img2: TImage32;
+begin
+  if img.IsEmpty then Exit;
+  intensity := max(1, min(10, intensity));
+  img.Grayscale;
+  img2 := TImage32.Create(img);
+  try
+    img2.InvertColors;
+    FastGaussianBlur(img2, img2.Bounds, intensity, 2);
+    img.CopyBlend(img2, img2.Bounds, img.Bounds, BlendColorDodge);
+  finally
+    img2.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TraceContours(img: TImage32; intensity: integer);
+var
+  i,j, w,h: integer;
+  tmp, tmp2: TArrayOfColor32;
+  s, s2: PColor32;
+  d: PARGB;
+begin
+  w := img.Width; h := img.Height;
+  if w * h = 0 then Exit;
+  SetLength(tmp, w * h);
+  SetLength(tmp2, w * h);
+  s := img.PixelRow[0]; d := @tmp[0];
+  for j := 0 to h-1 do
+  begin
+    s2 := IncPColor32(s, 1);
+    for i := 0 to w-2 do
+    begin
+      d.A := ColorDifference(s^, s2^);
+      inc(s); inc(s2); inc(d);
+    end;
+    inc(s); inc(d);
+  end;
+  for j := 0 to w-1 do
+  begin
+    s := @tmp[j]; d := @tmp2[j];
+    s2 := IncPColor32(s, w);
+    for i := 0 to h-2 do
+    begin
+      d.A := AlphaAverage(s^, s2^);
+      inc(s, w); inc(s2, w); inc(d, w);
+    end;
+  end;
+  Move(tmp2[0], img.PixelBase^, w * h * sizeOf(TColor32));
+  if intensity < 1 then Exit;
+  if intensity > 10 then
+    intensity := 10; // range = 1-10
+  img.ScaleAlpha(intensity);
+end;
+//------------------------------------------------------------------------------
+// FLOODFILL - AND SUPPORT FUNCTIONS
+//------------------------------------------------------------------------------
+type
+  PFloodFillRec = ^TFloodFillRec;
+  TFloodFillRec = record
+    xLeft     : Integer;
+    xRight    : Integer;
+    y         : Integer;
+    dirY      : Integer;
+    next      : PFloodFillRec;
+  end;
+  TFloodFillStack = class
+    first     : PFloodFillRec;
+    maxY      : integer;
+    constructor Create(maxY: integer);
+    destructor Destroy; override;
+    procedure Push(xLeft, xRight,y, direction: Integer);
+    procedure Pop(out xLeft, xRight,y, direction: Integer);
+    function IsEmpty: Boolean;
+  end;
+  TFloodFillMask = class
+  private
+    img          : TImage32;
+    mask         : TImage32;
+    colorsRow    : PColor32Array;
+    maskRow      : PColor32Array;
+    initialColor : TColor32;
+    compareFunc  : TCompareFunctionEx;
+    tolerance    : Integer;
+  public
+    function Execute(imgIn, imgMaskOut: TImage32; x,y: integer;
+      aTolerance: Byte = 0; compFunc: TCompareFunctionEx = nil): Boolean;
+    procedure SetCurrentY(y: Integer);
+    function IsMatch(x: Integer): Boolean;
+  end;
+//------------------------------------------------------------------------------
+// TFloodFillStack methods
+//------------------------------------------------------------------------------
+constructor TFloodFillStack.Create(maxY: integer);
+begin
+  self.maxY := maxY;
+end;
+//------------------------------------------------------------------------------
+destructor TFloodFillStack.Destroy;
+var
+  ffr: PFloodFillRec;
+begin
+  while assigned(first) do
+  begin
+    ffr := first;
+    first := first.next;
+    dispose(ffr);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFloodFillStack.Push(xLeft, xRight, y, direction: Integer);
+var
+  ffr: PFloodFillRec;
+begin
+  if ((y <= 0) and (direction = -1)) or
+    ((y >= maxY) and (direction = 1)) then Exit;
+  new(ffr);
+  ffr.xLeft  := xLeft;
+  ffr.xRight := xRight;
+  ffr.y      := y;
+  ffr.dirY   := direction;
+  ffr.next   := first;
+  first      := ffr;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFloodFillStack.Pop(out xLeft, xRight, y, direction: Integer);
+var
+  ffr: PFloodFillRec;
+begin
+  xLeft     := first.xLeft;
+  xRight    := first.xRight;
+  direction := first.dirY;
+  y         := first.y + direction;
+  ffr := first;
+  first := first.next;
+  dispose(ffr);
+end;
+//------------------------------------------------------------------------------
+
+function TFloodFillStack.IsEmpty: Boolean;
+begin
+  result := not assigned(first);
+end;
+//------------------------------------------------------------------------------
+// TFloodFillMask methods
+//------------------------------------------------------------------------------
+
+function TFloodFillMask.Execute(imgIn, imgMaskOut: TImage32; x,y: integer;
+  aTolerance: Byte; compFunc: TCompareFunctionEx): Boolean;
+var
+  ffs          : TFloodFillStack;
+  w,h          : integer;
+  xl, xr, xr2  : Integer;
+  maxX         : Integer;
+  dirY         : Integer;
+begin
+  Result := Assigned(imgIn) and Assigned(imgMaskOut) and
+    InRange(x,0,imgIn.Width -1) and InRange(y,0,imgIn.Height -1);
+  if not Result then Exit;
+  w := imgIn.Width; h := imgIn.Height;
+  // make sure the mask is the size of the image
+  imgMaskOut.SetSize(w,h);
+  img   := imgIn;
+  mask  := imgMaskOut;
+  compareFunc := compFunc;
+  tolerance := aTolerance;
+  maxX := w -1;
+  ffs := TFloodFillStack.create(h -1);
+  try
+    initialColor := imgIn.Pixel[x, y];
+    xl := x; xr := x;
+    SetCurrentY(y);
+    IsMatch(x);
+    while (xl > 0) and IsMatch(xl -1) do dec(xl);
+    while (xr < maxX) and IsMatch(xr +1) do inc(xr);
+    ffs.Push(xl, xr, y, -1); // down
+    ffs.Push(xl, xr, y, 1);  // up
+    while not ffs.IsEmpty do
+    begin
+      ffs.Pop(xl, xr, y, dirY);
+      SetCurrentY(y);
+      xr2 := xl;
+      // check left ...
+      if IsMatch(xl) then
+      begin
+        while (xl > 0) and IsMatch(xl-1) do dec(xl);
+        if xl <= xr2 -2 then
+          ffs.Push(xl, xr2-2, y, -dirY);
+        while (xr2 < maxX) and IsMatch(xr2+1) do inc(xr2);
+        ffs.Push(xl, xr2, y, dirY);
+        if xr2 >= xr +2 then
+          ffs.Push(xr+2, xr2, y, -dirY);
+        xl := xr2 +2;
+      end;
+      // check right ...
+      while (xl <= xr) and not IsMatch(xl) do inc(xl);
+      while (xl <= xr) do
+      begin
+        xr2 := xl;
+        while (xr2 < maxX) and IsMatch(xr2+1) do inc(xr2);
+        ffs.Push(xl, xr2, y, dirY);
+        if xr2 >= xr +2 then
+        begin
+          ffs.Push(xr+2, xr2, y, -dirY);
+          break;
+        end;
+        inc(xl, 2);
+        while (xl <= xr) and not IsMatch(xl) do inc(xl);
+      end;
+    end;
+  finally
+    ffs.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFloodFillMask.SetCurrentY(y: Integer);
+begin
+  colorsRow := PColor32Array(img.PixelRow[y]);
+  maskRow   := PColor32Array(mask.PixelRow[y]);
+end;
+//------------------------------------------------------------------------------
+
+function TFloodFillMask.IsMatch(x: Integer): Boolean;
+var
+  b: Byte;
+begin
+  if (maskRow[x] > 0) then
+    result := false
+  else
+  begin
+    b := compareFunc(initialColor, colorsRow[x]);
+    result := b < tolerance;
+    if Result then
+      maskRow[x] := tolerance - b else
+      maskRow[x] := 1;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function GetFloodFillMask(imgIn, imgMaskOut: TImage32; x, y: Integer;
+  tolerance: Byte; compareFunc: TCompareFunctionEx): Boolean;
+var
+  ffm: TFloodFillMask;
+begin
+  if not Assigned(compareFunc) then compareFunc := CompareRGBEx;
+  ffm := TFloodFillMask.Create;
+  try
+    Result := ffm.Execute(imgIn, imgMaskOut, x, y, tolerance, compareFunc);
+  finally
+    ffm.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure FloodFill(img: TImage32; x, y: Integer; newColor: TColor32;
+  tolerance: Byte; compareFunc: TCompareFunctionEx);
+var
+  i: Integer;
+  pc, pm: PColor32;
+  mask: TImage32;
+begin
+  if not assigned(compareFunc) then
+  begin
+    compareFunc := CompareRGBEx;
+    if tolerance = 0 then
+      tolerance := FloodFillDefaultRGBTolerance;
+  end;
+  mask := TImage32.Create;
+  try
+    if not GetFloodFillMask(img, mask, x, y, tolerance, compareFunc) then
+      Exit;
+    pc := img.PixelBase;
+    pm := mask.PixelBase;
+    for i := 0 to img.Width * img.Height -1 do
+    begin
+      if (pm^ > 1) then pc^ := newColor;
+      inc(pm); inc(pc);
+    end;
+  finally
+    mask.free;
+  end;
+end;
+//------------------------------------------------------------------------------
+// EMBOSS - AND SUPPORT FUNCTIONS
+//------------------------------------------------------------------------------
+
+function IncPWeightColor(pwc: PWeightedColor; cnt: Integer): PWeightedColor;
+begin
+  result := PWeightedColor(PByte(pwc) + cnt * SizeOf(TWeightedColor));
+end;
+//------------------------------------------------------------------------------
+
+function Intensity(color: TColor32): byte;
+var
+  c: TARGB absolute color;
+begin
+  Result := (c.R * 61 + c.G * 174 + c.B * 21) shr 8;
+end;
+//------------------------------------------------------------------------------
+
+function Gray(color: TColor32): TColor32;
+var
+  c: TARGB absolute color;
+  res: TARGB absolute Result;
+begin
+  res.A := c.A;
+  res.R := Intensity(color);
+  res.G := res.R;
+  res.B := res.R;
+end;
+//------------------------------------------------------------------------------
+
+procedure Emboss(img: TImage32; radius: Integer;
+  depth: Integer; luminance: Integer; preserveColor: Boolean);
+var
+  yy,xx, x,y, w,h: Integer;
+  b: byte;
+  kernel: array [0 .. MaxBlur, 0 .. MaxBlur] of Integer;
+  wca: TArrayOfWeightedColor;
+  pc0, pcf, pcb: PColor32; // pointers to pixels (forward & backward in kernel)
+  pw0, pw: PWeightedColor; // pointers to weight
+  customGray: TColor32;
+  pc: PColor32;
+const
+  maxDepth = 50;
+begin
+  // grayscale luminance as percent where 0% is black and 100% is white
+  //(luminance is ignored when preserveColor = true)
+  luminance := ClampRange(luminance, 0, 100);
+  b := luminance *255 div 100;
+  customGray := $FF000000 + b shl 16 + b shl 8 + b;
+  ClampRange(radius, 1, 5);
+  inc(depth);
+  ClampRange(depth, 2, maxDepth);
+  kernel[0][0] := 1;
+  for y := 1 to radius do
+    for x := 1 to radius do
+      kernel[y][x] := depth;
+  w := img.Width; h := img.Height;
+  // nb: dynamic arrays are zero-initialized (unless they're a function result)
+  SetLength(wca, w * h);
+  pc0 := IncPColor32(img.PixelBase, radius * w);
+  pw0 := @wca[radius * w];
+  for y := radius to h -1 - radius do
+  begin
+    for x := radius to w -1 - radius do
+    begin
+      pw := IncPWeightColor(pw0, x);
+      pcb := IncPColor32(pc0, x - 1);
+      if preserveColor then
+      begin
+        pcf := IncPColor32(pc0, x);
+        pw^.Add(pcf^, kernel[0,0]);
+        inc(pcf);
+      end else
+      begin
+        pw^.Add(customGray, kernel[0,0]);
+        pcf := IncPColor32(pc0, x + 1);
+      end;
+      // parse the kernel ...
+      for yy := 1 to radius do
+      begin
+        for xx := 1 to radius do
+        begin
+          pw^.Subtract(Gray(pcf^), kernel[yy,xx]);
+          pw^.Add(Gray(pcb^), kernel[yy,xx]);
+          dec(pcb); inc(pcf);
+        end;
+        dec(pcb, img.Width - radius);
+        inc(pcf, img.Width - radius);
+      end;
+    end;
+    inc(pc0, img.Width);
+    inc(pw0, img.Width);
+  end;
+  pc := @img.Pixels[0]; pw := @wca[0];
+  for x := 0 to img.width * img.Height - 1 do
+  begin
+    pc^ := pw.Color or $FF000000;
+    inc(pc); inc(pw);
+  end;
+end;
+//------------------------------------------------------------------------------
+// Structure and functions used by the Vectorize routine
+//------------------------------------------------------------------------------
+type
+  TPt2Container = class;
+  TPt2 = class
+    pt         : TPointD;
+    owner      : TPt2Container;
+    isStart    : Boolean;
+    isHole     : Boolean;
+    nextInPath : TPt2;
+    prevInPath : TPt2;
+    nextInRow  : TPt2;
+    prevInRow  : TPt2;
+    destructor Destroy; override;
+    procedure Update(x, y: double);
+    function GetCount: integer;
+    function GetPoints: TPathD;
+    property IsAscending: Boolean read isStart;
+  end;
+  TPt2Container = class
+    prevRight: integer;
+    leftMostPt, rightMost: TPt2;
+    solution: TPathsD;
+    procedure AddToSolution(const path: TPathD);
+    function StartNewPath(insertBefore: TPt2;
+      xLeft, xRight, y: integer; isHole: Boolean): TPt2;
+    procedure AddRange(var current: TPt2; xLeft, xRight, y: integer);
+    function JoinAscDesc(path1, path2: TPt2): TPt2;
+    function JoinDescAsc(path1, path2: TPt2): TPt2;
+    procedure CheckRowEnds(pt2Left, pt2Right: TPt2);
+  end;
+//------------------------------------------------------------------------------
+destructor TPt2.Destroy;
+var
+  startPt, endPt, pt: TPt2;
+begin
+  if not isStart then Exit;
+  startPt := self;
+  endPt := startPt.prevInPath;
+  // remove 'endPt' from double linked list
+  if endPt = owner.rightMost then
+    owner.rightMost := endPt.prevInRow
+  else if assigned(endPt.nextInRow) then
+    endPt.nextInRow.prevInRow := endPt.prevInRow;
+  if endPt = owner.leftMostPt then
+    owner.leftMostPt := endPt.nextInRow
+  else if assigned(endPt.prevInRow) then
+    endPt.prevInRow.nextInRow := endPt.nextInRow;
+  // remove 'startPt' from double linked list
+  if startPt = owner.leftMostPt then
+    owner.leftMostPt := startPt.nextInRow
+  else if assigned(startPt.prevInRow) then
+    startPt.prevInRow.nextInRow := startPt.nextInRow;
+  if assigned(startPt.nextInRow) then
+    startPt.nextInRow.prevInRow := startPt.prevInRow;
+  owner.AddToSolution(GetPoints);
+  // now Free the entire path (except self)
+  pt := startPt.nextInPath;
+  while pt <> startPt do
+  begin
+    endPt := pt;
+    pt := pt.nextInPath;
+    endPt.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function IsColinear(const pt1, pt2, pt3: TPoint): Boolean; overload;
+begin
+  // cross product = 0
+  result := (pt1.X - pt2.X)*(pt2.Y - pt3.Y) = (pt2.X - pt3.X)*(pt1.Y - pt2.Y);
+end;
+//------------------------------------------------------------------------------
+
+function IsColinear(const pt1, pt2, pt3, pt4: TPoint): Boolean; overload;
+begin
+  result := (pt1.X - pt2.X)*(pt3.Y - pt4.Y) = (pt3.X - pt4.X)*(pt1.Y - pt2.Y);
+end;
+//------------------------------------------------------------------------------
+
+function CreatePt2After(pt: TPt2; const p: TPointD): TPt2;
+begin
+  Result := TPt2.Create;
+  Result.pt := p;
+  Result.nextInPath := pt.nextInPath;
+  Result.prevInPath := pt;
+  pt.nextInPath.prevInPath := Result;
+  pt.nextInPath := Result;
+end;
+//------------------------------------------------------------------------------
+
+procedure TPt2.Update(x, y: double);
+var
+  newPt2: TPt2;
+begin
+  if isStart then
+  begin
+    // just update self.pt when colinear
+    if (x = pt.X) and (pt.X = nextInPath.pt.X) then
+    begin
+      pt := PointD(x,y);
+      Exit;
+    end;
+    // self -> 2 -> 1 -> nip
+    CreatePt2After(self, pt);
+    if (x <> pt.X) or (x <> nextInPath.pt.X) then
+    begin
+      // add a pixel either below or beside
+      if IsAscending then
+        CreatePt2After(self, PointD(pt.X, y)) else
+        CreatePt2After(self, PointD(x, pt.Y));
+    end;
+    pt := PointD(x,y);
+  end else
+  begin
+    // just update self.pt when colinear
+    if (x = pt.X) and (pt.X = prevInPath.pt.X) then
+    begin
+      pt := PointD(x,y);
+      Exit;
+    end;
+    // self <- 2 <- 1 <- pip
+    newPt2 := CreatePt2After(prevInPath, pt);
+    if (x <> pt.X) or (x <> prevInPath.pt.X) then
+    begin
+      // add a pixel either below or beside
+      if IsAscending then
+        CreatePt2After(newPt2, PointD(x, pt.Y)) else
+        CreatePt2After(newPt2, PointD(pt.X, y));
+    end;
+    pt := PointD(x,y);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TPt2.GetCount: integer;
+var
+  pt2: TPt2;
+begin
+  result := 1;
+  pt2 := nextInPath;
+  while pt2 <> self do
+  begin
+    inc(Result);
+    pt2 := pt2.nextInPath;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TPt2.GetPoints: TPathD;
+var
+  i, count: integer;
+  pt2: TPt2;
+begin
+  Update(pt.X, pt.Y+1);
+  with prevInPath do Update(pt.X, pt.Y+1); // path 'end'
+  count := GetCount;
+  SetLength(Result, count);
+  pt2 := self;
+  for i := 0 to count -1 do
+  begin
+    Result[i] := pt2.pt;
+    pt2 := pt2.nextInPath;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TPt2Container.AddToSolution(const path: TPathD);
+var
+  len: integer;
+begin
+  if Length(path) < 2 then Exit;
+  len := Length(solution);
+  SetLength(solution, len + 1);
+  solution[len] := path;
+end;
+//------------------------------------------------------------------------------
+
+function TPt2Container.StartNewPath(insertBefore: TPt2;
+  xLeft, xRight, y: integer; isHole: Boolean): TPt2;
+var
+  pt2Left, pt2Right: TPt2;
+begin
+  inc(xRight);
+  pt2Left := TPt2.Create;
+  pt2Left.owner := self;
+  pt2Left.isStart := not isHole;
+  pt2Left.isHole := isHole;
+  pt2Left.pt := PointD(xLeft, y);
+  pt2Right := TPt2.Create;
+  pt2Right.owner := self;
+  pt2Right.isStart := isHole;
+  pt2Right.isHole := isHole;
+  pt2Right.pt := PointD(xRight, y);
+  pt2Left.nextInPath := pt2Right;
+  pt2Left.prevInPath := pt2Right;
+  pt2Right.nextInPath := pt2Left;
+  pt2Right.prevInPath := pt2Left;
+  pt2Left.nextInRow := pt2Right;
+  pt2Right.prevInRow := pt2Left;
+  if not Assigned(insertBefore) then
+  begin
+    // must be a new rightMost path
+    pt2Left.prevInRow := rightMost;
+    if Assigned(rightMost) then rightMost.nextInRow := pt2Left;
+    pt2Right.nextInRow := nil;
+    rightMost := pt2Right;
+    if not Assigned(leftMostPt) then leftMostPt := pt2Left;
+  end else
+  begin
+    pt2Right.nextInRow := insertBefore;
+    if leftMostPt = insertBefore then
+    begin
+      // must be a new leftMostPt path
+      leftMostPt := pt2Left;
+      pt2Left.prevInRow := nil;
+    end else
+    begin
+      pt2Left.prevInRow := insertBefore.prevInRow;
+      insertBefore.prevInRow.nextInRow := pt2Left;
+    end;
+    insertBefore.prevInRow := pt2Right;
+  end;
+  result := pt2Right.nextInRow;
+end;
+//------------------------------------------------------------------------------
+
+procedure TPt2Container.CheckRowEnds(pt2Left, pt2Right: TPt2);
+begin
+  if pt2Left = leftMostPt then leftMostPt := pt2Right.nextInRow;
+  if pt2Right = rightMost then rightMost := pt2Left.prevInRow;
+end;
+//------------------------------------------------------------------------------
+
+function TPt2Container.JoinAscDesc(path1, path2: TPt2): TPt2;
+begin
+  result := path2.nextInRow;
+  CheckRowEnds(path1, path2);
+  if path2 = path1.prevInPath then
+  begin
+    path1.Free;
+    Exit;
+  end;
+  with path1 do Update(pt.X, pt.Y+1);
+  with path2 do Update(pt.X, pt.Y+1);
+  path1.isStart := false;
+  // remove path1 from double linked list
+  if assigned(path1.nextInRow) then
+    path1.nextInRow.prevInRow := path1.prevInRow;
+  if assigned(path1.prevInRow) then
+    path1.prevInRow.nextInRow := path1.nextInRow;
+  // remove path2 from double linked list
+  if assigned(path2.nextInRow) then
+    path2.nextInRow.prevInRow := path2.prevInRow;
+  if assigned(path2.prevInRow) then
+    path2.prevInRow.nextInRow := path2.nextInRow;
+  path1.prevInPath.nextInPath := path2.nextInPath;
+  path2.nextInPath.prevInPath := path1.prevInPath;
+  path2.nextInPath := path1;
+  path1.prevInPath := path2;
+end;
+//------------------------------------------------------------------------------
+
+function TPt2Container.JoinDescAsc(path1, path2: TPt2): TPt2;
+begin
+  result := path2.nextInRow;
+  CheckRowEnds(path1, path2);
+  if path1 = path2.prevInPath then
+  begin
+    path2.Free;
+    Exit;
+  end;
+  with path1 do Update(pt.X, pt.Y+1);
+  with path2 do Update(pt.X, pt.Y+1);
+  path2.isStart := false;
+  // remove path1 'end' from double linked list
+  if assigned(path1.nextInRow) then
+    path1.nextInRow.prevInRow := path1.prevInRow;
+  if assigned(path1.prevInRow) then
+    path1.prevInRow.nextInRow := path1.nextInRow;
+  // remove path2 'start' from double linked list
+  if assigned(path2.nextInRow) then
+    path2.nextInRow.prevInRow := path2.prevInRow;
+  if assigned(path2.prevInRow) then
+    path2.prevInRow.nextInRow := path2.nextInRow;
+  path1.nextInPath.prevInPath := path2.prevInPath;
+  path2.prevInPath.nextInPath := path1.nextInPath;
+  path1.nextInPath := path2;
+  path2.prevInPath := path1;
+end;
+//------------------------------------------------------------------------------
+
+function IsHeadingLeft(current: TPt2; r: integer): Boolean;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result := r <= current.pt.X;
+end;
+//------------------------------------------------------------------------------
+
+procedure TPt2Container.AddRange(var current: TPt2;
+  xLeft, xRight, y: integer);
+begin
+  if (prevRight > 0) then
+  begin
+    // nb: prevRight always ends a range (whether a hole or an outer)
+    // check if we're about to start a hole
+    if xLeft < current.pt.X then
+    begin
+      //'current' must be descending and hence prevRight->xLeft a hole
+      current := StartNewPath(current, prevRight, xLeft -1, y, true);
+      prevRight := xRight;
+      Exit; // nb: it's possible for multiple holes
+    end;
+    // check if we're passing under a pending join
+    while assigned(current) and assigned(current.nextInRow) and
+      (prevRight > current.nextInRow.pt.X) do
+    begin
+      // Assert(not current.IsAscending, 'oops!');
+      // Assert(current.nextInRow.IsAscending, 'oops!');
+      current := JoinDescAsc(current, current.nextInRow);
+    end;
+    // check again for a new hole
+    if (xLeft < current.pt.X) then
+    begin
+      current := StartNewPath(current, prevRight, xLeft -1, y, true);
+      prevRight := xRight;
+      Exit;
+    end;
+    current.Update(prevRight, y);
+    current := current.nextInRow;
+    prevRight := 0;
+  end;
+  // check if we're passing under a pending join
+  while assigned(current) and assigned(current.nextInRow) and
+    (xLeft > current.nextInRow.pt.X) do
+      current := JoinAscDesc(current, current.nextInRow);
+  if not assigned(current) or (xRight < current.pt.X) then
+  begin
+    StartNewPath(current, xLeft, xRight -1, y, false);
+    // nb: current remains unchanged
+  end else
+  begin
+    //'range' must somewhat overlap one or more paths above
+    if IsHeadingLeft(current, xRight) then
+    begin
+      if current.isHole then
+      begin
+        current.Update(xLeft, y);
+        current := current.nextInRow;
+      end;
+      current.Update(xRight, y);
+      current.Update(xLeft, y);
+      if current.IsAscending then
+        prevRight := xRight else
+        prevRight := 0;
+      current := current.nextInRow;
+    end else
+    begin
+      current.Update(xLeft, y);
+      current := current.nextInRow;
+      prevRight := xRight;
+    end;
+  end
+end;
+//------------------------------------------------------------------------------
+
+function VectorizeMask(const mask: TArrayOfByte; maskWidth: integer): TPathsD;
+var
+  i,j, len, height, blockStart: integer;
+  current: TPt2;
+  ba: PByteArray;
+  pt2Container: TPt2Container;
+begin
+  Result := nil;
+  len := Length(mask);
+  if (len = 0) or (maskWidth = 0) or (len mod maskWidth <> 0) then Exit;
+  height := len div maskWidth;
+  pt2Container := TPt2Container.Create;
+  try
+    for i := 0 to height -1 do
+    begin
+      ba := @mask[maskWidth * i];
+      blockStart := -2;
+      current := pt2Container.leftMostPt;
+      for j := 0 to maskWidth -1 do
+      begin
+        if (ba[j] > 0) = (blockStart >= 0) then Continue;
+        if blockStart >= 0 then
+        begin
+          pt2Container.AddRange(current, blockStart, j, i);
+          blockStart := -1;
+        end else
+          blockStart := j;
+      end;
+      if blockStart >= 0 then
+        pt2Container.AddRange(current, blockStart, maskWidth, i);
+      if (pt2Container.prevRight > 0) then
+      begin
+        while Assigned(current.nextInRow) and
+          (pt2Container.prevRight >= current.nextInRow.pt.X) do
+        begin
+          if current.isStart then
+            current := pt2Container.JoinAscDesc(current, current.nextInRow)
+          else
+            current := pt2Container.JoinDescAsc(current, current.nextInRow);
+        end;
+        current.Update(pt2Container.prevRight, i);
+        current := current.nextInRow;
+        pt2Container.prevRight := 0;
+      end;
+      while assigned(current) do
+      begin
+        if current.isStart then
+          current := pt2Container.JoinAscDesc(current, current.nextInRow) else
+          current := pt2Container.JoinDescAsc(current, current.nextInRow);
+      end
+    end;
+    with pt2Container do
+      while Assigned(leftMostPt) do
+        if leftMostPt.isStart then
+          JoinAscDesc(leftMostPt, leftMostPt.nextInRow) else
+          JoinDescAsc(leftMostPt, leftMostPt.nextInRow);
+    Result := pt2Container.solution;
+  finally
+    pt2Container.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function Tidy(const poly: TPathD; tolerance: integer): TPathD;
+var
+  i,j, highI: integer;
+  prev: TPointD;
+  tolSqrd: double;
+begin
+  Result := nil;
+  highI := High(poly);
+  while  (HighI >= 0) and PointsEqual(poly[highI], poly[0]) do dec(highI);
+  if highI < 1 then Exit;
+  tolSqrd := Sqr(Max(2.02, Min(16.1, tolerance + 0.01)));
+  SetLength(Result, highI +1);
+  prev := poly[highI];
+  Result[0] := prev;
+  Result[1] := poly[0];
+  j := 1;
+  for i := 1 to highI -1 do
+  begin
+    if ((DistanceSqrd(prev, Result[j]) > tolSqrd) and
+        (DistanceSqrd(Result[j], poly[i]) > tolSqrd)) or
+      (TurnsRight(prev, result[j], poly[i]) or
+        TurnsLeft(result[j], poly[i], poly[i+1])) then
+    begin
+      prev := result[j];
+      inc(j);
+    end;
+    result[j] := poly[i];
+  end;
+  if ((DistanceSqrd(prev, Result[j]) > tolSqrd) and
+    (DistanceSqrd(Result[j], Result[0]) > tolSqrd)) or
+    TurnsRight(prev, result[j], Result[0]) or
+    TurnsLeft(result[j], Result[0], Result[1]) then
+      SetLength(Result, j +1) else
+      SetLength(Result, j);
+  if Abs(Area(Result)) < Length(Result) * tolerance/2 then Result := nil;
+end;
+//------------------------------------------------------------------------------
+
+function Vectorize(img: TImage32; compareColor: TColor32;
+  compareFunc: TCompareFunction; colorTolerance: Integer;
+  roundingTolerance: integer): TPathsD;
+var
+  i,j: integer;
+  mask: TArrayOfByte;
+begin
+  mask := GetBoolMask(img, compareColor, compareFunc, colorTolerance);
+  Result := VectorizeMask(mask, img.Width);
+  j := 0;
+  for i := 0 to high(Result) do
+  begin
+    Result[j] := Tidy(Result[i], roundingTolerance);
+    if Assigned(Result[j]) then inc(j);
+  end;
+  SetLength(Result, j);
+end;
+//------------------------------------------------------------------------------
+// RamerDouglasPeucker - and support functions
+//------------------------------------------------------------------------------
+
+procedure RDP(const path: TPathD; startIdx, endIdx: integer;
+  epsilonSqrd: double; var flags: TArrayOfInteger);
+var
+  i, idx: integer;
+  d, maxD: double;
+begin
+  idx := 0;
+  maxD := 0;
+  for i := startIdx +1 to endIdx -1 do
+  begin
+    // PerpendicularDistSqrd - avoids expensive Sqrt()
+    d := PerpendicularDistSqrd(path[i], path[startIdx], path[endIdx]);
+    if d <= maxD then Continue;
+    maxD := d;
+    idx := i;
+  end;
+  if maxD < epsilonSqrd then Exit;
+  flags[idx] := 1;
+  if idx > startIdx + 1 then RDP(path, startIdx, idx, epsilonSqrd, flags);
+  if endIdx > idx + 1 then RDP(path, idx, endIdx, epsilonSqrd, flags);
+end;
+//------------------------------------------------------------------------------
+
+function RamerDouglasPeucker(const path: TPathD;
+  epsilon: double): TPathD;
+var
+  i,j, len: integer;
+  buffer: TArrayOfInteger;
+begin
+  len := length(path);
+  if len < 5 then
+  begin
+    result := Copy(path, 0, len);
+    Exit;
+  end;
+  SetLength(buffer, len); // buffer is zero initialized
+  buffer[0] := 1;
+  buffer[len -1] := 1;
+  RDP(path, 0, len -1, Sqr(epsilon), buffer);
+  j := 0;
+  SetLength(Result, len);
+  for i := 0 to len -1 do
+    if buffer[i] = 1 then
+    begin
+      Result[j] := path[i];
+      inc(j);
+    end;
+  SetLength(Result, j);
+end;
+//------------------------------------------------------------------------------
+
+function RamerDouglasPeucker(const paths: TPathsD;
+  epsilon: double): TPathsD;
+var
+  i,j, len: integer;
+begin
+  j := 0;
+  len := length(paths);
+  setLength(Result, len);
+  for i := 0 to len -1 do
+  begin
+    Result[j] := RamerDouglasPeucker(paths[i], epsilon);
+    if Result[j] <> nil then inc(j);
+  end;
+  setLength(Result, j);
+end;
+//------------------------------------------------------------------------------
+
+function DotProdVecs(const vec1, vec2: TPointD): double;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  result := (vec1.X * vec2.X + vec1.Y * vec2.Y);
+end;
+//---------------------------------------------------------------------------
+
+function SmoothToCubicBezier(const path: TPathD;
+  pathIsClosed: Boolean; maxOffset: integer): TPathD;
+var
+  i, j, len, prev: integer;
+  vec: TPointD;
+  pl: TArrayOfDouble;
+  unitVecs: TPathD;
+  d, angle, d1,d2: double;
+begin
+  // SmoothToCubicBezier - returns cubic bezier control points
+  Result := nil;
+  len := Length(path);
+  if len < 3 then Exit;
+
+  SetLength(Result, len *3 +1);
+  prev := len-1;
+  SetLength(pl, len);
+  SetLength(unitVecs, len);
+  pl[0] := Distance(path[prev], path[0]);
+  unitVecs[0] := GetUnitVector(path[prev], path[0]);
+  for i := 0 to len -1 do
+  begin
+    if i = prev then
+    begin
+      j := 0;
+    end else
+    begin
+      j := i +1;
+      pl[j] := Distance(path[i], path[j]);
+      unitVecs[j] := GetUnitVector(path[i], path[j]);
+    end;
+    vec := GetAvgUnitVector(unitVecs[i], unitVecs[j]);
+
+    angle := arccos(Max(-1,Min(1,(DotProdVecs(unitVecs[i], unitVecs[j])))));
+    d := abs(Pi-angle)/TwoPi;
+    d1 := pl[i] * d;
+    d2 := pl[j] * d;
+
+    if maxOffset > 0 then
+    begin
+      d1 := Min(maxOffset, d1);
+      d2 := Min(maxOffset, d2);
+    end;
+
+    if i = 0 then
+      Result[len*3-1] := OffsetPoint(path[0], -vec.X * d1, -vec.Y * d1)
+    else
+      Result[i*3-1] := OffsetPoint(path[i], -vec.X * d1, -vec.Y * d1);
+    Result[i*3] := path[i];
+    Result[i*3+1] := OffsetPoint(path[i], vec.X * d2, vec.Y * d2);
+  end;
+  Result[len*3] := path[0];
+
+  if pathIsClosed then Exit;
+  Result[1] := Result[0];
+  dec(len);
+  Result[len*3-1] := Result[len*3];
+  SetLength(Result, Len*3 +1);
+end;
+//------------------------------------------------------------------------------
+
+function HermiteInterpolation(y1, y2, y3, y4: double;
+  mu, tension: double): double;
+var
+   m0,m1,mu2,mu3: double;
+   a0,a1,a2,a3: double;
+begin
+  // http://paulbourke.net/miscellaneous/interpolation/
+  // nb: optional bias toward left or right has been disabled.
+	mu2 := mu * mu;
+	mu3 := mu2 * mu;
+   m0  := (y2-y1)*(1-tension)/2;
+   m0 := m0  + (y3-y2)*(1-tension)/2;
+   m1 := (y3-y2)*(1-tension)/2;
+   m1 := m1 + (y4-y3)*(1-tension)/2;
+   a0 :=  2*mu3 - 3*mu2 + 1;
+   a1 :=    mu3 - 2*mu2 + mu;
+   a2 :=    mu3 -   mu2;
+   a3 := -2*mu3 + 3*mu2;
+   Result := a0*y2+a1*m0+a2*m1+a3*y3;
+end;
+//------------------------------------------------------------------------------
+
+function InterpolateY(const y1,y2,y3,y4: double;
+  dx: integer; tension: double): TArrayOfDouble;
+var
+  i: integer;
+begin
+  SetLength(Result, dx);
+  if dx = 0 then Exit;
+  Result[0] := y2;
+  for i := 1 to dx-1 do
+    Result[i] := HermiteInterpolation(y1,y2,y3,y4, i/dx, tension);
+end;
+//------------------------------------------------------------------------------
+
+function InterpolatePoints(const points: TPathD; tension: integer): TPathD;
+var
+  i, j, len, len2: integer;
+  p, p2: TPathD;
+  ys: TArrayOfDouble;
+begin
+  if tension < -1 then tension := -1
+  else if tension > 1 then tension := 1;
+  Result := nil;
+  len := Length(points);
+  if len < 2 then Exit;
+  SetLength(p, len +2);
+  p[0] := points[0];
+  p[len+1] := points[len -1];
+  Move(points[0],p[1], len * SizeOf(TPointD));
+  for i := 1 to len-1 do
+  begin
+    ys := InterpolateY(p[i-1].Y,p[i].Y,p[i+1].Y,p[i+2].Y,
+      Trunc(p[i+1].X - p[i].X), tension);
+    len2 := Length(ys);
+    SetLength(p2, len2);
+    for j := 0 to len2 -1 do
+      p2[j] := PointD(p[i].X +j, ys[j]);
+    AppendPath(Result, p2);
+  end;
+  AppendPoint(Result, p[len]);
+end;
+
+//------------------------------------------------------------------------------
+// GaussianBlur
+//------------------------------------------------------------------------------
+
+procedure GaussianBlur(img: TImage32; rec: TRect; radius: Integer);
+var
+  i, w,h, x,y,yy,z: Integer;
+  gaussTable: array [-MaxBlur .. MaxBlur] of Cardinal;
+  wc: TWeightedColor;
+  wca: TArrayOfWeightedColor;
+  row: PColor32Array;
+  wcRow: PWeightedColorArray;
+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
+  begin
+    gaussTable[i] := Sqr(Radius - i +1);
+    gaussTable[-i] := gaussTable[i];
+  end;
+  RectWidthHeight(rec, w, h);
+  setLength(wca, w * h);
+  for y := 0 to h -1 do
+  begin
+    row := PColor32Array(@img.Pixels[(y + rec.Top) * img.Width + 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
+        wcRow[x].Add(row[z], gaussTable[x-z]);
+  end;
+  for x := 0 to w -1 do
+  begin
+    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
+      begin
+        wc.Add(wca[x + yy].Color, gaussTable[y-z]);
+        inc(yy, w);
+      end;
+      img.Pixels[x + rec.Left + (y + rec.Top) * img.Width] := wc.Color;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+// FastGaussian blur - and support functions
+//------------------------------------------------------------------------------
+//http://blog.ivank.net/fastest-gaussian-blur.html
+//https://www.peterkovesi.com/papers/FastGaussianSmoothing.pdf
+function BoxesForGauss(stdDev, boxCnt: integer): TArrayOfInteger;
+var
+  i, wl, wu, m: integer;
+  wIdeal, mIdeal: double;
+begin
+  SetLength(Result, boxCnt);
+  wIdeal := Sqrt((12*stdDev*stdDev/boxCnt)+1); // Ideal averaging filter width
+  wl := Floor(wIdeal); if not Odd(wl) then dec(wl);
+  mIdeal :=
+    (-3*stdDev*stdDev +0.25*boxCnt*wl*wl +boxCnt*wl +0.75*boxCnt)/(wl+1);
+  m := Floor(mIdeal) div 2;   // nb: variation on Ivan Kutskir's code.
+  wl := (wl -1) div 2;        //    It's better to do this here
+  wu := wl+1;                 //    than later in both BoxBlurH & BoxBlurV
+  for i := 0 to boxCnt -1 do
+    if i < m then
+      Result[i] := wl else
+      Result[i] := wu;
+end;
+//------------------------------------------------------------------------------
+
+procedure BoxBlurH(var src, dst: TArrayOfColor32; w,h, stdDev: integer);
+var
+  i,j, ti, li, ri, re, ovr: integer;
+  fv, lv, val: TWeightedColor;
+  rc: TColor32;
+begin
+  ovr := Max(0, stdDev - w);
+  for i := 0 to h -1 do
+  begin
+    ti := i * w;
+    li := ti;
+    ri := ti +stdDev;
+    re := ti +w -1; // idx of last pixel in row
+    rc := src[re];  // color of last pixel in row
+    fv.Reset;
+    lv.Reset;
+    val.Reset;
+    fv.Add(src[ti], 1);
+    lv.Add(rc, 1);
+    val.Add(src[ti], stdDev +1);
+    for j := 0 to stdDev -1 - ovr do
+      val.Add(src[ti + j]);
+    if ovr > 0 then val.Add(rc, ovr);
+    for j := 0 to stdDev do
+    begin
+      if ri > re then
+        val.Add(rc) else
+        val.Add(src[ri]);
+      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
+    begin
+      if ri <= re then
+      begin
+        val.Add(src[ri]); inc(ri);
+        val.Subtract(src[li]); inc(li);
+      end;
+      dst[ti] := val.Color; inc(ti);
+    end;
+    while ti <= re do
+    begin
+      if ti > re then Break;
+      val.Add(lv);
+      val.Subtract(src[li]); inc(li);
+      dst[ti] := val.Color;
+      inc(ti);
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure BoxBlurV(var src, dst: TArrayOfColor32; w, h, stdDev: integer);
+var
+  i,j, ti, li, ri, re, ovr: integer;
+  fv, lv, val: TWeightedColor;
+  rc: TColor32;
+begin
+  ovr := Max(0, stdDev - h);
+  for i := 0 to w -1 do
+  begin
+    ti := i;
+    li := ti;
+    ri := ti + stdDev * w;
+    fv.Reset;
+    lv.Reset;
+    val.Reset;
+    re := ti +w *(h-1); // idx of last pixel in column
+    rc := src[re];      // color of last pixel in column
+    fv.Add(src[ti]);
+    lv.Add(rc, 1);
+    val.Add(src[ti], stdDev +1);
+    for j := 0 to stdDev -1 -ovr do
+      val.Add(src[ti + j *w]);
+    if ovr > 0 then val.Add(rc, ovr);
+    for j := 0 to stdDev do
+    begin
+      if ri > re then
+        val.Add(rc) else
+        val.Add(src[ri]);
+      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
+    begin
+      if ri <= re then
+      begin
+        val.Add(src[ri]); inc(ri, w);
+        val.Subtract(src[li]); inc(li, w);
+      end;
+      dst[ti] := val.Color; inc(ti, w);
+    end;
+    while ti <= re do
+    begin
+      val.Add(lv);
+      val.Subtract(src[li]); inc(li, w);
+      dst[ti] := val.Color;
+      inc(ti, w);
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure FastGaussianBlur(img: TImage32;
+  const rec: TRect; stdDev: integer; repeats: integer);
+begin
+  FastGaussianBlur(img, rec, stdDev, stdDev, repeats);
+end;
+//------------------------------------------------------------------------------
+
+procedure FastGaussianBlur(img: TImage32;
+  const rec: TRect; stdDevX, stdDevY: integer; repeats: integer);
+var
+  i,j,len, w,h: integer;
+  rec2: TRect;
+  boxesH: TArrayOfInteger;
+  boxesV: TArrayOfInteger;
+  src, dst: TArrayOfColor32;
+  blurFullImage: Boolean;
+  pSrc, pDst: PColor32;
+begin
+  if not Assigned(img) then Exit;
+  Types.IntersectRect(rec2, rec, img.Bounds);
+  if IsEmptyRect(rec2) then Exit;
+  blurFullImage := RectsEqual(rec2, img.Bounds);
+  RectWidthHeight(rec2, w, h);
+  if (Min(w, h) < 2) or ((stdDevX < 1) and (stdDevY < 1)) then Exit;
+  len := w * h;
+  SetLength(src, len);
+  SetLength(dst, len);
+  if blurFullImage then
+  begin
+    // copy the entire image into 'dst'
+    Move(img.PixelBase^, dst[0], len * SizeOf(TColor32));
+  end else
+  begin
+    // copy a rectangular region into 'dst'
+    pSrc := img.PixelRow[rec2.Top];
+    inc(pSrc, rec2.Left);
+    pDst := @dst[0];
+    for i := 0 to h -1 do
+    begin
+      Move(pSrc^, pDst^, w * SizeOf(TColor32));
+      inc(pSrc, img.Width);
+      inc(pDst, w);
+    end;
+  end;
+  // do the blur
+  inc(repeats); // now represents total iterations
+  boxesH := BoxesForGauss(stdDevX, repeats);
+  if stdDevY = stdDevX then
+    boxesV := boxesH else
+    boxesV := BoxesForGauss(stdDevY, repeats);
+  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
+    begin
+      pDst := img.PixelRow[rec2.Top];
+      inc(pDst, rec2.Left);
+      pSrc := @dst[0];
+      for i := 0 to h -1 do
+      begin
+        Move(pSrc^, pDst^, w * SizeOf(TColor32));
+        inc(pSrc, w);
+        inc(pDst, img.Width);
+      end;
+    end;
+  finally
+    img.EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+//------------------------------------------------------------------------------
+// CurveFit() support structures and functions
+//------------------------------------------------------------------------------
+
+//CurveFit: this function is based on -
+//"An Algorithm for Automatically Fitting Digitized Curves"
+//by Philip J. Schneider in "Graphics Gems", Academic Press, 1990
+//Smooths out many very closely positioned points
+//tolerance range: 1..10 where 10 == max tolerance.
+
+//This function has been archived as I believe that
+//RamerDouglasPeuker(), GetSmoothPath() and FlattenCBezier()
+//will usually achieve a better result
+
+function Scale(const vec: TPointD; newLen: double): TPointD;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result.X := vec.X * newLen;
+  Result.Y := vec.Y * newLen;
+end;
+//------------------------------------------------------------------------------
+
+function Mul(const vec: TPointD; val: double): TPointD;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result.X := vec.X * val;
+  Result.Y := vec.Y * val;
+end;
+//------------------------------------------------------------------------------
+
+function AddVecs(const vec1, vec2: TPointD): TPointD;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result.X := vec1.X + vec2.X;
+  Result.Y := vec1.Y + vec2.Y;
+end;
+//------------------------------------------------------------------------------
+
+function SubVecs(const vec1, vec2: TPointD): TPointD;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result.X := vec1.X - vec2.X;
+  Result.Y := vec1.Y - vec2.Y;
+end;
+//------------------------------------------------------------------------------
+
+function NormalizeVec(const vec: TPointD): TPointD;
+  {$IFDEF INLINE} inline; {$ENDIF}
+var
+  len: double;
+begin
+  len := Sqrt(vec.X * vec.X + vec.Y * vec.Y);
+  if len <> 0 then
+  begin
+    Result.X := vec.X / len;
+    Result.Y := vec.Y / len;
+  end else
+    result := vec;
+end;
+//------------------------------------------------------------------------------
+
+function NormalizeTPt(const pt: PPt): TPointD;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  with pt^ do
+    if len <> 0 then
+    begin
+      Result.X := vec.X / len;
+      Result.Y := vec.Y / len;
+    end else
+      result := vec;
+end;
+//------------------------------------------------------------------------------
+
+function NegateVec(vec: TPointD): TPointD;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result.X := -vec.X;
+  Result.Y := -vec.Y;
+end;
+//------------------------------------------------------------------------------
+
+function B0(u: double): double; {$IFDEF INLINE} inline; {$ENDIF}
+var
+  tmp: double;
+begin
+  tmp := 1.0 - u;
+  result := tmp * tmp * tmp;
+end;
+//------------------------------------------------------------------------------
+
+function B1(u: double): double; {$IFDEF INLINE} inline; {$ENDIF}
+var
+  tmp: double;
+begin
+  tmp := 1.0 - u;
+  result := 3 * u * tmp * tmp;
+end;
+//------------------------------------------------------------------------------
+
+function B2(u: double): double; {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  result := 3 * u * u * (1.0 - u);
+end;
+//------------------------------------------------------------------------------
+
+function B3(u: double): double; {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  result := u * u * u;
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.AddPt(const pt: TPointD): PPt;
+begin
+  new(Result);
+  Result.pt := pt;
+  if not assigned(ppts) then
+  begin
+    Result.prev := Result;
+    Result.next := Result;
+    ppts := Result;
+  end else
+  begin
+    Result.prev := ppts.prev;
+    ppts.prev.next := Result;
+    ppts.prev := Result;
+    Result.next := ppts;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFitCurveContainer.Clear;
+var
+  p: PPt;
+begin
+  solution := nil;
+  ppts.prev.next := nil; //break loop
+  while assigned(ppts) do
+  begin
+    p := ppts;
+    ppts := ppts.next;
+    Dispose(p);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.Count(first, last: PPt): integer;
+begin
+  if first = last then
+    result := 0 else
+    result := 1;
+  repeat
+    inc(Result);
+    first := first.next;
+  until (first = last);
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.ComputeLeftTangent(p: PPt): TPointD;
+begin
+  Result := NormalizeTPt(p);
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.ComputeRightTangent(p: PPt): TPointD;
+begin
+  Result := NegateVec(NormalizeTPt(p.prev));
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.ComputeCenterTangent(p: PPt): TPointD;
+var
+  v1, v2: TPointD;
+begin
+  v1 := SubVecs(p.pt, p.prev.pt);
+  v2 := SubVecs(p.next.pt, p.pt);
+  Result := AddVecs(v1, v2);
+  Result := NormalizeVec(Result);
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.ChordLengthParameterize(
+  first: PPt; cnt: integer): TArrayOfDouble;
+var
+  d: double;
+  i: integer;
+begin
+  SetLength(Result, cnt);
+  Result[0] := 0;
+  d := 0;
+  for i := 1 to cnt -1 do
+  begin
+    d := d + first.len;
+    Result[i] := d;
+    first := first.next;
+  end;
+  for i := 1 to cnt -1 do
+    Result[i] := Result[i] / d;
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.GenerateBezier(first, last: PPt; cnt: integer;
+  const u: TArrayOfDouble; const firstTan, lastTan: TPointD): TPathD;
+var
+  i: integer;
+  p: PPt;
+  dist, epsilon: double;
+  v1,v2, tmp: TPointD;
+  a0, a1: TPathD;
+  c: array [0..1, 0..1] of double;
+  x: array [0..1] of double;
+  det_c0_c1, det_c0_x, det_x_c1, alphaL, alphaR: double;
+begin
+  SetLength(a0, cnt);
+  SetLength(a1, cnt);
+  dist := Distance(first.pt, last.pt);
+  for i := 0 to cnt -1 do
+  begin
+		v1 := Scale(firstTan, B1(u[i]));
+		v2 := Scale(lastTan, B2(u[i]));
+		a0[i] := v1;
+		a1[i] := v2;
+  end;
+  FillChar(c[0][0], 4 * SizeOf(double), 0);
+  FillChar(x[0], 2 * SizeOf(double), 0);
+  p := first;
+  for i := 0 to cnt -1 do
+  begin
+		c[0][0] := c[0][0] + DotProdVecs(a0[i], (a0[i]));
+		c[0][1] := c[0][1] + DotProdVecs(a0[i], (a1[i]));
+		c[1][0] := c[0][1];
+		c[1][1] := c[1][1] + DotProdVecs(a1[i], (a1[i]));
+    tmp := SubVecs(p.pt,
+      AddVecs(Mul(first.pt, B0(u[i])),
+      AddVecs(Mul(first.pt, B1(u[i])),
+      AddVecs(Mul(last.pt, B2(u[i])),
+      Mul(last.pt, B3(u[i]))))));
+    x[0] := x[0] + DotProdVecs(a0[i], tmp);
+    x[1] := x[1] + DotProdVecs(a1[i], tmp);
+    p := p.next;
+  end;
+  det_c0_c1 := c[0][0] * c[1][1] - c[1][0] * c[0][1];
+	det_c0_x := c[0][0] * x[1] - c[1][0] * x[0];
+	det_x_c1 := x[0] * c[1][1] - x[1] * c[0][1];
+  if det_c0_c1 = 0 then
+    alphaL := 0 else
+    alphaL := det_x_c1 / det_c0_c1;
+  if det_c0_c1 = 0 then
+    alphaR := 0 else
+    alphaR := det_c0_x / det_c0_c1;
+  //check for unlikely fit
+  if (alphaL > dist * 2) then alphaL := 0
+  else if (alphaR > dist * 2) then alphaR := 0;
+  epsilon := 1.0e-6 * dist;
+  SetLength(Result, 4);
+  Result[0] := first.pt;
+  Result[3] := last.pt;
+  if (alphaL < epsilon) or (alphaR < epsilon) then
+  begin
+    dist := dist / 3;
+    Result[1] := AddVecs(Result[0], Scale(firstTan, dist));
+    Result[2] := AddVecs(Result[3], Scale(lastTan, dist));
+  end else
+  begin
+    Result[1] := AddVecs(Result[0], Scale(firstTan, alphaL));
+    Result[2] := AddVecs(Result[3], Scale(lastTan, alphaR));
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.Reparameterize(first: PPt; cnt: integer;
+  const u: TArrayOfDouble; const bezier: TPathD): TArrayOfDouble;
+var
+  i: integer;
+begin
+  SetLength(Result, cnt);
+  for i := 0 to cnt -1 do
+  begin
+    Result[i] := NewtonRaphsonRootFind(bezier, first.pt, u[i]);
+    first := first.next;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function BezierII(degree: integer; const v: array of TPointD; t: double): TPointD;
+var
+  i,j: integer;
+  tmp: array[0..3] of TPointD;
+begin
+  Move(v[0], tmp[0], degree * sizeOf(TPointD));
+  for i := 1 to degree do
+    for j := 0 to degree - i do
+    begin
+      tmp[j].x := (1.0 - t) * tmp[j].x + t * tmp[j+1].x;
+      tmp[j].y := (1.0 - t) * tmp[j].y + t * tmp[j+1].y;
+    end;
+  Result := tmp[0];
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.ComputeMaxErrorSqrd(first, last: PPt;
+	const bezier: TPathD; const u: TArrayOfDouble;
+  out SplitPoint: PPt): double;
+var
+  i: integer;
+  distSqrd: double;
+  pt: TPointD;
+  p: PPt;
+begin
+	Result := 0;
+  i := 1;
+  SplitPoint := first.next;
+  p := first.next;
+	while p <> last do
+	begin
+		pt := BezierII(3, bezier, u[i]);
+		distSqrd := DistanceSqrd(pt, p.pt);
+		if (distSqrd >= Result) then
+    begin
+      Result := distSqrd;
+      SplitPoint := p;
+    end;
+    inc(i);
+    p := p.next;
+	end;
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.NewtonRaphsonRootFind(const q: TPathD;
+  const pt: TPointD; u: double): double;
+var
+  numerator, denominator: double;
+  qu, q1u, q2u: TPointD;
+  q1: array[0..2] of TPointD;
+  q2: array[0..1] of TPointD;
+begin
+  q1[0].x := (q[1].x - q[0].x) * 3.0;
+  q1[0].y := (q[1].y - q[0].y) * 3.0;
+  q1[1].x := (q[2].x - q[1].x) * 3.0;
+  q1[1].y := (q[2].y - q[1].y) * 3.0;
+  q1[2].x := (q[3].x - q[2].x) * 3.0;
+  q1[2].y := (q[3].y - q[2].y) * 3.0;
+  q2[0].x := (q1[1].x - q1[0].x) * 2.0;
+  q2[0].y := (q1[1].y - q1[0].y) * 2.0;
+  q2[1].x := (q1[2].x - q1[1].x) * 2.0;
+  q2[1].y := (q1[2].y - q1[1].y) * 2.0;
+  qu  := BezierII(3, q, u);
+  q1u := BezierII(2, q1, u);
+  q2u := BezierII(1, q2, u);
+  numerator := (qu.x - pt.x) * (q1u.x) + (qu.y - pt.y) * (q1u.y);
+  denominator := (q1u.x) * (q1u.x) + (q1u.y) * (q1u.y) +
+    (qu.x - pt.x) * (q2u.x) + (qu.y - pt.y) * (q2u.y);
+  if (denominator = 0) then
+    Result := u else
+    Result := u - (numerator / denominator);
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.FitCubic(first, last: PPt;
+  firstTan, lastTan: TPointD): Boolean;
+var
+  i, cnt: integer;
+  splitPoint: PPt;
+  centerTan: TPointD;
+  bezier: TPathD;
+  clps, uPrime: TArrayOfDouble;
+  maxErrorSqrd: double;
+const
+  maxRetries = 4;
+begin
+  Result := true;
+  cnt := Count(first, last);
+  if cnt = 2 then
+  begin
+    SetLength(bezier, 4);
+    bezier[0] := first.pt;
+    bezier[3] := last.pt;
+    bezier[1] := bezier[0];
+    bezier[2] := bezier[3];
+    AppendSolution(bezier);
+    Exit;
+  end
+  else if cnt = 3 then
+  begin
+    if TurnsLeft(first.prev.pt, first.pt, first.next.pt) =
+      TurnsLeft(first.pt, first.next.pt, last.pt) then
+        firstTan := ComputeCenterTangent(first);
+    if TurnsLeft(last.prev.pt, last.pt, last.next.pt) =
+      TurnsLeft(first.pt, first.next.pt, last.pt) then
+        lastTan := NegateVec(ComputeCenterTangent(last));
+  end;
+
+  clps := ChordLengthParameterize(first, cnt);
+  bezier := GenerateBezier(first, last, cnt, clps, firstTan, lastTan);
+  maxErrorSqrd := ComputeMaxErrorSqrd(first, last, bezier, clps, splitPoint);
+  if (maxErrorSqrd < tolSqrd) then
+  begin
+    AppendSolution(bezier);
+    Exit;
+  end;
+  if (maxErrorSqrd < tolSqrd * 4) then //close enough to try again
+  begin
+		for i := 1 to maxRetries do
+    begin
+      uPrime := Reparameterize(first, cnt, clps, bezier);
+      bezier := GenerateBezier(first, last, cnt, uPrime, firstTan, lastTan);
+      maxErrorSqrd :=
+        ComputeMaxErrorSqrd(first, last, bezier, uPrime, splitPoint);
+			if (maxErrorSqrd < tolSqrd) then
+      begin
+        AppendSolution(bezier);
+        Exit;
+			end;
+			clps := uPrime;
+		end;
+	end;
+  //We need to break the curve because it's too complex for a single Bezier.
+  //If we're changing direction then make this a 'hard' break (see below).
+  if TurnsLeft(splitPoint.prev.prev.pt, splitPoint.prev.pt, splitPoint.pt) <>
+    TurnsLeft(splitPoint.prev.pt, splitPoint.pt, splitPoint.next.pt) then
+  begin
+    centerTan := ComputeRightTangent(splitPoint);
+    FitCubic(first, splitPoint, firstTan, centerTan);
+    centerTan := ComputeLeftTangent(splitPoint);
+    FitCubic(splitPoint, last, centerTan, lastTan);
+  end else
+  begin
+    centerTan := ComputeCenterTangent(splitPoint);
+    FitCubic(first, splitPoint, firstTan, NegateVec(centerTan));
+    FitCubic(splitPoint, last, centerTan, lastTan);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function HardBreakCheck(ppt: PPt; compareLen: double): Boolean;
+var
+  q: double;
+const
+  longLen = 15;
+begin
+  //A 'break' means starting a new Bezier. A 'hard' break avoids smoothing
+  //whereas a 'soft' break will still be smoothed. There is as much art as
+  //science in determining where to smooth and where not to. For example,
+  //long edges should generally remain straight but how long does an edge
+  //have to be to be considered a 'long' edge?
+  if (ppt.prev.len * 4 < ppt.len) or (ppt.len * 4 < ppt.prev.len) then
+  begin
+    //We'll hard break whenever there's significant asymmetry between
+    //segment lengths because GenerateBezier() will perform poorly.
+    result := true;
+  end
+  else if ((ppt.prev.len > longLen) and (ppt.len > longLen)) then
+  begin
+    //hard break long segments only when turning by more than ~45 degrees
+    q := (Sqr(ppt.prev.len) + Sqr(ppt.len) - DistanceSqrd(ppt.prev.pt, ppt.next.pt)) /
+      (2 * ppt.prev.len * ppt.len); //Cosine Rule.
+    result := (1 - abs(q)) > 0.3;
+  end
+  else if ((TurnsLeft(ppt.prev.prev.pt, ppt.prev.pt, ppt.pt) =
+    TurnsRight(ppt.prev.pt, ppt.pt, ppt.next.pt)) and
+    (ppt.prev.len > compareLen) and (ppt.len > compareLen)) then
+  begin
+    //we'll also hard break whenever there's a significant inflection point
+    result := true;
+  end else
+  begin
+    //Finally, we'll also force a 'hard' break when there's a significant bend.
+    //Again uses the Cosine Rule.
+    q :=(Sqr(ppt.prev.len) + Sqr(ppt.len) -
+      DistanceSqrd(ppt.prev.pt, ppt.next.pt)) / (2 * ppt.prev.len * ppt.len);
+    Result := (q > -0.2); //ie more than 90%
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFitCurveContainer.FitCurve(const path: TPathD;
+  closed: Boolean; tolerance: double; minSegLength: double): TPathD;
+var
+  i, highI: integer;
+  d: double;
+  p, p2, pEnd: PPt;
+begin
+  //tolerance: specifies the maximum allowed variance between the existing
+  //vertices and the new Bezier curves. More tolerance will produce
+  //fewer Beziers and simpler paths, but at the cost of less precison.
+  tolSqrd := Sqr(Max(1, Min(10, tolerance))); //range 1..10
+  //minSegLength: Typically when vectorizing raster images, the produced
+  //vector paths will have many series of axis aligned segments that trace
+  //pixel boundaries. These paths will also contain many 1 unit segments at
+  //right angles to adjacent segments. Importantly, these very short segments
+  //will cause artifacts in the solution unless they are trimmed.
+  highI     := High(path);
+  if closed then
+    while (highI > 0) and (Distance(path[highI], path[0]) < minSegLength) do
+      dec(highI);
+  p := AddPt(path[0]);
+  for i := 1 to highI do
+  begin
+    d := Distance(p.pt, path[i]);
+    //skip line segments with lengths less than 'minSegLength'
+    if d < minSegLength then Continue;
+    p := AddPt(path[i]);
+    p.prev.len := d;
+    p.prev.vec := SubVecs(p.pt, p.prev.pt);
+  end;
+  p.len := Distance(ppts.pt, p.pt);
+  p.vec := SubVecs(p.next.pt, p.pt);
+  p := ppts;
+  if (p.next = p) or (closed and (p.next = p.prev)) then
+  begin
+    Clear;
+    result := nil;
+    Exit;
+  end;
+  //for closed paths, find a good starting point
+  if closed then
+  begin
+    repeat
+      if HardBreakCheck(p, tolerance) then break;
+      p := p.next;
+    until p = ppts;
+    pEnd := p;
+  end else
+    pEnd := ppts.prev;
+  p2 := p.next;
+  repeat
+    if HardBreakCheck(p2, tolerance) then
+    begin
+      FitCubic(p, p2, ComputeLeftTangent(p), ComputeRightTangent(p2));
+      p := p2;
+    end;
+    p2 := p2.next;
+  until (p2 = pEnd);
+  FitCubic(p, p2, ComputeLeftTangent(p), ComputeRightTangent(p2));
+  Result := solution;
+  Clear;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFitCurveContainer.AppendSolution(const bezier: TPathD);
+var
+  i, len: integer;
+begin
+  len := Length(solution);
+  if len > 0 then
+  begin
+    SetLength(solution, len + 3);
+    for i := 0 to 2 do
+      solution[len +i] := bezier[i +1];
+  end else
+    solution := bezier;
+end;
+//------------------------------------------------------------------------------
+
+function CurveFit(const path: TPathD; closed: Boolean;
+  tolerance: double; minSegLength: double): TPathD;
+var
+  paths, solution: TPathsD;
+begin
+  SetLength(paths, 1);
+  paths[0] := path;
+  solution := CurveFit(paths, closed, tolerance, minSegLength);
+  if solution <> nil then
+    Result := solution[0];
+end;
+//------------------------------------------------------------------------------
+
+function CurveFit(const paths: TPathsD; closed: Boolean;
+  tolerance: double; minSegLength: double): TPathsD;
+var
+  i,j, len: integer;
+begin
+  j := 0;
+  len := Length(paths);
+  SetLength(Result, len);
+  with TFitCurveContainer.Create do
+  try
+    for i := 0 to len -1 do
+      if (paths[i] <> nil) and (Abs(Area(paths[i])) > Sqr(tolerance)) then
+      begin
+        Result[j] := FitCurve(paths[i], closed, tolerance, minSegLength);
+        inc(j);
+      end;
+  finally
+    Free;
+  end;
+  SetLength(Result, j);
+end;
+//------------------------------------------------------------------------------
+
+
+end.

+ 472 - 0
components/Image32/source/Img32.Fmt.SVG.pas

@@ -0,0 +1,472 @@
+unit Img32.Fmt.SVG;
+
+(*******************************************************************************
+* Author    :  Angus Johnson                                                   *
+* Version   :  4.3.1                                                           *
+* Date      :  5 October 2022                                                  *
+* Website   :  http://www.angusj.com                                           *
+* Copyright :  Angus Johnson 2019-2022                                         *
+* Purpose   :  SVG file format extension for TImage32                          *
+* License   :  http://www.boost.org/LICENSE_1_0.txt                            *
+*******************************************************************************)
+
+interface
+
+{$I Img32.inc}
+
+uses
+  {$IFDEF MSWINDOWS} Windows, {$ENDIF} SysUtils, Classes, Math,
+  {$IFDEF XPLAT_GENERICS} Generics.Collections, Generics.Defaults, {$ENDIF}
+  Img32, Img32.Vector, Img32.SVG.Reader;
+
+type
+  TImageFormat_SVG = class(TImageFormat)
+  public
+    class function IsValidImageStream(stream: TStream): Boolean; override;
+    function LoadFromStream(stream: TStream; img32: TImage32): Boolean; override;
+    procedure SaveToStream(stream: TStream; img32: TImage32); override;
+    class function CanCopyToClipboard: Boolean; override;
+    class function CopyToClipboard(img32: TImage32): Boolean; override;
+    class function CanPasteFromClipboard: Boolean; override;
+    class function PasteFromClipboard(img32: TImage32): Boolean; override;
+  end;
+
+  TSvgListObject = class
+    xml           : string;
+    name          : string;
+  end;
+
+  TSvgImageList32 = class(TInterfacedObj, INotifySender)
+  private
+    fReader     : TSvgReader;
+{$IFDEF XPLAT_GENERICS}
+    fList       : TList<TSvgListObject>;
+{$ELSE}
+    fList       : TList;
+{$ENDIF}
+    fDefWidth       : integer;
+    fDefHeight      : integer;
+    fRecipientList  : TRecipients;
+    fUpdateCnt      : integer;
+{$IFDEF MSWINDOWS}
+    fResName        : string;
+    procedure SetResName(const resName: string);
+{$ENDIF}
+    procedure SetDefWidth(value: integer);
+    procedure SetDefHeight(value: integer);
+  protected
+    procedure Changed; virtual;
+    procedure BeginUpdate;
+    procedure EndUpdate;
+    procedure NotifyRecipients(notifyFlag: TImg32Notification);
+  public
+    constructor Create;
+    destructor Destroy; override;
+    procedure Clear;
+    function Count: integer;
+    function Find(const aName: string): integer;
+    procedure AddRecipient(recipient: INotifyRecipient);
+    procedure DeleteRecipient(recipient: INotifyRecipient);
+    function CreateImage(index: integer): TImage32;
+    procedure GetImage(index: integer; image: TImage32); overload;
+    procedure GetImage(index: integer; image: TImage32; out aName: string); overload;
+    procedure Add(const aName, xml: string);
+    procedure AddFromFile(const aName, filename: string);
+    procedure AddFromResource(const aName, resName: string; resType: PChar);
+    procedure Insert(index: integer; const name, xml: string);
+    procedure Move(currentIndex, newIndex: integer);
+    procedure Delete(index: integer);
+    property DefaultWidth: integer read fDefWidth write SetDefWidth;
+    property DefaultHeight: integer read fDefHeight write SetDefHeight;
+{$IFDEF MSWINDOWS}
+    property ResourceName: string read fResName write SetResName;
+{$ENDIF}
+  end;
+
+var
+  defaultSvgWidth: integer = 800;
+  defaultSvgHeight: integer = 600;
+
+implementation
+
+//------------------------------------------------------------------------------
+// Three routines used to enumerate a resource type
+//------------------------------------------------------------------------------
+
+function Is_IntResource(lpszType: PChar): Boolean;
+begin
+  Result := NativeUInt(lpszType) shr 16 = 0;
+end;
+//------------------------------------------------------------------------------
+
+function ResourceNameToString(lpszName: PChar): string;
+begin
+  if Is_IntResource(lpszName) then
+    Result := '#' + IntToStr(NativeUInt(lpszName)) else
+    Result := lpszName;
+end;
+//------------------------------------------------------------------------------
+
+function EnumResNameProc(hModule: HMODULE; lpszType, lpszName: PChar;
+  lParam: NativeInt): Boolean; stdcall;
+var
+  n: string;
+begin
+  n:= ResourceNameToString(lpszName);
+  TSvgImageList32(lParam).AddFromResource(n, n, lpszType);
+  Result := true;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgImageList32
+//------------------------------------------------------------------------------
+
+constructor TSvgImageList32.Create;
+begin
+  fReader := TSvgReader.Create;
+{$IFDEF XPLAT_GENERICS}
+  fList := TList<TSvgListObject>.Create;
+{$ELSE}
+  fList := TList.Create;
+{$ENDIF}
+end;
+//------------------------------------------------------------------------------
+
+destructor TSvgImageList32.Destroy;
+begin
+  NotifyRecipients(inDestroy);
+  Clear;
+  fList.Free;
+  fReader.Free;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+{$IFDEF MSWINDOWS}
+procedure TSvgImageList32.SetResName(const resName: string);
+begin
+  if fResName = resName then Exit;
+  fResName := resName;
+  BeginUpdate;
+  try
+    Clear;
+    EnumResourceNames(HInstance, PChar(resName), @EnumResNameProc, lParam(self));
+  finally
+    EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+{$ENDIF}
+
+function TSvgImageList32.Count: integer;
+begin
+  result := fList.Count;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.Clear;
+var
+  i: integer;
+begin
+  for i := 0 to fList.Count -1 do
+    TSvgListObject(fList[i]).Free;
+  fList.Clear;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgImageList32.Find(const aName: string): integer;
+var
+  i: integer;
+begin
+  for i := 0 to fList.Count -1 do
+    with TSvgListObject(fList[i]) do
+      if SameText(name, aName) then
+      begin
+        Result := i;
+        Exit;
+      end;
+  Result := -1;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.GetImage(index: integer; image: TImage32; out aName: string);
+begin
+  if not Assigned(image) or (index < 0) or (index >= count) then Exit;
+  if image.IsEmpty then
+   image.SetSize(fDefWidth, fDefHeight);
+  with TSvgListObject(fList[index]) do
+  begin
+    fReader.LoadFromString(xml);
+    aName := name;
+  end;
+  fReader.DrawImage(image, true);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgImageList32.CreateImage(index: integer): TImage32;
+begin
+  Result := TImage32.Create(DefaultWidth, DefaultHeight);
+  GetImage(index, Result);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.GetImage(index: integer; image: TImage32);
+var
+  dummy: string;
+begin
+  GetImage(index, image, dummy);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.Add(const aName, xml: string);
+begin
+  Insert(count, aName, xml);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.AddFromFile(const aName, filename: string);
+begin
+  if not FileExists(filename) then Exit;
+  with TStringList.Create do
+  try
+    LoadFromFile(filename);
+    Self.Insert(Self.Count, aName, Text);
+  finally
+    Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.AddFromResource(const aName, resName: string; resType: PChar);
+var
+  rs: TResourceStream;
+  ansi: AnsiString;
+begin
+  rs := TResourceStream.Create(hInstance, resName, resType);
+  try
+    SetLength(ansi, rs.Size);
+    rs.Read(ansi[1], rs.Size);
+    Self.Insert(Self.Count, aName, string(ansi));
+  finally
+    rs.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.Insert(index: integer; const name, xml: string);
+var
+  lo: TSvgListObject;
+begin
+  if index < 0 then index := 0
+  else if index > Count then index := Count;
+
+  lo := TSvgListObject.Create;
+  lo.name := name;
+  lo.xml := xml;
+  fList.Insert(index, lo);
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.Move(currentIndex, newIndex: integer);
+begin
+  fList.Move(currentIndex, newIndex);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.Delete(index: integer);
+begin
+  TSvgListObject(fList[index]).Free;
+  fList.Delete(index);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.BeginUpdate;
+begin
+  inc(fUpdateCnt);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.EndUpdate;
+begin
+  dec(fUpdateCnt);
+  if fUpdateCnt = 0 then Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.Changed;
+begin
+  if (fUpdateCnt = 0) then
+    NotifyRecipients(inStateChange);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.SetDefWidth(value: integer);
+begin
+  if fDefWidth = value then Exit;
+  fDefWidth := value;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.SetDefHeight(value: integer);
+begin
+  if fDefHeight = value then Exit;
+  fDefHeight := value;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.AddRecipient(recipient: INotifyRecipient);
+var
+  len: integer;
+begin
+  len := Length(fRecipientList);
+  SetLength(fRecipientList, len+1);
+  fRecipientList[len] := Recipient;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.DeleteRecipient(recipient: INotifyRecipient);
+var
+  i, highI: integer;
+begin
+  highI := High(fRecipientList);
+  i := highI;
+  while (i >= 0) and (fRecipientList[i] <> Recipient) do dec(i);
+  if i < 0 then Exit;
+  if i < highI then
+    System.Move(fRecipientList[i+1], fRecipientList[i],
+      (highI - i) * SizeOf(INotifyRecipient));
+  SetLength(fRecipientList, highI);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgImageList32.NotifyRecipients(notifyFlag: TImg32Notification);
+var
+  i: integer;
+begin
+  if fUpdateCnt > 0 then Exit;
+  for i := High(fRecipientList) downto 0 do
+    try
+      //when destroying in a finalization section
+      //it's possible for recipients to have been destroyed
+      //without their destructors being called.
+      fRecipientList[i].ReceiveNotification(self, notifyFlag);
+    except
+    end;
+end;
+
+//------------------------------------------------------------------------------
+// Loading (reading) SVG images from file ...
+//------------------------------------------------------------------------------
+
+function TImageFormat_SVG.LoadFromStream(stream: TStream; img32: TImage32): Boolean;
+var
+  r: TRectWH;
+  w,h, sx,sy: double;
+begin
+  with TSvgReader.Create do
+  try
+    Result := LoadFromStream(stream);
+    if not Result then Exit;
+    r := GetViewbox(img32.Width, img32.Height);
+
+    img32.BeginUpdate;
+    try
+      if img32.IsEmpty and not r.IsEmpty then
+        img32.SetSize(Round(r.Width), Round(r.Height))
+      else if not r.IsEmpty then
+      begin
+        //then scale the SVG to fit image
+        w := r.Width;
+        h := r.Height;
+        sx := img32.Width / w;
+        sy := img32.Height / h;
+        if sy < sx then sx := sy;
+        if not(SameValue(sx, 1, 0.00001)) then
+        begin
+          w := w * sx;
+          h := h * sx;
+        end;
+        img32.SetSize(Round(w), Round(h));
+      end
+      else
+        img32.SetSize(defaultSvgWidth, defaultSvgHeight);
+
+      //draw the SVG image to fit inside the canvas
+      DrawImage(img32, True);
+    finally
+      img32.EndUpdate;
+    end;
+  finally
+    Free;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// Saving (writing) SVG images to file (not currently implemented) ...
+//------------------------------------------------------------------------------
+
+class function TImageFormat_SVG.IsValidImageStream(stream: TStream): Boolean;
+var
+  i, savedPos, len: integer;
+  buff: array [1..1024] of AnsiChar;
+begin
+  Result := false;
+  savedPos := stream.Position;
+  len := Min(1024, stream.Size - savedPos);
+  stream.Read(buff[1], len);
+  stream.Position := savedPos;
+  for i := 1 to len -4 do
+  begin
+    if buff[i] < #9 then Exit
+    else if (buff[i] = '<') and
+      (buff[i +1] = 's') and
+      (buff[i +2] = 'v') and
+      (buff[i +3] = 'g') then
+    begin
+      Result := true;
+      break;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImageFormat_SVG.SaveToStream(stream: TStream; img32: TImage32);
+begin
+  //not enabled
+end;
+//------------------------------------------------------------------------------
+
+class function TImageFormat_SVG.CanCopyToClipboard: Boolean;
+begin
+  Result := false;
+end;
+//------------------------------------------------------------------------------
+
+class function TImageFormat_SVG.CopyToClipboard(img32: TImage32): Boolean;
+begin
+  Result := false;
+end;
+//------------------------------------------------------------------------------
+
+class function TImageFormat_SVG.CanPasteFromClipboard: Boolean;
+begin
+  Result := false;
+end;
+//------------------------------------------------------------------------------
+
+class function TImageFormat_SVG.PasteFromClipboard(img32: TImage32): Boolean;
+begin
+  Result := false;
+end;
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+initialization
+  TImage32.RegisterImageFormatClass('SVG', TImageFormat_SVG, cpLow);
+
+end.

+ 424 - 0
components/Image32/source/Img32.Resamplers.pas

@@ -0,0 +1,424 @@
+unit Img32.Resamplers;
+
+(*******************************************************************************
+* Author    :  Angus Johnson                                                   *
+* Version   :  4.3                                                             *
+* Date      :  27 September 2022                                               *
+* Website   :  http://www.angusj.com                                           *
+* Copyright :  Angus Johnson 2019-2021                                         *
+* Purpose   :  For image transformations (scaling, rotating etc.)              *
+* License   :  http://www.boost.org/LICENSE_1_0.txt                            *
+*******************************************************************************)
+
+interface
+
+{$I Img32.inc}
+
+uses
+  SysUtils, Classes, Img32;
+
+//BoxDownSampling: As the name implies, this routine is only intended for
+//image down-sampling (ie when shrinking images) where it generally performs
+//better than other resamplers which tend to lose too much detail. However,
+//because this routine is inferior to other resamplers when performing other
+//transformations (ie when enlarging, rotating, and skewing images), it's not
+//intended as a general purpose resampler.
+procedure BoxDownSampling(Image: TImage32; newWidth, newHeight: Integer);
+
+(* The following functions are registered in the initialization section below
+function NearestResampler(img: TImage32; x256, y256: Integer): TColor32;
+function BilinearResample(img: TImage32; x256, y256: Integer): TColor32;
+function BicubicResample(img: TImage32; x256, y256: Integer): TColor32;
+*)
+
+implementation
+
+uses
+  Img32.Transform;
+
+//------------------------------------------------------------------------------
+// NearestNeighbor resampler
+//------------------------------------------------------------------------------
+
+function NearestResampler(img: TImage32; x256, y256: Integer): TColor32;
+begin
+  if (x256 < -$7f) then
+  begin
+    Result := clNone32;
+    Exit;
+  end;
+
+  if (y256 < -$7f) then
+  begin
+    Result := clNone32;
+    Exit;
+  end;
+
+  if (x256 and $FF > $7F) then inc(x256, $100);
+  x256 := x256 shr 8;
+  if y256 and $FF > $7F then inc(y256, $100);
+  y256 := y256 shr 8;
+
+  if (x256 < 0) or (x256 >= img.Width) or
+    (y256 < 0) or (y256 >= img.Height) then
+      Result := clNone32 else
+      Result := img.Pixels[y256 * img.Width + x256];
+end;
+
+//------------------------------------------------------------------------------
+// BiLinear resampler
+//------------------------------------------------------------------------------
+
+function BilinearResample(img: TImage32; x256, y256: Integer): TColor32;
+var
+  xi,yi, weight: Integer;
+  iw, ih: integer;
+  pixels: TArrayOfColor32;
+  color: TWeightedColor;
+  xf, yf: cardinal;
+begin
+  iw := img.Width;
+  ih := img.Height;
+  pixels := img.Pixels;
+
+  if (x256 <= -$100) or (x256 >= iw *$100) or
+     (y256 <= -$100) or (y256 >= ih *$100) then
+  begin
+    result := clNone32;
+    Exit;
+  end;
+
+  if x256 < 0 then xi := -1
+  else xi := x256 shr 8;
+
+  if y256 < 0 then yi := -1
+  else yi := y256 shr 8;
+
+  xf := x256 and $FF;
+  yf := y256 and $FF;
+
+  color.Reset;
+
+  weight := (($100 - xf) * ($100 - yf)) shr 8;        //top-left
+  if (xi < 0) or (yi < 0) then
+    color.AddWeight(weight) else
+    color.Add(pixels[xi + yi * iw], weight);
+
+  weight := (xf * ($100 - yf)) shr 8;                 //top-right
+  if ((xi+1) >= iw) or (yi < 0) then
+    color.AddWeight(weight) else
+    color.Add(pixels[(xi+1) + yi * iw], weight);
+
+  weight := (($100 - xf) * yf) shr 8;                 //bottom-left
+  if (xi < 0) or ((yi+1) >= ih) then
+    color.AddWeight(weight) else
+    color.Add(pixels[(xi) + (yi+1) * iw], weight);
+
+  weight := (xf * yf) shr 8;                          //bottom-right
+  if (xi + 1 >= iw) or (yi + 1 >= ih) then
+    color.AddWeight(weight) else
+    color.Add(pixels[(xi+1) + (yi+1) * iw], weight);
+
+  Result := color.Color;
+end;
+
+//------------------------------------------------------------------------------
+// BiCubic resampler
+//------------------------------------------------------------------------------
+
+type
+  TBiCubicEdgeAdjust = (eaNone, eaOne, eaTwo, eaThree, eaFour);
+
+var
+  byteFrac: array [0..255] of double;
+  byteFracSq: array [0..255] of double;
+  byteFracCubed: array [0..255] of double;
+
+//------------------------------------------------------------------------------
+
+function CubicHermite(aclr: PColor32; t: Byte; bce: TBiCubicEdgeAdjust): TColor32;
+var
+  a,b,c,d: PARGB;
+  q: TARGB;
+	aa, bb, cc: integer;
+  t1, t2, t3: double;
+  res: TARGB absolute Result;
+const
+  clTrans: TColor32 = clNone32;
+begin
+  case bce of
+    eaOne:
+      begin
+        a := @clTrans;
+        b := @clTrans;
+        c := PARGB(aclr);
+        Inc(aclr);
+        d := PARGB(aclr);
+      end;
+    eaTwo:
+      begin
+        a := PARGB(aclr);
+        b := a;
+        Inc(aclr);
+        c := PARGB(aclr);
+        Inc(aclr);
+        d := PARGB(aclr);
+      end;
+    eaThree:
+      begin
+        a := PARGB(aclr);
+        Inc(aclr);
+        b := PARGB(aclr);
+        Inc(aclr);
+        c := PARGB(aclr);
+        d := c;
+      end;
+    eaFour:
+      begin
+        a := PARGB(aclr);
+        Inc(aclr);
+        b := PARGB(aclr);
+        c := @clTrans;
+        d := @clTrans;
+      end;
+    else
+      begin
+        a := PARGB(aclr);
+        Inc(aclr);
+        b := PARGB(aclr);
+        Inc(aclr);
+        c := PARGB(aclr);
+        Inc(aclr);
+        d := PARGB(aclr);
+      end;
+  end;
+
+  if (b.A = 0) and (c.A = 0) then
+  begin
+    result := clNone32;
+    Exit;
+  end
+  else if b.A = 0 then
+  begin
+    q := c^;
+    q.A := 0;
+    b := @q;
+  end;
+  if c.A = 0 then
+  begin
+    q := b^;
+    q.A := 0;
+    c := @q;
+  end;
+
+  t1 := byteFrac[t];
+  t2 := byteFracSq[t];
+  t3 := byteFracCubed[t];
+
+	aa := Integer(-a.A + 3*b.A - 3*c.A + d.A) div 2;
+	bb := Integer(2*a.A - 5*b.A + 4*c.A - d.A) div 2;
+	cc := Integer(-a.A + c.A) div 2;
+  Res.A := ClampByte(aa*t3 + bb*t2 + cc*t1 + b.A);
+
+	aa := Integer(-a.R + 3*b.R - 3*c.R + d.R) div 2;
+	bb := Integer(2*a.R - 5*b.R + 4*c.R - d.R) div 2;
+	cc := Integer(-a.R + c.R) div 2;
+  Res.R := ClampByte(aa*t3 + bb*t2 + cc*t1 + b.R);
+
+	aa := Integer(-a.G + 3*b.G - 3*c.G + d.G) div 2;
+	bb := Integer(2*a.G - 5*b.G + 4*c.G - d.G) div 2;
+	cc := Integer(-a.G + c.G) div 2;
+  Res.G := ClampByte(aa*t3 + bb*t2 + cc*t1 + b.G);
+
+	aa := Integer(-a.B + 3*b.B - 3*c.B + d.B) div 2;
+	bb := Integer(2*a.B - 5*b.B + 4*c.B - d.B) div 2;
+	cc := Integer(-a.B + c.B) div 2;
+  Res.B := ClampByte(aa*t3 + bb*t2 + cc*t1 + b.B);
+end;
+//------------------------------------------------------------------------------
+
+function BicubicResample(img: TImage32; x256, y256: Integer): TColor32;
+var
+  i, dx,dy, pi, iw, w,h: Integer;
+  c: array[0..3] of TColor32;
+  x, y: Integer;
+  bceX, bceY: TBiCubicEdgeAdjust;
+begin
+  Result := clNone32;
+
+  iw := img.Width;
+  w := iw -1;
+  h := img.Height -1;
+
+  x := Abs(x256) shr 8;
+  y := Abs(y256) shr 8;
+
+  if (x256 < -$FF) or (x > w) or  (y256 < -$FF) or (y > h) then Exit;
+
+  if (x256 < 0) then bceX := eaOne
+  else if (x = 0) then bceX := eaTwo
+  else if (x256 > w shl 8) then bceX := eaFour
+  else if (x256 > (w -1) shl 8) then bceX := eaThree
+  else bceX := eaNone;
+
+  if (bceX = eaOne) or (bceX = eaTwo) then dx := 1
+  else dx := 0;
+
+  if (y256 < 0) then bceY := eaOne
+  else if y = 0 then bceY := eaTwo
+  else if y = h -1 then bceY := eaThree
+  else if y = h then bceY := eaFour
+  else bceY := eaNone;
+
+  if (bceY = eaOne) or (bceY = eaTwo) then dy := 1
+  else dy := 0;
+
+  pi := (y -1 +dy) * iw + (x -1 + dx);
+
+  if bceY = eaFour then dx := 2
+  else if bceY = eaThree then dx := 1
+  else dx := 0;
+
+  for i := dy to 3 -dx do
+  begin
+    c[i] := CubicHermite(@img.Pixels[pi], x256 and $FF, bceX);
+    inc(pi, iw);
+  end;
+  Result := CubicHermite(@c[dy], y256 and $FF, bceY);
+end;
+
+//------------------------------------------------------------------------------
+// BoxDownSampling and related functions
+//------------------------------------------------------------------------------
+
+function GetWeightedColor(const srcBits: TArrayOfColor32;
+  x256, y256, xx256, yy256, maxX: Integer): TColor32;
+var
+  i, j, xi, yi, xxi, yyi, weight: Integer;
+  xf, yf, xxf, yyf: cardinal;
+  color: TWeightedColor;
+begin
+  //This function performs 'box sampling' and differs from GetWeightedPixel
+  //(bilinear resampling) in one important aspect - it accommodates weighting
+  //any number of pixels (rather than just adjacent pixels) and this produces
+  //better image quality when significantly downsizing.
+
+  //Note: there's no range checking here, so the precondition is that the
+  //supplied boundary values are within the bounds of the srcBits array.
+
+  color.Reset;
+
+  xi := x256 shr 8; xf := x256 and $FF;
+  yi := y256 shr 8; yf := y256 and $FF;
+  xxi := xx256 shr 8; xxf := xx256 and $FF;
+  yyi := yy256 shr 8; yyf := yy256 and $FF;
+
+  //1. average the corners ...
+  weight := (($100 - xf) * ($100 - yf)) shr 8;
+  color.Add(srcBits[xi + yi * maxX], weight);
+  weight := (xxf * ($100 - yf)) shr 8;
+  if (weight <> 0) then color.Add(srcBits[xxi + yi * maxX], weight);
+  weight := (($100 - xf) * yyf) shr 8;
+  if (weight <> 0) then color.Add(srcBits[xi + yyi * maxX], weight);
+  weight := (xxf * yyf) shr 8;
+  if (weight <> 0) then color.Add(srcBits[xxi + yyi * maxX], weight);
+
+  //2. average the edges
+  if (yi +1 < yyi) then
+  begin
+    xf := $100 - xf;
+    for i := yi + 1 to yyi - 1 do
+      color.Add(srcBits[xi + i * maxX], xf);
+    if (xxf <> 0) then
+      for i := yi + 1 to yyi - 1 do
+        color.Add(srcBits[xxi + i * maxX], xxf);
+  end;
+  if (xi + 1 < xxi) then
+  begin
+    yf := $100 - yf;
+    for i := xi + 1 to xxi - 1 do
+      color.Add(srcBits[i + yi * maxX], yf);
+    if (yyf <> 0) then
+      for i := xi + 1 to xxi - 1 do
+        color.Add(srcBits[i + yyi * maxX], yyf);
+  end;
+
+  //3. average the non-fractional pixel 'internals' ...
+  for i := xi + 1 to xxi - 1 do
+    for j := yi + 1 to yyi - 1 do
+      color.Add(srcBits[i + j * maxX], $100);
+
+  //4. finally get the weighted color ...
+  if color.AddCount = 0 then
+    Result := srcBits[xi + yi * maxX] else
+    Result := color.Color;
+end;
+//------------------------------------------------------------------------------
+
+procedure BoxDownSampling(Image: TImage32; newWidth, newHeight: Integer);
+var
+  x,y, x256,y256,xx256,yy256: Integer;
+  sx,sy: double;
+  tmp: TArrayOfColor32;
+  pc: PColor32;
+  scaledX: array of Integer;
+begin
+  sx := Image.Width/newWidth * 256;
+  sy := Image.Height/newHeight * 256;
+  SetLength(tmp, newWidth * newHeight);
+
+  SetLength(scaledX, newWidth +1); //+1 for fractional overrun
+  for x := 0 to newWidth -1 do
+    scaledX[x] := Round((x+1) * sx);
+
+  y256 := 0;
+  pc := @tmp[0];
+  for y := 0 to newHeight - 1 do
+  begin
+    x256 := 0;
+    yy256 := Round((y+1) * sy);
+    for x := 0 to newWidth - 1 do
+    begin
+      xx256 := scaledX[x];
+      pc^ := GetWeightedColor(Image.Pixels,
+        x256, y256, xx256, yy256, Image.Width);
+      x256 := xx256;
+      inc(pc);
+    end;
+    y256 := yy256;
+  end;
+
+  Image.BeginUpdate;
+  Image.SetSize(newWidth, newHeight);
+  Move(tmp[0], Image.Pixels[0], newWidth * newHeight * SizeOf(TColor32));
+  Image.EndUpdate;
+end;
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+procedure InitByteExponents;
+var
+  i: integer;
+const
+  inv255     : double = 1/255;
+  inv255sqrd : double = 1/(255*255);
+  inv255cubed: double = 1/(255*255*255);
+begin
+  for i := 0 to 255 do
+  begin
+    byteFrac[i]  := i     *inv255;
+    byteFracSq[i]  := i*i   *inv255sqrd;
+    byteFracCubed[i] := i*i*i *inv255cubed;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+initialization
+  InitByteExponents;
+
+  rNearestResampler  := RegisterResampler(NearestResampler, 'NearestNeighbor');
+  rBilinearResampler := RegisterResampler(BilinearResample, 'Bilinear');
+  rBicubicResampler  := RegisterResampler(BicubicResample, 'HermiteBicubic');
+  DefaultResampler   := rBilinearResampler;
+
+end.

+ 2188 - 0
components/Image32/source/Img32.SVG.Core.pas

@@ -0,0 +1,2188 @@
+unit Img32.SVG.Core;
+
+(*******************************************************************************
+* Author    :  Angus Johnson                                                   *
+* Version   :  4.3                                                             *
+* Date      :  27 September 2022                                               *
+* Website   :  http://www.angusj.com                                           *
+* Copyright :  Angus Johnson 2019-2022                                         *
+*                                                                              *
+* 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                            *
+*******************************************************************************)
+
+interface
+
+{$I Img32.inc}
+
+uses
+  SysUtils, Classes, Types, Math,
+  {$IFDEF XPLAT_GENERICS} Generics.Collections, Generics.Defaults,{$ENDIF}
+  Img32, Img32.Vector, Img32.Text, Img32.Transform;
+
+{$IFDEF ZEROBASEDSTR}
+  {$ZEROBASEDSTRINGS OFF}
+{$ENDIF}
+
+type
+  TSvgEncoding = (eUnknown, eUtf8, eUnicodeLE, eUnicodeBE);
+
+  TUnitType = (utUnknown, utNumber, utPercent, utEm, utEx, utPixel,
+    utCm, utMm, utInch, utPt, utPica, utDegree, utRadian);
+
+  //////////////////////////////////////////////////////////////////////
+  // TValue - Structure to store numerics with measurement units.
+  // See https://www.w3.org/TR/SVG/types.html#InterfaceSVGLength
+  // and https://www.w3.org/TR/SVG/types.html#InterfaceSVGAngle
+  //////////////////////////////////////////////////////////////////////
+
+  //Unfortunately unit-less values can exhibit ambiguity, especially when their
+  //values are small (eg < 1.0). These values can be either absolute values or
+  //relative values (ie relative to the supplied dimension size).
+  //The 'assumeRelValBelow' parameter (see below) attempts to address this
+  //ambiguity, such that unit-less values will be assumed to be 'relative' when
+  //'rawVal' is less than the supplied 'assumeRelValBelow' value.
+
+  TValue = {$IFDEF RECORD_METHODS} record {$ELSE} object {$ENDIF}
+    rawVal    : double;
+    unitType  : TUnitType;
+    procedure Init;
+    procedure SetValue(val: double; unitTyp: TUnitType = utNumber);
+    function  GetValue(relSize: double; assumeRelValBelow: Double): double;
+    function  GetValueXY(const relSize: TRectD; assumeRelValBelow: Double): double;
+    function  IsValid: Boolean;
+    function  IsRelativeValue(assumeRelValBelow: double): Boolean;
+      {$IFDEF INLINE} inline; {$ENDIF}
+    function  HasFontUnits: Boolean;
+    function  HasAngleUnits: Boolean;
+  end;
+
+  TValuePt = {$IFDEF RECORD_METHODS} record {$ELSE} object {$ENDIF}
+    X, Y    : TValue;
+    procedure Init;
+    function  GetPoint(const relSize: double; assumeRelValBelow: Double): TPointD; overload;
+    function  GetPoint(const relSize: TRectD; assumeRelValBelow: Double): TPointD; overload;
+    function  IsValid: Boolean;
+  end;
+
+  TValueRecWH = {$IFDEF RECORD_METHODS} record {$ELSE} object {$ENDIF}
+    left    : TValue;
+    top     : TValue;
+    width   : TValue;
+    height  : TValue;
+    procedure Init;
+    function  GetRectD(const relSize: TRectD; assumeRelValBelow: Double): TRectD; overload;
+    function  GetRectD(relSize: double; assumeRelValBelow: Double): TRectD; overload;
+    function  GetRectWH(const relSize: TRectD; assumeRelValBelow: Double): TRectWH;
+    function  IsValid: Boolean;
+    function  IsEmpty: Boolean;
+  end;
+
+  {$IFNDEF UNICODE}
+  UTF8Char  = Char;
+  PUTF8Char = PChar;
+  {$ELSE}
+  {$IF COMPILERVERSION < 31}
+  UTF8Char = AnsiChar;
+  PUTF8Char = PAnsiChar;
+  {$IFEND}
+  {$ENDIF}
+
+  TSvgItalicSyle  = (sfsUndefined, sfsNone, sfsItalic);
+  TFontDecoration = (fdUndefined, fdNone, fdUnderline, fdStrikeThrough);
+  TSvgTextAlign = (staUndefined, staLeft, staCenter, staRight);
+
+  TSVGFontInfo = record
+    family      : TTtfFontFamily;
+    size        : double;
+    spacing     : double;
+    textLength  : double;
+    italic      : TSvgItalicSyle;
+    weight      : Integer;
+    align       : TSvgTextAlign;
+    decoration  : TFontDecoration;
+    baseShift   : TValue;
+  end;
+
+  //////////////////////////////////////////////////////////////////////
+  // TClassStylesList: custom TStringList that stores ansistring objects
+  //////////////////////////////////////////////////////////////////////
+
+  PAnsStringiRec = ^TAnsiStringRec;   //used internally by TClassStylesList
+  TAnsiStringRec = record
+    ansi  : UTF8String;
+  end;
+
+  TClassStylesList = class
+  private
+    fList : TStringList;
+  public
+    constructor Create;
+    destructor Destroy; override;
+    function  AddAppendStyle(const classname: string; const ansi: UTF8String): integer;
+    function  GetStyle(const classname: UTF8String): UTF8String;
+    procedure Clear;
+  end;
+
+  //////////////////////////////////////////////////////////////////////
+  // TSvgParser and associated classes - a simple parser for SVG xml
+  //////////////////////////////////////////////////////////////////////
+
+  PSvgAttrib = ^TSvgAttrib;   //element attribute
+  TSvgAttrib = record
+    hash      : Cardinal;     //hashed name
+    name      : UTF8String;
+    value     : UTF8String;
+  end;
+
+  TSvgParser = class;
+
+  TXmlEl = class              //base element class
+  private
+    {$IFDEF XPLAT_GENERICS}
+    attribs     : TList <PSvgAttrib>;
+    {$ELSE}
+    attribs     : TList;
+    {$ENDIF}
+    function GetAttrib(index: integer): PSvgAttrib;
+    function GetAttribCount: integer;
+  public
+    {$IFDEF XPLAT_GENERICS}
+    childs      : TList<TXmlEl>;
+    {$ELSE}
+    childs      : TList;
+    {$ENDIF}
+    name        : UTF8String;
+    owner       : TSvgParser;
+    hash        : Cardinal;
+    text        : UTF8String;
+    selfClosed  : Boolean;
+    constructor Create(owner: TSvgParser); virtual;
+    destructor  Destroy; override;
+    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;
+    function    ParseAttributes(var c: PUTF8Char; endC: PUTF8Char): Boolean; virtual;
+    procedure   ParseStyleAttribute(const style: UTF8String);
+    property    Attrib[index: integer]: PSvgAttrib read GetAttrib;
+    property    AttribCount: integer read GetAttribCount;
+  end;
+
+  TDocTypeEl = class(TXmlEl)
+  private
+    procedure   SkipWord(var c, endC: PUTF8Char);
+    function    ParseEntities(var c, endC: PUTF8Char): Boolean;
+  public
+    function    ParseAttributes(var c: PUTF8Char; endC: PUTF8Char): Boolean; override;
+  end;
+
+  TSvgTreeEl = class(TXmlEl)
+  public
+    constructor Create(owner: TSvgParser); override;
+    procedure   Clear; override;
+    function    ParseHeader(var c: PUTF8Char; endC: PUTF8Char): Boolean; override;
+  end;
+
+  TSvgParser = class
+  private
+    svgStream : TMemoryStream;
+    procedure ParseStream;
+  public
+    classStyles :TClassStylesList;
+    xmlHeader   : TXmlEl;
+    docType     : TDocTypeEl;
+    svgTree     : TSvgTreeEl;
+    constructor Create;
+    destructor  Destroy; override;
+    procedure Clear;
+    function  FindEntity(hash: Cardinal): PSvgAttrib;
+    function  LoadFromFile(const filename: string): Boolean;
+    function  LoadFromStream(stream: TStream): Boolean;
+    function  LoadFromString(const str: string): Boolean;
+  end;
+
+  //////////////////////////////////////////////////////////////////////
+  // Miscellaneous SVG functions
+  //////////////////////////////////////////////////////////////////////
+
+  //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 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 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 Match(c: PUTF8Char; const compare: UTF8String): Boolean; overload;
+  function Match(const compare1, compare2: UTF8String): Boolean; overload;
+  function ToUTF8String(var c: PUTF8Char; endC: PUTF8Char): UTF8String;
+
+  //special parsing functions //////////////////////////////////////////
+  procedure ParseStyleElementContent(const value: UTF8String; stylesList: TClassStylesList);
+  function ParseTransform(const transform: UTF8String): TMatrixD;
+
+  procedure GetSvgFontInfo(const value: UTF8String; var fontInfo: TSVGFontInfo);
+  function HtmlDecode(const html: UTF8String): UTF8String;
+
+  function GetXmlEncoding(memory: Pointer; len: integer): TSvgEncoding;
+  function ClampRange(val, min, max: double): double;
+
+  function SkipBlanks(var c: PUTF8Char; endC: PUTF8Char): Boolean;
+  function SkipBlanksAndComma(var current: PUTF8Char; currentEnd: PUTF8Char): Boolean;
+
+type
+  TSetOfUTF8Char = set of UTF8Char;
+  UTF8Strings = array of UTF8String;
+
+function CharInSet(chr: UTF8Char; chrs: TSetOfUTF8Char): Boolean;
+
+const
+  clInvalid   = $00010001;
+  clCurrent   = $00010002;
+  sqrt2       = 1.4142135623731;
+  quote       = '''';
+  dquote      = '"';
+  space       = #32;
+  SvgDecimalSeparator = '.'; //do not localize
+
+  {$I Img32.SVG.HashConsts.inc}
+
+var
+  LowerCaseTable : array[#0..#255] of UTF8Char;
+  ColorConstList : TStringList;
+
+implementation
+
+
+type
+  TColorConst = record
+    ColorName : string;
+    ColorValue: Cardinal;
+  end;
+
+  TColorObj = class
+    cc: TColorConst;
+  end;
+
+const
+  buffSize    = 8;
+
+  //include hashed html entity constants
+  {$I Img32.SVG.HtmlHashConsts.inc}
+
+//------------------------------------------------------------------------------
+// Miscellaneous functions ...
+//------------------------------------------------------------------------------
+
+function ClampRange(val, min, max: double): double;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  if val <= min then Result := min
+  else if val >= max then Result := max
+  else Result := val;
+end;
+//------------------------------------------------------------------------------
+
+function CharInSet(chr: UTF8Char; chrs: TSetOfUTF8Char): Boolean;
+begin
+  Result := chr in chrs;
+end;
+//------------------------------------------------------------------------------
+
+function Match(c: PUTF8Char; const compare: UTF8String): Boolean;
+var
+  i: integer;
+begin
+  Result := false;
+  for i := 1 to Length(compare) do
+  begin
+    if LowerCaseTable[c^] <> compare[i] then Exit;
+    inc(c);
+  end;
+  Result := true;
+end;
+//------------------------------------------------------------------------------
+
+function Match(const compare1, compare2: UTF8String): Boolean;
+var
+  i, len: integer;
+  c1, c2: PUTF8Char;
+begin
+  Result := false;
+  len := Length(compare1);
+  if len <> Length(compare2) then Exit;
+  c1 := @compare1[1]; c2 := @compare2[1];
+  for i := 1 to len do
+  begin
+    if LowerCaseTable[c1^] <> LowerCaseTable[c2^] then Exit;
+    inc(c1); inc(c2);
+  end;
+  Result := true;
+end;
+//------------------------------------------------------------------------------
+
+function Split(const str: UTF8String): UTF8Strings;
+var
+  i,j,k, spcCnt, len: integer;
+begin
+  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);
+  for j := i + 1 to len do
+    if (str[j] <= #32) and (str[j -1] > #32) 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);
+    SetLength(Result[k], j -i);
+    Move(str[i], Result[k][1], j -i);
+    while (j <= len) and (str[j] <= #32) do inc(j);
+    i := j;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function GetXmlEncoding(memory: Pointer; len: integer): TSvgEncoding;
+var
+  p: PUTF8Char;
+begin
+  Result := eUnknown;
+  if (len < 4) or not Assigned(memory) then Exit;
+  p := PUTF8Char(memory);
+  case p^ of
+    #$EF: if ((p +1)^ = #$BB) and ((p +2)^ = #$BF) then Result := eUtf8;
+    #$FF: if ((p +1)^ = #$FE) then Result := eUnicodeLE;
+    #$FE: if ((p +1)^ = #$FF) then Result := eUnicodeBE;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function SkipBlanks(var c: PUTF8Char; endC: PUTF8Char): Boolean;
+begin
+  while (c < endC) and (c^ <= space) do inc(c);
+  Result := (c < endC);
+end;
+//------------------------------------------------------------------------------
+
+function SkipBlanksAndComma(var current: PUTF8Char; currentEnd: PUTF8Char): Boolean;
+begin
+  Result := SkipBlanks(current, currentEnd);
+  if not Result or (current^ <> ',') then Exit;
+  inc(current);
+  Result := SkipBlanks(current, currentEnd);
+end;
+//------------------------------------------------------------------------------
+
+function SkipStyleBlanks(var c: PUTF8Char; endC: PUTF8Char): Boolean;
+var
+  inComment: Boolean;
+begin
+  //style content may include multi-line comment blocks
+  inComment := false;
+  while (c < endC) do
+  begin
+    if inComment then
+    begin
+      if (c^ = '*') and ((c +1)^ = '/')  then
+      begin
+        inComment := false;
+        inc(c);
+      end;
+    end
+    else if (c^ > space) then
+    begin
+      inComment := (c^ = '/') and ((c +1)^ = '*');
+      if not inComment then break;
+    end;
+    inc(c);
+  end;
+  Result := (c < endC);
+end;
+//------------------------------------------------------------------------------
+
+function IsAlpha(c: UTF8Char): Boolean; {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result := CharInSet(c, ['A'..'Z','a'..'z']);
+end;
+//------------------------------------------------------------------------------
+
+function ParseStyleNameLen(var c: PUTF8Char; endC: PUTF8Char): integer;
+var
+  c2: PUTF8Char;
+const
+  validNonFirstChars =  ['0'..'9','A'..'Z','a'..'z','-'];
+begin
+  Result := 0;
+  //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 := c; inc(c);
+  while (c < endC) and CharInSet(c^, validNonFirstChars) do inc(c);
+  Result := c - c2;
+end;
+//------------------------------------------------------------------------------
+
+function ParseNextWord(var c: PUTF8Char; endC: PUTF8Char; out word: UTF8String): Boolean;
+var
+  c2: PUTF8Char;
+begin
+  Result := SkipBlanksAndComma(c, endC);
+  if not Result then Exit;
+  c2 := c;
+  while (c < endC) and
+    (LowerCaseTable[c^] >= 'a') and (LowerCaseTable[c^] <= 'z') do
+      inc(c);
+  word := ToUTF8String(c2, c);
+end;
+//------------------------------------------------------------------------------
+
+function ParseNextWordEx(var c: PUTF8Char; endC: PUTF8Char;
+  out word: UTF8String): Boolean;
+var
+  isQuoted: Boolean;
+  c2: PUTF8Char;
+begin
+  Result := SkipBlanksAndComma(c, endC);
+  if not Result then Exit;
+  isQuoted := (c^) = quote;
+  if isQuoted then
+  begin
+    inc(c);
+    c2 := c;
+    while (c < endC) and (c^ <> quote) do inc(c);
+    word := ToUTF8String(c2, c);
+    inc(c);
+  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);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function ParseNameLength(var c: PUTF8Char; endC: PUTF8Char): integer; overload;
+var
+  c2: PUTF8Char;
+const
+  validNonFirstChars =  ['0'..'9','A'..'Z','a'..'z','_',':','-'];
+begin
+  c2 := c;
+  inc(c);
+  while (c < endC) and CharInSet(c^, validNonFirstChars) do inc(c);
+  Result := c - c2;
+end;
+//------------------------------------------------------------------------------
+
+{$OVERFLOWCHECKS OFF}
+function GetHash(const name: UTF8String): 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
+  begin
+    Result := (Result + Ord(LowerCaseTable[c^]));
+    Result := Result + (Result shl 10);
+    Result := Result xor (Result shr 6);
+    inc(c);
+  end;
+  Result := Result + (Result shl 3);
+  Result := Result xor (Result shr 11);
+  Result := Result + (Result shl 15);
+end;
+{$OVERFLOWCHECKS ON}
+//------------------------------------------------------------------------------
+
+{$OVERFLOWCHECKS OFF}
+function GetHashCaseSensitive(name: PUTF8Char; nameLen: integer): cardinal;
+var
+  i: integer;
+begin
+  Result := 0;
+  for i := 1 to nameLen do
+  begin
+    Result := (Result + Ord(name^));
+    Result := Result + (Result shl 10);
+    Result := Result xor (Result shr 6);
+    inc(name);
+  end;
+  Result := Result + (Result shl 3);
+  Result := Result xor (Result shr 11);
+  Result := Result + (Result shl 15);
+end;
+{$OVERFLOWCHECKS ON}
+//------------------------------------------------------------------------------
+
+function ParseNextWordHashed(var c: PUTF8Char; endC: PUTF8Char): cardinal;
+var
+  c2: PUTF8Char;
+  name: UTF8String;
+begin
+  c2 := c;
+  ParseNameLength(c, endC);
+  name := ToUTF8String(c2, c);
+  if name = '' then Result := 0
+  else Result := GetHash(name);
+end;
+//------------------------------------------------------------------------------
+
+function ParseNextNumEx(var c: PUTF8Char; endC: PUTF8Char; skipComma: Boolean;
+  out val: double; out unitType: TUnitType): Boolean;
+var
+  decPos,exp: integer;
+  isNeg, expIsNeg: Boolean;
+  start: PUTF8Char;
+begin
+  Result := false;
+  unitType := utNumber;
+
+  //skip white space +/- single comma
+  if skipComma then
+  begin
+    while (c < endC) and (c^ <= space) do inc(c);
+    if (c^ = ',') then inc(c);
+  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);
+
+  val := 0;
+  start := c;
+  while c < endC do
+  begin
+    if Ord(c^) = 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
+    begin
+      val := val *10 + Ord(c^) - Ord('0');
+      if decPos >= 0 then inc(decPos);
+    end;
+    inc(c);
+  end;
+  Result := c > start;
+  if not Result then Exit;
+
+  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);
+  end;
+
+  //https://oreillymedia.github.io/Using_SVG/guide/units.html
+  case c^ of
+    '%':
+      begin
+        inc(c);
+        unitType := utPercent;
+      end;
+    'c': //convert cm to pixels
+      if ((c+1)^ = 'm') then
+      begin
+        inc(c, 2);
+        unitType := utCm;
+      end;
+    'd': //ignore deg
+      if ((c+1)^ = 'e') and ((c+2)^ = 'g') then
+      begin
+        inc(c, 3);
+        unitType := utDegree;
+      end;
+    'e': //convert cm to pixels
+      if ((c+1)^ = 'm') then
+      begin
+        inc(c, 2);
+        unitType := utEm;
+      end
+      else if ((c+1)^ = 'x') then
+      begin
+        inc(c, 2);
+        unitType := utEx;
+      end;
+    'i': //convert inchs to pixels
+      if ((c+1)^ = 'n') then
+      begin
+        inc(c, 2);
+        unitType := utInch;
+      end;
+    'm': //convert mm to pixels
+      if ((c+1)^ = 'm') then
+      begin
+        inc(c, 2);
+        unitType := utMm;
+      end;
+    'p':
+      case (c+1)^ of
+        'c':
+          begin
+            inc(c, 2);
+            unitType := utPica;
+          end;
+        't':
+          begin
+            inc(c, 2);
+            unitType := utPt;
+          end;
+        'x':
+          begin
+            inc(c, 2);
+            unitType := utPixel;
+          end;
+      end;
+    'r': //convert radian angles to degrees
+      if Match(c, 'rad') then
+      begin
+        inc(c, 3);
+        unitType := utRadian;
+      end;
+  end;
+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);
+  Result := ToUTF8String(c2, c);
+end;
+//------------------------------------------------------------------------------
+
+function ParseNextChar(var c: PUTF8Char; endC: PUTF8Char): UTF8Char;
+begin
+  Result := #0;
+  if not SkipBlanks(c, endC) then Exit;
+  Result := c^;
+  inc(c);
+end;
+//------------------------------------------------------------------------------
+
+function ParseQuoteChar(var c: PUTF8Char; endC: PUTF8Char): UTF8Char;
+begin
+  if SkipBlanks(c, endC) and (c^ in [quote, dquote]) then
+  begin
+    Result := c^;
+    inc(c);
+  end else
+    Result := #0;
+end;
+//------------------------------------------------------------------------------
+
+function AllTrim(var name: UTF8String): Boolean;
+var
+  i, len: integer;
+begin
+  len := Length(name);
+  i := 0;
+  while (len > 0) and (name[1] <= space) do
+  begin
+    inc(i); dec(len);
+  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;
+var
+  len: integer;
+begin
+  len := endC - c;
+  SetLength(Result, len);
+  if len = 0 then Exit;
+  Move(c^, Result[1], len * SizeOf(UTF8Char));
+  c := endC;
+end;
+//------------------------------------------------------------------------------
+
+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));
+  Result := (c3^ = ';') and Assigned(entity);
+  //nb: increments 'c' only if the entity is found.
+  if Result then c := c3 +1 else dec(c);
+end;
+//------------------------------------------------------------------------------
+
+function ParseQuotedString(var c: PUTF8Char; endC: PUTF8Char;
+  out quotStr: UTF8String): Boolean;
+var
+  quote: UTF8Char;
+  c2: PUTF8Char;
+begin
+  quote := c^;
+  inc(c);
+  c2 := c;
+  while (c < endC) and (c^ <> quote) do inc(c);
+  Result := (c < endC);
+  if not Result then Exit;
+  quotStr := ToUTF8String(c2, c);
+  inc(c);
+end;
+//------------------------------------------------------------------------------
+
+function IsNumPending(var c: PUTF8Char;
+  endC: PUTF8Char; ignoreComma: Boolean): Boolean;
+var
+  c2: PUTF8Char;
+begin
+  Result := false;
+
+  //skip white space +/- single comma
+  if ignoreComma then
+  begin
+    while (c < endC) and (c^ <= space) do inc(c);
+    if (c^ = ',') then inc(c);
+  end;
+  while (c < endC) and (c^ <= ' ') do inc(c);
+  if (c = endC) then Exit;
+
+  c2 := c;
+  if (c2^ = '-') then inc(c2);
+  if (c2^ = SvgDecimalSeparator) then inc(c2);
+  Result := (c2 < endC) and (c2^ >= '0') and (c2^ <= '9');
+end;
+//------------------------------------------------------------------------------
+
+function ParseTransform(const transform: UTF8String): TMatrixD;
+var
+  i: integer;
+  c, endC: PUTF8Char;
+  c2: UTF8Char;
+  word: UTF8String;
+  values: array[0..5] of double;
+  mat: TMatrixD;
+begin
+  c := PUTF8Char(transform);
+  endC := c + Length(transform);
+  Result := IdentityMatrix; //in case of invalid or referenced value
+
+  while ParseNextWord(c, endC, word) do
+  begin
+    if Length(word) < 5 then Exit;
+    if ParseNextChar(c, endC) <> '(' then Exit; //syntax check
+    //reset values variables
+    for i := 0 to High(values) do values[i] := InvalidD;
+    //and since every transform function requires at least one value
+    if not ParseNextNum(c, endC, false, values[0]) then Break;
+    //now get additional variables
+    i := 1;
+    while (i < 6) and IsNumPending(c, endC, true) and
+      ParseNextNum(c, endC, true, values[i]) do inc(i);
+    if ParseNextChar(c, endC) <> ')' then Exit; //syntax check
+
+    mat := IdentityMatrix;
+    //scal(e), matr(i)x, tran(s)late, rota(t)e, skew(X), skew(Y)
+    case LowerCaseTable[word[5]] of
+      'e' : //scalE
+        if not IsValid(values[1]) then
+          MatrixScale(mat, values[0]) else
+            MatrixScale(mat, values[0], values[1]);
+      'i' : //matrIx
+        if IsValid(values[5]) then
+        begin
+          mat[0,0] :=  values[0];
+          mat[0,1] :=  values[1];
+          mat[1,0] :=  values[2];
+          mat[1,1] :=  values[3];
+          mat[2,0] :=  values[4];
+          mat[2,1] :=  values[5];
+        end;
+      's' : //tranSlateX, tranSlateY & tranSlate
+        if Length(word) =10  then
+        begin
+          c2 := LowerCaseTable[word[10]];
+          if c2 = 'x' then
+            MatrixTranslate(mat, values[0], 0)
+          else if c2 = 'y' then
+            MatrixTranslate(mat, 0, values[0]);
+        end
+        else if IsValid(values[1]) then
+          MatrixTranslate(mat, values[0], values[1])
+        else
+          MatrixTranslate(mat, values[0], 0);
+      't' : //rotaTe
+        if IsValid(values[2]) then
+          MatrixRotate(mat, PointD(values[1],values[2]), DegToRad(values[0]))
+        else
+          MatrixRotate(mat, NullPointD, DegToRad(values[0]));
+       'x' : //skewX
+         begin
+            MatrixSkew(mat, DegToRad(values[0]), 0);
+         end;
+       'y' : //skewY
+         begin
+            MatrixSkew(mat, 0, DegToRad(values[0]));
+         end;
+    end;
+    Result := MatrixMultiply(Result, mat);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure GetSvgFontInfo(const value: UTF8String; var fontInfo: TSVGFontInfo);
+var
+  c, endC: PUTF8Char;
+  hash: Cardinal;
+begin
+  c := PUTF8Char(value);
+  endC := c + Length(value);
+  while (c < endC) and SkipBlanks(c, endC) do
+  begin
+    if c = ';' then
+      break
+    else if IsNumPending(c, endC, true) then
+      ParseNextNum(c, endC, true, fontInfo.size)
+    else
+    begin
+      hash := ParseNextWordHashed(c, endC);
+      case hash of
+        hSans_045_Serif   : fontInfo.family := ttfSansSerif;
+        hSerif            : fontInfo.family := ttfSerif;
+        hMonospace        : fontInfo.family := ttfMonospace;
+        hBold             : fontInfo.weight := 600;
+        hItalic           : fontInfo.italic := sfsItalic;
+        hNormal           : 
+          begin
+            fontInfo.weight := 400;
+            fontInfo.italic := sfsNone;
+          end;
+        hStart            : fontInfo.align := staLeft;
+        hMiddle           : fontInfo.align := staCenter;
+        hEnd              : fontInfo.align := staRight;
+        hline_045_through : fontInfo.decoration := fdStrikeThrough;
+        hUnderline        : fontInfo.decoration := fdUnderline;
+      end;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function HtmlDecode(const html: UTF8String): UTF8String;
+var
+  val, len: integer;
+  c,ce,endC: PUTF8Char;
+begin
+  len := Length(html);
+  SetLength(Result, len*3);
+  c := PUTF8Char(html);
+  endC := c + len;
+  ce := c;
+  len := 1;
+  while (ce < endC) and (ce^ <> '&') do
+    inc(ce);
+
+  while (ce < endC) do
+  begin
+    if ce > c then
+    begin
+      Move(c^, Result[len], ce - c);
+      inc(len, ce - c);
+    end;
+    c := ce; inc(ce);
+    while (ce < endC) and (ce^ <> ';') do inc(ce);
+    if ce = endC then break;
+
+    val := -1; //assume error
+    if (c +1)^ = '#' then
+    begin
+      val := 0;
+      //decode unicode value
+      if (c +2)^ = 'x' then
+      begin
+        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
+          else
+          begin
+            val := -1;
+            break;
+          end;
+          inc(c);
+        end;
+      end else
+      begin
+        inc(c, 2);
+        while c < ce do
+        begin
+          val := val * 10 + Ord(c^) - 48;
+          inc(c);
+        end;
+      end;
+    end else
+    begin
+      //decode html entity ...
+      case GetHashCaseSensitive(c, ce - c) of
+        {$I Img32.SVG.HtmlValues.inc}
+      end;
+    end;
+
+    //convert unicode value to utf8 chars
+    //this saves the overhead of multiple UTF8String<-->string conversions.
+    case val of
+      0 .. $7F:
+        begin
+          result[len] := UTF8Char(val);
+          inc(len);
+        end;
+      $80 .. $7FF:
+        begin
+          Result[len] := UTF8Char($C0 or (val shr 6));
+          Result[len+1] := UTF8Char($80 or (val and $3f));
+          inc(len, 2);
+        end;
+      $800 .. $7FFF:
+        begin
+          Result[len] := UTF8Char($E0 or (val shr 12));
+          Result[len+1] := UTF8Char($80 or ((val shr 6) and $3f));
+          Result[len+2] := UTF8Char($80 or (val and $3f));
+          inc(len, 3);
+        end;
+      $10000 .. $10FFFF:
+        begin
+          Result[len] := UTF8Char($F0 or (val shr 18));
+          Result[len+1] := UTF8Char($80 or ((val shr 12) and $3f));
+          Result[len+2] := UTF8Char($80 or ((val shr 6) and $3f));
+          Result[len+3] := UTF8Char($80 or (val and $3f));
+          inc(len, 4);
+        end;
+      else
+      begin
+        //ie: error
+        Move(c^, Result[len], ce- c +1);
+        inc(len, ce - c +1);
+      end;
+    end;
+    inc(ce);
+    c := ce;
+    while (ce < endC) and (ce^ <> '&') do inc(ce);
+  end;
+  if (c < endC) and (ce > c) then
+  begin
+    Move(c^, Result[len], (ce - c));
+    inc(len, ce - c);
+  end;
+  setLength(Result, len -1);
+end;
+//------------------------------------------------------------------------------
+
+function HexByteToInt(h: UTF8Char): Cardinal; {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  case h of
+    '0'..'9': Result := Ord(h) - Ord('0');
+    'A'..'F': Result := 10 + Ord(h) - Ord('A');
+    'a'..'f': Result := 10 + Ord(h) - Ord('a');
+    else Result := 0;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function IsFraction(val: double): Boolean; {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result := (val <> 0) and (Abs(val) < 1);
+end;
+//------------------------------------------------------------------------------
+
+function UTF8StringToColor32(const value: UTF8String; var color: TColor32): Boolean;
+var
+  i, len  : integer;
+  j       : Cardinal;
+  clr     : TColor32;
+  alpha   : Byte;
+  vals    : array[0..3] of double;
+  mus     :  array[0..3] of TUnitType;
+  c, endC : PUTF8Char;
+begin
+  Result := false;
+  len := Length(value);
+  if len < 3 then Exit;
+  c := PUTF8Char(value);
+
+  if (color = clInvalid) or (color = clCurrent) or (color = clNone32) then
+    alpha := 255 else
+    alpha := GetAlpha(color);
+
+  if Match(c, 'rgb') then
+  begin
+    endC := c + len;
+    inc(c, 3);
+    if (c^ = 'a') then inc(c);
+    if (ParseNextChar(c, endC) <> '(') or
+      not ParseNextNumEx(c, endC, false, vals[0], mus[0]) or
+      not ParseNextNumEx(c, endC, true, vals[1], mus[1]) or
+      not ParseNextNumEx(c, endC, true, vals[2], mus[2]) then Exit;
+    for i := 0 to 2 do
+      if mus[i] = utPercent then
+        vals[i] := vals[i] * 255 / 100;
+
+    if 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]));
+  end
+  else if (c^ = '#') then           //#RRGGBB or #RGB
+  begin
+    if (len = 9) then
+    begin
+      clr := $0;
+      alpha := $0;
+      for i := 1 to 6 do
+      begin
+        inc(c);
+        clr := clr shl 4 + HexByteToInt(c^);
+      end;
+      for i := 1 to 2 do
+      begin
+        inc(c);
+        alpha := alpha shl 4 + HexByteToInt(c^);
+      end;
+      clr := clr or alpha shl 24;
+    end
+    else if (len = 7) then
+    begin
+      clr := $0;
+      for i := 1 to 6 do
+      begin
+        inc(c);
+        clr := clr shl 4 + HexByteToInt(c^);
+      end;
+      clr := clr or $FF000000;
+    end
+    else if (len = 5) then
+    begin
+      clr := $0;
+      for i := 1 to 3 do
+      begin
+        inc(c);
+        j := HexByteToInt(c^);
+        clr := clr shl 4 + j;
+        clr := clr shl 4 + j;
+      end;
+      inc(c);
+      alpha := HexByteToInt(c^);
+      alpha := alpha + alpha shl 4;
+      clr := clr or alpha shl 24;
+    end
+    else if (len = 4) then
+    begin
+      clr := $0;
+      for i := 1 to 3 do
+      begin
+        inc(c);
+        j := HexByteToInt(c^);
+        clr := clr shl 4 + j;
+        clr := clr shl 4 + j;
+      end;
+      clr := clr or $FF000000;
+    end
+    else
+      Exit;
+    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;
+  end;
+
+  //and in case the opacity has been set before the color
+  if (alpha < 255) then
+    color := (color and $FFFFFF) or alpha shl 24;
+{$IF DEFINED(ANDROID)}
+  color := SwapRedBlue(color);
+{$IFEND}
+  Result := true;
+end;
+//------------------------------------------------------------------------------
+
+function MakeDashArray(const dblArray: TArrayOfDouble; scale: double): TArrayOfInteger;
+var
+  i, len: integer;
+  dist: double;
+begin
+  dist := 0;
+  len := Length(dblArray);
+  SetLength(Result, len);
+  for i := 0 to len -1 do
+  begin
+    Result[i] := Ceil(dblArray[i] * scale);
+    dist := Result[i] + dist;
+  end;
+
+  if dist = 0 then
+  begin
+    Result := nil;
+  end
+  else if Odd(len) then
+  begin
+    SetLength(Result, len *2);
+    Move(Result[0], Result[len], len * SizeOf(integer));
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function PeekNextChar(var c: PUTF8Char; endC: PUTF8Char): UTF8Char;
+begin
+  if not SkipBlanks(c, endC) then
+    Result := #0 else
+    Result := c^;
+end;
+//------------------------------------------------------------------------------
+
+procedure ParseStyleElementContent(const value: UTF8String;
+  stylesList: TClassStylesList);
+var
+  len, cap: integer;
+  names: array of string;
+
+  procedure AddName(const name: string);
+  begin
+    if len = cap then
+    begin
+      cap := cap + buffSize;
+      SetLength(names, cap);
+    end;
+    names[len] := name;
+    inc(len);
+  end;
+
+var
+  i: integer;
+  aclassName: UTF8String;
+  aStyle: UTF8String;
+  c, c2, endC: PUTF8Char;
+begin
+  //https://oreillymedia.github.io/Using_SVG/guide/style.html
+
+  stylesList.Clear;
+  if value = '' then Exit;
+
+  len := 0; cap := 0;
+  c := @value[1];
+  endC := c + Length(value);
+
+  SkipBlanks(c, endC);
+  if Match(c, '<![cdata[') then inc(c, 9);
+
+  while SkipStyleBlanks(c, endC) and
+    CharInSet(LowerCaseTable[PeekNextChar(c, endC)],
+      [SvgDecimalSeparator, '#', 'a'..'z']) do
+  begin
+    //get one or more class names for each pending style
+    c2 := c;
+    ParseNameLength(c, endC);
+    aclassName := ToUTF8String(c2, c);
+
+    AddName(Lowercase(String(aclassName)));
+    if PeekNextChar(c, endC) = ',' 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;
+    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;
+    inc(c);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TXmlEl classes
+//------------------------------------------------------------------------------
+
+constructor TXmlEl.Create(owner: TSvgParser);
+begin
+{$IFDEF XPLAT_GENERICS}
+  attribs := TList<PSvgAttrib>.Create;
+  childs := TList<TXmlEl>.Create;
+{$ELSE}
+  attribs := TList.Create;
+  childs := TList.Create;
+{$ENDIF}
+  selfClosed := true;
+  Self.owner := owner;
+end;
+//------------------------------------------------------------------------------
+
+destructor TXmlEl.Destroy;
+begin
+  Clear;
+  attribs.Free;
+  childs.Free;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+procedure TXmlEl.Clear;
+var
+  i: integer;
+begin
+  for i := 0 to attribs.Count -1 do
+    Dispose(PSvgAttrib(attribs[i]));
+  attribs.Clear;
+
+  for i := 0 to childs.Count -1 do
+    TXmlEl(childs[i]).free;
+  childs.Clear;
+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 := ToUTF8String(c2, c);
+
+  //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;
+begin
+  Result := SkipBlanks(c, endC);
+  if not Result then Exit;
+  c2 := c;
+  ParseNameLength(c, endC);
+  attrib.Name := ToUTF8String(c2, c);
+  attrib.hash := GetHash(attrib.Name);
+end;
+//------------------------------------------------------------------------------
+
+function TXmlEl.ParseAttribValue(var c: PUTF8Char;
+  endC: PUTF8Char; attrib: PSvgAttrib): Boolean;
+var
+  quoteChar : UTF8Char;
+  c2, c3: 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
+end;
+//------------------------------------------------------------------------------
+
+function TXmlEl.ParseAttributes(var c: PUTF8Char; endC: PUTF8Char): Boolean;
+var
+  i: integer;
+  attrib, styleAttrib, classAttrib, idAttrib: PSvgAttrib;
+  classes: UTF8Strings;
+  ansi: UTF8String;
+begin
+  Result := false;
+  styleAttrib := nil;  classAttrib := nil;  idAttrib := nil;
+
+  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
+    end;
+
+    New(attrib);
+    if not ParseAttribName(c, endC, attrib) or
+      not ParseAttribValue(c, endC, attrib) then
+    begin
+      Dispose(attrib);
+      Exit;
+    end;
+
+    attribs.Add(attrib);    
+    case attrib.hash of
+      hId     : idAttrib := attrib;
+      hClass  : classAttrib := attrib;
+      hStyle  : styleAttrib := attrib;
+    end;    
+  end;
+
+  if assigned(classAttrib) then
+    with classAttrib^ do
+    begin
+      //get the 'dotted' classname(s)
+      classes := Split(value);
+      for i := 0 to High(classes) do
+      begin
+        ansi := SvgDecimalSeparator + classes[i];
+        //get the style definition
+        ansi := owner.classStyles.GetStyle(ansi);
+        if ansi <> '' then ParseStyleAttribute(ansi);
+      end;
+    end;
+
+  if assigned(styleAttrib) then
+    ParseStyleAttribute(styleAttrib.value);
+
+  if assigned(idAttrib) then
+  begin
+    //get the 'hashed' classname
+    ansi := '#' + idAttrib.value;
+    //get the style definition
+    ansi := owner.classStyles.GetStyle(ansi);
+    if ansi <> '' then ParseStyleAttribute(ansi);
+  end;
+  
+end;
+//------------------------------------------------------------------------------
+
+procedure TXmlEl.ParseStyleAttribute(const style: UTF8String);
+var
+  styleName, styleVal: UTF8String;
+  c, c2, endC: PUTF8Char;
+  attrib: PSvgAttrib;
+begin
+  //there are 4 ways to load styles (in ascending precedence) -
+  //1. a class element style (called during element contruction)
+  //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
+  begin
+    c2 := c;
+    ParseStyleNameLen(c, endC);
+    styleName := ToUTF8String(c2, c);
+    if styleName = '' then Break;
+
+    if (ParseNextChar(c, endC) <> ':') or  //syntax check
+      not SkipBlanks(c,endC) then Break;
+
+    c2 := c;
+    inc(c);
+    while (c < endC) and (c^ <> ';') do inc(c);
+    styleVal := ToUTF8String(c2, c);
+    AllTrim(styleVal);
+    inc(c);
+
+    new(attrib);
+    attrib.name := styleName;
+    attrib.value := styleVal;
+    attrib.hash := GetHash(attrib.name);
+    attribs.Add(attrib);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TXmlEl.GetAttribCount: integer;
+begin
+  Result := attribs.Count;
+end;
+//------------------------------------------------------------------------------
+
+function TXmlEl.GetAttrib(index: integer): PSvgAttrib;
+begin
+  Result := PSvgAttrib(attribs[index]);
+end;
+//------------------------------------------------------------------------------
+
+function TXmlEl.ParseContent(var c: PUTF8Char; endC: PUTF8Char): Boolean;
+var
+  child: TSvgTreeEl;
+  entity: PSvgAttrib;
+  c2, tmpC, tmpEndC: PUTF8Char;
+begin
+  Result := false;
+  while SkipBlanks(c, endC) do
+  begin
+    if (c^ = '<') then
+    begin
+      inc(c);
+      case c^ of
+        '!':
+          begin
+            if Match(c, '!--') then             //start comment
+            begin
+              inc(c, 3);
+              while (c < endC) and ((c^ <> '-') or
+                not Match(c, '-->')) do inc(c); //end comment
+              inc(c, 3);
+            end else
+            begin
+              //it's very likely <![CDATA[
+              c2 := c - 1;
+              if Match(c, '![cdata[') then
+              begin
+                while (c < endC) and ((c^ <> ']') or not Match(c, ']]>')) do
+                  inc(c);
+                text := ToUTF8String(c2, c);
+                inc(c, 3);
+                if (hash = hStyle) then
+                  ParseStyleElementContent(text, owner.classStyles);
+              end else
+              begin
+                while (c < endC) and (c^ <> '<') do inc(c);
+                text := ToUTF8String(c2, c);
+              end;
+            end;
+          end;
+        '/', '?':
+          begin
+            //element closing tag
+            inc(c);
+            if Match(c, name) then
+            begin
+              inc(c, Length(name));
+              //very rarely there's a space before '>'
+              SkipBlanks(c, endC);
+              Result := c^ = '>';
+              inc(c);
+            end;
+            Exit;
+          end;
+        else
+        begin
+          //starting a new element
+          child := TSvgTreeEl.Create(owner);
+          childs.Add(child);
+          if not child.ParseHeader(c, endC) then break;
+          if not child.selfClosed then
+              child.ParseContent(c, endC);
+        end;
+      end;
+    end
+    else if c^ = '>' then
+    begin
+      break; //oops! something's wrong
+    end
+    else if (c^ = '&') and IsKnownEntity(owner, c, endC, entity) then
+    begin
+      tmpC := PUTF8Char(entity.value);
+      tmpEndC := tmpC + Length(entity.value);
+      ParseContent(tmpC, tmpEndC);
+    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
+      //<tspan> elements we need to create sub-elements for each text block.
+      //And <tspan> elements can even have <tspan> 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;
+    end else
+    begin
+      tmpC := c;
+      while (c < endC) and (c^ <> '<') do inc(c);
+      text := ToUTF8String(tmpC, c);
+
+      //if <style> element then load styles into owner.classStyles
+      if (hash = hStyle) then
+        ParseStyleElementContent(text, owner.classStyles);
+    end;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TDocTypeEl
+//------------------------------------------------------------------------------
+
+procedure TDocTypeEl.SkipWord(var c, endC: PUTF8Char);
+begin
+  while (c < endC) and (c^ > space) do inc(c);
+  inc(c);
+end;
+//------------------------------------------------------------------------------
+
+function TDocTypeEl.ParseEntities(var c, endC: PUTF8Char): Boolean;
+var
+  attrib: PSvgAttrib;
+begin
+  attrib := nil;
+  inc(c); //skip opening '['
+  while (c < endC) and SkipBlanks(c, endC) do
+  begin
+    if (c^ = ']') then break
+    else if not Match(c, '<!entity') then
+    begin
+      while c^ > space do inc(c); //skip word.
+      Continue;
+    end;
+    inc(c, 8);
+    new(attrib);
+    if not ParseAttribName(c, endC, attrib) then break;
+    SkipBlanks(c, endC);
+    if not (c^ in [quote, dquote]) then break;
+    if not ParseQuotedString(c, endC, attrib.value) then break;
+    attribs.Add(attrib);
+    attrib := nil;
+    SkipBlanks(c, endC);
+    if c^ <> '>' then break;
+    inc(c); //skip entity's trailing '>'
+  end;
+  if Assigned(attrib) then Dispose(attrib);
+  Result := (c < endC) and (c^ = ']');
+  inc(c);
+end;
+//------------------------------------------------------------------------------
+
+function TDocTypeEl.ParseAttributes(var c: PUTF8Char; endC: PUTF8Char): Boolean;
+var
+  dummy : UTF8String;
+begin
+  while SkipBlanks(c, endC) do
+  begin
+    //we're currently only interested in ENTITY declarations
+    case c^ of
+      '[': ParseEntities(c, endC);
+      '"', '''': ParseQuotedString(c, endC, dummy);
+      '>': break;
+      else SkipWord(c, endC);
+    end;
+  end;
+  Result := (c < endC) and (c^ = '>');
+  inc(c);
+end;
+
+//------------------------------------------------------------------------------
+// TSvgTreeEl
+//------------------------------------------------------------------------------
+
+constructor TSvgTreeEl.Create(owner: TSvgParser);
+begin
+  inherited Create(owner);
+  selfClosed := false;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgTreeEl.Clear;
+var
+  i: integer;
+begin
+  for i := 0 to childs.Count -1 do
+    TSvgTreeEl(childs[i]).free;
+  childs.Clear;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgTreeEl.ParseHeader(var c: PUTF8Char; endC: PUTF8Char): Boolean;
+begin
+  Result := inherited ParseHeader(c, endC);
+  if Result then hash := GetHash(name);
+end;
+//------------------------------------------------------------------------------
+
+//function TSvgTreeEl.ParseContent(var c: PUTF8Char; endC: PUTF8Char): Boolean;
+
+constructor TSvgParser.Create;
+begin
+  classStyles := TClassStylesList.Create;
+  svgStream   := TMemoryStream.Create;
+  xmlHeader   := TXmlEl.Create(Self);
+  docType     := TDocTypeEl.Create(Self);
+  svgTree     := nil;
+end;
+//------------------------------------------------------------------------------
+
+destructor TSvgParser.Destroy;
+begin
+  Clear;
+  svgStream.Free;
+  xmlHeader.Free;
+  docType.Free;
+  classStyles.Free;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgParser.Clear;
+begin
+  classStyles.Clear;
+  svgStream.Clear;
+  xmlHeader.Clear;
+  docType.Clear;
+  FreeAndNil(svgTree);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgParser.FindEntity(hash: Cardinal): PSvgAttrib;
+var
+  i: integer;
+begin
+  //there are usually so few, that there seems little point sorting etc.
+  for i := 0 to docType.attribs.Count -1 do
+    if PSvgAttrib(docType.attribs[i]).hash = hash then
+    begin
+      Result := PSvgAttrib(docType.attribs[i]);
+      Exit;
+    end;
+  Result := nil;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgParser.LoadFromFile(const filename: string): Boolean;
+var
+  fs: TFileStream;
+begin
+  Result := false;
+  if not FileExists(filename) then Exit;
+
+  fs := TFileStream.Create(filename, fmOpenRead or fmShareDenyNone);
+  try
+    Result := LoadFromStream(fs);
+  finally
+    fs.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgParser.LoadFromStream(stream: TStream): Boolean;
+var
+  i, len: LongInt;
+  encoding: TSvgEncoding;
+  s: UnicodeString;
+  wc: PWord;
+  utf8: UTF8String;
+begin
+  Clear;
+  Result := true;
+  try
+    svgStream.LoadFromStream(stream);
+
+    //check encoding and set to UTF-8 if necessary
+    encoding := GetXmlEncoding(svgStream.Memory, svgStream.Size);
+    case encoding of
+      eUnicodeLE, eUnicodeBE:
+        begin
+          SetLength(s, svgStream.Size div 2);
+          Move(svgStream.Memory^, s[1], svgStream.Size);
+          if encoding = eUnicodeBE then
+          begin
+            wc := @s[1];
+            for i := 1 to Length(s) do
+            begin
+              wc^ := Swap(wc^);
+              inc(wc);
+            end;
+          end;
+          utf8 := UTF8Encode(s);
+          len := Length(utf8);
+          svgStream.SetSize(len);
+          Move(utf8[1], svgStream.Memory^, len);
+        end;
+    end;
+    ParseStream;
+  except
+    Result := false;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgParser.LoadFromString(const str: string): Boolean;
+var
+  ss: TStringStream;
+begin
+{$IFDEF UNICODE}
+  ss := TStringStream.Create(str, TEncoding.UTF8);
+{$ELSE}
+  ss := TStringStream.Create(UTF8Encode(str));
+{$ENDIF}
+  try
+    Result := LoadFromStream(ss);
+  finally
+    ss.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgParser.ParseStream;
+var
+  c, endC: PUTF8Char;
+begin
+  c := svgStream.Memory;
+  endC := c + svgStream.Size;
+  SkipBlanks(c, endC);
+  if Match(c, '<?xml') then
+  begin
+    inc(c, 2); //todo: accommodate space after '<' eg using sMatchEl function
+    if not xmlHeader.ParseHeader(c, endC) then Exit;
+    SkipBlanks(c, endC);
+  end;
+  if Match(c, '<!doctype') then
+  begin
+    inc(c, 2);
+    if not docType.ParseHeader(c, endC) then Exit;
+  end;
+  while SkipBlanks(c, endC) do
+  begin
+    if (c^ = '<') and Match(c, '<svg') then
+    begin
+      inc(c);
+      svgTree := TSvgTreeEl.Create(self);
+      if svgTree.ParseHeader(c, endC) and
+        not svgTree.selfClosed then
+          svgTree.ParseContent(c, endC);
+      break;
+    end;
+    inc(c);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TValue
+//------------------------------------------------------------------------------
+
+function ConvertValue(const value: TValue; scale: double): double;
+const
+  mm  = 96 / 25.4;
+  cm  = 96 / 2.54;
+  rad = 180 / PI;
+  pt  = 4 / 3;
+begin
+  //https://oreillymedia.github.io/Using_SVG/guide/units.html
+  //todo: still lots of units to support (eg times for animation)
+  with value do
+    if not IsValid or (rawVal = 0) then
+      Result := 0
+    else
+      case value.unitType of
+        utNumber:
+          Result := rawVal;
+        utPercent:
+          Result := rawVal * 0.01 * scale;
+        utRadian:
+          Result := rawVal * rad;
+        utInch:
+          Result := rawVal * 96;
+        utCm:
+          Result := rawVal * cm;
+        utMm:
+          Result := rawVal * mm;
+        utEm:
+          if scale <= 0 then
+            Result := rawVal * 16 else
+            Result := rawVal * scale;
+        utEx:
+          if scale <= 0 then
+            Result := rawVal * 8 else
+            Result := rawVal * scale * 0.5;
+        utPica:
+          Result := rawVal * 16;
+        utPt:
+          Result := rawVal * pt;
+        else
+          Result := rawVal;
+      end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TValue.Init;
+begin
+  rawVal      := InvalidD;
+  unitType          := utNumber;
+end;
+//------------------------------------------------------------------------------
+
+procedure TValue.SetValue(val: double; unitTyp: TUnitType);
+begin
+  rawVal  := val;
+  unitType      := unitTyp;
+end;
+//------------------------------------------------------------------------------
+
+function TValue.GetValue(relSize: double; assumeRelValBelow: Double): double;
+begin
+  if not IsValid or (rawVal = 0) then
+    Result := 0
+  else if IsRelativeValue(assumeRelValBelow) then
+    Result := rawVal * relSize
+  else
+    Result := ConvertValue(self, relSize);
+end;
+//------------------------------------------------------------------------------
+
+function TValue.GetValueXY(const relSize: TRectD; assumeRelValBelow: Double): double;
+begin
+  //https://www.w3.org/TR/SVG11/coords.html#Units
+  Result := GetValue(Hypot(relSize.Width, relSize.Height)/sqrt2, assumeRelValBelow);
+end;
+//------------------------------------------------------------------------------
+
+function  TValue.IsRelativeValue(assumeRelValBelow: double): Boolean;
+begin
+  Result := (unitType = utNumber) and (Abs(rawVal) <= assumeRelValBelow);
+end;
+//------------------------------------------------------------------------------
+
+function TValue.IsValid: Boolean;
+begin
+  Result := (unitType <> utUnknown) and Img32.Vector.IsValid(rawVal);
+end;
+//------------------------------------------------------------------------------
+
+function TValue.HasFontUnits: Boolean;
+begin
+  case unitType of
+    utEm, utEx: Result := true;
+    else Result := False;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+
+function TValue.HasAngleUnits: Boolean;
+begin
+  case unitType of
+    utDegree, utRadian: Result := true;
+    else Result := False;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TValuePt
+//------------------------------------------------------------------------------
+
+procedure TValuePt.Init;
+begin
+  X.Init;
+  Y.Init;
+end;
+//------------------------------------------------------------------------------
+
+function TValuePt.GetPoint(const relSize: double; assumeRelValBelow: Double): TPointD;
+begin
+  Result.X := X.GetValue(relSize, assumeRelValBelow);
+  Result.Y := Y.GetValue(relSize, assumeRelValBelow);
+end;
+//------------------------------------------------------------------------------
+
+function TValuePt.GetPoint(const relSize: TRectD; assumeRelValBelow: Double): TPointD;
+begin
+  Result.X := X.GetValue(relSize.Width, assumeRelValBelow);
+  Result.Y := Y.GetValue(relSize.Height, assumeRelValBelow);
+end;
+//------------------------------------------------------------------------------
+
+function TValuePt.IsValid: Boolean;
+begin
+  Result := X.IsValid and Y.IsValid;
+end;
+
+//------------------------------------------------------------------------------
+// TValueRec
+//------------------------------------------------------------------------------
+
+procedure TValueRecWH.Init;
+begin
+  left.Init;
+  top.Init;
+  width.Init;
+  height.Init;
+end;
+//------------------------------------------------------------------------------
+
+function TValueRecWH.GetRectD(const relSize: TRectD; assumeRelValBelow: Double): TRectD;
+begin
+  with GetRectWH(relSize, assumeRelValBelow) do
+  begin
+    Result.Left :=Left;
+    Result.Top := Top;
+    Result.Right := Left + Width;
+    Result.Bottom := Top + Height;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TValueRecWH.GetRectD(relSize: double; assumeRelValBelow: Double): TRectD;
+begin
+  if not left.IsValid then
+    Result.Left := 0 else
+    Result.Left := left.GetValue(relSize, assumeRelValBelow);
+
+  if not top.IsValid then
+    Result.Top := 0 else
+    Result.Top := top.GetValue(relSize, assumeRelValBelow);
+
+  Result.Right := Result.Left + width.GetValue(relSize, assumeRelValBelow);
+  Result.Bottom := Result.Top + height.GetValue(relSize, assumeRelValBelow);
+end;
+//------------------------------------------------------------------------------
+
+function TValueRecWH.GetRectWH(const relSize: TRectD; assumeRelValBelow: Double): TRectWH;
+begin
+  if not left.IsValid then
+    Result.Left := 0 else
+    Result.Left := left.GetValue(relSize.Width, assumeRelValBelow);
+
+  if not top.IsValid then
+    Result.Top := 0 else
+    Result.Top := top.GetValue(relSize.Height, assumeRelValBelow);
+
+  Result.Width := width.GetValue(relSize.Width, assumeRelValBelow);
+  Result.Height := height.GetValue(relSize.Height, assumeRelValBelow);
+end;
+//------------------------------------------------------------------------------
+
+function TValueRecWH.IsValid: Boolean;
+begin
+  Result := width.IsValid and height.IsValid;
+end;
+//------------------------------------------------------------------------------
+
+function TValueRecWH.IsEmpty: Boolean;
+begin
+  Result := (width.rawVal <= 0) or (height.rawVal <= 0);
+end;
+
+//------------------------------------------------------------------------------
+// TClassStylesList
+//------------------------------------------------------------------------------
+
+constructor TClassStylesList.Create;
+begin
+  fList := TStringList.Create;
+  fList.Duplicates := dupIgnore;
+  fList.CaseSensitive := false;
+  fList.Sorted := True;
+end;
+//------------------------------------------------------------------------------
+
+destructor TClassStylesList.Destroy;
+begin
+  Clear;
+  fList.Free;
+  inherited Destroy;
+end;
+//------------------------------------------------------------------------------
+
+function TClassStylesList.AddAppendStyle(const classname: string; const ansi: UTF8String): integer;
+var
+  i: integer;
+  sr: PAnsStringiRec;
+begin
+  Result := fList.IndexOf(classname);
+  if (Result >= 0) then
+  begin
+    sr := PAnsStringiRec(fList.Objects[Result]);
+    i := Length(sr.ansi);
+    if sr.ansi[i] <> ';' then
+      sr.ansi := sr.ansi + ';' + ansi else
+      sr.ansi := sr.ansi + ansi;
+  end else
+  begin
+    new(sr);
+    sr.ansi := ansi;
+    Result := fList.AddObject(classname, Pointer(sr));
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TClassStylesList.GetStyle(const classname: UTF8String): UTF8String;
+var
+  i: integer;
+begin
+  SetLength(Result, 0);
+  i := fList.IndexOf(string(className));
+  if i >= 0 then
+    Result := PAnsStringiRec(fList.objects[i]).ansi;
+end;
+//------------------------------------------------------------------------------
+
+procedure TClassStylesList.Clear;
+var
+  i: integer;
+begin
+  for i := 0 to fList.Count -1 do
+    Dispose(PAnsStringiRec(fList.Objects[i]));
+  fList.Clear;
+end;
+
+//------------------------------------------------------------------------------
+// initialization procedures
+//------------------------------------------------------------------------------
+
+procedure MakeLowerCaseTable;
+var
+  i: UTF8Char;
+begin
+  for i:= #0 to #$40 do LowerCaseTable[i]:= i;
+  for i:= #$41 to #$5A do LowerCaseTable[i]:= UTF8Char(Ord(i) + $20);
+  for i:= #$5B to #$FF do LowerCaseTable[i]:= i;
+end;
+//------------------------------------------------------------------------------
+
+procedure MakeColorConstList;
+var
+  i   : integer;
+  co  : TColorObj;
+  {$I Img32.SVG.HtmlColorConsts.inc}
+begin
+  ColorConstList := TStringList.Create;
+  ColorConstList.CaseSensitive := false;
+  //ColorConstList.OwnsObjects := true; //not all versions of Delphi
+  ColorConstList.Capacity := Length(ColorConsts);
+  for i := 0 to High(ColorConsts) do
+  begin
+    co := TColorObj.Create;
+    co.cc := ColorConsts[i];
+    ColorConstList.AddObject(co.cc.ColorName, co);
+  end;
+  ColorConstList.Sorted := true;
+end;
+//------------------------------------------------------------------------------
+
+procedure CleanupColorConstList;
+var
+  i   : integer;
+begin
+  for i := 0 to ColorConstList.Count -1 do
+    TColorObj(ColorConstList.Objects[i]).Free;
+  ColorConstList.Free;
+end;
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+initialization
+  MakeLowerCaseTable;
+  MakeColorConstList;
+
+finalization
+  CleanupColorConstList;
+end.

+ 191 - 0
components/Image32/source/Img32.SVG.HashConsts.inc

@@ -0,0 +1,191 @@
+const
+  hAmplitude                  = $DA119D26;    // Amplitude
+  hArial                      = $EC47D4A0;    // Arial
+  harithmetic                 = $978C7D6F;    // arithmetic
+  hAtop                       = $7655315D;    // Atop
+  hauto_045_start_045_reverse = $EE4545A2;    // auto-start-reverse
+  hBackgroundAlpha            = $DA55EB83;    // BackgroundAlpha
+  hBackgroundImage            = $0313D8C9;    // BackgroundImage
+  hBaseline                   = $C7461648;    // Baseline
+  hbaseline_045_shift         = $56FA5107;    // baseline-shift
+  hBevel                      = $BF36E150;    // Bevel
+  hBold                       = $2F420533;    // Bold
+  hBolder                     = $9C928BF1;    // Bolder
+  hButt                       = $B619E60B;    // Butt
+  hCDATA                      = $FF50BB67;    // CDATA
+  hCircle                     = $C6AE4953;    // Circle
+  hClass                      = $E45C07DD;    // Class
+  hClip_045_path              = $B6C7D97A;    // Clip-path
+  hClippath                   = $241B0E0D;    // Clippath
+  hColor                      = $E953FD29;    // Color
+  hCurrentColor               = $5B8F31B0;    // CurrentColor
+  hCx                         = $B5177A89;    // Cx
+  hCy                         = $A6DCDE14;    // Cy
+  hD                          = $2350C685;    // D
+  hDarken                     = $3DD2B17F;    // Darken
+  hDefs                       = $A819FE18;    // Defs
+  hdiffuseConstant            = $08AD9D7D;    // diffuseConstant
+  hDiscrete                   = $ECBBCCA1;    // Discrete
+  hDisplay                    = $17D82A47;    // Display
+  hDx                         = $6798DE45;    // Dx
+  hDy                         = $776B7DEA;    // Dy
+  hEllipse                    = $30C7994A;    // Ellipse
+  hEnd                        = $B0C485D7;    // End
+  hEntity                     = $395039A9;    // Entity
+  hExponent                   = $D8013FFD;    // Exponent
+  hfeBlend                    = $751904B9;    // feBlend
+  hfeColorMatrix              = $C0C4CF02;    // feColorMatrix
+  hfeComponentTransfer        = $8B01CCC8;    // feComponentTransfer
+  hfeComposite                = $E652E33C;    // feComposite
+  hfeDefuseLighting           = $D9F6C722;    // feDefuseLighting
+  hfeDropShadow               = $16269B00;    // feDropShadow
+  hfeFlood                    = $0B6C5F4E;    // feFlood
+  hfeFuncA                    = $2EA47C9E;    // feFuncA
+  hfeFuncB                    = $1C5DD811;    // feFuncB
+  hfeFuncG                    = $E45FE81A;    // feFuncG
+  hfeFuncR                    = $F8BB10C8;    // feFuncR
+  hfeGaussianBlur             = $B2225552;    // feGaussianBlur
+  hfeMerge                    = $A2C358C0;    // feMerge
+  hfeMergeNode                = $F5F1E90F;    // feMergeNode
+  hfeOffset                   = $04493A72;    // feOffset
+  hfePointLight               = $D3FFCCA4;    // fePointLight
+  hfeSpecularLighting         = $577C6E4B;    // feSpecularLighting
+  hFill                       = $A3AAAB69;    // Fill
+  hFill_045_Opacity           = $18000C02;    // Fill-Opacity
+  hFill_045_Rule              = $5264679F;    // Fill-Rule
+  hFilter                     = $49DC73B3;    // Filter
+  hflood_045_color            = $4E87F3B3;    // flood-color
+  hflood_045_opacity          = $691CCD42;    // flood-opacity
+  hFlowRegion                 = $822E901A;    // FlowRegion
+  hFlowRoot                   = $A802ADFD;    // FlowRoot
+  hFont                       = $21461EC6;    // Font
+  hFont_045_Family            = $371C9BE4;    // Font-Family
+  hFont_045_Size              = $8BFECA77;    // Font-Size
+  hFont_045_Style             = $4CBDA7BE;    // Font-Style
+  hFont_045_Weight            = $EF7234B4;    // Font-Weight
+  hFx                         = $D6733A04;    // Fx
+  hFy                         = $A4AD5675;    // Fy
+  hG                          = $3593EB0B;    // G
+  hGradientTransform          = $5B6A9CF7;    // GradientTransform
+  hGradientUnits              = $07DD34B6;    // GradientUnits
+  hGramma                     = $302368DC;    // Gramma
+  hHeight                     = $52FDF336;    // Height
+  hHref                       = $8E926F4B;    // Href
+  hId                         = $1B60404D;    // Id
+  hIn                         = $4D5FA44B;    // In
+  hIn2                        = $FBFE02B1;    // In2
+  hIntercept                  = $7CBB607F;    // Intercept
+  hItalic                     = $F07E7786;    // Italic
+  hK1                         = $33176D43;    // K1
+  hK2                         = $DD11C139;    // K2
+  hK3                         = $CF4B25AC;    // K3
+  hK4                         = $77CCF6AD;    // K4
+  hkernelUnitLength           = $3B25E980;    // kernelUnitLength
+  hletter_045_spacing         = $5130BCF5;    // letter-spacing
+  hLighten                    = $85E826DE;    // Lighten
+  hLighter                    = $CCEDB4E8;    // Lighter
+  hlighting_045_color         = $B4114C27;    // lighting-color
+  hLine                       = $7A6D718F;    // Line
+  hline_045_through           = $1C703CCD;    // line-through
+  hLinear                     = $39327DA3;    // Linear
+  hLinearGradient             = $C16E9BCD;    // LinearGradient
+  hMarker                     = $72660BE9;    // Marker
+  hMarker_045_End             = $B06101FD;    // Marker-End
+  hMarker_045_Mid             = $FD95C733;    // Marker-Mid
+  hMarker_045_Start           = $CF854A0E;    // Marker-Start
+  hMarkerHeight               = $10360978;    // MarkerHeight
+  hMarkerWidth                = $EF8DF427;    // MarkerWidth
+  hMask                       = $FAF8F32F;    // Mask
+  hMatrix                     = $31E3AD46;    // Matrix
+  hMiddle                     = $80732990;    // Middle
+  hMiter                      = $33868A3B;    // Miter
+  hMode                       = $68155C83;    // Mode
+  hMonospace                  = $049AE941;    // Monospace
+  hMultiply                   = $9110BAC4;    // Multiply
+  hNone                       = $1D632BA1;    // None
+  hNormal                     = $4F485502;    // Normal
+  hObjectBoundingBox          = $3D4B4519;    // ObjectBoundingBox
+  hOffset                     = $A13C79EF;    // Offset
+  hOpacity                    = $D9394F28;    // Opacity
+  hOperator                   = $AA661628;    // Operator
+  hOrient                     = $EFF6C65A;    // Orient
+  hOut                        = $12067E90;    // Out
+  hOver                       = $C9B82D21;    // Over
+  hOverlay                    = $383BEA5E;    // Overlay
+  hPad                        = $4A0CAD48;    // Pad
+  hPath                       = $AD8A7AB5;    // Path
+  hPattern                    = $FF0BFA79;    // Pattern
+  hPatternContentUnits        = $C6D9BCA5;    // PatternContentUnits
+  hPatternTransform           = $7051BE8C;    // PatternTransform
+  hPatternUnits               = $2FDF622C;    // PatternUnits
+  hPoints                     = $87F41B44;    // Points
+  hPolygon                    = $509667FD;    // Polygon
+  hPolyline                   = $58B3D280;    // Polyline
+  hR                          = $26014BE2;    // R
+  hRadialGradient             = $B0D7E1F9;    // RadialGradient
+  hRect                       = $FA3BCBEA;    // Rect
+  hReflect                    = $98BC393E;    // Reflect
+  hRefX                       = $00C1DE46;    // RefX
+  hRefY                       = $480CECDB;    // RefY
+  hRepeat                     = $FF80069D;    // Repeat
+  hResult                     = $47CD519F;    // Result
+  hRound                      = $323B0E24;    // Round
+  hRx                         = $3ABF70F3;    // Rx
+  hRy                         = $0C7D9470;    // Ry
+  hSans                       = $DE5518F6;    // Sans
+  hSans_045_Serif             = $95106804;    // Sans-Serif
+  hScreen                     = $207A08BB;    // Screen
+  hSerif                      = $B88D4332;    // Serif
+  hSlope                      = $4774735C;    // Slope
+  hSourceAlpha                = $F891B958;    // SourceAlpha
+  hSourceGraphic              = $A4DB1DC2;    // SourceGraphic
+  hspecularExponent           = $93D520E8;    // specularExponent
+  hSpreadMethod               = $A08CEB57;    // SpreadMethod
+  hSquare                     = $20E48144;    // Square
+  hStart                      = $84DC271F;    // Start
+  hstdDeviation               = $00D13F6A;    // stdDeviation
+  hStop                       = $930CA7F2;    // Stop
+  hStop_045_Color             = $9FD6E0BE;    // Stop-Color
+  hStop_045_Opacity           = $FC7751F2;    // Stop-Opacity
+  hStroke                     = $E04ABDF3;    // Stroke
+  hStroke_045_DashArray       = $795F2D78;    // Stroke-DashArray
+  hStroke_045_DashOffset      = $A418B357;    // Stroke-DashOffset
+  hstroke_045_linecap         = $29D0178F;    // stroke-linecap
+  hstroke_045_linejoin        = $79642287;    // stroke-linejoin
+  hstroke_045_miterlimit      = $16C29890;    // stroke-miterlimit
+  hStroke_045_Opacity         = $D237C6B5;    // Stroke-Opacity
+  hStroke_045_Width           = $AFF671ED;    // Stroke-Width
+  hStyle                      = $A9473AB4;    // Style
+  hSub                        = $1B3C1FFA;    // Sub
+  hSuper                      = $9C5EE034;    // Super
+  hsurfaceScale               = $EA29CC4F;    // surfaceScale
+  hSvg                        = $ED67C37E;    // Svg
+  hSwitch                     = $D60E64BB;    // Switch
+  hSymbol                     = $07754611;    // Symbol
+  hTable                      = $AB55E184;    // Table
+  hTableValues                = $2F999AD9;    // TableValues
+  hText                       = $7F3B51AB;    // Text
+  hText_045_Anchor            = $D0D82FE8;    // Text-Anchor
+  hText_045_Decoration        = $3BC20862;    // Text-Decoration
+  hTextLength                 = $DA75EDC0;    // TextLength
+  hTextPath                   = $18F2748E;    // TextPath
+  hTimes                      = $FF0E1FBE;    // Times
+  hTransform                  = $A0930B32;    // Transform
+  hTSpan                      = $33C1F865;    // TSpan
+  hType                       = $3165B05D;    // Type
+  hUnderline                  = $70D1B303;    // Underline
+  hUrl                        = $BC113F8C;    // Url
+  hUse                        = $65736EEA;    // Use
+  hUserSpaceOnUse             = $3D19F8A4;    // UserSpaceOnUse
+  hValues                     = $33429E55;    // Values
+  hViewbox                    = $AEF665C8;    // Viewbox
+  hWidth                      = $96BEECA0;    // Width
+  hX                          = $9303A5E5;    // X
+  hX1                         = $74A7DE27;    // X1
+  hX2                         = $168E21F1;    // X2
+  hXlink_058_Href             = $2BDA798B;    // Xlink:Href
+  hXor                        = $4D86273A;    // Xor
+  hY                          = $80950108;    // Y
+  hY1                         = $7F247680;    // Y1
+  hY2                         = $6DE3D3FF;    // Y2
+  hZ                          = $B6606C9E;    // Z

+ 150 - 0
components/Image32/source/Img32.SVG.HtmlColorConsts.inc

@@ -0,0 +1,150 @@
+const
+  ColorConsts: array[0..147] of TColorConst = (
+    (ColorName: 'aliceblue'; ColorValue: $FFF0F8FF),
+    (ColorName: 'antiquewhite'; ColorValue: $FFFAEBD7),
+    (ColorName: 'aqua'; ColorValue: $FF00FFFF),
+    (ColorName: 'aquamarine'; ColorValue: $FF7FFFD4),
+    (ColorName: 'azure'; ColorValue: $FFF0FFFF),
+    (ColorName: 'beige'; ColorValue: $FFF5F5DC),
+    (ColorName: 'bisque'; ColorValue: $FFFFE4C4),
+    (ColorName: 'black'; ColorValue: $FF000000),
+    (ColorName: 'blanchedalmond'; ColorValue: $FFFFEBCD),
+    (ColorName: 'blue'; ColorValue: $FF0000FF),
+    (ColorName: 'blueviolet'; ColorValue: $FF8A2BE2),
+    (ColorName: 'brown'; ColorValue: $FFA52A2A),
+    (ColorName: 'burlywood'; ColorValue: $FFDEB887),
+    (ColorName: 'cadetblue'; ColorValue: $FF5F9EA0),
+    (ColorName: 'chartreuse'; ColorValue: $FF7FFF00),
+    (ColorName: 'chocolate'; ColorValue: $FFD2691E),
+    (ColorName: 'coral'; ColorValue: $FFFF7F50),
+    (ColorName: 'cornflowerblue'; ColorValue: $FF6495ED),
+    (ColorName: 'cornsilk'; ColorValue: $FFFFF8DC),
+    (ColorName: 'crimson'; ColorValue: $FFDC143C),
+    (ColorName: 'currentcolor'; ColorValue: $00010002),
+    (ColorName: 'cyan'; ColorValue: $FF00FFFF),
+    (ColorName: 'darkblue'; ColorValue: $FF00008B),
+    (ColorName: 'darkcyan'; ColorValue: $FF008B8B),
+    (ColorName: 'darkgoldenrod'; ColorValue: $FFB8860B),
+    (ColorName: 'darkgray'; ColorValue: $FFA9A9A9),
+    (ColorName: 'darkgreen'; ColorValue: $FF006400),
+    (ColorName: 'darkgrey'; ColorValue: $FFA9A9A9),
+    (ColorName: 'darkkhaki'; ColorValue: $FFBDB76B),
+    (ColorName: 'darkmagenta'; ColorValue: $FF8B008B),
+    (ColorName: 'darkolivegreen'; ColorValue: $FF556B2F),
+    (ColorName: 'darkorange'; ColorValue: $FFFF8C00),
+    (ColorName: 'darkorchid'; ColorValue: $FF9932CC),
+    (ColorName: 'darkred'; ColorValue: $FF8B0000),
+    (ColorName: 'darksalmon'; ColorValue: $FFE9967A),
+    (ColorName: 'darkseagreen'; ColorValue: $FF8FBC8F),
+    (ColorName: 'darkslateblue'; ColorValue: $FF483D8B),
+    (ColorName: 'darkslategray'; ColorValue: $FF2F4F4F),
+    (ColorName: 'darkslategrey'; ColorValue: $FF2F4F4F),
+    (ColorName: 'darkturquoise'; ColorValue: $FF00CED1),
+    (ColorName: 'darkviolet'; ColorValue: $FF9400D3),
+    (ColorName: 'deeppink'; ColorValue: $FFFF1493),
+    (ColorName: 'deepskyblue'; ColorValue: $FF00BFFF),
+    (ColorName: 'dimgray'; ColorValue: $FF696969),
+    (ColorName: 'dimgrey'; ColorValue: $FF696969),
+    (ColorName: 'dodgerblue'; ColorValue: $FF1E90FF),
+    (ColorName: 'firebrick'; ColorValue: $FFB22222),
+    (ColorName: 'floralwhite'; ColorValue: $FFFFFAF0),
+    (ColorName: 'forestgreen'; ColorValue: $FF228B22),
+    (ColorName: 'fuchsia'; ColorValue: $FFFF00FF),
+    (ColorName: 'gainsboro'; ColorValue: $FFDCDCDC),
+    (ColorName: 'ghostwhite'; ColorValue: $FFF8F8FF),
+    (ColorName: 'gold'; ColorValue: $FFFFD700),
+    (ColorName: 'goldenrod'; ColorValue: $FFDAA520),
+    (ColorName: 'gray'; ColorValue: $FF808080),
+    (ColorName: 'green'; ColorValue: $FF008000),
+    (ColorName: 'greenyellow'; ColorValue: $FFADFF2F),
+    (ColorName: 'grey'; ColorValue: $FF808080),
+    (ColorName: 'honeydew'; ColorValue: $FFF0FFF0),
+    (ColorName: 'hotpink'; ColorValue: $FFFF69B4),
+    (ColorName: 'indianred'; ColorValue: $FFCD5C5C),
+    (ColorName: 'indigo'; ColorValue: $FF4B0082),
+    (ColorName: 'ivory'; ColorValue: $FFFFFFF0),
+    (ColorName: 'khaki'; ColorValue: $FFF0E68C),
+    (ColorName: 'lavender'; ColorValue: $FFE6E6FA),
+    (ColorName: 'lavenderblush'; ColorValue: $FFFFF0F5),
+    (ColorName: 'lawngreen'; ColorValue: $FF7CFC00),
+    (ColorName: 'lemonchiffon'; ColorValue: $FFFFFACD),
+    (ColorName: 'lightblue'; ColorValue: $FFADD8E6),
+    (ColorName: 'lightcoral'; ColorValue: $FFF08080),
+    (ColorName: 'lightcyan'; ColorValue: $FFE0FFFF),
+    (ColorName: 'lightgoldenrodyellow'; ColorValue: $FFFAFAD2),
+    (ColorName: 'lightgray'; ColorValue: $FFD3D3D3),
+    (ColorName: 'lightgreen'; ColorValue: $FF90EE90),
+    (ColorName: 'lightgrey'; ColorValue: $FFD3D3D3),
+    (ColorName: 'lightpink'; ColorValue: $FFFFB6C1),
+    (ColorName: 'lightsalmon'; ColorValue: $FFFFA07A),
+    (ColorName: 'lightseagreen'; ColorValue: $FF20B2AA),
+    (ColorName: 'lightskyblue'; ColorValue: $FF87CEFA),
+    (ColorName: 'lightslategray'; ColorValue: $FF778899),
+    (ColorName: 'lightslategrey'; ColorValue: $FF778899),
+    (ColorName: 'lightsteelblue'; ColorValue: $FFB0C4DE),
+    (ColorName: 'lightyellow'; ColorValue: $FFFFFFE0),
+    (ColorName: 'lime'; ColorValue: $FF00FF00),
+    (ColorName: 'limegreen'; ColorValue: $FF32CD32),
+    (ColorName: 'linen'; ColorValue: $FFFAF0E6),
+    (ColorName: 'magenta'; ColorValue: $FFFF00FF),
+    (ColorName: 'maroon'; ColorValue: $FF800000),
+    (ColorName: 'mediumaquamarine'; ColorValue: $FF66CDAA),
+    (ColorName: 'mediumblue'; ColorValue: $FF0000CD),
+    (ColorName: 'mediumorchid'; ColorValue: $FFBA55D3),
+    (ColorName: 'mediumpurple'; ColorValue: $FF9370DB),
+    (ColorName: 'mediumseagreen'; ColorValue: $FF3CB371),
+    (ColorName: 'mediumslateblue'; ColorValue: $FF7B68EE),
+    (ColorName: 'mediumspringgreen'; ColorValue: $FF00FA9A),
+    (ColorName: 'mediumturquoise'; ColorValue: $FF48D1CC),
+    (ColorName: 'mediumvioletred'; ColorValue: $FFC71585),
+    (ColorName: 'midnightblue'; ColorValue: $FF191970),
+    (ColorName: 'mintcream'; ColorValue: $FFF5FFFA),
+    (ColorName: 'mistyrose'; ColorValue: $FFFFE4E1),
+    (ColorName: 'moccasin'; ColorValue: $FFFFE4B5),
+    (ColorName: 'navajowhite'; ColorValue: $FFFFDEAD),
+    (ColorName: 'navy'; ColorValue: $FF000080),
+    (ColorName: 'none'; ColorValue: $0),
+    (ColorName: 'oldlace'; ColorValue: $FFFDF5E6),
+    (ColorName: 'olive'; ColorValue: $FF808000),
+    (ColorName: 'olivedrab'; ColorValue: $FF6B8E23),
+    (ColorName: 'orange'; ColorValue: $FFFFA500),
+    (ColorName: 'orangered'; ColorValue: $FFFF4500),
+    (ColorName: 'orchid'; ColorValue: $FFDA70D6),
+    (ColorName: 'palegoldenrod'; ColorValue: $FFEEE8AA),
+    (ColorName: 'palegreen'; ColorValue: $FF98FB98),
+    (ColorName: 'paleturquoise'; ColorValue: $FFAFEEEE),
+    (ColorName: 'palevioletred'; ColorValue: $FFDB7093),
+    (ColorName: 'papayawhip'; ColorValue: $FFFFEFD5),
+    (ColorName: 'peachpuff'; ColorValue: $FFFFDAB9),
+    (ColorName: 'peru'; ColorValue: $FFCD853F),
+    (ColorName: 'pink'; ColorValue: $FFFFC0CB),
+    (ColorName: 'plum'; ColorValue: $FFDDA0DD),
+    (ColorName: 'powderblue'; ColorValue: $FFB0E0E6),
+    (ColorName: 'purple'; ColorValue: $FF800080),
+    (ColorName: 'red'; ColorValue: $FFFF0000),
+    (ColorName: 'rosybrown'; ColorValue: $FFBC8F8F),
+    (ColorName: 'royalblue'; ColorValue: $FF4169E1),
+    (ColorName: 'saddlebrown'; ColorValue: $FF8B4513),
+    (ColorName: 'salmon'; ColorValue: $FFFA8072),
+    (ColorName: 'sandybrown'; ColorValue: $FFF4A460),
+    (ColorName: 'seagreen'; ColorValue: $FF2E8B57),
+    (ColorName: 'seashell'; ColorValue: $FFFFF5EE),
+    (ColorName: 'sienna'; ColorValue: $FFA0522D),
+    (ColorName: 'silver'; ColorValue: $FFC0C0C0),
+    (ColorName: 'skyblue'; ColorValue: $FF87CEEB),
+    (ColorName: 'slateblue'; ColorValue: $FF6A5ACD),
+    (ColorName: 'slategray'; ColorValue: $FF708090),
+    (ColorName: 'slategrey'; ColorValue: $FF708090),
+    (ColorName: 'springgreen'; ColorValue: $FF00FF7F),
+    (ColorName: 'steelblue'; ColorValue: $FF4682B4),
+    (ColorName: 'tan'; ColorValue: $FFD2B48C),
+    (ColorName: 'teal'; ColorValue: $FF008080),
+    (ColorName: 'thistle'; ColorValue: $FFD8BFD8),
+    (ColorName: 'tomato'; ColorValue: $FFFF6347),
+    (ColorName: 'turquoise'; ColorValue: $FF40E0D0),
+    (ColorName: 'violet'; ColorValue: $FFEE82EE),
+    (ColorName: 'wheat'; ColorValue: $FFF5DEB3),
+    (ColorName: 'white'; ColorValue: $FFFFFFFF),
+    (ColorName: 'whitesmoke'; ColorValue: $FFF5F5F5),
+    (ColorName: 'yellow'; ColorValue: $FFFFFF00),
+    (ColorName: 'yellowgreen'; ColorValue: $FF9ACD32));

+ 202 - 0
components/Image32/source/Img32.SVG.HtmlHashConsts.inc

@@ -0,0 +1,202 @@
+const
+  h_038_aacute                = $C40CD1AC;    // &aacute
+  h_038_Aacute_               = $849FF409;    // &Aacute
+  h_038_acirc                 = $840D1F02;    // &acirc
+  h_038_Acirc_                = $7F451D3E;    // &Acirc
+  h_038_acute                 = $2680A5D5;    // &acute
+  h_038_aelig                 = $3D929B9C;    // &aelig
+  h_038_AElig_                = $61DC6137;    // &AElig
+  h_038_agrave                = $CA183FB7;    // &agrave
+  h_038_Agrave_               = $F4CF5768;    // &Agrave
+  h_038_alefsym               = $6808FBEB;    // &alefsym
+  h_038_alpha                 = $EC092241;    // &alpha
+  h_038_Alpha_                = $66CC6726;    // &Alpha
+  h_038_amp                   = $037B88FA;    // &amp
+  h_038_and                   = $62F7C96D;    // &and
+  h_038_ang                   = $938EAA9A;    // &ang
+  h_038_apos                  = $22CA8797;    // &apos
+  h_038_aring                 = $21C30C85;    // &aring
+  h_038_Aring_                = $E12CC5CC;    // &Aring
+  h_038_ast                   = $57D2388E;    // &ast
+  h_038_asymp                 = $98C2269E;    // &asymp
+  h_038_atilde                = $C6B2E000;    // &atilde
+  h_038_Atilde_               = $01CDB917;    // &Atilde
+  h_038_auml                  = $614FA447;    // &auml
+  h_038_Auml_                 = $9677EFF3;    // &Auml
+  h_038_bigcup                = $B6CD4440;    // &bigcup
+  h_038_bigvee                = $03DC1C48;    // &bigvee
+  h_038_bigwedge              = $A7E74E34;    // &bigwedge
+  h_038_brvbar                = $D0BFD065;    // &brvbar
+  h_038_bull                  = $C248AC08;    // &bull
+  h_038_cap                   = $CC74A0B4;    // &cap
+  h_038_ccedil                = $A3A63E8D;    // &ccedil
+  h_038_Ccedil_               = $DDCB49C7;    // &Ccedil
+  h_038_cedil                 = $573159F9;    // &cedil
+  h_038_cent                  = $E9BF1FFA;    // &cent
+  h_038_Cent_                 = $577BC371;    // &Cent
+  h_038_centerdot             = $2DFD2629;    // &centerdot
+  h_038_clubs                 = $6BB65CA1;    // &clubs
+  h_038_cong                  = $F0C66C02;    // &cong
+  h_038_Cong_                 = $AE6DC6E9;    // &Cong
+  h_038_copy                  = $010005D9;    // &copy
+  h_038_Copy_                 = $EF3C2855;    // &Copy
+  h_038_cup                   = $002CC197;    // &cup
+  h_038_curren                = $0DF89EA4;    // &curren
+  h_038_Curren_               = $DE0CAB36;    // &Curren
+  h_038_dagger                = $7656FFBF;    // &dagger
+  h_038_Dagger_               = $D0B7CF56;    // &Dagger
+  h_038_darr                  = $96C9CD3E;    // &darr
+  h_038_Darr_                 = $6F934755;    // &Darr
+  h_038_deg                   = $3DF5EABA;    // &deg
+  h_038_diams                 = $A9A6FD96;    // &diams
+  h_038_divide                = $CAF03BB9;    // &divide
+  h_038_dollar                = $D9E72A5D;    // &dollar
+  h_038_dotsquare             = $C3397AE0;    // &dotsquare
+  h_038_eacute                = $B054C095;    // &eacute
+  h_038_Eacute_               = $FA18A8B9;    // &Eacute
+  h_038_ecirc                 = $F6524191;    // &ecirc
+  h_038_Ecirc_                = $5C7D8715;    // &Ecirc
+  h_038_egrave                = $34A4D3BE;    // &egrave
+  h_038_Egrave_               = $0C9283D5;    // &Egrave
+  h_038_empty                 = $FBD5F648;    // &empty
+  h_038_emsp                  = $E2F82BAC;    // &emsp
+  h_038_ensp                  = $5AE82EFD;    // &ensp
+  h_038_equiv                 = $FBDA3E8C;    // &equiv
+  h_038_eth                   = $61C393B2;    // &eth
+  h_038_ETH_                  = $371CECCD;    // &ETH
+  h_038_euml                  = $831CC155;    // &euml
+  h_038_Euml_                 = $B12AEDB4;    // &Euml
+  h_038_euro                  = $8E9DCA23;    // &euro
+  h_038_exist                 = $60EF4BBD;    // &exist
+  h_038_fnof                  = $27A6853B;    // &fnof
+  h_038_forall                = $00CFD7CA;    // &forall
+  h_038_frac12                = $AFAB928E;    // &frac12
+  h_038_frac14                = $D13ED5B4;    // &frac14
+  h_038_frac34                = $26F38364;    // &frac34
+  h_038_ge                    = $3FE463F0;    // &ge
+  h_038_gt                    = $0FD403D4;    // &gt
+  h_038_harr                  = $228B0A67;    // &harr
+  h_038_hArr_                 = $C3485709;    // &hArr
+  h_038_hearts                = $C2D7805D;    // &hearts
+  h_038_hellip                = $3B6A2517;    // &hellip
+  h_038_iacute                = $B2178DF7;    // &iacute
+  h_038_Iacute_               = $33FE3E74;    // &Iacute
+  h_038_icirc                 = $F728EACE;    // &icirc
+  h_038_Icirc_                = $68FEF48B;    // &Icirc
+  h_038_iexcl                 = $8C9BEDEB;    // &iexcl
+  h_038_igrave                = $BE24CE59;    // &igrave
+  h_038_Igrave_               = $865EE18F;    // &Igrave
+  h_038_image                 = $A3B28075;    // &image
+  h_038_infin                 = $2A0C2732;    // &infin
+  h_038_int                   = $0CE86696;    // &int
+  h_038_iquest                = $FF64EBE5;    // &iquest
+  h_038_isin                  = $35015C96;    // &isin
+  h_038_iuml                  = $B0B2544F;    // &iuml
+  h_038_Iuml_                 = $3311D951;    // &Iuml
+  h_038_lang                  = $9C5C37A0;    // &lang
+  h_038_laquo                 = $C884260F;    // &laquo
+  h_038_larr                  = $0F0233CE;    // &larr
+  h_038_lceil                 = $0E7A49B4;    // &lceil
+  h_038_ldquo                 = $471C971F;    // &ldquo
+  h_038_le                    = $451E7194;    // &le
+  h_038_lfloor                = $77C9F190;    // &lfloor
+  h_038_lowast                = $A79F8857;    // &lowast
+  h_038_loz                   = $65CA22E4;    // &loz
+  h_038_lsquo                 = $F1ECC4BC;    // &lsquo
+  h_038_lt                    = $2F63C61B;    // &lt
+  h_038_macr                  = $321199A1;    // &macr
+  h_038_mdash                 = $A7E292F7;    // &mdash
+  h_038_micro                 = $361E671A;    // &micro
+  h_038_middot                = $451E45D5;    // &middot
+  h_038_minus                 = $AA0B27F0;    // &minus
+  h_038_nbsp                  = $B80B19D4;    // &nbsp
+  h_038_ndash                 = $1BE48A01;    // &ndash
+  h_038_ne                    = $8ABC7A9F;    // &ne
+  h_038_ni                    = $3462CDED;    // &ni
+  h_038_not                   = $6571D361;    // &not
+  h_038_notin                 = $C267D23D;    // &notin
+  h_038_nsub                  = $3173CA9F;    // &nsub
+  h_038_nsube                 = $1AD041BF;    // &nsube
+  h_038_nsup                  = $71994AED;    // &nsup
+  h_038_nsupe                 = $0BF489BB;    // &nsupe
+  h_038_ntilde                = $8EFF31C4;    // &ntilde
+  h_038_Ntilde_               = $70995F88;    // &Ntilde
+  h_038_oacute                = $7C30558E;    // &oacute
+  h_038_Oacute_               = $EC941630;    // &Oacute
+  h_038_ocirc                 = $F453A7D7;    // &ocirc
+  h_038_Ocirc_                = $1B62B2F0;    // &Ocirc
+  h_038_ograve                = $7412F9CF;    // &ograve
+  h_038_Ograve_               = $CC3707A0;    // &Ograve
+  h_038_oplus                 = $83A0D7E5;    // &oplus
+  h_038_or                    = $0B850432;    // &or
+  h_038_ordf                  = $634ADF56;    // &ordf
+  h_038_ordm                  = $2326DF17;    // &ordm
+  h_038_oslash                = $F5890720;    // &oslash
+  h_038_oSlash_               = $D81B4F6F;    // &oSlash
+  h_038_otilde                = $A8F53BA4;    // &otilde
+  h_038_Otilde_               = $1222634D;    // &Otilde
+  h_038_otimes                = $60D268D0;    // &otimes
+  h_038_ouml                  = $E1690926;    // &ouml
+  h_038_Ouml_                 = $10A4C723;    // &Ouml
+  h_038_para                  = $908DCE0A;    // &para
+  h_038_part                  = $9EF86AD3;    // &part
+  h_038_percnt                = $32B29175;    // &percnt
+  h_038_permil                = $8BDC8C51;    // &permil
+  h_038_perp                  = $91CB5D3A;    // &perp
+  h_038_phi                   = $B6BD54CC;    // &phi
+  h_038_pi                    = $1C659BC3;    // &pi
+  h_038_plus                  = $F31E4F4F;    // &plus
+  h_038_plusmn                = $DED28303;    // &plusmn
+  h_038_pound                 = $FA3E1E4A;    // &pound
+  h_038_prime                 = $C00FD683;    // &prime
+  h_038_Prime_                = $343E35F0;    // &Prime
+  h_038_prop                  = $FBA00FBD;    // &prop
+  h_038_quot                  = $3EF3A069;    // &quot
+  h_038_radic                 = $E3557A8E;    // &radic
+  h_038_rang                  = $E665B199;    // &rang
+  h_038_raquo                 = $CB0641C1;    // &raquo
+  h_038_rarr                  = $4EB6874D;    // &rarr
+  h_038_Rarr_                 = $E8708758;    // &Rarr
+  h_038_rceil                 = $71227E13;    // &rceil
+  h_038_rdquo                 = $0E641212;    // &rdquo
+  h_038_real                  = $CBC6DF72;    // &real
+  h_038_reg                   = $9A362741;    // &reg
+  h_038_rfloor                = $BF3EE28E;    // &rfloor
+  h_038_rsquo                 = $3B95A919;    // &rsquo
+  h_038_sdot                  = $B6410D51;    // &sdot
+  h_038_sect                  = $DBCD5E27;    // &sect
+  h_038_shy                   = $C5EDE58D;    // &shy
+  h_038_sigma                 = $7E384D95;    // &sigma
+  h_038_sim                   = $37602F84;    // &sim
+  h_038_spades                = $9B527690;    // &spades
+  h_038_sub                   = $945E4A23;    // &sub
+  h_038_sube                  = $EFD32218;    // &sube
+  h_038_sum                   = $F4A00AC9;    // &sum
+  h_038_sup                   = $23086799;    // &sup
+  h_038_sup1                  = $FDB1BC70;    // &sup1
+  h_038_sup2                  = $EBC8189D;    // &sup2
+  h_038_sup3                  = $D2D3E6BD;    // &sup3
+  h_038_supe                  = $379D3036;    // &supe
+  h_038_szlig                 = $F47D8D0F;    // &szlig
+  h_038_there4                = $F447995B;    // &there4
+  h_038_thinsp                = $E9E04445;    // &thinsp
+  h_038_thorn                 = $56DCFD2E;    // &thorn
+  h_038_THORN_                = $182FE89F;    // &THORN
+  h_038_times                 = $432DB063;    // &times
+  h_038_trade                 = $F7216429;    // &trade
+  h_038_uacute                = $05E35265;    // &uacute
+  h_038_Uacute_               = $35F6CEC7;    // &Uacute
+  h_038_uarr                  = $D0A59EAE;    // &uarr
+  h_038_Uarr_                 = $05F25B6E;    // &Uarr
+  h_038_ucirc                 = $C896F030;    // &ucirc
+  h_038_Ucirc_                = $2DC48084;    // &Ucirc
+  h_038_ugrave                = $6383FA6E;    // &ugrave
+  h_038_Ugrave_               = $4A2102A0;    // &Ugrave
+  h_038_uml                   = $98C50DEB;    // &uml
+  h_038_uuml                  = $5E1AD134;    // &uuml
+  h_038_Uuml_                 = $6DFDCE3C;    // &Uuml
+  h_038_yacute                = $45BAD69D;    // &yacute
+  h_038_Yacute_               = $9AF55263;    // &Yacute
+  h_038_yen                   = $087FF08F;    // &yen
+  h_038_yuml                  = $152D09A6;    // &yuml
+  h_038_Yuml_                 = $DD9EA01E;    // &Yuml

+ 111 - 0
components/Image32/source/Img32.SVG.HtmlValues.inc

@@ -0,0 +1,111 @@
+      //hash identifiers with appended underscores refer to
+      //case sensitive string values (ie they contain capitals)
+      h_038_aacute  : val := 225;
+      h_038_aacute_ : val := 193;
+      h_038_acirc   : val := 226;
+      h_038_acirc_  : val := 194;
+      h_038_acute   : val := 180;
+      h_038_aelig   : val := 230;
+      h_038_aelig_  : val := 198;
+      h_038_agrave  : val := 224;
+      h_038_agrave_ : val := 192;
+      h_038_alefsym : val := $2135;
+      h_038_alpha   : val := $3B1;
+      h_038_Alpha_  : val := $391;
+      h_038_amp     : val := $26;
+      h_038_apos    : val := $27;
+      h_038_aring_  : val := 197;
+      h_038_aring   : val := 229;
+      h_038_ast     : val := $2A;
+      h_038_atilde_ : val := 195;
+      h_038_atilde  : val := 227;
+      h_038_auml_   : val := 196;
+      h_038_auml    : val := 228;
+      h_038_bull    : val := $2022;
+      h_038_cap     : val := $2229;
+      h_038_clubs   : val := $2663;
+      h_038_cup     : val := $222A;
+      h_038_brvbar  : val := 166;
+      h_038_ccedil  : val := 231;
+      h_038_cedil   : val := 184;
+      h_038_cent    : val := 162;
+      h_038_copy    : val := 169;
+      h_038_curren  : val := 164;
+      h_038_deg     : val := 176;
+      h_038_diams   : val := $2666;
+      h_038_divide  : val := $F7;
+      h_038_eacute_ : val := 201;
+      h_038_eacute  : val := 233;
+      h_038_ecirc_  : val := 202;
+      h_038_ecirc   : val := 234;
+      h_038_egrave  : val := 232;
+      h_038_egrave_ : val := 200;
+      h_038_eth     : val := 240;
+      h_038_euml_   : val := 203;
+      h_038_euml    : val := 235;
+      h_038_euro    : val := 128;
+      h_038_frac12  : val := 189;
+      h_038_frac14  : val := 188;
+      h_038_frac34  : val := 190;
+      h_038_ge      : val := $2265;
+      h_038_gt      : val := $3E;
+      h_038_hearts  : val := $2665;
+      h_038_iacute_ : val := 205;
+      h_038_iacute  : val := 237;
+      h_038_icirc_  : val := 206;
+      h_038_icirc   : val := 238;
+      h_038_iexcl   : val := 161;
+      h_038_igrave_ : val := 204;
+      h_038_igrave  : val := 236;
+      h_038_int     : val := $222B;
+      h_038_iquest  : val := 191;
+      h_038_iuml_   : val := 207;
+      h_038_iuml    : val := 239;
+      h_038_laquo   : val := 171;
+      h_038_le      : val := $2264;
+      h_038_lt      : val := $3C;
+      h_038_macr    : val := 175;
+      h_038_micro   : val := 181;
+      h_038_middot  : val := 183;
+      h_038_nbsp    : val := 32;
+      h_038_not     : val := 172;
+      h_038_ntilde  : val := 241;
+      h_038_oacute_ : val := 211;
+      h_038_oacute  : val := 243;
+      h_038_ocirc_  : val := 212;
+      h_038_ocirc   : val := 244;
+      h_038_ograve_ : val := 210;
+      h_038_ograve  : val := 242;
+      h_038_ordf    : val := 170;
+      h_038_ordm    : val := 186;
+      h_038_oslash  : val := 248;
+      h_038_otilde_ : val := 213;
+      h_038_otilde  : val := 245;
+      h_038_ouml_   : val := 214;
+      h_038_ouml    : val := 246;
+      h_038_para    : val := 182;
+      h_038_plusmn  : val := 177;
+      h_038_pound   : val := 163;
+      h_038_quot    : val := $22;
+      h_038_raquo   : val := 187;
+      h_038_reg     : val := 174;
+      h_038_sect    : val := 167;
+      h_038_shy     : val := 173;
+      h_038_spades  : val := $2660;
+      h_038_sup1    : val := 185;
+      h_038_sup2    : val := 178;
+      h_038_sup3    : val := 179;
+      h_038_szlig   : val := 223;
+      h_038_thorn   : val := 254;
+      h_038_times   : val := 215;
+      h_038_uacute_ : val := 218;
+      h_038_uacute  : val := 250;
+      h_038_ucirc   : val := 251;
+      h_038_ucirc_  : val := 219;
+      h_038_ugrave_ : val := 217;
+      h_038_ugrave  : val := 249;
+      h_038_uml     : val := 168;
+      h_038_uuml_   : val := 220;
+      h_038_uuml    : val := 252;
+      h_038_yacute  : val := 253;
+      h_038_yen     : val := 165;

+ 1690 - 0
components/Image32/source/Img32.SVG.Path.pas

@@ -0,0 +1,1690 @@
+unit Img32.SVG.Path;
+
+(*******************************************************************************
+* Author    :  Angus Johnson                                                   *
+* Version   :  4.0                                                             *
+* Date      :  28 December 2021                                                *
+* Website   :  http://www.angusj.com                                           *
+* Copyright :  Angus Johnson 2019-2021                                         *
+*                                                                              *
+* Purpose   :  Essential structures and functions to read SVG Path elements    *
+*                                                                              *
+* License   :  Use, modification & distribution is subject to                  *
+*              Boost Software License Ver 1                                    *
+*              http://www.boost.org/LICENSE_1_0.txt                            *
+*******************************************************************************)
+
+interface
+
+{$I Img32.inc}
+
+uses
+  SysUtils, Classes, Types, Math,
+  {$IFDEF XPLAT_GENERICS} Generics.Collections, Generics.Defaults,{$ENDIF}
+  Img32, Img32.SVG.Core, Img32.Vector, Img32.Text;
+
+{$IFDEF ZEROBASEDSTR}
+  {$ZEROBASEDSTRINGS OFF}
+{$ENDIF}
+
+type
+
+  TSvgPathSegType =
+    (stUnknown, stMove, stLine, stHorz, stVert, stArc,
+    stQBezier, stCBezier, stQSpline, stCSpline, stClose);
+
+  TArcInfo = record
+    rec         : TRectD;
+    startPos    : TPointD;
+    endPos      : TPointD;
+    rectAngle   : double;
+    sweepClockW : Boolean;
+  end;
+  TArcInfos = array of TArcInfo;
+
+  TSvgPath = class;
+  TSvgSubPath = class;
+
+  TSvgPathSeg = class
+  private
+    fParent   : TSvgSubPath;
+    fOwner    : TSvgPath;
+    fIdx      : integer;
+    fFirstPt  : TPointD;
+    fFlatPath : TPathD;
+    fSegType  : TSvgPathSegType;
+    fCtrlPts  : TPathD;
+    fExtend   : integer;
+  protected
+    function GetFlattened: TPathD; virtual;
+    procedure GetFlattenedInternal; virtual; abstract;
+    procedure Scale(value: double); virtual;
+    function DescaleAndOffset(const pt: TPointD): TPointD; overload;
+    function DescaleAndOffset(const p: TPathD): TPathD; overload;
+    procedure SetCtrlPts(const pts: TPathD); virtual;
+  public
+    constructor Create(parent: TSvgSubPath;
+      idx: integer; const firstPt : TPointD); virtual;
+    function GetCtrlBounds: TRectD; virtual;
+    function GetOnPathCtrlPts: TPathD; virtual;
+    procedure Offset(dx, dy: double); virtual;
+    function GetStringDef(relative: Boolean; decimalPrec: integer): string; virtual;
+    function ExtendSeg(const pts: TPathD): Boolean; virtual;
+
+    property Parent   : TSvgSubPath read fParent;
+    property Owner    : TSvgPath read fOwner;
+    property CtrlPts  : TPathD read fCtrlPts write SetCtrlPts;
+    property FirstPt  : TPointD read fFirstPt;
+    property FlatPath : TPathD read GetFlattened;
+    property Index    : integer read fIdx;
+    property SegType  : TSvgPathSegType read fSegType;
+  end;
+
+  TSvgStraightSeg = class(TSvgPathSeg)
+  protected
+    procedure GetFlattenedInternal; override;
+  end;
+
+  TSvgCurvedSeg = class(TSvgPathSeg)
+  protected
+    pendingScale: double;
+    function GetFlattened: TPathD; override;
+    function GetPreviousCtrlPt: TPointD;
+  public
+    function GetLastCtrlPt: TPointD; virtual;
+    constructor Create(parent: TSvgSubPath; idx: integer;
+      const firstPt : TPointD); override;
+  end;
+
+  TSvgASegment = class(TSvgCurvedSeg)
+  private
+    fRectTop  : Boolean;
+    fRectLeft : Boolean;
+    fArcInfo   : TArcInfo;
+    procedure SetArcInfo(ai: TArcInfo);
+    procedure GetRectBtnPoints(out pt1, pt2, pt3: TPointD);
+    procedure SetCtrlPtsFromArcInfo;
+  protected
+    procedure SetCtrlPts(const ctrlPts: TPathD); override;
+    procedure GetFlattenedInternal; override;
+    procedure Scale(value: double); override;
+  public
+  public
+    constructor Create(parent: TSvgSubPath; idx: integer;
+      const firstPt : TPointD); override;
+    procedure Offset(dx, dy: double); override;
+    procedure ReverseArc;
+    function  GetStartAngle: double;
+    function  GetEndAngle: double;
+    function  GetStringDef(relative: Boolean; decimalPrec: integer): string; override;
+    property  ArcInfo: TArcInfo read fArcInfo write SetArcInfo;
+    property  IsLeftCtrl: Boolean read fRectLeft;
+    property  IsTopCtrl: Boolean read fRectTop;
+  end;
+
+  TSvgCSegment = class(TSvgCurvedSeg)
+  protected
+    procedure GetFlattenedInternal; override;
+  public
+    constructor Create(parent: TSvgSubPath; idx: integer;
+      const firstPt : TPointD); override;
+    function GetOnPathCtrlPts: TPathD; override;
+    function  GetStringDef(relative: Boolean; decimalPrec: integer): string; override;
+  end;
+
+  TSvgHSegment = class(TSvgStraightSeg)
+  public
+    constructor Create(parent: TSvgSubPath; idx: integer;
+      const firstPt : TPointD); override;
+    function  GetStringDef(relative: Boolean; decimalPrec: integer): string; override;
+  end;
+
+  TSvgLSegment = class(TSvgStraightSeg)
+  public
+    constructor Create(parent: TSvgSubPath; idx: integer;
+      const firstPt : TPointD); override;
+    function  GetStringDef(relative: Boolean; decimalPrec: integer): string; override;
+  end;
+
+  TSvgQSegment = class(TSvgCurvedSeg)
+  protected
+    procedure GetFlattenedInternal; override;
+  public
+    constructor Create(parent: TSvgSubPath; idx: integer;
+      const firstPt : TPointD); override;
+    function GetOnPathCtrlPts: TPathD; override;
+    function  GetStringDef(relative: Boolean; decimalPrec: integer): string; override;
+  end;
+
+  TSvgSSegment = class(TSvgCurvedSeg)
+  protected
+    procedure GetFlattenedInternal; override;
+  public
+    constructor Create(parent: TSvgSubPath; idx: integer;
+      const firstPt : TPointD); override;
+    function GetOnPathCtrlPts: TPathD; override;
+    function  GetStringDef(relative: Boolean; decimalPrec: integer): string; override;
+  end;
+
+  TSvgTSegment = class(TSvgCurvedSeg)
+  protected
+    procedure GetFlattenedInternal; override;
+  public
+    constructor Create(parent: TSvgSubPath; idx: integer;
+      const firstPt : TPointD); override;
+    function GetLastCtrlPt: TPointD; override;
+    function  GetStringDef(relative: Boolean; decimalPrec: integer): string; override;
+  end;
+
+  TSvgVSegment = class(TSvgStraightSeg)
+  public
+    constructor Create(parent: TSvgSubPath; idx: integer;
+      const firstPt : TPointD); override;
+    function  GetStringDef(relative: Boolean; decimalPrec: integer): string; override;
+  end;
+
+  TSvgZSegment = class(TSvgStraightSeg)
+  public
+    constructor Create(parent: TSvgSubPath; idx: integer;
+      const firstPt : TPointD); override;
+    function  GetStringDef(relative: Boolean; decimalPrec: integer): string; override;
+  end;
+
+  TSvgSegmentClass = class of TSvgPathSeg;
+
+  TSvgSubPath = class
+  private
+    fParent       : TSvgPath;
+    fSegs         : array of TSvgPathSeg;
+    fPendingScale : double;
+    fPathOffset   : TPointD;
+    function GetCount: integer;
+    function GetSeg(index: integer): TSvgPathSeg;
+    function AddSeg(segType: TSvgPathSegType;
+      const startPt: TPointD; const pts: TPathD): TSvgPathSeg;
+  public
+    isClosed  : Boolean;
+    constructor Create(parent: TSvgPath);
+    destructor Destroy; override;
+    procedure Clear;
+    procedure Offset(dx, dy: double);
+    function GetFirstPt: TPointD;
+    function GetLastPt: TPointD;
+    function GetBounds: TRectD;
+
+    function AddASeg(const startPt, endPt: TPointD; const rect: TRectD;
+      angle: double; isClockwise: Boolean): TSvgASegment;
+    function AddCSeg(const startPt: TPointD; const pts: TPathD): TSvgCSegment;
+    function AddHSeg(const startPt: TPointD; const pts: TPathD): TSvgHSegment;
+    function AddLSeg(const startPt: TPointD; const pts: TPathD): TSvgLSegment;
+    function AddQSeg(const startPt: TPointD; const pts: TPathD): TSvgQSegment;
+    function AddSSeg(const startPt: TPointD; const pts: TPathD): TSvgSSegment;
+    function AddTSeg(const startPt: TPointD; const pts: TPathD): TSvgTSegment;
+    function AddVSeg(const startPt: TPointD; const pts: TPathD): TSvgVSegment;
+    function AddZSeg(const endPt, firstPt: TPointD): TSvgZSegment;
+
+    function GetLastSeg: TSvgPathSeg;
+    function DeleteLastSeg: Boolean;
+    //pendingScale: allows 'flattening' to occur with curve precision
+    //that will accommodate future (anticipated) scaling.
+    //Eg: a native image is 32x32 px but will be displayed at 512x512,
+    //so pendingScale should be 16 to ensure a smooth curve
+    function GetFlattenedPath(pendingScale: double = 1.0): TPathD;
+    //GetSimplePath - only used for markers
+    function GetSimplePath: TPathD;
+    function GetMoveStrDef(relative: Boolean; decimalPrec: integer): string;
+    function GetStringDef(relative: Boolean; decimalPrec: integer): string;
+    property Count      : integer read GetCount;
+    property Parent     : TSvgPath read fParent;
+    property PathOffset : TPointD read fPathOffset;
+    property Seg[index: integer]: TSvgPathSeg read GetSeg; default;
+  end;
+
+  TSvgPath = class
+  private
+    fPathScale : double;
+    fPathOffs  : TPointD;
+    fSubPaths: array of TSvgSubPath;
+    function GetPath(index: integer): TSvgSubPath;
+    function GetBounds: TRectD;
+    function GetControlBounds: TRectD;
+    function GetCount: integer;
+  public
+    destructor Destroy; override;
+    procedure Clear;
+    procedure Parse(const value: UTF8String);
+    procedure ScaleAndOffset(scale: double; dx, dy: integer);
+    function  GetStringDef(relative: Boolean; decimalPrec: integer): string;
+
+    function AddPath: TSvgSubPath;
+    procedure DeleteSubPath(subPath: TSvgSubPath);
+    property Bounds: TRectD read GetBounds;
+    property CtrlBounds: TRectD read GetControlBounds;
+    property Count: integer read GetCount;
+    property Path[index: integer]: TSvgSubPath read GetPath; default;
+    property Scale: double read fPathScale;
+    property Offset : TPointD read fPathOffs;
+  end;
+
+  UTF8Strings = array of UTF8String;
+
+  function GetSvgArcInfoRect(const p1, p2: TPointD; radii: TPointD; phi_rads: double;
+    fA, fS: boolean): TRectD;
+
+implementation
+
+resourcestring
+  rsSvgPathRangeError = 'TSvgPath.GetPath range error';
+  rsSvgSubPathRangeError = 'TSvgSubPath.GetSeg range error';
+  //rsSvgSegmentRangeError = 'TSvgSegment.GetVal range error';
+
+//------------------------------------------------------------------------------
+// Miscellaneous functions ...
+//------------------------------------------------------------------------------
+
+function CheckPathLen(const p: TPathD; modLength: integer): TPathD;
+var
+  i, len: integer;
+begin
+  Result := nil;
+  len := Length(p);
+  if (len < modLength) then Exit;
+  Result := p;
+  i := len mod modLength;
+  SetLength(Result, len -i);
+end;
+//------------------------------------------------------------------------------
+
+function TrimTrailingZeros(const floatValStr: string): string;
+var
+  i: integer;
+begin
+  Result := floatValStr;
+  if Pos('.', floatValStr) = 0 then Exit;
+  i := Length(Result);
+  while Result[i] = '0' do dec(i);
+  if Result[i] = '.' then dec(i);
+  SetLength(Result, i);
+end;
+//------------------------------------------------------------------------------
+
+function AsIntStr(val: double): string;
+begin
+  Result := Format('%1.0n ', [val]);
+end;
+//------------------------------------------------------------------------------
+
+function AsFloatStr(val: double; precision: integer): string;
+begin
+  Result := TrimTrailingZeros(Format('%1.*f', [precision, val]));
+end;
+//------------------------------------------------------------------------------
+
+function AsCoordStr(pt: TPointD;
+  const relPt: TPointD; relative: Boolean; precision: integer): string;
+var
+  s1, s2: string;
+begin
+  if relative then
+  begin
+    pt.X := pt.X - relPt.X;
+    pt.Y := pt.Y - relPt.Y;
+  end;
+  s1 := TrimTrailingZeros(Format('%1.*f', [precision, pt.x]));
+  s2 := TrimTrailingZeros(Format('%1.*f', [precision, pt.y]));
+  Result := s1 + ',' + s2 + ' ';
+end;
+//------------------------------------------------------------------------------
+
+function GetSingleDigit(var c, endC: PUTF8Char;
+  out digit: integer): Boolean;
+begin
+  Result := SkipBlanksAndComma(c, endC) and (c^ >= '0') and (c^ <= '9');
+  if not Result then Exit;
+  digit := Ord(c^) - Ord('0');
+  inc(c);
+end;
+//------------------------------------------------------------------------------
+
+function GetSegType(var c, endC: PUTF8Char; out isRelative: Boolean): TSvgPathSegType;
+var
+  ch: UTF8Char;
+begin
+  Result := stUnknown;
+  if not SkipBlanks(c, endC) then Exit;
+  ch := upcase(c^);
+  if not CharInSet(ch,
+    ['A','C','H','M','L','Q','S','T','V','Z']) then Exit;
+  case ch of
+    'M': Result := stMove;
+    'L': Result := stLine;
+    'H': Result := stHorz;
+    'V': Result := stVert;
+    'A': Result := stArc;
+    'Q': Result := stQBezier;
+    'C': Result := stCBezier;
+    'T': Result := stQSpline;
+    'S': Result := stCSpline;
+    'Z': Result := stClose;
+  end;
+  isRelative := c^ >= 'a';
+  inc(c);
+end;
+//------------------------------------------------------------------------------
+
+function Parse2Num(var c, endC: PUTF8Char;
+  out pt: TPointD; const relPt: TPointD): Boolean;
+begin
+  Result := ParseNextNum(c, endC, true, pt.X) and
+    ParseNextNum(c, endC, true, pt.Y);
+  if not Result or (relPt.X = InvalidD) then Exit;
+  pt.X := pt.X + relPt.X;
+  pt.Y := pt.Y + relPt.Y;
+end;
+//------------------------------------------------------------------------------
+
+function Parse1Num(var c: PUTF8Char; endC: PUTF8Char;
+  out val: double; relVal: double): Boolean;
+begin
+  Result := ParseNextNum(c, endC, true, val);
+  if Result and (relVal <> InvalidD) then
+    val := val + relVal;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgPathSeg
+//------------------------------------------------------------------------------
+
+constructor TSvgPathSeg.Create(parent: TSvgSubPath;
+  idx: integer; const firstPt : TPointD);
+begin
+  Self.fParent  := parent;
+  Self.fOwner   := parent.fParent;
+  Self.fIdx     := idx;
+  Self.fFirstPt := firstPt;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgPathSeg.Scale(value: double);
+begin
+  if (value <> 0) and (value <> 1) then
+  begin
+    fCtrlPts := ScalePath(fCtrlPts, value);
+    fFirstPt := ScalePoint(fFirstPt, value);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPathSeg.DescaleAndOffset(const pt: TPointD): TPointD;
+begin
+  Result := pt;
+  OffsetPoint(Result, -parent.PathOffset.X, -parent.PathOffset.Y);
+  Result := ScalePoint(Result, 1/Owner.Scale);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPathSeg.DescaleAndOffset(const p: TPathD): TPathD;
+begin
+  Result := OffsetPath(p, -parent.PathOffset.X, -parent.PathOffset.Y);
+  Result := ScalePath(Result, 1/Owner.Scale);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgPathSeg.Offset(dx, dy: double);
+begin
+  fFirstPt := OffsetPoint(fFirstPt, dx, dy);
+  fCtrlPts := OffsetPath(fCtrlPts, dx, dy);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgPathSeg.SetCtrlPts(const pts: TPathD);
+begin
+  fCtrlPts := pts;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPathSeg.ExtendSeg(const pts: TPathD): Boolean;
+var
+  len: integer;
+begin
+  len := Length(pts);
+  Result := (len <> 0) and (fExtend <> 0) and (len mod fExtend = 0);
+  if Result then Img32.Vector.AppendPath(fCtrlPts, pts);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPathSeg.GetCtrlBounds: TRectD;
+begin
+  Result := GetBoundsD(PrePendPoint(fFirstPt, CtrlPts));
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPathSeg.GetFlattened: TPathD;
+begin
+  GetFlattenedInternal;
+  Result := fFlatPath;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPathSeg.GetOnPathCtrlPts: TPathD;
+begin
+  Result := fCtrlPts;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPathSeg.GetStringDef(relative: Boolean; decimalPrec: integer): string;
+begin
+  Result := '';
+end;
+
+//------------------------------------------------------------------------------
+// TSvgStraightSeg
+//------------------------------------------------------------------------------
+
+procedure TSvgStraightSeg.GetFlattenedInternal;
+begin
+  fFlatPath := PrePendPoint(fFirstPt, fCtrlPts);
+end;
+
+//------------------------------------------------------------------------------
+// TSvgCurvedSeg
+//------------------------------------------------------------------------------
+
+constructor TSvgCurvedSeg.Create(parent: TSvgSubPath; idx: integer;
+  const firstPt : TPointD);
+begin
+  inherited;
+  pendingScale := 1.0;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgCurvedSeg.GetFlattened: TPathD;
+begin
+  //if the image has been rendered previously at a lower resolution, then
+  //redo the flattening otherwise curves my look very rough.
+  if (pendingScale < Parent.fPendingScale) then
+    pendingScale := Parent.fPendingScale;
+
+  GetFlattenedInternal;
+  Result := fFlatPath;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgCurvedSeg.GetLastCtrlPt: TPointD;
+begin
+  Result := CtrlPts[High(CtrlPts) -1];
+end;
+//------------------------------------------------------------------------------
+
+function TSvgCurvedSeg.GetPreviousCtrlPt: TPointD;
+begin
+  if (fIdx > 0) and (fSegType in [stQSpline, stCSpline]) then
+  begin
+    if (fSegType = stQSpline) and
+      (fParent[fIdx -1].fSegType in [stQBezier, stQSpline]) then
+        Result := TSvgCurvedSeg(fParent[fIdx -1]).GetLastCtrlPt
+    else if (fSegType = stCSpline) and
+      (fParent[fIdx -1].fSegType in [stCBezier, stCSpline]) then
+        Result := TSvgCurvedSeg(fParent[fIdx -1]).GetLastCtrlPt
+    else
+      Result := fFirstPt;
+  end else
+    Result := fFirstPt;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgASegment
+//------------------------------------------------------------------------------
+
+constructor TSvgASegment.Create(parent: TSvgSubPath; idx: integer;
+  const firstPt : TPointD);
+begin
+  inherited;
+  fSegType  := stArc;
+  fExtend   := 0;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgASegment.SetArcInfo(ai: TArcInfo);
+var
+  dx, dy: double;
+begin
+  //make sure that all the ai fields are valid,
+  //otherwise adjust them and align with ai.startpos
+  with fArcInfo do
+  begin
+    rec       := ai.rec;
+    rectAngle := ai.rectAngle;
+    startPos  := GetClosestPtOnRotatedEllipse(rec, rectAngle, ai.startPos);
+    endPos    := GetClosestPtOnRotatedEllipse(rec, rectAngle, ai.endPos);
+    sweepClockW := ai.sweepClockW;
+    if not PointsNearEqual(ai.startPos, startPos, 0.01) then
+    begin
+      dx := ai.startPos.X - startPos.X;
+      dy := ai.startPos.Y - startPos.Y;
+      OffsetRect(rec, dx, dy);
+      startPos := ai.startPos;
+      endPos := OffsetPoint(endPos, dx, dy);
+    end;
+  end;
+  SetCtrlPtsFromArcInfo;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgASegment.GetRectBtnPoints(out pt1, pt2, pt3: TPointD);
+var
+  d         : double;
+  pt, sp    : TPointD;
+begin
+  with fArcInfo do
+  begin
+    //keep rec oriented to the XY axis and rotate startpos
+    sp := startPos;
+    pt2 := rec.MidPoint;
+
+    if rectAngle <> 0 then
+      RotatePoint(sp, pt2, -rectAngle);
+
+    pt := PointD(rec.Left, pt2.Y);
+    pt3 := PointD(rec.Right, pt2.Y);
+
+    d := DistanceSqrd(pt, sp) - DistanceSqrd(pt3, sp);
+    if not ValueAlmostZero(d, 0.01) then
+      fRectLeft := d > 0;
+    if fRectLeft then
+      pt1 := PointD(rec.Left, pt2.Y) else
+      pt1 := PointD(rec.Right, pt2.Y);
+
+    pt := PointD(pt2.X, rec.Top);
+    pt3 := PointD(pt2.X, rec.Bottom);
+    d := DistanceSqrd(pt, sp) - DistanceSqrd(pt3, sp);
+    if not ValueAlmostZero(d, 0.01) then fRectTop := d > 0;
+    if fRectTop then
+      pt3 := PointD(pt2.X, rec.Top) else
+      pt3 := PointD(pt2.X, rec.Bottom);
+
+    RotatePoint(pt1, pt2, rectAngle);
+    RotatePoint(pt3, pt2, rectAngle);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgASegment.SetCtrlPtsFromArcInfo;
+begin
+  SetLength(fCtrlPts, 5);
+  with fArcInfo do
+  begin
+    fCtrlPts[0] := startPos;
+    GetRectBtnPoints(fCtrlPts[1], fCtrlPts[2], fCtrlPts[3]);
+    fCtrlPts[4] := endPos;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgASegment.GetFlattenedInternal;
+var
+  a1,a2: double;
+  p: TPathD;
+begin
+  fFlatPath := nil;
+  with fArcInfo do
+  begin
+    a1 := GetStartAngle;
+    a2 := GetEndAngle;
+    if not sweepClockW then
+    begin
+      p := Arc(rec, a2, a1, pendingScale);
+      p := ReversePath(p);
+    end else
+      p := Arc(rec, a1, a2, pendingScale);
+    if rectAngle <> 0 then
+      p := RotatePath(p, rec.MidPoint, rectAngle);
+    AppendPath(fFlatPath, p);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgASegment.GetStartAngle: double;
+begin
+  with fArcInfo do
+    Result := GetRotatedEllipticalAngleFromPoint(rec, rectAngle, startPos);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgASegment.GetEndAngle: double;
+begin
+  with fArcInfo do
+    Result := GetRotatedEllipticalAngleFromPoint(rec, rectAngle, endPos);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgASegment.ReverseArc;
+begin
+  fArcInfo.sweepClockW := not fArcInfo.sweepClockW;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgASegment.Offset(dx, dy: double);
+begin
+  inherited;
+  with fArcInfo do
+  begin
+    OffsetRect(rec, dx, dy);
+    startPos := OffsetPoint(startPos, dx, dy);
+    endPos := OffsetPoint(endPos, dx, dy);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgASegment.Scale(value: Double);
+begin
+  if (value = 0) or (value = 1) then Exit;
+  inherited;
+  with fArcInfo do
+  begin
+    rec := ScaleRect(rec, value);
+    startPos :=  ScalePoint(startPos, value);
+    endPos := ScalePoint(endPos, value);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgASegment.SetCtrlPts(const ctrlPts: TPathD);
+begin
+  //SetCtrlPtsFromArcInfo;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgASegment.GetStringDef(relative: Boolean; decimalPrec: integer): string;
+var
+  a, a1,a2: double;
+  sp, ep: TPointD;
+begin
+  with fArcInfo do
+  begin
+    if relative then Result := 'a ' else Result := 'A ';
+
+    Result := Result +
+      AsFloatStr(rec.Width *0.5 /Owner.Scale, decimalPrec) + ',';
+    Result := Result +
+      AsFloatStr(rec.Height *0.5 /Owner.Scale, decimalPrec) + ' ';
+    //angle as degrees
+    Result := Result + AsIntStr(RadToDeg(rectAngle));
+
+    a1 := GetStartAngle;
+    a2 := GetEndAngle;
+
+    //large arce and direction flags
+    a := a2 - a1;
+    if a < 0 then a := a + angle360;
+
+    if sweepClockW then
+    begin
+      if a  >= angle180 then
+        Result := Result + '1 1 ' else
+        Result := Result + '0 1 ';
+    end else
+    begin
+      if a >= angle180 then
+        Result := Result + '0 0 ' else
+        Result := Result + '1 0 ';
+    end;
+    //descaled and de-offset end position
+    ep := DescaleAndOffset(endPos);
+    sp := DescaleAndOffset(startPos);
+    Result := Result + AsCoordStr(ep, sp, relative, decimalPrec);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgCSegment
+//------------------------------------------------------------------------------
+
+constructor TSvgCSegment.Create(parent: TSvgSubPath; idx: integer;
+  const firstPt : TPointD);
+begin
+  inherited;
+  fSegType  := stCBezier;
+  fExtend   := 3;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgCSegment.GetOnPathCtrlPts: TPathD;
+var
+  i, len: integer;
+begin
+  len := Length(fCtrlPts) div 3;
+  SetLength(Result, len);
+  for i := 0 to High(Result) do
+    Result[i] := fCtrlPts[i*3 +2];
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgCSegment.GetFlattenedInternal;
+var
+  bt  : double;
+  p: TPathD;
+begin
+  bt := BezierTolerance / pendingScale;
+  p := CheckPathLen(fCtrlPts, 3);
+  if p = nil then
+    fFlatPath := nil else
+    fFlatPath := FlattenCBezier(fFirstPt, p, bt);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgCSegment.GetStringDef(relative: Boolean; decimalPrec: integer): string;
+var
+  i: integer;
+  pt, relPt: TPointD;
+begin
+  if relative then Result := 'c ' else Result := 'C ';
+  relPt := DescaleAndOffset(fFirstPt);
+  for i := 0 to High(fCtrlPts) do
+  begin
+    pt:= DescaleAndOffset(fCtrlPts[i]);
+    Result := Result + AsCoordStr(pt, relPt, relative, decimalPrec);
+    if relative and (i mod 3 = 2) then relPt := pt;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgHSegment
+//------------------------------------------------------------------------------
+
+constructor TSvgHSegment.Create(parent: TSvgSubPath; idx: integer;
+  const firstPt : TPointD);
+begin
+  inherited;
+  fSegType  := stHorz;
+  fExtend   := 1;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgHSegment.GetStringDef(relative: Boolean; decimalPrec: integer): string;
+var
+  i: integer;
+  pt, relPt: TPointD;
+begin
+  if relative then
+  begin
+    Result := 'h ';
+    relPt := DescaleAndOffset(fFirstPt);
+    for i := 0 to High(fCtrlPts) do
+    begin
+      pt := DescaleAndOffset(fCtrlPts[i]);
+      Result := Result + AsFloatStr(pt.X - relPt.X, decimalPrec) + ' ';
+      relPt := pt;
+    end;
+  end else
+  begin
+    Result := 'H ';
+    for i := 0 to High(fCtrlPts) do
+    begin
+      pt := DescaleAndOffset(fCtrlPts[i]);
+      Result := Result + AsFloatStr(pt.X, decimalPrec) + ' ';
+    end;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgLSegment
+//------------------------------------------------------------------------------
+
+constructor TSvgLSegment.Create(parent: TSvgSubPath; idx: integer;
+  const firstPt : TPointD);
+begin
+  inherited;
+  fSegType  := stLine;
+  fExtend   := 1;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgLSegment.GetStringDef(relative: Boolean; decimalPrec: integer): string;
+var
+  i: integer;
+  pt, relPt: TPointD;
+begin
+  if relative then Result := 'l ' else Result := 'L ';
+  relPt := DescaleAndOffset(fFirstPt);
+  for i := 0 to High(fCtrlPts) do
+  begin
+    pt:= DescaleAndOffset(fCtrlPts[i]);
+    Result := Result + AsCoordStr(pt, relPt, relative, decimalPrec);
+    relPt := pt;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgQSegment
+//------------------------------------------------------------------------------
+
+constructor TSvgQSegment.Create(parent: TSvgSubPath; idx: integer;
+  const firstPt : TPointD);
+begin
+  inherited;
+  fSegType  := stQBezier;
+  fExtend   := 2;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgQSegment.GetOnPathCtrlPts: TPathD;
+var
+  i, len: integer;
+begin
+  len := Length(fCtrlPts) div 2;
+  SetLength(Result, len);
+  for i := 0 to High(Result) do
+    Result[i] := fCtrlPts[i*2+1];
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgQSegment.GetFlattenedInternal;
+var
+  bt  : double;
+  p: TPathD;
+begin
+  bt := BezierTolerance / pendingScale;
+  p := CheckPathLen(fCtrlPts, 2);
+  if p = nil then
+    fFlatPath := nil else
+    fFlatPath := FlattenQBezier(fFirstPt, fCtrlPts, bt);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgQSegment.GetStringDef(relative: Boolean; decimalPrec: integer): string;
+var
+  i: integer;
+  pt, relPt: TPointD;
+begin
+  if relative then Result := 'q ' else Result := 'Q ';
+  relPt := DescaleAndOffset(fFirstPt);
+  for i := 0 to High(fCtrlPts) do
+  begin
+    pt := DescaleAndOffset(fCtrlPts[i]);
+    Result := Result + AsCoordStr(pt, relPt, relative, decimalPrec);
+    if (i mod 2) = 1 then relPt := pt;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgSSegment
+//------------------------------------------------------------------------------
+
+constructor TSvgSSegment.Create(parent: TSvgSubPath; idx: integer;
+  const firstPt : TPointD);
+begin
+  inherited;
+  fSegType  := stCSpline;
+  fExtend   := 2;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgSSegment.GetFlattenedInternal;
+var
+  bt  : double;
+  p: TPathD;
+begin
+  bt := BezierTolerance / pendingScale;
+  p := CheckPathLen(fCtrlPts, 2);
+  if p = nil then
+    fFlatPath := nil else
+    fFlatPath := FlattenCSpline(GetPreviousCtrlPt, fFirstPt, fCtrlPts, bt);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSSegment.GetOnPathCtrlPts: TPathD;
+var
+  i, len: integer;
+begin
+  len := Length(fCtrlPts) div 2;
+  SetLength(Result, len);
+  for i := 0 to High(Result) do
+    Result[i] := fCtrlPts[i*2+1];
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSSegment.GetStringDef(relative: Boolean; decimalPrec: integer): string;
+var
+  i: integer;
+  pt, relPt: TPointD;
+begin
+  if relative then Result := 's ' else Result := 'S ';
+  relPt := DescaleAndOffset(fFirstPt);
+  for i := 0 to High(fCtrlPts) do
+  begin
+    pt := DescaleAndOffset(fCtrlPts[i]);
+    Result := Result + AsCoordStr(pt, relPt, relative, decimalPrec);
+    if relative and (i mod 2 = 1) then relPt := pt;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgTSegment
+//------------------------------------------------------------------------------
+
+constructor TSvgTSegment.Create(parent: TSvgSubPath; idx: integer;
+  const firstPt : TPointD);
+begin
+  inherited;
+  fSegType  := stQSpline;
+  fExtend   := 1;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgTSegment.GetFlattenedInternal;
+var
+  bt: double;
+begin
+  bt := BezierTolerance / pendingScale;
+  if fCtrlPts = nil then
+    fFlatPath := nil else
+    fFlatPath := FlattenQSpline(GetPreviousCtrlPt, fFirstPt, fCtrlPts, bt);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgTSegment.GetLastCtrlPt: TPointD;
+var
+  i: integer;
+begin
+  Result := ReflectPoint(GetPreviousCtrlPt, fFirstPt);
+  for i := 0 to High(CtrlPts) -1 do
+    Result := ReflectPoint(Result, CtrlPts[i]);
+end;
+//------------------------------------------------------------------------------
+
+
+function  TSvgTSegment.GetStringDef(relative: Boolean; decimalPrec: integer): string;
+var
+  i: integer;
+  pt, relPt: TPointD;
+begin
+  if relative then Result := 't ' else Result := 'T ';
+  relPt := DescaleAndOffset(fFirstPt);
+  for i := 0 to High(fCtrlPts) do
+  begin
+    pt := DescaleAndOffset(fCtrlPts[i]);
+    Result := Result + AsCoordStr(pt, relPt, relative, decimalPrec);
+    if relative then relPt := pt;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgVSegment
+//------------------------------------------------------------------------------
+
+constructor TSvgVSegment.Create(parent: TSvgSubPath; idx: integer;
+  const firstPt : TPointD);
+begin
+  inherited;
+  fSegType  := stVert;
+  fExtend   := 1;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgVSegment.GetStringDef(relative: Boolean; decimalPrec: integer): string;
+var
+  i: integer;
+  pt, relPt: TPointD;
+begin
+  if relative then
+  begin
+    Result := 'v ';
+    relPt := DescaleAndOffset(fFirstPt);
+    for i := 0 to High(fCtrlPts) do
+    begin
+      pt := DescaleAndOffset(fCtrlPts[i]);
+      Result := Result + AsFloatStr(pt.Y - relPt.Y, decimalPrec) + ' ';
+      relPt := pt;
+    end;
+  end else
+  begin
+    Result := 'V ';
+    for i := 0 to High(fCtrlPts) do
+    begin
+      pt := DescaleAndOffset(fCtrlPts[i]);
+      Result := Result + AsFloatStr(pt.Y, decimalPrec) + ' ';
+    end;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgZSegment
+//------------------------------------------------------------------------------
+
+constructor TSvgZSegment.Create(parent: TSvgSubPath;
+  idx: integer; const firstPt : TPointD);
+begin
+  inherited;
+  fSegType  := stClose;
+  fExtend   := 0;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgZSegment.GetStringDef(relative: Boolean;
+  decimalPrec: integer): string;
+begin
+  Result := 'Z ';
+end;
+
+//------------------------------------------------------------------------------
+// TSvgSubPath
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.GetFlattenedPath(pendingScale: double): TPathD;
+var
+  i: integer;
+begin
+  if pendingScale <= 0 then pendingScale := 1.0;
+  if (pendingScale > fPendingScale) then
+    fPendingScale := pendingScale;
+  Result := nil;
+  for i := 0 to High(fSegs) do
+    AppendPath(Result, fSegs[i].GetFlattened);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.AddSeg(segType: TSvgPathSegType;
+  const startPt: TPointD; const pts: TPathD): TSvgPathSeg;
+var
+  i: integer;
+begin
+  i := Length(fSegs);
+  SetLength(fSegs, i+1);
+  case segType of
+    stCBezier    : Result := TSvgCSegment.Create(self, i, startPt);
+    stHorz    : Result := TSvgHSegment.Create(self, i, startPt);
+    stLine    : Result := TSvgLSegment.Create(self, i, startPt);
+    stQBezier    : Result := TSvgQSegment.Create(self, i, startPt);
+    stCSpline : Result := TSvgSSegment.Create(self, i, startPt);
+    stQSpline : Result := TSvgTSegment.Create(self, i, startPt);
+    stVert    : Result := TSvgVSegment.Create(self, i, startPt);
+    else raise Exception.Create('TSvgSubPath.AddSeg error');
+  end;
+  fSegs[i] := Result;
+  Result.fCtrlPts := pts;
+  if Result is TSvgCurvedSeg then
+    TSvgCurvedSeg(Result).pendingScale := fPendingScale;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.AddASeg(const startPt, endPt: TPointD; const rect: TRectD;
+  angle: double; isClockwise: Boolean): TSvgASegment;
+var
+  i: integer;
+begin
+  i := Length(fSegs);
+  SetLength(fSegs, i+1);
+  Result := TSvgASegment.Create(self, i, startPt);
+  fSegs[i] := Result;
+  Result.pendingScale := self.fPendingScale;
+  with Result.fArcInfo do
+  begin
+    rec := rect;
+    startPos := startPt;
+    endPos   := endPt;
+    rectAngle := angle;
+    sweepClockW := isClockwise;
+  end;
+  Result.SetCtrlPtsFromArcInfo;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.AddHSeg(const startPt: TPointD; const pts: TPathD): TSvgHSegment;
+begin
+  Result := AddSeg(stHorz, startPt, pts) as TSvgHSegment;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.AddCSeg(const startPt: TPointD; const pts: TPathD): TSvgCSegment;
+begin
+  Result := AddSeg(stCBezier, startPt, pts) as TSvgCSegment;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.AddLSeg(const startPt: TPointD; const pts: TPathD): TSvgLSegment;
+begin
+  Result := AddSeg(stLine, startPt, pts) as TSvgLSegment;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.AddQSeg(const startPt: TPointD; const pts: TPathD): TSvgQSegment;
+begin
+  Result := AddSeg(stQBezier, startPt, pts) as TSvgQSegment;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.AddSSeg(const startPt: TPointD; const pts: TPathD): TSvgSSegment;
+begin
+  Result := AddSeg(stCSpline, startPt, pts) as TSvgSSegment;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.AddTSeg(const startPt: TPointD; const pts: TPathD): TSvgTSegment;
+begin
+  Result := AddSeg(stQSpline, startPt, pts) as TSvgTSegment;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.AddVSeg(const startPt: TPointD; const pts: TPathD): TSvgVSegment;
+begin
+  Result := AddSeg(stVert, startPt, pts) as TSvgVSegment;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.AddZSeg(const endPt, firstPt: TPointD): TSvgZSegment;
+var
+  i: integer;
+begin
+  i := Length(fSegs);
+  SetLength(fSegs, i+1);
+  Result := TSvgZSegment.Create(self, i, endPt);
+  fSegs[i] := Result;
+  SetLength(Result.fCtrlPts, 1);
+  Result.fCtrlPts[0] := firstPt;
+  isClosed := true;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.GetLastSeg: TSvgPathSeg;
+var
+  cnt: integer;
+begin
+  cnt := Count;
+  if cnt = 0 then
+    Result := nil else
+    Result := seg[cnt -1];
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.DeleteLastSeg: Boolean;
+var
+  cnt: integer;
+begin
+  cnt := Count;
+  Result := cnt > 0;
+  if not Result then Exit;
+  seg[cnt -1].Free;
+  SetLength(fSegs, cnt -1);
+  if isClosed then isClosed := false;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.GetSimplePath: TPathD;
+var
+  i: integer;
+begin
+  Result := Img32.Vector.MakePath([GetFirstPt.X, GetFirstPt.Y]);
+  for i := 0 to High(fSegs) do
+    AppendPath(Result, fSegs[i].GetOnPathCtrlPts);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.GetMoveStrDef(relative: Boolean; decimalPrec: integer): string;
+var
+  pt: TPointD;
+begin
+  Result := '';
+  if Length(fSegs) = 0 then Exit;
+
+  if decimalPrec < -3 then decimalPrec := -3
+  else if decimalPrec > 4 then decimalPrec := 4;
+
+  with fParent do
+  begin
+    pt.X := (fSegs[0].fFirstPt.X - self.PathOffset.X - Offset.X)/fPathScale;
+    pt.Y := (fSegs[0].fFirstPt.Y - self.PathOffset.Y - Offset.Y)/fPathScale;
+  end;
+  Result := 'M ' + AsCoordStr(pt, NullPointD, false, decimalPrec);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.GetStringDef(relative: Boolean; decimalPrec: integer): string;
+var
+  i: integer;
+begin
+  if decimalPrec < -3 then decimalPrec := -3
+  else if decimalPrec > 4 then decimalPrec := 4;
+  if Count = 0 then Exit;
+
+  Result := GetMoveStrDef(relative, decimalPrec);
+  for i := 0 to Count -1 do
+    Result := Result + fSegs[i].GetStringDef(relative, decimalPrec);
+end;
+//------------------------------------------------------------------------------
+
+constructor TSvgSubPath.Create(parent: TSvgPath);
+begin
+  fParent := parent;
+end;
+//------------------------------------------------------------------------------
+
+destructor TSvgSubPath.Destroy;
+begin
+  Clear;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgSubPath.Clear;
+var
+  i: integer;
+begin
+  for i := 0 to Count -1 do
+    fSegs[i].Free;
+  fSegs := nil;
+  fPathOffset := NullPointD;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.GetCount: integer;
+begin
+  Result := Length(fSegs);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgSubPath.Offset(dx, dy: double);
+var
+  i: integer;
+begin
+  //fPathOffset := OffsetPoint(pathOffset, dx,dy); //DON'T DO THIS!
+  for i := 0 to High(fSegs) do fSegs[i].Offset(dx, dy);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.GetSeg(index: integer): TSvgPathSeg;
+begin
+  if (index < 0) or (index >= Count) then
+    raise Exception.Create(rsSvgSubPathRangeError);
+  Result := fSegs[index];
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.GetFirstPt: TPointD;
+begin
+  if Count = 0 then Result := NullPointD
+  else Result := fSegs[0].FirstPt;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.GetLastPt: TPointD;
+begin
+  if Count = 0 then
+    Result := NullPointD
+  else with fSegs[Count -1] do
+    Result := CtrlPts[High(CtrlPts)];
+end;
+//------------------------------------------------------------------------------
+
+function TSvgSubPath.GetBounds: TRectD;
+var
+  i: integer;
+  p: TPathD;
+begin
+  p := nil;
+  for i := 0 to Count -1 do
+    AppendPath(p, fSegs[i].fFlatPath);
+  Result := Img32.Vector.GetBoundsD(p);
+end;
+
+//------------------------------------------------------------------------------
+// TSvgPath
+//------------------------------------------------------------------------------
+
+destructor TSvgPath.Destroy;
+begin
+  Clear;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgPath.ScaleAndOffset(scale: double; dx, dy: integer);
+var
+  i,j: integer;
+begin
+  if fPathScale = 0 then fPathScale := 1;
+  if scale = 0 then scale := 1;
+  fPathScale := fPathScale * scale;
+
+  fPathOffs := PointD(dx, dy);
+  for i := 0 to Count -1 do
+    with fSubPaths[i] do
+    begin
+      if scale <> 1 then
+      for j := 0 to High(fSegs) do
+        fSegs[j].Scale(scale);
+      Offset(dx,dy);
+    end;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPath.GetStringDef(relative: Boolean; decimalPrec: integer): string;
+var
+  i : integer;
+begin
+  result := '';
+  if fPathScale = 0 then  fPathScale := 1;
+  for i := 0 to High(fSubPaths) do
+    Result := Result + fSubPaths[i].GetStringDef(relative, decimalPrec);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgPath.Parse(const value: UTF8String);
+var
+  c, endC     : PUTF8Char;
+  firstPt     : TPointD;
+  lastPt      : TPointD;
+  currPt      : TPointD;
+  pt2, pt3    : TPointD;
+  angle       : double;
+  sweepCW     : integer;
+  largeArc    : integer;
+  arcRec      : TRectD;
+  isRelative  : Boolean;
+  currSegType : TSvgPathSegType;
+  currSubPath : TSvgSubPath;
+  pts         : TPathD;
+  ptCap       : integer;
+  ptCnt       : integer;
+
+  procedure AddPt(const pt: TPointD);
+  begin
+    if ptCnt = ptCap then
+    begin
+      inc(ptCap, 8);
+      setLength(pts, ptCap);
+    end;
+    pts[ptCnt] := pt;
+    inc(ptCnt);
+  end;
+
+begin
+  Clear;
+  currSubPath := nil;
+
+  c := PUTF8Char(value);
+  endC := c + Length(value);
+  isRelative := false;
+  currPt := NullPointD;
+
+  while true do
+  begin
+    currSegType := GetSegType(c, endC, isRelative);
+    if currSegType = stUnknown then Break;
+
+    if currSegType = stMove then
+    begin
+      currSubPath := nil;
+
+      if isRelative then
+        lastPt := currPt else
+        lastPt := InvalidPointD;
+
+      if not Parse2Num(c, endC, currPt, lastPt) then break;
+      lastPt :=  currPt;
+      //values immediately following a Move are implicitly Line statements
+      if IsNumPending(c, endC, true) then
+        currSegType := stLine else
+        Continue;
+    end
+    else if (currSegType = stClose) then
+    begin
+      if Assigned(currSubPath) and (currSubPath.Count > 0) then
+      begin
+        lastPt := currPt;
+        currPt := currSubPath.GetFirstPt;
+        currSubPath.AddZSeg(lastPt, currPt);
+      end;
+      currSubPath := nil;
+      Continue;
+    end;
+
+    if not Assigned(currSubPath) then
+      currSubPath := AddPath;
+
+    pts := nil;
+    ptCnt := 0; ptCap := 0;
+    firstPt := currPt;
+    if isRelative then
+      lastPt := firstPt else
+      lastPt := InvalidPointD;
+
+    case currSegType of
+      stArc:
+        begin
+          //nb: unlike other segment types,
+          //consecutive arc segs are separated.
+          while IsNumPending(c, endC, true) and
+            Parse2Num(c, endC, pt2, InvalidPointD) and
+            ParseNextNum(c, endC, true, angle) and
+            GetSingleDigit(c, endC, largeArc) and
+            GetSingleDigit(c, endC, sweepCW) and
+            Parse2Num(c, endC, currPt, lastPt) do
+          begin
+            angle := DegToRad(angle);
+            arcRec := GetSvgArcInfoRect(firstPt, currPt, pt2,
+              angle, largeArc <> 0, sweepCW <> 0);
+            if arcRec.IsEmpty then break;
+
+            currSubPath.AddASeg(firstPt, currPt,
+              arcRec, angle, sweepCW <> 0);
+
+            if isRelative then lastPt := currPt;
+            firstPt := currPt;
+          end;
+        end;
+      stCBezier:
+        begin
+          while IsNumPending(c, endC, true) and
+            Parse2Num(c, endC, pt2, lastPt) and
+            Parse2Num(c, endC, pt3, lastPt) and
+            Parse2Num(c, endC, currPt, lastPt) do
+          begin
+            AddPt(pt2);
+            AddPt(pt3);
+            AddPt(currPt);
+            if isRelative then lastPt := currPt;
+          end;
+          SetLength(pts, ptCnt);
+          currSubPath.AddSeg(stCBezier, firstPt, pts);
+        end;
+      stHorz:
+        begin
+          while IsNumPending(c, endC, true) and
+            Parse1Num(c, endC, currPt.X, lastPt.X) do
+          begin
+            AddPt(currPt);
+            if isRelative then lastPt.X := currPt.X;
+          end;
+          SetLength(pts, ptCnt);
+          currSubPath.AddHSeg(firstPt, pts);
+        end;
+      stQBezier, stCSpline:
+        begin
+          while IsNumPending(c, endC, true) and
+            Parse2Num(c, endC, pt2, lastPt) and
+            Parse2Num(c, endC, currPt, lastPt) do
+          begin
+            AddPt(pt2);
+            AddPt(currPt);
+            if isRelative then lastPt := currPt;
+          end;
+          SetLength(pts, ptCnt);
+          currSubPath.AddSeg(currSegType, firstPt, pts);
+        end;
+      stLine, stQSpline:
+        begin
+          while IsNumPending(c, endC, true) and
+            Parse2Num(c, endC, currPt, lastPt) do
+          begin
+            AddPt(currPt);
+            if isRelative then lastPt := currPt;
+          end;
+          SetLength(pts, ptCnt);
+          currSubPath.AddSeg(currSegType, firstPt, pts);
+        end;
+      stVert:
+        begin
+          while IsNumPending(c, endC, true) and
+            Parse1Num(c, endC, currPt.Y, lastPt.Y) do
+          begin
+            AddPt(currPt);
+            if isRelative then lastPt.Y := currPt.Y;
+          end;
+          SetLength(pts, ptCnt);
+          currSubPath.AddVSeg(firstPt, pts);
+        end;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPath.GetCount: integer;
+begin
+  Result := Length(fSubPaths);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPath.GetPath(index: integer): TSvgSubPath;
+begin
+  if (index < 0) or (index >= Count) then
+    raise Exception.Create(rsSvgPathRangeError);
+  Result := fSubPaths[index];
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgPath.Clear;
+var
+  i: integer;
+begin
+  for i := 0 to Count -1 do
+    fSubPaths[i].Free;
+  fSubPaths := nil;
+  fPathScale := 1;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPath.GetBounds: TRectD;
+var
+  i: integer;
+  p: TPathD;
+begin
+  p := nil;
+  for i := 0 to Count -1 do
+      AppendPath(p, fSubPaths[i].GetFlattenedPath);
+  Result := Img32.Vector.GetBoundsD(p);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPath.GetControlBounds: TRectD;
+var
+  i,j: integer;
+  p: TPathD;
+begin
+  p := nil;
+  for i := 0 to Count -1 do
+    with fSubPaths[i] do
+    begin
+      AppendPath(p, GetFirstPt);
+      for j := 0 to High(fSegs) do
+        AppendPath(p, fSegs[j].fCtrlPts);
+    end;
+  Result := GetBoundsD(p);
+
+  //watch out for straight horizontal or vertical lines
+  if not IsEmptyRect(Result) then Exit;
+  p := Grow(p, nil, 1, jsSquare, 0);
+  Result := GetBoundsD(p);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgPath.AddPath: TSvgSubPath;
+var
+  i: integer;
+begin
+  i := Count;
+  Result := TSvgSubPath.Create(self);
+  SetLength(fSubPaths, i + 1);
+  fSubPaths[i] := Result;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgPath.DeleteSubPath(subPath: TSvgSubPath);
+var
+  i, len: integer;
+begin
+  len := Length(fSubPaths);
+  for i := 0 to len -1 do
+    if subPath = fSubPaths[i] then
+    begin
+      fSubPaths[i].Free;
+      if i < len -1 then
+        Move(fSubPaths[i+1], fSubPaths[i],
+          (len - i -1) * SizeOf(Pointer));
+      SetLength(fSubPaths, len -1);
+      break;
+    end;
+end;
+
+//------------------------------------------------------------------------------
+// GetSvgArcInfoRect
+//------------------------------------------------------------------------------
+
+//https://stackoverflow.com/a/12329083
+function GetSvgArcInfoRect(const p1, p2: TPointD; radii: TPointD;
+  phi_rads: double; fA, fS: boolean): TRectD;
+var
+  x1_, y1_, rxry, rxy1_, ryx1_, s_phi, c_phi: double;
+  hd_x, hd_y, hs_x, hs_y, sum_of_sq, lambda, coe: double;
+  cx, cy, cx_, cy_: double;
+begin
+    Result := NullRectD;
+    if (radii.X < 0) then radii.X := -radii.X;
+    if (radii.Y < 0) then radii.Y := -radii.Y;
+    if (radii.X = 0) or (radii.Y = 0) then Exit;
+
+    GetSinCos(phi_rads, s_phi, c_phi);;
+    hd_x := (p1.X - p2.X) / 2.0; // half diff of x
+    hd_y := (p1.Y - p2.Y) / 2.0; // half diff of y
+    hs_x := (p1.X + p2.X) / 2.0; // half sum of x
+    hs_y := (p1.Y + p2.Y) / 2.0; // half sum of y
+
+    // F6.5.1
+    x1_ := c_phi * hd_x + s_phi * hd_y;
+    y1_ := c_phi * hd_y - s_phi * hd_x;
+
+    // F.6.6 Correction of out-of-range radii
+    // Step 3: Ensure radii are large enough
+    lambda := (x1_ * x1_) / (radii.X * radii.X) +
+      (y1_ * y1_) / (radii.Y * radii.Y);
+    if (lambda > 1) then
+    begin
+      radii.X := radii.X * Sqrt(lambda);
+      radii.Y := radii.Y * Sqrt(lambda);
+    end;
+
+    rxry := radii.X * radii.Y;
+    rxy1_ := radii.X * y1_;
+    ryx1_ := radii.Y * x1_;
+    sum_of_sq := rxy1_ * rxy1_ + ryx1_ * ryx1_; // sum of square
+    if (sum_of_sq = 0) then Exit;
+
+    coe := Sqrt(Abs((rxry * rxry - sum_of_sq) / sum_of_sq));
+    if (fA = fS) then coe := -coe;
+
+    // F6.5.2
+    cx_ := coe * rxy1_ / radii.Y;
+    cy_ := -coe * ryx1_ / radii.X;
+
+    // F6.5.3
+    cx := c_phi * cx_ - s_phi * cy_ + hs_x;
+    cy := s_phi * cx_ + c_phi * cy_ + hs_y;
+
+    Result.Left := cx - radii.X;
+    Result.Right := cx + radii.X;
+    Result.Top := cy - radii.Y;
+    Result.Bottom := cy + radii.Y;
+end;
+//------------------------------------------------------------------------------
+
+end.

+ 4723 - 0
components/Image32/source/Img32.SVG.Reader.pas

@@ -0,0 +1,4723 @@
+unit Img32.SVG.Reader;
+
+(*******************************************************************************
+* Author    :  Angus Johnson                                                   *
+* Version   :  4.3                                                             *
+* Date      :  27 September 2022                                               *
+* Website   :  http://www.angusj.com                                           *
+* Copyright :  Angus Johnson 2019-2022                                         *
+*                                                                              *
+* Purpose   :  Read SVG 2.0 files                                              *
+*                                                                              *
+* License   :  Use, modification & distribution is subject to                  *
+*              Boost Software License Ver 1                                    *
+*              http://www.boost.org/LICENSE_1_0.txt                            *
+*******************************************************************************)
+
+interface
+
+{$I Img32.inc}
+
+uses
+  SysUtils, Classes, Types, Math, StrUtils,
+  {$IFDEF XPLAT_GENERICS} Generics.Collections, Generics.Defaults,{$ENDIF}
+  Img32, Img32.SVG.Core, Img32.SVG.Path, Img32.Vector,
+  Img32.Draw, Img32.Text, Img32.Transform;
+
+{$IFDEF ZEROBASEDSTR}
+  {$ZEROBASEDSTRINGS OFF}
+{$ENDIF}
+
+type
+  TSvgElement = class;
+
+  TDrawData = record
+    currentColor  : TColor32;
+    fillColor     : TColor32;
+    fillOpacity   : double;
+    fillRule      : TFillRule;
+    fillEl        : UTF8String;
+    strokeColor   : TColor32;
+    strokeOpacity : double;
+    strokeWidth   : TValue;
+    strokeCap     : TEndStyle;
+    strokeJoin    : TJoinStyle;
+    strokeMitLim  : double;
+    strokeEl      : UTF8String;
+    dashArray     : TArrayOfDouble;
+    dashOffset    : double;
+    fontInfo      : TSVGFontInfo;
+    markerStart   : UTF8String;
+    markerMiddle  : UTF8String;
+    markerEnd     : UTF8String;
+    filterElRef   : UTF8String;
+    maskElRef     : UTF8String;
+    clipElRef     : UTF8String;
+    opacity       : integer;
+    matrix        : TMatrixD;
+    visible       : Boolean;
+    useEl         : TSvgElement; //to check for and prevent <USE> recursion
+    bounds        : TRectD;
+  end;
+
+  TSvgReader = class;
+  TElementClass = class of TSvgElement;
+
+  TSvgElement = class
+  private
+    fParent         : TSvgElement;
+    fParserEl       : TSvgTreeEl;
+    fReader         : TSvgReader;
+{$IFDEF XPLAT_GENERICS}
+    fChilds         : TList<TSvgElement>;
+{$ELSE}
+    fChilds         : TList;
+{$ENDIF}
+    fId             : UTF8String;
+    fDrawData       : TDrawData;    //currently both static and dynamic vars
+    function  FindRefElement(refname: UTF8String): TSvgElement;
+    function GetChildCount: integer;
+    function GetChild(index: integer): TSvgElement;
+    function FindChild(const idName: UTF8String): TSvgElement;
+  protected
+    elRectWH        : TValueRecWH;  //multifunction variable
+    function  IsFirstChild: Boolean;
+    procedure LoadAttributes;
+    procedure LoadAttribute(attrib: PSvgAttrib);
+    function  LoadContent: Boolean; virtual;
+    //GetRelFracLimit: ie when to assume untyped vals are relative vals
+    function  GetRelFracLimit: double; virtual;
+    procedure Draw(image: TImage32; drawDat: TDrawData); virtual;
+    procedure DrawChildren(image: TImage32; drawDat: TDrawData); virtual;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); virtual;
+    destructor  Destroy; override;
+    property Child[index: integer]: TSvgElement read GetChild; default;
+    property ChildCount: integer read GetChildCount;
+    property DrawData: TDrawData read fDrawData write fDrawData;
+    property Id: UTF8String read fId;
+  end;
+
+  TSvgRootElement = class(TSvgElement)
+  protected
+    viewboxWH       : TRectWH;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TSvgReader = class
+  private
+    fSvgParser        : TSvgParser;
+    fBkgndColor       : TColor32;
+    fBackgndImage     : TImage32;
+    fTempImage        : TImage32;
+    fBlurQuality      : integer;
+    fIdList           : TStringList;
+    fClassStyles      : TClassStylesList;
+    fLinGradRenderer  : TLinearGradientRenderer;
+    fRadGradRenderer  : TSvgRadialGradientRenderer;
+    fImgRenderer      : TImageRenderer;
+    fRootElement      : TSvgRootElement;
+    fFontCache        : TFontCache;
+    fUsePropScale     : Boolean;
+    function  LoadInternal: Boolean;
+    function  GetIsEmpty: Boolean;
+    procedure SetBlurQuality(quality: integer);
+  protected
+    userSpaceBounds : TRectD;
+    currentColor    : TColor32;
+    procedure GetBestFontForFontCache(const svgFontInfo: TSVGFontInfo);
+    property  RadGradRenderer: TSvgRadialGradientRenderer read fRadGradRenderer;
+    property  LinGradRenderer: TLinearGradientRenderer read fLinGradRenderer;
+    property  ImageRenderer  : TImageRenderer read fImgRenderer;
+    property  BackgndImage   : TImage32 read fBackgndImage;
+    property  TempImage      : TImage32 read fTempImage;
+  public
+    constructor Create;
+    destructor Destroy; override;
+    procedure Clear;
+    function  GetViewbox(containerWidth, containerHeight: integer): TRectWH;
+    procedure DrawImage(img: TImage32; scaleToImage: Boolean);
+    function  LoadFromStream(stream: TStream): Boolean;
+    function  LoadFromFile(const filename: string): Boolean;
+    function  LoadFromString(const str: string): Boolean;
+
+    //The following two methods are deprecated and intended only for ...
+    //https://github.com/EtheaDev/SVGIconImageList
+    procedure SetOverrideFillColor(color: TColor32); //deprecated;
+    procedure SetOverrideStrokeColor(color: TColor32); //deprecated;
+
+    function  FindElement(const idName: UTF8String): TSvgElement;
+    property  BackgroundColor : TColor32 read fBkgndColor write fBkgndColor;
+    property  BlurQuality     : integer read fBlurQuality write SetBlurQuality;
+    property  IsEmpty         : Boolean read GetIsEmpty;
+    //KeepAspectRatio: this property has also been added for the convenience of
+    //the third-party SVGIconImageList. (IMHO it should always = true)
+    property  KeepAspectRatio: Boolean
+      read fUsePropScale write fUsePropScale;
+    property  RootElement     : TSvgRootElement read fRootElement;
+  end;
+
+implementation
+
+uses
+  Img32.Extra;
+
+type
+  TFourDoubles = array [0..3] of double;
+
+  TDefsElement = class(TSvgElement)
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  //-------------------------------------
+
+  TShapeElement = class(TSvgElement)
+  protected
+    hasPaths    : Boolean;
+    drawPathsO  : TPathsD; //open only
+    drawPathsC  : TPathsD; //closed only
+    drawPathsF  : TPathsD; //both open and closed (for filling)
+    function  GetBounds: TRectD; virtual;
+    function  HasMarkers: Boolean;
+    procedure GetPaths(const drawDat: TDrawData); virtual;
+    //GetSimplePath: required only for markers
+    function  GetSimplePath(const drawDat: TDrawData): TPathsD; virtual;
+    procedure DrawFilled(img: TImage32; drawDat: TDrawData);
+    procedure DrawStroke(img: TImage32; drawDat: TDrawData; isClosed: Boolean);
+    procedure DrawMarkers(img: TImage32; drawDat: TDrawData);
+    procedure Draw(image: TImage32; drawDat: TDrawData); override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TGroupElement = class(TShapeElement)
+  protected
+    procedure Draw(image: TImage32; drawDat: TDrawData); override;
+  end;
+
+  TSwitchElement = class(TShapeElement)
+  protected
+    procedure Draw(image: TImage32; drawDat: TDrawData); override;
+  end;
+
+  TUseElement = class(TShapeElement)
+  private
+    callerUse: TSvgElement;
+    function ValidateNonRecursion(el: TSvgElement): Boolean;
+  protected
+    refEl: UTF8String;
+    procedure GetPaths(const drawDat: TDrawData); override;
+    procedure Draw(img: TImage32; drawDat: TDrawData); override;
+  end;
+
+  TMaskElement = class(TShapeElement)
+  protected
+    maskRec: TRect;
+    procedure GetPaths(const drawDat: TDrawData); override;
+    procedure ApplyMask(img: TImage32; const drawDat: TDrawData);
+  end;
+
+  TSymbolElement = class(TShapeElement)
+  protected
+    viewboxWH: TRectWH;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  //-------------------------------------
+
+  TPathElement = class(TShapeElement)
+  private
+    fSvgPaths   : TSvgPath;
+    procedure Flatten(index: integer; scalePending: double;
+      out path: TPathD; out isClosed: Boolean);
+  protected
+    function  GetBounds: TRectD; override;
+    procedure ParseDAttrib(const value: UTF8String);
+    procedure GetPaths(const drawDat: TDrawData); override;
+    function  GetSimplePath(const drawDat: TDrawData): TPathsD; override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+    destructor Destroy; override;
+  end;
+
+  TPolyElement = class(TShapeElement) //polyline or polygon
+  protected
+    path        : TPathD;
+    function  GetBounds: TRectD; override;
+    procedure ParsePoints(const value: UTF8String);
+    procedure GetPaths(const drawDat: TDrawData); override;
+    function  GetSimplePath(const drawDat: TDrawData): TPathsD; override;
+  end;
+
+  TLineElement = class(TShapeElement)
+  protected
+    path      : TPathD;
+    function  GetBounds: TRectD; override;
+    procedure GetPaths(const drawDat: TDrawData); override;
+    function  GetSimplePath(const drawDat: TDrawData): TPathsD; override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TCircleElement = class(TShapeElement)
+  protected
+    centerPt        : TValuePt;
+    radius          : TValue;
+    function  GetBounds: TRectD; override;
+    procedure GetPaths(const drawDat: TDrawData); override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TEllipseElement = class(TShapeElement)
+  protected
+    centerPt  : TValuePt;
+    radius    : TValuePt;
+    function  GetBounds: TRectD; override;
+    procedure GetPaths(const drawDat: TDrawData); override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TRectElement = class(TShapeElement)
+  protected
+    radius    : TValuePt;
+    function  GetBounds: TRectD; override;
+    procedure GetPaths(const drawDat: TDrawData); override;
+    function  GetSimplePath(const drawDat: TDrawData): TPathsD; override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  //TTextElement: although this is a TShapeElement descendant, it's really
+  //only a container for 'tspan' and 'subtext' elements. (See Draw method.)
+  TTextElement = class(TShapeElement)
+  protected
+    offset    : TValuePt;
+    startX    : double;
+    currentPt : TPointD;
+    function  GetTopTextElement: TTextElement;
+    procedure DoOffsetX(dx: double);
+    procedure ResetTmpPt;
+    procedure GetPaths(const drawDat: TDrawData); override;
+    function  LoadContent: Boolean; override;
+    procedure Draw(img: TImage32; drawDat: TDrawData); override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TTSpanElement = class(TTextElement)
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TSubtextElement = class(TShapeElement)
+  protected
+    text      : UTF8String;
+    procedure GetPaths(const drawDat: TDrawData); override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  //-------------------------------------
+
+  TTextPathElement = class(TSubtextElement)
+  protected
+    pathEl: UTF8String; //name (id) of path element
+    procedure GetPaths(const drawDat: TDrawData); override;
+  end;
+
+  TMarkerElement = class(TShapeElement)
+  private
+    fPoints     : TPathD;
+  protected
+    refPt       : TValuePt;
+    angle       : double;
+    angle2      : double;
+    markerBoxWH : TRectWH;
+    autoStartReverse  : Boolean;
+    procedure SetEndPoint(const pt: TPointD; angle: double);
+    function SetMiddlePoints(const points: TPathD): Boolean;
+    procedure Draw(img: TImage32; drawDat: TDrawData); override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TSvgColorStop = record
+    offset    : double;
+    color     : TColor32;
+  end;
+  TSvgColorStops = array of TSvgColorStop;
+
+  TFillElement = class(TSvgElement)
+  protected
+    refEl : UTF8String;
+    units : Cardinal;
+    function  GetRelFracLimit: double; override;
+  end;
+
+  TPatternElement = class(TFillElement)
+  protected
+    pattBoxWH : TRectWH;
+    function PrepareRenderer(renderer: TImageRenderer;
+      drawDat: TDrawData): Boolean; virtual;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  //nb: gradients with objectBoundingBox should not be applied to
+  //elements without width and height.
+  TGradientElement = class(TFillElement)
+  protected
+    stops         : TSvgColorStops;
+    spreadMethod  : TGradientFillStyle;
+    function LoadContent: Boolean; override;
+    procedure AddStop(color: TColor32; offset: double);
+    procedure AssignTo(other: TSvgElement);  virtual;
+    function PrepareRenderer(renderer: TCustomGradientRenderer;
+      drawDat: TDrawData): Boolean; virtual;
+  end;
+
+  TRadGradElement = class(TGradientElement)
+  protected
+    radius: TValuePt;
+    F, C: TValuePt;
+    procedure AssignTo(other: TSvgElement); override;
+    function PrepareRenderer(renderer: TCustomGradientRenderer;
+      drawDat: TDrawData): Boolean; override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TLinGradElement = class(TGradientElement)
+  protected
+    startPt, endPt: TValuePt;
+    procedure AssignTo(other: TSvgElement); override;
+    function PrepareRenderer(renderer: TCustomGradientRenderer;
+      drawDat: TDrawData): Boolean; override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TGradStopElement = class(TSvgElement)
+  protected
+    offset: double;
+    color: TColor32;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TFilterElement = class(TSvgElement)
+  private
+    fSrcImg       : TImage32;
+    fLastImg      : TImage32;
+    fScale        : double;
+    fFilterBounds : TRect;
+    fObjectBounds : TRect;
+    fImages       : array of TImage32;
+    fNames        : array of UTF8String;
+  protected
+    procedure Clear;
+    function GetRelFracLimit: double; override;
+    function GetAdjustedBounds(const bounds: TRectD): TRectD;
+    function FindNamedImage(const name: UTF8String): TImage32;
+    function AddNamedImage(const name: UTF8String): TImage32;
+    function GetNamedImage(const name: UTF8String): TImage32;
+    procedure Apply(img: TImage32;
+      const filterBounds: TRect; const matrix: TMatrixD);
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+    destructor Destroy; override;
+  end;
+
+  TFeBaseElement = class(TSvgElement)
+  private
+    function GetParentAsFilterEl: TFilterElement;
+  protected
+    in1: UTF8String;
+    in2: UTF8String;
+    res: UTF8String;
+    srcImg, dstImg: TImage32;
+    srcRec, dstRec: TRect;
+    function GetSrcAndDst: Boolean;
+    function GetBounds(img: TImage32): TRect;
+    procedure Apply; virtual; abstract;
+    property ParentFilterEl: TFilterElement read GetParentAsFilterEl;
+  end;
+
+  TFeBlendElement  = class(TFeBaseElement)
+  protected
+    procedure Apply; override;
+  end;
+
+  TCompositeOp = (coOver, coIn, coOut, coAtop, coXOR, coArithmetic);
+
+  TFeCompositeElement  = class(TFeBaseElement)
+  protected
+    fourKs: TFourDoubles; //arithmetic constants
+    compositeOp: TCompositeOp;
+    procedure Apply; override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TFeColorMatrixElement  = class(TFeBaseElement)
+  protected
+    values: TArrayOfDouble;
+    procedure Apply; override;
+  end;
+
+  TFeDefuseLightElement = class(TFeBaseElement)
+  protected
+    color         : TColor32;
+    surfaceScale  : double;
+    diffuseConst  : double;
+    kernelSize    : integer;
+    procedure Apply; override;
+  end;
+
+  TFeDropShadowElement = class(TFeBaseElement)
+  protected
+    stdDev      : double;
+    offset        : TValuePt;
+    floodColor  : TColor32;
+    procedure Apply; override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TFeFloodElement  = class(TFeBaseElement)
+  protected
+    floodColor  : TColor32;
+    procedure Apply; override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TFeGaussElement  = class(TFeBaseElement)
+  protected
+    stdDev: double;
+    procedure Apply; override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+
+  TFeMergeElement  = class(TFeBaseElement)
+  protected
+    procedure Apply; override;
+  end;
+
+  TFeMergeNodeElement  = class(TFeBaseElement)
+  protected
+    procedure Apply; override;
+  end;
+
+  TFeOffsetElement = class(TFeBaseElement)
+  protected
+    offset        : TValuePt;
+    procedure Apply; override;
+  end;
+
+  TFePointLightElement = class(TFeBaseElement)
+  protected
+    z             : double;
+  end;
+
+  TFeSpecLightElement = class(TFeBaseElement)
+  protected
+    exponent      : double;
+    color         : TColor32;
+    procedure Apply; override;
+  end;
+
+  TClipPathElement = class(TShapeElement)
+  protected
+    units: Cardinal;
+    procedure GetPaths(const drawDat: TDrawData); override;
+  public
+    constructor Create(parent: TSvgElement; svgEl: TSvgTreeEl); override;
+  end;
+  //-------------------------------------
+
+
+const
+  buffSize    = 32;
+  clAlphaSet  = $00010101;
+  SourceImage   : UTF8String = 'SourceGraphic';
+  //SourceAlpha   : UTF8String = 'SourceAlpha';
+  tmpFilterImg  : UTF8String = 'tmp';
+
+  //https://www.w3.org/TR/css-fonts-3/#font-family-prop
+  emptyDrawInfo: TDrawData =
+    (currentColor: clInvalid;
+    fillColor: clInvalid; fillOpacity: InvalidD;
+    fillRule: frNonZero; fillEl: '';
+    strokeColor: clInvalid; strokeOpacity: InvalidD;
+    strokeWidth: (rawVal: InvalidD; unitType: utNumber);
+    strokeCap: esPolygon; strokeJoin: jsAuto; strokeMitLim: 0.0; strokeEl : '';
+    dashArray: nil; dashOffset: 0;
+    fontInfo: (family: ttfUnknown; size: 0; spacing: 0.0;
+    textLength: 0; italic: sfsUndefined; weight: -1; align: staUndefined;
+    decoration: fdUndefined; baseShift: (rawVal: InvalidD; unitType: utNumber));
+    markerStart: ''; markerMiddle: ''; markerEnd: '';
+    filterElRef: ''; maskElRef: ''; clipElRef: ''; opacity: MaxInt;
+    matrix: ((1, 0, 0),(0, 1, 0),(0, 0, 1)); visible: true;
+    useEl: nil; bounds: (Left:0; Top:0; Right:0; Bottom:0));
+
+var
+  //defaultFontHeight: this size will be used to retrieve all glyph contours
+  //(and later scaled as necessary). This relatively large default ensures
+  //that contours will have adequate detail.
+  defaultFontHeight: double = 20.0;
+
+//------------------------------------------------------------------------------
+// Miscellaneous functions ...
+//------------------------------------------------------------------------------
+
+function HashToElementClass(hash: Cardinal): TElementClass;
+begin
+  case hash of
+    hClippath       : Result := TClipPathElement;
+    hCircle         : Result := TCircleElement;
+    hDefs           : Result := TDefsElement;
+    hEllipse        : Result := TEllipseElement;
+    hFilter         : Result := TFilterElement;
+    hfeBlend        : Result := TFeBlendElement;
+    hfeColorMatrix  : Result := TFeColorMatrixElement;
+    hfeComposite    : Result := TFeCompositeElement;
+    hfeDefuseLighting : Result := TFeDefuseLightElement;
+    hfeDropShadow   : Result := TFeDropShadowElement;
+    hfeFlood        : Result := TFeFloodElement;
+    hFeGaussianBlur : Result := TFeGaussElement;
+    hfeMerge        : Result := TFeMergeElement;
+    hfeMergeNode    : Result := TFeMergeNodeElement;
+    hfeOffset       : Result := TFeOffsetElement;
+    hfePointLight   : Result := TFePointLightElement;
+    hfeSpecularLighting : Result := TFeSpecLightElement;
+    hG              : Result := TGroupElement;
+    hLine           : Result := TLineElement;
+    hLineargradient : Result := TLinGradElement;
+    hMarker         : Result := TMarkerElement;
+    hMask           : Result := TMaskElement;
+    hPath           : Result := TPathElement;
+    hPattern        : Result := TPatternElement;
+    hPolyline       : Result := TPolyElement;
+    hPolygon        : Result := TPolyElement;
+    hRadialgradient : Result := TRadGradElement;
+    hRect           : Result := TRectElement;
+    hStop           : Result := TGradStopElement;
+    hSvg            : Result := TSvgRootElement;
+    hSwitch         : Result := TSwitchElement;
+    hSymbol         : Result := TSymbolElement;
+    hText           : Result := TTextElement;
+    hTextPath       : Result := TTextPathElement;
+    hTSpan          : Result := TTSpanElement;
+    hUse            : Result := TUseElement;
+    else              Result := TSvgElement; //use generic class
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure UpdateDrawInfo(var drawDat: TDrawData; thisElement: TSvgElement);
+begin
+  with thisElement.fDrawData do
+  begin
+    if currentColor <> clInvalid then
+      thisElement.fReader.currentColor := currentColor;
+    drawDat.fillRule := fillRule;
+    if (fillColor = clCurrent) then
+      drawDat.fillColor := thisElement.fReader.currentColor
+    else if (fillColor <> clInvalid) then
+      drawDat.fillColor := fillColor;
+    if fillOpacity <> InvalidD then
+      drawDat.fillOpacity := fillOpacity;
+    if (fillEl <> '') then
+      drawDat.fillEl := fillEl;
+    if (strokeColor = clCurrent) then
+      drawDat.strokeColor := thisElement.fReader.currentColor
+    else if strokeColor <> clInvalid then
+      drawDat.strokeColor := strokeColor;
+    if strokeOpacity <> InvalidD then
+      drawDat.strokeOpacity := strokeOpacity;
+    if strokeWidth.IsValid then
+      drawDat.strokeWidth := strokeWidth;
+    if strokeCap = esPolygon then
+      drawDat.strokeCap := strokeCap;
+    if strokeJoin = jsAuto then
+      drawDat.strokeJoin := strokeJoin;
+    if strokeMitLim > 0 then
+      drawDat.strokeMitLim := strokeMitLim;
+    if Assigned(dashArray) then
+      drawDat.dashArray := Copy(dashArray, 0, Length(dashArray));
+    if dashOffset <> 0 then
+      drawDat.dashOffset := dashOffset;
+    if (strokeEl <> '') then
+      drawDat.strokeEl := strokeEl;
+    if opacity < MaxInt then
+      drawDat.opacity := opacity;
+
+    if (clipElRef <> '') then
+      drawDat.clipElRef := clipElRef;
+    if (maskElRef <> '') then
+      drawDat.maskElRef := maskElRef;
+    if (filterElRef <> '') then
+      drawDat.filterElRef := filterElRef;
+
+    if fontInfo.family <> ttfUnknown then
+      drawDat.fontInfo.family := fontInfo.family;
+    if fontInfo.size > 0 then
+      drawDat.fontInfo.size := fontInfo.size;
+    if fontInfo.spacing <> 0 then
+      drawDat.fontInfo.spacing := fontInfo.spacing;
+    if fontInfo.textLength > 0 then
+      drawDat.fontInfo.textLength := fontInfo.textLength;
+
+    if (fontInfo.italic <> sfsUndefined) then
+      drawDat.fontInfo.italic := fontInfo.italic;
+    if (fontInfo.weight <> -1) then
+      drawDat.fontInfo.weight := fontInfo.weight;
+
+    if fontInfo.align <> staUndefined then
+      drawDat.fontInfo.align := fontInfo.align;
+
+    if (thisElement is TTextElement) or
+      (fontInfo.decoration <> fdUndefined) then
+      drawDat.fontInfo.decoration := fontInfo.decoration;
+    if fontInfo.baseShift.IsValid then
+      drawDat.fontInfo.baseShift := fontInfo.baseShift;
+
+    if not IsIdentityMatrix(matrix) then
+      drawDat.matrix := MatrixMultiply(drawDat.matrix, matrix);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function IsFilled(const drawDat: TDrawData): Boolean;
+begin
+  with drawDat do
+    Result := (fillOpacity <> 0) and
+      ((fillColor <> clNone32) or (fillEl <> ''));
+end;
+//------------------------------------------------------------------------------
+
+function IsStroked(const drawDat: TDrawData): Boolean;
+begin
+  with drawDat do
+    if (strokeOpacity = 0) then
+      Result := false
+    else if (strokeEl <> '') then
+      Result := ((strokeWidth.rawVal = InvalidD) or (strokeWidth.rawVal > 0))
+    else if (strokeColor = clNone32) or
+        ((strokeColor = clInvalid) and (strokeWidth.rawVal = InvalidD)) then
+      Result := false
+    else
+      Result := ((strokeWidth.rawVal = InvalidD) or (strokeWidth.rawVal > 0));
+end;
+//------------------------------------------------------------------------------
+
+function MergeColorAndOpacity(color: TColor32; opacity: double): TColor32;
+begin
+  if (opacity < 0) or (opacity >= 1.0) then Result := color
+  else if opacity = 0 then Result := clNone32
+  else Result := (color and $FFFFFF) + Round(opacity * 255) shl 24;
+end;
+//------------------------------------------------------------------------------
+
+function UTF8StringToFloat(const ansiValue: UTF8String;
+  out value: double): Boolean;
+var
+  c: PUTF8Char;
+begin
+  c := PUTF8Char(ansiValue);
+  Result := ParseNextNum(c, c + Length(ansiValue), false, value);
+end;
+//------------------------------------------------------------------------------
+
+function UTF8StringToFloatEx(const ansiValue: UTF8String;
+  var value: double; out measureUnit: TUnitType): Boolean;
+var
+  c: PUTF8Char;
+begin
+  c := PUTF8Char(ansiValue);
+  Result := ParseNextNumEx(c, c + Length(ansiValue), false, value, measureUnit);
+end;
+//------------------------------------------------------------------------------
+
+procedure UTF8StringToOpacity(const ansiValue: UTF8String; var color: TColor32);
+var
+  opacity: double;
+begin
+  if color = clNone32 then
+  begin
+    color := clAlphaSet;
+    Exit;
+  end;
+
+  if color = clInvalid then color := clNone32;
+  if not UTF8StringToFloat(ansiValue, opacity) then Exit;
+  with TARGB(color) do
+    if (opacity <= 0) then
+    begin
+      if Color = clNone32 then Color := clAlphaSet
+      else A := 0;
+    end
+    else if (opacity >= 1) then A := 255
+    else A := Round(255 * opacity);
+end;
+//------------------------------------------------------------------------------
+
+function MatrixApply(const paths: TPathsD; const matrix: TMatrixD): TPathsD; overload;
+var
+  i,j,len,len2: integer;
+  pp,rr: PPointD;
+begin
+  if not Assigned(paths) then
+    Result := nil
+  else if IsIdentityMatrix(matrix) then
+    Result := CopyPaths(paths)
+  else
+  begin
+    len := Length(paths);
+    SetLength(Result, len);
+    for i := 0 to len -1 do
+    begin
+      len2 := Length(paths[i]);
+      SetLength(Result[i], len2);
+      if len2 = 0 then Continue;
+      pp := @paths[i][0];
+      rr := @Result[i][0];
+      for j := 0 to High(paths[i]) do
+      begin
+        rr.X := pp.X * matrix[0, 0] + pp.Y * matrix[1, 0] + matrix[2, 0];
+        rr.Y := pp.X * matrix[0, 1] + pp.Y * matrix[1, 1] + matrix[2, 1];
+        inc(pp); inc(rr);
+      end;
+    end;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TDefsElement
+//------------------------------------------------------------------------------
+
+constructor TDefsElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  fDrawData.visible := false;
+end;
+
+//------------------------------------------------------------------------------
+// TGroupElement
+//------------------------------------------------------------------------------
+
+procedure TGroupElement.Draw(image: TImage32; drawDat: TDrawData);
+var
+  clipEl    : TSvgElement;
+  maskEl    : TSvgElement;
+  tmpImg    : TImage32;
+  clipPaths : TPathsD;
+  clipRec   : TRect;
+begin
+  if fChilds.Count = 0 then Exit;
+
+  UpdateDrawInfo(drawDat, self);
+
+  maskEl := FindRefElement(drawDat.maskElRef);
+  clipEl := FindRefElement(drawDat.clipElRef);
+  if Assigned(clipEl) then
+  begin
+    with TClipPathElement(clipEl) do
+    begin
+      drawDat.clipElRef := '';
+      GetPaths(drawDat);
+      clipPaths := CopyPaths(drawPathsF);
+
+      MatrixApply(drawDat.matrix, clipPaths);
+      clipRec := Img32.Vector.GetBounds(clipPaths);
+    end;
+    if IsEmptyRect(clipRec) then Exit;
+
+    //nb: it's not safe to use fReader.TempImage when calling DrawChildren
+    tmpImg := TImage32.Create(Image.Width, Image.Height);
+    try
+      DrawChildren(tmpImg, drawDat);
+      with TClipPathElement(clipEl) do
+        EraseOutsidePaths(tmpImg, clipPaths, fDrawData.fillRule, clipRec);
+      image.CopyBlend(tmpImg, clipRec, clipRec, BlendToAlpha);
+    finally
+      tmpImg.Free;
+    end;
+
+  end
+  else if Assigned(maskEl) then
+  begin
+    drawDat.maskElRef := '';
+    with TMaskElement(maskEl) do
+    begin
+      GetPaths(drawDat);
+      clipRec := maskRec;
+    end;
+    tmpImg := TImage32.Create(image.Width, image.Height);
+    try
+      DrawChildren(tmpImg, drawDat);
+      TMaskElement(maskEl).ApplyMask(tmpImg, drawDat);
+      image.CopyBlend(tmpImg, clipRec, clipRec, BlendToAlpha);
+    finally
+      tmpImg.Free;
+    end;
+  end else
+    DrawChildren(image, drawDat);
+end;
+
+//------------------------------------------------------------------------------
+// TSwitchElement
+//------------------------------------------------------------------------------
+
+procedure TSwitchElement.Draw(image: TImage32; drawDat: TDrawData);
+var
+  i: integer;
+begin
+  for i := 0 to fChilds.Count -1 do
+    if TSvgElement(fChilds[i]) is TShapeElement then
+      with TShapeElement(fChilds[i]) do
+        if fDrawData.visible then
+        begin
+          Draw(image, drawDat);
+          break; //break after the first successful drawing
+        end;
+end;
+
+//------------------------------------------------------------------------------
+// TUseElement
+//------------------------------------------------------------------------------
+
+procedure TUseElement.GetPaths(const drawDat: TDrawData);
+var
+  el: TSvgElement;
+  dx, dy: double;
+begin
+  if Assigned(drawPathsF) or (refEl = '') then Exit;
+
+  el := FindRefElement(refEl);
+  if not Assigned(el) or not (el is TShapeElement) then Exit;
+  with TShapeElement(el) do
+  begin
+    GetPaths(drawDat);
+    self.drawPathsC := CopyPaths(drawPathsC);
+    self.drawPathsO := CopyPaths(drawPathsO);
+  end;
+
+  if elRectWH.left.IsValid then
+    dx := elRectWH.left.rawVal else
+    dx := 0;
+  if elRectWH.top.IsValid  then
+    dy := elRectWH.top.rawVal else
+    dy := 0;
+
+  if (dx <> 0) or (dy <> 0) then
+  begin
+    drawPathsC := OffsetPath(drawPathsC, dx, dy);
+    drawPathsO := OffsetPath(drawPathsO, dx, dy);
+  end;
+
+  drawPathsF := CopyPaths(drawPathsC);
+  AppendPath(drawPathsF, drawPathsO);
+end;
+//------------------------------------------------------------------------------
+
+function TUseElement.ValidateNonRecursion(el: TSvgElement): Boolean;
+begin
+  Result := false;
+  while assigned(el) do
+  begin
+    if (el = Self) then Exit;
+    if not (el is TUseElement) then break; //shouldn't happen
+    el := TUseElement(el).callerUse;
+  end;
+  Result := true;
+end;
+//------------------------------------------------------------------------------
+
+procedure TUseElement.Draw(img: TImage32; drawDat: TDrawData);
+var
+  el: TSvgElement;
+  s, dx, dy: double;
+  scale, scale2: TSizeD;
+  mat: TMatrixD;
+begin
+
+  //make sure there's not recursion, either directly or indirectly
+  if not ValidateNonRecursion(drawDat.useEl) then Exit;
+
+  callerUse := drawDat.useEl;
+  drawDat.useEl := self;
+
+  el := FindRefElement(refEl);
+  if not Assigned(el) then Exit;
+
+  UpdateDrawInfo(drawDat, self); //nb: <use> attribs override el's.
+  scale := ExtractScaleFromMatrix(drawDat.matrix);
+
+  if elRectWH.left.IsValid then dx := elRectWH.left.rawVal else dx := 0;
+  if elRectWH.top.IsValid  then dy := elRectWH.top.rawVal  else dy := 0;
+
+  if (dx <> 0) or (dy <> 0) then
+  begin
+    mat := IdentityMatrix;
+    MatrixTranslate(mat, dx, dy);
+    drawDat.matrix := MatrixMultiply(drawDat.matrix, mat);
+  end;
+
+  if el is TSymbolElement then
+  begin
+    with TSymbolElement(el) do
+    begin
+      if not viewboxWH.IsEmpty then
+      begin
+        //scale the symbol according to its width and height attributes
+        if elRectWH.width.IsValid and elRectWH.height.IsValid then
+        begin
+          scale2.cx := elRectWH.width.rawVal / viewboxWH.Width;
+          scale2.cy := elRectWH.height.rawVal / viewboxWH.Height;
+          if scale2.cy < scale2.cx then s := scale2.cy else s := scale2.cx;
+          //the following 3 lines will scale without translating
+          mat := IdentityMatrix;
+          MatrixScale(mat, s, s);
+          drawDat.matrix := MatrixMultiply(drawDat.matrix, mat);
+          drawDat.bounds := RectD(0,0,viewboxWH.Width, viewboxWH.Height);
+        end;
+
+        if self.elRectWH.width.IsValid and
+          self.elRectWH.height.IsValid then
+        begin
+          with viewboxWH do
+          begin
+            dx := -Left/Width * self.elRectWH.width.rawVal;
+            dy := -Top/Height * self.elRectWH.height.rawVal;
+
+            //scale <symbol> proportionally to fill the <use> element
+            scale2.cx := self.elRectWH.width.rawVal / Width;
+            scale2.cy := self.elRectWH.height.rawVal / Height;
+            if scale2.cy < scale2.cx then s := scale2.cy else s := scale2.cx;
+          end;
+
+          mat := IdentityMatrix;
+          MatrixScale(mat, s, s);
+          MatrixTranslate(mat, dx, dy);
+          drawDat.matrix := MatrixMultiply(drawDat.matrix, mat);
+
+          //now center after scaling
+          if scale2.cx > scale2.cy then
+          begin
+            if scale2.cx > 1 then
+            begin
+              s := (self.elRectWH.width.rawVal - viewboxWH.Width) * 0.5;
+              MatrixTranslate(drawDat.matrix, s * scale.cx, 0);
+            end;
+          end else if scale2.cy > 1 then
+          begin
+            s := (self.elRectWH.height.rawVal - viewboxWH.Height) * 0.5;
+            MatrixTranslate(drawDat.matrix, 0, s * scale.cy);
+          end;
+
+        end;
+      end;
+      DrawChildren(img, drawDat);
+    end;
+  end
+  else if el is TShapeElement then
+    el.Draw(img, drawDat);
+end;
+
+//------------------------------------------------------------------------------
+// TMaskElement
+//------------------------------------------------------------------------------
+
+procedure TMaskElement.GetPaths(const drawDat: TDrawData);
+var
+  i   : integer;
+  el  : TShapeElement;
+begin
+  maskRec := NullRect;
+  for i := 0 to fChilds.Count -1 do
+    if TSvgElement(fChilds[i]) is TShapeElement then
+    begin
+      el := TShapeElement(fChilds[i]);
+      el.GetPaths(drawDat);
+      maskRec :=
+        Img32.Vector.UnionRect(maskRec, Img32.Vector.GetBounds(el.drawPathsF));
+    end;
+  MatrixApply(drawDat.matrix, maskRec);
+end;
+//------------------------------------------------------------------------------
+
+procedure TMaskElement.ApplyMask(img: TImage32; const drawDat: TDrawData);
+var
+  tmpImg: TImage32;
+begin
+  tmpImg := TImage32.Create(img.Width, img.Height);
+  try
+    DrawChildren(tmpImg, drawDat);
+    img.CopyBlend(tmpImg, maskRec, maskRec, BlendBlueChannel);
+  finally
+    tmpImg.Free;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TSymbolElement
+//------------------------------------------------------------------------------
+
+constructor TSymbolElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  fDrawData.visible := false;
+end;
+
+//------------------------------------------------------------------------------
+// TGradElement
+//------------------------------------------------------------------------------
+
+function TGradientElement.LoadContent: Boolean;
+var
+  i: integer;
+begin
+  Result := inherited LoadContent;
+  for i := 0 to fChilds.Count -1 do
+    if TSvgElement(fChilds[i]) is TGradStopElement then
+      with TGradStopElement(fChilds[i]) do
+        AddStop(color, offset);
+end;
+//------------------------------------------------------------------------------
+
+procedure TGradientElement.AddStop(color: TColor32; offset: double);
+var
+  len: integer;
+begin
+  //if a stop is less than previous stops, it is set equal to the largest stop.
+  //If two stops are equal the last stop controls the color from that point.
+  len := Length(stops);
+  if (len > 0) and (stops[len-1].offset > offset) then
+    offset := stops[len-1].offset;
+  setLength(stops, len+1);
+  stops[len].offset := Min(1,Max(0, offset));
+  stops[len].color := color;
+end;
+//------------------------------------------------------------------------------
+
+procedure TGradientElement.AssignTo(other: TSvgElement);
+var
+  i, len: integer;
+begin
+  if not Assigned(other) or not (other is TGradientElement) then Exit;
+  inherited;
+
+  with TGradientElement(other) do
+  begin
+    if units = 0 then
+      units := Self.units;
+
+    if Length(stops) = 0 then
+    begin
+      len := Length(self.stops);
+      SetLength(stops, len);
+      for i := 0 to len -1 do
+        stops[i] := Self.stops[i];
+    end;
+
+    if IsIdentityMatrix(fDrawData.matrix) then
+      fDrawData.matrix := self.fDrawData.matrix;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TGradientElement.PrepareRenderer(
+  renderer: TCustomGradientRenderer; drawDat: TDrawData): Boolean;
+var
+  el: TSvgElement;
+begin
+  if (refEl <> '') then
+  begin
+    el := FindRefElement(refEl);
+    if Assigned(el) and (el is TGradientElement) then
+      TGradientElement(el).AssignTo(self);
+  end;
+  Result := Length(stops) > 0;
+end;
+
+//------------------------------------------------------------------------------
+// TRadGradElement
+//------------------------------------------------------------------------------
+
+constructor TRadGradElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  radius.Init;
+  F.Init;
+  C.Init;
+end;
+//------------------------------------------------------------------------------
+
+procedure TRadGradElement.AssignTo(other: TSvgElement);
+begin
+  if not Assigned(other) or not (other is TGradientElement) then Exit;
+  inherited;
+  if other is TRadGradElement then
+    with TRadGradElement(other) do
+    begin
+      if not radius.IsValid then radius := self.radius;
+      if not C.IsValid then C := self.C;
+      if not F.IsValid then F := self.F;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+function TRadGradElement.PrepareRenderer(renderer: TCustomGradientRenderer;
+  drawDat: TDrawData): Boolean;
+var
+  i, hiStops: integer;
+  cp, fp, r: TPointD;
+  scale, scale2: TSizeD;
+  rec2, rec3: TRectD;
+begin
+  inherited PrepareRenderer(renderer, drawDat);
+  hiStops := High(stops);
+  Result := hiStops >= 0;
+  if not Result then Exit;
+
+  if units = hUserSpaceOnUse then
+    rec2 := fReader.userSpaceBounds else
+    rec2 := drawDat.bounds;
+
+  if radius.IsValid then
+  begin
+    if radius.X.HasFontUnits then
+      r := radius.GetPoint(drawDat.fontInfo.size, GetRelFracLimit) else
+      r := radius.GetPoint(rec2, GetRelFracLimit);
+  end else
+  begin
+    r.X := rec2.Width * 0.5;
+    r.Y := rec2.Height * 0.5;
+  end;
+  scale := ExtractScaleFromMatrix(drawDat.matrix);
+  scale2 := ExtractScaleFromMatrix(fDrawData.matrix);
+  r := ScalePoint(r, scale.cx * scale2.cx, scale.cy * scale2.cy);
+
+  if C.IsValid then
+  begin
+    if C.X.HasFontUnits then
+      cp := C.GetPoint(drawDat.fontInfo.size, GetRelFracLimit) else
+      cp := C.GetPoint(rec2, GetRelFracLimit);
+    cp := OffsetPoint(cp, rec2.Left, rec2.Top);
+  end else
+    cp := rec2.MidPoint;
+  MatrixApply(fDrawData.matrix, cp);
+  MatrixApply(drawDat.matrix, cp);
+
+  rec3 := RectD(cp.X-r.X, cp.Y-r.Y, cp.X+r.X, cp.Y+r.Y);
+
+  if F.IsValid then
+  begin
+    if F.X.HasFontUnits then
+      fp := F.GetPoint(drawDat.fontInfo.size, GetRelFracLimit) else
+      fp := F.GetPoint(rec2, GetRelFracLimit);
+    fp := OffsetPoint(fp, rec2.Left, rec2.Top);
+    MatrixApply(fDrawData.matrix, fp);
+    MatrixApply(drawDat.matrix, fp);
+  end else
+    fp := MidPoint(rec3);
+
+  with renderer as TSvgRadialGradientRenderer do
+  begin
+    SetParameters(Rect(rec3), Point(fp),
+      stops[0].color, stops[hiStops].color, spreadMethod);
+    for i := 1 to hiStops -1 do
+      with stops[i] do
+        renderer.InsertColorStop(offset, color);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TLinGradElement
+//------------------------------------------------------------------------------
+
+constructor TLinGradElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  startPt.Init;
+  endPt.Init;
+end;
+//------------------------------------------------------------------------------
+
+procedure TLinGradElement.AssignTo(other: TSvgElement);
+begin
+  if not Assigned(other) or not (other is TGradientElement) then Exit;
+  inherited;
+  if other is TLinGradElement then
+    with TLinGradElement(other) do
+    begin
+      if not startPt.IsValid then startPt := self.startPt;
+      if not endPt.IsValid then endPt := self.endPt;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+function TLinGradElement.PrepareRenderer(
+  renderer: TCustomGradientRenderer; drawDat: TDrawData): Boolean;
+var
+  pt1, pt2: TPointD;
+  i, hiStops: integer;
+  rec2: TRectD;
+begin
+  inherited PrepareRenderer(renderer, drawDat);
+  hiStops := High(stops);
+  Result := (hiStops >= 0);
+  if not Result then Exit;
+
+  //w3c-coords-units-01-b.svg
+
+  //if gradientUnits=objectBoundingBox (default) then all values must be
+  //percentages. Also... when the object's bounding box is not square, the
+  //gradient may render non-perpendicular relative to the gradient vector
+  //unless the gradient vector is vertical or horizontal.
+  //https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/gradientUnits
+
+  if units = hUserSpaceOnUse then
+    rec2 := fReader.userSpaceBounds else
+    rec2 := drawDat.bounds;
+
+  with TLinearGradientRenderer(renderer) do
+  begin
+    if startPt.X.HasFontUnits then
+      pt1 := startPt.GetPoint(drawDat.fontInfo.size, GetRelFracLimit) else
+      pt1 := startPt.GetPoint(rec2, GetRelFracLimit);
+
+    if (startPt.X.unitType <> utPixel) or
+      (units <> hUserSpaceOnUse) then
+        pt1.X := pt1.X + rec2.Left;
+
+    if (startPt.Y.unitType <> utPixel) or
+      (units <> hUserSpaceOnUse) then
+        pt1.Y := pt1.Y + rec2.Top;
+
+    MatrixApply(fDrawData.matrix, pt1);
+    MatrixApply(drawDat.matrix, pt1);
+
+
+    if not endPt.X.IsValid then
+      pt2.X := rec2.Width else
+      pt2.X := endPt.X.GetValue(rec2.Width, GetRelFracLimit);
+    pt2.Y := endPt.Y.GetValue(rec2.Height, GetRelFracLimit);
+    pt2 := OffsetPoint(pt2, rec2.Left, rec2.Top);
+
+    MatrixApply(fDrawData.matrix, pt2);
+    MatrixApply(drawDat.matrix, pt2);
+
+    if (units <> hUserSpaceOnUse) and
+      ((pt2.X <> pt1.X) or (pt2.Y <> pt1.Y)) then
+    begin
+      //skew the gradient
+    end;
+
+    SetParameters(pt1, pt2, stops[0].color,
+      stops[hiStops].color, spreadMethod);
+    for i := 1 to hiStops -1 do
+      with stops[i] do
+        renderer.InsertColorStop(offset, color);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TGradStopElement
+//------------------------------------------------------------------------------
+
+constructor TGradStopElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  color := clBlack32;
+end;
+
+//------------------------------------------------------------------------------
+// TFilterElement
+//------------------------------------------------------------------------------
+
+constructor TFilterElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  fDrawData.visible := false;
+  elRectWH.Init;
+end;
+//------------------------------------------------------------------------------
+
+destructor TFilterElement.Destroy;
+begin
+  Clear;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFilterElement.Clear;
+var
+  i: integer;
+begin
+  for i := 0 to High(fImages) do
+    fImages[i].Free;
+  fImages := nil;
+  fNames := nil;
+  fLastImg := nil;
+end;
+//------------------------------------------------------------------------------
+
+function TFilterElement.GetRelFracLimit: double;
+begin
+  //always assume fractional values below 2.5 are relative
+  Result := 2.5;
+end;
+//------------------------------------------------------------------------------
+
+function TFilterElement.GetAdjustedBounds(const bounds: TRectD): TRectD;
+var
+  recWH: TRectWH;
+  delta: TSizeD;
+  d: double;
+  pt: TPointD;
+  i: integer;
+  hasOffset: Boolean;
+begin
+  fObjectBounds := Rect(bounds);
+  if elRectWH.IsValid then
+  begin
+    recWH := elRectWH.GetRectWH(bounds, GetRelFracLimit);
+    Result.Left := bounds.Left + recWH.Left;
+    Result.Top := bounds.Top + recWH.Top;
+    Result.Right := Result.Left + recWH.Width;
+    Result.Bottom := Result.Top + recWH.Height;
+  end else
+  begin
+    Result := bounds;
+
+    //when the filter's width and height are undefined then limit the filter
+    //margin to 20% of the bounds when just blurring, not also offsetting.
+    hasOffset := false;
+
+    delta.cx := 0; delta.cy := 0;
+    for i := 0 to ChildCount -1 do
+    begin
+      if Child[i] is TFeGaussElement then
+      begin
+        d := TFeGaussElement(Child[i]).stdDev * 3 * fScale;
+        delta.cx := delta.cx + d;
+        delta.cy := delta.cy + d;
+      end
+      else if Child[i] is TFeDropShadowElement then
+        with TFeDropShadowElement(Child[i]) do
+        begin
+          d := stdDev * 0.75 * fScale;
+          pt := offset.GetPoint(bounds, 1);
+          delta.cx := delta.cx + d + Abs(pt.X) * fScale;
+          delta.cy := delta.cy + d + Abs(pt.Y) * fScale;
+          hasOffset := true;
+        end
+      else if Child[i] is TFeOffsetElement then
+        with TFeOffsetElement(Child[i]) do
+        begin
+          pt := offset.GetPoint(bounds, 1);
+          delta.cx := delta.cx + Abs(pt.X) * fScale;
+          delta.cy := delta.cy + Abs(pt.Y) * fScale;
+          hasOffset := true;
+        end;
+    end;
+
+    //limit the filter margin to 20% if only blurring
+    if not hasOffset then
+      with delta, bounds do
+      begin
+        if cx > Width * 0.2 then cx := Width * 0.2;
+        if cy > Height * 0.2 then cy := Height * 0.2;
+      end;
+    Img32.Vector.InflateRect(Result, delta.cx, delta.cy);
+
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFilterElement.FindNamedImage(const name: UTF8String): TImage32;
+var
+  i, len: integer;
+begin
+  Result := nil;
+  len := Length(fNames);
+  for i := 0 to len -1 do
+    if name = fNames[i] then
+    begin
+      Result := fImages[i];
+      Break;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+function TFilterElement.AddNamedImage(const name: UTF8String): TImage32;
+var
+  len, w, h: integer;
+begin
+  len := Length(fNames);
+  SetLength(fNames, len+1);
+  SetLength(fImages, len+1);
+  RectWidthHeight(fFilterBounds, w, h);
+  Result := TImage32.Create(w, h);
+  fImages[len] := Result;
+  fNames[len] := name;
+end;
+//------------------------------------------------------------------------------
+
+function TFilterElement.GetNamedImage(const name: UTF8String): TImage32;
+var
+  i, len: integer;
+  hash: Cardinal;
+begin
+  hash := GetHash(name);
+  case hash of
+    hBackgroundImage:
+      begin
+        Result := FindNamedImage(name);
+        if not Assigned(Result) then
+          Result := AddNamedImage(name);
+        Result.Copy(fReader.BackgndImage, fFilterBounds, Result.Bounds);
+        Exit;
+      end;
+    hBackgroundAlpha:
+      begin
+        Result := FindNamedImage(name);
+        if not Assigned(Result) then
+          Result := AddNamedImage(name);
+        Result.Copy(fReader.BackgndImage, fFilterBounds, Result.Bounds);
+        Result.SetRGB(clNone32, Result.Bounds);
+        Exit;
+      end;
+    hSourceGraphic:
+      begin
+        Result := FindNamedImage(name);
+        if not Assigned(Result) then
+          Result := AddNamedImage(name);
+        Result.Copy(fSrcImg, fFilterBounds, Result.Bounds);
+        Exit;
+      end;
+    hSourceAlpha:
+      begin
+        Result := FindNamedImage(name);
+        if not Assigned(Result) then
+        begin
+          Result := AddNamedImage(name);
+          Result.Copy(fSrcImg, fFilterBounds, Result.Bounds);
+          Result.SetRGB(clNone32, Result.Bounds);
+        end;
+        Exit;
+      end;
+  end;
+
+  len := Length(fNames);
+  for i := 0 to len -1 do
+    if name = fNames[i] then
+    begin
+      Result := fImages[i];
+      Exit;
+    end;
+  Result := AddNamedImage(name);
+end;
+//------------------------------------------------------------------------------
+
+procedure TFilterElement.Apply(img: TImage32;
+  const filterBounds: TRect; const matrix: TMatrixD);
+var
+  i: integer;
+begin
+  fScale := ExtractAvgScaleFromMatrix(matrix);
+  fFilterBounds := filterBounds;
+  Types.IntersectRect(fObjectBounds, fObjectBounds, img.Bounds);
+  fSrcImg := img;
+
+  try
+    for i := 0 to fChilds.Count -1 do
+    begin
+      case TSvgElement(fChilds[i]).fParserEl.hash of
+        hfeBlend            : TFeBlendElement(fChilds[i]).Apply;
+        hfeColorMatrix      : TFeColorMatrixElement(fChilds[i]).Apply;
+        hfeComposite        : TFeCompositeElement(fChilds[i]).Apply;
+        hfeDefuseLighting   : TFeDefuseLightElement(fChilds[i]).Apply;
+        hfeDropShadow       : TFeDropShadowElement(fChilds[i]).Apply;
+        hfeFlood            : TFeFloodElement(fChilds[i]).Apply;
+        hFeGaussianBlur     : TFeGaussElement(fChilds[i]).Apply;
+        hfeMerge            : TFeMergeElement(fChilds[i]).Apply;
+        hfeOffset           : TFeOffsetElement(fChilds[i]).Apply;
+        hfeSpecularLighting : TFeSpecLightElement(fChilds[i]).Apply;
+      end;
+    end;
+    fSrcImg.Copy(fLastImg, fLastImg.Bounds, fFilterBounds);
+  finally
+    Clear;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TFeBaseElement
+//------------------------------------------------------------------------------
+
+function TFeBaseElement.GetParentAsFilterEl: TFilterElement;
+var
+  el: TSvgElement;
+begin
+  el := fParent;
+  while Assigned(el) and not (el is TFilterElement) do
+    el := el.fParent;
+  if not Assigned(el) then
+    Result := nil else
+    Result := TFilterElement(el);
+end;
+//------------------------------------------------------------------------------
+
+function TFeBaseElement.GetBounds(img: TImage32): TRect;
+var
+  pfe: TFilterElement;
+begin
+  pfe := ParentFilterEl;
+  if img = pfe.fSrcImg then
+    Result := pfe.fFilterBounds else
+    Result := img.Bounds;
+end;
+//------------------------------------------------------------------------------
+
+function TFeBaseElement.GetSrcAndDst: Boolean;
+var
+  pfe: TFilterElement;
+begin
+  pfe := ParentFilterEl;
+  if (in1 <> '') then
+    srcImg := pfe.GetNamedImage(in1)
+  else if Assigned(pfe.fLastImg) then
+    srcImg := pfe.fLastImg
+  else
+    srcImg := pfe.GetNamedImage(SourceImage);
+
+  if (res <> '') then
+    dstImg := pfe.GetNamedImage(res) else
+    dstImg := pfe.GetNamedImage(SourceImage);
+
+  Result := Assigned(srcImg) and Assigned(dstImg);
+  if not Result then Exit;
+  pfe.fLastImg := dstImg;
+  srcRec := GetBounds(srcImg);
+  dstRec := GetBounds(dstImg);
+end;
+
+//------------------------------------------------------------------------------
+// TFeBlendElement
+//------------------------------------------------------------------------------
+
+procedure TFeBlendElement.Apply;
+var
+  pfe: TFilterElement;
+  srcImg2, dstImg2: TImage32;
+  srcRec2, dstRec2: TRect;
+begin
+  if not GetSrcAndDst then Exit;
+  pfe := ParentFilterEl;
+  if (in2 = '') then Exit;
+  if dstImg = srcImg then
+    dstImg2 := pfe.AddNamedImage(tmpFilterImg) else
+    dstImg2 := dstImg;
+  dstRec2 := GetBounds(dstImg2);
+
+  srcImg2 := pfe.GetNamedImage(in2);
+  srcRec2 := GetBounds(srcImg2);
+  dstImg2.CopyBlend(srcImg2, srcRec2, dstRec2, BlendToAlpha);
+  dstImg2.CopyBlend(srcImg,  srcRec,  dstRec2, BlendToAlpha);
+  if dstImg = srcImg then
+    dstImg.Copy(dstImg2, dstRec2, dstRec);
+end;
+
+//------------------------------------------------------------------------------
+// TFeCompositeElement
+//------------------------------------------------------------------------------
+
+constructor TFeCompositeElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  fourKs[0] := InvalidD; fourKs[1] := InvalidD;
+  fourKs[2] := InvalidD; fourKs[3] := InvalidD;
+end;
+//------------------------------------------------------------------------------
+
+procedure Arithmetic(p1, p2, r: PColor32; const ks: array of byte);
+var
+  c1  : PARGB absolute p1;
+  c2  : PARGB absolute p2;
+  res : PARGB absolute r;
+begin
+  res.A := (((c1.A xor 255) * (c2.A xor 255)) shr 8) xor 255;
+  res.R := ClampByte((ks[0] * c1.R * c2.R +
+    ks[1] * c1.R * 255 + ks[2] * c2.R * 255 + ks[3] * 65025) shr 16);
+  res.G := ClampByte((ks[0] * c1.G * c2.G +
+    ks[1] * c1.G * 255 + ks[2] * c2.G * 255 + ks[3] * 65025) shr 16);
+  res.B := ClampByte((ks[0] * c1.B * c2.B +
+    ks[1] * c1.B * 255 + ks[2] * c2.B * 255 + ks[3] * 65025) shr 16);
+end;
+//------------------------------------------------------------------------------
+
+procedure ArithmeticBlend(src1, src2, dst: TImage32;
+  const recS1, recS2, recDst: TRect; const ks: TFourDoubles);
+var
+  kk: array[0..3] of byte;
+  w,h, w2,h2, w3,h3, i,j: integer;
+  p1,p2,r: PColor32;
+begin
+  RectWidthHeight(recS1, w, h);
+  RectWidthHeight(recS2, w2, h2);
+  RectWidthHeight(recDst, w3, h3);
+  if (w2 <> w) or (w3 <> w) or (h2 <> h) or (h3 <> h) or
+    (ks[0] = InvalidD) or (ks[1] = InvalidD) or
+    (ks[2] = InvalidD) or (ks[3] = InvalidD) then Exit;
+
+  for i := 0 to 3 do
+    kk[i] := ClampByte(ks[i]*255);
+
+  for i := 0 to h -1 do
+  begin
+    p1 := @src1.Pixels[(recS1.Top + i) * src1.Width + recS1.Left];
+    p2 := @src2.Pixels[(recS2.Top + i) * src2.Width + recS2.Left];
+    r  := @dst.Pixels[(recDst.Top + i) * dst.Width + recDst.Left];
+    for j := 0 to w -1 do
+    begin
+      Arithmetic(p1, p2, r, kk);
+      inc(p1); inc(p2); inc(r);
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFeCompositeElement.Apply;
+var
+  pfe: TFilterElement;
+  srcImg2, dstImg2: TImage32;
+  srcRec2, dstRec2: TRect;
+begin
+  if not GetSrcAndDst then Exit;
+  pfe := ParentFilterEl;
+  if (in2 = '') then Exit;
+
+  srcImg2 := pfe.GetNamedImage(in2);
+  srcRec2 := GetBounds(srcImg2); //either filter bounds or image bounds
+
+  if (dstImg = srcImg) or (dstImg = srcImg2) then
+    dstImg2 := pfe.AddNamedImage(tmpFilterImg) else
+    dstImg2 := dstImg;
+  dstRec2 := GetBounds(dstImg2); //either filter bounds or image bounds
+
+  case compositeOp of
+    coIn:
+      begin
+        dstImg2.Copy(srcImg, srcRec, dstRec2);
+        dstImg2.CopyBlend(srcImg2,  srcRec2,  dstRec2, BlendMask);
+      end;
+    coOut:
+      begin
+        dstImg2.Copy(srcImg, srcRec, dstRec2);
+        dstImg2.CopyBlend(srcImg2,  srcRec2,  dstRec2, BlendInvertedMask);
+      end;
+    coAtop:
+      begin
+        dstImg2.Copy(srcImg2, srcRec2, dstRec2);
+        dstImg2.CopyBlend(srcImg,  srcRec,  dstRec2, BlendToAlpha);
+        dstImg2.CopyBlend(srcImg2,  srcRec2,  dstRec2, BlendMask);
+      end;
+    coXOR:
+      begin
+        dstImg2.Copy(srcImg2, srcRec2, dstRec2);
+        dstImg2.CopyBlend(srcImg, srcRec, dstRec2, BlendToAlpha);
+        dstImg2.CopyBlend(srcImg2,  srcRec2,  dstRec2, BlendInvertedMask);
+      end;
+    coArithmetic:
+      begin
+        ArithmeticBlend(srcImg, srcImg2, dstImg2,
+          srcRec, srcRec2, dstRec2, fourKs);
+      end;
+    else     //coOver
+      begin
+        dstImg2.CopyBlend(srcImg2, srcRec2, dstRec2, BlendToAlpha);
+        dstImg2.CopyBlend(srcImg,  srcRec,  dstRec2, BlendToAlpha);
+      end;
+  end;
+  if (dstImg <> dstImg2) then
+    dstImg.Copy(dstImg2, dstRec2, dstRec);
+end;
+
+//------------------------------------------------------------------------------
+// TFeColorMatrixElement
+//------------------------------------------------------------------------------
+
+type
+  TColorMatrix = array[0..19] of Byte;
+
+function ApplyColorMatrix(color: TColor32; const mat: TColorMatrix): TColor32;
+var
+  clrIn : TARGB absolute color;
+  clrOut: TARGB absolute Result;
+begin
+  clrOut.R := ClampByte(MulBytes(mat[0],clrIn.R) + MulBytes(mat[1],clrIn.G) +
+    MulBytes(mat[2],clrIn.B) + MulBytes(mat[3],clrIn.A) + mat[4]);
+  clrOut.G := ClampByte(MulBytes(mat[5],clrIn.R) + MulBytes(mat[6],clrIn.G) +
+    MulBytes(mat[7],clrIn.B) + MulBytes(mat[8],clrIn.A) + mat[9]);
+  clrOut.B := ClampByte(MulBytes(mat[10],clrIn.R) + MulBytes(mat[11],clrIn.G) +
+    MulBytes(mat[12],clrIn.B) + MulBytes(mat[13],clrIn.A) + mat[14]);
+  clrOut.A := ClampByte(MulBytes(mat[15],clrIn.R) + MulBytes(mat[16],clrIn.G) +
+    MulBytes(mat[17],clrIn.B) + MulBytes(mat[18],clrIn.A) + mat[19]);
+end;
+//------------------------------------------------------------------------------
+
+procedure TFeColorMatrixElement.Apply;
+var
+  i,j, dx1,dx2: integer;
+  colorMatrix: TColorMatrix;
+  p1, p2: PColor32;
+begin
+  if not GetSrcAndDst or not Assigned(values) then Exit;
+  for i := 0 to 19 do
+    colorMatrix[i] := ClampByte(Round(values[i]*255));
+
+  dx1 := srcImg.Width - RectWidth(srcRec);
+  dx2 := dstImg.Width - RectWidth(dstRec);
+  p1 := @srcImg.Pixels[srcRec.Top * srcImg.Width + srcRec.Left];
+  p2 := @dstImg.Pixels[dstRec.Top * dstImg.Width + dstRec.Left];
+  for i := srcRec.Top to srcRec.Bottom -1 do
+  begin
+    for j := srcRec.Left to srcRec.Right -1 do
+    begin
+      p2^ := ApplyColorMatrix(p1^, colorMatrix);
+      inc(p1); inc(p2);
+    end;
+    inc(p1, dx1); inc(p2, dx2);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TFeDefuseLightElement
+//------------------------------------------------------------------------------
+
+procedure TFeDefuseLightElement.Apply;
+begin
+  //not implemented
+  if not GetSrcAndDst then Exit;
+  if srcImg <> dstImg then
+    dstImg.Copy(srcImg, srcRec, dstRec);
+end;
+
+//------------------------------------------------------------------------------
+// TFeDropShadowElement
+//------------------------------------------------------------------------------
+
+constructor TFeDropShadowElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  stdDev := InvalidD;
+  floodColor := clInvalid;
+  offset.X.SetValue(0);
+  offset.Y.SetValue(0);
+end;
+//------------------------------------------------------------------------------
+
+procedure TFeDropShadowElement.Apply;
+var
+  alpha: Byte;
+  off: TPointD;
+  dstOffRec: TRect;
+  pfe: TFilterElement;
+  dropShadImg: TImage32;
+begin
+  if not GetSrcAndDst then Exit;
+  pfe := ParentFilterEl;
+  dropShadImg := pfe.GetNamedImage(tmpFilterImg);
+  dropShadImg.Copy(srcImg, srcRec, dropShadImg.Bounds);
+
+  off := offset.GetPoint(RectD(pfe.fObjectBounds), GetRelFracLimit);
+  off := ScalePoint(off, pfe.fScale);
+  dstOffRec := dstRec;
+  with Point(off) do Types.OffsetRect(dstOffRec, X, Y);
+  dstImg.Copy(srcImg, srcRec, dstOffRec);
+  dstImg.SetRGB(floodColor);
+  alpha := GetAlpha(floodColor);
+  if (alpha > 0) and (alpha < 255) then
+    dstImg.ReduceOpacity(alpha);
+  if stdDev > 0 then
+    FastGaussianBlur(dstImg, dstRec,
+      Ceil(stdDev *0.75 * ParentFilterEl.fScale) , 0);
+  dstImg.CopyBlend(dropShadImg, dropShadImg.Bounds, dstRec, BlendToAlpha);
+end;
+
+//------------------------------------------------------------------------------
+// TFeFloodElement
+//------------------------------------------------------------------------------
+
+constructor TFeFloodElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  floodColor := clInvalid;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFeFloodElement.Apply;
+var
+  rec: TRect;
+begin
+  if not GetSrcAndDst then Exit;
+  if elRectWH.IsValid then
+    rec := Rect(elRectWH.GetRectD(RectD(srcRec), GetRelFracLimit)) else
+    rec := dstRec;
+  dstImg.FillRect(rec, floodColor);
+end;
+
+//------------------------------------------------------------------------------
+// TFeGaussElement
+//------------------------------------------------------------------------------
+
+constructor TFeGaussElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  stdDev := InvalidD;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFeGaussElement.Apply;
+begin
+  if not GetSrcAndDst then Exit;
+
+  if srcImg <> dstImg then
+    dstImg.Copy(srcImg, srcRec, dstRec);
+
+  ////True GaussianBlur is visually optimal, but it's also *extremely* slow.
+  //GaussianBlur(dstImg, dstRec, Ceil(stdDev *PI * ParentFilterEl.fScale));
+
+  //FastGaussianBlur is a very good approximation and also very much faster.
+  //Empirically stdDev * PI/4 more closely emulates other renderers.
+  FastGaussianBlur(dstImg, dstRec,
+    Ceil(stdDev * PI/4 * ParentFilterEl.fScale), fReader.fBlurQuality);
+end;
+
+//------------------------------------------------------------------------------
+// TFeMergeElement
+//------------------------------------------------------------------------------
+
+procedure TFeMergeElement.Apply;
+var
+  i: integer;
+  tmpImg: TImage32;
+  pfe: TFilterElement;
+begin
+  tmpImg := nil;
+  if not GetSrcAndDst then Exit;
+  pfe := ParentFilterEl;
+
+  for i := 0 to fChilds.Count -1 do
+    if TSvgElement(fChilds[i]) is TFeMergeNodeElement then
+      with TFeMergeNodeElement(fChilds[i]) do
+      begin
+        if not GetSrcAndDst then Continue;
+        if Assigned(tmpImg) then
+          tmpImg.CopyBlend(srcImg, srcRec, tmpImg.Bounds, BlendToAlpha)
+        else if srcImg = pfe.fSrcImg then
+          tmpImg := pfe.GetNamedImage(SourceImage)
+        else
+          tmpImg := srcImg;
+      end;
+
+  dstImg.Copy(tmpImg, tmpImg.Bounds, dstRec);
+  pfe.fLastImg := dstImg;
+end;
+
+//------------------------------------------------------------------------------
+// TFeMergeNodeElement
+//------------------------------------------------------------------------------
+
+procedure TFeMergeNodeElement.Apply;
+begin
+  //should never get here ;)
+end;
+
+//------------------------------------------------------------------------------
+// TFeOffsetElement
+//------------------------------------------------------------------------------
+
+procedure TFeOffsetElement.Apply;
+var
+  off: TPointD;
+  dstOffRec: TRect;
+  tmpImg: TImage32;
+  pfe: TFilterElement;
+begin
+  if not GetSrcAndDst then Exit;
+  pfe := ParentFilterEl;
+  off := offset.GetPoint(RectD(pfe.fObjectBounds), GetRelFracLimit);
+  off := ScalePoint(off, pfe.fScale);
+  dstOffRec := dstRec;
+  with Point(off) do Types.OffsetRect(dstOffRec, X, Y);
+
+  if srcImg = dstImg then
+  begin
+    tmpImg := pfe.GetNamedImage(tmpFilterImg);
+    tmpImg.Copy(srcImg, srcRec, tmpImg.Bounds);
+    dstImg.Clear(dstRec);
+    dstImg.Copy(tmpImg, tmpImg.Bounds, dstOffRec);
+  end else
+  begin
+    dstImg.Clear(dstRec);
+    dstImg.Copy(srcImg, srcRec, dstOffRec);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TFeSpecLightElement
+//------------------------------------------------------------------------------
+
+procedure TFeSpecLightElement.Apply;
+begin
+  //not implemented
+  if not GetSrcAndDst then Exit;
+  if srcImg <> dstImg then
+    dstImg.Copy(srcImg, srcRec, dstRec);
+end;
+
+//------------------------------------------------------------------------------
+// TClipPathElement
+//------------------------------------------------------------------------------
+
+constructor TClipPathElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  fDrawData.visible := false;
+end;
+//------------------------------------------------------------------------------
+
+procedure TClipPathElement.GetPaths(const drawDat: TDrawData);
+var
+  i: integer;
+begin
+  if Assigned(drawPathsC) or Assigned(drawPathsO) then Exit;
+  for i := 0 to fChilds.Count -1 do
+    if TSvgElement(fChilds[i]) is TShapeElement then
+      with TShapeElement(fChilds[i]) do
+      begin
+        GetPaths(drawDat);
+        AppendPath(self.drawPathsO, drawPathsO);
+        AppendPath(self.drawPathsC, drawPathsC);
+      end;
+  drawPathsF := CopyPaths(drawPathsC);
+  AppendPath(drawPathsF, drawPathsO);
+end;
+
+//------------------------------------------------------------------------------
+// TShapeElement
+//------------------------------------------------------------------------------
+
+constructor TShapeElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  elRectWH.Init;
+  hasPaths := true;
+  fDrawData.visible := true;
+  if fParserEl.name = '' then Exit;
+end;
+//------------------------------------------------------------------------------
+
+function  TShapeElement.GetBounds: TRectD;
+var
+  i: integer;
+begin
+  Result := NullRectD;
+  for i := 0 to fChilds.Count -1 do
+    if TSvgElement(fChilds[i]) is TShapeElement then
+       Result := UnionRect(Result, TShapeElement(fChilds[i]).GetBounds);
+end;
+//------------------------------------------------------------------------------
+
+function TShapeElement.HasMarkers: Boolean;
+begin
+  Result := IsStroked(fDrawData) and ((fDrawData.markerStart <> '') or
+    (fDrawData.markerMiddle <> '') or (fDrawData.markerEnd <> ''));
+end;
+//------------------------------------------------------------------------------
+
+procedure TShapeElement.Draw(image: TImage32; drawDat: TDrawData);
+var
+  d           : double;
+  img         : TImage32;
+  stroked     : Boolean;
+  filled      : Boolean;
+  clipRec     : TRectD;
+  clipRec2    : TRect;
+  clipPathEl  : TSvgElement;
+  filterEl    : TSvgElement;
+  maskEl      : TSvgElement;
+  clipPaths   : TPathsD;
+  di          : TDrawData;
+  usingTempImage: Boolean;
+begin
+  UpdateDrawInfo(drawDat, self);
+
+  filled := IsFilled(drawDat);
+  stroked := IsStroked(drawDat);
+  GetPaths(drawDat);
+
+  if not (filled or stroked) or not hasPaths then Exit;
+  drawDat.bounds := GetBoundsD(drawPathsF);
+
+  img := image;
+  clipRec2 := NullRect;
+
+  maskEl := FindRefElement(drawDat.maskElRef);
+  clipPathEl := FindRefElement(drawDat.clipElRef);
+  filterEl := FindRefElement(drawDat.filterElRef);
+
+  if (drawDat.fillEl <> '') and
+    (drawDat.fillOpacity > 0) and (drawDat.fillOpacity < 1) then
+      drawDat.opacity := Round(drawDat.fillOpacity * 255);
+  usingTempImage := Assigned(clipPathEl) or
+    Assigned(filterEl) or Assigned(maskEl) or (drawDat.opacity < 255);
+
+  if usingTempImage then
+  begin
+    img := fReader.TempImage;
+
+    //get special effects bounds
+    if Assigned(clipPathEl) then
+    begin
+      drawDat.clipElRef := '';
+      di := drawDat;
+      with TClipPathElement(clipPathEl) do
+      begin
+        GetPaths(di);
+        clipPaths := MatrixApply(drawPathsF, di.matrix);
+        clipRec := GetBoundsD(clipPaths);
+      end;
+    end
+    else if Assigned(maskEl) then
+    begin
+      drawDat.maskElRef := '';
+      with TMaskElement(maskEl) do
+      begin
+        GetPaths(drawDat);
+        clipRec := RectD(maskRec);
+      end;
+    end else
+    begin
+      clipRec := drawDat.bounds;
+      if stroked and drawDat.strokeWidth.IsValid then
+      begin
+        with drawDat.strokeWidth do
+          if HasFontUnits then
+            d := GetValue(drawDat.fontInfo.size, GetRelFracLimit) else
+            d := GetValueXY(clipRec, GetRelFracLimit);
+        Img32.Vector.InflateRect(clipRec, d * 0.5, d * 0.5);
+      end;
+      if Assigned(filterEl) then
+      begin
+        drawDat.filterElRef := '';
+        with TFilterElement(filterEl) do
+        begin
+          fScale := ExtractAvgScaleFromMatrix(DrawData.matrix);
+          clipRec := GetAdjustedBounds(clipRec);
+        end;
+      end;
+      MatrixApply(drawDat.matrix, clipRec);
+    end;
+    clipRec2 := Rect(clipRec);
+    Types.IntersectRect(clipRec2, clipRec2, img.Bounds);
+    if IsEmptyRect(clipRec2) then Exit;
+    if image <> fReader.TempImage then
+      img.Clear(clipRec2);
+  end;
+
+  if not IsValidMatrix(drawDat.matrix) then
+    raise Exception.Create('Invalid matrix found when drawing element');
+
+  if filled then
+    DrawFilled(img, drawDat);
+
+  if stroked then
+  begin
+    if Assigned(drawPathsC) then
+      DrawStroke(img, drawDat, true);
+    if stroked and Assigned(drawPathsO) then
+      DrawStroke(img, drawDat, false);
+  end;
+
+  if Assigned(filterEl) then
+    with TFilterElement(filterEl) do
+      Apply(img, clipRec2, drawDat.matrix);
+
+  if drawDat.opacity < 255 then
+    img.ReduceOpacity(drawDat.opacity, clipRec2);
+
+  if Assigned(maskEl) then
+    TMaskElement(maskEl).ApplyMask(img, drawDat)
+  else if Assigned(clipPathEl) then
+    with TClipPathElement(clipPathEl) do
+      EraseOutsidePaths(img, clipPaths, fDrawData.fillRule, clipRec2);
+
+  if usingTempImage and (img <> image) then
+    image.CopyBlend(img, clipRec2, clipRec2, BlendToAlpha);
+
+  //todo: enable "paint-order" to change filled/stroked/marker paint order
+  if HasMarkers then DrawMarkers(img, drawDat);
+end;
+//------------------------------------------------------------------------------
+
+procedure TShapeElement.DrawMarkers(img: TImage32; drawDat: TDrawData);
+var
+  i,j: integer;
+  sw: double;
+  markerEl: TSvgElement;
+  markerPaths: TPathsD;
+  pt1, pt2: TPointD;
+  di: TDrawData;
+begin
+  markerPaths := GetSimplePath(drawDat);
+  markerPaths := StripNearDuplicates(markerPaths, 0.01, false);
+
+  if not Assigned(markerPaths) then Exit;
+  MatrixApply(drawDat.matrix, markerPaths);
+
+  di := emptyDrawInfo;
+
+  //prepare to scale the markers by the stroke width
+  with fDrawData.strokeWidth do
+    if not IsValid then sw := 1
+    else if HasFontUnits then
+      sw := GetValue(drawDat.fontInfo.size, GetRelFracLimit)
+    else sw := GetValueXY(drawDat.bounds, GetRelFracLimit);
+
+  MatrixScale(di.matrix, sw * ExtractAvgScaleFromMatrix(drawDat.matrix));
+
+  if (fDrawData.markerStart <> '') then
+  begin
+    markerEl := FindRefElement(fDrawData.markerStart);
+    if Assigned(markerEl) and (markerEl is TMarkerElement) then
+      with TMarkerElement(markerEl) do
+      begin
+        for i := 0 to High(markerPaths) do
+        begin
+          if Length(markerPaths[i]) < 2 then Continue;
+          pt1 := markerPaths[i][0];
+          pt2 := markerPaths[i][1];
+          if autoStartReverse then
+            SetEndPoint(pt1, GetAngle(pt2, pt1)) else
+            SetEndPoint(pt1, GetAngle(pt1, pt2));
+          Draw(img, di);
+        end;
+      end;
+  end;
+
+  if (fDrawData.markerMiddle <> '') then
+  begin
+    markerEl := FindRefElement(fDrawData.markerMiddle);
+    if Assigned(markerEl) and (markerEl is TMarkerElement) then
+      with TMarkerElement(markerEl) do
+        for i := 0 to High(markerPaths) do
+          if SetMiddlePoints(markerPaths[i]) then
+            Draw(img, di);
+  end;
+
+  if (fDrawData.markerEnd <> '') then
+  begin
+    markerEl := FindRefElement(fDrawData.markerEnd);
+    if Assigned(markerEl) and (markerEl is TMarkerElement) then
+      with TMarkerElement(markerEl) do
+      begin
+        for i := 0 to High(markerPaths) do
+        begin
+          j := High(markerPaths[i]);
+          if j < 1 then Continue;
+          pt1 := markerPaths[i][j];
+          pt2 := markerPaths[i][j-1];
+          SetEndPoint(pt1, GetAngle(pt2, pt1));
+          Draw(img, di);
+        end;
+      end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TShapeElement.GetPaths(const drawDat: TDrawData);
+begin
+  drawPathsO := nil; drawPathsC := nil; drawPathsF := nil;
+end;
+//------------------------------------------------------------------------------
+
+function  TShapeElement.GetSimplePath(const drawDat: TDrawData): TPathsD;
+begin
+  Result := nil;
+end;
+//------------------------------------------------------------------------------
+
+procedure TShapeElement.DrawFilled(img: TImage32; drawDat: TDrawData);
+var
+  refEl: TSvgElement;
+  fillPaths: TPathsD;
+begin
+  if not assigned(drawPathsF) then Exit;
+  if drawDat.fillColor = clCurrent then
+    drawDat.fillColor := fReader.currentColor;
+
+  fillPaths := MatrixApply(drawPathsF, drawDat.matrix);
+  if (drawDat.fillEl <> '') then
+  begin
+    refEl := FindRefElement(drawDat.fillEl);
+    if Assigned(refEl) and (refEl is TFillElement) then
+    begin
+      if refEl is TRadGradElement then
+      begin
+        with TRadGradElement(refEl), fReader do
+          if PrepareRenderer(RadGradRenderer, drawDat) then
+            DrawPolygon(img, fillPaths, drawDat.fillRule, RadGradRenderer);
+      end
+      else if refEl is TLinGradElement then
+      begin
+        with TLinGradElement(refEl), fReader do
+          if PrepareRenderer(LinGradRenderer, drawDat) then
+            DrawPolygon(img, fillPaths, drawDat.fillRule, LinGradRenderer);
+      end
+      else if refEl is TPatternElement then
+      begin
+        with TPatternElement(refEl), fReader do
+          if PrepareRenderer(ImageRenderer, drawDat) then
+            DrawPolygon(img, fillPaths, drawDat.fillRule, ImageRenderer);
+      end;
+    end;
+  end
+  else if drawDat.fillColor = clInvalid then
+    DrawPolygon(img, fillPaths, drawDat.fillRule, clBlack32)
+  else
+    with drawDat do
+      DrawPolygon(img, fillPaths, fillRule,
+        MergeColorAndOpacity(fillColor, fillOpacity));
+end;
+//------------------------------------------------------------------------------
+
+procedure TShapeElement.DrawStroke(img: TImage32;
+  drawDat: TDrawData; isClosed: Boolean);
+var
+  dashOffset, scaledStrokeWidth, roundingScale: double;
+  dashArray: TArrayOfInteger;
+  scale: Double;
+  strokeClr: TColor32;
+  strokePaths: TPathsD;
+  refEl: TSvgElement;
+  endStyle: TEndStyle;
+  joinStyle: TJoinStyle;
+  bounds: TRectD;
+begin
+  if isClosed then
+  begin
+    strokePaths := MatrixApply(drawPathsC, drawDat.matrix);
+    endStyle := esPolygon;
+  end else
+  begin
+    strokePaths := MatrixApply(drawPathsO, drawDat.matrix);
+    if fDrawData.strokeCap = esPolygon then
+      endStyle := esButt else
+      endStyle := fDrawData.strokeCap;
+  end;
+  if not Assigned(strokePaths) then Exit;
+  joinStyle := fDrawData.strokeJoin;
+  if drawDat.strokeColor = clCurrent then
+    drawDat.strokeColor := fReader.currentColor;
+
+  scale := ExtractAvgScaleFromMatrix(drawDat.matrix);
+  bounds := fReader.userSpaceBounds;
+  with drawDat.strokeWidth do
+  begin
+    if not IsValid then
+      scaledStrokeWidth := scale
+    else if HasFontUnits then
+      scaledStrokeWidth :=
+        GetValue(drawDat.fontInfo.size, GetRelFracLimit) * scale
+    else
+      scaledStrokeWidth := GetValueXY(bounds, 0) * scale;
+  end;
+  roundingScale := scale;
+
+  if Length(drawDat.dashArray) > 0 then
+    dashArray := MakeDashArray(drawDat.dashArray, scale) else
+    dashArray := nil;
+
+  with drawDat do
+    strokeClr := MergeColorAndOpacity(strokeColor, strokeOpacity);
+
+  if Assigned(dashArray) then
+  begin
+    dashOffset := drawDat.dashOffset * scale;
+    DrawDashedLine(img, strokePaths, dashArray,
+      @dashOffset, scaledStrokeWidth, strokeClr, endStyle);
+  end
+  else if (drawDat.strokeEl <> '') then
+  begin
+    refEl := FindRefElement(drawDat.strokeEl);
+    if not Assigned(refEl) then Exit;
+
+    if refEl is TRadGradElement then
+    begin
+      with TRadGradElement(refEl) do
+        PrepareRenderer(fReader.RadGradRenderer, drawDat);
+      DrawLine(img, strokePaths, scaledStrokeWidth,
+        fReader.RadGradRenderer, endStyle, joinStyle, roundingScale);
+    end
+    else if refEl is TLinGradElement then
+    begin
+      with TLinGradElement(refEl) do
+        PrepareRenderer(fReader.LinGradRenderer, drawDat);
+      DrawLine(img, strokePaths, scaledStrokeWidth,
+        fReader.LinGradRenderer, endStyle, joinStyle, roundingScale);
+    end
+    else if refEl is TPatternElement then
+    begin
+      with TPatternElement(refEl) do
+        PrepareRenderer(fReader.ImageRenderer, drawDat);
+      DrawLine(img, strokePaths,  scaledStrokeWidth,
+        fReader.ImageRenderer, endStyle, joinStyle, roundingScale);
+    end;
+  end
+  else if (joinStyle = jsMiter) then
+    DrawLine(img, strokePaths, scaledStrokeWidth,
+      strokeClr, endStyle, joinStyle, drawDat.strokeMitLim)
+  else
+    DrawLine(img, strokePaths, scaledStrokeWidth,
+      strokeClr, endStyle, joinStyle, roundingScale);
+end;
+
+//------------------------------------------------------------------------------
+// TPathElement
+//------------------------------------------------------------------------------
+
+constructor TPathElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  fSvgPaths := TSvgPath.Create;
+end;
+//------------------------------------------------------------------------------
+
+destructor TPathElement.Destroy;
+begin
+  fSvgPaths.Free;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+function TPathElement.GetBounds: TRectD;
+var
+  i: integer;
+begin
+  Result := NullRectD;
+  for i := 0 to fSvgPaths.Count -1 do
+    Result := UnionRect(Result, fSvgPaths[i].GetBounds);
+end;
+//------------------------------------------------------------------------------
+
+procedure TPathElement.ParseDAttrib(const value: UTF8String);
+begin
+  fSvgPaths.Parse(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure TPathElement.Flatten(index: integer; scalePending: double;
+  out path: TPathD; out isClosed: Boolean);
+begin
+  isClosed := fSvgPaths[index].isClosed;
+  path := fSvgPaths[index].GetFlattenedPath(scalePending);
+end;
+//------------------------------------------------------------------------------
+
+procedure TPathElement.GetPaths(const drawDat: TDrawData);
+var
+  i: integer;
+  scalePending: double;
+  isClosed: Boolean;
+  path: TPathD;
+begin
+  if Assigned(drawPathsC) or Assigned(drawPathsO) then inherited;
+  scalePending := ExtractAvgScaleFromMatrix(drawDat.matrix);
+  for i := 0 to fSvgPaths.Count -1 do
+  begin
+    Flatten(i, scalePending, path, isClosed);
+    if not Assigned(path) then Continue;
+
+    if isClosed then
+      AppendPath(drawPathsC, path) else
+      AppendPath(drawPathsO, path);
+  end;
+  AppendPath(drawPathsF, drawPathsO);
+  AppendPath(drawPathsF, drawPathsC);
+end;
+//------------------------------------------------------------------------------
+
+function TPathElement.GetSimplePath(const drawDat: TDrawData): TPathsD;
+var
+  i: integer;
+begin
+  Result := nil;
+  SetLength(Result, fSvgPaths.Count);
+  for i := 0 to fSvgPaths.Count -1 do
+    Result[i] := fSvgPaths[i].GetSimplePath;
+end;
+
+//------------------------------------------------------------------------------
+// TPolyElement
+//------------------------------------------------------------------------------
+
+function TPolyElement.GetBounds: TRectD;
+begin
+  Result := GetBoundsD(path);
+end;
+//------------------------------------------------------------------------------
+
+procedure TPolyElement.GetPaths(const drawDat: TDrawData);
+begin
+  if Assigned(drawPathsC) or Assigned(drawPathsO) then Exit;
+  if not Assigned(path) then Exit;
+  if (fParserEl.hash = hPolygon) then
+  begin
+    AppendPath(drawPathsC, path);                    //hPolygon
+    drawPathsF := drawPathsC;
+  end else
+  begin
+    AppendPath(drawPathsO, path);                   //hPolyline
+    drawPathsF := drawPathsO;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TPolyElement.GetSimplePath(const drawDat: TDrawData): TPathsD;
+begin
+  Result := nil;
+  AppendPath(Result, path);
+end;
+//------------------------------------------------------------------------------
+
+procedure TPolyElement.ParsePoints(const value: UTF8String);
+var
+  currCnt, currCap: integer;
+
+  procedure AddPoint(const pt: TPointD);
+  begin
+    if currCnt = currCap then
+    begin
+      currCap := currCap + buffSize;
+      SetLength(path, currCap);
+    end;
+    path[currCnt] := pt;
+    inc(currCnt);
+  end;
+
+var
+  pt: TPointD;
+  c, endC: PUTF8Char;
+begin
+  currCnt     := 0;
+  currCap     := buffSize;
+  c := PUTF8Char(value);
+  endC := c + Length(value);
+  SetLength(path, currCap);
+  while IsNumPending(c, endC, true) and
+    ParseNextNum(c, endC, true, pt.X) and
+    ParseNextNum(c, endC, true, pt.Y) do
+      AddPoint(pt);
+  SetLength(path, currCnt);
+end;
+
+//------------------------------------------------------------------------------
+// TLineElement
+//------------------------------------------------------------------------------
+
+constructor TLineElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  SetLength(path, 2);
+  path[0] := NullPointD; path[1] := NullPointD;
+end;
+//------------------------------------------------------------------------------
+
+function TLineElement.GetBounds: TRectD;
+begin
+  Result := GetBoundsD(path);
+end;
+//------------------------------------------------------------------------------
+
+procedure TLineElement.GetPaths(const drawDat: TDrawData);
+begin
+  if Assigned(drawPathsO) then Exit;
+  AppendPath(drawPathsO, path);
+  drawPathsF := drawPathsO;
+end;
+//------------------------------------------------------------------------------
+
+function TLineElement.GetSimplePath(const drawDat: TDrawData): TPathsD;
+begin
+  Result := nil;
+  AppendPath(Result, path);
+end;
+
+//------------------------------------------------------------------------------
+// TCircleElement
+//------------------------------------------------------------------------------
+
+constructor TCircleElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  centerPt.Init;
+  radius.Init;
+end;
+//------------------------------------------------------------------------------
+
+function TCircleElement.GetBounds: TRectD;
+var
+  cp  : TPointD;
+  r   : double;
+begin
+  Result := NullRectD;
+  if not radius.IsValid then Exit;
+  r := radius.GetValue(1, GetRelFracLimit);
+  cp := centerPt.GetPoint(NullRectD, GetRelFracLimit);
+  Result := RectD(cp.X -r, cp.Y -r, cp.X +r, cp.Y +r);
+end;
+//------------------------------------------------------------------------------
+
+procedure TCircleElement.GetPaths(const drawDat: TDrawData);
+var
+  scalePending : double;
+  rec   : TRectD;
+  pt    : TPointD;
+  path  : TPathD;
+  r: double;
+begin
+  if Assigned(drawPathsC) then inherited;
+  if not radius.IsValid then Exit;
+  r := radius.GetValueXY(drawDat.bounds, GetRelFracLimit);
+  pt := centerPt.GetPoint(drawDat.bounds, GetRelFracLimit);
+  scalePending := ExtractAvgScaleFromMatrix(drawDat.matrix);
+  rec := RectD(pt.X -r, pt.Y -r, pt.X +r, pt.Y +r);
+  path := Ellipse(rec, scalePending);
+  AppendPath(drawPathsC, path);
+  drawPathsF := drawPathsC;
+end;
+
+//------------------------------------------------------------------------------
+// TEllipseElement
+//------------------------------------------------------------------------------
+
+constructor TEllipseElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  centerPt.Init;
+  radius.Init;
+end;
+//------------------------------------------------------------------------------
+
+function TEllipseElement.GetBounds: TRectD;
+var
+  cp  : TPointD;
+  r   : TPointD;
+begin
+  Result := NullRectD;
+  if not radius.IsValid then Exit;
+  r := radius.GetPoint(NullRectD, GetRelFracLimit);
+  cp := centerPt.GetPoint(NullRectD, GetRelFracLimit);
+  Result := RectD(cp.X -r.X, cp.Y -r.Y, cp.X +r.X, cp.Y +r.X);
+end;
+//------------------------------------------------------------------------------
+
+procedure TEllipseElement.GetPaths(const drawDat: TDrawData);
+var
+  scalePending  : double;
+  rec       : TRectD;
+  path      : TPathD;
+  rad       : TPointD;
+  centPt    : TPointD;
+begin
+  if Assigned(drawPathsC) then inherited;
+  rad := radius.GetPoint(drawDat.bounds, GetRelFracLimit);
+  centPt := centerPt.GetPoint(drawDat.bounds, GetRelFracLimit);
+  with centPt do
+    rec := RectD(X -rad.X, Y -rad.Y, X +rad.X, Y +rad.Y);
+  scalePending := ExtractAvgScaleFromMatrix(drawDat.matrix);
+  path := Ellipse(rec, scalePending);
+  AppendPath(drawPathsC, path);
+  drawPathsF := drawPathsC;
+end;
+
+//------------------------------------------------------------------------------
+// TRectElement
+//------------------------------------------------------------------------------
+
+constructor TRectElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  radius.Init;
+  elRectWH.width.SetValue(100, utPercent);
+  elRectWH.height.SetValue(100, utPercent);
+end;
+//------------------------------------------------------------------------------
+
+function  TRectElement.GetBounds: TRectD;
+begin
+  Result := elRectWH.GetRectD(NullRectD, GetRelFracLimit);
+end;
+//------------------------------------------------------------------------------
+
+procedure TRectElement.GetPaths(const drawDat: TDrawData);
+var
+  radXY : TPointD;
+  bounds: TRectD;
+  path  : TPathD;
+begin
+  if Assigned(drawPathsC) then Exit;
+  if elRectWH.width.HasFontUnits then
+    bounds := elRectWH.GetRectD(drawDat.fontInfo.size, GetRelFracLimit) else
+    bounds := elRectWH.GetRectD(drawDat.bounds, GetRelFracLimit);
+  if bounds.IsEmpty then Exit;
+
+  radXY := radius.GetPoint(bounds, GetRelFracLimit);
+  if (radXY.X > 0) or (radXY.Y > 0) then
+  begin
+    if (radXY.X <= 0) then radXY.X := radXY.Y
+    else if (radXY.Y <= 0) then radXY.Y := radXY.X;
+    path := RoundRect(bounds, radXY);
+  end else
+    path := Rectangle(bounds);
+  AppendPath(drawPathsC, path);
+  drawPathsF := drawPathsC;
+end;
+//------------------------------------------------------------------------------
+
+function TRectElement.GetSimplePath(const drawDat: TDrawData): TPathsD;
+var
+  rec: TRectD;
+begin
+  Result := nil;
+  rec := elRectWH.GetRectD(drawDat.bounds, GetRelFracLimit);
+  if not rec.IsEmpty then
+    AppendPath(Result, Rectangle(rec));
+end;
+
+//------------------------------------------------------------------------------
+// TTextElement
+//------------------------------------------------------------------------------
+
+constructor TTextElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  offset.Init;
+  hasPaths := false;
+end;
+//------------------------------------------------------------------------------
+
+function  TTextElement.LoadContent: Boolean;
+var
+  i       : integer;
+  svgEl   : TSvgTreeEl;
+  elClass : TElementClass;
+  el      : TSvgElement;
+begin
+  Result := false;
+  for i := 0 to fParserEl.childs.Count -1 do
+  begin
+    svgEl := TSvgTreeEl(fParserEl.childs[i]);
+    if svgEl.hash = 0 then
+    begin
+      el := TSubtextElement.Create(self, svgEl);
+      Self.fChilds.Add(el);
+      if svgEl.text <> '' then
+        TSubtextElement(el).text := svgEl.text;
+    end else
+    begin
+      elClass := HashToElementClass(svgEl.hash);
+      if elClass = TSvgElement then Continue;
+      el := elClass.Create(self, svgEl);
+      Self.fChilds.Add(el);
+      el.LoadAttributes;
+      if not el.LoadContent then Exit; //error
+    end;
+  end;
+  Result := true;
+end;
+//------------------------------------------------------------------------------
+
+function TTextElement.GetTopTextElement: TTextElement;
+var
+  el: TSvgElement;
+begin
+  el := self;
+  while Assigned(el.fParent) and (el.fParent is TTextElement) do
+    el := el.fParent;
+  Result := TTextElement(el);
+end;
+//------------------------------------------------------------------------------
+
+procedure TTextElement.DoOffsetX(dx: double);
+var
+  i: integer;
+begin
+  for i := 0 to fChilds.Count -1 do
+    if TSvgElement(fChilds[i]) is TTextElement then
+      TTextElement(fChilds[i]).DoOffsetX(dx)
+    else if TSvgElement(fChilds[i]) is TSubTextElement then
+      with TSubTextElement(fChilds[i]) do
+      begin
+        drawPathsC := OffsetPath(drawPathsC, dx, 0);
+        drawPathsO := OffsetPath(drawPathsO, dx, 0);
+        drawPathsF := OffsetPath(drawPathsF, dx, 0);
+      end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TTextElement.GetPaths(const drawDat: TDrawData);
+var
+  i         : integer;
+  el        : TSvgElement;
+  di        : TDrawData;
+  topTextEl : TTextElement;
+begin
+  di := drawDat;
+  if self <> GetTopTextElement then
+    UpdateDrawInfo(di, self);
+
+  if Self is TTSpanElement then
+  begin
+    el := fParent;
+    while (el is TTSpanElement) do
+      el := el.fParent;
+    if not (el is TTextElement) then Exit; //ie error (eg <textarea>)
+    topTextEl := TTextElement(el);
+
+    if elRectWH.left.IsValid then
+      currentPt.X := elRectWH.left.rawVal else
+      currentPt.X := topTextEl.currentPt.X;
+    if elRectWH.top.IsValid then
+      currentPt.Y := elRectWH.top.rawVal else
+      currentPt.Y := topTextEl.currentPt.Y;
+
+    if offset.X.IsValid then
+      currentPt.X := currentPt.X + offset.X.GetValue(0, 0);
+    if offset.Y.IsValid then
+      currentPt.Y := currentPt.Y + offset.Y.GetValue(0, 0);
+
+    topTextEl.currentPt := currentPt;
+  end else
+  begin
+    if elRectWH.left.IsValid then
+      currentPt.X := elRectWH.left.rawVal else
+      currentPt.X := 0;
+    if elRectWH.top.IsValid then
+      currentPt.Y := elRectWH.top.rawVal else
+      currentPt.Y := 0;
+    startX := currentPt.X;
+  end;
+
+  for i := 0 to fChilds.Count -1 do
+    if TSvgElement(fChilds[i]) is TShapeElement then
+      TShapeElement(fChilds[i]).GetPaths(di);
+end;
+//------------------------------------------------------------------------------
+
+procedure TTextElement.ResetTmpPt;
+begin
+  startX    := 0;
+  currentPt := InvalidPointD;
+end;
+//------------------------------------------------------------------------------
+
+procedure TTextElement.Draw(img: TImage32; drawDat: TDrawData);
+var
+  dx        : double;
+begin
+  if self = GetTopTextElement then
+  begin
+    UpdateDrawInfo(drawDat, self);
+    //get child paths
+    GetPaths(drawDat);
+
+    case drawDat.FontInfo.align of
+      staCenter:
+        begin
+          dx := (currentPt.X - startX) * 0.5;
+          DoOffsetX(-dx);
+        end;
+      staRight:
+        begin
+          dx := (currentPt.X - startX);
+          DoOffsetX(-dx);
+        end;
+    end;
+  end
+  else if (currentPt.X = InvalidD) or
+    (currentPt.Y = InvalidD) then
+      Exit; //probably a <textarea> element
+
+  DrawChildren(img, drawDat);
+end;
+
+//------------------------------------------------------------------------------
+// TSubtextElement
+//------------------------------------------------------------------------------
+
+constructor TSubtextElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  hasPaths := true;
+  fDrawData := fParent.fDrawData;
+  fDrawData.matrix := IdentityMatrix;
+end;
+//------------------------------------------------------------------------------
+
+function FixSpaces(const text: UnicodeString): UnicodeString;
+var
+  i,j, len: integer;
+begin
+  //changes \r\n\t chars to spaces
+  //and trims consecutive spaces
+
+  len  := Length(text);
+  SetLength(Result, len);
+  if len = 0 then Exit;
+
+  if text[1] <= #32 then
+    Result[1] := #32 else
+    Result[1] := text[1];
+
+  j := 1;
+  for i := 2 to len do
+  begin
+    if text[i] <= #32 then
+    begin
+      if Result[j] = #32 then Continue
+      else Result[j+1] := #32;
+    end
+    else
+      Result[j+1] := text[i];
+    inc(j);
+  end;
+  SetLength(Result, j);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSubtextElement.GetPaths(const drawDat: TDrawData);
+var
+  el : TSvgElement;
+  topTextEl : TTextElement;
+  s: UnicodeString;
+  tmpX, offsetX, scale, fontSize, bs: double;
+  mat: TMatrixD;
+begin
+  if Assigned(drawPathsC) then Exit;
+  fReader.GetBestFontForFontCache(drawDat.FontInfo);
+  if drawDat.FontInfo.size = 0 then
+    fontSize := 16 else
+    fontSize := drawDat.FontInfo.size;
+  if (Length(text) = 0) or (fontSize < 2) or
+    not Assigned(fReader.fFontCache) then Exit;
+
+  el := self;
+  while (el.fParent is TTextElement) do
+    el := el.fParent;
+  if not (el is TTextElement) then Exit;
+  topTextEl := TTextElement(el);
+
+  if (topTextEl.currentPt.X = InvalidD) or
+    (topTextEl.currentPt.Y = InvalidD) then Exit;
+
+  //trim CRLFs and multiple spaces
+  {$IFDEF UNICODE}
+  s := UTF8ToUnicodeString(HtmlDecode(text));
+  {$ELSE}
+  s := Utf8Decode(HtmlDecode(text));
+  {$ENDIF}
+  s := FixSpaces(s);
+
+  drawPathsC := fReader.fFontCache.GetTextOutline(0, 0, s, tmpX);
+  //by not changing the fontCache.FontHeight, the quality of
+  //small font render improves very significantly (though of course
+  //this requires additional glyph scaling and offsetting).
+  scale := fontSize / fReader.fFontCache.FontHeight;
+
+  with topTextEl.currentPt do
+  begin
+    offsetX := X;
+    X := X + tmpX * scale;
+  end;
+
+  with drawDat.fontInfo do
+    if baseShift.rawVal = 0 then
+      bs := 0 else
+      bs := baseShift.GetValue(size, GetRelFracLimit);
+
+  mat := IdentityMatrix;
+  MatrixScale(mat, scale);
+  MatrixTranslate(mat, offsetX, topTextEl.currentPt.Y - bs);
+  MatrixApply(mat, drawPathsC);
+  drawPathsF := drawPathsC;
+end;
+
+//------------------------------------------------------------------------------
+// TTSpanElement
+//------------------------------------------------------------------------------
+
+constructor TTSpanElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  fDrawData.FontInfo.decoration := fdUndefined;
+  fDrawData.FontInfo.baseShift.SetValue(0);
+
+  elRectWH.Init;
+  currentPt := InvalidPointD;
+end;
+
+//------------------------------------------------------------------------------
+// TTextPathElement
+//------------------------------------------------------------------------------
+
+procedure TTextPathElement.GetPaths(const drawDat: TDrawData);
+var
+  parentTextEl, topTextEl: TTextElement;
+  el: TSvgElement;
+  isFirst: Boolean;
+  s: UnicodeString;
+  i, len, charsThatFit: integer;
+  d, fontScale, spacing: double;
+  utf8: UTF8String;
+  mat: TMatrixD;
+  tmpPath: TPathD;
+  isClosed: Boolean;
+const
+  dblSpace: UnicodeString = #32#32;
+begin
+  if Assigned(drawPathsC) then Exit;
+  fReader.GetBestFontForFontCache(drawDat.FontInfo);
+  if (drawDat.FontInfo.size < 2) or
+    not Assigned(fReader.fFontCache) then Exit;
+
+  parentTextEl := TTextElement(fParent);
+  topTextEl := parentTextEl;
+  isFirst := IsFirstChild;
+  while topTextEl.fParserEl.hash <> hText do
+  begin
+    isFirst := isFirst and topTextEl.IsFirstChild;
+    topTextEl := TTextElement(topTextEl.fParent);
+  end;
+
+  //if first subtext then reset X offset
+  if not isFirst then Exit;
+  topTextEl.ResetTmpPt;
+  utf8 := '';
+
+  //nb: only exit AFTER setting parentTextEl.tmpPt.
+  if (fParserEl.text = '') then
+  begin
+    if (fChilds.Count = 0) or
+      not (TSvgElement(fChilds[0]) is TTSpanElement) then
+        Exit;
+    el := TSvgElement(fChilds[0]);
+    if (el.fChilds.Count = 0) or
+      not (TSvgElement(el.fChilds[0]) is TSubtextElement) then
+        Exit;
+    with TSubtextElement(el.fChilds[0]) do
+    begin
+      utf8 := text;
+      spacing := fDrawData.FontInfo.spacing;
+    end;
+  end else
+  begin
+    utf8 := fParserEl.text;
+    spacing := drawDat.FontInfo.spacing;
+  end;
+
+  //trim CRLFs and multiple spaces
+  {$IFDEF UNICODE}
+  s := UTF8ToUnicodeString(HtmlDecode(utf8));
+  {$ELSE}
+  s := UnicodeString(Utf8Decode(HtmlDecode(utf8)));
+  {$ENDIF}
+  for i := 1 to Length(s) do
+    if s[i] < #32 then s[i] := #32;
+
+  i := PosEx(dblSpace, s);
+  while i > 0 do
+  begin
+    Delete(s, i, 1);
+    i := PosEx(dblSpace, s, i);
+  end;
+
+  el := FindRefElement(pathEl);
+  if not (el is TPathElement) then Exit;
+  fontScale := drawDat.FontInfo.size/fReader.fFontCache.FontHeight;
+  spacing := spacing /fontScale;
+
+  //adjust glyph spacing when fFontInfo.textLength is assigned.
+  len := Length(s);
+  if (len > 1) and (drawDat.FontInfo.textLength > 0) then
+  begin
+    d := fReader.fFontCache.GetTextWidth(s);
+    spacing := (drawDat.FontInfo.textLength/fontScale) - d;
+    spacing := spacing / (len -1);
+  end;
+
+  with TPathElement(el) do
+  begin
+    mat := fDrawData.matrix;
+    MatrixScale(mat, 1/fontScale);
+    for i := 0 to fSvgPaths.Count -1 do
+    begin
+      Flatten(i, fontScale, tmpPath, isClosed);
+      //'path' is temporarily scaled to accommodate fReader.fFontCache's
+      //static fontheight. The returned glyphs will be de-scaled later.
+      MatrixApply(mat, tmpPath);
+      AppendPath(self.drawPathsC,
+        GetTextOutlineOnPath(s, tmpPath, fReader.fFontCache,
+          taLeft, 0, spacing, charsThatFit));
+      if charsThatFit = Length(s) then Break;
+      Delete(s, 1, charsThatFit);
+    end;
+  end;
+  drawPathsC := ScalePath(drawPathsC, fontScale);
+  drawPathsF := drawPathsC;
+end;
+
+//------------------------------------------------------------------------------
+// TMarkerElement
+//------------------------------------------------------------------------------
+
+constructor TMarkerElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  fDrawData.visible := false;
+end;
+//------------------------------------------------------------------------------
+
+procedure TMarkerElement.Draw(img: TImage32; drawDat: TDrawData);
+var
+  i, len: integer;
+  l,t,w,h,scale, a, a2: double;
+  mat: TMatrixD;
+  angles: TArrayOfDouble;
+begin
+  UpdateDrawInfo(drawDat, self);
+  mat := drawDat.matrix;
+
+  if elRectWH.width.IsValid and elRectWH.height.IsValid and
+    not markerBoxWH.IsEmpty then
+  begin
+    w := elRectWH.width.rawVal;
+    h := elRectWH.height.rawVal;
+    //currently assume preserve aspect ratio
+    scale := Min(w/markerBoxWH.Width, h/markerBoxWH.Height);
+    MatrixScale(mat, scale, scale);
+  end;
+
+  if refPt.X.IsValid and refPt.Y.IsValid then
+  begin
+    l := refPt.X.rawVal;
+    t := refPt.Y.rawVal;
+    scale := ExtractAvgScaleFromMatrix(mat);
+    MatrixTranslate(mat, -l * scale, -t * scale);
+  end;
+
+  len := Length(fPoints);
+  if len = 0 then Exit;
+  SetLength(angles, len);
+  angles[0] := angle;
+  a := angle;
+  for i := 0 to len -2 do
+  begin
+    a2 := GetAngle(fPoints[i], fPoints[i+1]);
+    angles[i] := Average(a, a2);
+    a := a2;
+  end;
+  if len > 1 then
+    angles[len -1] := Average(a, angle2);
+
+  //for each 'point' draw the marker
+  for i := 0 to len -1 do
+  begin
+    drawDat.matrix := mat;
+    MatrixRotate(drawDat.matrix, NullPointD, angles[i]);
+    MatrixTranslate(drawDat.matrix, fPoints[i].X, fPoints[i].Y);
+    DrawChildren(img, drawDat);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TMarkerElement.SetEndPoint(const pt: TPointD; angle: double);
+begin
+  SetLength(fPoints, 1);
+  fPoints[0] := pt;
+  self.angle := angle;
+end;
+//------------------------------------------------------------------------------
+
+function TMarkerElement.SetMiddlePoints(const points: TPathD): Boolean;
+var
+  len: integer;
+begin
+  len := Length(points);
+  Result := len > 2;
+  if Result then
+  begin
+    angle := GetAngle(Points[0],Points[1]);
+    angle2 := GetAngle(Points[len-2],Points[len-1]);
+    Self.fPoints := Copy(points, 1, len -2);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TFillElement
+//------------------------------------------------------------------------------
+
+function TFillElement.GetRelFracLimit: double;
+begin
+  //always assume fractional values below 1 are relative
+  Result := 1.0;
+end;
+
+//------------------------------------------------------------------------------
+// TPatternElement
+//------------------------------------------------------------------------------
+
+constructor TPatternElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited;
+  elRectWH.Init;
+  pattBoxWH.Width   := InvalidD;
+  pattBoxWH.Height  := InvalidD;
+  fDrawData.visible := false;
+end;
+//------------------------------------------------------------------------------
+
+function TPatternElement.PrepareRenderer(renderer: TImageRenderer;
+  drawDat: TDrawData): Boolean;
+var
+  i     : integer;
+  recWH : TRectWH;
+  el    : TSvgElement;
+  rec   : TRectD;
+  mat   : TMatrixD;
+  sx,sy : double;
+  scale: TSizeD;
+begin
+  Result := false;
+
+  scale := ExtractScaleFromMatrix(drawDat.matrix);
+  if units = hUserSpaceOnUse then
+    rec := fReader.userSpaceBounds else
+    rec := drawDat.bounds;
+
+  //todo: implement patternUnits & patternContentUnits too
+
+  sx := 1; sy := 1;
+  if elRectWH.Width.IsValid and elRectWH.Height.IsValid then
+  begin
+    recWH := elRectWH.GetRectWH(rec, GetRelFracLimit);
+
+    //also scale if necessary
+    if not pattBoxWH.IsEmpty then
+    begin
+      sx := recWH.Width/pattBoxWH.Width;
+      sy := recWH.Height/pattBoxWH.Height;
+    end;
+
+  end
+  else if not pattBoxWH.IsEmpty then
+  begin
+    recWH.Width   := pattBoxWH.Width;
+    recWH.Height  := pattBoxWH.Width;
+  end else
+    Exit;
+
+  renderer.Image.SetSize(
+    Round(recWH.Width * scale.cx),
+    Round(recWH.Height * scale.cy));
+
+  Result := true;
+
+  mat := IdentityMatrix;
+  MatrixScale(mat, scale.cx * sx, scale.cy * sy);
+
+  //recWH.Left := 0; recWH.Top := 0;
+  if (refEl <> '') then
+  begin
+    el := FindRefElement(refEl);
+    if Assigned(el) and (el is TShapeElement) then
+      with TShapeElement(el) do
+      begin
+        drawDat := fDrawData;
+        drawDat.matrix := mat;
+        drawDat.bounds := recWH.RectD;
+        Draw(renderer.Image, drawDat);
+      end;
+  end;
+
+  for i := 0 to fChilds.Count -1 do
+    if TSvgElement(fChilds[i]) is TShapeElement then
+      with TShapeElement(fChilds[i]) do
+      begin
+        drawDat := fDrawData;
+        drawDat.matrix := mat;
+        drawDat.bounds := rec;
+        Draw(renderer.Image, drawDat);
+      end;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgRootElement
+//------------------------------------------------------------------------------
+
+constructor TSvgRootElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+  inherited Create(parent, svgEl);
+end;
+
+//------------------------------------------------------------------------------
+// TElement
+//------------------------------------------------------------------------------
+
+constructor TSvgElement.Create(parent: TSvgElement; svgEl: TSvgTreeEl);
+begin
+{$IFDEF XPLAT_GENERICS}
+  fChilds         := TList<TSvgElement>.create;
+{$ELSE}
+  fChilds         := TList.Create;
+{$ENDIF}
+  fParserEl          := svgEl;
+  self.fParent    := parent;
+  fDrawData       := emptyDrawInfo;
+  elRectWH.Init;
+  if Assigned(parent) then
+    fReader := parent.fReader;
+end;
+//------------------------------------------------------------------------------
+
+destructor TSvgElement.Destroy;
+var
+  i: integer;
+begin
+  for i := 0 to fChilds.Count -1 do
+    TSvgElement(fChilds[i]).Free;
+  fChilds.Free;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+function  TSvgElement.IsFirstChild: Boolean;
+begin
+  Result := not Assigned(fParent) or (self = fParent.fChilds[0]);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgElement.Draw(image: TImage32; drawDat: TDrawData);
+begin
+  DrawChildren(image, drawDat);
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgElement.DrawChildren(image: TImage32; drawDat: TDrawData);
+var
+  i: integer;
+begin
+  for i := 0 to fChilds.Count -1 do
+    with TSvgElement(fChilds[i]) do
+      if fDrawData.visible then Draw(image, drawDat);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgElement.GetChildCount: integer;
+begin
+  Result := fChilds.Count;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgElement.FindChild(const idName: UTF8String): TSvgElement;
+var
+  i: integer;
+begin
+  if Match(self.fId, idName) then
+  begin
+    Result := self;
+    Exit;
+  end;
+
+  Result := nil;
+  for i := 0 to ChildCount -1 do
+  begin
+    Result := Child[i].FindChild(idName);
+    if Assigned(Result) then Break;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgElement.GetChild(index: integer): TSvgElement;
+begin
+  if (index < 0) or (index >= fChilds.count) then
+    Result := nil else
+    Result := TSvgElement(fChilds[index]);
+end;
+//------------------------------------------------------------------------------
+
+function TSvgElement.FindRefElement(refname: UTF8String): TSvgElement;
+var
+  i, len: integer;
+  c, endC: PUTF8Char;
+  ref: UTF8String;
+begin
+  result := nil;
+  len := Length(refname);
+  if len = 0 then Exit;
+  c := PUTF8Char(refname);
+  endC := c + len;
+  if Match(c, 'url(') then
+  begin
+    inc(c, 4);
+    dec(endC); //removes trailing ')'
+  end;
+  if c^ = '#' then inc(c);
+  ref := ToUTF8String(c, endC);
+  i := fReader.fIdList.IndexOf(string(ref));
+  if i >= 0 then
+    Result := TSvgElement(fReader.fIdList.Objects[i]) else
+    Result := nil;
+end;
+
+//------------------------------------------------------------------------------
+// dozens of function to process various element attributes
+//------------------------------------------------------------------------------
+
+procedure Id_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  aOwnerEl.fId := value;
+  aOwnerEl.fReader.fIdList.AddObject(string(value), aOwnerEl);
+end;
+//------------------------------------------------------------------------------
+
+procedure In_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if aOwnerEl is TFeBaseElement then
+    TFeBaseElement(aOwnerEl).in1 := value;
+end;
+//------------------------------------------------------------------------------
+
+procedure In2_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if aOwnerEl is TFeBaseElement then
+    TFeBaseElement(aOwnerEl).in2 := value;
+end;
+//------------------------------------------------------------------------------
+
+procedure LetterSpacing_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  with TTextElement(aOwnerEl) do
+    UTF8StringToFloat(value, fDrawData.FontInfo.spacing);
+end;
+//------------------------------------------------------------------------------
+
+procedure Href_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  el: TSvgElement;
+begin
+  el := aOwnerEl;
+  case el.fParserEl.Hash of
+    hUse:
+      TUseElement(el).refEl := ExtractRef(value);
+    hTextPath:
+      TTextPathElement(el).pathEl := ExtractRef(value);
+    else if el is TFillElement then
+      TFillElement(el).refEl := ExtractRef(value);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure BaselineShift_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+  word: UTF8String;
+  c, endC: PUTF8Char;
+begin
+  c := PUTF8Char(value);
+  endC := c + Length(value);
+  ParseNextWord(c, endC, word);
+  with aOwnerEl.fDrawData.FontInfo do
+    case GetHash(word) of
+      hSuper: baseShift.SetValue(50, utPercent);
+      hSub: baseShift.SetValue(-50, utPercent);
+      hBaseline: baseShift.SetValue(0, utPixel);
+      else
+      begin
+        UTF8StringToFloatEx(value, val, mu);
+        baseShift.SetValue(val, mu);
+      end;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Color_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  color: TColor32;
+begin
+  color := clInvalid;
+  UTF8StringToColor32(value, color);
+  //for setting currentcolor when drawing (eg drawing shapes)
+  aOwnerEl.fDrawData.currentColor := color;
+  //for setting currentcolor during element creation (eg gradient colors)
+  aOwnerEl.fReader.currentColor := color;
+end;
+//------------------------------------------------------------------------------
+
+procedure LightingColor_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  color: TColor32;
+begin
+  color := clInvalid;
+  UTF8StringToColor32(value, color);
+  if (aOwnerEl is TFeSpecLightElement) then
+    TFeSpecLightElement(aOwnerEl).color := color
+  else if (aOwnerEl is TFeDefuseLightElement) then
+    TFeDefuseLightElement(aOwnerEl).color := color
+end;
+//------------------------------------------------------------------------------
+
+procedure ClipPath_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  aOwnerEl.fDrawData.clipElRef := ExtractRef(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure D_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if aOwnerEl is TPathElement then
+    TPathElement(aOwnerEl).ParseDAttrib(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure Fill_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  case aOwnerEl.fParserEl.Hash of
+    hfeDropShadow:
+      UTF8StringToColor32(value, TFeDropShadowElement(aOwnerEl).floodColor);
+    hfeFlood:
+      UTF8StringToColor32(value, TFeFloodElement(aOwnerEl).floodColor);
+    else
+    begin
+      if Match(PUTF8Char(value), 'url(') then
+        aOwnerEl.fDrawData.fillEl := ExtractRef(value)
+      else
+        UTF8StringToColor32(value, aOwnerEl.fDrawData.fillColor);
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure FillOpacity_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  val: double;
+begin
+  case aOwnerEl.fParserEl.Hash of
+    hfeDropShadow:
+      UTF8StringToOpacity(value, TFeDropShadowElement(aOwnerEl).floodColor);
+    hfeFlood:
+      UTF8StringToOpacity(value, TFeFloodElement(aOwnerEl).floodColor);
+    else
+    begin
+      UTF8StringToFloat(value, val);
+      aOwnerEl.fDrawData.fillOpacity := ClampRange(val, 0,1);
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DashArray_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  c, endC: PUTF8Char;
+  val: double;
+  len: integer;
+begin
+  c := PUTF8Char(value);
+  endC := c + Length(value);
+  with aOwnerEl.fDrawData do
+  begin
+    len := Length(dashArray);
+    while ParseNextNum(c, endC, true, val) do
+    begin
+      SetLength(dashArray, len +1);
+      dashArray[len] := val;
+      inc(len);
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure DashOffset_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  c, endC: PUTF8Char;
+begin
+  c := PUTF8Char(value);
+  endC := c + Length(value);
+  with aOwnerEl.fDrawData do
+    ParseNextNum(c, endC, true, dashOffset);
+end;
+//------------------------------------------------------------------------------
+
+procedure Display_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if GetHash(value) = hNone then
+    aOwnerEl.fDrawData.visible := false;
+end;
+//------------------------------------------------------------------------------
+
+procedure Font_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  GetSvgFontInfo(value, aOwnerEl.fDrawData.FontInfo);
+end;
+//------------------------------------------------------------------------------
+
+procedure FontFamily_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  word: UTF8String;
+  c, endC: PUTF8Char;
+begin
+  with aOwnerEl.fDrawData.FontInfo do
+  begin
+    family := ttfUnknown;
+    c := PUTF8Char(value);
+    endC := c + Length(value);
+    while ParseNextWordEx(c, endC, word) do
+    begin
+      case GetHash(word) of
+        hSans_045_Serif, hArial  : family := ttfSansSerif;
+        hSerif, hTimes: family := ttfSerif;
+        hMonospace: family := ttfMonospace;
+        else Continue;
+      end;
+      break;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure FontSize_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  num: double;
+  c, endC: PUTF8Char;
+begin
+  c := PUTF8Char(value); endC := c + Length(value);
+  if not ParseNextNum(c, endC, false, num) then Exit;
+  aOwnerEl.fDrawData.FontInfo.size := num;
+end;
+//------------------------------------------------------------------------------
+
+procedure FontStyle_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  with aOwnerEl.fDrawData.FontInfo do
+    if GetHash(value) = hItalic then
+      italic := sfsItalic else
+      italic := sfsNone;
+end;
+//------------------------------------------------------------------------------
+
+procedure FontWeight_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+
+var
+  num: double;
+  word: UTF8String;
+  c, endC: PUTF8Char;
+begin
+  c := PUTF8Char(value);
+  endC := c + Length(value);
+  with aOwnerEl.fDrawData.FontInfo do
+  begin
+    if IsNumPending(c, endC, false) and
+      ParseNextNum(c, endC, false, num) then
+        weight := Round(num)
+    else if ParseNextWord(c, endC, word) then
+      case GetHash(word) of
+        hBold   : weight := 600;
+        hNormal : weight := 400;
+        hBolder : if weight >= 0 then weight := Min(900, weight + 200)
+                  else weight := 600;
+        hLighter: if weight >= 0 then weight := Max(0, weight - 200)
+                  else weight := 200;
+
+      end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Fx_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if (aOwnerEl is TRadGradElement) then
+    with TRadGradElement(aOwnerEl) do
+    begin
+      UTF8StringToFloatEx(value, F.X.rawVal, F.X.unitType);
+    end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Fy_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if (aOwnerEl is TRadGradElement) then
+    with TRadGradElement(aOwnerEl) do
+    begin
+      UTF8StringToFloatEx(value, F.Y.rawVal, F.Y.unitType);
+    end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TextAlign_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  with aOwnerEl.fDrawData.FontInfo do
+    case GetHash(value) of
+      hMiddle : align := staCenter;
+      hEnd    : align := staRight;
+      else align := staLeft;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TextDecoration_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  with aOwnerEl.fDrawData.FontInfo do
+    case GetHash(value) of
+      hUnderline        : decoration := fdUnderline;
+      hline_045_through : decoration := fdStrikeThrough;
+      else                decoration := fdNone;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TextLength_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  UTF8StringToFloat(value, aOwnerEl.fDrawData.FontInfo.textLength);
+end;
+//------------------------------------------------------------------------------
+
+
+procedure MarkerStart_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if not (aOwnerEl is TShapeElement) then Exit;
+  aOwnerEl.fDrawData.markerStart := ExtractRef(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure MarkerMiddle_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if not (aOwnerEl is TShapeElement) then Exit;
+  aOwnerEl.fDrawData.markerMiddle := ExtractRef(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure MarkerEnd_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if not (aOwnerEl is TShapeElement) then Exit;
+  aOwnerEl.fDrawData.markerEnd := ExtractRef(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure Filter_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if (aOwnerEl is TShapeElement) then
+    aOwnerEl.fDrawData.filterElRef := ExtractRef(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure Mask_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if (aOwnerEl is TShapeElement) then
+    aOwnerEl.fDrawData.maskElRef := ExtractRef(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure Offset_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  val: TValue;
+begin
+  if (aOwnerEl is TGradStopElement) then
+    with TGradStopElement(aOwnerEl) do
+    begin
+      val.Init;
+      UTF8StringToFloatEx(value, val.rawVal, val.unitType);
+      offset := val.GetValue(1, GetRelFracLimit);
+    end
+end;
+//------------------------------------------------------------------------------
+
+procedure Opacity_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  opacity: double;
+begin
+  if not UTF8StringToFloat(value, opacity) then Exit;
+  if opacity < 0 then opacity := 0
+  else if opacity > 1 then opacity := 1;
+  aOwnerEl.fDrawData.opacity := Round(opacity * 255);
+end;
+//------------------------------------------------------------------------------
+
+procedure Operator_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if (aOwnerEl is TFeCompositeElement) then
+    with TFeCompositeElement(aOwnerEl) do
+      case GetHash(value) of
+        hAtop       : compositeOp := coAtop;
+        hIn         : compositeOp := coIn;
+        hOut        : compositeOp := coOut;
+        hOver       : compositeOp := coOver;
+        hXor        : compositeOp := coXor;
+        hArithmetic : compositeOp := coArithmetic;
+      end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Orient_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if (aOwnerEl is TMarkerElement) and
+    (GetHash(value) = hauto_045_start_045_reverse) then
+        TMarkerElement(aOwnerEl).autoStartReverse := true;
+end;
+//------------------------------------------------------------------------------
+
+procedure StopColor_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  acolor: TColor32;
+begin
+  if aOwnerEl is TGradStopElement then
+  begin
+    acolor := clInvalid;
+    UTF8StringToColor32(value, acolor);
+    with TGradStopElement(aOwnerEl) do
+      if acolor = clCurrent then
+        color := aOwnerEl.fReader.currentColor else
+        color := acolor;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure StopOpacity_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if aOwnerEl is TGradStopElement then
+  UTF8StringToOpacity(value, TGradStopElement(aOwnerEl).color);
+end;
+//------------------------------------------------------------------------------
+
+procedure Points_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if aOwnerEl is TPolyElement then
+    TPolyElement(aOwnerEl).ParsePoints(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure Stroke_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if Match(PUTF8Char(value), 'url(') then
+    aOwnerEl.fDrawData.strokeEl := ExtractRef(value)
+  else
+    UTF8StringToColor32(value, aOwnerEl.fDrawData.strokeColor);
+end;
+//------------------------------------------------------------------------------
+
+procedure StrokeLineCap_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  word: UTF8String;
+  c, endC: PUTF8Char;
+begin
+  c := PUTF8Char(value);
+  endC := c + Length(value);
+  ParseNextWord(c, endC, word);
+  with aOwnerEl.fDrawData do
+    case GetHash(word) of
+      hButt   : strokeCap := esButt;
+      hRound  : strokeCap := esRound;
+      hSquare : strokeCap := esSquare;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+procedure StrokeLineJoin_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  word: UTF8String;
+  c, endC: PUTF8Char;
+begin
+  c := PUTF8Char(value);
+  endC := c + Length(value);
+  ParseNextWord(c, endC, word);
+  with aOwnerEl.fDrawData do
+    case GetHash(word) of
+      hMiter  : strokeJoin := jsMiter;
+      hRound  : strokeJoin := jsRound;
+      hBevel  : strokeJoin := jsSquare;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+procedure StrokeMiterLimit_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  UTF8StringToFloat(value, aOwnerEl.fDrawData.strokeMitLim);
+end;
+//------------------------------------------------------------------------------
+
+procedure StrokeOpacity_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  val: double;
+begin
+  UTF8StringToFloat(value, val);
+  aOwnerEl.fDrawData.strokeOpacity := ClampRange(val, 0,1);
+end;
+//------------------------------------------------------------------------------
+
+procedure StrokeWidth_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  with aOwnerEl do
+  begin
+    UTF8StringToFloatEx(value, fDrawData.strokewidth.rawVal,
+      fDrawData.strokewidth.unitType);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure FillRule_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if LowerCaseTable[value[1]] = 'e' then
+    aOwnerEl.fDrawData.fillRule := frEvenOdd else
+    aOwnerEl.fDrawData.fillRule := frNonZero;
+end;
+//------------------------------------------------------------------------------
+
+procedure Transform_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  with aOwnerEl.fDrawData do
+    matrix := MatrixMultiply(matrix, ParseTransform(value));
+end;
+//------------------------------------------------------------------------------
+
+procedure Values_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  cnt: integer;
+  c, endC: PUTF8Char;
+begin
+  if aOwnerEl is TFeColorMatrixElement then
+    with TFeColorMatrixElement(aOwnerEl) do
+    begin
+      SetLength(values, 20);
+      c := PUTF8Char(value);
+      endC := c + Length(value);
+      cnt := 0;
+      while (cnt < 20) and ParseNextNum(c, endC, true, values[cnt]) do
+        inc(cnt);
+      if cnt < 20 then values := nil;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+procedure GradientTransform_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mat: TMatrixD;
+begin
+  if not (aOwnerEl is TGradientElement) then Exit;
+  mat := ParseTransform(value);
+  with aOwnerEl.fDrawData do
+    matrix := MatrixMultiply(matrix, mat);
+end;
+//------------------------------------------------------------------------------
+
+procedure GradientUnits_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if aOwnerEl is TFillElement then
+    with TFillElement(aOwnerEl) do
+      units := GetHash(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure Viewbox_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+
+  function LoadViewbox: TRectWH;
+  var
+    c, endC: PUTF8Char;
+  begin
+    c := PUTF8Char(value);
+    endC := c + Length(value);
+    with Result do
+      if not ParseNextNum(c, endC, false, Left) or
+        not ParseNextNum(c, endC, true, Top) or
+        not ParseNextNum(c, endC, true, Width) or
+        not ParseNextNum(c, endC, true, Height) then
+          Result := RectWH(0,0,0,0);
+  end;
+
+begin
+  case aOwnerEl.fParserEl.Hash of
+    hSvg    : TSvgRootElement(aOwnerEl).viewboxWH := LoadViewbox;
+    hMarker : TMarkerElement(aOwnerEl).markerBoxWH := LoadViewbox;
+    hSymbol : TSymbolElement(aOwnerEl).viewboxWH := LoadViewbox;
+    else if aOwnerEl is TPatternElement then
+      TPatternElement(aOwnerEl).pattBoxWH := LoadViewbox;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Height_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  with aOwnerEl do
+  begin
+    elRectWH.height.SetValue(val, mu);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Width_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  with aOwnerEl do
+  begin
+    elRectWH.width.SetValue(val, mu);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Cx_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  case aOwnerEl.fParserEl.Hash of
+    hCircle:
+      with TCircleElement(aOwnerEl) do centerPt.X.SetValue(val, mu);
+    hEllipse:
+      with TEllipseElement(aOwnerEl) do centerPt.X.SetValue(val, mu);
+    hRadialGradient:
+      with TRadGradElement(aOwnerEl) do
+      begin
+        C.X.SetValue(val, mu);
+      end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Cy_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  case aOwnerEl.fParserEl.Hash of
+    hCircle:
+      with TCircleElement(aOwnerEl) do centerPt.Y.SetValue(val, mu);
+    hEllipse:
+      with TEllipseElement(aOwnerEl) do centerPt.Y.SetValue(val, mu);
+    hRadialGradient:
+      with TRadGradElement(aOwnerEl) do
+      begin
+        C.Y.SetValue(val, mu);
+      end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Dx_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  case aOwnerEl.fParserEl.Hash of
+    hfeDropShadow:
+      TFeDropShadowElement(aOwnerEl).offset.X.SetValue(val, mu);
+    hfeOffset:
+      TFeOffsetElement(aOwnerEl).offset.X.SetValue(val, mu);
+    hText, hTSpan:
+      TTextElement(aOwnerEl).offset.X.SetValue(val, mu);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Dy_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  case aOwnerEl.fParserEl.Hash of
+    hfeDropShadow:
+      TFeDropShadowElement(aOwnerEl).offset.Y.SetValue(val, mu);
+    hfeOffset:
+      TFeOffsetElement(aOwnerEl).offset.Y.SetValue(val, mu);
+    hText, hTSpan:
+      TTextElement(aOwnerEl).offset.Y.SetValue(val, mu);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Result_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+begin
+  if (aOwnerEl is TFeBaseElement) then
+    TFeBaseElement(aOwnerEl).res := ExtractRef(value);
+end;
+//------------------------------------------------------------------------------
+
+procedure Rx_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  case aOwnerEl.fParserEl.Hash of
+    hRect:
+      with TRectElement(aOwnerEl) do
+      begin
+        radius.X.SetValue(val, mu);
+      end;
+    hCircle:
+      with TCircleElement(aOwnerEl) do
+      begin
+        radius.SetValue(val, mu);
+      end;
+    hEllipse:
+      with TEllipseElement(aOwnerEl) do
+      begin
+        radius.X.SetValue(val, mu);
+      end;
+    hRadialGradient:
+      with TRadGradElement(aOwnerEl) do
+      begin
+        radius.X. SetValue(val, mu);
+        radius.Y. SetValue(val, mu);
+      end;
+    hMarker:
+      with TMarkerElement(aOwnerEl) do
+        refPt.X.SetValue(val, mu);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Ry_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  case aOwnerEl.fParserEl.Hash of
+    hRect:
+      with TRectElement(aOwnerEl) do
+      begin
+        radius.Y.SetValue(val, mu);
+      end;
+    hEllipse:
+      with TEllipseElement(aOwnerEl) do
+      begin
+        radius.Y.SetValue(val, mu);
+      end;
+    hMarker:
+      with TMarkerElement(aOwnerEl) do refPt.Y.SetValue(val, mu);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure SpreadMethod_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  word: UTF8String;
+  c, endC: PUTF8Char;
+begin
+  if not (aOwnerEl is TGradientElement) then Exit;
+  c := PUTF8Char(value);
+  endC := c + Length(value);
+  ParseNextWord(c, endC, word);
+  with TGradientElement(aOwnerEl) do
+    case GetHash(word) of
+      hPad      : spreadMethod := gfsClamp;
+      hReflect  : spreadMethod := gfsMirror;
+      hRepeat   : spreadMethod := gfsRepeat;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+procedure SpectacularExponent(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  se: double;
+begin
+  if not (aOwnerEl is TFeSpecLightElement) then Exit;
+  UTF8StringToFloat(value, se);
+  if (se > 0) and (se < 100) then
+    TFeSpecLightElement(aOwnerEl).exponent := se;
+end;
+//------------------------------------------------------------------------------
+
+procedure StdDev_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  sd: double;
+begin
+  UTF8StringToFloat(value, sd);
+  if (sd < 0) and (sd > 100) then Exit;
+  case aOwnerEl.fParserEl.Hash of
+    hfeGaussianBlur:
+      TFeGaussElement(aOwnerEl).stdDev := sd;
+    hfeDropShadow:
+      TFeDropShadowElement(aOwnerEl).stdDev := sd;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure K1_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  val: double;
+begin
+  UTF8StringToFloat(value, val);
+  if aOwnerEl is TFeCompositeElement then
+    TFeCompositeElement(aOwnerEl).fourKs[0] := val;
+end;
+//------------------------------------------------------------------------------
+
+procedure K2_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  val: double;
+begin
+  UTF8StringToFloat(value, val);
+  if aOwnerEl is TFeCompositeElement then
+    TFeCompositeElement(aOwnerEl).fourKs[1] := val;
+end;
+//------------------------------------------------------------------------------
+
+procedure K3_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  val: double;
+begin
+  UTF8StringToFloat(value, val);
+  if aOwnerEl is TFeCompositeElement then
+    TFeCompositeElement(aOwnerEl).fourKs[2] := val;
+end;
+//------------------------------------------------------------------------------
+
+procedure K4_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  val: double;
+begin
+  UTF8StringToFloat(value, val);
+  if aOwnerEl is TFeCompositeElement then
+    TFeCompositeElement(aOwnerEl).fourKs[3] := val;
+end;
+//------------------------------------------------------------------------------
+
+procedure X1_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  case aOwnerEl.fParserEl.Hash of
+    hLine:
+      TLineElement(aOwnerEl).path[0].X := val;
+    hLinearGradient:
+      with TLinGradElement(aOwnerEl) do
+      begin
+        startPt.X.SetValue(val, mu);
+      end;
+    hFilter:
+      with aOwnerEl do
+      begin
+        elRectWH.left.SetValue(val, mu);
+      end;
+    else
+      aOwnerEl.elRectWH.left.SetValue(val, mu);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure X2_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  case aOwnerEl.fParserEl.Hash of
+    hLine:
+      TLineElement(aOwnerEl).path[1].X := val;
+    hLinearGradient:
+      with TLinGradElement(aOwnerEl) do
+      begin
+        endPt.X.SetValue(val, mu);
+      end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Y1_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  case aOwnerEl.fParserEl.Hash of
+    hLine:
+      TLineElement(aOwnerEl).path[0].Y := val;
+    hLinearGradient:
+      with TLinGradElement(aOwnerEl) do
+      begin
+        startPt.Y.SetValue(val, mu);
+      end;
+    hFilter:
+      with aOwnerEl do
+      begin
+        elRectWH.top.SetValue(val, mu);
+      end;
+    else
+      aOwnerEl.elRectWH.top.SetValue(val, mu);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Y2_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  mu: TUnitType;
+  val: double;
+begin
+  UTF8StringToFloatEx(value, val, mu);
+  case aOwnerEl.fParserEl.Hash of
+    hLine:
+      TLineElement(aOwnerEl).path[1].Y := val;
+    hLinearGradient:
+      with TLinGradElement(aOwnerEl) do
+      begin
+        endPt.Y.SetValue(val, mu);
+      end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure Z_Attrib(aOwnerEl: TSvgElement; const value: UTF8String);
+var
+  val: double;
+begin
+  UTF8StringToFloat(value, val);
+  if aOwnerEl is TFePointLightElement then
+    TFePointLightElement(aOwnerEl).z := val;
+end;
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+procedure TSvgElement.LoadAttribute(attrib: PSvgAttrib);
+begin
+  with attrib^ do
+  case hash of
+    hbaseline_045_shift:    BaselineShift_Attrib(self, value);
+    hColor:                 Color_Attrib(self, value);
+    hClip_045_path:         ClipPath_Attrib(self, value);
+    hCx:                    Cx_Attrib(self, value);
+    hCy:                    Cy_Attrib(self, value);
+    hD:                     D_Attrib(self, value);
+    hDisplay:               Display_Attrib(self, value);
+    hDx:                    Dx_Attrib(self, value);
+    hDy:                    Dy_Attrib(self, value);
+    hStroke_045_DashArray:  DashArray_Attrib(self, value);
+    hStroke_045_DashOffset: DashOffset_Attrib(self, value);
+    hFill:                  Fill_Attrib(self, value);
+    hFill_045_Opacity:      FillOpacity_Attrib(self, value);
+    hFill_045_Rule:         FillRule_Attrib(self, value);
+    hFilter:                Filter_Attrib(self, value);
+    hflood_045_color:       Fill_Attrib(self, value);
+    hflood_045_opacity:     FillOpacity_Attrib(self, value);
+    hFont:                  Font_Attrib(self, value);
+    hFont_045_Family:       FontFamily_Attrib(self, value);
+    hFont_045_Size:         FontSize_Attrib(self, value);
+    hFont_045_Style:        FontStyle_Attrib(self, value);
+    hFont_045_Weight:       FontWeight_Attrib(self, value);
+    hFx:                    Fx_Attrib(self, value);
+    hFy:                    Fy_Attrib(self, value);
+    hGradientTransform:     GradientTransform_Attrib(self, value);
+    hGradientUnits:         GradientUnits_Attrib(self, value);
+    hHeight:                Height_Attrib(self, value);
+    hHref:                  Href_Attrib(self, value);
+    hId:                    Id_Attrib(self, value);
+    hIn:                    In_Attrib(self, value);
+    hIn2:                   In2_Attrib(self, value);
+    hk1:                    K1_Attrib(self, value);
+    hk2:                    K2_Attrib(self, value);
+    hk3:                    K3_Attrib(self, value);
+    hk4:                    K4_Attrib(self, value);
+    hletter_045_spacing:    LetterSpacing_Attrib(self, value);
+//    hlighting_045_color:    LightingColor_Attrib(self, value);
+    hMarker_045_End:        MarkerEnd_Attrib(self, value);
+    hMarkerHeight:          Height_Attrib(self, value);
+    hMarker_045_Mid:        MarkerMiddle_Attrib(self, value);
+    hMarker_045_Start:      MarkerStart_Attrib(self, value);
+    hMarkerWidth:           Width_Attrib(self, value);
+    hMask:                  Mask_Attrib(self, value);
+    hOffset:                Offset_Attrib(self, value);
+    hOpacity:               Opacity_Attrib(self, value);
+    hOperator:              Operator_Attrib(self, value);
+    hOrient:                Orient_Attrib(self, value);
+    hPatternUnits:          GradientUnits_Attrib(self, value);
+    hPatternTransform:      Transform_Attrib(self, value);
+    hPoints:                Points_Attrib(self, value);
+    hR:                     Rx_Attrib(self, value);
+    hRefX:                  Rx_Attrib(self, value);
+    hRefY:                  Ry_Attrib(self, value);
+    hResult:                Result_Attrib(self, value);
+    hRx:                    Rx_Attrib(self, value);
+    hRy:                    Ry_Attrib(self, value);
+    hspecularExponent:      SpectacularExponent(self, value);
+    hSpreadMethod:          SpreadMethod_Attrib(self, value);
+    hstdDeviation:          StdDev_Attrib(self, value);
+    hStop_045_Color:        StopColor_Attrib(self, value);
+    hStop_045_Opacity:      StopOpacity_Attrib(self, value);
+    hStroke:                Stroke_Attrib(self, value);
+    hstroke_045_linecap:    StrokeLineCap_Attrib(self, value);
+    hstroke_045_linejoin:   StrokeLineJoin_Attrib(self, value);
+    hstroke_045_miterlimit: StrokeMiterLimit_Attrib(self, value);
+    hStroke_045_Opacity:    StrokeOpacity_Attrib(self, value);
+    hStroke_045_Width:      StrokeWidth_Attrib(self, value);
+    hText_045_Anchor:       TextAlign_Attrib(self, value);
+    hText_045_Decoration:   TextDecoration_Attrib(self, value);
+    hTextLength:            TextLength_Attrib(self, value);
+    hTransform:             Transform_Attrib(self, value);
+    hValues:                Values_Attrib(self, value);
+    hViewbox:               Viewbox_Attrib(self, value);
+    hWidth:                 Width_Attrib(self, value);
+    hX:                     X1_Attrib(self, value);
+    hX1:                    X1_Attrib(self, value);
+    hX2:                    X2_Attrib(self, value);
+    hXlink_058_Href:        Href_Attrib(self, value);
+    hY:                     Y1_Attrib(self, value);
+    hY1:                    Y1_Attrib(self, value);
+    hY2:                    Y2_Attrib(self, value);
+    hZ:                     Z_Attrib(self, value);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgElement.LoadAttributes;
+var
+  i: integer;
+begin
+  for i := 0 to fParserEl.AttribCount -1 do
+    LoadAttribute(PSvgAttrib(fParserEl.attrib[i]));
+end;
+//------------------------------------------------------------------------------
+
+function PreferRelativeFraction(val: TValue): TTriState;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  if (val.rawVal = InvalidD) or (val.unitType = utUnknown) then
+    Result := tsUnknown
+  else if val.unitType = utPercent then Result := tsYes
+  else if val.unitType <> utNumber then Result := tsNo
+  else if (Abs(val.rawVal) < 1) then Result := tsYes
+  else Result := tsNo;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgElement.GetRelFracLimit: double;
+begin
+  //the default behaviour here is to assume untyped fractional values
+  //below 1.0 are values relative (to the bounding size) BUT ONLY WHEN
+  //the parent element's width or height are relative (ie percentages).
+  if Assigned(fParent) and (fParent.fParserEl.hash <> hSvg) then
+  begin
+    case PreferRelativeFraction(fParent.elRectWH.width) of
+      tsYes: begin Result := 1.0; Exit; end;
+      tsNo : begin Result := 0; Exit; end;
+    end;
+    case PreferRelativeFraction(fParent.elRectWH.height) of
+      tsYes: begin Result := 1.0; Exit; end;
+      tsNo : begin Result := 0; Exit; end;
+    end;
+  end;
+  Result := 0;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgElement.LoadContent: Boolean;
+var
+  i       : integer;
+  svgEl   : TSvgTreeEl;
+  elClass : TElementClass;
+  el      : TSvgElement;
+begin
+  Result := false;
+  for i := 0 to fParserEl.childs.Count -1 do
+  begin
+    svgEl := TSvgTreeEl(fParserEl.childs[i]);
+    if svgEl.hash = 0 then
+      Continue;
+    elClass := HashToElementClass(svgEl.hash);
+    el := elClass.Create(self, svgEl);
+    Self.fChilds.Add(el);
+    el.LoadAttributes;
+    if el.fParserEl.childs.Count = 0 then Continue
+    else if not el.LoadContent then Exit; //error
+  end;
+  Result := true;
+end;
+
+//------------------------------------------------------------------------------
+// TSvgReader
+//------------------------------------------------------------------------------
+
+constructor TSvgReader.Create;
+begin
+  fSvgParser        := TSvgParser.Create;
+  fClassStyles        := TClassStylesList.Create;
+  fLinGradRenderer  := TLinearGradientRenderer.Create;
+  fRadGradRenderer  := TSvgRadialGradientRenderer.Create;
+  fImgRenderer      := TImageRenderer.Create;
+
+  fIdList             := TStringList.Create;
+  fIdList.Duplicates  := dupIgnore;
+  fIdList.CaseSensitive := false;
+  fIdList.Sorted      := True;
+
+  fBlurQuality        := 1; //0: draft (faster); 1: good; 2: excellent (slow)
+  currentColor        := clBlack32;
+  fUsePropScale       := true;
+end;
+//------------------------------------------------------------------------------
+
+destructor TSvgReader.Destroy;
+begin
+  Clear;
+  fSvgParser.Free;
+  fIdList.Free;
+  fClassStyles.Free;
+
+  fLinGradRenderer.Free;
+  fRadGradRenderer.Free;
+  fImgRenderer.Free;
+  FreeAndNil(fFontCache);
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgReader.Clear;
+begin
+  FreeAndNil(fRootElement);
+  fSvgParser.Clear;
+  fIdList.Clear;
+  fClassStyles.Clear;
+  fLinGradRenderer.Clear;
+  fRadGradRenderer.Clear;
+  fImgRenderer.Image.Clear;
+  currentColor := clBlack32;
+  userSpaceBounds := NullRectD;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgReader.GetViewbox(containerWidth, containerHeight: integer): TRectWH;
+begin
+  if not Assigned(RootElement) then
+  begin
+    Result := RectWH(0,0,0,0);
+    Exit;
+  end;
+
+  with RootElement do
+  begin
+    Result.Left := 0;
+    Result.Top := 0;
+    Result.Width := elRectWH.width.GetValue(containerWidth, 0);
+    Result.Height := elRectWH.height.GetValue(containerHeight, 0);
+
+    if viewboxWH.IsEmpty then
+    begin
+      if Result.IsEmpty  then
+        Result := RectWH(0, 0,containerWidth, containerHeight);
+      viewboxWH := Result;
+    end else if Result.IsEmpty then
+      Result := viewboxWH;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgReader.DrawImage(img: TImage32; scaleToImage: Boolean);
+var
+  scale, scale2: double;
+  vbox: TRectWH;
+  di: TDrawData;
+begin
+  if not Assigned(fRootElement) or not assigned(img) then Exit;
+  vbox := GetViewbox(img.Width, img.Height);
+  if vbox.IsEmpty then Exit;
+  fBackgndImage := img;
+
+  with fRootElement do
+  begin
+    di := fDrawData;
+    if di.currentColor = clInvalid then
+      di.currentColor := currentColor;
+
+    MatrixTranslate(di.matrix, -viewboxWH.Left, -viewboxWH.Top);
+
+    //the width and height attributes generally indicate the size of the
+    //rendered image unless they are percentage values. Nevertheless, these
+    //values can be still overridden by the scaleToImage parameter above
+
+    if vbox.IsEmpty then
+      di.bounds := RectD(img.Bounds) else
+      di.bounds := viewboxWH.RectD;
+    userSpaceBounds  := fDrawData.bounds;
+
+    if scaleToImage and not img.IsEmpty then
+    begin
+      //nb: the calculated vbox.width and vbox.height are ignored here since
+      //we're scaling the SVG image to the display image. However we still
+      //need to call GetViewbox (above) to make sure that viewboxWH is filled.
+
+      scale := img.width / viewboxWH.Width;
+      scale2 := img.height / viewboxWH.Height;
+      if fUsePropScale then
+      begin
+        if scale2 < scale then scale := scale2
+        else scale2 := scale;
+      end;
+      MatrixScale(di.matrix, scale, scale2);
+      img.SetSize(
+        Round(viewboxWH.Width * scale),
+        Round(viewboxWH.Height * scale2));
+    end else
+    begin
+      img.SetSize(Round(vbox.Width), Round(vbox.Height));
+      scale := vbox.Width / viewboxWH.Width;
+      scale2 := vbox.Height / viewboxWH.Height;
+      MatrixScale(di.matrix, scale, scale2);
+    end;
+  end;
+
+  if fBkgndColor <> clNone32 then
+    img.Clear(fBkgndColor);
+
+  img.BeginUpdate;
+  fTempImage := TImage32.Create(img.Width, img.Height);
+  try
+    fTempImage.BlockNotify;
+    fRootElement.Draw(img, di);
+  finally
+    fTempImage.Free;
+    img.EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgReader.LoadInternal: Boolean;
+begin
+  Result := false;
+  if not Assigned(fSvgParser.svgTree) or
+    (fSvgParser.svgTree.hash <> hSvg) then Exit;
+  fRootElement := TSvgRootElement.Create(nil, fSvgParser.svgTree);
+  fRootElement.fReader := self;
+  fRootElement.LoadAttributes;
+  Result := fRootElement.LoadContent;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgReader.LoadFromFile(const filename: string): Boolean;
+begin
+  Clear;
+  Result := fSvgParser.LoadFromFile(filename) and LoadInternal;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgReader.LoadFromStream(stream: TStream): Boolean;
+begin
+  Clear;
+  Result := fSvgParser.LoadFromStream(stream) and LoadInternal;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgReader.LoadFromString(const str: string): Boolean;
+begin
+  Clear;
+  Result := fSvgParser.LoadFromString(str) and LoadInternal;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgReader.SetOverrideFillColor(color: TColor32);
+var
+  dd: TDrawData;
+begin
+  if not Assigned(RootElement) or (color = clNone32) then Exit;
+  dd := RootElement.DrawData;
+  dd.fillColor := color;
+  RootElement.DrawData := dd;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgReader.SetOverrideStrokeColor(color: TColor32);
+var
+  dd: TDrawData;
+begin
+  if not Assigned(RootElement) or (color = clNone32) then Exit;
+  dd := RootElement.DrawData;
+  if dd.strokeColor = clInvalid then Exit;
+  dd.strokeColor := color;
+  RootElement.DrawData := dd;
+end;
+//------------------------------------------------------------------------------
+
+function TSvgReader.FindElement(const idName: UTF8String): TSvgElement;
+begin
+  if Assigned(RootElement) then
+    Result := RootElement.FindChild(idName) else
+    Result := nil;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgReader.GetBestFontForFontCache(const svgFontInfo: TSVGFontInfo);
+var
+  bestFontReader: TFontReader;
+  fi: TFontInfo;
+begin
+  fi.fontFamily := svgFontInfo.family;
+  fi.faceName := ''; //just match to a family here, not to a specific facename
+  fi.macStyles := [];
+  if svgFontInfo.italic = sfsItalic then
+    Include(fi.macStyles, msItalic);
+  if svgFontInfo.weight >= 600 then
+    Include(fi.macStyles, msBold);
+
+  bestFontReader := FontManager.GetBestMatchFont(fi);
+  if not Assigned(bestFontReader) then Exit;
+
+  if Assigned(fFontCache) then
+    fFontCache.FontReader := bestFontReader else
+    fFontCache := TFontCache.Create(bestFontReader, defaultFontHeight);
+
+  fFontCache.Underlined := False;
+  fFontCache.StrikeOut := False;
+  case svgFontInfo.decoration of
+    fdUnderline     : fFontCache.Underlined := true;
+    fdStrikeThrough : fFontCache.StrikeOut := true;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TSvgReader.SetBlurQuality(quality: integer);
+begin
+  fBlurQuality := Max(0, Min(2, quality));
+end;
+//------------------------------------------------------------------------------
+
+function TSvgReader.GetIsEmpty: Boolean;
+begin
+  Result := not Assigned(fRootElement);
+end;
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+end.

+ 3659 - 0
components/Image32/source/Img32.Text.pas

@@ -0,0 +1,3659 @@
+unit Img32.Text;
+
+(*******************************************************************************
+* Author    :  Angus Johnson                                                   *
+* Version   :  4.3                                                             *
+* Date      :  6 October 2022                                                  *
+* Website   :  http://www.angusj.com                                           *
+* Copyright :  Angus Johnson 2019-2022                                         *
+*                                                                              *
+* Purpose   :  TrueType fonts for TImage32 (without Windows dependencies)      *
+*                                                                              *
+* License   :  Use, modification & distribution is subject to                  *
+*              Boost Software License Ver 1                                    *
+*              http://www.boost.org/LICENSE_1_0.txt                            *
+*******************************************************************************)
+
+interface
+
+{$I Img32.inc}
+
+uses
+  {$IFDEF MSWINDOWS} Windows, ShlObj, ActiveX, {$ENDIF}
+  Types, SysUtils, Classes, Math,
+  {$IFDEF XPLAT_GENERICS} Generics.Collections, Generics.Defaults,{$ENDIF}
+  Img32, Img32.Draw;
+
+type
+  TFixed = type single;
+  Int16 = type SmallInt;
+  TFontFormat = (ffInvalid, ffTrueType, ffCompact);
+  TTtfFontFamily = (ttfUnknown, ttfSerif, ttfSansSerif, ttfMonospace);
+
+  {$IFNDEF Unicode}
+  UnicodeString = WideString;
+  {$ENDIF}
+
+  TMacStyle = (msBold, msItalic, msUnderline, msOutline,
+    msShadow, msCondensed, msExtended);
+  TMacStyles = set of TMacStyle;
+
+  TTextAlign = (taLeft, taRight, taCenter, taJustify);
+  TTextVAlign = (tvaTop, tvaMiddle, tvaBottom);
+
+  //nb: Avoid "packed" records as these cause problems with Android
+
+  TFontHeaderTable = record
+    sfntVersion   : Cardinal;  // $10000 or 'OTTO'
+    numTables     : WORD;
+    searchRange   : WORD;
+    entrySelector : WORD;
+    rangeShift    : WORD;
+  end;
+
+  TFontTable = record
+    tag           : Cardinal;
+    checkSum      : Cardinal;
+    offset        : Cardinal;
+    length        : Cardinal;
+  end;
+
+  TFontTable_Cmap = record
+    version       : WORD;
+    numTables     : WORD;
+  end;
+
+  TCmapTblRec = record
+    platformID    : WORD; //Unicode = 0; Windows = 3 (obsolete);
+    encodingID    : WORD;
+    offset        : Cardinal;
+  end;
+
+  TCmapFormat0 = record
+    format        : WORD; //0
+    length        : WORD;
+    language      : WORD;
+  end;
+
+  TCmapFormat4 = record
+    format        : WORD; //4
+    length        : WORD;
+    language      : WORD;
+    segCountX2    : WORD;
+    searchRange   : WORD;
+    entrySelector : WORD;
+    rangeShift    : WORD;
+    //endCodes    : array of WORD; //last = $FFFF
+    //reserved    : WORD; //0
+    //startCodes  : array of WORD;
+  end;
+
+  TCmapFormat6 = record
+    format        : WORD; //6
+    length        : WORD;
+    language      : WORD;
+    firstCode     : WORD;
+    entryCount    : WORD;
+  end;
+
+  TFontTable_Kern = record
+    version       : WORD;
+    numTables     : WORD;
+  end;
+
+  TKernSubTbl = record
+    version       : WORD;
+    length        : WORD;
+    coverage      : WORD;
+  end;
+
+  TFormat0KernHdr = record
+    nPairs        : WORD;
+    searchRange   : WORD;
+    entrySelector : WORD;
+    rangeShift    : WORD;
+  end;
+
+  TFormat0KernRec = record
+    left          : WORD;
+    right         : WORD;
+    value         : int16;
+  end;
+  TArrayOfKernRecs = array of TFormat0KernRec;
+
+  TFontTable_Name = record
+    format        : WORD;
+    count         : WORD;
+    stringOffset  : WORD;
+    //nameRecords[]
+  end;
+
+  TNameRec = record
+    platformID        : WORD;
+    encodingID        : WORD;
+    languageID        : WORD;
+    nameID            : WORD;
+    length            : WORD;
+    offset            : WORD;
+  end;
+
+  TFontTable_Head = record
+    majorVersion   : Word;
+    minorVersion   : Word;
+    fontRevision   : TFixed;
+    checkSumAdjust : Cardinal;
+    magicNumber    : Cardinal;  // $5F0F3CF5
+    flags          : Word;
+    unitsPerEm     : Word;
+    dateCreated    : UInt64;
+    dateModified   : UInt64;
+    xMin           : Int16;
+    yMin           : Int16;
+    xMax           : Int16;
+    yMax           : Int16;
+    macStyle       : Word;      //see TMacStyles
+    lowestRecPPEM  : Word;
+    fontDirHint    : Int16;     //ltr, rtl
+    indexToLocFmt  : Int16;
+    glyphDataFmt   : Int16;
+  end;
+
+  TFontTable_Maxp = record
+    version        : TFixed;
+    numGlyphs      : WORD;
+    maxPoints      : WORD;
+    maxContours    : WORD;
+  end;
+
+  TFontTable_Glyf = record
+    numContours    : Int16;
+    xMin           : Int16;
+    yMin           : Int16;
+    xMax           : Int16;
+    yMax           : Int16;
+  end;
+
+  TFontTable_Hhea = record
+    version        : TFixed;
+    ascent         : Int16;
+    descent        : Int16;
+    lineGap        : Int16;
+    advWidthMax    : WORD;
+    minLSB         : Int16;
+    minRSB         : Int16;
+    xMaxExtent     : Int16;
+    caretSlopeRise : Int16;
+    caretSlopeRun  : Int16;
+    caretOffset    : Int16;
+    reserved       : UInt64;
+    metricDataFmt  : Int16;
+    numLongHorMets : WORD;
+  end;
+
+  TFontTable_Hmtx = record
+    advanceWidth    : WORD;
+    leftSideBearing : Int16;
+  end;
+
+  TFontTable_Post = record
+    majorVersion   : Word;
+    minorVersion   : Word;
+    italicAngle    : TFixed;
+    underlinePos   : Int16;
+    underlineWidth : Int16;
+    isFixedPitch   : Cardinal;
+    //minMemType42   : Cardinal;
+    //maxMemType42   : Cardinal;
+    //minMemType1   : Cardinal;
+    //maxMemType1   : Cardinal;
+  end;
+
+  TFontInfo = record                  //a custom summary record
+    fontFormat     : TFontFormat;
+    fontFamily     : TTtfFontFamily;
+    faceName       : UnicodeString;
+    style          : UnicodeString;
+    copyright      : UnicodeString;
+    manufacturer   : UnicodeString;
+    dateCreated    : TDatetime;
+    dateModified   : TDatetime;
+    macStyles      : TMacStyles;
+    glyphCount     : integer;
+    unitsPerEm     : integer;
+    xMin           : integer;
+    yMin           : integer;
+    xMax           : integer;
+    yMax           : integer;
+    ascent         : integer;
+    descent        : integer;
+    lineGap        : integer;
+    advWidthMax    : integer;
+    minLSB         : integer;
+    minRSB         : integer;
+    xMaxExtent     : integer;
+  end;
+
+  TKern = record
+    rightGlyphIdx  : integer;
+    kernValue      : integer;
+  end;
+  TArrayOfTKern = array of TKern;
+
+  TGlyphMetrics = record              //a custom metrics record
+    glyphIdx   : integer;
+    unitsPerEm : integer;
+    glyf       : TFontTable_Glyf;
+    hmtx       : TFontTable_Hmtx;
+    kernList   : TArrayOfTKern;
+  end;
+
+  TFontTableArray = array of TFontTable;
+  TArrayOfWord = array of WORD;
+  TArrayOfCardinal = array of Cardinal;
+  TArrayOfCmapTblRec = array of TCmapTblRec;
+
+  TPointEx = record
+    pt: TPointD;
+    flag: byte;
+  end;
+  TPathEx = array of TPointEx;
+  TPathsEx = array of TPathEx;
+
+  TTableName = (tblName, tblHead, tblHhea,
+    tblCmap, tblMaxp, tblLoca, tblGlyf, tblHmtx, tblKern, tblPost);
+
+{$IFDEF ZEROBASEDSTR}
+  {$ZEROBASEDSTRINGS OFF}
+{$ENDIF}
+
+  TFontReader = class;
+
+  TFontManager = class
+  private
+    fMaxFonts: integer;
+{$IFDEF XPLAT_GENERICS}
+    fFontList: TList<TFontReader>;
+{$ELSE}
+    fFontList: TList;
+{$ENDIF}
+    procedure SetMaxFonts(value: integer);
+    function ValidateAdd(fr: TFontReader): Boolean;
+  public
+    constructor Create;
+    destructor Destroy; override;
+    procedure Clear;
+    function GetFont(const fontName: string): TFontReader;
+{$IFDEF MSWINDOWS}
+    function Load(const fontName: string): TFontReader;
+{$ENDIF}
+    function LoadFromStream(stream: TStream): TFontReader;
+    function LoadFromResource(const resName: string; resType: PChar): TFontReader;
+    function LoadFromFile(const filename: string): TFontReader;
+    function GetBestMatchFont(const fontInfo: TFontInfo): TFontReader;
+    function Delete(fontReader: TFontReader): Boolean;
+    property MaxFonts: integer read fMaxFonts write SetMaxFonts;
+  end;
+
+  TFontReader = class(TInterfacedObj, INotifySender)
+  private
+    fFontManager       : TFontManager;
+    fDestroying        : Boolean;
+    fUpdateCount       : integer;
+    fRecipientList     : TRecipients;
+    fStream            : TMemoryStream;
+    fFontWeight        : integer;
+    fFontInfo          : TFontInfo;
+    fTables            : TFontTableArray;
+    fTblIdxes          : array[TTableName] of integer;
+    fTbl_name          : TFontTable_Name;
+    fTbl_head          : TFontTable_Head;
+    fTbl_hhea          : TFontTable_Hhea;
+    fTbl_cmap          : TFontTable_Cmap;
+    fTbl_maxp          : TFontTable_Maxp;
+    fTbl_glyf          : TFontTable_Glyf;
+    fTbl_hmtx          : TFontTable_Hmtx;
+    fTbl_post          : TFontTable_Post;
+    fTbl_loca2         : TArrayOfWord;
+    fTbl_loca4         : TArrayOfCardinal;
+    fCmapTblRecs       : TArrayOfCmapTblRec;
+    fFormat0CodeMap    : array[0..255] of byte;
+    fFormat4EndCodes   : TArrayOfWord;
+    fFormat4StartCodes : TArrayOfWord;
+    fFormat4IdDelta    : TArrayOfWord;
+    fFormat4RangeOff   : TArrayOfWord;
+    fFormat4Offset     : integer;
+    fKernTable         : TArrayOfKernRecs;
+
+    function GetTables: Boolean;
+    function GetTable_name: Boolean;
+    function GetTable_cmap: Boolean;
+    function GetTable_maxp: Boolean;
+    function GetTable_head: Boolean;
+    function GetTable_loca: Boolean;
+    function GetTable_hhea: Boolean;
+    procedure GetTable_kern;
+    procedure GetTable_post;
+
+    function GetGlyphPaths(glyphIdx: integer): TPathsEx;
+    function GetGlyphIdxFromCmapIdx(idx: Word): integer;
+    function GetSimpleGlyph: TPathsEx;
+    function GetCompositeGlyph: TPathsEx;
+    function ConvertSplinesToBeziers(const pathsEx: TPathsEx): TPathsEx;
+    procedure GetPathCoords(var paths: TPathsEx);
+    function GetGlyphHorzMetrics(glyphIdx: integer): Boolean;
+    function GetFontInfo: TFontInfo;
+    function GetGlyphKernList(glyphIdx: integer): TArrayOfTKern;
+    function GetGlyphMetricsInternal(glyphIdx: integer): TGlyphMetrics;
+    function GetWeight: integer;
+    function GetFontFamily: TTtfFontFamily;
+    procedure BeginUpdate;
+    procedure EndUpdate;
+    procedure NotifyRecipients(notifyFlag: TImg32Notification);
+  protected
+    property PostTable: TFontTable_Post read fTbl_post;
+  public
+    constructor Create; overload;
+    constructor CreateFromResource(const resName: string; resType: PChar);
+{$IFDEF MSWINDOWS}
+    constructor Create(const fontname: string); overload;
+{$ENDIF}
+    destructor Destroy; override;
+    procedure Clear;
+    procedure AddRecipient(recipient: INotifyRecipient);
+    procedure DeleteRecipient(recipient: INotifyRecipient);
+    function IsValidFontFormat: Boolean;
+    function LoadFromStream(stream: TStream): Boolean;
+    function LoadFromResource(const resName: string; resType: PChar): Boolean;
+    function LoadFromFile(const filename: string): Boolean;
+{$IFDEF MSWINDOWS}
+    function Load(const fontname: string): Boolean;
+    function LoadUsingFontHdl(hdl: HFont): Boolean;
+{$ENDIF}
+    function GetGlyphInfo(unicode: Word; out paths: TPathsD;
+      out nextX: integer; out glyphMetrics: TGlyphMetrics): Boolean;
+    property FontFamily: TTtfFontFamily read GetFontFamily;
+    property FontInfo: TFontInfo read GetFontInfo;
+    property Weight: integer read GetWeight; //range 100-900
+  end;
+
+  PGlyphInfo = ^TGlyphInfo;
+  TGlyphInfo = record
+    unicode  : Word;
+    contours : TPathsD;
+    metrics  : TGlyphMetrics;
+  end;
+
+  TTextPageMetrics = record
+    lineCount       : integer;
+    maxLineWidth    : double;
+    wordListOffsets : TArrayOfInteger;
+    justifyDeltas   : TArrayOfDouble;
+    lineWidths      : TArrayOfDouble;
+  end;
+
+  TWordInfoList = class;
+
+  TWordInfo = class
+    index         : integer;
+    aWord         : UnicodeString;
+    width         : double;
+    length        : integer;
+    paths         : TArrayOfPathsD;
+    constructor Create(owner: TWordInfoList; idx: integer);
+  end;
+
+  TFontCache = class;
+
+  //TWordInfoList: A font formatted word list where text is broken into
+  //individual words and stored with their glyph info. This class is very
+  //useful with custom text editors.
+  TWordInfoList = class
+  private
+{$IFDEF XPLAT_GENERICS}
+    fList         : TList<TWordInfo>;
+{$ELSE}
+    fList         : TList;
+{$ENDIF}
+    fSingleLine   : Boolean;
+    //fListUpdates: accommodates many calls to UpdateWordList
+    //by occasionally refreshing glyph outlines.
+    //fListUpdates: integer;
+    fUpdateCount: integer;
+    fOnChanged  : TNotifyEvent;
+    function  GetWord(index: integer): TWordInfo;
+    function GetText: UnicodeString;
+  protected
+    procedure Changed; Virtual;
+  public
+    constructor Create;
+    destructor Destroy; override;
+    procedure BeginUpdate;
+    procedure EndUpdate;
+    procedure ApplyNewFont(font: TFontCache);
+    procedure Clear;
+    function  Count: integer;
+    procedure Edit(font: TFontCache; index: Integer; const newWord: string);
+    procedure Delete(Index: Integer);
+    procedure DeleteRange(startIdx, endIdx: Integer);
+    procedure AddNewline;
+    procedure AddSpace(font: TFontCache); overload;
+    procedure AddSpace(spaceWidth: double); overload;
+    procedure AddWord(font: TFontCache;
+      const word: UnicodeString; underlineIdx: integer = 0);
+    procedure InsertNewline(index: integer);
+    procedure InsertSpace(font: TFontCache; index: integer); overload;
+    procedure InsertSpace(spaceWidth: double; index: integer); overload;
+    procedure InsertWord(font: TFontCache; index: integer;
+      const word: UnicodeString; underlineIdx: integer = 0);
+    procedure SetText(const text: UnicodeString;
+      font: TFontCache; underlineIdx: integer = 0);
+    property ForceSingleLine: Boolean read fSingleLine write fSingleLine;
+    property WordInfo[index: integer]: TWordInfo read GetWord; default;
+    property Text: UnicodeString read GetText;
+    property OnChanged: TNotifyEvent read fOnChanged write fOnChanged;
+  end;
+
+  //TFontCache: speeds up text rendering by parsing font files only once
+  //for each accessed character. It can also scale glyphs to a specified
+  //font height and invert them too (which is necessary on Windows PCs).
+  TFontCache = class(TInterfacedObj, INotifySender, INotifyRecipient)
+  private
+{$IFDEF XPLAT_GENERICS}
+    fGlyphInfoList     : TList<PGlyphInfo>;
+{$ELSE}
+    fGlyphInfoList     : TList;
+{$ENDIF}
+    fFontReader        : TFontReader;
+    fRecipientList     : TRecipients;
+    fSorted            : Boolean;
+    fScale             : double;
+    fUseKerning        : Boolean;
+    fFontHeight        : double;
+    fFlipVert          : Boolean;
+    fUnderlined        : Boolean;
+    fStrikeOut         : Boolean;
+    procedure NotifyRecipients(notifyFlag: TImg32Notification);
+    function FoundInList(charOrdinal: WORD): Boolean;
+    function AddGlyph(unicode: Word): PGlyphInfo;
+    procedure VerticalFlip(var paths: TPathsD);
+    procedure SetFlipVert(value: Boolean);
+    procedure SetFontHeight(newHeight: double);
+    procedure SetFontReader(newFontReader: TFontReader);
+    procedure UpdateScale;
+    procedure Sort;
+    procedure GetMissingGlyphs(const ordinals: TArrayOfWord);
+    function IsValidFont: Boolean;
+    function GetAscent: double;
+    function GetDescent: double;
+    function GetLineHeight: double;
+    function GetYyHeight: double;
+
+    function GetTextOutlineInternal(x, y: double;
+      const text: UnicodeString; out glyphs: TArrayOfPathsD;
+      out nextX: double; underlineIdx: integer = 0): Boolean;
+  public
+    constructor Create(fontReader: TFontReader = nil; fontHeight: double = 10); overload;
+    destructor Destroy; override;
+    procedure Clear;
+    //TFontCache is both an INotifySender and an INotifyRecipient.
+    //It receives notifications from a TFontReader object and it sends
+    //notificiations to any number of TFontCache object users
+    procedure ReceiveNotification(Sender: TObject; notify: TImg32Notification);
+    procedure AddRecipient(recipient: INotifyRecipient);
+    procedure DeleteRecipient(recipient: INotifyRecipient);
+    function GetCharInfo(charOrdinal: WORD): PGlyphInfo;
+
+    function GetTextOutline(x, y: double;
+      const text: UnicodeString): TPathsD; overload;
+    function GetTextOutline(const rec: TRect; const text: UnicodeString;
+      textAlign: TTextAlign; textAlignV: TTextVAlign;
+      underlineIdx: integer = 0): TPathsD; overload;
+    function GetTextOutline(const rec: TRect; wordList: TWordInfoList;
+      tpm: TTextPageMetrics; textAlign: TTextAlign;
+      startLine, endLine: integer): TPathsD; overload;
+    function GetTextOutline(x, y: double; const text: UnicodeString;
+      out nextX: double; underlineIdx: integer = 0): TPathsD; overload;
+    function GetVerticalTextOutline(x, y: double;
+      const text: UnicodeString; interCharSpace: double =0): TPathsD;
+
+
+    function GetAngledTextGlyphs(x, y: double; const text: UnicodeString;
+      angleRadians: double; const rotatePt: TPointD;
+      out nextPt: TPointD): TPathsD;
+    function GetCharOffsets(const text: UnicodeString;
+      interCharSpace: double = 0): TArrayOfDouble;
+    function GetTextWidth(const text: UnicodeString): double;
+    function GetSpaceWidth: double;
+
+    property Ascent: double read GetAscent;
+    property Descent: double read GetDescent;
+
+    property FontHeight: double read fFontHeight write SetFontHeight;
+    property FontReader: TFontReader read
+      fFontReader write SetFontReader;
+    property InvertY: boolean read fFlipVert write SetFlipVert;
+    property Kerning: boolean read fUseKerning write fUseKerning;
+    property LineHeight: double read GetLineHeight;
+    property YyHeight: double read GetYyHeight;
+    property Scale : double read fScale;
+    property Underlined: Boolean read fUnderlined write fUnderlined;
+    property StrikeOut: Boolean read fStrikeOut write fStrikeOut;
+  end;
+
+  //Given a wordList and a specified maximum line width (in pixels),
+  //get both the line count and the offsets to the words in wordlist
+  //that will start each line.
+  function GetPageMetrics(lineWidth: double;
+    wordList: TWordInfoList): TTextPageMetrics;
+
+  function DrawText(image: TImage32; x, y: double;
+    const text: UnicodeString; font: TFontCache;
+    textColor: TColor32 = clBlack32;
+    useClearType: Boolean = false;
+    clearTypeBgColor: TColor32 = clWhite32): double; overload;
+
+  function DrawText(image: TImage32; x, y: double;
+    const text: UnicodeString; font: TFontCache;
+    renderer: TCustomRenderer): double; overload;
+
+  procedure DrawText(image: TImage32; const rec: TRect;
+    const text: UnicodeString;
+    textAlign: TTextAlign; textAlignV: TTextVAlign;
+    font: TFontCache; textColor: TColor32 = clBlack32;
+    useClearType: Boolean = false;
+    clearTypeBgColor: TColor32 = clWhite32); overload;
+
+  function DrawAngledText(image: TImage32;
+  x, y: double; angleRadians: double;
+  const text: UnicodeString; font: TFontCache;
+  textColor: TColor32 = clBlack32): TPointD;
+
+  function DrawVerticalText(image: TImage32;
+    x, y, interCharSpace: double;
+    const text: UnicodeString; font: TFontCache;
+    textColor: TColor32 = clBlack32): double;
+
+  function GetTextOutlineOnPath(const text: UnicodeString;
+    const path: TPathD; font: TFontCache; textAlign: TTextAlign;
+    perpendicOffset: integer = 0; charSpacing: double = 0): TPathsD; overload;
+
+  function GetTextOutlineOnPath(const text: UnicodeString;
+    const path: TPathD; font: TFontCache; textAlign: TTextAlign;
+    perpendicOffset: integer; charSpacing: double;
+    out charsThatFit: integer): TPathsD; overload;
+
+  {$IFDEF MSWINDOWS}
+  procedure CheckFontHeight(var logFont: TLogFont);
+  function PointHeightToPixelHeight(pt: double): double;
+  function GetFontFolder: string;
+  function GetInstalledTtfFilenames: TArrayOfString;
+  {$ENDIF}
+
+  function FontManager: TFontManager;
+
+implementation
+
+uses
+  Img32.Vector;
+
+resourcestring
+  rsTooManyFonts        = 'TFontManager error: Too many fonts are open.';
+  rsWordListRangeError  = 'TFFWordList: range error.';
+  rsFontCacheError      = 'TFontCache error: message from incorrect TFontReader';
+  rsWordListFontError  = 'TFFWordList: invalid font error.';
+
+var
+  aFontManager: TFontManager;
+
+const
+  lineFrac = 0.05;
+
+//------------------------------------------------------------------------------
+// Miscellaneous functions
+//------------------------------------------------------------------------------
+
+//GetMeaningfulDateTime: returns UTC date & time
+procedure GetMeaningfulDateTime(const secsSince1904: Uint64;
+  out yy,mo,dd, hh,mi,ss: cardinal);
+const
+  dim: array[boolean, 0..12] of cardinal =
+    ((0,31,59,90,120,151,181,212,243,273,304,334,365), //non-leap year
+    (0,31,60,91,121,152,182,213,244,274,305,335,366)); //leap year
+var
+  isLeapYr: Boolean;
+const
+  maxValidYear  = 2100;
+  secsPerHour   = 3600;
+  secsPerDay    = 86400;
+  secsPerNormYr = 31536000;
+  secsPerLeapYr = secsPerNormYr + secsPerDay;
+  secsPer4Years = secsPerNormYr * 3 + secsPerLeapYr; //126230400;
+begin
+  //nb: Centuries are not leap years unless they are multiples of 400.
+  //    2000 WAS a leap year, but 2100 won't be.
+  //    Validate function at http://www.mathcats.com/explore/elapsedtime.html
+
+  ss := (secsSince1904 div secsPer4Years);       //count '4years' since 1904
+
+  //manage invalid dates
+  if (secsSince1904 = 0) or
+    (ss > (maxValidYear-1904) div 4) then
+  begin
+    yy := 1904; mo := 1; dd := 1;
+    hh := 0; mi := 0; ss := 0;
+    Exit;
+  end;
+  yy := 1904 + (ss * 4);
+
+  ss := secsSince1904 mod secsPer4Years;         //secs since START last leap yr
+  isLeapYr := ss < secsPerLeapYr;
+  if not isLeapYr then
+  begin
+    dec(ss, secsPerLeapYr);
+    yy := yy + (ss div secsPerNormYr) + 1;
+    ss := ss mod secsPerNormYr;                  //remaining secs in final year
+  end;
+  dd := 1 + ss div secsPerDay;                   //day number in final year
+  mo := 1;
+  while dim[isLeapYr, mo] < dd do inc(mo);
+  ss := ss - (dim[isLeapYr, mo-1] * secsPerDay); //remaining secs in month
+  dd := 1 + (ss div secsPerDay);
+  ss := ss mod secsPerDay;
+  hh := ss div secsPerHour;
+  ss := ss mod secsPerHour;
+  mi := ss div 60;
+  ss := ss mod 60;
+end;
+//------------------------------------------------------------------------------
+
+function MergePathsArray(const pa: TArrayOfPathsD): TPathsD;
+var
+  i: integer;
+begin
+  Result := nil;
+  for i := 0 to High(pa) do
+    AppendPath(Result, pa[i]);
+end;
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+function WordSwap(val: WORD): WORD;
+{$IFDEF ASM_X86}
+asm
+  rol ax,8;
+end;
+{$ELSE}
+var
+  v: array[0..1] of byte absolute val;
+  r: array[0..1] of byte absolute result;
+begin
+  r[0] := v[1];
+  r[1] := v[0];
+end;
+{$ENDIF}
+//------------------------------------------------------------------------------
+
+function Int16Swap(val: Int16): Int16;
+{$IFDEF ASM_X86}
+asm
+  rol ax,8;
+end;
+{$ELSE}
+var
+  v: array[0..1] of byte absolute val;
+  r: array[0..1] of byte absolute result;
+begin
+  r[0] := v[1];
+  r[1] := v[0];
+end;
+{$ENDIF}
+//------------------------------------------------------------------------------
+
+function Int32Swap(val: integer): integer;
+{$IFDEF ASM_X86}
+asm
+  bswap eax
+end;
+{$ELSE}
+var
+  i: integer;
+  v: array[0..3] of byte absolute val;
+  r: array[0..3] of byte absolute Result; //warning: do not inline
+begin
+  for i := 0 to 3 do r[3-i] := v[i];
+end;
+{$ENDIF}
+//------------------------------------------------------------------------------
+
+function UInt64Swap(val: UInt64): UInt64;
+{$IFDEF ASM_X86}
+asm
+  MOV     EDX, val.Int64Rec.Lo
+  BSWAP   EDX
+  MOV     EAX, val.Int64Rec.Hi
+  BSWAP   EAX
+end;
+{$ELSE}
+var
+  i: integer;
+  v: array[0..7] of byte absolute val;
+  r: array[0..7] of byte absolute Result;
+begin
+  for i := 0 to 7 do r[7-i] := v[i];
+end;
+{$ENDIF}
+//------------------------------------------------------------------------------
+
+procedure GetByte(stream: TStream; out value: byte);
+begin
+  stream.Read(value, 1);
+end;
+//------------------------------------------------------------------------------
+
+procedure GetShortInt(stream: TStream; out value: ShortInt);
+begin
+  stream.Read(value, 1);
+end;
+//------------------------------------------------------------------------------
+
+function GetWord(stream: TStream; out value: WORD): Boolean;
+begin
+  result := stream.Position + SizeOf(value) < stream.Size;
+  stream.Read(value, SizeOf(value));
+  value := WordSwap(value);
+end;
+//------------------------------------------------------------------------------
+
+function GetInt16(stream: TStream; out value: Int16): Boolean;
+begin
+  result := stream.Position + SizeOf(value) < stream.Size;
+  stream.Read(value, SizeOf(value));
+  value := Int16Swap(value);
+end;
+//------------------------------------------------------------------------------
+
+function GetCardinal(stream: TStream; out value: Cardinal): Boolean;
+begin
+  result := stream.Position + SizeOf(value) < stream.Size;
+  stream.Read(value, SizeOf(value));
+  value := Cardinal(Int32Swap(Integer(value)));
+end;
+//------------------------------------------------------------------------------
+
+function GetInt(stream: TStream; out value: integer): Boolean;
+begin
+  result := stream.Position + SizeOf(value) < stream.Size;
+  stream.Read(value, SizeOf(value));
+  value := Int32Swap(value);
+end;
+//------------------------------------------------------------------------------
+
+function GetUInt64(stream: TStream; out value: UInt64): Boolean;
+begin
+  result := stream.Position + SizeOf(value) < stream.Size;
+  stream.Read(value, SizeOf(value));
+  value := UInt64Swap(value);
+end;
+//------------------------------------------------------------------------------
+
+function Get2Dot14(stream: TStream; out value: single): Boolean;
+var
+  val: Int16;
+begin
+  result := GetInt16(stream, val);
+  if result then value := val * 6.103515625e-5; // 16384;
+end;
+//------------------------------------------------------------------------------
+
+function GetFixed(stream: TStream; out value: TFixed): Boolean;
+var
+  val: integer;
+begin
+  result := GetInt(stream, val);
+  value := val * 1.52587890625e-5; // 1/35536
+end;
+//------------------------------------------------------------------------------
+
+function GetWideString(stream: TStream; length: integer): UnicodeString;
+var
+  i: integer;
+  w: Word;
+begin
+  length := length div 2;
+  setLength(Result, length);
+  for i := 1 to length do
+  begin
+    GetWord(stream, w); //nb: reverses byte order
+    if w = 0 then
+    begin
+      SetLength(Result, i -1);
+      break;
+    end;
+    result[i] := WideChar(w);
+   end;
+end;
+//------------------------------------------------------------------------------
+
+function GetAnsiString(stream: TStream; len: integer): string;
+var
+  i: integer;
+  ansi: UTF8String;
+begin
+  setLength(ansi, len+1);
+  ansi[len+1] := #0;
+  stream.Read(ansi[1], len);
+  result := string(ansi);
+  for i := 1 to length(Result) do
+    if Result[i] = #0 then
+    begin
+      SetLength(Result, i -1);
+      break;
+    end;
+end;
+
+//------------------------------------------------------------------------------
+// TTrueTypeReader
+//------------------------------------------------------------------------------
+
+constructor TFontReader.Create;
+begin
+  fStream := TMemoryStream.Create;
+end;
+//------------------------------------------------------------------------------
+
+constructor TFontReader.CreateFromResource(const resName: string; resType: PChar);
+begin
+  Create;
+  LoadFromResource(resName, resType);
+end;
+//------------------------------------------------------------------------------
+
+{$IFDEF MSWINDOWS}
+constructor TFontReader.Create(const fontname: string);
+begin
+  Create;
+  Load(fontname);
+end;
+//------------------------------------------------------------------------------
+{$ENDIF}
+
+destructor TFontReader.Destroy;
+begin
+  Clear;
+  NotifyRecipients(inDestroy);
+  fStream.Free;
+  if Assigned(fFontManager) then
+  begin
+    fDestroying := true;
+    fFontManager.Delete(self);
+  end;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontReader.Clear;
+begin
+  fTables               := nil;
+  fCmapTblRecs          := nil;
+  fFormat4Offset        := 0;
+  fFormat4EndCodes      := nil;
+  fKernTable            := nil;
+  FillChar(fTbl_post, SizeOf(fTbl_post), 0);
+  fTbl_glyf.numContours := 0;
+  fFontInfo.fontFormat  := ffInvalid;
+  fFontInfo.fontFamily  := ttfUnknown;
+  fFontWeight           := 0;
+  fStream.Clear;
+  NotifyRecipients(inStateChange);
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontReader.BeginUpdate;
+begin
+  inc(fUpdateCount);
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontReader.EndUpdate;
+begin
+  dec(fUpdateCount);
+  if fUpdateCount = 0 then NotifyRecipients(inStateChange);
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontReader.NotifyRecipients(notifyFlag: TImg32Notification);
+var
+  i: integer;
+begin
+  if fUpdateCount > 0 then Exit;
+  for i := High(fRecipientList) downto 0 do
+    try
+      //when destroying in a finalization section
+      //it's possible for recipients to have been destroyed
+      //without their destructors being called.
+      fRecipientList[i].ReceiveNotification(self, notifyFlag);
+    except
+    end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontReader.AddRecipient(recipient: INotifyRecipient);
+var
+  len: integer;
+begin
+  len := Length(fRecipientList);
+  SetLength(fRecipientList, len+1);
+  fRecipientList[len] := Recipient;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontReader.DeleteRecipient(recipient: INotifyRecipient);
+var
+  i, highI: integer;
+begin
+  highI := High(fRecipientList);
+  i := highI;
+  while (i >= 0) and (fRecipientList[i] <> Recipient) do dec(i);
+  if i < 0 then Exit;
+  if i < highI then
+    Move(fRecipientList[i+1], fRecipientList[i],
+      (highI - i) * SizeOf(INotifyRecipient));
+  SetLength(fRecipientList, highI);
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.IsValidFontFormat: Boolean;
+begin
+  result := fFontInfo.fontFormat = ffTrueType;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.LoadFromStream(stream: TStream): Boolean;
+begin
+  BeginUpdate;
+  try
+    Clear;
+    fStream.CopyFrom(stream, 0);
+    fStream.Position := 0;
+    result := GetTables;
+    if not result then Clear;
+  finally
+    EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.LoadFromResource(const resName: string; resType: PChar): Boolean;
+var
+  rs: TResourceStream;
+begin
+  BeginUpdate;
+  rs := CreateResourceStream(resName, resType);
+  try
+    Result := assigned(rs) and LoadFromStream(rs);
+  finally
+    rs.free;
+    EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.LoadFromFile(const filename: string): Boolean;
+var
+  fs: TFileStream;
+begin
+  try
+    fs := TFileStream.Create(filename, fmOpenRead or fmShareDenyNone);
+    try
+      Result := LoadFromStream(fs);
+    finally
+      fs.free;
+    end;
+  except
+    Result := False;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+{$IFDEF MSWINDOWS}
+function GetFontMemStreamFromFontHdl(hdl: HFont;
+  memStream: TMemoryStream): Boolean;
+var
+  memDc: HDC;
+  cnt: DWORD;
+begin
+  result := false;
+  if not Assigned(memStream) or (hdl = 0) then Exit;
+
+  memDc := CreateCompatibleDC(0);
+  try
+    SelectObject(memDc, hdl);
+    //get the required size of the font data (file)
+    cnt := Windows.GetFontData(memDc, 0, 0, nil, 0);
+    result := cnt <> $FFFFFFFF;
+    if not Result then Exit;
+    //copy the font data into the memory stream
+    memStream.SetSize(cnt);
+    Windows.GetFontData(memDc, 0, 0, memStream.Memory, cnt);
+  finally
+    DeleteDC(memDc);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.LoadUsingFontHdl(hdl: HFont): Boolean;
+var
+  ms: TMemoryStream;
+begin
+  ms := TMemoryStream.Create;
+  try
+    Result := GetFontMemStreamFromFontHdl(hdl, ms) and
+      LoadFromStream(ms);
+  finally
+    ms.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.Load(const fontname: string): Boolean;
+var
+  logfont: TLogFont;
+  hdl: HFont;
+begin
+  Result := false;
+  FillChar(logfont, SizeOf(logfont), 0);
+  StrPCopy(@logfont.lfFaceName[0], fontname);
+  hdl := CreateFontIndirect(logfont);
+  if hdl = 0 then Exit;
+  try
+    Result := LoadUsingFontHdl(hdl);
+  finally
+    DeleteObject(hdl);
+  end;
+end;
+//------------------------------------------------------------------------------
+{$ENDIF}
+
+function GetHeaderTable(stream: TStream;
+  out headerTable: TFontHeaderTable): Boolean;
+begin
+  result := stream.Position < stream.Size - SizeOf(TFontHeaderTable);
+  if not result then Exit;
+  GetCardinal(stream, headerTable.sfntVersion);
+  GetWord(stream, headerTable.numTables);
+  GetWord(stream, headerTable.searchRange);
+  GetWord(stream, headerTable.entrySelector);
+  GetWord(stream, headerTable.rangeShift);
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetTables: Boolean;
+var
+  i, tblCount: integer;
+  tbl: TTableName;
+  headerTable: TFontHeaderTable;
+begin
+  result := false;
+  if not GetHeaderTable(fStream, headerTable) then Exit;
+  tblCount := headerTable.numTables;
+  result := fStream.Position < fStream.Size - SizeOf(TFontTable) * tblCount;
+  if not result then Exit;
+
+  for tbl := low(TTableName) to High(TTableName) do fTblIdxes[tbl] := -1;
+  SetLength(fTables, tblCount);
+
+  for i := 0 to tblCount -1 do
+  begin
+    GetCardinal(fStream, fTables[i].tag);
+    GetCardinal(fStream, fTables[i].checkSum);
+    GetCardinal(fStream, fTables[i].offset);
+    GetCardinal(fStream, fTables[i].length);
+    case
+      fTables[i].tag of
+        $6E616D65: fTblIdxes[tblName] := i;
+        $68656164: fTblIdxes[tblHead] := i;
+        $676C7966: fTblIdxes[tblGlyf] := i;
+        $6C6F6361: fTblIdxes[tblLoca] := i;
+        $6D617870: fTblIdxes[tblMaxp] := i;
+        $636D6170: fTblIdxes[tblCmap] := i;
+        $68686561: fTblIdxes[tblHhea] := i;
+        $686D7478: fTblIdxes[tblHmtx] := i;
+        $6B65726E: fTblIdxes[tblKern] := i;
+        $706F7374: fTblIdxes[tblPost] := i;
+    end;
+  end;
+
+  if fTblIdxes[tblName] < 0 then fFontInfo.fontFormat := ffInvalid
+  else if fTblIdxes[tblGlyf] < 0 then fFontInfo.fontFormat := ffCompact
+  else fFontInfo.fontFormat := ffTrueType;
+
+  result := (fFontInfo.fontFormat = ffTrueType) and
+    (fTblIdxes[tblName] >= 0) and GetTable_name and
+    (fTblIdxes[tblHead] >= 0) and GetTable_head and
+    (fTblIdxes[tblHhea] >= 0) and GetTable_hhea and
+    (fTblIdxes[tblMaxp] >= 0) and GetTable_maxp and
+    (fTblIdxes[tblLoca] >= 0) and GetTable_loca and //loca must follow maxp
+    (fTblIdxes[tblCmap] >= 0) and GetTable_cmap;
+
+  if not Result then Exit;
+  if (fTblIdxes[tblKern] >= 0) then GetTable_kern;
+  if (fTblIdxes[tblPost] >= 0) then GetTable_post;
+
+  fFontInfo.fontFamily := GetFontFamily;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetTable_cmap: Boolean;
+var
+  i, segCount: integer;
+  reserved: WORD;
+  cmapRec: TCmapTblRec;
+  format4Rec: TCmapFormat4;
+  cmapTbl: TFontTable;
+begin
+  Result := false;
+  cmapTbl := fTables[fTblIdxes[tblCmap]];
+  if (fStream.Size < cmapTbl.offset + cmapTbl.length) then Exit;
+
+  fStream.Position := cmapTbl.offset;
+  GetWord(fStream, fTbl_cmap.version);
+  GetWord(fStream, fTbl_cmap.numTables);
+
+  //only use the unicode table (0: always first)
+  SetLength(fCmapTblRecs, fTbl_cmap.numTables);
+  for i := 0 to fTbl_cmap.numTables -1 do
+  begin
+    GetWord(fStream, fCmapTblRecs[i].platformID);
+    GetWord(fStream, fCmapTblRecs[i].encodingID);
+    GetCardinal(fStream, fCmapTblRecs[i].offset);
+  end;
+
+  i := 0;
+  while (i < fTbl_cmap.numTables) and
+    (fCmapTblRecs[i].platformID <> 0) and
+    (fCmapTblRecs[i].platformID <> 3) do inc(i);
+  if i = fTbl_cmap.numTables then Exit;
+  cmapRec := fCmapTblRecs[i];
+
+  fStream.Position := cmapTbl.offset + cmapRec.offset;
+  GetWord(fStream, format4Rec.format);
+  GetWord(fStream, format4Rec.length);
+  GetWord(fStream, format4Rec.language);
+
+  if format4Rec.format = 0 then
+  begin
+    for i := 0 to 255 do
+      GetByte(fStream, fFormat0CodeMap[i]);
+    fFontInfo.glyphCount := 255;
+  end
+  else if format4Rec.format = 4 then
+  begin
+    fFontInfo.glyphCount := 0;
+    GetWord(fStream, format4Rec.segCountX2);
+    segCount := format4Rec.segCountX2 shr 1;
+    GetWord(fStream, format4Rec.searchRange);
+    GetWord(fStream, format4Rec.entrySelector);
+    GetWord(fStream, format4Rec.rangeShift);
+    SetLength(fFormat4EndCodes, segCount);
+    for i := 0 to segCount -1 do
+      GetWord(fStream, fFormat4EndCodes[i]);
+    if fFormat4EndCodes[segCount-1] <> $FFFF then Exit; //error
+    GetWord(fStream, reserved);
+    if reserved <> 0 then Exit; //error
+    SetLength(fFormat4StartCodes, segCount);
+    for i := 0 to segCount -1 do
+      GetWord(fStream, fFormat4StartCodes[i]);
+    if fFormat4StartCodes[segCount-1] <> $FFFF then Exit; //error
+    SetLength(fFormat4IdDelta, segCount);
+    for i := 0 to segCount -1 do
+      GetWord(fStream, fFormat4IdDelta[i]);
+    SetLength(fFormat4RangeOff, segCount);
+    fFormat4Offset := fStream.Position;
+    for i := 0 to segCount -1 do
+      GetWord(fStream, fFormat4RangeOff[i]);
+  end else
+    Exit; //unsupported format
+
+  Result := true;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetGlyphIdxFromCmapIdx(idx: Word): integer;
+var
+  i: integer;
+  w: WORD;
+begin
+
+  if fFormat4Offset = 0 then
+  begin
+    if idx > 255 then Result := 0
+    else Result := fFormat0CodeMap[idx];
+    Exit;
+  end;
+
+  //Format4 mapping
+
+  result := 0; //default to the 'missing' glyph
+  for i := 0 to High(fFormat4EndCodes) do
+    if idx <= fFormat4EndCodes[i] then
+    begin
+      if idx < fFormat4StartCodes[i] then Exit;
+      if fFormat4RangeOff[i] > 0 then
+      begin
+        fStream.Position := fFormat4Offset + fFormat4RangeOff[i] +
+          2 * (i + idx - fFormat4StartCodes[i]);
+        GetWord(fStream, w);
+        if w < fTbl_maxp.numGlyphs then Result := w;
+      end else
+        result := (fFormat4IdDelta[i] + idx) and $FFFF;
+      Exit;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetTable_maxp: Boolean;
+var
+  maxpTbl: TFontTable;
+begin
+  maxpTbl := fTables[fTblIdxes[tblMaxp]];
+  Result := (fStream.Size >= maxpTbl.offset + maxpTbl.length) and
+    (maxpTbl.length >= SizeOf(TFixed) + SizeOf(WORD));
+  if not Result then Exit;
+  fStream.Position := maxpTbl.offset;
+  GetFixed(fStream, fTbl_maxp.version);
+  GetWord(fStream, fTbl_maxp.numGlyphs);
+  if fTbl_maxp.version >= 1 then
+  begin
+    GetWord(fStream, fTbl_maxp.maxPoints);
+    GetWord(fStream, fTbl_maxp.maxContours);
+    fFontInfo.glyphCount := fTbl_maxp.numGlyphs;
+  end else
+    Result := false;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetTable_loca: Boolean;
+var
+  i: integer;
+  locaTbl: TFontTable;
+begin
+  locaTbl := fTables[fTblIdxes[tblLoca]];
+  Result := fStream.Size >= locaTbl.offset + locaTbl.length;
+  if not Result then Exit;
+  fStream.Position := locaTbl.offset;
+
+  if fTbl_head.indexToLocFmt = 0 then
+  begin
+    SetLength(fTbl_loca2, fTbl_maxp.numGlyphs +1);
+    for i := 0 to fTbl_maxp.numGlyphs do
+      GetWord(fStream, fTbl_loca2[i]);
+  end else
+  begin
+    SetLength(fTbl_loca4, fTbl_maxp.numGlyphs +1);
+    for i := 0 to fTbl_maxp.numGlyphs do
+      GetCardinal(fStream, fTbl_loca4[i]);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+
+function IsUnicode(platformID: Word): Boolean;
+begin
+  Result := (platformID = 0) or (platformID = 3);
+end;
+//------------------------------------------------------------------------------
+
+function GetNameRecString(stream: TStream;
+  const nameRec: TNameRec; offset: cardinal): UnicodeString;
+var
+  sPos, len: integer;
+begin
+  sPos := stream.Position;
+  stream.Position := offset + nameRec.offset;
+  if IsUnicode(nameRec.platformID) then
+    Result := GetWideString(stream, nameRec.length) else
+    Result := UnicodeString(GetAnsiString(stream, nameRec.length));
+  len := Length(Result);
+  if (len > 0) and (Result[len] = #0) then SetLength(Result, len -1);
+  stream.Position := sPos;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetTable_name: Boolean;
+var
+  i: integer;
+  offset: cardinal;
+  nameRec: TNameRec;
+  nameTbl: TFontTable;
+begin
+  fFontInfo.faceName := '';
+  fFontInfo.style   := '';
+  nameTbl := fTables[fTblIdxes[tblName]];
+  Result := (fStream.Size >= nameTbl.offset + nameTbl.length) and
+    (nameTbl.length >= SizeOf(TFontTable_Name));
+  if not Result then Exit;
+  fStream.Position := nameTbl.offset;
+  GetWord(fStream, fTbl_name.format);
+  GetWord(fStream, fTbl_name.count);
+  GetWord(fStream, fTbl_name.stringOffset);
+  offset := nameTbl.offset + fTbl_name.stringOffset;
+  for i := 1 to fTbl_name.count do
+  begin
+    GetWord(fStream, nameRec.platformID);
+    GetWord(fStream, nameRec.encodingID);
+    GetWord(fStream, nameRec.languageID);
+    GetWord(fStream, nameRec.nameID);
+    GetWord(fStream, nameRec.length);
+    GetWord(fStream, nameRec.offset);
+    case nameRec.nameID of
+      0: fFontInfo.copyright    := GetNameRecString(fStream, nameRec, offset);
+      1: fFontInfo.faceName     := GetNameRecString(fStream, nameRec, offset);
+      2: fFontInfo.style        := GetNameRecString(fStream, nameRec, offset);
+      3..7: continue;
+      8: fFontInfo.manufacturer := GetNameRecString(fStream, nameRec, offset);
+      else break;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetTable_head: Boolean;
+var
+  headTbl: TFontTable;
+  yy,mo,dd,hh,mi,ss: cardinal;
+begin
+  headTbl := fTables[fTblIdxes[tblHead]];
+  Result := (fStream.Size >= headTbl.offset +
+    headTbl.length) and (headTbl.length >= 54);
+  if not Result then Exit;
+  fStream.Position := headTbl.offset;
+  GetWord(fStream, fTbl_head.majorVersion);
+  GetWord(fStream, fTbl_head.minorVersion);
+  GetFixed(fStream, fTbl_head.fontRevision);
+
+  GetCardinal(fStream, fTbl_head.checkSumAdjust);
+  GetCardinal(fStream, fTbl_head.magicNumber);
+  GetWord(fStream, fTbl_head.flags);
+  GetWord(fStream, fTbl_head.unitsPerEm);
+
+  GetUInt64(fStream, fTbl_head.dateCreated);
+  GetMeaningfulDateTime(fTbl_head.dateCreated, yy,mo,dd,hh,mi,ss);
+  fFontInfo.dateCreated := EncodeDate(yy,mo,dd) + EncodeTime(hh,mi,ss, 0);
+  GetUInt64(fStream, fTbl_head.dateModified);
+  GetMeaningfulDateTime(fTbl_head.dateModified, yy,mo,dd,hh,mi,ss);
+  fFontInfo.dateModified := EncodeDate(yy,mo,dd) + EncodeTime(hh,mi,ss, 0);
+
+  GetInt16(fStream, fTbl_head.xMin);
+  GetInt16(fStream, fTbl_head.yMin);
+  GetInt16(fStream, fTbl_head.xMax);
+  GetInt16(fStream, fTbl_head.yMax);
+  GetWord(fStream, fTbl_head.macStyle);
+  fFontInfo.macStyles := TMacStyles(Byte(fTbl_head.macStyle));
+  GetWord(fStream, fTbl_head.lowestRecPPEM);
+  GetInt16(fStream, fTbl_head.fontDirHint);
+  GetInt16(fStream, fTbl_head.indexToLocFmt);
+  GetInt16(fStream, fTbl_head.glyphDataFmt);
+  result := fTbl_head.magicNumber = $5F0F3CF5
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetTable_hhea: Boolean;
+var
+  hheaTbl: TFontTable;
+begin
+  hheaTbl := fTables[fTblIdxes[tblHhea]];
+  Result := (fStream.Size >= hheaTbl.offset + hheaTbl.length) and
+    (hheaTbl.length >= 36);
+  if not Result then Exit;
+  fStream.Position := hheaTbl.offset;
+
+  GetFixed(fStream,  fTbl_hhea.version);
+  GetInt16(fStream,  fTbl_hhea.ascent);
+  GetInt16(fStream,  fTbl_hhea.descent);
+  GetInt16(fStream,  fTbl_hhea.lineGap);
+  GetWord(fStream,   fTbl_hhea.advWidthMax);
+  GetInt16(fStream,  fTbl_hhea.minLSB);
+  GetInt16(fStream,  fTbl_hhea.minRSB);
+  GetInt16(fStream,  fTbl_hhea.xMaxExtent);
+  GetInt16(fStream,  fTbl_hhea.caretSlopeRise);
+  GetInt16(fStream,  fTbl_hhea.caretSlopeRun);
+  GetInt16(fStream,  fTbl_hhea.caretOffset);
+  GetUInt64(fStream, fTbl_hhea.reserved);
+  GetInt16(fStream,  fTbl_hhea.metricDataFmt);
+  GetWord(fStream,   fTbl_hhea.numLongHorMets);
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetGlyphHorzMetrics(glyphIdx: integer): Boolean;
+var
+  tbl            : TFontTable;
+begin
+  tbl := fTables[fTblIdxes[tblHmtx]];
+  Result := (fStream.Size >= tbl.offset + tbl.length);
+  if not Result then Exit;
+  if glyphIdx < fTbl_hhea.numLongHorMets then
+  begin
+    fStream.Position := Integer(tbl.offset) + glyphIdx * 4;
+    GetWord(fStream, fTbl_hmtx.advanceWidth);
+    GetInt16(fStream, fTbl_hmtx.leftSideBearing);
+  end else
+  begin
+    fStream.Position := Integer(tbl.offset) +
+      Integer(fTbl_hhea.numLongHorMets -1) * 4;
+    GetWord(fStream, fTbl_hmtx.advanceWidth);
+    fStream.Position := Integer(tbl.offset +
+      fTbl_hhea.numLongHorMets * 4) +
+      2 * (glyphIdx - Integer(fTbl_hhea.numLongHorMets));
+    GetInt16(fStream, fTbl_hmtx.leftSideBearing);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontReader.GetTable_kern;
+var
+  i              : integer;
+  tbl            : TFontTable;
+  tbl_kern       : TFontTable_Kern;
+  kernSub        : TKernSubTbl;
+  format0KernHdr : TFormat0KernHdr;
+begin
+  if fTblIdxes[tblKern] < 0 then Exit;
+  tbl := fTables[fTblIdxes[tblKern]];
+  if (fStream.Size < tbl.offset + tbl.length) then Exit;
+  fStream.Position := Integer(tbl.offset);
+
+  GetWord(fStream, tbl_kern.version);
+  GetWord(fStream, tbl_kern.numTables);
+  if tbl_kern.numTables = 0 then Exit;
+  //assume there's only one kern table
+
+  GetWord(fStream, kernSub.version);
+  GetWord(fStream, kernSub.length);
+  GetWord(fStream, kernSub.coverage);
+  //we're currently only interested in Format0 horizontal kerning
+  if kernSub.coverage <> 1 then Exit;
+
+  GetWord(fStream, format0KernHdr.nPairs);
+  GetWord(fStream, format0KernHdr.searchRange);
+  GetWord(fStream, format0KernHdr.entrySelector);
+  GetWord(fStream, format0KernHdr.rangeShift);
+
+  SetLength(fKernTable, format0KernHdr.nPairs);
+  for i := 0 to format0KernHdr.nPairs -1 do
+  begin
+    GetWord(fStream, fKernTable[i].left);
+    GetWord(fStream, fKernTable[i].right);
+    GetInt16(fStream, fKernTable[i].value);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontReader.GetTable_post;
+var
+  tbl: TFontTable;
+begin
+  if fTblIdxes[tblPost] < 0 then Exit;
+  tbl := fTables[fTblIdxes[tblPost]];
+  if (fStream.Size < tbl.offset + tbl.length) then Exit;
+  fStream.Position := Integer(tbl.offset);
+
+  GetWord(fStream,      fTbl_post.majorVersion);
+  GetWord(fStream,      fTbl_post.minorVersion);
+  GetFixed(fStream,     fTbl_post.italicAngle);
+  GetInt16(fStream,     fTbl_post.underlinePos);
+  GetInt16(fStream,     fTbl_post.underlineWidth);
+  GetCardinal(fStream,  fTbl_post.isFixedPitch);
+end;
+//------------------------------------------------------------------------------
+
+function FindKernInTable(glyphIdx: integer;
+  const kernTable: TArrayOfKernRecs): integer;
+var
+  i,l,r: integer;
+begin
+  l := 0;
+  r := High(kernTable);
+  while l <= r do
+  begin
+    Result := (l + r) shr 1;
+    i := kernTable[Result].left - glyphIdx;
+    if i < 0 then
+    begin
+      l := Result +1
+    end else
+    begin
+      if i = 0 then
+      begin
+        //found a match! Now find the very first one ...
+        while (Result > 0) and
+          (kernTable[Result-1].left = glyphIdx) do dec(Result);
+        Exit;
+      end;
+      r := Result -1;
+    end;
+  end;
+  Result := -1;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetGlyphKernList(glyphIdx: integer): TArrayOfTKern;
+var
+  i,j,len: integer;
+begin
+  result := nil;
+  i := FindKernInTable(glyphIdx, fKernTable);
+  if i < 0 then Exit;
+  len := Length(fKernTable);
+  j := i +1;
+  while (j < len) and (fKernTable[j].left = glyphIdx) do inc(j);
+  SetLength(Result, j - i);
+  for j := 0 to High(Result) do
+    with fKernTable[i+j] do
+  begin
+    Result[j].rightGlyphIdx := right;
+    Result[j].kernValue := value;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetGlyphPaths(glyphIdx: integer): TPathsEx;
+var
+  offset: cardinal;
+  glyfTbl: TFontTable;
+begin
+  result := nil;
+  if fTbl_head.indexToLocFmt = 0 then
+  begin
+    offset := fTbl_loca2[glyphIdx] *2;
+    if offset = fTbl_loca2[glyphIdx+1] *2 then Exit; //no contours
+  end else
+  begin
+    offset := fTbl_loca4[glyphIdx];
+    if offset = fTbl_loca4[glyphIdx+1] then Exit; //no contours
+  end;
+  glyfTbl := fTables[fTblIdxes[tblGlyf]];
+  if offset >= glyfTbl.length then Exit;
+  inc(offset, glyfTbl.offset);
+
+  fStream.Position := offset;
+  GetInt16(fStream, fTbl_glyf.numContours);
+  GetInt16(fStream, fTbl_glyf.xMin);
+  GetInt16(fStream, fTbl_glyf.yMin);
+  GetInt16(fStream, fTbl_glyf.xMax);
+  GetInt16(fStream, fTbl_glyf.yMax);
+
+  if fTbl_glyf.numContours < 0 then
+    result := GetCompositeGlyph else
+    result := GetSimpleGlyph;
+end;
+//------------------------------------------------------------------------------
+
+const
+  //glyf flags - simple
+  ON_CURVE                  = $1;
+  X_SHORT_VECTOR            = $2;
+  Y_SHORT_VECTOR            = $4;
+  REPEAT_FLAG               = $8;
+  X_DELTA                   = $10;
+  Y_DELTA                   = $20;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetSimpleGlyph: TPathsEx;
+var
+  i,j: integer;
+  instructLen: WORD;
+  flag, repeats: byte;
+  contourEnds: TArrayOfWord;
+begin
+  SetLength(contourEnds, fTbl_glyf.numContours);
+  for i := 0 to High(contourEnds) do
+    GetWord(fStream, contourEnds[i]);
+
+  //hints are currently ignored
+  GetWord(fStream, instructLen);
+  fStream.Position := fStream.Position + instructLen;
+
+  setLength(result, fTbl_glyf.numContours);
+  setLength(result[0], contourEnds[0] +1);
+  for i := 1 to High(result) do
+    setLength(result[i], contourEnds[i] - contourEnds[i-1]);
+
+  repeats := 0;
+  for i := 0 to High(result) do
+  begin
+    for j := 0 to High(result[i]) do
+    begin
+      if repeats = 0 then
+      begin
+        GetByte(fStream, flag);
+        if flag and REPEAT_FLAG = REPEAT_FLAG then
+          GetByte(fStream, repeats);
+      end else
+        dec(repeats);
+      result[i][j].flag := flag;
+    end;
+  end;
+  GetPathCoords(result);
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontReader.GetPathCoords(var paths: TPathsEx);
+var
+  i,j: integer;
+  xi,yi: Int16;
+  flag, xb,yb: byte;
+  pt: TPoint;
+begin
+  if fTbl_glyf.numContours = 0 then Exit;
+
+  //get X coords
+  pt := Point(0,0);
+  xi := 0;
+  for i := 0 to high(paths) do
+  begin
+    for j := 0 to high(paths[i]) do
+    begin
+      flag := paths[i][j].flag;
+      if flag and X_SHORT_VECTOR = X_SHORT_VECTOR then
+      begin
+        GetByte(fStream, xb);
+        if (flag and X_DELTA) = 0 then
+          dec(pt.X, xb) else
+          inc(pt.X, xb);
+      end else
+      begin
+        if flag and X_DELTA = 0 then
+        begin
+          GetInt16(fStream, xi);
+          pt.X := pt.X + xi;
+        end;
+      end;
+      paths[i][j].pt.X := pt.X;
+    end;
+  end;
+
+  //get Y coords
+  yi := 0;
+  for i := 0 to high(paths) do
+  begin
+    for j := 0 to high(paths[i]) do
+    begin
+      flag := paths[i][j].flag;
+      if flag and Y_SHORT_VECTOR = Y_SHORT_VECTOR then
+      begin
+        GetByte(fStream, yb);
+        if (flag and Y_DELTA) = 0 then
+          dec(pt.Y, yb) else
+          inc(pt.Y, yb);
+      end else
+      begin
+        if flag and Y_DELTA = 0 then
+        begin
+          GetInt16(fStream, yi);
+          pt.Y := pt.Y + yi;
+        end;
+      end;
+      paths[i][j].pt.Y := pt.Y;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function OnCurve(flag: byte): Boolean;
+begin
+  result := flag and ON_CURVE <> 0;
+end;
+//------------------------------------------------------------------------------
+
+function MidPoint(const pt1, pt2: TPointEx): TPointEx;
+begin
+  Result.pt.X := (pt1.pt.X + pt2.pt.X) / 2;
+  Result.pt.Y := (pt1.pt.Y + pt2.pt.Y) / 2;
+  Result.flag := ON_CURVE;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.ConvertSplinesToBeziers(const pathsEx: TPathsEx): TPathsEx;
+var
+  i,j,k: integer;
+  pt: TPointEx;
+  prevOnCurve: Boolean;
+begin
+  SetLength(Result, Length(pathsEx));
+  for i := 0 to High(pathsEx) do
+  begin
+    SetLength(Result[i], Length(pathsEx[i]) *2);
+    Result[i][0] := pathsEx[i][0]; k := 1;
+    prevOnCurve := true;
+    for j := 1 to High(pathsEx[i]) do
+    begin
+      if OnCurve(pathsEx[i][j].flag) then
+      begin
+        prevOnCurve := true;
+      end
+      else if not prevOnCurve then
+      begin
+        pt := MidPoint(pathsEx[i][j-1], pathsEx[i][j]);
+        Result[i][k] := pt; inc(k);
+      end else
+        prevOnCurve := false;
+      Result[i][k] := pathsEx[i][j]; inc(k);
+    end;
+    SetLength(Result[i], k);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure AppendPathsEx(var paths: TPathsEx; const extra: TPathsEx);
+var
+  i, len1, len2: integer;
+begin
+  len2 := length(extra);
+  len1 := length(paths);
+  setLength(paths, len1 + len2);
+  for i := 0 to len2 -1 do
+    paths[len1+i] := Copy(extra[i], 0, length(extra[i]));
+end;
+//------------------------------------------------------------------------------
+
+procedure AffineTransform(const a,b,c,d,e,f: double; var pathsEx: TPathsEx);
+const
+  q = 9.2863575e-4; // 33/35536
+var
+  i,j: integer;
+  m0,n0,m,n,xx: double;
+begin
+  m0 := max(abs(a), abs(b));
+  n0 := max(abs(c), abs(d));
+
+  if (m0 = 0) or (n0 = 0) then
+  begin
+    if (e = 0) and (f = 0) then Exit;
+
+    for i := 0 to High(pathsEx) do
+      for j := 0 to High(pathsEx[i]) do
+        with pathsEx[i][j].pt do
+        begin
+          X := X + e;
+          y := Y + f;
+        end;
+
+  end else
+  begin
+    //see https://developer.apple.com/fonts ...
+    //... /TrueType-Reference-Manual/RM06/Chap6glyf.html
+
+    if (abs(a)-abs(c))< q then m := 2 * m0 else m := m0;
+    if (abs(b)-abs(d))< q then n := 2 * n0 else n := n0;
+
+    for i := 0 to High(pathsEx) do
+      for j := 0 to High(pathsEx[i]) do
+        with pathsEx[i][j].pt do
+        begin
+          xx := m * ((a/m)*X + (c/m)*Y + e);
+          y := m * ((b/n)*X + (d/n)*Y + f);
+          X := xx;
+        end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetCompositeGlyph: TPathsEx;
+var
+  streamPos: integer;
+  flag, glyphIndex: WORD;
+  arg1b, arg2b: ShortInt;
+  arg1i, arg2i: Int16;
+  tmp: single;
+  a,b,c,d,e,f: double;
+  componentPaths: TPathsEx;
+  tbl_glyf_old: TFontTable_Glyf;
+const
+  ARG_1_AND_2_ARE_WORDS     = $1;
+  ARGS_ARE_XY_VALUES        = $2;
+  ROUND_XY_TO_GRID          = $4;
+  WE_HAVE_A_SCALE           = $8;
+  MORE_COMPONENTS           = $20;
+  WE_HAVE_AN_X_AND_Y_SCALE  = $40;
+  WE_HAVE_A_TWO_BY_TWO      = $80;
+  WE_HAVE_INSTRUCTIONS      = $100;
+  USE_MY_METRICS            = $200;
+begin
+  result := nil;
+  flag := MORE_COMPONENTS;
+
+  while (flag and MORE_COMPONENTS <> 0) do
+  begin
+    glyphIndex := 0;
+    a := 0; b := 0; c := 0; d := 0; e := 0; f := 0;
+
+    GetWord(fStream, flag);
+    GetWord(fStream, glyphIndex);
+
+    if (flag and ARG_1_AND_2_ARE_WORDS <> 0) then
+    begin
+      GetInt16(fStream, arg1i);
+      GetInt16(fStream, arg2i);
+      if (flag and ARGS_ARE_XY_VALUES <> 0) then
+      begin
+        e := arg1i;
+        f := arg2i;
+      end;
+    end else
+    begin
+      GetShortInt(fStream, arg1b);
+      GetShortInt(fStream, arg2b);
+      if (flag and ARGS_ARE_XY_VALUES <> 0) then
+      begin
+        e := arg1b;
+        f := arg2b;
+      end;
+    end;
+
+    if (flag and WE_HAVE_A_SCALE <> 0) then
+    begin
+      Get2Dot14(fStream, tmp);
+      a := tmp; d := tmp;
+    end
+    else if (flag and WE_HAVE_AN_X_AND_Y_SCALE <> 0) then
+    begin
+      Get2Dot14(fStream, tmp); a := tmp;
+      Get2Dot14(fStream, tmp); d := tmp;
+    end
+    else if (flag and WE_HAVE_A_TWO_BY_TWO <> 0) then
+    begin
+      Get2Dot14(fStream, tmp); a := tmp;
+      Get2Dot14(fStream, tmp); b := tmp;
+      Get2Dot14(fStream, tmp); c := tmp;
+      Get2Dot14(fStream, tmp); d := tmp;
+    end;
+
+    tbl_glyf_old := fTbl_glyf;
+
+    streamPos := fStream.Position;
+    componentPaths := GetGlyphPaths(glyphIndex);
+    fStream.Position := streamPos;
+
+    if (flag and ARGS_ARE_XY_VALUES <> 0) then
+    begin
+      if (flag and USE_MY_METRICS <> 0) then
+      begin
+        if Result <> nil then
+          AffineTransform(a,b,c,d,e,f, result);
+      end else
+        AffineTransform(a,b,c,d,e,f, componentPaths);
+    end;
+
+    if tbl_glyf_old.numContours > 0 then
+    begin
+      inc(fTbl_glyf.numContours, tbl_glyf_old.numContours);
+      fTbl_glyf.xMin := Min(fTbl_glyf.xMin, tbl_glyf_old.xMin);
+      fTbl_glyf.xMax := Max(fTbl_glyf.xMax, tbl_glyf_old.xMax);
+      fTbl_glyf.yMin := Min(fTbl_glyf.yMin, tbl_glyf_old.yMin);
+      fTbl_glyf.yMax := Max(fTbl_glyf.yMax, tbl_glyf_old.yMax);
+    end;
+
+    AppendPathsEx(result, componentPaths);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetGlyphInfo(unicode: Word; out paths: TPathsD;
+  out nextX: integer; out glyphMetrics: TGlyphMetrics): Boolean;
+var
+  i,j, glyphIdx: integer;
+  pt2: TPointEx;
+  bez: TPathD;
+  pathsEx: TPathsEx;
+begin
+  paths  := nil;
+  Result := IsValidFontFormat;
+  if not Result then Exit;
+
+  glyphIdx := GetGlyphIdxFromCmapIdx(unicode);
+  if not GetGlyphHorzMetrics(glyphIdx) then Exit;
+
+  pathsEx := GetGlyphPaths(glyphIdx); //gets raw splines
+  pathsEx := ConvertSplinesToBeziers(pathsEx);
+  glyphMetrics := GetGlyphMetricsInternal(glyphIdx); //nb: must follow GetGlyphPaths
+  nextX   := fTbl_hmtx.advanceWidth;
+  if pathsEx = nil then Exit; //eg space character
+
+  //now flatten ...
+  setLength(paths, length(pathsEx));
+  for i := 0 to High(pathsEx) do
+  begin
+    SetLength(paths[i],1);
+    paths[i][0] := pathsEx[i][0].pt;
+    for j := 1 to High(pathsEx[i]) do
+    begin
+      if OnCurve(pathsEx[i][j].flag) then
+      begin
+        AppendPoint(paths[i], pathsEx[i][j].pt);
+      end else
+      begin
+        if j = High(pathsEx[i]) then
+          pt2 := pathsEx[i][0] else
+          pt2 := pathsEx[i][j+1];
+        bez := FlattenQBezier(pathsEx[i][j-1].pt,
+                  pathsEx[i][j].pt, pt2.pt);
+        AppendPath(paths[i], bez);
+      end;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetFontInfo: TFontInfo;
+begin
+  if not IsValidFontFormat then
+  begin
+    result.faceName := '';
+    result.style := '';
+    result.unitsPerEm := 0;
+  end else
+  begin
+    result := fFontInfo;
+    //and updated the record with everything except the strings
+    result.unitsPerEm  := fTbl_head.unitsPerEm;
+    result.xMin        := fTbl_head.xMin;
+    result.xMax        := fTbl_head.xMax;
+    result.yMin        := fTbl_head.yMin;
+    result.yMax        := fTbl_head.yMax;
+
+    //note: the following three fields "represent the design
+    //intentions of the font's creator rather than any computed value"
+    //https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6hhea.html
+    result.ascent      := fTbl_hhea.ascent;
+    result.descent     := abs(fTbl_hhea.descent);
+    result.lineGap     := fTbl_hhea.lineGap;
+
+    result.advWidthMax := fTbl_hhea.advWidthMax;
+    result.minLSB      := fTbl_hhea.minLSB;
+    result.minRSB      := fTbl_hhea.minRSB;
+    result.xMaxExtent  := fTbl_hhea.xMaxExtent;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetGlyphMetricsInternal(glyphIdx: integer): TGlyphMetrics;
+begin
+  if IsValidFontFormat then
+  begin
+    result.glyphIdx := glyphIdx;
+    result.unitsPerEm  := fTbl_head.unitsPerEm;
+    result.glyf := fTbl_glyf;
+    result.hmtx := ftbl_hmtx;
+    Result.kernList := GetGlyphKernList(glyphIdx);
+  end else
+    FillChar(result, sizeOf(Result), 0)
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetWeight: integer;
+var
+  glyph: TPathsD;
+  i, dummy: integer;
+  accum: Cardinal;
+  gm: TGlyphMetrics;
+  rec: TRectD;
+  img: TImage32;
+  p: PARGB;
+const
+  imgSize = 16;
+  k = 5; //empirical constant
+begin
+  //get an empirical weight based on the character 'G'
+  result := 0;
+  if not IsValidFontFormat then Exit;
+  if fFontWeight > 0 then
+  begin
+    Result := fFontWeight;
+    Exit;
+  end;
+  GetGlyphInfo(Ord('G'),glyph, dummy, gm);
+  rec := GetBoundsD(glyph);
+  glyph := Img32.Vector.OffsetPath(glyph, -rec.Left, -rec.Top);
+  glyph := Img32.Vector.ScalePath(glyph,
+    imgSize/rec.Width, imgSize/rec.Height);
+  img := TImage32.Create(imgSize,imgSize);
+  try
+    DrawPolygon(img, glyph, frEvenOdd, clBlack32);
+    accum := 0;
+    p := PARGB(img.PixelBase);
+    for i := 0 to imgSize * imgSize do
+    begin
+      inc(accum, p.A);
+      inc(p);
+    end;
+  finally
+    img.Free;
+  end;
+  fFontWeight := Max(100, Min(900,
+    Round(k * accum / (imgSize * imgSize * 100)) * 100));
+  Result := fFontWeight;
+end;
+//------------------------------------------------------------------------------
+
+function TFontReader.GetFontFamily: TTtfFontFamily;
+var
+  dummy: integer;
+  glyphsT, glyphsI, glyphsM: TPathsD;
+  gmT, gmI, gmM: TGlyphMetrics;
+begin
+  result := ttfUnknown;
+  if (fTbl_post.majorVersion > 0) and
+    (fTbl_post.isFixedPitch <> 0) then
+  begin
+    result := ttfMonospace;
+    Exit;
+  end else
+  begin
+    if not GetGlyphInfo(Ord('T'), glyphsT, dummy, gmT) or
+      not Assigned(glyphsT) or
+      not GetGlyphInfo(Ord('i'), glyphsI, dummy, gmI) or
+      not GetGlyphInfo(Ord('m'), glyphsM, dummy, gmM) then
+        Exit;
+    if gmi.hmtx.advanceWidth = gmm.hmtx.advanceWidth then
+      Result := ttfMonospace
+    else if Length(glyphsT[0]) <= 12 then //probably <= 8 is fine too.
+      Result := ttfSansSerif else
+      Result := ttfSerif;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TFontCache
+//------------------------------------------------------------------------------
+
+constructor TFontCache.Create(fontReader: TFontReader; fontHeight: double);
+begin
+{$IFDEF XPLAT_GENERICS}
+  fGlyphInfoList := TList<PGlyphInfo>.Create;
+{$ELSE}
+  fGlyphInfoList := TList.Create;
+{$ENDIF}
+  fSorted := false;
+  fUseKerning := true;
+  fFlipVert := true;
+
+  fFontHeight := fontHeight;
+  SetFontReader(fontReader);
+end;
+//------------------------------------------------------------------------------
+
+destructor TFontCache.Destroy;
+begin
+  SetFontReader(nil);
+  Clear;
+  NotifyRecipients(inDestroy);
+  fGlyphInfoList.Free;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.ReceiveNotification(Sender: TObject; notify: TImg32Notification);
+begin
+  if Sender <> fFontReader then
+    raise Exception.Create(rsFontCacheError);
+  if notify = inStateChange then
+  begin
+    Clear;
+    UpdateScale;
+  end else
+    SetFontReader(nil);
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.NotifyRecipients(notifyFlag: TImg32Notification);
+var
+  i: integer;
+begin
+  for i := High(fRecipientList) downto 0 do
+    try
+      //when destroying in in a finalization section
+      //it's possible for recipients to have been destroyed
+      //without their destructors being called.
+      fRecipientList[i].ReceiveNotification(self, notifyFlag);
+    except
+    end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.AddRecipient(recipient: INotifyRecipient);
+var
+  len: integer;
+begin
+  len := Length(fRecipientList);
+  SetLength(fRecipientList, len+1);
+  fRecipientList[len] := Recipient;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.DeleteRecipient(recipient: INotifyRecipient);
+var
+  i, highI: integer;
+begin
+  highI := High(fRecipientList);
+  i := highI;
+  while (i >= 0) and (fRecipientList[i] <> Recipient) do dec(i);
+  if i < 0 then Exit;
+  if i < highI then
+    Move(fRecipientList[i+i], fRecipientList[i],
+      (highI - i) * SizeOf(INotifyRecipient));
+  SetLength(fRecipientList, highI);
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.Clear;
+var
+  i: integer;
+begin
+  for i := 0 to fGlyphInfoList.Count -1 do
+    Dispose(PGlyphInfo(fGlyphInfoList[i]));
+  fGlyphInfoList.Clear;
+  fSorted := false;
+end;
+//------------------------------------------------------------------------------
+
+{$IFDEF XPLAT_GENERICS}
+function FindInSortedList(charOrdinal: WORD; glyphList: TList<PGlyphInfo>): integer;
+{$ELSE}
+function FindInSortedList(charOrdinal: WORD; glyphList: TList): integer;
+{$ENDIF}
+var
+  i,l,r: integer;
+begin
+  //binary search the sorted list ...
+  l := 0;
+  r := glyphList.Count -1;
+  while l <= r do
+  begin
+    Result := (l + r) shr 1;
+    i := PGlyphInfo(glyphList[Result]).unicode - charOrdinal;
+    if i < 0 then
+    begin
+      l := Result +1
+    end else
+    begin
+      if i = 0 then Exit;
+      r := Result -1;
+    end;
+  end;
+  Result := -1;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.FoundInList(charOrdinal: WORD): Boolean;
+begin
+  if not fSorted then Sort;
+  result := FindInSortedList(charOrdinal, fGlyphInfoList) >= 0;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.GetMissingGlyphs(const ordinals: TArrayOfWord);
+var
+  i, len: integer;
+begin
+  if not IsValidFont then Exit;
+  len := Length(ordinals);
+  for i := 0 to len -1 do
+  begin
+    if ordinals[i] < 32 then continue
+    else if not FoundInList(ordinals[i]) then AddGlyph(ordinals[i]);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.IsValidFont: Boolean;
+begin
+  Result := assigned(fFontReader) and fFontReader.IsValidFontFormat;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetAscent: double;
+begin
+  if not IsValidFont then
+    Result := 0
+  else with fFontReader.FontInfo do
+    Result := Max(ascent, yMax) * fScale;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetDescent: double;
+begin
+  if not IsValidFont then
+    Result := 0
+  else with fFontReader.FontInfo do
+    Result := Max(descent, -yMin) * fScale;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetLineHeight: double;
+begin
+  if not IsValidFont then Result := 0
+  else Result := Ascent + Descent;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetYyHeight: double;
+var
+  minY, maxY: double;
+begin
+  //nb: non-inverted Y coordinates.
+  maxY := GetCharInfo(ord('Y')).metrics.glyf.yMax;
+  minY := GetCharInfo(ord('y')).metrics.glyf.yMin;
+  Result := (maxY - minY) * fScale;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.VerticalFlip(var paths: TPathsD);
+var
+  i,j: integer;
+begin
+  for i := 0 to High(paths) do
+    for j := 0 to High(paths[i]) do
+      with paths[i][j] do Y := -Y;
+end;
+//------------------------------------------------------------------------------
+
+function FindInKernList(glyphIdx: integer; const kernList: TArrayOfTKern): integer;
+var
+  i,l,r: integer;
+begin
+  l := 0;
+  r := High(kernList);
+  while l <= r do
+  begin
+    Result := (l + r) shr 1;
+    i := kernList[Result].rightGlyphIdx - glyphIdx;
+    if i < 0 then
+    begin
+      l := Result +1
+    end else
+    begin
+      if i = 0 then Exit; //found!
+      r := Result -1;
+    end;
+  end;
+  Result := -1;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetCharInfo(charOrdinal: WORD): PGlyphInfo;
+var
+  listIdx: integer;
+begin
+  Result := nil;
+  if not fSorted then Sort;
+  listIdx := FindInSortedList(charOrdinal, fGlyphInfoList);
+  if listIdx < 0 then
+  begin
+    if not IsValidFont then Exit;
+    Result := AddGlyph(Ord(charOrdinal));
+  end else
+    Result := PGlyphInfo(fGlyphInfoList[listIdx]);
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetCharOffsets(const text: UnicodeString;
+  interCharSpace: double): TArrayOfDouble;
+var
+  i,j, len: integer;
+  ordinals: TArrayOfWord;
+  glyphInfo: PGlyphInfo;
+  thisX: double;
+  prevGlyphKernList: TArrayOfTKern;
+begin
+  len := length(text);
+  SetLength(ordinals, len);
+  for i := 0 to len -1 do
+    ordinals[i] := Ord(text[i+1]);
+  SetLength(Result, len +1);
+  Result[0] := 0;
+  if len = 0 then Exit;
+  GetMissingGlyphs(ordinals);
+
+  thisX := 0;
+  prevGlyphKernList := nil;
+  for i := 0 to High(ordinals) do
+  begin
+    glyphInfo := GetCharInfo(ordinals[i]);
+    if not assigned(glyphInfo) then Break;
+    if fUseKerning and assigned(prevGlyphKernList) then
+    begin
+      j := FindInKernList(glyphInfo.metrics.glyphIdx, prevGlyphKernList);
+      if (j >= 0) then
+        thisX := thisX + prevGlyphKernList[j].kernValue*fScale;
+    end;
+    Result[i] := thisX;
+    thisX := thisX + glyphInfo.metrics.hmtx.advanceWidth*fScale +interCharSpace;
+    prevGlyphKernList := glyphInfo.metrics.kernList;
+  end;
+  Result[len] := thisX - interCharSpace;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetTextWidth(const text: UnicodeString): double;
+var
+  offsets: TArrayOfDouble;
+begin
+  Result := 0;
+  if not IsValidFont then Exit;
+  offsets := GetCharOffsets(text);
+  Result := offsets[high(offsets)];
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetSpaceWidth: double;
+begin
+  Result := GetCharInfo(32).metrics.hmtx.advanceWidth * fScale;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetTextOutline(x, y: double;
+  const text: UnicodeString): TPathsD;
+var
+  dummy: double;
+begin
+  Result := GetTextOutline(x, y, text, dummy);
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetTextOutline(const rec: TRect;
+  wordList: TWordInfoList; tpm: TTextPageMetrics;
+  textAlign: TTextAlign; startLine, endLine: integer): TPathsD;
+var
+  i,j, a,b: integer;
+  x,y,lh, spcDx, lineWidth: double;
+  pp: TPathsD;
+  app: TArrayOfPathsD;
+begin
+  Result := nil;
+  if not Assigned(wordList) or (wordList.Count = 0) then Exit;
+
+  lh := GetLineHeight;
+  y := rec.Top;
+
+  if startLine < 0 then startLine := 0;
+  if (endLine < 0) or (endLine >= tpm.lineCount) then
+    endLine := tpm.lineCount -1;
+
+  for i := startLine to endLine do
+  begin
+    a := tpm.wordListOffsets[i];
+    b := tpm.wordListOffsets[i+1] -1;
+    if textAlign = taJustify then
+      spcDx := tpm.justifyDeltas[i] else
+      spcDx := 0;
+    lineWidth := tpm.lineWidths[i];
+
+    //ingore trailing spaces
+    while (b >= a) do
+      with wordList.GetWord(b) do
+        if aWord <= #32 then
+          dec(b) else
+          break;
+
+    case textAlign of
+      taRight   : x := rec.Left + (RectWidth(rec) - lineWidth);
+      taCenter  : x := rec.Left + (RectWidth(rec) - lineWidth)/2;
+      else        x := rec.Left;
+    end;
+
+    for j := a to b do
+      with wordList.GetWord(j) do
+        if aWord > #32 then
+        begin
+          app := OffsetPath(paths, x, y + Ascent);
+          pp := MergePathsArray(app);
+          AppendPath(Result, pp);
+          x := x + width;
+        end
+        else
+          x := x + width + spcDx;
+    y := y + lh;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetTextOutline(const rec: TRect;
+  const text: UnicodeString; textAlign: TTextAlign; textAlignV: TTextVAlign;
+  underlineIdx: integer): TPathsD;
+var
+  y,dy    : double;
+  wl      : TWordInfoList;
+  tpm     : TTextPageMetrics;
+begin
+  Result := nil;
+
+  wl := TWordInfoList.Create;
+  try
+    wl.SetText(text, Self, underlineIdx);
+    tpm := GetPageMetrics(RectWidth(rec), wl);
+    Result := GetTextOutline(rec, wl, tpm, textAlign, 0, -1);
+
+    case textAlignV of
+      tvaMiddle:
+        begin
+          y := GetLineHeight * tpm.lineCount;
+          dy := (RectHeight(rec) -y) /2 -1;
+        end;
+      tvaBottom:
+        begin
+          y := GetLineHeight * tpm.lineCount;
+          dy := (RectHeight(rec) -y);
+        end;
+      else
+        Exit;
+    end;
+    Result := OffsetPath(Result, 0, dy);
+  finally
+    wl.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetTextOutline(x, y: double; const text: UnicodeString;
+  out nextX: double; underlineIdx: integer): TPathsD;
+var
+  i: integer;
+  w, y2: double;
+  p: TPathD;
+  arrayOfGlyphs: TArrayOfPathsD;
+begin
+  Result := nil;
+  if not GetTextOutlineInternal(x, y, text,
+    arrayOfGlyphs, nextX, underlineIdx) then Exit;
+
+  if fUnderlined then
+  begin
+    w := LineHeight * lineFrac;
+    y2 := y + 1.5 *(1+w);
+    p := Rectangle(x, y2, nextX, y2 + w);
+    AppendPath(Result, p);
+  end;
+
+  for i := 0 to high(arrayOfGlyphs) do
+    AppendPath(Result, arrayOfGlyphs[i]);
+
+  if fStrikeOut then
+  begin
+    w := LineHeight * lineFrac;
+    y := y - LineHeight/4;
+    p := Rectangle(x, y , nextX, y + w);
+    AppendPath(Result, p);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetVerticalTextOutline(x, y: double;
+  const text: UnicodeString; interCharSpace: double): TPathsD;
+var
+  i, xxMax: integer;
+  glyphInfo: PGlyphInfo;
+  dx, dy: double;
+begin
+  Result := nil;
+  if not IsValidFont then Exit;
+
+  xxMax := 0;
+  for i := 1 to Length(text) do
+  begin
+    glyphInfo := GetCharInfo(ord(text[i]));
+    if not assigned(glyphInfo) then Exit;
+    with glyphInfo.metrics.glyf do
+      if xMax > xxMax then
+         xxMax := xMax;
+  end;
+
+  for i := 1 to Length(text) do
+  begin
+    glyphInfo := GetCharInfo(ord(text[i]));
+    with glyphInfo.metrics.glyf do
+    begin
+      dx :=  (xxMax - xMax) * 0.5 * scale;
+      y := y + yMax  * scale; //yMax = char ascent
+      dy := - yMin * scale;   //yMin = char descent
+    end;
+    AppendPath(Result, Img32.Vector.OffsetPath( glyphInfo.contours, x + dx, y));
+    if text[i] = #32 then
+      y := y + dy - interCharSpace else
+      y := y + dy + interCharSpace;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetTextOutlineInternal(x, y: double;
+  const text: UnicodeString; out glyphs: TArrayOfPathsD;
+  out nextX: double; underlineIdx: integer): Boolean;
+var
+  i,j, len  : integer;
+  dx,y2,w   : double;
+  unicodes  : TArrayOfWord;
+  glyphInfo : PGlyphInfo;
+  p         : TPathD;
+  currGlyph : TPathsD;
+  prevGlyphKernList: TArrayOfTKern;
+begin
+  len := Length(text);
+  unicodes := nil;
+  setLength(unicodes, len);
+  for i := 0 to len -1 do
+    unicodes[i] := Ord(text[i +1]);
+  Result := true;
+  GetMissingGlyphs(unicodes);
+  nextX := x;
+  prevGlyphKernList := nil;
+  dec(underlineIdx);//convert from 1 base to 0 base index
+  for i := 0 to len -1 do
+  begin
+    glyphInfo := GetCharInfo(unicodes[i]);
+    if not assigned(glyphInfo) then Break;
+    if fUseKerning and assigned(prevGlyphKernList) then
+    begin
+      j := FindInKernList(glyphInfo.metrics.glyphIdx, prevGlyphKernList);
+      if (j >= 0) then
+        nextX := nextX + prevGlyphKernList[j].kernValue * fScale;
+    end;
+
+    currGlyph := OffsetPath(glyphInfo.contours, nextX, y);
+    dx := glyphInfo.metrics.hmtx.advanceWidth * fScale;
+
+    if i = underlineIdx then
+    begin
+      w := LineHeight * lineFrac;
+      y2 := y + 1.5 * (1 + w);
+      p := Rectangle(nextX, y2, nextX +dx, y2 + w);
+      AppendPath(currGlyph, p);
+    end;
+
+    AppendPath(glyphs, currGlyph);
+    nextX := nextX + dx;
+    prevGlyphKernList := glyphInfo.metrics.kernList;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.GetAngledTextGlyphs(x, y: double;
+  const text: UnicodeString; angleRadians: double;
+  const rotatePt: TPointD; out nextPt: TPointD): TPathsD;
+begin
+  nextPt.Y := y;
+  Result := GetTextOutline(x,y, text, nextPt.X);
+  if not Assigned(Result) then Exit;
+  Result := RotatePath(Result, rotatePt, angleRadians);
+  RotatePoint(nextPt, PointD(x,y), angleRadians);
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.SetFontReader(newFontReader: TFontReader);
+begin
+  if newFontReader = fFontReader then Exit;
+  if Assigned(fFontReader) then
+  begin
+    fFontReader.DeleteRecipient(self as INotifyRecipient);
+    Clear;
+  end;
+  fFontReader := newFontReader;
+  if Assigned(fFontReader) then
+    fFontReader.AddRecipient(self as INotifyRecipient);
+  UpdateScale;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.UpdateScale;
+begin
+  if IsValidFont and (fFontHeight > 0) then
+  begin
+    fScale := fFontHeight / fFontReader.FontInfo.unitsPerEm;
+    NotifyRecipients(inStateChange);
+  end else
+  begin
+    fScale := 1;
+    NotifyRecipients(inDestroy);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.SetFontHeight(newHeight: double);
+begin
+  if fFontHeight = newHeight then Exit;
+  fFontHeight := abs(newHeight);
+  Clear;
+  UpdateScale;
+end;
+//------------------------------------------------------------------------------
+
+procedure FlipVert(var paths: TPathsD);
+var
+  i,j: integer;
+begin
+  for i := 0 to High(paths) do
+    for j := 0 to High(paths[i]) do
+      paths[i][j].Y := -paths[i][j].Y;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.SetFlipVert(value: Boolean);
+var
+  i: integer;
+  glyphInfo: PGlyphInfo;
+begin
+  if fFlipVert = value then Exit;
+  for i := 0 to fGlyphInfoList.Count -1 do
+  begin
+     glyphInfo := PGlyphInfo(fGlyphInfoList[i]);
+     FlipVert(glyphInfo.contours);
+  end;
+  fFlipVert := value;
+end;
+//------------------------------------------------------------------------------
+
+function GlyphSorter(glyph1, glyph2: pointer): integer;
+begin
+  Result := PGlyphInfo(glyph1).unicode - PGlyphInfo(glyph2).unicode;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontCache.Sort;
+begin
+{$IFDEF XPLAT_GENERICS}
+  fGlyphInfoList.Sort(TComparer<PGlyphInfo>.Construct(
+    function (const glyph1, glyph2: PGlyphInfo): integer
+    begin
+      Result := glyph1.unicode - glyph2.unicode;
+    end));
+{$ELSE}
+  fGlyphInfoList.Sort(GlyphSorter);
+{$ENDIF}
+  fSorted := true;
+end;
+//------------------------------------------------------------------------------
+
+function TFontCache.AddGlyph(unicode: Word): PGlyphInfo;
+var
+  dummy: integer;
+const
+  minLength = 0.25;
+begin
+
+  New(Result);
+  Result.unicode := unicode;
+  fFontReader.GetGlyphInfo(unicode,
+    Result.contours, dummy, Result.metrics);
+  fGlyphInfoList.Add(Result);
+
+  if fFontHeight > 0 then
+  begin
+    Result.contours := ScalePath(Result.contours, fScale);
+    //text rendering is about twice as fast when excess detail is removed
+    Result.contours :=
+      StripNearDuplicates(Result.contours, minLength, true);
+  end;
+
+  if fFlipVert then VerticalFlip(Result.contours);
+  fSorted := false;
+end;
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+function AppendSlash(const foldername: string): string;
+begin
+  Result := foldername;
+  if (Result = '') or (Result[Length(Result)] = '\') then Exit;
+  Result := Result + '\';
+end;
+//------------------------------------------------------------------------------
+
+{$IFDEF MSWINDOWS}
+
+procedure CheckFontHeight(var logFont: TLogFont);
+const
+  _96Div72 = 96/72;
+begin
+  if logFont.lfHeight > 0 then
+    logFont.lfHeight := -Round(DpiAware(logFont.lfHeight * _96Div72));
+end;
+//------------------------------------------------------------------------------
+
+function PointHeightToPixelHeight(pt: double): double;
+const
+  _96Div72 = 96/72;
+begin
+  Result := Abs(pt) * _96Div72;
+end;
+//------------------------------------------------------------------------------
+
+function GetFontFolder: string;
+var
+  pidl: PItemIDList;
+  path: array[0..MAX_PATH] of char;
+begin
+  SHGetSpecialFolderLocation(0, CSIDL_FONTS, pidl);
+  SHGetPathFromIDList(pidl, path);
+  CoTaskMemFree(pidl);
+  result := path;
+end;
+//------------------------------------------------------------------------------
+
+function GetInstalledTtfFilenames: TArrayOfString;
+var
+  cnt, buffLen: integer;
+  fontFolder: string;
+  sr: TSearchRec;
+  res: integer;
+begin
+  cnt := 0; buffLen := 1024;
+  SetLength(Result, buffLen);
+  fontFolder := AppendSlash(GetFontFolder);
+  res := FindFirst(fontFolder + '*.ttf', faAnyFile, sr);
+  while res = 0 do
+  begin
+    if cnt = buffLen then
+    begin
+      inc(buffLen, 128);
+      SetLength(Result, buffLen);
+    end;
+    Result[cnt] := fontFolder + sr.Name;
+    inc(cnt);
+    res := FindNext(sr);
+  end;
+  FindClose(sr);
+  SetLength(Result, cnt);
+end;
+{$ENDIF}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+function DrawText(image: TImage32; x, y: double; const text: UnicodeString;
+  font: TFontCache; textColor: TColor32 = clBlack32;
+    useClearType: Boolean = false;
+    clearTypeBgColor: TColor32 = clWhite32): double;
+var
+  glyphs: TPathsD;
+begin
+  Result := 0;
+  if (text = '') or not assigned(font) or not font.IsValidFont then Exit;
+  glyphs := font.GetTextOutline(x,y, text, Result);
+  if useClearType then
+    DrawPolygon_ClearType(image, glyphs,
+      frNonZero, textColor, clearTypeBgColor)
+  else
+    DrawPolygon(image, glyphs, frNonZero, textColor);
+end;
+//------------------------------------------------------------------------------
+
+function DrawText(image: TImage32; x, y: double; const text: UnicodeString;
+  font: TFontCache; renderer: TCustomRenderer): double;
+var
+  glyphs: TPathsD;
+begin
+  Result := 0;
+  if (text = '') or not assigned(font) or
+    not font.IsValidFont then Exit;
+  glyphs := font.GetTextOutline(x,y, text, Result);
+  DrawPolygon(image, glyphs, frNonZero, renderer);
+end;
+//------------------------------------------------------------------------------
+
+procedure DrawText(image: TImage32; const rec: TRect; const text: UnicodeString;
+  textAlign: TTextAlign; textAlignV: TTextVAlign; font: TFontCache;
+  textColor: TColor32 = clBlack32; useClearType: Boolean = false;
+  clearTypeBgColor: TColor32 = clWhite32);
+var
+  glyphs: TPathsD;
+begin
+  if (text = '') or not assigned(font) or
+    not font.IsValidFont then Exit;
+  glyphs := font.GetTextOutline(rec, text, textAlign, textAlignV);
+  if useClearType then
+    DrawPolygon_ClearType(image, glyphs, frNonZero, textColor, clearTypeBgColor)
+  else
+    DrawPolygon(image, glyphs, frNonZero, textColor);
+end;
+//------------------------------------------------------------------------------
+
+function DrawAngledText(image: TImage32;
+  x, y: double; angleRadians: double;
+  const text: UnicodeString; font: TFontCache;
+  textColor: TColor32 = clBlack32): TPointD;
+var
+  glyphs: TPathsD;
+  rotatePt: TPointD;
+begin
+  rotatePt := PointD(x,y);
+  if not assigned(font) or not font.IsValidFont then Exit;
+  glyphs := font.GetAngledTextGlyphs(x, y,
+    text, angleRadians, rotatePt, Result);
+  DrawPolygon(image, glyphs, frNonZero, textColor);
+end;
+//------------------------------------------------------------------------------
+
+function DrawVerticalText(image: TImage32; x, y, interCharSpace: double;
+  const text: UnicodeString; font: TFontCache;
+  textColor: TColor32 = clBlack32): double;
+var
+  i, xxMax: integer;
+  glyphs: TPathsD;
+  glyphInfo: PGlyphInfo;
+  dx, dy, scale: double;
+begin
+  Result := y;
+  if not assigned(font) or not font.IsValidFont then Exit;
+
+  xxMax := 0;
+  for i := 1 to Length(text) do
+  begin
+    glyphInfo := font.GetCharInfo(ord(text[i]));
+    if not assigned(glyphInfo) then Exit;
+    with glyphInfo.metrics.glyf do
+      if xMax > xxMax then
+         xxMax := xMax;
+  end;
+
+  scale := font.Scale;
+  for i := 1 to Length(text) do
+  begin
+    glyphInfo := font.GetCharInfo(ord(text[i]));
+    with glyphInfo.metrics.glyf do
+    begin
+      dx :=  (xxMax - xMax) * 0.5 * scale;
+      y := y + yMax  * scale; //yMax = char ascent
+      dy := - yMin * scale;   //yMin = char descent
+    end;
+    glyphs := Img32.Vector.OffsetPath( glyphInfo.contours, x + dx, y);
+    DrawPolygon(image, glyphs, frNonZero, textColor);
+    if text[i] = #32 then
+      y := y + dy - interCharSpace else
+      y := y + dy + interCharSpace;
+  end;
+  Result := y;
+end;
+//------------------------------------------------------------------------------
+
+type
+  TPathInfo = record
+    pt     : TPointD;
+    vector : TPointD;
+    angle  : Double;
+    dist   : double;
+  end;
+  TPathInfos = array of TPathInfo;
+
+function GetTextOutlineOnPath(const text: UnicodeString;
+  const path: TPathD; font: TFontCache; textAlign: TTextAlign;
+  perpendicOffset: integer = 0; charSpacing: double = 0): TPathsD;
+var
+  dummy: integer;
+begin
+  Result := GetTextOutlineOnPath(text, path, font, textAlign,
+    perpendicOffset, charSpacing, dummy);
+end;
+//------------------------------------------------------------------------------
+
+function GetTextOutlineOnPath(const text: UnicodeString;
+  const path: TPathD; font: TFontCache; textAlign: TTextAlign;
+  perpendicOffset: integer; charSpacing: double;
+  out charsThatFit: integer): TPathsD; overload;
+var
+  pathLen: integer;
+  pathInfos: TPathInfos;
+
+  function GetPathInfo(var startIdx: integer; offset: double): TPathInfo;
+  begin
+    while startIdx <= pathLen do
+    begin
+      if pathInfos[startIdx].dist > offset then break;
+      inc(startIdx);
+    end;
+    Result := pathInfos[startIdx -1];
+    if Result.angle >= 0 then Exit; //ie already initialized
+    Result.angle  := GetAngle(path[startIdx-1], path[startIdx]);
+    Result.vector := GetUnitVector(path[startIdx-1], path[startIdx]);
+    Result.pt     := path[startIdx -1];
+  end;
+
+var
+  i, pathInfoIdx: integer;
+  textWidth, left, center, center2, scale, dist, dx: double;
+  glyph: PGlyphInfo;
+  offsets: TArrayOfDouble;
+  pathInfo: TPathInfo;
+  pt, rotatePt: TPointD;
+  tmpPaths: TPathsD;
+begin
+  Result := nil;
+  pathLen := Length(path);
+  charsThatFit := Length(text);
+
+  offsets := font.GetCharOffsets(text, charSpacing);
+  textWidth := offsets[charsThatFit];
+
+  setLength(pathInfos, pathLen +1);
+  if (pathLen < 2) or (charsThatFit = 0) then Exit;
+
+  dist := 0;
+  pathInfos[0].angle := -1;
+  pathInfos[0].dist := 0;
+  for i:= 1 to pathLen -1 do
+  begin
+    pathInfos[i].angle := -1; //flag uninitialized.
+    dist := dist + Distance(path[i-1], path[i]);
+    pathInfos[i].dist := dist;
+  end;
+
+  //truncate text that doesn't fit ...
+  if offsets[charsThatFit] -
+    ((offsets[charsThatFit] - offsets[charsThatFit-1])*0.5) > dist then
+  begin
+    repeat
+      dec(charsThatFit);
+    until offsets[charsThatFit] <= dist;
+    //break text word boundaries
+    while (charsThatFit > 1) and (text[charsThatFit] <> #32) do
+      dec(charsThatFit);
+    if charsThatFit = 0 then charsThatFit := 1;
+  end;
+
+  case textAlign of
+    taCenter: Left := (dist - textWidth) * 0.5;
+    taRight : Left := dist - textWidth;
+    else      Left := 0;
+  end;
+
+  scale := font.Scale;
+  Result := nil;
+  pathInfoIdx := 1;
+  for i := 1 to charsThatFit do
+  begin
+    glyph :=  font.GetCharInfo(Ord(text[i]));
+    with glyph.metrics do
+      center := (glyf.xMax - glyf.xMin) * scale * 0.5;
+    center2 := left + center;
+    left := left + glyph.metrics.hmtx.advanceWidth * scale + charSpacing;
+    pathInfo := GetPathInfo(pathInfoIdx, center2);
+    rotatePt := PointD(center, -perpendicOffset);
+    tmpPaths := RotatePath(glyph.contours, rotatePt, pathInfo.angle);
+    dx := center2 - pathInfo.dist;
+    pt.X := pathInfo.pt.X + pathInfo.vector.X * dx - rotatePt.X;
+    pt.Y := pathInfo.pt.Y + pathInfo.vector.Y * dx - rotatePt.Y;
+
+    tmpPaths := OffsetPath(tmpPaths, pt.X, pt.Y);
+    AppendPath(Result, tmpPaths);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TWordInfo
+//------------------------------------------------------------------------------
+
+constructor TWordInfo.Create(owner: TWordInfoList; idx: integer);
+begin
+  index := idx;
+end;
+
+//------------------------------------------------------------------------------
+// TWordInfoList
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.SetText(const text: UnicodeString;
+  font: TFontCache; underlineIdx: integer);
+var
+  len: integer;
+  spaceW: double;
+  p, p2, pEnd: PWideChar;
+  s: UnicodeString;
+begin
+  if not Assigned(font) then Exit;
+
+  BeginUpdate;
+  try
+    Clear;
+    spaceW := font.GetSpaceWidth;
+    p := PWideChar(text);
+    pEnd := p;
+    Inc(pEnd, Length(text));
+    while p < pEnd do
+    begin
+      if (p^ <= #32) then
+      begin
+        if (p^ = #32) then AddSpace(spaceW)
+        else if (p^ = #10) then AddNewline;
+
+        inc(p);
+        dec(underlineIdx);
+      end else
+      begin
+        p2 := p;
+        inc(p);
+        while (p < pEnd) and (p^ > #32) do inc(p);
+        len := p - p2;
+        SetLength(s, len);
+        Move(p2^, s[1], len * SizeOf(Char));
+        AddWord(font, s, underlineIdx);
+        dec(underlineIdx, len);
+      end;
+    end;
+  finally
+    EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.ApplyNewFont(font: TFontCache);
+var
+  i: integer;
+  spaceW, dummy: double;
+  wi: TWordInfo;
+begin
+  if not Assigned(font) then Exit;
+  spaceW := font.GetSpaceWidth;
+  BeginUpdate;
+  try
+    for i := 0 to Count -1 do
+    begin
+      wi := GetWord(i);
+      if wi.aWord <= #32 then
+      begin
+        if wi.aWord = #32 then wi.width := spaceW
+        else wi.width := 0;
+      end else
+      begin
+        font.GetTextOutlineInternal(0,0, wi.aWord, wi.paths, dummy);
+        wi.width := font.GetTextWidth(wi.aWord);
+      end;
+    end;
+  finally
+    EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+constructor TWordInfoList.Create;
+begin
+  inherited;
+{$IFDEF XPLAT_GENERICS}
+  fList := TList<TWordInfo>.Create;
+{$ELSE}
+  fList := TList.Create;
+{$ENDIF}
+end;
+//------------------------------------------------------------------------------
+
+destructor TWordInfoList.Destroy;
+begin
+  fOnChanged := nil;
+  Clear;
+  fList.Free;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+function TWordInfoList.GetWord(index: integer): TWordInfo;
+begin
+  if (index < 0) or (index >= fList.Count) then
+    raise Exception.Create(rsWordListRangeError);
+  Result :=  TWordInfo(fList.Items[index]);
+end;
+//------------------------------------------------------------------------------
+
+function TWordInfoList.GetText: UnicodeString;
+var
+  i: integer;
+begin
+  Result := '';
+  for i := 0 to Count -1 do
+    Result := Result + TWordInfo(fList.Items[i]).aWord;
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.AddNewline;
+begin
+  InsertNewline(MaxInt);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.AddSpace(font: TFontCache);
+begin
+  InsertSpace(font, MaxInt);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.AddSpace(spaceWidth: double);
+begin
+  InsertSpace(spaceWidth, MaxInt);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.AddWord(font: TFontCache;
+  const word: UnicodeString; underlineIdx: integer);
+begin
+  InsertWord(font, MaxInt, word, underlineIdx);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.InsertNewline(index: integer);
+var
+  i, cnt: integer;
+  newWord : TWordInfo;
+begin
+  cnt := fList.Count;
+  if (index > cnt) then index := cnt
+  else if (index < 0) then index := 0;
+
+  newWord := TWordInfo.Create(self, index);
+  newWord.aWord  := #10;
+  newWord.width := 0;
+  newWord.length := 1;
+  newWord.paths := nil;
+  fList.Insert(index, newWord);
+
+  //reindex
+  if index < cnt then
+    for i := index +1 to cnt do
+      TWordInfo(fList[i]).index := i;
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.InsertSpace(font: TFontCache; index: integer);
+var
+  width: double;
+begin
+  if not Assigned(font) or not font.IsValidFont then
+    raise Exception.Create(rsWordListFontError);
+  width := font.GetCharInfo(32).metrics.hmtx.advanceWidth * font.fScale;
+  InsertSpace(width, index);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.InsertSpace(spaceWidth: double; index: integer);
+var
+  i, cnt: integer;
+  newWord : TWordInfo;
+begin
+  cnt := fList.Count;
+  if (index > cnt) then index := cnt
+  else if (index < 0) then index := 0;
+
+  newWord := TWordInfo.Create(self, index);
+  newWord.aWord  := #32;
+  newWord.width := spaceWidth;
+  newWord.length := 1;
+  newWord.paths := nil;
+  fList.Insert(index, newWord);
+
+  //reindex
+  if index < cnt then
+    for i := index +1 to cnt do
+      TWordInfo(fList[i]).index := i;
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.InsertWord(font: TFontCache;
+  index: integer; const word: UnicodeString; underlineIdx: integer);
+var
+  i, cnt: integer;
+  width: double;
+  newWord : TWordInfo;
+  ap: TArrayOfPathsD;
+begin
+  if not Assigned(font) or not font.IsValidFont then
+    raise Exception.Create(rsWordListFontError);
+
+  font.GetTextOutlineInternal(0,0, word, ap, width, underlineIdx);
+  cnt := fList.Count;
+  if (index > cnt) then index := cnt
+  else if (index < 0) then index := 0;
+
+  newWord := TWordInfo.Create(self, index);
+  newWord.aWord  := word;
+  newWord.width := width;
+  newWord.length := Length(word);
+  newWord.paths := ap;
+  fList.Insert(index, newWord);
+
+  //reindex
+  if index < cnt then
+    for i := index +1 to cnt do
+      TWordInfo(fList[i]).index := i;
+end;
+//------------------------------------------------------------------------------
+
+function TWordInfoList.Count: integer;
+begin
+  Result := fList.Count;
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.Clear;
+var
+  i: integer;
+begin
+  for i := 0 to fList.Count -1 do
+      TWordInfo(fList.Items[i]).Free;
+  fList.Clear;
+  if Assigned(fOnChanged) then fOnChanged(Self);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.BeginUpdate;
+begin
+  inc(fUpdateCount);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.EndUpdate;
+begin
+  dec(fUpdateCount);
+  if (fUpdateCount = 0) then Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.Changed;
+begin
+  if Assigned(fOnChanged) then fOnChanged(Self);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.Delete(Index: Integer);
+begin
+  if (index < 0) or (index >= fList.Count) then
+    raise Exception.Create(rsWordListRangeError);
+  TWordInfo(fList.Items[index]).Free;
+  fList.Delete(index);
+  if Assigned(fOnChanged) then fOnChanged(Self);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.DeleteRange(startIdx, endIdx: Integer);
+var
+  i, cnt, cnt2: Integer;
+begin
+  if (startIdx < 0) or (endIdx >= fList.Count) then
+    raise Exception.Create(rsWordListRangeError);
+  for i := startIdx to endIdx do
+    TWordInfo(fList.Items[i]).Free;
+
+  //fList.DeleteRange(startIdx, endIdx - startIdx +1);
+  cnt := endIdx - startIdx +1;
+  cnt2 := fList.Count - cnt;
+  for i := startIdx to cnt2 -1 do
+    fList[i] := fList[i +cnt];
+  fList.Count := cnt2;
+
+  if Assigned(fOnChanged) then fOnChanged(Self);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWordInfoList.Edit(font: TFontCache;
+  index: Integer; const newWord: string);
+var
+  len: integer;
+  dummy: double;
+begin
+  if (index < 0) or (index >= fList.Count) then
+    raise Exception.Create(rsWordListRangeError);
+  len := system.Length(newWord);
+  if len = 0 then
+    Delete(index)
+  else if Assigned(font) then
+    with TWordInfo(fList.Items[index]) do
+    begin
+      aWord := newWord;
+      length := 1;
+      while (length < len) and (aWord[length+1] > #32) do
+        inc(length);
+      if length < len then SetLength(aWord, length);
+      width := font.GetTextWidth(aWord);
+      font.GetTextOutlineInternal(0,0,aWord, paths, dummy);
+      if Assigned(fOnChanged) then fOnChanged(Self);
+    end;
+end;
+//------------------------------------------------------------------------------
+
+function GetPageMetrics(lineWidth: double; wordList: TWordInfoList): TTextPageMetrics;
+var
+  arrayCnt, arrayCap: integer;
+
+  procedure CalcLineWidthsAndJustify(idx: integer);
+  var
+    i,j,k, spcCnt: integer;
+    x: double;
+    forceLeftAlign: Boolean;
+  begin
+    j := Result.wordListOffsets[idx] -1;
+    if j < 0 then Exit;
+
+    forceLeftAlign := wordList.GetWord(j).aWord = #10;
+    i := Result.wordListOffsets[idx -1];
+
+    while (j > i) and (wordList.GetWord(j).aWord = #32) do
+      dec(j);
+
+    spcCnt := 0;
+    x := 0;
+    for k := i to j do
+      with wordList.GetWord(k) do
+      begin
+        if aWord = #32 then inc(spcCnt);
+        x := x + width;
+      end;
+    Result.lineWidths[idx-1] := x;
+    if not forceLeftAlign and (spcCnt > 0) then
+      Result.justifyDeltas[idx-1] := (Result.maxLineWidth - x)/spcCnt;
+  end;
+
+  procedure AddLine(i: integer);
+  begin
+    if arrayCnt = arrayCap then
+    begin
+      inc(arrayCap, 16);
+      SetLength(Result.wordListOffsets, arrayCap);
+      SetLength(Result.justifyDeltas, arrayCap);
+      SetLength(Result.lineWidths, arrayCap);
+    end;
+    inc(Result.lineCount);
+    Result.wordListOffsets[arrayCnt] := i;
+    Result.justifyDeltas[arrayCnt] := 0.0;
+    if (arrayCnt > 0) then
+      CalcLineWidthsAndJustify(arrayCnt);
+    inc(arrayCnt);
+  end;
+
+var
+  i,j, cnt: integer;
+  x: double;
+  wi: TWordInfo;
+begin
+  Result.lineCount := 0;
+  Result.maxLineWidth := lineWidth;
+  Result.wordListOffsets := nil;
+  arrayCnt := 0; arrayCap := 0;
+  if not Assigned(wordList) or (wordList.Count = 0) then Exit;
+
+  i := 0; j := 0;
+  cnt := wordList.Count;
+  x := 0;
+
+  while (i < cnt) do
+  begin
+    wi := wordList.GetWord(i);
+    if (i = j) and (wi.aWord = #32) then
+    begin
+      inc(i); inc(j); Continue;
+    end;
+
+    if (wi.aWord = #10) then
+    begin
+      AddLine(j);
+      inc(i); j := i; x := 0;
+    end
+    else if (x + wi.width > lineWidth) then
+    begin
+      if j = i then Break; //word is too long for line. Todo: ??hiphenate
+      AddLine(j);
+      j := i; x := 0;
+    end else
+    begin
+      x := x + wi.width;
+      inc(i);
+    end;
+  end;
+
+  if (j < cnt)then AddLine(j); //add end short line
+  AddLine(cnt);
+  dec(Result.lineCount);
+  SetLength(Result.wordListOffsets, arrayCnt);
+  SetLength(Result.justifyDeltas, arrayCnt);
+  SetLength(Result.lineWidths, arrayCnt);
+  //make sure the 'real' last line isn't justified.
+  Result.justifyDeltas[arrayCnt-2] := 0;
+  //nb: the 'lineWidths' for the very last line may be longer
+  //than maxLineWidth when a word's width exceeds 'maxLineWidth
+end;
+
+//------------------------------------------------------------------------------
+// TFontManager
+//------------------------------------------------------------------------------
+
+constructor TFontManager.Create;
+begin
+  fMaxFonts := 20;
+{$IFDEF XPLAT_GENERICS}
+    fFontList := TList<TFontReader>.Create;
+{$ELSE}
+    fFontList:= TList.Create;
+{$ENDIF}
+end;
+//------------------------------------------------------------------------------
+
+destructor TFontManager.Destroy;
+begin
+  Clear;
+  fFontList.Free;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontManager.Clear;
+var
+  i: integer;
+begin
+  for i := 0 to fFontList.Count -1 do
+    with TFontReader(fFontList[i]) do
+    begin
+      fFontManager := nil;
+      Free;
+    end;
+  fFontList.Clear;
+end;
+//------------------------------------------------------------------------------
+
+function TFontManager.GetFont(const fontName: string): TFontReader;
+var
+  i: integer;
+begin
+  Result := nil;
+  for i := 0 to fFontList.Count -1 do
+    if SameText(TFontReader(fFontList[i]).fFontInfo.faceName, fontName) then
+    begin
+      Result := fFontList[i];
+      Exit;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+{$IFDEF MSWINDOWS}
+function TFontManager.Load(const fontName: string): TFontReader;
+begin
+  if fFontList.Count >= fMaxFonts then
+    raise Exception.Create(rsTooManyFonts);
+
+  Result := GetFont(fontname);
+  if Assigned(Result) then Exit;
+
+  Result := TFontReader.Create;
+  try
+    if not Result.Load(fontName) or
+      not ValidateAdd(Result) then
+        FreeAndNil(Result);
+  except
+    FreeAndNil(Result);
+  end;
+  if Assigned(Result) then
+    Result.fFontManager := self;
+end;
+//------------------------------------------------------------------------------
+{$ENDIF}
+
+function TFontManager.LoadFromStream(stream: TStream): TFontReader;
+begin
+  if fFontList.Count >= fMaxFonts then
+    raise Exception.Create(rsTooManyFonts);
+
+  Result := TFontReader.Create;
+  try
+    if not Result.LoadFromStream(stream) or
+      not ValidateAdd(Result) then
+        FreeAndNil(Result);
+  except
+    FreeAndNil(Result);
+  end;
+  if Assigned(Result) then
+    Result.fFontManager := self;
+end;
+//------------------------------------------------------------------------------
+
+function TFontManager.LoadFromResource(const resName: string; resType: PChar): TFontReader;
+begin
+  if fFontList.Count >= fMaxFonts then
+    raise Exception.Create(rsTooManyFonts);
+
+  Result := TFontReader.Create;
+  try
+    if not Result.LoadFromResource(resName, resType) or
+      not ValidateAdd(Result) then
+        FreeAndNil(Result);
+  except
+    FreeAndNil(Result);
+  end;
+  if Assigned(Result) then
+    Result.fFontManager := self;
+end;
+//------------------------------------------------------------------------------
+
+function TFontManager.LoadFromFile(const filename: string): TFontReader;
+begin
+  if fFontList.Count >= fMaxFonts then
+    raise Exception.Create(rsTooManyFonts);
+
+  Result := TFontReader.Create;
+  try
+    if not Result.LoadFromFile(filename) or
+      not ValidateAdd(Result) then
+        FreeAndNil(Result);
+  except
+    FreeAndNil(Result);
+  end;
+  if Assigned(Result) then
+    Result.fFontManager := self;
+end;
+//------------------------------------------------------------------------------
+
+function TFontManager.ValidateAdd(fr: TFontReader): Boolean;
+var
+  fr2: TFontReader;
+begin
+  Result := Assigned(fr);
+  if not Result then Exit;
+  //avoid adding duplicates
+  fr2 := GetBestMatchFont(fr.fFontInfo);
+  if not Assigned(fr2) or
+    ((fr.fFontInfo.macStyles <> fr2.fFontInfo.macStyles) or
+    not SameText(fr.fFontInfo.faceName, fr2.fFontInfo.faceName)) then
+    fFontList.Add(fr)
+  else
+      Result := false;
+end;
+//------------------------------------------------------------------------------
+
+function TFontManager.Delete(fontReader: TFontReader): Boolean;
+var
+  i: integer;
+begin
+  for i := 0 to fFontList.Count -1 do
+    if TFontReader(fFontList[i]) = fontReader then
+    begin
+      //make sure the FontReader object isn't destroying itself externally
+      if not fontReader.fDestroying then fontReader.Free;
+      fFontList.Delete(i);
+      Result := true;
+      Exit;
+    end;
+  Result := false;
+end;
+//------------------------------------------------------------------------------
+
+function TFontManager.GetBestMatchFont(const fontInfo: TFontInfo): TFontReader;
+
+  function StylesToInt(macstyles: TMacStyles): integer;
+  begin
+    if msBold in macStyles then
+      Result := 1 else Result := 0;
+    if msItalic in macStyles then inc(Result, 2);
+  end;
+
+  function FamilyToInt(fontFamily: TTtfFontFamily): integer;
+  begin
+    Result := Ord(fontFamily);
+  end;
+
+  function NameDiff(const name1, name2: string): integer;
+  begin
+    if SameText(name1, name2) then Result := 0 else Result := 1;
+  end;
+
+  function CompareFontInfos(const fi1, fi2: TFontInfo): integer;
+  var
+    styleDiff: integer;
+  begin
+    styleDiff := Abs(StylesToInt(fi1.macStyles) - StylesToInt(fi2.macStyles));
+    if styleDiff = 2 then Dec(styleDiff);
+    Result := styleDiff shl 8 +
+      Abs(FamilyToInt(fi1.fontFamily) - FamilyToInt(fi2.fontFamily)) shl 4 +
+      NameDiff(fi1.faceName, fi2.faceName);
+  end;
+
+var
+  i, bestIdx: integer;
+  bestDiff, currDiff: integer;
+begin
+  Result := nil;
+  if fFontList.Count = 0 then Exit;
+
+  bestIdx := 0;
+  bestDiff := CompareFontInfos(fontInfo,
+    TFontReader(fFontList[0]).fFontInfo);
+
+  for i := 1 to fFontList.Count -1 do
+  begin
+    currDiff := CompareFontInfos(fontInfo,
+      TFontReader(fFontList[i]).fFontInfo);
+    if (currDiff < bestDiff) then
+    begin
+      bestIdx := i;
+      bestDiff := currDiff;
+      if bestDiff = 0 then Break;
+    end;
+  end;
+  Result := TFontReader(fFontList[bestIdx]);
+end;
+//------------------------------------------------------------------------------
+
+procedure TFontManager.SetMaxFonts(value: integer);
+begin
+  if value < 0 then value := 0;
+  if value <= 0 then Clear
+  else while value > fFontList.Count do
+    Delete(TFontReader(fFontList[0]));
+  fMaxFonts := value;
+end;
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+function FontManager: TFontManager;
+begin
+  result := aFontManager;
+end;
+//------------------------------------------------------------------------------
+
+initialization
+  aFontManager := TFontManager.Create;
+
+finalization
+  aFontManager.Free;
+
+end.

+ 1017 - 0
components/Image32/source/Img32.Transform.pas

@@ -0,0 +1,1017 @@
+unit Img32.Transform;
+
+(*******************************************************************************
+* Author    :  Angus Johnson                                                   *
+* Version   :  4.3                                                             *
+* Date      :  27 September 2022                                               *
+* Website   :  http://www.angusj.com                                           *
+* Copyright :  Angus Johnson 2019-2021                                         *
+*                                                                              *
+* Purpose   :  Affine and projective transformation routines for TImage32      *
+*                                                                              *
+* License   :  Use, modification & distribution is subject to                  *
+*              Boost Software License Ver 1                                    *
+*              http://www.boost.org/LICENSE_1_0.txt                            *
+*******************************************************************************)
+
+interface
+
+{$I Img32.inc}
+
+uses
+  SysUtils, Classes, Math, Types,
+  Img32, Img32.Vector;
+
+type
+  TMatrixD = array [0..2, 0..2] of double;
+
+  //Matrix functions
+  function IsIdentityMatrix(const matrix: TMatrixD): Boolean;
+  function IsValidMatrix(const matrix: TMatrixD): Boolean;
+  function Matrix(const m00, m01, m02, m10, m11, m12, m20, m21, m22: double): TMatrixD;
+  function MatrixDeterminant(const matrix: TMatrixD): double;
+  function MatrixAdjugate(const matrix: TMatrixD): TMatrixD;
+  function MatrixMultiply(const modifier, matrix: TMatrixD): TMatrixD;
+
+  procedure MatrixApply(const matrix: TMatrixD;
+    var x, y: double); overload; {$IFDEF INLINE} inline; {$ENDIF}
+  procedure MatrixApply(const matrix: TMatrixD;
+    var pt: TPointD); overload; {$IFDEF INLINE} inline; {$ENDIF}
+  procedure MatrixApply(const matrix: TMatrixD; var rec: TRect); overload;
+  procedure MatrixApply(const matrix: TMatrixD; var rec: TRectD); overload;
+  procedure MatrixApply(const matrix: TMatrixD; var path: TPathD); overload;
+  procedure MatrixApply(const matrix: TMatrixD; var paths: TPathsD); overload;
+  function  MatrixInvert(var matrix: TMatrixD): Boolean;
+
+  //MatrixSkew: dx represents the delta offset of an X coordinate as a
+  //fraction of its Y coordinate, and likewise for dy. For example, if dx = 0.1
+  //and dy = 0, and the matrix is applied to the coordinate [20,15], then the
+  //transformed coordinate will become [20 + (15 * 0.1),10], ie [21.5,10].
+  procedure MatrixSkew(var matrix: TMatrixD; angleX, angleY: double);
+  procedure MatrixScale(var matrix: TMatrixD; scale: double); overload;
+  procedure MatrixScale(var matrix: TMatrixD; scaleX, scaleY: double); overload;
+  procedure MatrixRotate(var matrix: TMatrixD;
+    const center: TPointD; angRad: double);
+  procedure MatrixTranslate(var matrix: TMatrixD; dx, dy: double);
+
+  //AffineTransformImage: automagically resizes and translates the image
+  function AffineTransformImage(img: TImage32; matrix: TMatrixD): TPoint;
+
+  //ProjectiveTransform:
+  //  srcPts, dstPts => each path must contain 4 points
+  //  margins => the margins around dstPts (in the dest. projective).
+  //  Margins are only meaningful when srcPts are inside the image.
+  function ProjectiveTransform(img: TImage32;
+    const srcPts, dstPts: TPathD; const margins: TRect): Boolean;
+
+  function SplineVertTransform(img: TImage32; const topSpline: TPathD;
+    splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean;
+  function SplineHorzTransform(img: TImage32; const leftSpline: TPathD;
+    splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean;
+
+  function ExtractAngleFromMatrix(const mat: TMatrixD): double;
+  function ExtractScaleFromMatrix(const mat: TMatrixD): TSizeD;
+  function ExtractAvgScaleFromMatrix(const mat: TMatrixD): double;
+  procedure ExtractAllFromMatrix(const mat: TMatrixD;
+    out angle: double; out scale, skew, trans: TPointD);
+
+type
+  PWeightedColor = ^TWeightedColor;
+  TWeightedColor = {$IFDEF RECORD_METHODS} record {$ELSE} object {$ENDIF}
+  private
+    fAddCount : Integer;
+    fAlphaTot : Int64;
+    fColorTotR: Int64;
+    fColorTotG: Int64;
+    fColorTotB: Int64;
+    function GetColor: TColor32;
+  public
+    procedure Reset; {$IFDEF INLINE} inline; {$ENDIF}
+    procedure Add(c: TColor32; w: Integer = 1); overload;
+    procedure Add(const other: TWeightedColor); overload;
+      {$IFDEF INLINE} inline; {$ENDIF}
+    procedure Subtract(c: TColor32; w: Integer =1); overload;
+    procedure Subtract(const other: TWeightedColor); overload;
+      {$IFDEF INLINE} inline; {$ENDIF}
+    procedure AddWeight(w: Integer); {$IFDEF INLINE} inline; {$ENDIF}
+    property AddCount: Integer read fAddCount;
+    property Color: TColor32 read GetColor;
+    property Weight: integer read fAddCount;
+  end;
+  TArrayOfWeightedColor = array of TWeightedColor;
+
+const
+  IdentityMatrix: TMatrixD = ((1, 0, 0),(0, 1, 0),(0, 0, 1));
+
+implementation
+
+resourcestring
+  rsInvalidScale   = 'Invalid matrix scaling factor (0)';
+
+//------------------------------------------------------------------------------
+// Matrix functions
+//------------------------------------------------------------------------------
+
+function IsIdentityMatrix(const matrix: TMatrixD): Boolean;
+var
+  i,j: integer;
+const
+  matVal: array [boolean] of double = (0.0, 1.0);
+begin
+  result := false;
+  for i := 0 to 2 do
+    for j := 0 to 2 do
+      if matrix[i][j] <> matVal[j=i] then Exit;
+  Result := true;
+end;
+//------------------------------------------------------------------------------
+
+function IsValidMatrix(const matrix: TMatrixD): Boolean;
+begin
+  result := matrix[2][2] = 1.0;
+end;
+//------------------------------------------------------------------------------
+
+function Matrix(const m00, m01, m02, m10, m11, m12, m20, m21, m22: double): TMatrixD;
+begin
+  Result[0,0] := m00; Result[0,1] := m01; Result[0,2] := m02;
+  Result[1,0] := m10; Result[1,1] := m11; Result[1,2] := m12;
+  Result[2,0] := m20; Result[2,1] := m21; Result[2,2] := m22;
+end;
+//------------------------------------------------------------------------------
+
+function Det4(a1, a2, b1, b2: double): double; {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result := a1 * b2 - a2 * b1;
+end;
+//------------------------------------------------------------------------------
+
+function Det9(a1, a2, a3, b1, b2, b3, c1, c2, c3: double): double;
+{$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result := a1 * Det4(b2, b3, c2, c3) -
+            b1 * Det4(a2, a3, c2, c3) +
+            c1 * Det4(a2, a3, b2, b3);
+end;
+//------------------------------------------------------------------------------
+
+function MatrixDeterminant(const matrix: TMatrixD): double;
+{$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result := Det9(matrix[0,0], matrix[1,0], matrix[2,0],
+                 matrix[0,1], matrix[1,1], matrix[2,1],
+                 matrix[0,2], matrix[1,2], matrix[2,2]);
+end;
+//------------------------------------------------------------------------------
+
+function MatrixAdjugate(const matrix: TMatrixD): TMatrixD;
+begin
+  //https://en.wikipedia.org/wiki/Adjugate_matrix
+  Result[0,0] :=  Det4(matrix[1,1], matrix[1,2], matrix[2,1], matrix[2,2]);
+  Result[0,1] := -Det4(matrix[0,1], matrix[0,2], matrix[2,1], matrix[2,2]);
+  Result[0,2] :=  Det4(matrix[0,1], matrix[0,2], matrix[1,1], matrix[1,2]);
+
+  Result[1,0] := -Det4(matrix[1,0], matrix[1,2], matrix[2,0], matrix[2,2]);
+  Result[1,1] :=  Det4(matrix[0,0], matrix[0,2], matrix[2,0], matrix[2,2]);
+  Result[1,2] := -Det4(matrix[0,0], matrix[0,2], matrix[1,0], matrix[1,2]);
+
+  Result[2,0] :=  Det4(matrix[1,0], matrix[1,1], matrix[2,0], matrix[2,1]);
+  Result[2,1] := -Det4(matrix[0,0], matrix[0,1], matrix[2,0], matrix[2,1]);
+  Result[2,2] :=  Det4(matrix[0,0], matrix[0,1], matrix[1,0], matrix[1,1]);
+end;
+//------------------------------------------------------------------------------
+
+procedure MatrixApply(const matrix: TMatrixD; var x, y: double);
+var
+  tmpX: double;
+begin
+  tmpX := x;
+  x := tmpX * matrix[0, 0] + y * matrix[1, 0] + matrix[2, 0];
+  y := tmpX * matrix[0, 1] + y * matrix[1, 1] + matrix[2, 1];
+end;
+//------------------------------------------------------------------------------
+
+procedure MatrixApply(const matrix: TMatrixD; var pt: TPointD);
+var
+  tmpX: double;
+begin
+  tmpX := pt.x;
+  pt.X := tmpX * matrix[0, 0] + pt.Y * matrix[1, 0] + matrix[2, 0];
+  pt.Y := tmpX * matrix[0, 1] + pt.Y * matrix[1, 1] + matrix[2, 1];
+end;
+//------------------------------------------------------------------------------
+
+procedure MatrixApply(const matrix: TMatrixD; var rec: TRect);
+var
+  l,t,b,r,tmpX: double;
+begin
+  tmpX := rec.Left;
+  l := tmpX * matrix[0, 0] + rec.Top * matrix[1, 0] + matrix[2, 0];
+  t := tmpX * matrix[0, 1] + rec.Top * matrix[1, 1] + matrix[2, 1];
+  tmpX := rec.Right;
+  r := tmpX * matrix[0, 0] + rec.Bottom * matrix[1, 0] + matrix[2, 0];
+  b := tmpX * matrix[0, 1] + rec.Bottom * matrix[1, 1] + matrix[2, 1];
+  rec := Rect(RectD(l,t,r,b));
+end;
+//------------------------------------------------------------------------------
+
+procedure MatrixApply(const matrix: TMatrixD; var rec: TRectD);
+var
+  path: TPathD;
+begin
+  path := Rectangle(rec);
+  MatrixApply(matrix, path);
+  rec := GetBoundsD(path);
+end;
+//------------------------------------------------------------------------------
+
+procedure MatrixApply(const matrix: TMatrixD; var path: TPathD);
+var
+  i, len: integer;
+  tmpX: double;
+  pp: PPointD;
+begin
+  len := Length(path);
+  if (len = 0) or IsIdentityMatrix(matrix) then Exit;
+  pp := @path[0];
+  for i := 0 to len -1 do
+  begin
+    tmpX := pp.X;
+    pp.X := tmpX * matrix[0, 0] + pp.Y * matrix[1, 0] + matrix[2, 0];
+    pp.Y := tmpX * matrix[0, 1] + pp.Y * matrix[1, 1] + matrix[2, 1];
+    inc(pp);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure MatrixApply(const matrix: TMatrixD; var paths: TPathsD);
+var
+  i,j,len: integer;
+  tmpX: double;
+  pp: PPointD;
+begin
+  if not Assigned(paths) or IsIdentityMatrix(matrix) then
+    Exit;
+
+  for i := 0 to High(paths) do
+  begin
+    len := Length(paths[i]);
+    if len = 0 then Continue;
+    pp := @paths[i][0];
+    for j := 0 to High(paths[i]) do
+    begin
+      tmpX := pp.X;
+      pp.X := tmpX * matrix[0, 0] + pp.Y * matrix[1, 0] + matrix[2, 0];
+      pp.Y := tmpX * matrix[0, 1] + pp.Y * matrix[1, 1] + matrix[2, 1];
+      inc(pp);
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function MatrixMultiply(const modifier, matrix: TMatrixD): TMatrixD;
+var
+  i, j: Integer;
+begin
+  for i := 0 to 2 do
+    for j := 0 to 2 do
+      Result[i, j] :=
+        (modifier[0, j] * matrix[i, 0]) +
+        (modifier[1, j] * matrix[i, 1]) +
+        (modifier[2, j] * matrix[i, 2]);
+end;
+//------------------------------------------------------------------------------
+
+procedure MatrixScale(var matrix: TMatrixD; scaleX, scaleY: double);
+var
+  m: TMatrixD;
+begin
+  m := IdentityMatrix;
+  if (scaleX = 0) or (scaleY = 0) then
+    raise Exception(rsInvalidScale);
+
+  if ValueAlmostOne(scaleX) and ValueAlmostOne(scaleY) then Exit;
+  m[0, 0] := scaleX;
+  m[1, 1] := scaleY;
+  matrix := MatrixMultiply(m, matrix);
+end;
+//------------------------------------------------------------------------------
+
+procedure MatrixScale(var matrix: TMatrixD; scale: double);
+begin
+  if (scale = 0) or (scale = 1) then Exit;
+  MatrixScale(matrix, scale, scale);
+end;
+//------------------------------------------------------------------------------
+
+procedure MatrixRotate(var matrix: TMatrixD;
+  const center: TPointD; angRad: double);
+var
+  m: TMatrixD;
+  sinA, cosA: double;
+  origOffset: Boolean;
+begin
+  NormalizeAngle(angRad);
+  if angRad = 0 then Exit;
+  if ClockwiseRotationIsAnglePositive then
+    angRad := -angRad; //negated angle because of inverted Y-axis.
+  m := IdentityMatrix;
+  origOffset := (center.X <> 0) or (center.Y <> 0);
+  if origOffset then MatrixTranslate(matrix, -center.X, -center.Y);
+  GetSinCos(angRad, sinA, cosA);
+  m := IdentityMatrix;
+  m[0, 0] := cosA;   m[1, 0] := sinA;
+  m[0, 1] := -sinA;  m[1, 1] := cosA;
+  matrix := MatrixMultiply(m, matrix);
+  if origOffset then MatrixTranslate(matrix, center.X, center.Y);
+end;
+//------------------------------------------------------------------------------
+
+procedure MatrixTranslate(var matrix: TMatrixD; dx, dy: double);
+var
+  m: TMatrixD;
+begin
+  if ValueAlmostZero(dx) and ValueAlmostZero(dy) then Exit;
+  m := IdentityMatrix;
+  m[2, 0] := dx;
+  m[2, 1] := dy;
+  matrix := MatrixMultiply(m, matrix);
+end;
+//------------------------------------------------------------------------------
+
+procedure ScaleInternal(var matrix: TMatrixD; s: double);
+var
+  i, j: Integer;
+begin
+  for i := 0 to 2 do
+    for j := 0 to 2 do
+      matrix[i,j] := matrix[i,j] * s;
+end;
+//------------------------------------------------------------------------------
+
+function MatrixInvert(var matrix: TMatrixD): Boolean;
+var
+  d: double;
+const
+  tolerance = 1.0E-5;
+begin
+  d := MatrixDeterminant(matrix);
+  Result := abs(d) > tolerance;
+  if Result then
+  begin
+    matrix := MatrixAdjugate(matrix);
+    ScaleInternal(matrix, 1/d);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure MatrixSkew(var matrix: TMatrixD; angleX, angleY: double);
+var
+  m: TMatrixD;
+begin
+  if ValueAlmostZero(angleX) and ValueAlmostZero(angleY) then Exit;
+  m := IdentityMatrix;
+  m[1, 0] := tan(angleX);
+  m[0, 1] := tan(angleY);
+  matrix := MatrixMultiply(m, matrix);
+end;
+
+//------------------------------------------------------------------------------
+// Affine Transformation
+//------------------------------------------------------------------------------
+
+function GetTransformBounds(img: TImage32; const matrix: TMatrixD): TRect;
+var
+  pts: TPathD;
+begin
+  pts := Rectangle(img.Bounds);
+  MatrixApply(matrix, pts);
+  Result := GetBounds(pts);
+end;
+//------------------------------------------------------------------------------
+
+function AffineTransformImage(img: TImage32; matrix: TMatrixD): TPoint;
+var
+  i,j, srcWidth, srcHeight: integer;
+  newWidth, newHeight: integer;
+  x,y: double;
+  pc: PColor32;
+  tmp: TArrayOfColor32;
+  dstRec: TRect;
+  resampler: TResamplerFunction;
+begin
+  Result := NullPoint;
+  srcWidth := img.Width;
+  srcHeight := img.Height;
+
+  if img.Resampler = 0 then
+    resampler := nil else
+    resampler := GetResampler(img.Resampler);
+
+  if not Assigned(resampler) or
+    (srcWidth * srcHeight = 0) or IsIdentityMatrix(matrix) then
+      Exit;
+
+  //auto-resize the image so it'll fit transformed image
+  dstRec := GetTransformBounds(img, matrix);
+  RectWidthHeight(dstRec, newWidth, newHeight);
+  //auto-translate the image too
+  Result := dstRec.TopLeft;
+
+  //starting with the result pixel coords, reverse lookup
+  //the fractional coordinates in the untransformed image
+  if not MatrixInvert(matrix) then Exit;
+
+  SetLength(tmp, newWidth * newHeight);
+  pc := @tmp[0];
+
+  for i := dstRec.Top to + dstRec.Bottom -1 do
+    for j := dstRec.Left to dstRec.Right -1 do
+    begin
+      //convert dest X,Y to src X,Y ...
+      x := j; y := i;
+      MatrixApply(matrix, x, y);
+      //get weighted pixel (slow)
+      pc^ := resampler(img, Round(x * 256), Round(y * 256));
+      inc(pc);
+    end;
+  img.BeginUpdate;
+  try
+    img.SetSize(newWidth, newHeight);
+    Move(tmp[0], img.Pixels[0], newWidth * newHeight * sizeOf(TColor32));
+  finally
+    img.EndUpdate;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// Projective Transformation
+//------------------------------------------------------------------------------
+
+procedure MatrixMulCoord(const matrix: TMatrixD; var x,y,z: double);
+{$IFDEF INLINE} inline; {$ENDIF}
+var
+  xx, yy: double;
+begin
+  xx := x; yy := y;
+  x := matrix[0,0] *xx + matrix[0,1] *yy + matrix[0,2] *z;
+  y := matrix[1,0] *xx + matrix[1,1] *yy + matrix[1,2] *z;
+  z := matrix[2,0] *xx + matrix[2,1] *yy + matrix[2,2] *z;
+end;
+//------------------------------------------------------------------------------
+
+function BasisToPoints(x1, y1, x2, y2, x3, y3, x4, y4: double): TMatrixD;
+var
+  m, m2: TMatrixD;
+  z4: double;
+begin
+  m := Matrix(x1, x2, x3, y1, y2, y3, 1,  1,  1);
+  m2 := MatrixAdjugate(m);
+  z4 := 1;
+  MatrixMulCoord(m2, x4, y4, z4);
+  m2 := Matrix(x4, 0, 0, 0, y4, 0, 0, 0, z4);
+  Result := MatrixMultiply(m2, m);
+end;
+//------------------------------------------------------------------------------
+
+procedure GetSrcCoords256(const matrix: TMatrixD; var x, y: integer);
+{$IFDEF INLINE} inline; {$ENDIF}
+var
+  xx,yy,zz: double;
+const
+  Q: integer = MaxInt div 256;
+begin
+  //returns coords multiplied by 256 in anticipation of the following
+  //GetWeightedPixel function call which in turn expects the lower 8bits
+  //of the integer coord value to represent a fraction.
+  xx := x; yy := y; zz := 1;
+  MatrixMulCoord(matrix, xx, yy, zz);
+
+  if zz = 0 then
+  begin
+    if xx >= 0 then x := Q else x := -MaxInt;
+    if yy >= 0 then y := Q else y := -MaxInt;
+  end else
+  begin
+    xx := xx/zz;
+    if xx > Q then x := MaxInt
+    else if xx < -Q then x := -MaxInt
+    else x := Round(xx *256);
+
+    yy := yy/zz;
+    if yy > Q then y := MaxInt
+    else if yy < -Q then y := -MaxInt
+    else y := Round(yy *256);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function GetProjectionMatrix(const srcPts, dstPts: TPathD): TMatrixD;
+var
+  srcMat, dstMat: TMatrixD;
+begin
+  if (length(srcPts) <> 4) or (length(dstPts) <> 4) then
+  begin
+    Result := IdentityMatrix;
+    Exit;
+  end;
+  srcMat := BasisToPoints(srcPts[0].X, srcPts[0].Y,
+    srcPts[1].X, srcPts[1].Y, srcPts[2].X, srcPts[2].Y, srcPts[3].X, srcPts[3].Y);
+  dstMat := BasisToPoints(dstPts[0].X, dstPts[0].Y,
+    dstPts[1].X, dstPts[1].Y, dstPts[2].X, dstPts[2].Y, dstPts[3].X, dstPts[3].Y);
+  Result := MatrixMultiply(MatrixAdjugate(dstMat), srcMat);
+end;
+//------------------------------------------------------------------------------
+
+function ProjectiveTransform(img: TImage32;
+  const srcPts, dstPts: TPathD; const margins: TRect): Boolean;
+var
+  w,h,i,j: integer;
+  x,y: integer;
+  rec: TRect;
+  dstPts2: TPathD;
+  mat: TMatrixD;
+  tmp: TArrayOfColor32;
+  pc: PColor32;
+  resampler: TResamplerFunction;
+begin
+  //https://math.stackexchange.com/a/339033/384709
+
+  if img.Resampler = 0 then
+    resampler := nil else
+    resampler := GetResampler(img.Resampler);
+
+  Result := Assigned(resampler) and not img.IsEmpty and
+    (Length(dstPts) = 4) and IsPathConvex(dstPts);
+  if not Result then Exit;
+
+  rec := GetBounds(dstPts);
+  dec(rec.Left, margins.Left);
+  dec(rec.Top, margins.Top);
+  inc(rec.Right, margins.Right);
+  inc(rec.Bottom, margins.Bottom);
+  dstPts2 := OffsetPath(dstPts, -rec.Left, -rec.Top);
+
+  mat := GetProjectionMatrix(srcPts, dstPts2);
+  RectWidthHeight(rec, w, h);
+  SetLength(tmp, w * h);
+  pc := @tmp[0];
+  for i :=  0 to h -1 do
+    for j := 0 to w -1 do
+    begin
+      x := j; y := i;
+      GetSrcCoords256(mat, x, y);
+      pc^ := resampler(img, x, y);
+      inc(pc);
+    end;
+  img.SetSize(w, h);
+  Move(tmp[0], img.PixelBase^, w * h * sizeOf(TColor32));
+end;
+
+//------------------------------------------------------------------------------
+// Spline transformations
+//------------------------------------------------------------------------------
+
+function ReColor(color, newColor: TColor32): TColor32;
+{$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result := (color and $FF000000) or newColor;
+end;
+//------------------------------------------------------------------------------
+
+function InterpolateSegX(const pt1, pt2: TPointD): TPathD;
+var
+  i, x1, x2: integer;
+  xo,dydx: double;
+begin
+  Result := nil;
+  if pt2.X > pt1.X then
+  begin
+    x1 := Ceil(pt1.X);
+    x2 := Ceil(pt2.X);
+    if x1 = x2 then Exit;
+    dydx := (pt2.Y - pt1.Y)/(pt2.X - pt1.X);
+    xo := x1 -pt1.X;
+    SetLength(Result, x2-x1);
+    for i:= 0 to x2 - x1 -1 do
+    begin
+      Result[i].X := x1 +i;
+      Result[i].Y := pt1.Y + dydx * (xo +i);
+    end;
+  end else
+  begin
+    x1 := Floor(pt1.X);
+    x2 := Floor(pt2.X);
+    if x1 = x2 then Exit;
+    dydx := (pt2.Y - pt1.Y)/(pt2.X - pt1.X);
+    xo := x1 -pt1.X;
+    SetLength(Result, x1-x2);
+    for i:= 0 to x1 - x2 -1 do
+    begin
+      Result[i].X := x1 -i;
+      Result[i].Y := pt1.Y + dydx * (xo -i);
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function InterpolateSegY(const pt1, pt2: TPointD): TPathD;
+var
+  i, y1,y2: integer;
+  yo,dxdy: double;
+begin
+  Result := nil;
+  if pt2.Y > pt1.Y then
+  begin
+    y1 := Ceil(pt1.Y);
+    y2 := Ceil(pt2.Y);
+    if y1 = y2 then Exit;
+    dxdy := (pt2.X - pt1.X)/(pt2.Y - pt1.Y);
+    yo := y1 -pt1.Y;
+    SetLength(Result, y2-y1);
+    for i:= 0 to y2 - y1 -1 do
+    begin
+      Result[i].Y := y1 +i;
+      Result[i].X := pt1.X + dxdy * (yo +i);
+    end;
+  end else
+  begin
+    y1 := Floor(pt1.Y);
+    y2 := Floor(pt2.Y);
+    if y1 = y2 then Exit;
+    dxdy := (pt2.X - pt1.X)/(pt2.Y - pt1.Y);
+    yo := y1 -pt1.Y;
+    SetLength(Result, y1-y2);
+    for i:= 0 to y1 - y2 -1 do
+    begin
+      Result[i].Y := y1 -i;
+      Result[i].X := pt1.X + dxdy * (yo -i);
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function InterpolatePathForX(const path: TPathD): TPathD;
+var
+  i,len: integer;
+  tmp: TPathD;
+begin
+  Result := nil;
+  len := length(path);
+  if len < 2 then Exit;
+  for i := 1 to len -1 do
+  begin
+    tmp := InterpolateSegX(path[i-1], path[i]);
+    AppendPath(Result, tmp);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function InterpolatePathForY(const path: TPathD): TPathD;
+var
+  i, len: integer;
+  tmp: TPathD;
+begin
+  Result := nil;
+  len := length(path);
+  if len < 2 then Exit;
+  for i := 1 to len -1 do
+  begin
+    tmp := InterpolateSegY(path[i-1], path[i]);
+    AppendPath(Result, tmp);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function SplineVertTransform(img: TImage32; const topSpline: TPathD;
+  splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean;
+var
+  i,j, w,h, len: integer;
+  y, q: double;
+  distances: TArrayOfDouble;
+  pc: PColor32;
+  rec: TRect;
+  tmp: TArrayOfColor32;
+  topPath: TPathD;
+  prevX: double;
+  resampler: TResamplerFunction;
+  backColoring, allowBackColoring: Boolean;
+begin
+  offset := NullPoint;
+  if img.Resampler = 0 then
+    resampler := nil else
+    resampler := GetResampler(img.Resampler);
+
+  //convert the top spline control points into a flattened path
+  if splineType = stQuadratic then
+    topPath := FlattenQSpline(topSpline) else
+    topPath := FlattenCSpline(topSpline);
+
+  rec := GetBounds(topPath);
+  //return false if the spline is invalid or there's no vertical transformation
+  Result := Assigned(resampler) and not IsEmptyRect(rec);
+  if not Result then Exit;
+
+  offset := rec.TopLeft;
+  topPath := InterpolatePathForX(topPath);
+  len := Length(topPath);
+  inc(rec.Bottom, img.Height);
+  RectWidthHeight(rec, w, h);
+  SetLength(tmp, (w+1) * h);
+
+  prevX := topPath[0].X;
+  allowBackColoring := GetAlpha(backColor) > 2;
+  backColor := backColor and $00FFFFFF;
+
+  distances := GetCumulativeDistances(topPath);
+  q := img.Width * 256 / distances[High(distances)];;
+  for i := 0 to len -1 do
+  begin
+    pc := @tmp[Round(topPath[i].X)-rec.Left];
+    backColoring := allowBackColoring and (prevX >= topPath[i].X);
+    prevX := topPath[i].X;
+    y := topPath[i].Y;
+    for j := rec.top to rec.bottom -1 do
+    begin
+      if (j > y-1.0) and (j < y + img.Height) then
+        if backColoring then
+          pc^ := BlendToAlpha(pc^,
+            ReColor(resampler(img, Round(Distances[i]*q) ,Round((j - y)*256)), backColor))
+        else
+          pc^ := BlendToAlpha(pc^,
+            resampler(img, Round(Distances[i]*q) ,Round((j - y)*256)));
+      inc(pc, w);
+    end;
+  end;
+
+  img.BeginUpdate;
+  img.SetSize(w,h);
+  Move(tmp[0], img.Pixels[0], img.Width * img.Height * SizeOf(TColor32));
+  img.EndUpdate;
+end;
+//------------------------------------------------------------------------------
+
+function SplineHorzTransform(img: TImage32; const leftSpline: TPathD;
+  splineType: TSplineType; backColor: TColor32; out offset: TPoint): Boolean;
+var
+  i,j, len, w,h: integer;
+  x, q, prevY: double;
+  leftPath: TPathD;
+  distances: TArrayOfDouble;
+  rec: TRect;
+  pc: PColor32;
+  tmp: TArrayOfColor32;
+  backColoring, allowBackColoring: Boolean;
+  resampler: TResamplerFunction;
+begin
+  offset := NullPoint;
+
+  if img.Resampler = 0 then
+    resampler := nil else
+    resampler := GetResampler(img.Resampler);
+
+  //convert the left spline control points into a flattened path
+  if splineType = stQuadratic then
+    leftPath := FlattenQSpline(leftSpline) else
+    leftPath := FlattenCSpline(leftSpline);
+  rec := GetBounds(leftPath);
+
+  //return false if the spline is invalid or there's no horizontal transformation
+  Result := Assigned(resampler) and not IsEmptyRect(rec);
+  if not Result then Exit;
+
+  offset := rec.TopLeft;
+  leftPath := InterpolatePathForY(leftPath);
+  len := Length(leftPath);
+  inc(rec.Right, img.Width);
+  RectWidthHeight(rec, w, h);
+  SetLength(tmp, w * (h+1));
+
+  prevY := leftPath[0].Y;
+  allowBackColoring := GetAlpha(backColor) > 2;
+  backColor :=   backColor and $00FFFFFF;
+
+  distances := GetCumulativeDistances(leftPath);
+  q := img.Height * 256 / distances[High(distances)];;
+  for i := 0 to len -1 do
+  begin
+    pc := @tmp[Round(leftPath[i].Y - rec.Top) * w];
+    backColoring := allowBackColoring and (prevY >= leftPath[i].Y);
+    prevY := leftPath[i].Y;
+    x := leftPath[i].X;
+    for j := rec.left to rec.right -1 do
+    begin
+      if (j > x-1.0) and (j < x + img.Width) then
+        if backColoring then
+          pc^ := BlendToAlpha(pc^,
+            ReColor(resampler(img, Round((j - x) *256), Round(Distances[i]*q)), backColor))
+        else
+          pc^ := BlendToAlpha(pc^,
+            resampler(img, Round((j - x) *256), Round(Distances[i]*q)));
+      inc(pc);
+    end;
+  end;
+
+  img.BeginUpdate;
+  img.SetSize(w,h);
+  Move(tmp[0], img.Pixels[0], img.Width * img.Height * SizeOf(TColor32));
+  img.EndUpdate;
+end;
+
+//------------------------------------------------------------------------------
+// TWeightedColor
+//------------------------------------------------------------------------------
+
+procedure TWeightedColor.Reset;
+begin
+  fAddCount := 0;
+  fAlphaTot := 0;
+  fColorTotR := 0;
+  fColorTotG := 0;
+  fColorTotB := 0;
+end;
+//------------------------------------------------------------------------------
+
+procedure TWeightedColor.AddWeight(w: Integer);
+begin
+  inc(fAddCount, w);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWeightedColor.Add(c: TColor32; w: Integer);
+var
+  a: Integer;
+  argb: TARGB absolute c;
+begin
+  inc(fAddCount, w);
+  a := w * argb.A;
+  if a = 0 then Exit;
+  inc(fAlphaTot, a);
+  inc(fColorTotB, (a * argb.B));
+  inc(fColorTotG, (a * argb.G));
+  inc(fColorTotR, (a * argb.R));
+end;
+//------------------------------------------------------------------------------
+
+procedure TWeightedColor.Add(const other: TWeightedColor);
+begin
+  inc(fAddCount, other.fAddCount);
+  inc(fAlphaTot, other.fAlphaTot);
+  inc(fColorTotR, other.fColorTotR);
+  inc(fColorTotG, other.fColorTotG);
+  inc(fColorTotB, other.fColorTotB);
+end;
+//------------------------------------------------------------------------------
+
+procedure TWeightedColor.Subtract(c: TColor32; w: Integer);
+var
+  a: Integer;
+  argb: TARGB absolute c;
+begin
+  dec(fAddCount, w);
+  a := w * argb.A;
+  if a = 0 then Exit;
+  dec(fAlphaTot, a);
+  dec(fColorTotB, (a * argb.B));
+  dec(fColorTotG, (a * argb.G));
+  dec(fColorTotR, (a * argb.R));
+end;
+//------------------------------------------------------------------------------
+
+procedure TWeightedColor.Subtract(const other: TWeightedColor);
+begin
+  dec(fAddCount, other.fAddCount);
+  dec(fAlphaTot, other.fAlphaTot);
+  dec(fColorTotR, other.fColorTotR);
+  dec(fColorTotG, other.fColorTotG);
+  dec(fColorTotB, other.fColorTotB);
+end;
+//------------------------------------------------------------------------------
+
+function TWeightedColor.GetColor: TColor32;
+var
+  invAlpha: double;
+  res: TARGB absolute Result;
+begin
+  if (fAlphaTot <= 0) or (fAddCount <= 0) then
+  begin
+    result := clNone32;
+    Exit;
+  end;
+  res.A := Min(255, (fAlphaTot  + (fAddCount shr 1)) div fAddCount);
+  //nb: alpha weighting is applied to colors when added,
+  //so we now need to div by fAlphaTot here ...
+  invAlpha := 1/fAlphaTot;
+  res.R := ClampByte(fColorTotR * invAlpha);
+  res.G := ClampByte(fColorTotG * invAlpha);
+  res.B := ClampByte(fColorTotB * invAlpha);
+end;
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+procedure ExtractAllFromMatrix(const mat: TMatrixD; out angle: double;
+  out scale, skew, trans: TPointD);
+var
+  a,b,c,d,e,f: double;
+  delta, r,s: double;
+begin
+  a := mat[0][0]; b := mat[1][0];
+  c := mat[0][1]; d := mat[1][1];
+  e := mat[2][0]; f := mat[2][1];
+
+  delta := a * d - b * c;
+  trans := PointD(e,f);
+  angle := 0;
+  scale := PointD(1,1);
+  skew := NullPointD;
+
+  if (a <> 0) or (b <> 0) then
+  begin
+    r := Sqrt(a * a + b * b);
+	  angle :=  ArcCos(a / r);
+	  if b < 0 then angle := -angle;
+    scale.X	:= r;
+	  scale.Y	:= delta / r;
+    skew.X	:= ArcTan((a * c + b * d) / (r * r));
+  end
+  else if (c <> 0) or (d <> 0) then
+  begin
+    s := Sqrt(c * c + d * d);
+    if d > 0 then
+      angle := Angle90 - ArcCos(-c / s) else
+	  angle := Angle90 + ArcCos(c / s);
+    scale.X := delta / s;
+    scale.Y := s;
+    skew.Y  := ArcTan((a * c + b * d) / (s * s));
+  end;
+  angle := -angle;
+  NormalizeAngle(angle);
+end;
+//------------------------------------------------------------------------------
+
+function ExtractAngleFromMatrix(const mat: TMatrixD): double;
+var
+  a,b,c,d: double;
+  r,s: double;
+begin
+  a := mat[0][0]; b := mat[1][0];
+  c := mat[0][1]; d := mat[1][1];
+
+  if (a <> 0) or (b <> 0) then
+  begin
+    r := Sqrt(a * a + b * b);
+	  Result :=  ArcCos(a / r);
+	  if b < 0 then Result := -Result;
+  end
+  else if (c <> 0) or (d <> 0) then
+  begin
+    s := Sqrt(c * c + d * d);
+    if d > 0 then
+      Result := Angle90 - ArcCos(-c / s) else
+	  Result := Angle90 + ArcCos(c / s);
+  end else
+  begin
+    Result := InvalidD; //error
+    Exit;
+  end;
+  Result := -Result;
+  NormalizeAngle(Result);
+end;
+//------------------------------------------------------------------------------
+
+function ExtractScaleFromMatrix(const mat: TMatrixD): TSizeD;
+var
+  a,b,c,d: double;
+  delta, q: double;
+begin
+  a := mat[0][0]; b := mat[1][0];
+  c := mat[0][1]; d := mat[1][1];
+
+  delta := a * d - b * c;
+  if (a <> 0) or (b <> 0) then
+  begin
+    q := Sqrt(a * a + b * b);
+    Result.cx	:= q;
+	  Result.cy	:= delta / q;
+  end
+  else if (c <> 0) or (d <> 0) then
+  begin
+    q := Sqrt(c * c + d * d);
+    Result.cx := delta / q;
+    Result.cy := q;
+  end else
+    Result := SizeD(0.0, 0.0);
+end;
+//------------------------------------------------------------------------------
+
+function ExtractAvgScaleFromMatrix(const mat: TMatrixD): double;
+var
+  scale: TSizeD;
+begin
+  scale := ExtractScaleFromMatrix(mat);
+  Result := Average(Abs(scale.cx), Abs(scale.cy));
+end;
+//------------------------------------------------------------------------------
+
+end.

+ 3689 - 0
components/Image32/source/Img32.Vector.pas

@@ -0,0 +1,3689 @@
+unit Img32.Vector;
+
+(*******************************************************************************
+* Author    :  Angus Johnson                                                   *
+* Version   :  4.3                                                             *
+* Date      :  27 September 2022                                               *
+* Website   :  http://www.angusj.com                                           *
+* Copyright :  Angus Johnson 2019-2022                                         *
+*                                                                              *
+* Purpose   :  Vector drawing for TImage32                                     *
+*                                                                              *
+* License   :  Use, modification & distribution is subject to                  *
+*              Boost Software License Ver 1                                    *
+*              http://www.boost.org/LICENSE_1_0.txt                            *
+*******************************************************************************)
+
+interface
+
+{$I Img32.inc}
+
+uses
+  SysUtils, Classes, Math, Types, Img32;
+
+type
+  TArrowStyle = (asNone, asSimple, asFancy, asDiamond, asCircle, asTail);
+  TJoinStyle  = (jsAuto, jsSquare, jsMiter, jsRound);
+  TEndStyle   = (esPolygon = 0, esClosed = 0, esButt, esSquare, esRound);
+  TPathEnd    = (peStart, peEnd, peBothEnds);
+  TSplineType = (stQuadratic, stCubic);
+  TFillRule = (frEvenOdd, frNonZero, frPositive, frNegative);
+  TImg32FillRule = TFillRule; //useful whenever there's ambiguity with Clipper
+
+  TSizeD = {$IFDEF RECORD_METHODS} record {$ELSE} object {$ENDIF}
+    cx  : double;
+    cy  : double;
+    function average: double;
+    property Width: Double read cx write cx;
+    property Height: Double read cy write cy;
+  end;
+
+  TRectWH = {$IFDEF RECORD_METHODS} record {$ELSE} object {$ENDIF}
+  public
+    Left, Top, Width, Height: double;
+    function IsEmpty: Boolean;
+    function IsValid: Boolean;
+    function Right: double;
+    function Bottom: double;
+    function Contains(const Pt: TPoint): Boolean; overload;
+    function Contains(const Pt: TPointD): Boolean; overload;
+    function MidPoint: TPointD;
+    function RectD: TRectD;
+    function Rect: TRect;
+  end;
+
+  function RectWH(left, top, width, height: integer): TRectWH; overload;
+  function RectWH(left, top, width, height: double ): TRectWH; overload;
+  function RectWH(const rec: TRectD): TRectWH; overload;
+
+  //InflateRect: missing in Delphi 7
+  procedure InflateRect(var rec: TRect; dx, dy: integer); overload;
+  procedure InflateRect(var rec: TRectD; dx, dy: double); overload;
+
+  function NormalizeRect(var rect: TRect): Boolean;
+
+  function PrePendPoint(const pt: TPointD; const p: TPathD): TPathD;
+  function PrePendPoints(const pt1, pt2: TPointD; const p: TPathD): TPathD;
+
+  function Rectangle(const rec: TRect): TPathD; overload;
+  function Rectangle(const rec: TRectD): TPathD; overload;
+  function Rectangle(l, t, r, b: double): TPathD; overload;
+
+  function RoundRect(const rec: TRect; radius: integer): TPathD; overload;
+  function RoundRect(const rec: TRectD; radius: double): TPathD; overload;
+  function RoundRect(const rec: TRect; radius: TPoint): TPathD; overload;
+  function RoundRect(const rec: TRectD; radius: TPointD): TPathD; overload;
+
+  function Ellipse(const rec: TRect; steps: integer = 0): TPathD; overload;
+  function Ellipse(const rec: TRectD; steps: integer = 0): TPathD; overload;
+  function Ellipse(const rec: TRectD; pendingScale: double): TPathD; overload;
+
+  function RotatedEllipse(const rec: TRectD; angle: double; steps: integer = 0): TPathD; overload;
+  function RotatedEllipse(const rec: TRectD; angle: double; pendingScale: double): TPathD; overload;
+
+  function AngleToEllipticalAngle(const ellRec: TRectD; angle: double): double;
+
+  function EllipticalAngleToAngle(const ellRec: TRectD; angle: double): double;
+
+  function Circle(const pt: TPoint; radius: double): TPathD; overload;
+  function Circle(const pt: TPointD; radius: double): TPathD; overload;
+  function Circle(const pt: TPointD; radius: double; pendingScale: double): TPathD; overload;
+
+  function Star(const rec: TRectD; points: integer; indentFrac: double = 0.4): TPathD; overload;
+  function Star(const focalPt: TPointD;
+    innerRadius, outerRadius: double; points: integer): TPathD; overload;
+
+  function Arc(const rec: TRectD;
+    startAngle, endAngle: double; scale: double = 0): TPathD;
+
+  function Pie(const rec: TRectD;
+    StartAngle, EndAngle: double; scale: double = 0): TPathD;
+
+  function FlattenQBezier(const pt1, pt2, pt3: TPointD;
+    tolerance: double = 0.0): TPathD; overload;
+  function FlattenQBezier(const pts: TPathD;
+    tolerance: double = 0.0): TPathD; overload;
+  function FlattenQBezier(const firstPt: TPointD; const pts: TPathD;
+    tolerance: double = 0.0): TPathD; overload;
+
+  function GetPointInQuadBezier(const a,b,c: TPointD; t: double): TPointD;
+
+  function FlattenCBezier(const pt1, pt2, pt3, pt4: TPointD;
+    tolerance: double = 0.0): TPathD; overload;
+  function FlattenCBezier(const pts: TPathD;
+    tolerance: double = 0.0): TPathD; overload;
+  function FlattenCBezier(const firstPt: TPointD; const pts: TPathD;
+    tolerance: double = 0.0): TPathD; overload;
+
+  function GetPointInCubicBezier(const a,b,c,d: TPointD; t: double): TPointD;
+
+  //FlattenCSpline: Approximates the 'S' command inside the 'd' property of an
+  //SVG path. (See https://www.w3.org/TR/SVG/paths.html#DProperty)
+  function FlattenCSpline(const pts: TPathD;
+    tolerance: double = 0.0): TPathD; overload;
+  function FlattenCSpline(const priorCtrlPt, startPt: TPointD;
+    const pts: TPathD; tolerance: double = 0.0): TPathD; overload;
+
+  //FlattenQSpline: Approximates the 'T' command inside the 'd' property of an
+  //SVG path. (See https://www.w3.org/TR/SVG/paths.html#DProperty)
+  function FlattenQSpline(const pts: TPathD;
+    tolerance: double = 0.0): TPathD; overload;
+  function FlattenQSpline(const priorCtrlPt, startPt: TPointD;
+    const pts: TPathD; tolerance: double = 0.0): TPathD; overload;
+
+  //ArrowHead: The ctrlPt's only function is to control the angle of the arrow.
+  function ArrowHead(const arrowTip, ctrlPt: TPointD; size: double;
+    arrowStyle: TArrowStyle): TPathD;
+
+  function GetDefaultArrowHeadSize(lineWidth: double): double;
+
+  procedure AdjustPoint(var pt: TPointD;
+    const referencePt: TPointD; delta: double);
+
+  function ShortenPath(const path: TPathD;
+    pathEnd: TPathEnd; amount: double): TPathD;
+
+  //GetDashPath: Returns a polyline (not polygons)
+  function GetDashedPath(const path: TPathD;
+    closed: Boolean; const pattern: TArrayOfInteger;
+    patternOffset: PDouble): TPathsD;
+
+  function GetDashedOutLine(const path: TPathD;
+    closed: Boolean; const pattern: TArrayOfInteger;
+    patternOffset: PDouble; lineWidth: double;
+    joinStyle: TJoinStyle; endStyle: TEndStyle): TPathsD;
+
+  function OffsetPoint(const pt: TPoint; dx, dy: integer): TPoint; overload;
+  function OffsetPoint(const pt: TPointD; dx, dy: double): TPointD; overload;
+
+  function OffsetPath(const path: TPathD;
+    dx, dy: double): TPathD; overload;
+  function OffsetPath(const paths: TPathsD;
+    dx, dy: double): TPathsD; overload;
+  function OffsetPath(const ppp: TArrayOfPathsD;
+    dx, dy: double): TArrayOfPathsD; overload;
+
+  function Paths(const path: TPathD): TPathsD;
+  {$IFDEF INLINING} inline; {$ENDIF}
+
+  //CopyPath: note that only dynamic string arrays are copy-on-write
+  function CopyPath(const path: TPathD): TPathD;
+  {$IFDEF INLINING} inline; {$ENDIF}
+  function CopyPaths(const paths: TPathsD): TPathsD;
+
+  function ScalePoint(const pt: TPointD; scale: double): TPointD; overload;
+  {$IFDEF INLINING} inline; {$ENDIF}
+  function ScalePoint(const pt: TPointD; sx, sy: double): TPointD; overload;
+  {$IFDEF INLINING} inline; {$ENDIF}
+  function ScalePath(const path: TPathD;
+    sx, sy: double): TPathD; overload;
+
+  function ScalePath(const path: TPathD;
+    scale: double): TPathD; overload;
+  function ScalePath(const paths: TPathsD;
+    sx, sy: double): TPathsD; overload;
+  function ScalePath(const paths: TPathsD;
+    scale: double): TPathsD; overload;
+
+  function ScaleRect(const rec: TRect; scale: double): TRect; overload;
+  function ScaleRect(const rec: TRectD; scale: double): TRectD; overload;
+  function ScaleRect(const rec: TRect; sx, sy: double): TRect; overload;
+  function ScaleRect(const rec: TRectD; sx, sy: double): TRectD; overload;
+
+  function ReversePath(const path: TPathD): TPathD; overload;
+  function ReversePath(const paths: TPathsD): TPathsD; overload;
+
+  function OpenPathToFlatPolygon(const path: TPathD): TPathD;
+
+  procedure AppendPoint(var path: TPathD; const extra: TPointD);
+
+  procedure AppendPath(var path: TPathD; const pt: TPointD); overload;
+  procedure AppendPath(var path1: TPathD; const path2: TPathD); overload;
+  procedure AppendPath(var paths: TPathsD; const extra: TPathD); overload;
+  procedure AppendPath(var paths: TPathsD; const extra: TPathsD); overload;
+  procedure AppendPath(var ppp: TArrayOfPathsD; const extra: TPathsD); overload;
+
+  function GetAngle(const origin, pt: TPoint): double; overload;
+  function GetAngle(const origin, pt: TPointD): double; overload;
+  function GetAngle(const a, b, c: TPoint): double; overload;
+  function GetAngle(const a, b, c: TPointD): double; overload;
+
+  procedure GetSinCos(angle: double; out sinA, cosA: double);
+
+  function GetPointAtAngleAndDist(const origin: TPointD;
+    angle, distance: double): TPointD;
+
+  function IntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPointD): TPointD; overload;
+  function IntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPointD; out ip: TPointD): Boolean; overload;
+
+  function SegmentIntersectPt(const ln1a, ln1b, ln2a, ln2b: TPointD): TPointD;
+  function SegmentsIntersect(const ln1a, ln1b, ln2a, ln2b: TPointD;
+    out ip: TPointD): Boolean;
+
+  procedure RotatePoint(var pt: TPointD;
+    const focalPoint: TPointD; sinA, cosA: double); overload;
+  procedure RotatePoint(var pt: TPointD;
+    const focalPoint: TPointD; angleRad: double); overload;
+
+  function RotatePath(const path: TPathD;
+    const focalPoint: TPointD; angleRads: double): TPathD; overload;
+  function RotatePath(const paths: TPathsD;
+    const focalPoint: TPointD; angleRads: double): TPathsD; overload;
+
+  //function MakePath(const pts: array of integer): TPathD; overload;
+  function MakePath(const pts: array of double): TPathD; overload;
+
+  function GetBounds(const path: TPathD): TRect; overload;
+  function GetBounds(const paths: TPathsD): TRect; overload;
+
+  function GetBoundsD(const path: TPathD): TRectD; overload;
+  function GetBoundsD(const paths: TPathsD): TRectD; overload;
+
+  function GetRotatedRectBounds(const rec: TRect; angle: double): TRect; overload;
+  function GetRotatedRectBounds(const rec: TRectD; angle: double): TRectD; overload;
+
+  function Rect(const recD: TRectD): TRect; overload;
+  function Rect(const left,top,right,bottom: integer): TRect; overload;
+
+  function PtInRect(const rec: TRectD; const pt: TPointD): Boolean; overload;
+
+  function Size(cx, cy: integer): TSize;
+  function SizeD(cx, cy: double): TSizeD;
+
+  function IsClockwise(const path: TPathD): Boolean;
+
+  function Area(const path: TPathD): Double; overload;
+
+  function RectsEqual(const rec1, rec2: TRect): Boolean;
+
+  procedure OffsetRect(var rec: TRectD; dx, dy: double); overload;
+
+  function MakeSquare(rec: TRect): TRect;
+
+  function IsValid(value: integer): Boolean; overload;
+  function IsValid(value: double): Boolean; overload;
+  function IsValid(const pt: TPoint): Boolean; overload;
+  function IsValid(const pt: TPointD): Boolean; overload;
+  function IsValid(const rec: TRect): Boolean; overload;
+
+  function Point(X,Y: Integer): TPoint; overload;
+  function Point(const pt: TPointD): TPoint; overload;
+
+  function PointsEqual(const pt1, pt2: TPointD): Boolean; overload;
+  {$IFDEF INLINING} inline; {$ENDIF}
+
+  function PointsNearEqual(const pt1, pt2: TPoint;
+    dist: integer): Boolean; overload;
+  function PointsNearEqual(const pt1, pt2: TPointD;
+    distSqrd: double): Boolean; overload;
+  {$IFDEF INLINING} inline; {$ENDIF}
+
+  function StripNearDuplicates(const path: TPathD;
+    minDist: double; isClosedPath: Boolean): TPathD; overload;
+  function StripNearDuplicates(const paths: TPathsD;
+    minLength: double; isClosedPaths: Boolean): TPathsD; overload;
+
+  function MidPoint(const rec: TRect): TPoint; overload;
+  function MidPoint(const rec: TRectD): TPointD; overload;
+  function MidPoint(const pt1, pt2: TPoint): TPoint; overload;
+  function MidPoint(const pt1, pt2: TPointD): TPointD; overload;
+
+  function Average(val1, val2: integer): integer; overload;
+  function Average(val1, val2: double): double; overload;
+
+  function ReflectPoint(const pt, pivot: TPointD): TPointD;
+  {$IFDEF INLINING} inline; {$ENDIF}
+
+  function RectsOverlap(const rec1, rec2: TRect): Boolean;
+
+  function IsSameRect(const rec1, rec2: TRect): Boolean;
+
+  function RectsIntersect(const rec1, rec2: TRect): Boolean; overload;
+  function RectsIntersect(const rec1, rec2: TRectD): Boolean; overload;
+  function IntersectRect(const rec1, rec2: TRectD): TRectD; overload;
+
+  //UnionRect: this behaves differently to types.UnionRect
+  //in that if either parameter is empty the other parameter is returned
+  function UnionRect(const rec1, rec2: TRect): TRect; overload;
+  function UnionRect(const rec1, rec2: TRectD): TRectD; overload;
+
+  //these 2 functions are only needed to support older versions of Delphi
+  function MakeArrayOfInteger(const ints: array of integer): TArrayOfInteger;
+  function MakeArrayOfDouble(const doubles: array of double): TArrayOfDouble;
+
+  function CrossProduct(const vector1, vector2: TPointD): double; overload;
+  {$IFDEF INLINING} inline; {$ENDIF}
+  function CrossProduct(const pt1, pt2, pt3: TPointD): double; overload;
+  {$IFDEF INLINING} inline; {$ENDIF}
+  function CrossProduct(const pt1, pt2, pt3, pt4: TPointD): double; overload;
+  {$IFDEF INLINING} inline; {$ENDIF}
+
+  function DotProduct(const vector1, vector2: TPointD): double; overload;
+  {$IFDEF INLINING} inline; {$ENDIF}
+  function DotProduct(const pt1, pt2, pt3: TPointD): double; overload;
+  {$IFDEF INLINING} inline; {$ENDIF}
+
+  function TurnsLeft(const pt1, pt2, pt3: TPointD): boolean;
+  {$IFDEF INLINING} inline; {$ENDIF}
+  function TurnsRight(const pt1, pt2, pt3: TPointD): boolean;
+  {$IFDEF INLINING} inline; {$ENDIF}
+
+  function IsPathConvex(const path: TPathD): Boolean;
+
+  function NormalizeVector(const vec: TPointD): TPointD;
+  {$IFDEF INLINING} inline; {$ENDIF}
+
+  //GetUnitVector: Used internally
+  function GetUnitVector(const pt1, pt2: TPointD): TPointD;
+
+  //GetUnitNormal: Used internally
+  function GetUnitNormal(const pt1, pt2: TPointD): TPointD; overload;
+  {$IFDEF INLINING} inline; {$ENDIF}
+  function GetUnitNormal(const pt1, pt2: TPointD; out norm: TPointD): Boolean; overload;
+  {$IFDEF INLINING} inline; {$ENDIF}
+
+  function GetAvgUnitVector(const vec1, vec2: TPointD): TPointD;
+  {$IFDEF INLINING} inline; {$ENDIF}
+
+  //GetVectors: Used internally
+  function GetVectors(const path: TPathD): TPathD;
+  //GetNormals: Used internally
+
+  function GetNormals(const path: TPathD): TPathD;
+
+  //DistanceSqrd: Used internally
+  function DistanceSqrd(const pt1, pt2: TPoint): double; overload;
+  {$IFDEF INLINE} inline; {$ENDIF}
+  //DistanceSqrd: Used internally
+  function DistanceSqrd(const pt1, pt2: TPointD): double; overload;
+  {$IFDEF INLINE} inline; {$ENDIF}
+
+  function Distance(const pt1, pt2: TPoint): double; overload;
+  {$IFDEF INLINE} inline; {$ENDIF}
+  function Distance(const pt1, pt2: TPointD): double; overload;
+  {$IFDEF INLINE} inline; {$ENDIF}
+  function Distance(const path: TPathD; stopAt: integer = 0): double; overload;
+
+  function GetDistances(const path: TPathD): TArrayOfDouble;
+
+  function GetCumulativeDistances(const path: TPathD): TArrayOfDouble;
+
+  function PerpendicularDistSqrd(const pt, line1, line2: TPointD): double;
+
+  function PointInPolygon(const pt: TPointD;
+    const polygon: TPathD; fillRule: TFillRule): Boolean;
+
+  function PointInPolygons(const pt: TPointD;
+    const polygons: TPathsD; fillRule: TFillRule): Boolean;
+
+  function PerpendicularDist(const pt, line1, line2: TPointD): double;
+
+  function ClosestPointOnLine(const pt, linePt1, linePt2: TPointD): TPointD;
+
+  function ClosestPointOnSegment(const pt, segPt1, segPt2: TPointD): TPointD;
+
+  function IsPointInEllipse(const ellipseRec: TRect; const pt: TPoint): Boolean;
+
+  //GetIntersectsEllipseAndLine: Gets the intersection of an ellipse and
+  //a line. The function result = true when the line either touches
+  //tangentially or passes through the ellipse. If the line touches
+  //tangentially, the coordintates returned in pt1 and pt2 will match.
+  function GetLineEllipseIntersects(const ellipseRec: TRect;
+    var linePt1, linePt2: TPointD): Boolean;
+
+  function GetPtOnEllipseFromAngle(const ellipseRect: TRectD; angle: double): TPointD;
+
+  function GetPtOnRotatedEllipseFromAngle(const ellipseRect: TRectD;
+    ellipseRotAngle, angle: double): TPointD;
+
+  function GetEllipticalAngleFromPoint(const ellipseRect: TRectD;
+    const pt: TPointD): double;
+
+  function GetRotatedEllipticalAngleFromPoint(const ellipseRect: TRectD;
+    ellipseRotAngle: double; pt: TPointD): double;
+
+  function GetClosestPtOnRotatedEllipse(const ellipseRect: TRectD;
+    ellipseRotation: double; const pt: TPointD): TPointD;
+
+  function Outline(const line: TPathD; lineWidth: double;
+    joinStyle: TJoinStyle; endStyle: TEndStyle;
+    miterLimOrRndScale: double = 0): TPathsD; overload;
+  function Outline(const lines: TPathsD; lineWidth: double;
+    joinStyle: TJoinStyle; endStyle: TEndStyle;
+    miterLimOrRndScale: double = 0): TPathsD; overload;
+
+  //Grow: Offsets path by 'delta' (positive is away from the left of the path).
+  //With a positive delta, clockwise paths will expand and counter-clockwise
+  //ones will contract. The reverse happens with negative deltas.
+  function Grow(const path, normals: TPathD; delta: double; joinStyle: TJoinStyle;
+    miterLim: double; isOpen: Boolean = false): TPathD;
+
+  function ValueAlmostZero(val: double; epsilon: double = 0.001): Boolean;
+  function ValueAlmostOne(val: double; epsilon: double = 0.001): Boolean;
+const
+  Invalid       = -MaxInt;
+  InvalidD      = -Infinity;
+  NullPoint     : TPoint  = (X: 0; Y: 0);
+  NullPointD    : TPointD = (X: 0; Y: 0);
+  InvalidPoint  : TPoint  = (X: -MaxInt; Y: -MaxInt);
+  InvalidPointD : TPointD = (X: -Infinity; Y: -Infinity);
+  NullRect      : TRect = (left: 0; top: 0; right: 0; Bottom: 0);
+  NullRectD     : TRectD = (left: 0; top: 0; right: 0; Bottom: 0);
+  InvalidRect   : TRect = (left: MaxInt; top: MaxInt; right: 0; Bottom: 0);
+  BezierTolerance: double  = 0.25;
+var
+  //AutoWidthThreshold: When JoinStyle = jsAuto, this is the threshold at
+  //which line joins will be rounded instead of squared. With wider strokes,
+  //rounded joins generally look better, but as rounding is more complex it
+  //also requries more processing and hence is slower to execute.
+  AutoWidthThreshold: double = 5.0;
+  //When lines are too narrow, they become too faint to sensibly draw
+  MinStrokeWidth: double = 0.5;
+  //Miter limit avoids excessive spikes when line offsetting
+  DefaultMiterLimit: double = 4.0;
+
+resourcestring
+  rsInvalidMatrix = 'Invalid matrix.'; //nb: always start with IdentityMatrix
+
+implementation
+
+resourcestring
+  rsInvalidQBezier = 'Invalid number of control points for a QBezier';
+  rsInvalidCBezier = 'Invalid number of control points for a CBezier';
+
+const
+  BuffSize = 64;
+
+//------------------------------------------------------------------------------
+// TSizeD
+//------------------------------------------------------------------------------
+
+function TSizeD.average: double;
+begin
+  Result := (cx + cy) * 0.5;
+end;
+
+//------------------------------------------------------------------------------
+// TRectWH record/object.
+//------------------------------------------------------------------------------
+
+function TRectWH.IsEmpty: Boolean;
+begin
+  Result := (Width <= 0) or (Height <= 0);
+end;
+//------------------------------------------------------------------------------
+
+function TRectWH.IsValid: Boolean;
+begin
+  Result := (Left <> InvalidD) and (Top <> InvalidD)
+    and (Width >= 0) and (Height >= 0);
+end;
+//------------------------------------------------------------------------------
+
+function TRectWH.Right: double;
+begin
+  Result := Left + Width;
+end;
+//------------------------------------------------------------------------------
+
+function TRectWH.Bottom: double;
+begin
+  Result := Top + Height;
+end;
+//------------------------------------------------------------------------------
+
+function TRectWH.Contains(const Pt: TPoint): Boolean;
+begin
+  Result := (pt.X >= Left) and (pt.X <= Left + Width) and
+    (pt.Y >= Top) and (pt.Y <= Top + Height)
+end;
+//------------------------------------------------------------------------------
+
+function TRectWH.Contains(const Pt: TPointD): Boolean;
+begin
+  Result := (pt.X >= Left) and (pt.X <= Left + Width) and
+    (pt.Y >= Top) and (pt.Y <= Top + Height)
+end;
+//------------------------------------------------------------------------------
+
+function TRectWH.MidPoint: TPointD;
+begin
+  Result := PointD(left + Width * 0.5, top + Height * 0.5);
+end;
+//------------------------------------------------------------------------------
+
+function TRectWH.RectD: TRectD;
+begin
+  Result := Img32.RectD(left, top, left + Width, top + Height);
+end;
+//------------------------------------------------------------------------------
+
+function TRectWH.Rect: TRect;
+begin
+  Result := Img32.Vector.Rect(RectD);
+end;
+//------------------------------------------------------------------------------
+
+function RectWH(left, top, width, height: integer): TRectWH;
+begin
+  Result.Left := left;
+  Result.Top := top;
+  Result.Width := width;
+  Result.Height := height;
+end;
+//------------------------------------------------------------------------------
+
+function RectWH(left, top, width, height: double): TRectWH;
+begin
+  Result.Left := left;
+  Result.Top := top;
+  Result.Width := width;
+  Result.Height := height;
+end;
+//------------------------------------------------------------------------------
+
+function RectWH(const rec: TRectD): TRectWH;
+begin
+  Result.Left := rec.left;
+  Result.Top := rec.top;
+  Result.Width := rec.width;
+  Result.Height := rec.height;
+end;
+//------------------------------------------------------------------------------
+
+function RectsEqual(const rec1, rec2: TRect): Boolean;
+begin
+  result := (rec1.Left = rec2.Left) and (rec1.Top = rec2.Top) and
+    (rec1.Right = rec2.Right) and (rec1.Bottom = rec2.Bottom);
+end;
+//------------------------------------------------------------------------------
+
+function Rect(const left, top, right, bottom: integer): TRect;
+begin
+  Result.Left := left;
+  Result.Top := top;
+  Result.Right := right;
+  Result.Bottom := bottom;
+end;
+//------------------------------------------------------------------------------
+
+function IsValid(value: integer): Boolean;
+begin
+  Result := value <> -MaxInt;
+end;
+//------------------------------------------------------------------------------
+
+function IsValid(value: double): Boolean;
+begin
+  Result := value <> InvalidD;
+end;
+//------------------------------------------------------------------------------
+
+function IsValid(const pt: TPoint): Boolean;
+begin
+  result := (pt.X <> Invalid) and (pt.Y <> Invalid);
+end;
+//------------------------------------------------------------------------------
+
+function IsValid(const pt: TPointD): Boolean;
+begin
+  result := (pt.X <> -Infinity) and (pt.Y <> -Infinity);
+end;
+//------------------------------------------------------------------------------
+
+function IsValid(const rec: TRect): Boolean;
+begin
+  result := (rec.Left <> MaxInt) and (rec.Top <> MaxInt);
+end;
+//------------------------------------------------------------------------------
+
+function Point(X,Y: Integer): TPoint;
+begin
+  result.X := X;
+  result.Y := Y;
+end;
+//------------------------------------------------------------------------------
+
+function Point(const pt: TPointD): TPoint;
+begin
+  result.X := Round(pt.x);
+  result.Y := Round(pt.y);
+end;
+//------------------------------------------------------------------------------
+
+function PointsEqual(const pt1, pt2: TPointD): Boolean;
+begin
+  result := (pt1.X = pt2.X) and (pt1.Y = pt2.Y);
+end;
+//------------------------------------------------------------------------------
+
+function PointsNearEqual(const pt1, pt2: TPoint; dist: integer): Boolean;
+begin
+  Result := (Abs(pt1.X - pt2.X) <= dist) and (Abs(pt1.Y - pt2.Y) < dist);
+end;
+//------------------------------------------------------------------------------
+
+function PointsNearEqual(const pt1, pt2: TPointD; distSqrd: double): Boolean;
+begin
+  Result := Sqr(pt1.X - pt2.X) + Sqr(pt1.Y - pt2.Y) < distSqrd;
+end;
+//------------------------------------------------------------------------------
+
+function StripNearDuplicates(const path: TPathD;
+  minDist: double; isClosedPath: Boolean): TPathD;
+var
+  i,j, len: integer;
+begin
+  len := length(path);
+  SetLength(Result, len);
+  if len = 0 then Exit;
+  Result[0] := path[0];
+  j := 0;
+  minDist := minDist * minDist;
+  for i := 1 to len -1 do
+    if not PointsNearEqual(Result[j], path[i], minDist) then
+    begin
+      inc(j);
+      Result[j] := path[i];
+    end;
+  if isClosedPath and
+    PointsNearEqual(Result[j], Result[0], minDist) then dec(j);
+  SetLength(Result, j +1);
+end;
+//------------------------------------------------------------------------------
+
+function StripNearDuplicates(const paths: TPathsD;
+  minLength: double; isClosedPaths: Boolean): TPathsD;
+var
+  i, len: integer;
+begin
+  len := Length(paths);
+  SetLength(Result, len);
+  for i := 0 to len -1 do
+    Result[i] := StripNearDuplicates(paths[i], minLength, isClosedPaths);
+end;
+//------------------------------------------------------------------------------
+
+function ValueAlmostZero(val: double; epsilon: double = 0.001): Boolean;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result := Abs(val) < epsilon;
+end;
+//------------------------------------------------------------------------------
+
+function ValueAlmostOne(val: double; epsilon: double = 0.001): Boolean;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result := Abs(val-1) < epsilon;
+end;
+//------------------------------------------------------------------------------
+
+procedure GetSinCos(angle: double; out sinA, cosA: double);
+{$IFDEF INLINE} inline; {$ENDIF}
+{$IFNDEF FPC}
+var s, c: extended;
+{$ENDIF}
+begin
+{$IFDEF FPC}
+  Math.SinCos(angle, sinA, cosA);
+{$ELSE}
+  Math.SinCos(angle, s, c);
+  sinA := s; cosA := c;
+{$ENDIF}
+end;
+//------------------------------------------------------------------------------
+
+function GetRotatedRectBounds(const rec: TRect; angle: double): TRect;
+var
+  sinA, cosA: double;
+  w,h, recW, recH: integer;
+  mp: TPoint;
+begin
+  NormalizeAngle(angle);
+  if angle <> 0 then
+  begin
+    GetSinCos(angle, sinA, cosA); //the sign of the angle isn't important
+    sinA := Abs(sinA); cosA := Abs(cosA);
+    RectWidthHeight(rec, recW, recH);
+    w := Ceil((recW *cosA + recH *sinA) /2);
+    h := Ceil((recW *sinA + recH *cosA) /2);
+    mp := MidPoint(rec);
+    Result := Rect(mp.X - w, mp.Y - h, mp.X + w, mp.Y +h);
+  end
+  else
+    Result := rec;
+end;
+//------------------------------------------------------------------------------
+
+function GetRotatedRectBounds(const rec: TRectD; angle: double): TRectD;
+var
+  sinA, cosA: double;
+  w,h: double;
+  mp: TPointD;
+begin
+  NormalizeAngle(angle);
+  if angle <> 0 then
+  begin
+    GetSinCos(angle, sinA, cosA); //the sign of the angle isn't important
+    sinA := Abs(sinA); cosA := Abs(cosA);
+    w := (rec.Width *cosA + rec.Height *sinA) /2;
+    h := (rec.Width *sinA + rec.Height *cosA) /2;
+    mp := rec.MidPoint;
+    Result := RectD(mp.X - w, mp.Y - h, mp.X + w, mp.Y +h);
+  end
+  else
+    Result := rec;
+end;
+//------------------------------------------------------------------------------
+
+
+function Rect(const recD: TRectD): TRect;
+begin
+  Result.Left := Floor(recD.Left);
+  Result.Top := Floor(recD.Top);
+  Result.Right := Ceil(recD.Right);
+  Result.Bottom := Ceil(recD.Bottom);
+end;
+//------------------------------------------------------------------------------
+
+function PtInRect(const rec: TRectD; const pt: TPointD): Boolean;
+begin
+  Result := (pt.X >= rec.Left) and (pt.X < rec.Right) and
+    (pt.Y >= rec.Top) and (pt.Y < rec.Bottom);
+end;
+//------------------------------------------------------------------------------
+
+function Size(cx, cy: integer): TSize;
+begin
+  Result.cx := cx;
+  Result.cy := cy;
+end;
+//------------------------------------------------------------------------------
+
+function SizeD(cx, cy: double): TSizeD;
+begin
+  Result.cx := cx;
+  Result.cy := cy;
+end;
+//------------------------------------------------------------------------------
+
+function IsClockwise(const path: TPathD): Boolean;
+begin
+  Result := Area(path) > 0;
+end;
+//------------------------------------------------------------------------------
+
+function Area(const path: TPathD): Double;
+var
+  i, j, highI: Integer;
+  d: Double;
+begin
+  Result := 0.0;
+  highI := High(path);
+  if (highI < 2) then Exit;
+  j := highI;
+  for i := 0 to highI do
+  begin
+    d := (path[j].X + path[i].X);
+    Result := Result + d * (path[j].Y - path[i].Y);
+    j := i;
+  end;
+  Result := -Result * 0.5;
+end;
+//------------------------------------------------------------------------------
+
+procedure OffsetRect(var rec: TRectD; dx, dy: double);
+begin
+  rec.Left := rec.Left + dx;
+  rec.Top := rec.Top + dy;
+  rec.Right := rec.Right + dx;
+  rec.Bottom := rec.Bottom + dy;
+end;
+//------------------------------------------------------------------------------
+
+function MakeSquare(rec: TRect): TRect;
+var
+  i: integer;
+begin
+  Result := rec;
+  i := ((rec.Right - rec.Left) + (rec.Bottom - rec.Top)) div 2;
+  Result.Right := Result.Left + i;
+  Result.Bottom := Result.Top + i;
+end;
+//------------------------------------------------------------------------------
+
+function MidPoint(const rec: TRect): TPoint;
+begin
+  Result.X := (rec.Left + rec.Right) div 2;
+  Result.Y := (rec.Top + rec.Bottom) div 2;
+end;
+//------------------------------------------------------------------------------
+
+function MidPoint(const rec: TRectD): TPointD;
+begin
+  Result.X := (rec.Left + rec.Right) * 0.5;
+  Result.Y := (rec.Top + rec.Bottom) * 0.5;
+end;
+//------------------------------------------------------------------------------
+
+function MidPoint(const pt1, pt2: TPoint): TPoint;
+begin
+  Result.X := (pt1.X + pt2.X) div 2;
+  Result.Y := (pt1.Y + pt2.Y) div 2;
+end;
+//------------------------------------------------------------------------------
+
+function MidPoint(const pt1, pt2: TPointD): TPointD;
+begin
+  Result.X := (pt1.X + pt2.X) * 0.5;
+  Result.Y := (pt1.Y + pt2.Y) * 0.5;
+end;
+//------------------------------------------------------------------------------
+
+function Average(val1, val2: integer): integer;
+begin
+  Result := (val1 + val2) div 2;
+end;
+//------------------------------------------------------------------------------
+
+function Average(val1, val2: double): double;
+begin
+  Result := (val1 + val2) * 0.5;
+end;
+//------------------------------------------------------------------------------
+
+function RectsOverlap(const rec1, rec2: TRect): Boolean;
+begin
+  Result := (rec1.Left < rec2.Right) and (rec1.Right > rec2.Left) and
+     (rec1.Top < rec2.Bottom) and (rec1.Bottom > rec2.Top);
+end;
+//------------------------------------------------------------------------------
+
+function IsSameRect(const rec1, rec2: TRect): Boolean;
+begin
+  Result := (rec1.Left = rec2.Left) and (rec1.Top = rec2.Top) and
+    (rec1.Right = rec2.Right) and (rec1.Bottom = rec2.Bottom);
+end;
+//------------------------------------------------------------------------------
+
+function RectsIntersect(const rec1, rec2: TRect): Boolean;
+var
+  dummy: TRect;
+begin
+  Result := Types.IntersectRect(dummy, rec1, rec2);
+end;
+//------------------------------------------------------------------------------
+
+function RectsIntersect(const rec1, rec2: TRectD): Boolean;
+begin
+  Result := not IntersectRect(rec1, rec2).IsEmpty;
+end;
+//------------------------------------------------------------------------------
+
+function IntersectRect(const rec1, rec2: TRectD): TRectD;
+begin
+  result.Left := Max(rec1.Left, rec2.Left);
+  result.Top := Max(rec1.Top, rec2.Top);
+  result.Right := Min(rec1.Right, rec2.Right);
+  result.Bottom := Min(rec1.Bottom, rec2.Bottom);
+end;
+//------------------------------------------------------------------------------
+
+function UnionRect(const rec1, rec2: TRect): TRect;
+begin
+  if IsEmptyRect(rec1) then
+    Result := rec2
+  else if IsEmptyRect(rec2) then
+    Result := rec1
+  else
+  begin
+    result.Left := Min(rec1.Left, rec2.Left);
+    result.Top := Min(rec1.Top, rec2.Top);
+    result.Right := Max(rec1.Right, rec2.Right);
+    result.Bottom := Max(rec1.Bottom, rec2.Bottom);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function UnionRect(const rec1, rec2: TRectD): TRectD;
+begin
+  if IsEmptyRect(rec1) then
+    Result := rec2
+  else if IsEmptyRect(rec2) then
+    Result := rec1
+  else
+  begin
+    result.Left := Min(rec1.Left, rec2.Left);
+    result.Top := Min(rec1.Top, rec2.Top);
+    result.Right := Max(rec1.Right, rec2.Right);
+    result.Bottom := Max(rec1.Bottom, rec2.Bottom);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function MakeArrayOfInteger(const ints: array of integer): TArrayOfInteger;
+var
+  i, len: integer;
+begin
+  len := Length(ints);
+  SetLength(Result, len);
+  for i := 0 to len -1 do Result[i] := ints[i];
+end;
+//------------------------------------------------------------------------------
+
+function MakeArrayOfDouble(const doubles: array of double): TArrayOfDouble;
+var
+  i, len: integer;
+begin
+  len := Length(doubles);
+  SetLength(Result, len);
+  for i := 0 to len -1 do Result[i] := doubles[i];
+end;
+//------------------------------------------------------------------------------
+
+function CrossProduct(const vector1, vector2: TPointD): double;
+begin
+  result := vector1.X * vector2.Y - vector2.X * vector1.Y;
+end;
+//------------------------------------------------------------------------------
+
+function CrossProduct(const pt1, pt2, pt3: TPointD): double;
+var
+  x1,x2,y1,y2: double;
+begin
+  x1 := pt2.X - pt1.X;
+  y1 := pt2.Y - pt1.Y;
+  x2 := pt3.X - pt2.X;
+  y2 := pt3.Y - pt2.Y;
+  result := (x1 * y2 - y1 * x2);
+end;
+//---------------------------------------------------------------------------
+
+function CrossProduct(const pt1, pt2, pt3, pt4: TPointD): double;
+var
+  x1,x2,y1,y2: double;
+begin
+  x1 := pt2.X - pt1.X;
+  y1 := pt2.Y - pt1.Y;
+  x2 := pt4.X - pt3.X;
+  y2 := pt4.Y - pt3.Y;
+  result := (x1 * y2 - y1 * x2);
+end;
+//---------------------------------------------------------------------------
+
+function DotProduct(const vector1, vector2: TPointD): double;
+begin
+  result := vector1.X * vector2.X + vector1.Y * vector2.Y;
+end;
+//------------------------------------------------------------------------------
+
+function DotProduct(const pt1, pt2, pt3: TPointD): double;
+var
+  x1,x2,y1,y2: double;
+begin
+  x1 := pt2.X - pt1.X;
+  y1 := pt2.Y - pt1.Y;
+  x2 := pt2.X - pt3.X;
+  y2 := pt2.Y - pt3.Y;
+  result := (x1 * x2 + y1 * y2);
+end;
+//------------------------------------------------------------------------------
+
+function TurnsLeft(const pt1, pt2, pt3: TPointD): boolean;
+begin
+  result := CrossProduct(pt1, pt2, pt3) < 0;
+end;
+//------------------------------------------------------------------------------
+
+function TurnsRight(const pt1, pt2, pt3: TPointD): boolean;
+begin
+  result := CrossProduct(pt1, pt2, pt3) > 0;
+end;
+//------------------------------------------------------------------------------
+
+function IsPathConvex(const path: TPathD): Boolean;
+var
+  i, pathLen: integer;
+  dir: boolean;
+begin
+  result := false;
+  pathLen := length(path);
+  if pathLen < 3 then Exit;
+  //get the winding direction of the first angle
+  dir := TurnsRight(path[0], path[1], path[2]);
+  //check that each other angle has the same winding direction
+  for i := 1 to pathLen -1 do
+    if TurnsRight(path[i], path[(i+1) mod pathLen],
+      path[(i+2) mod pathLen]) <> dir then Exit;
+  result := true;
+end;
+//------------------------------------------------------------------------------
+
+function GetUnitVector(const pt1, pt2: TPointD): TPointD;
+var
+  dx, dy, inverseHypot: Double;
+begin
+  if (pt1.x = pt2.x) and (pt1.y = pt2.y) then
+  begin
+    Result.X := 0;
+    Result.Y := 0;
+    Exit;
+  end;
+  dx := (pt2.X - pt1.X);
+  dy := (pt2.Y - pt1.Y);
+  inverseHypot := 1 / Hypot(dx, dy);
+  dx := dx * inverseHypot;
+  dy := dy * inverseHypot;
+  Result.X := dx;
+  Result.Y := dy;
+end;
+//------------------------------------------------------------------------------
+
+function GetUnitNormal(const pt1, pt2: TPointD): TPointD;
+begin
+  if not GetUnitNormal(pt1, pt2, Result) then
+    Result := NullPointD;
+end;
+//------------------------------------------------------------------------------
+
+function GetUnitNormal(const pt1, pt2: TPointD; out norm: TPointD): Boolean;
+var
+  dx, dy, inverseHypot: Double;
+begin
+  result := not PointsNearEqual(pt1, pt2, 0.001);
+  if not result then Exit;
+  dx := (pt2.X - pt1.X);
+  dy := (pt2.Y - pt1.Y);
+  inverseHypot := 1 / Hypot(dx, dy);
+  dx := dx * inverseHypot;
+  dy := dy * inverseHypot;
+  norm.X := dy;
+  norm.Y := -dx
+end;
+//------------------------------------------------------------------------------
+
+function NormalizeVector(const vec: TPointD): TPointD;
+var
+  h, inverseHypot: Double;
+begin
+  h := Hypot(vec.X, vec.Y);
+  if ValueAlmostZero(h, 0.001) then
+  begin
+    Result := NullPointD;
+    Exit;
+  end;
+  inverseHypot := 1 / h;
+  Result.X := vec.X * inverseHypot;
+  Result.Y := vec.Y * inverseHypot;
+end;
+//------------------------------------------------------------------------------
+
+function GetAvgUnitVector(const vec1, vec2: TPointD): TPointD;
+begin
+  Result := NormalizeVector(PointD(vec1.X + vec2.X, vec1.Y + vec2.Y));
+end;
+//------------------------------------------------------------------------------
+
+function Paths(const path: TPathD): TPathsD;
+begin
+  SetLength(Result, 1);
+  result[0] := Copy(path, 0, length(path));
+end;
+//------------------------------------------------------------------------------
+
+function CopyPath(const path: TPathD): TPathD;
+begin
+  Result := Copy(path, 0, Length(path));
+end;
+//------------------------------------------------------------------------------
+
+function CopyPaths(const paths: TPathsD): TPathsD;
+var
+  i, len1: integer;
+begin
+  len1 := length(paths);
+  setLength(result, len1);
+  for i := 0 to len1 -1 do
+    result[i] := Copy(paths[i], 0, length(paths[i]));
+end;
+//------------------------------------------------------------------------------
+
+function OffsetPoint(const pt: TPoint; dx, dy: integer): TPoint;
+begin
+  result.x := pt.x + dx;
+  result.y := pt.y + dy;
+end;
+//------------------------------------------------------------------------------
+
+function OffsetPoint(const pt: TPointD; dx, dy: double): TPointD;
+begin
+  result.x := pt.x + dx;
+  result.y := pt.y + dy;
+end;
+//------------------------------------------------------------------------------
+
+function OffsetPath(const path: TPathD; dx, dy: double): TPathD;
+var
+  i, len: integer;
+begin
+  len := length(path);
+  setLength(result, len);
+  for i := 0 to len -1 do
+  begin
+    result[i].x := path[i].x + dx;
+    result[i].y := path[i].y + dy;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function OffsetPath(const paths: TPathsD;
+  dx, dy: double): TPathsD;
+var
+  i,len: integer;
+begin
+  len := length(paths);
+  setLength(result, len);
+  for i := 0 to len -1 do
+    result[i] := OffsetPath(paths[i], dx, dy);
+end;
+//------------------------------------------------------------------------------
+
+function OffsetPath(const ppp: TArrayOfPathsD; dx, dy: double): TArrayOfPathsD;
+var
+  i,len: integer;
+begin
+  len := length(ppp);
+  setLength(result, len);
+  for i := 0 to len -1 do
+    result[i] := OffsetPath(ppp[i], dx, dy);
+end;
+//------------------------------------------------------------------------------
+
+function ScalePoint(const pt: TPointD; scale: double): TPointD;
+begin
+  Result.X := pt.X * scale;
+  Result.Y := pt.Y * scale;
+end;
+//------------------------------------------------------------------------------
+
+function ScalePoint(const pt: TPointD; sx, sy: double): TPointD;
+begin
+  Result.X := pt.X * sx;
+  Result.Y := pt.Y * sy;
+end;
+//------------------------------------------------------------------------------
+
+function ScalePath(const path: TPathD; sx, sy: double): TPathD;
+var
+  i, len: integer;
+begin
+  if (sx = 0) or (sy = 0) then
+    Result := nil
+  else if ((sx = 1) and (sy = 1)) then
+  begin
+    Result := Copy(path, 0, Length(path));
+  end else
+  begin
+    len := length(path);
+    setLength(result, len);
+    for i := 0 to len -1 do
+    begin
+      result[i].x := path[i].x * sx;
+      result[i].y := path[i].y * sy;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function ScalePath(const path: TPathD;
+  scale: double): TPathD;
+begin
+  result := ScalePath(path, scale, scale);
+end;
+//------------------------------------------------------------------------------
+
+function ScalePath(const paths: TPathsD;
+  sx, sy: double): TPathsD;
+var
+  i,len: integer;
+begin
+  len := length(paths);
+  setLength(result, len);
+  for i := 0 to len -1 do
+    result[i] := ScalePath(paths[i], sx, sy);
+end;
+//------------------------------------------------------------------------------
+
+function ScalePath(const paths: TPathsD;
+  scale: double): TPathsD;
+begin
+  result := ScalePath(paths, scale, scale);
+end;
+//------------------------------------------------------------------------------
+
+function ScaleRect(const rec: TRect; scale: double): TRect;
+begin
+  result := rec;
+  Result.Left := Round(Result.Left * scale);
+  Result.Top := Round(Result.Top * scale);
+  Result.Right := Round(Result.Right * scale);
+  Result.Bottom := Round(Result.Bottom * scale);
+end;
+//------------------------------------------------------------------------------
+
+function ScaleRect(const rec: TRect; sx, sy: double): TRect;
+begin
+  result := rec;
+  Result.Left := Round(Result.Left * sx);
+  Result.Top := Round(Result.Top * sy);
+  Result.Right := Round(Result.Right * sx);
+  Result.Bottom := Round(Result.Bottom * sy);
+end;
+//------------------------------------------------------------------------------
+
+function ScaleRect(const rec: TRectD; scale: double): TRectD;
+begin
+  result := rec;
+  Result.Left := Result.Left * scale;
+  Result.Top := Result.Top * scale;
+  Result.Right := Result.Right * scale;
+  Result.Bottom := Result.Bottom * scale;
+end;
+//------------------------------------------------------------------------------
+
+function ScaleRect(const rec: TRectD; sx, sy: double): TRectD;
+begin
+  result := rec;
+  Result.Left := Result.Left * sx;
+  Result.Top := Result.Top * sy;
+  Result.Right := Result.Right * sx;
+  Result.Bottom := Result.Bottom * sy;
+end;
+//------------------------------------------------------------------------------
+
+function ReversePath(const path: TPathD): TPathD;
+var
+  i, highI: integer;
+begin
+  highI := High(path);
+  SetLength(result, highI +1);
+  for i := 0 to highI do
+    result[i] := path[highI -i];
+end;
+//------------------------------------------------------------------------------
+
+function ReversePath(const paths: TPathsD): TPathsD;
+var
+  i, len: integer;
+begin
+  len := Length(paths);
+  SetLength(result, len);
+  for i := 0 to len -1 do
+    result[i] := ReversePath(paths[i]);
+end;
+//------------------------------------------------------------------------------
+
+function OpenPathToFlatPolygon(const path: TPathD): TPathD;
+var
+  i, len, len2: integer;
+begin
+  len := Length(path);
+  len2 := Max(0, len - 2);
+  setLength(Result, len + len2);
+  if len = 0 then Exit;
+  Move(path[0], Result[0], len * SizeOf(TPointD));
+  if len2 = 0 then Exit;
+  for i := 0 to len - 3 do
+    result[len + i] := path[len - 2 -i];
+end;
+//------------------------------------------------------------------------------
+
+function GetVectors(const path: TPathD): TPathD;
+var
+  i,j, len: cardinal;
+  pt: TPointD;
+begin
+  len := length(path);
+  setLength(result, len);
+  if len = 0 then Exit;
+  pt := path[0];
+  //skip duplicates
+  i := len -1;
+  while (i > 0) and
+    (path[i].X = pt.X) and (path[i].Y = pt.Y) do dec(i);
+  if (i = 0) then
+  begin
+    //all points are equal!
+    for i := 0 to len -1 do result[i] := PointD(0,0);
+    Exit;
+  end;
+  result[i] := GetUnitVector(path[i], pt);
+  //fix up any duplicates at the end of the path
+  for j := i +1 to len -1 do
+    result[j] := result[j-1];
+  //with at least one valid vector, we can now
+  //safely get the remaining vectors
+  pt := path[i];
+  for i := i -1 downto 0 do
+  begin
+    if (path[i].X <> pt.X) or (path[i].Y <> pt.Y) then
+    begin
+      result[i] := GetUnitVector(path[i], pt);
+      pt := path[i];
+    end else
+      result[i] := result[i+1]
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function GetNormals(const path: TPathD): TPathD;
+var
+  i, highI: integer;
+  last: TPointD;
+begin
+  highI := High(path);
+  setLength(result, highI+1);
+  if highI < 0 then Exit;
+
+  last := NullPointD;
+  for i := 0 to highI -1 do
+  begin
+    if GetUnitNormal(path[i], path[i+1], result[i]) then
+      last := result[i] else
+      result[i] := last;
+  end;
+  if GetUnitNormal(path[highI], path[0], result[highI]) then
+    last := result[highI];
+
+  for i := 0 to highI do
+  begin
+    if (result[i].X <> 0) or (result[i].Y <> 0) then Break;
+    result[i] := last;
+  end;
+
+end;
+//------------------------------------------------------------------------------
+
+function DistanceSqrd(const pt1, pt2: TPoint): double;
+begin
+  result := Sqr(pt1.X - pt2.X) + Sqr(pt1.Y - pt2.Y);
+end;
+//------------------------------------------------------------------------------
+
+function DistanceSqrd(const pt1, pt2: TPointD): double;
+begin
+  result := Sqr(pt1.X - pt2.X) + Sqr(pt1.Y - pt2.Y);
+end;
+//------------------------------------------------------------------------------
+
+function Distance(const pt1, pt2: TPoint): double;
+begin
+  Result := Sqrt(DistanceSqrd(pt1, pt2));
+end;
+//------------------------------------------------------------------------------
+
+function Distance(const pt1, pt2: TPointD): double;
+begin
+  Result := Sqrt(DistanceSqrd(pt1, pt2));
+end;
+//------------------------------------------------------------------------------
+
+function Distance(const path: TPathD; stopAt: integer): double;
+var
+  i, highI: integer;
+begin
+  Result := 0;
+  highI := High(path);
+  if (stopAt > 0) and (stopAt < HighI) then highI := stopAt;
+  for i := 1 to highI do
+    Result := Result + Distance(path[i-1],path[i]);
+end;
+//------------------------------------------------------------------------------
+
+function GetDistances(const path: TPathD): TArrayOfDouble;
+var
+  i, len: integer;
+begin
+  len := Length(path);
+  SetLength(Result, len);
+  if len = 0 then Exit;
+  Result[0] := 0;
+  for i := 1 to len -1 do
+    Result[i] := Distance(path[i-1], path[i]);
+end;
+//------------------------------------------------------------------------------
+
+function GetCumulativeDistances(const path: TPathD): TArrayOfDouble;
+var
+  i, len: integer;
+begin
+  len := Length(path);
+  SetLength(Result, len);
+  if len = 0 then Exit;
+  Result[0] := 0;
+  for i := 1 to len -1 do
+    Result[i] := Result[i-1] + Distance(path[i-1], path[i]);
+end;
+//------------------------------------------------------------------------------
+
+function PerpendicularDistSqrd(const pt, line1, line2: TPointD): double;
+var
+  a,b,c,d: double;
+begin
+  if PointsEqual(line1, line2) then
+  begin
+    Result := DistanceSqrd(pt, line1);
+  end else
+  begin
+    a := pt.X - line1.X;
+    b := pt.Y - line1.Y;
+    c := line2.X - line1.X;
+    d := line2.Y - line1.Y;
+    if (c = 0) and (d = 0) then
+      result := 0 else
+      result := Sqr(a * d - c * b) / (c * c + d * d);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function PointInPolyWindingCount(const pt: TPointD;
+  const path: TPathD; out PointOnEdgeDir: integer): integer;
+var
+  i, len: integer;
+  prevPt: TPointD;
+  isAbove: Boolean;
+  crossProd: double;
+begin
+  //nb: PointOnEdgeDir == 0 unless 'pt' is on 'path'
+  Result := 0;
+  PointOnEdgeDir := 0;
+  i := 0;
+  len := Length(path);
+  if len = 0 then Exit;
+  prevPt := path[len-1];
+  while (i < len) and (path[i].Y = prevPt.Y) do inc(i);
+  if i = len then Exit;
+  isAbove := (prevPt.Y < pt.Y);
+  while (i < len) do
+  begin
+    if isAbove then
+    begin
+      while (i < len) and (path[i].Y < pt.Y) do inc(i);
+      if i = len then break
+      else if i > 0 then prevPt := path[i -1];
+      crossProd := CrossProduct(prevPt, path[i], pt);
+      if crossProd = 0 then
+      begin
+        PointOnEdgeDir := -1;
+        //nb: could safely exit here with frNonZero or frEvenOdd fill rules
+      end
+      else if crossProd < 0 then dec(Result);
+    end else
+    begin
+      while (i < len) and (path[i].Y > pt.Y) do inc(i);
+      if i = len then break
+      else if i > 0 then prevPt := path[i -1];
+      crossProd := CrossProduct(prevPt, path[i], pt);
+      if crossProd = 0 then
+      begin
+        PointOnEdgeDir := 1;
+        //nb: could safely exit here with frNonZero or frEvenOdd fill rules
+      end
+      else if crossProd > 0 then inc(Result);
+    end;
+    inc(i);
+    isAbove := not isAbove;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function PointInPolygon(const pt: TPointD;
+  const polygon: TPathD; fillRule: TFillRule): Boolean;
+var
+  wc: integer;
+  PointOnEdgeDir: integer;
+begin
+  wc := PointInPolyWindingCount(pt, polygon, PointOnEdgeDir);
+  case fillRule of
+    frEvenOdd: result := (PointOnEdgeDir <> 0)  or Odd(wc);
+    frNonZero: result := (PointOnEdgeDir <> 0)  or (wc <> 0);
+    frPositive: result := (PointOnEdgeDir + wc > 0);
+    else {frNegative} result := (PointOnEdgeDir + wc < 0);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function PointInPolysWindingCount(const pt: TPointD;
+  const paths: TPathsD; out PointOnEdgeDir: integer): integer;
+var
+  i,j, len: integer;
+  p: TPathD;
+  prevPt: TPointD;
+  isAbove: Boolean;
+  crossProd: double;
+begin
+  //nb: PointOnEdgeDir == 0 unless 'pt' is on 'path'
+  Result := 0;
+  PointOnEdgeDir := 0;
+  for i := 0 to High(paths) do
+  begin
+    j := 0;
+    p := paths[i];
+    len := Length(p);
+    if len < 3 then Continue;
+    prevPt := p[len-1];
+    while (j < len) and (p[j].Y = prevPt.Y) do inc(j);
+    if j = len then continue;
+    isAbove := (prevPt.Y < pt.Y);
+    while (j < len) do
+    begin
+      if isAbove then
+      begin
+        while (j < len) and (p[j].Y < pt.Y) do inc(j);
+        if j = len then break
+        else if j > 0 then prevPt := p[j -1];
+        crossProd := CrossProduct(prevPt, p[j], pt);
+        if crossProd = 0 then PointOnEdgeDir := -1
+        else if crossProd < 0 then dec(Result);
+      end else
+      begin
+        while (j < len) and (p[j].Y > pt.Y) do inc(j);
+        if j = len then break
+        else if j > 0 then prevPt := p[j -1];
+        crossProd := CrossProduct(prevPt, p[j], pt);
+        if crossProd = 0 then PointOnEdgeDir := 1
+        else if crossProd > 0 then inc(Result);
+      end;
+      inc(j);
+      isAbove := not isAbove;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function PointInPolygons(const pt: TPointD;
+  const polygons: TPathsD; fillRule: TFillRule): Boolean;
+var
+  wc: integer;
+  PointOnEdgeDir: integer;
+begin
+  wc := PointInPolysWindingCount(pt, polygons, PointOnEdgeDir);
+  case fillRule of
+    frEvenOdd: result := (PointOnEdgeDir <> 0) or Odd(wc);
+    frNonZero: result := (PointOnEdgeDir <> 0) or (wc <> 0);
+    frPositive: result := (PointOnEdgeDir + wc > 0);
+    else {frNegative} result := (PointOnEdgeDir + wc < 0);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function PerpendicularDist(const pt, line1, line2: TPointD): double;
+var
+  a,b,c,d: double;
+begin
+  //given: cross product of 2 vectors = area of parallelogram
+  //and given: area of parallelogram = length base * height
+  //height (ie perpendic. dist.) = cross product of 2 vectors / length base
+  a := pt.X - line1.X;
+  b := pt.Y - line1.Y;
+  c := line2.X - line1.X;
+  d := line2.Y - line1.Y;
+  result := abs(a * d - c * b) / Sqrt(c * c + d * d);
+end;
+//------------------------------------------------------------------------------
+
+function ClosestPoint(const pt, linePt1, linePt2: TPointD;
+  constrainToSegment: Boolean): TPointD;
+var
+  q: double;
+begin
+  if (linePt1.X = linePt2.X) and (linePt1.Y = linePt2.Y) then
+  begin
+    Result := linePt1;
+  end else
+  begin
+    q := ((pt.X-linePt1.X)*(linePt2.X-linePt1.X) +
+      (pt.Y-linePt1.Y)*(linePt2.Y-linePt1.Y)) /
+      (sqr(linePt2.X-linePt1.X) + sqr(linePt2.Y-linePt1.Y));
+    if constrainToSegment then
+    begin
+      if q < 0 then q := 0 else if q > 1 then q := 1;
+    end;
+    Result.X := (1-q)*linePt1.X + q*linePt2.X;
+    Result.Y := (1-q)*linePt1.Y + q*linePt2.Y;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function ClosestPointOnLine(const pt, linePt1, linePt2: TPointD): TPointD;
+begin
+  result := ClosestPoint(pt, linePt1, linePt2, false);
+end;
+//------------------------------------------------------------------------------
+
+function ClosestPointOnSegment(const pt, segPt1, segPt2: TPointD): TPointD;
+begin
+  result := ClosestPoint(pt, segPt1, segPt2, true);
+end;
+//------------------------------------------------------------------------------
+
+function GetPtOnEllipseFromAngle(const ellipseRect: TRectD;
+  angle: double): TPointD;
+var
+  sn, co: double;
+begin
+  NormalizeAngle(angle);
+  GetSinCos(angle, sn, co);
+  Result.X := ellipseRect.MidPoint.X + ellipseRect.Width/2 * co;
+  Result.Y := ellipseRect.MidPoint.Y + ellipseRect.Height/2 * sn;
+end;
+//------------------------------------------------------------------------------
+
+function GetEllipticalAngleFromPoint(const ellipseRect: TRectD;
+  const pt: TPointD): double;
+begin
+  with ellipseRect do
+    Result := ArcTan2(Width/Height * (pt.Y - MidPoint.Y), (pt.X - MidPoint.X));
+end;
+//------------------------------------------------------------------------------
+
+function GetRotatedEllipticalAngleFromPoint(const ellipseRect: TRectD;
+  ellipseRotAngle: double; pt: TPointD): double;
+begin
+  Result := 0;
+  if ellipseRect.IsEmpty then Exit;
+  RotatePoint(pt, ellipseRect.MidPoint, -ellipseRotAngle);
+  Result := GetEllipticalAngleFromPoint(ellipseRect, pt);
+end;
+//------------------------------------------------------------------------------
+
+function GetPtOnRotatedEllipseFromAngle(const ellipseRect: TRectD;
+  ellipseRotAngle, angle: double): TPointD;
+begin
+  Result := GetPtOnEllipseFromAngle(ellipseRect, angle);
+  if ellipseRotAngle <> 0 then
+    img32.Vector.RotatePoint(Result, ellipseRect.MidPoint, ellipseRotAngle);
+end;
+//------------------------------------------------------------------------------
+
+function GetClosestPtOnRotatedEllipse(const ellipseRect: TRectD;
+  ellipseRotation: double; const pt: TPointD): TPointD;
+var
+  pt2: TPointD;
+  angle: double;
+begin
+  pt2 := pt;
+  Img32.Vector.RotatePoint(pt2, ellipseRect.MidPoint, -ellipseRotation);
+  angle := GetEllipticalAngleFromPoint(ellipseRect, pt2);
+  Result := GetPtOnEllipseFromAngle(ellipseRect, angle);
+  Img32.Vector.RotatePoint(Result, ellipseRect.MidPoint, ellipseRotation);
+end;
+//------------------------------------------------------------------------------
+
+function IsPointInEllipse(const ellipseRec: TRect; const pt: TPoint): Boolean;
+var
+  rec: TRectD;
+  w,h: integer;
+  x,y, y2, a,b, dx,dy: double;
+begin
+  RectWidthHeight(ellipseRec, w, h);
+  a := w * 0.5;
+  b := h * 0.5;
+  dx := ellipseRec.Left + a;
+  dy := ellipseRec.Top + b;
+  rec := RectD(ellipseRec);
+  OffsetRect(rec, -dx, -dy);
+  x := pt.X -dx; y := pt.Y -dy;
+  //first make sure pt is inside rect
+  Result := (abs(x) <= a) and (abs(y) <= b);
+  if not result then Exit;
+  //given (x*x)/(a*a) + (y*y)/(b*b) = 1
+  //then y*y = b*b(1 - (x*x)/(a*a))
+  //nb: contents of Sqrt below will always be positive
+  //since the substituted x must be within ellipseRec bounds
+  y2 := Sqrt((b*b*(1 - (x*x)/(a*a))));
+  Result := (y >= -y2) and (y <= y2);
+end;
+//------------------------------------------------------------------------------
+
+function GetLineEllipseIntersects(const ellipseRec: TRect;
+  var linePt1, linePt2: TPointD): Boolean;
+var
+  dx, dy, m,a,b,c,q: double;
+  qa,qb,qc,qs: double;
+  rec: TRectD;
+  pt1, pt2: TPointD;
+begin
+  rec := RectD(ellipseRec);
+  a := rec.Width *0.5;
+  b := rec.Height *0.5;
+  //offset ellipseRect so it's centered over the coordinate origin
+  dx := ellipseRec.Left + a; dy := ellipseRec.Top + b;
+  offsetRect(rec, -dx, -dy);
+  pt1 := OffsetPoint(linePt1, -dx, -dy);
+  pt2 := OffsetPoint(linePt2, -dx, -dy);
+  //equation of ellipse = (x*x)/(a*a) + (y*y)/(b*b) = 1
+  //equation of line = y = mx + c;
+  if (pt1.X = pt2.X) then //vertical line (ie infinite slope)
+  begin
+    //given x = K, then y*y = b*b(1 - (x*x)/(a*a))
+    q := (b*b)*(1 - Sqr(pt1.X)/(a*a));
+    result := q >= 0;
+    if not result then Exit;
+    q := Sqrt(q);
+    pt1.Y := q;
+    pt2.Y := -q;
+  end else
+  begin
+    //using simultaneous equations and substitution
+    //given y = mx + c
+    m := (pt1.Y - pt2.Y)/(pt1.X - pt2.X);
+    c := pt1.Y - m * pt1.X;
+    //given (x*x)/(a*a) + (y*y)/(b*b) = 1
+    //(x*x)/(a*a)*(b*b) + (y*y) = (b*b)
+    //(b*b)/(a*a) *(x*x) + Sqr(m*x +c) = (b*b)
+    //(b*b)/(a*a) *(x*x) + (m*m)*(x*x) + 2*m*x*c +c*c = b*b
+    //((b*b)/(a*a) +(m*m)) *(x*x) + 2*m*c*(x) + (c*c) - (b*b) = 0
+    //solving quadratic equation
+    qa := ((b*b)/(a*a) +(m*m));
+    qb := 2*m*c;
+    qc := (c*c) - (b*b);
+    qs := (qb*qb) - 4*qa*qc;
+    Result := qs >= 0;
+    if not result then Exit;
+    qs := Sqrt(qs);
+    pt1.X := (-qb +qs)/(2 * qa);
+    pt1.Y := m * pt1.X + c;
+    pt2.X := (-qb -qs)/(2 * qa);
+    pt2.Y := m * pt2.X + c;
+  end;
+  //finally reverse initial offset
+  linePt1 := OffsetPoint(pt1, dx, dy);
+  linePt2 := OffsetPoint(pt2, dx, dy);
+end;
+//------------------------------------------------------------------------------
+
+function Sign(const value: Double): integer; {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  if value < 0 then Result := -1
+  else if value > 0 then Result := 1
+  else Result := 0;
+end;
+//------------------------------------------------------------------------------
+
+function ApplyNormal(const pt, norm: TPointD; delta: double): TPointD;
+  {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  result := PointD(pt.X + norm.X * delta, pt.Y + norm.Y * delta);
+end;
+//------------------------------------------------------------------------------
+
+function GetParallelOffests(const path, norms: TPathD;
+  delta: double): TPathD;
+var
+  i, highI, len: integer;
+begin
+  len := Length(path);
+  highI := len -1;
+  SetLength(Result, len *2);
+  Result[0]  := ApplyNormal(path[0], norms[0], delta);
+  for i := 1 to highI do
+  begin
+    Result[i*2-1] := ApplyNormal(path[i], norms[i-1], delta);
+    Result[i*2]   := ApplyNormal(path[i], norms[i], delta);
+  end;
+  Result[highI*2+1] := ApplyNormal(path[0], norms[highI], delta);
+end;
+//------------------------------------------------------------------------------
+
+type
+  TGrowRec = record
+    StepsPerRad : double;
+    StepSin     : double;
+    StepCos     : double;
+    Radius      : double;
+    aSin        : double;
+    aCos        : double;
+  end;
+
+function DoRound(const pt, norm1: TPointD;
+  const growRec: TGrowRec): TPathD;
+var
+  i, steps: Integer;
+  a: Double;
+  pt2: TPointD;
+begin
+  a := ArcTan2(growRec.aSin, growRec.aCos);
+  steps := Round(growRec.StepsPerRad * Abs(a));
+  SetLength(Result, steps +1);
+
+  pt2 := PointD(norm1.x * growRec.Radius, norm1.y * growRec.Radius);
+  Result[0] := PointD(pt.x + pt2.x, pt.y + pt2.y);
+  with growRec do
+    for i := 1 to steps do
+    begin
+      pt2 := PointD(pt2.X * StepCos - StepSin * pt2.Y,
+        pt2.X * StepSin + pt2.Y * StepCos);
+      Result[i] := PointD(pt.X + pt2.X, pt.Y + pt2.Y);
+    end;
+end;
+//------------------------------------------------------------------------------
+
+function CalcRoundingSteps(radius: double): double;
+begin
+  //the results of this function have been derived empirically
+  //and may need further adjustment
+  if radius < 0.55 then result := 4
+  else result := Pi * Sqrt(radius);
+end;
+//------------------------------------------------------------------------------
+
+function Grow(const path, normals: TPathD; delta: double;
+  joinStyle: TJoinStyle; miterLim: double; isOpen: Boolean): TPathD;
+var
+  resCnt, resCap: integer;
+  norms : TPathD;
+  parallelOffsets : TPathD;
+
+  procedure AddPoint(const pt: TPointD);
+  begin
+    if resCnt >= resCap then
+    begin
+      inc(resCap, 64);
+      setLength(result, resCap);
+    end;
+    result[resCnt] := pt;
+    inc(resCnt);
+  end;
+
+  procedure DoMiter(i, prevI: integer; cosA: double);
+  var
+    a: double;
+  begin
+    a := delta / (1 + cosA); //see offset_triginometry4.svg
+    AddPoint(PointD(path[i].X + (norms[i].X + norms[prevI].X) * a,
+          path[i].Y + (norms[i].Y + norms[prevI].Y) * a));
+  end;
+  
+  procedure DoSquare(i, prevI: integer);
+  var
+    pt1, pt2, pt3, pt4: TPointD;
+    pt, ptQ : TPointD;
+    vec     : TPointD;
+  begin
+    // using the reciprocal of unit normals (as unit vectors)
+    // get the average unit vector ...
+    vec := GetAvgUnitVector(
+      PointD(-norms[prevI].Y, norms[prevI].X),
+      PointD(norms[i].Y, -norms[i].X));
+    // now offset the original vertex delta units along unit vector
+    ptQ := OffsetPoint(path[i], delta * vec.X, delta * vec.Y);
+
+    // get perpendicular vertices
+    pt1 := OffsetPoint(ptQ, delta * vec.Y, delta * -vec.X);
+    pt2 := OffsetPoint(ptQ, delta * -vec.Y, delta * vec.X);
+    // get 2 vertices along one edge offset
+    pt3 :=  parallelOffsets[prevI*2];
+    pt4 := parallelOffsets[prevI*2 +1];
+    IntersectPoint(pt1,pt2,pt3,pt4, pt);
+    AddPoint(pt);
+    //get the second intersect point through reflecion
+    pt := ReflectPoint(pt, ptQ);
+    AddPoint(pt);
+  end;
+  
+  procedure AppendPath(const path: TPathD);
+  var
+    len: integer;
+  begin
+    len := Length(path);
+    if resCnt + len > resCap then
+    begin
+      inc(resCap, len);
+      setLength(result, resCap);
+    end;
+    Move(path[0], result[resCnt], len * SizeOf(TPointD));
+    inc(resCnt, len);
+  end;
+
+var
+  i       : cardinal;
+  prevI   : cardinal;
+  len     : cardinal;
+  highI   : cardinal;
+  iLo,iHi : cardinal;
+  growRec   : TGrowRec;
+  absDelta  : double;
+  almostNoAngle: Boolean;
+begin
+  Result := nil;
+  if not Assigned(path) then exit;
+  len := Length(path);
+  if not isOpen then
+    while (len > 2) and
+      PointsNearEqual(path[len -1], path[0], 0.001) do
+        dec(len);
+  if len < 2 then Exit;
+
+  absDelta := Abs(delta);
+  if absDelta < MinStrokeWidth/2 then
+  begin
+    if delta < 0 then
+      delta := -MinStrokeWidth/2 else
+      delta := MinStrokeWidth/2;
+  end;
+  if absDelta < 1 then
+    joinStyle := jsSquare
+  else if joinStyle = jsAuto then
+  begin
+    if delta < AutoWidthThreshold / 2 then
+      joinStyle := jsSquare else
+      joinStyle := jsRound;
+  end;
+
+  if assigned(normals) then
+    norms := normals else
+    norms := GetNormals(path);
+
+  highI := len -1;
+  parallelOffsets := GetParallelOffests(path, norms, delta);
+
+  if joinStyle = jsRound then
+  begin
+    growRec.Radius := delta;
+    growRec.StepsPerRad := CalcRoundingSteps(growRec.Radius)/(Pi *2);
+    if delta < 0 then
+      GetSinCos(-1/growRec.StepsPerRad, growRec.StepSin, growRec.StepCos) else
+      GetSinCos(1/growRec.StepsPerRad, growRec.StepSin, growRec.StepCos);
+  end else
+  begin
+    if miterLim <= 0 then miterLim := DefaultMiterLimit
+    else if miterLim < 2 then miterLim := 2;
+    miterLim := 2 /(sqr(miterLim));
+    growRec.StepsPerRad := 0; //stop compiler warning.
+  end;
+
+  resCnt := 0; resCap := 0;
+
+  if isOpen then
+  begin
+    iLo := 1; iHi := highI -1;
+    prevI := 0;
+    AddPoint(parallelOffsets[0]);
+  end else
+  begin
+    iLo := 0; iHi := highI;
+    prevI := highI;
+  end;
+
+  for i := iLo to iHi do
+  begin
+
+    if PointsNearEqual(path[i], path[prevI], 0.01) then
+    begin
+       prevI := i;
+       Continue;
+    end;
+
+    growRec.aSin := CrossProduct(norms[prevI], norms[i]);
+    growRec.aCos := DotProduct(norms[prevI], norms[i]);
+
+    almostNoAngle := ValueAlmostZero(growRec.aCos -1);
+    if almostNoAngle or ((growRec.aSin * delta < 0)) then
+    begin //ie is concave
+      AddPoint(parallelOffsets[prevI*2+1]);
+      AddPoint(parallelOffsets[i*2]);
+    end
+    else if (joinStyle = jsRound) and
+      (Abs(growRec.aSin) > 0.08) then //only round if angle > ~5 deg
+    begin
+      AppendPath(DoRound(path[i], norms[prevI], growRec));
+    end
+    else if (joinStyle = jsMiter) then // nb: miterLim <= 2
+    begin                      
+      if (1 + growRec.aCos > miterLim) then //within miter range
+        DoMiter(i, prevI, growRec.aCos) else
+        DoSquare(i, prevI);
+    end
+    // don't bother squaring angles that deviate < ~20 deg. because squaring 
+    // will be indistinguishable from mitering and just be a lot slower
+    else if (growRec.aCos > 0.9) then
+      DoMiter(i, prevI, growRec.aCos) 
+    else
+      DoSquare(i, prevI);
+      
+    prevI := i;
+  end;
+  if isOpen then AddPoint(parallelOffsets[highI*2-1]);
+  SetLength(Result, resCnt);
+end;
+//------------------------------------------------------------------------------
+
+procedure AppendPath(var path: TPathD; const pt: TPointD);
+var
+  len: integer;
+begin
+  len := length(path);
+  if (len > 0) and PointsEqual(pt, path[len -1]) then Exit;
+  setLength(path, len + 1);
+  path[len] := pt;
+end;
+//------------------------------------------------------------------------------
+
+procedure AppendPath(var path1: TPathD; const path2: TPathD);
+var
+  len1, len2: integer;
+begin
+  len1 := length(path1);
+  len2 := length(path2);
+  if len2 = 0 then Exit;
+  if (len1 > 0) and PointsEqual(path2[0], path1[len1 -1]) then dec(len1);
+  setLength(path1, len1 + len2);
+  Move(path2[0], path1[len1], len2 * SizeOf(TPointD));
+end;
+//------------------------------------------------------------------------------
+
+procedure AppendPoint(var path: TPathD; const extra: TPointD);
+var
+  len: integer;
+begin
+  len := length(path);
+  SetLength(path, len +1);
+  path[len] := extra;
+end;
+//------------------------------------------------------------------------------
+
+procedure AppendPath(var paths: TPathsD;
+  const extra: TPathD);
+var
+  len1, len2: integer;
+begin
+  len2 := length(extra);
+  if len2 = 0 then Exit;
+  len1 := length(paths);
+  setLength(paths, len1 + 1);
+  paths[len1] := Copy(extra, 0, len2);
+end;
+//------------------------------------------------------------------------------
+
+procedure AppendPath(var paths: TPathsD;
+  const extra: TPathsD);
+var
+  i, len1, len2: integer;
+begin
+  len2 := length(extra);
+  if len2 = 0 then Exit;
+  len1 := length(paths);
+  setLength(paths, len1 + len2);
+  for i := 0 to len2 -1 do
+    paths[len1+i] := Copy(extra[i], 0, length(extra[i]));
+end;
+//------------------------------------------------------------------------------
+
+procedure AppendPath(var ppp: TArrayOfPathsD; const extra: TPathsD);
+var
+  len: integer;
+begin
+  len := length(ppp);
+  setLength(ppp, len + 1);
+  if Assigned(extra) then
+    AppendPath(ppp[len], extra) else
+    ppp[len] := nil;
+end;
+//------------------------------------------------------------------------------
+
+procedure RotatePoint(var pt: TPointD;
+  const focalPoint: TPointD; sinA, cosA: double);
+var
+  tmpX, tmpY: double;
+begin
+  tmpX := pt.X-focalPoint.X;
+  tmpY := pt.Y-focalPoint.Y;
+  pt.X := tmpX * cosA - tmpY * sinA + focalPoint.X;
+  pt.Y := tmpX * sinA + tmpY * cosA + focalPoint.Y;
+end;
+//------------------------------------------------------------------------------
+
+procedure RotatePoint(var pt: TPointD;
+  const focalPoint: TPointD; angleRad: double);
+var
+  sinA, cosA: double;
+begin
+  if angleRad = 0 then Exit;
+  if not ClockwiseRotationIsAnglePositive then angleRad := -angleRad;
+  GetSinCos(angleRad, sinA, cosA);
+  RotatePoint(pt, focalPoint, sinA, cosA);
+end;
+//------------------------------------------------------------------------------
+
+function RotatePathInternal(const path: TPathD;
+  const focalPoint: TPointD; sinA, cosA: double): TPathD;
+var
+  i: integer;
+  x,y: double;
+begin
+  SetLength(Result, length(path));
+  for i := 0 to high(path) do
+  begin
+    x := path[i].X - focalPoint.X;
+    y := path[i].Y - focalPoint.Y;
+    Result[i].X := x * cosA - y * sinA + focalPoint.X;
+    Result[i].Y := x * sinA + y * cosA + focalPoint.Y;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function RotatePath(const path: TPathD;
+  const focalPoint: TPointD; angleRads: double): TPathD;
+var
+  sinA, cosA: double;
+begin
+  if angleRads = 0 then
+  begin
+    Result := path;
+    Exit;
+  end;
+  if not ClockwiseRotationIsAnglePositive then angleRads := -angleRads;
+  GetSinCos(angleRads, sinA, cosA);
+  Result := RotatePathInternal(path, focalPoint, sinA, cosA);
+end;
+//------------------------------------------------------------------------------
+
+function RotatePath(const paths: TPathsD;
+  const focalPoint: TPointD; angleRads: double): TPathsD;
+var
+  i: integer;
+  sinA, cosA: double;
+  fp: TPointD;
+begin
+  Result := paths;
+  if not IsValid(angleRads) then Exit;
+  NormalizeAngle(angleRads);
+  if angleRads = 0 then Exit;
+  if not ClockwiseRotationIsAnglePositive then
+    angleRads := -angleRads;
+  GetSinCos(angleRads, sinA, cosA);
+  SetLength(Result, length(paths));
+  if IsValid(focalPoint) then
+    fp := focalPoint else
+    fp := GetBoundsD(paths).MidPoint;
+  for i := 0 to high(paths) do
+    Result[i] := RotatePathInternal(paths[i], fp, sinA, cosA);
+end;
+//------------------------------------------------------------------------------
+
+function GetAngle(const origin, pt: TPoint): double;
+var
+  x,y: double;
+begin
+  x := pt.X - origin.X;
+  y := pt.Y - origin.Y;
+  if x = 0 then
+  begin
+    if y > 0 then result := angle90
+    else result := -angle90;
+  end
+  else if y = 0 then
+  begin
+    if x > 0 then result := 0
+    else result := angle180;
+  end else
+    result := arctan2(y, x); //range between -Pi and Pi
+  if not ClockwiseRotationIsAnglePositive then Result := -Result;
+end;
+//------------------------------------------------------------------------------
+
+function GetAngle(const origin, pt: TPointD): double;
+var
+  x,y: double;
+begin
+  x := pt.X - origin.X;
+  y := pt.Y - origin.Y;
+  if x = 0 then
+  begin
+    if y > 0 then result := angle90
+    else result := -angle90;
+  end
+  else if y = 0 then
+  begin
+    if x > 0 then result := 0
+    else result := angle180;
+  end else
+    result := arctan2(y, x); //range between -Pi and Pi
+  if not ClockwiseRotationIsAnglePositive then Result := -Result;
+end;
+//------------------------------------------------------------------------------
+
+function GetAngle(const a, b, c: TPoint): double;
+var
+  ab, bc: TPointD;
+  dp, cp: double;
+begin
+  //https://stackoverflow.com/a/3487062/359538
+  ab := PointD(b.x - a.x, b.y - a.y);
+  bc := PointD(b.x - c.x, b.y - c.y);
+  dp := (ab.x * bc.x + ab.y * bc.y);
+  cp := (ab.x * bc.y - ab.y * bc.x);
+  Result := arctan2(cp, dp); //range between -Pi and Pi
+  if not ClockwiseRotationIsAnglePositive then Result := -Result;
+end;
+//------------------------------------------------------------------------------
+
+function GetAngle(const a, b, c: TPointD): double;
+var
+  ab, bc: TPointD;
+  dp, cp: double;
+begin
+  //https://stackoverflow.com/a/3487062/359538
+  ab := PointD(b.x - a.x, b.y - a.y);
+  bc := PointD(b.x - c.x, b.y - c.y);
+  dp := (ab.x * bc.x + ab.y * bc.y);
+  cp := (ab.x * bc.y - ab.y * bc.x);
+  Result := arctan2(cp, dp); //range between -Pi and Pi
+  if not ClockwiseRotationIsAnglePositive then Result := -Result;
+end;
+//------------------------------------------------------------------------------
+
+function GetPointAtAngleAndDist(const origin: TPointD;
+  angle, distance: double): TPointD;
+begin
+  Result := origin;
+  Result.X := Result.X + distance;
+  RotatePoint(Result, origin, angle);
+end;
+//------------------------------------------------------------------------------
+
+function IntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPointD): TPointD;
+var
+  m1,b1,m2,b2: double;
+begin
+  result := InvalidPointD;
+  //see http://astronomy.swin.edu.au/~pbourke/geometry/lineline2d/
+  if (ln1B.X = ln1A.X) then
+  begin
+    if (ln2B.X = ln2A.X) then exit; //parallel lines
+    m2 := (ln2B.Y - ln2A.Y)/(ln2B.X - ln2A.X);
+    b2 := ln2A.Y - m2 * ln2A.X;
+    Result.X := ln1A.X;
+    Result.Y := m2*ln1A.X + b2;
+  end
+  else if (ln2B.X = ln2A.X) then
+  begin
+    m1 := (ln1B.Y - ln1A.Y)/(ln1B.X - ln1A.X);
+    b1 := ln1A.Y - m1 * ln1A.X;
+    Result.X := ln2A.X;
+    Result.Y := m1*ln2A.X + b1;
+  end else
+  begin
+    m1 := (ln1B.Y - ln1A.Y)/(ln1B.X - ln1A.X);
+    b1 := ln1A.Y - m1 * ln1A.X;
+    m2 := (ln2B.Y - ln2A.Y)/(ln2B.X - ln2A.X);
+    b2 := ln2A.Y - m2 * ln2A.X;
+    if m1 = m2 then exit; //parallel lines
+    Result.X := (b2 - b1)/(m1 - m2);
+    Result.Y := m1 * Result.X + b1;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function IntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPointD;
+  out ip: TPointD): Boolean;
+begin
+  ip := IntersectPoint(ln1a, ln1b, ln2a, ln2b);
+  Result := IsValid(ip);
+end;
+//------------------------------------------------------------------------------
+
+function SegmentIntersectPt(const ln1a, ln1b, ln2a, ln2b: TPointD): TPointD;
+var
+  pqd,r,s : TPointD; //scalar vectors;
+  rs, t   : double;
+begin
+  //https://stackoverflow.com/a/565282/359538
+  Result := InvalidPointD;
+  r := PointD(ln1b.X - ln1a.X, ln1b.Y - ln1a.Y);
+  s := PointD(ln2b.X - ln2a.X, ln2b.Y - ln2a.Y);
+  rs := CrossProduct(r,s);
+  if Abs(rs) < 1 then Exit;
+  pqd.X := ln2a.X - ln1a.X;
+  pqd.y := ln2a.Y - ln1a.Y;
+  t := CrossProduct(pqd, s) / rs;
+  if (t < -0.025) or (t > 1.025) then Exit;
+  Result.X := ln1a.X + t * r.X;
+  Result.Y := ln1a.Y + t * r.Y;
+//  pqd.X := -pqd.X; pqd.Y := -pqd.Y;
+//  u := CrossProduct(pqd, r) / rs;
+//  if (u < -0.05) or (u > 1.05) then Exit;
+end;
+//------------------------------------------------------------------------------
+
+function SegmentsIntersect(const ln1a, ln1b, ln2a, ln2b: TPointD;
+  out ip: TPointD): Boolean;
+begin
+  ip := SegmentIntersectPt(ln1a, ln1b, ln2a, ln2b);
+  Result := IsValid(ip);
+end;
+//------------------------------------------------------------------------------
+
+function ReverseNormals(const norms: TPathD): TPathD;
+var
+  i, highI: integer;
+begin
+  highI := high(norms);
+  setLength(result, highI +1);
+  for i := 1 to highI  do
+  begin
+    result[i -1].X := -norms[highI -i].X;
+    result[i -1].Y := -norms[highI -i].Y;
+  end;
+  result[highI].X := -norms[highI].X;
+  result[highI].Y := -norms[highI].Y;
+end;
+//------------------------------------------------------------------------------
+
+function GrowOpenLine(const line: TPathD; width: double;
+  joinStyle: TJoinStyle; endStyle: TEndStyle;
+  miterLimOrRndScale: double): TPathD;
+var
+  len, x,y: integer;
+  segLen, halfWidth: double;
+  normals, lineL, lineR, arc: TPathD;
+  invNorm: TPointD;
+  growRec: TGrowRec;
+begin
+  Result := nil;
+  len := length(line);
+  if len = 0 then Exit;
+  if width < MinStrokeWidth then
+    width := MinStrokeWidth;
+  halfWidth := width * 0.5;
+  if len = 1 then
+  begin
+    x := Round(line[0].X);
+    y := Round(line[0].Y);
+    SetLength(result, 1);
+    result := Ellipse(RectD(x -halfWidth, y -halfWidth,
+      x +halfWidth, y +halfWidth));
+    Exit;
+  end;
+
+  //with very narrow lines, don't get fancy with joins and line ends
+  if (width <= 2) then
+  begin
+    joinStyle := jsSquare;
+    if endStyle = esRound then endStyle := esSquare;
+  end
+  else if joinStyle = jsAuto then
+  begin
+    if (endStyle = esRound) and
+      (width >= AutoWidthThreshold) then
+      joinStyle := jsRound
+    else
+      joinStyle := jsSquare;
+  end;
+
+  normals := GetNormals(line);
+  if endStyle = esRound then
+  begin
+    //get the rounding parameters
+    growRec.StepsPerRad :=
+      CalcRoundingSteps(halfWidth * miterLimOrRndScale)/(Pi*2);
+    GetSinCos(1/growRec.StepsPerRad, growRec.StepSin, growRec.StepCos);
+    growRec.Radius := halfWidth;
+
+    //grow the line's left side of the line => line1
+    lineL := Grow(line, normals,
+      halfWidth, joinStyle, miterLimOrRndScale, true);
+    //build the rounding at the start => result
+    invNorm.X := -normals[0].X;
+    invNorm.Y := -normals[0].Y;
+    growRec.aSin := invNorm.X * normals[0].Y - invNorm.Y * normals[0].X;
+    growRec.aCos := invNorm.X * normals[0].X + invNorm.Y * normals[0].Y;
+    Result := DoRound(line[0], invNorm, growRec);
+    //join line1 into result
+    AppendPath(Result, lineL);
+    //reverse the normals and build the end arc => arc
+    normals := ReverseNormals(normals);
+    invNorm.X := -normals[0].X; invNorm.Y := -normals[0].Y;
+    growRec.aSin := invNorm.X * normals[0].Y - invNorm.Y * normals[0].X;
+    growRec.aCos := invNorm.X * normals[0].X + invNorm.Y * normals[0].Y;
+    arc := DoRound(line[High(line)], invNorm, growRec);
+    //grow the line's right side of the line
+    lineR := Grow(ReversePath(line), normals,
+      halfWidth, joinStyle, miterLimOrRndScale, true);
+    //join arc and line2 into result
+    AppendPath(Result, arc);
+    AppendPath(Result, lineR);
+  end else
+  begin
+    lineL := Copy(line, 0, len);
+    if endStyle = esSquare then
+    begin
+      // esSquare => extends both line ends by 1/2 lineWidth
+      AdjustPoint(lineL[0], lineL[1], width * 0.5);
+      AdjustPoint(lineL[len-1], lineL[len-2], width * 0.5);
+    end else
+    begin
+      //esButt -> extend only very short end segments
+      segLen := Distance(lineL[0], lineL[1]);
+      if segLen < width * 0.5 then
+        AdjustPoint(lineL[0], lineL[1], width * 0.5 - segLen);
+      segLen := Distance(lineL[len-1], lineL[len-2]);
+      if segLen < width * 0.5 then
+        AdjustPoint(lineL[len-1], lineL[len-2], width * 0.5 - segLen);
+    end;
+    //first grow the left side of the line => Result
+    Result := Grow(lineL, normals,
+      halfWidth, joinStyle, miterLimOrRndScale, true);
+    //reverse normals and path and grow the right side => lineR
+    normals := ReverseNormals(normals);
+    lineR := Grow(ReversePath(lineL), normals,
+      halfWidth, joinStyle, miterLimOrRndScale, true);
+    //join both sides
+    AppendPath(Result, lineR);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function GrowClosedLine(const line: TPathD; width: double;
+  joinStyle: TJoinStyle; miterLimOrRndScale: double): TPathsD;
+var
+  line2, norms: TPathD;
+  rec: TRectD;
+  skipHole: Boolean;
+begin
+  rec := GetBoundsD(line);
+  skipHole := (rec.Width <= width) or (rec.Height <= width);
+  if skipHole then
+  begin
+    SetLength(Result, 1);
+    norms := GetNormals(line);
+    Result[0] := Grow(line, norms, width/2, joinStyle, miterLimOrRndScale);
+  end else
+  begin
+    SetLength(Result, 2);
+    norms := GetNormals(line);
+    Result[0] := Grow(line, norms, width/2, joinStyle, miterLimOrRndScale);
+    line2 := ReversePath(line);
+    norms := ReverseNormals(norms);
+    Result[1] := Grow(line2, norms, width/2, joinStyle, miterLimOrRndScale);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function Outline(const line: TPathD; lineWidth: double;
+  joinStyle: TJoinStyle; endStyle: TEndStyle;
+  miterLimOrRndScale: double): TPathsD;
+begin
+  if not assigned(line) then
+    Result := nil
+  else if endStyle = esClosed then
+    result := GrowClosedLine(line,
+      lineWidth, joinStyle, miterLimOrRndScale)
+  else
+  begin
+    SetLength(Result,1);
+    result[0] := GrowOpenLine(line, lineWidth,
+      joinStyle, endStyle, miterLimOrRndScale);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function Outline(const lines: TPathsD; lineWidth: double;
+  joinStyle: TJoinStyle; endStyle: TEndStyle;
+  miterLimOrRndScale: double): TPathsD;
+var
+  i: integer;
+begin
+  result := nil;
+  if not assigned(lines) then exit;
+  if joinStyle = jsAuto then
+  begin
+    if endStyle in [esPolygon, esRound] then
+      joinStyle := jsRound else
+      joinStyle := jsSquare;
+  end;
+  if endStyle = esPolygon then
+    for i := 0 to high(lines) do
+      AppendPath(Result, GrowClosedLine(lines[i],
+        lineWidth, joinStyle, miterLimOrRndScale))
+  else
+    for i := 0 to high(lines) do
+      AppendPath(Result, GrowOpenLine(lines[i], lineWidth,
+        joinStyle, endStyle, miterLimOrRndScale));
+end;
+//------------------------------------------------------------------------------
+
+function Rectangle(const rec: TRect): TPathD;
+begin
+  setLength(Result, 4);
+  with rec do
+  begin
+    result[0] := PointD(left, top);
+    result[1] := PointD(right, top);
+    result[2] := PointD(right, bottom);
+    result[3] := PointD(left, bottom);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function Rectangle(const rec: TRectD): TPathD;
+begin
+  setLength(Result, 4);
+  with rec do
+  begin
+    result[0] := PointD(left, top);
+    result[1] := PointD(right, top);
+    result[2] := PointD(right, bottom);
+    result[3] := PointD(left, bottom);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function Rectangle(l, t, r, b: double): TPathD;
+begin
+  setLength(Result, 4);
+  result[0] := PointD(l, t);
+  result[1] := PointD(r, t);
+  result[2] := PointD(r, b);
+  result[3] := PointD(l, b);
+end;
+//------------------------------------------------------------------------------
+
+procedure InflateRect(var rec: TRect; dx, dy: integer);
+begin
+  rec.Left := rec.Left - dx;
+  rec.Top := rec.Top - dy;
+  rec.Right := rec.Right + dx;
+  rec.Bottom := rec.Bottom + dy;
+end;
+//------------------------------------------------------------------------------
+
+procedure InflateRect(var rec: TRectD; dx, dy: double);
+begin
+  rec.Left := rec.Left - dx;
+  rec.Top := rec.Top - dy;
+  rec.Right := rec.Right + dx;
+  rec.Bottom := rec.Bottom + dy;
+end;
+//------------------------------------------------------------------------------
+
+function NormalizeRect(var rect: TRect): Boolean;
+var
+  i: integer;
+begin
+  Result := False;
+  with rect do
+  begin
+    if Left > Right then
+    begin
+      i := Left;
+      Left := Right;
+      Right := i;
+      Result := True;
+    end;
+    if Top > Bottom then
+    begin
+      i := Top;
+      Top := Bottom;
+      Bottom := i;
+      Result := True;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function RoundRect(const rec: TRect; radius: integer): TPathD;
+begin
+  Result := RoundRect(RectD(rec), PointD(radius, radius));
+end;
+//------------------------------------------------------------------------------
+
+function RoundRect(const rec: TRect; radius: TPoint): TPathD;
+begin
+  Result := RoundRect(RectD(rec), PointD(radius));
+end;
+//------------------------------------------------------------------------------
+
+function RoundRect(const rec: TRectD; radius: double): TPathD;
+begin
+  Result := RoundRect(rec, PointD(radius, radius));
+end;
+//------------------------------------------------------------------------------
+
+function RoundRect(const rec: TRectD; radius: TPointD): TPathD;
+var
+  i,j     : integer;
+  corners : TPathD;
+  bezPts  : TPathD;
+  magic   : TPointD;
+const
+  magicC: double = 0.55228475; // =4/3 * (sqrt(2)-1)
+begin
+  Result := nil;
+  if rec.IsEmpty then Exit;
+  radius.X := Min(radius.X, rec.Width/2);
+  radius.Y := Min(radius.Y, rec.Height/2);
+  if (radius.X < 1) and (radius.Y < 1) then
+  begin
+    Result := Rectangle(rec);
+    Exit;
+  end;
+  magic.X := radius.X * magicC;
+  magic.Y := radius.Y * magicC;
+  SetLength(Corners, 4);
+  with rec do
+  begin
+    corners[0] := PointD(Right, Top);
+    corners[1] := BottomRight;
+    corners[2] := PointD(Left, Bottom);
+    corners[3] := TopLeft;
+  end;
+  SetLength(Result, 1);
+  Result[0].X := corners[3].X + radius.X;
+  Result[0].Y := corners[3].Y;
+  SetLength(bezPts, 4);
+  for i := 0 to High(corners) do
+  begin
+    for j := 0 to 3 do bezPts[j] := corners[i];
+    case i of
+      3:
+        begin
+          bezPts[0].Y := bezPts[0].Y + radius.Y;
+          bezPts[1].Y := bezPts[0].Y - magic.Y;
+          bezPts[3].X := bezPts[3].X + radius.X;
+          bezPts[2].X := bezPts[3].X - magic.X;
+        end;
+      0:
+        begin
+          bezPts[0].X := bezPts[0].X - radius.X;
+          bezPts[1].X := bezPts[0].X + magic.X;
+          bezPts[3].Y := bezPts[3].Y + radius.Y;
+          bezPts[2].Y := bezPts[3].Y - magic.Y;
+        end;
+      1:
+        begin
+          bezPts[0].Y := bezPts[0].Y - radius.Y;
+          bezPts[1].Y := bezPts[0].Y + magic.Y;
+          bezPts[3].X := bezPts[3].X - radius.X;
+          bezPts[2].X := bezPts[3].X + magic.X;
+        end;
+      2:
+        begin
+          bezPts[0].X := bezPts[0].X + radius.X;
+          bezPts[1].X := bezPts[0].X - magic.X;
+          bezPts[3].Y := bezPts[3].Y - radius.Y;
+          bezPts[2].Y := bezPts[3].Y + magic.Y;
+        end;
+    end;
+    AppendPath(Result, FlattenCBezier(bezPts));
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function Circle(const pt: TPoint; radius: double): TPathD;
+var
+  rec: TRectD;
+begin
+  rec.Left := pt.X - radius;
+  rec.Right := pt.X + radius;
+  rec.Top := pt.Y - radius;
+  rec.Bottom := pt.Y + radius;
+  Result := Ellipse(rec);
+end;
+//------------------------------------------------------------------------------
+
+function Circle(const pt: TPointD; radius: double): TPathD;
+var
+  rec: TRectD;
+begin
+  rec.Left := pt.X - radius;
+  rec.Right := pt.X + radius;
+  rec.Top := pt.Y - radius;
+  rec.Bottom := pt.Y + radius;
+  Result := Ellipse(rec);
+end;
+//------------------------------------------------------------------------------
+
+function Circle(const pt: TPointD; radius: double; pendingScale: double): TPathD;
+var
+  rec: TRectD;
+begin
+  rec.Left := pt.X - radius;
+  rec.Right := pt.X + radius;
+  rec.Top := pt.Y - radius;
+  rec.Bottom := pt.Y + radius;
+  Result := Ellipse(rec, pendingScale);
+end;
+//------------------------------------------------------------------------------
+
+function Ellipse(const rec: TRectD; pendingScale: double): TPathD;
+var
+  steps: integer;
+begin
+  if pendingScale <= 0 then pendingScale := 1;
+  steps := Round(CalcRoundingSteps((rec.width + rec.Height) * pendingScale));
+  Result := Ellipse(rec, steps);
+end;
+//------------------------------------------------------------------------------
+
+
+function Ellipse(const rec: TRect; steps: integer): TPathD;
+begin
+  Result := Ellipse(RectD(rec), steps);
+end;
+//------------------------------------------------------------------------------
+
+function Ellipse(const rec: TRectD; steps: integer): TPathD;
+var
+  i: Integer;
+  sinA, cosA: double;
+  centre, radius, delta: TPointD;
+begin
+  result := nil;
+  if rec.IsEmpty then Exit;
+  with rec do
+  begin
+    centre := rec.MidPoint;
+    radius := PointD(Width * 0.5, Height  * 0.5);
+  end;
+  if steps < 4 then
+    steps := Round(CalcRoundingSteps(rec.width + rec.height));
+  GetSinCos(2 * Pi / Steps, sinA, cosA);
+  delta.x := cosA; delta.y := sinA;
+  SetLength(Result, Steps);
+  Result[0] := PointD(centre.X + radius.X, centre.Y);
+  for i := 1 to steps -1 do
+  begin
+    Result[i] := PointD(centre.X + radius.X * delta.x,
+      centre.Y + radius.y * delta.y);
+    delta :=  PointD(delta.X * cosA - delta.Y * sinA,
+      delta.Y * cosA + delta.X * sinA);
+  end; //rotates clockwise
+end;
+//------------------------------------------------------------------------------
+
+function RotatedEllipse(const rec: TRectD; angle: double; steps: integer = 0): TPathD;
+begin
+  Result := Ellipse(rec, steps);
+  if angle = 0 then Exit;
+  Result := RotatePath(Result, rec.MidPoint, angle);
+end;
+//------------------------------------------------------------------------------
+
+function RotatedEllipse(const rec: TRectD; angle: double; pendingScale: double): TPathD;
+begin
+  Result := Ellipse(rec, pendingScale);
+  if angle = 0 then Exit;
+  Result := RotatePath(Result, rec.MidPoint, angle);
+end;
+//------------------------------------------------------------------------------
+
+function AngleToEllipticalAngle(const ellRec: TRectD; angle: double): double;
+begin
+  Result := arctan2(ellRec.Height/ellRec.Width * sin(angle), cos(angle));
+end;
+//------------------------------------------------------------------------------
+
+function EllipticalAngleToAngle(const ellRec: TRectD; angle: double): double;
+begin
+  Result := ArcTan2(sin(angle) *ellRec.Width, cos(angle) * ellRec.Height);
+end;
+//------------------------------------------------------------------------------
+
+function Star(const rec: TRectD; points: integer; indentFrac: double): TPathD;
+var
+  i: integer;
+  innerOff: double;
+  p, p2: TPathD;
+  rec2: TRectD;
+begin
+  Result := nil;
+  if points < 5 then points := 5
+  else if points > 15 then points := 15;
+  if indentFrac < 0.2 then indentFrac := 0.2
+  else if indentFrac > 0.8 then indentFrac := 0.8;
+  innerOff := Min(rec.Width, rec.Height) * indentFrac * 0.5;
+  if not Odd(points) then inc(points);
+  p := Ellipse(rec, points);
+  if not Assigned(p) then Exit;
+  rec2 := rec;
+  Img32.Vector.InflateRect(rec2, -innerOff, -innerOff);
+  if rec2.IsEmpty then
+    p2 := Ellipse(rec, points*2) else
+    p2 := Ellipse(rec2, points*2);
+  SetLength(Result, points*2);
+  for i := 0 to points -1 do
+  begin
+    Result[i*2] := p[i];
+    Result[i*2+1] := p2[i*2+1];
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function Star(const focalPt: TPointD;
+  innerRadius, outerRadius: double; points: integer): TPathD;
+var
+  i: Integer;
+  sinA, cosA: double;
+  delta: TPointD;
+begin
+  result := nil;
+  if (innerRadius <= 0) or (outerRadius <= 0) then Exit;
+  if points <= 5 then points := 10
+  else points := points * 2;
+  GetSinCos(2 * Pi / points, sinA, cosA);
+  delta.x := cosA; delta.y := sinA;
+  SetLength(Result, points);
+  Result[0] := PointD(focalPt.X + innerRadius, focalPt.Y);
+  for i := 1 to points -1 do
+  begin
+    if Odd(i) then
+      Result[i] := PointD(focalPt.X + outerRadius * delta.x,
+        focalPt.Y + outerRadius * delta.y)
+    else
+      Result[i] := PointD(focalPt.X + innerRadius * delta.x,
+        focalPt.Y + innerRadius * delta.y);
+    delta :=  PointD(delta.X * cosA - delta.Y * sinA,
+      delta.Y * cosA + delta.X * sinA);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function Arc(const rec: TRectD;
+  startAngle, endAngle: double; scale: double): TPathD;
+var
+  i, steps: Integer;
+  angle: double;
+  sinA, cosA: double;
+  centre, radius: TPointD;
+  deltaX, deltaX2, deltaY: double;
+const
+  qtrDeg = PI/1440;
+begin
+  Result := nil;
+  if (endAngle = startAngle) or IsEmptyRect(rec) then Exit;
+  if scale <= 0 then scale := 4.0;
+  if not ClockwiseRotationIsAnglePositive then
+  begin
+    startAngle := -startAngle;
+    endAngle := -endAngle;
+  end;
+  NormalizeAngle(startAngle, qtrDeg);
+  NormalizeAngle(endAngle, qtrDeg);
+  with rec do
+  begin
+    centre := MidPoint;
+    radius := PointD(Width * 0.5, Height  * 0.5);
+  end;
+  if endAngle < startAngle then
+    angle := endAngle - startAngle + angle360 else
+    angle := endAngle - startAngle;
+  //steps = (No. steps for a whole ellipse) * angle/(2*Pi)
+  steps := Round(CalcRoundingSteps((rec.width + rec.height) * scale));
+  steps := steps div 2; /////////////////////////////////
+  if steps < 2 then steps := 2;
+  SetLength(Result, Steps +1);
+  //angle of the first step ...
+  GetSinCos(startAngle, deltaY, deltaX);
+  Result[0].X := centre.X + radius.X * deltaX;
+  Result[0].Y := centre.Y + radius.y * deltaY;
+  //angle of each subsequent step ...
+  GetSinCos(angle / Steps, sinA, cosA);
+  for i := 1 to steps do
+  begin
+    deltaX2 := deltaX * cosA - deltaY * sinA;
+    deltaY := deltaY * cosA + deltaX * sinA;
+    deltaX := deltaX2;
+    Result[i].X := centre.X + radius.X * deltaX;
+    Result[i].Y := centre.Y + radius.y * deltaY;
+  end; //progresses clockwise from start to end
+end;
+//------------------------------------------------------------------------------
+
+function Pie(const rec: TRectD;
+  StartAngle, EndAngle: double; scale: double): TPathD;
+var
+  len: integer;
+begin
+  result := Arc(rec, StartAngle, EndAngle, scale);
+  len := length(result);
+  setLength(result, len +1);
+  result[len] := PointD((rec.Left + rec.Right)/2, (rec.Top + rec.Bottom)/2);
+end;
+//------------------------------------------------------------------------------
+
+function ArrowHead(const arrowTip, ctrlPt: TPointD; size: double;
+  arrowStyle: TArrowStyle): TPathD;
+var
+  unitVec, basePt: TPointD;
+  sDiv40, sDiv50, sDiv60, sDiv120: double;
+begin
+  result := nil;
+  sDiv40 := size * 0.40;
+  sDiv50 := size * 0.50;
+  sDiv60 := size * 0.60;
+  sDiv120 := sDiv60 * 2;
+  unitVec := GetUnitVector(ctrlPt, arrowTip);
+  case arrowStyle of
+    asNone:
+      Exit;
+    asSimple:
+      begin
+        setLength(result, 3);
+        basePt := OffsetPoint(arrowTip, -unitVec.X * size, -unitVec.Y * size);
+        result[0] := arrowTip;
+        result[1] := OffsetPoint(basePt, -unitVec.Y * sDiv50, unitVec.X * sDiv50);
+        result[2] := OffsetPoint(basePt, unitVec.Y * sDiv50, -unitVec.X * sDiv50);
+      end;
+    asFancy:
+      begin
+        setLength(result, 4);
+        basePt := OffsetPoint(arrowTip,
+          -unitVec.X * sDiv120, -unitVec.Y * sDiv120);
+        result[0] := OffsetPoint(basePt, -unitVec.Y *sDiv50, unitVec.X *sDiv50);
+        result[1] := OffsetPoint(arrowTip, -unitVec.X *size, -unitVec.Y *size);
+        result[2] := OffsetPoint(basePt, unitVec.Y *sDiv50, -unitVec.X *sDiv50);
+        result[3] := arrowTip;
+      end;
+    asDiamond:
+      begin
+        setLength(result, 4);
+        basePt := OffsetPoint(arrowTip, -unitVec.X * sDiv60, -unitVec.Y * sDiv60);
+        result[0] := arrowTip;
+        result[1] := OffsetPoint(basePt, -unitVec.Y * sDiv50, unitVec.X * sDiv50);
+        result[2] := OffsetPoint(arrowTip, -unitVec.X * sDiv120, -unitVec.Y * sDiv120);
+        result[3] := OffsetPoint(basePt, unitVec.Y * sDiv50, -unitVec.X * sDiv50);
+      end;
+    asCircle:
+      begin
+        basePt := OffsetPoint(arrowTip, -unitVec.X * sDiv50, -unitVec.Y * sDiv50);
+        with Point(basePt) do
+          result := Ellipse(RectD(x - sDiv50, y - sDiv50, x + sDiv50, y + sDiv50));
+      end;
+    asTail:
+      begin
+        setLength(result, 6);
+        basePt := OffsetPoint(arrowTip, -unitVec.X * sDiv60, -unitVec.Y * sDiv60);
+        result[0] := OffsetPoint(arrowTip, -unitVec.X * sDiv50, -unitVec.Y * sDiv50);
+        result[1] := OffsetPoint(arrowTip, -unitVec.Y * sDiv40, unitVec.X * sDiv40);
+        result[2] := OffsetPoint(basePt, -unitVec.Y * sDiv40, unitVec.X * sDiv40);
+        result[3] := OffsetPoint(arrowTip, -unitVec.X * sDiv120, -unitVec.Y * sDiv120);
+        result[4] := OffsetPoint(basePt, unitVec.Y * sDiv40, -unitVec.X * sDiv40);
+        result[5] := OffsetPoint(arrowTip, unitVec.Y * sDiv40, -unitVec.X * sDiv40);
+      end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function GetDefaultArrowHeadSize(lineWidth: double): double;
+begin
+  Result := lineWidth *3 + 7;
+end;
+//------------------------------------------------------------------------------
+
+procedure AdjustPoint(var pt: TPointD; const referencePt: TPointD; delta: double);
+var
+  vec: TPointD;
+begin
+  //Positive delta moves pt away from referencePt, and
+  //negative delta moves pt toward referencePt.
+  vec := GetUnitVector(referencePt, pt);
+  pt.X := pt.X + (vec.X * delta);
+  pt.Y := pt.Y + (vec.Y * delta);
+end;
+//------------------------------------------------------------------------------
+
+function ShortenPath(const path: TPathD;
+  pathEnd: TPathEnd; amount: double): TPathD;
+var
+  len, amount2: double;
+  vec: TPointD;
+  i, highPath: integer;
+begin
+  result := path;
+  highPath := high(path);
+  if highPath < 1 then Exit;
+  amount2 := amount;
+  if pathEnd <> peEnd then
+  begin
+    //shorten start
+    i := 0;
+    while (i < highPath) do
+    begin
+      len := Distance(result[i], result[i+1]);
+      if (len >= amount) then Break;
+      amount := amount - len;
+      inc(i);
+    end;
+    if i > 0 then
+    begin
+      Move(path[i], Result[0], (highPath - i +1) * SizeOf(TPointD));
+      dec(highPath, i);
+      SetLength(Result, highPath +1);
+    end;
+    if amount > 0 then
+    begin
+      vec := GetUnitVector(result[0], result[1]);
+      result[0].X := result[0].X + vec.X * amount;
+      result[0].Y := result[0].Y + vec.Y * amount;
+    end;
+  end;
+  if pathEnd <> peStart then
+  begin
+    //shorten end
+    while (highPath > 1) do
+    begin
+      len := Distance(result[highPath], result[highPath -1]);
+      if (len >= amount2) then Break;
+      amount2 := amount2 - len;
+      dec(highPath);
+    end;
+    SetLength(Result, highPath +1);
+    if amount2 > 0 then
+    begin
+      vec := GetUnitVector(result[highPath], result[highPath -1]);
+      result[highPath].X := result[highPath].X + vec.X * amount2;
+      result[highPath].Y := result[highPath].Y + vec.Y * amount2;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function GetDashedPath(const path: TPathD;
+  closed: Boolean; const pattern: TArrayOfInteger;
+  patternOffset: PDouble): TPathsD;
+var
+  i, highI, paIdx: integer;
+  vecs, path2, dash: TPathD;
+  patCnt, patLen: integer;
+  dashCapacity, dashCnt, ptsCapacity, ptsCnt: integer;
+  segLen, residualPat, patOff: double;
+  filling: Boolean;
+  pt, pt2: TPointD;
+
+  procedure NewDash;
+  begin
+    if ptsCnt = 1 then ptsCnt := 0;
+    if ptsCnt = 0 then Exit;
+    if dashCnt = dashCapacity then
+    begin
+      inc(dashCapacity, BuffSize);
+      setLength(result, dashCapacity);
+    end;
+    result[dashCnt] := Copy(dash, 0, ptsCnt);
+    inc(dashCnt);
+    ptsCapacity := BuffSize;
+    setLength(dash, ptsCapacity);
+    ptsCnt := 0;
+  end;
+
+  procedure ExtendDash(const pt: TPointD);
+  begin
+    if ptsCnt = ptsCapacity then
+    begin
+      inc(ptsCapacity, BuffSize);
+      setLength(dash, ptsCapacity);
+    end;
+    dash[ptsCnt] := pt;
+    inc(ptsCnt);
+  end;
+
+begin
+  Result := nil;
+  paIdx := 0;
+  patCnt := length(pattern);
+  path2 := path;
+  highI := high(path2);
+  if (highI < 1) or (patCnt = 0) then Exit;
+  if closed and
+    ((path2[highI].X <> path2[0].X) or (path2[highI].Y <> path2[0].Y)) then
+  begin
+    inc(highI);
+    setLength(path2, highI +2);
+    path2[highI] := path2[0];
+  end;
+  vecs := GetVectors(path2);
+  if (vecs[0].X = 0) and (vecs[0].Y = 0) then Exit; //not a line
+  if not assigned(patternOffset) then
+    patOff := 0 else
+    patOff := patternOffset^;
+  patLen := 0;
+  for i := 0 to patCnt -1 do
+    inc(patLen, pattern[i]);
+  if patOff < 0 then
+  begin
+    patOff := patLen + patOff;
+    while patOff < 0 do
+      patOff := patOff + patLen;
+  end
+  else while patOff > patLen do
+    patOff := patOff - patLen;
+  //nb: each dash is made up of 2 or more pts
+  dashCnt := 0;
+  dashCapacity := 0;
+  ptsCnt := 0;
+  ptsCapacity := 0;
+  filling := true;
+  while patOff >= pattern[paIdx] do
+  begin
+    filling := not filling;
+    patOff := patOff - pattern[paIdx];
+    paIdx := (paIdx + 1) mod patCnt;
+  end;
+  residualPat := pattern[paIdx] - patOff;
+  pt := path2[0];
+  ExtendDash(pt);
+  i := 0;
+  while (i < highI) do
+  begin
+    segLen := Distance(pt, path2[i+1]);
+    if residualPat > segLen then
+    begin
+      if filling then ExtendDash(path2[i+1]);
+      residualPat := residualPat - segLen;
+      pt := path2[i+1];
+      inc(i);
+    end else
+    begin
+      pt2.X := pt.X + vecs[i].X * residualPat;
+      pt2.Y := pt.Y + vecs[i].Y * residualPat;
+      if filling then ExtendDash(pt2);
+      filling := not filling;
+      NewDash;
+      paIdx := (paIdx + 1) mod patCnt;
+      residualPat := pattern[paIdx];
+      pt := pt2;
+      ExtendDash(pt);
+    end;
+  end;
+  NewDash;
+  SetLength(Result, dashCnt);
+  if not assigned(patternOffset) then Exit;
+  patOff := 0;
+  for i := 0 to paIdx -1 do
+    patOff := patOff + pattern[i];
+  patternOffset^ := patOff + (pattern[paIdx] - residualPat);
+end;
+//------------------------------------------------------------------------------
+
+function GetDashedOutLine(const path: TPathD;
+  closed: Boolean; const pattern: TArrayOfInteger;
+  patternOffset: PDouble; lineWidth: double;
+  joinStyle: TJoinStyle; endStyle: TEndStyle): TPathsD;
+var
+  i: integer;
+  tmp: TPathsD;
+begin
+  Result := nil;
+  for i := 0 to High(pattern) do
+    if pattern[i] <= 0 then pattern[i] := 1;
+  tmp := GetDashedPath(path, closed, pattern, patternOffset);
+  for i := 0 to high(tmp) do
+    AppendPath(Result, GrowOpenLine(tmp[i],
+      lineWidth, joinStyle, endStyle, 2));
+end;
+//------------------------------------------------------------------------------
+
+function GetBoundsD(const paths: TPathsD): TRectD;
+var
+  i,j: integer;
+  l,t,r,b: double;
+  p: PPointD;
+begin
+  l := MaxInt; t := MaxInt;
+  r := -MaxInt; b := -MaxInt;
+  for i := 0 to high(paths) do
+  begin
+    p := PPointD(paths[i]);
+    if not assigned(p) then Continue;
+    for j := 0 to high(paths[i]) do
+    begin
+      if p.x < l then l := p.x;
+      if p.x > r then r := p.x;
+      if p.y < t then t := p.y;
+      if p.y > b then b := p.y;
+      inc(p);
+    end;
+  end;
+  if r < l then
+    result := NullRectD else
+    result := RectD(l, t, r, b);
+end;
+//------------------------------------------------------------------------------
+
+function GetBoundsD(const path: TPathD): TRectD;
+var
+  i,highI: integer;
+  l,t,r,b: double;
+  p: PPointD;
+begin
+  highI := High(path);
+  if highI < 0 then
+  begin
+    Result := NullRectD;
+    Exit;
+  end;
+  l := path[0].X; r := l;
+  t := path[0].Y; b := t;
+  p := PPointD(path);
+  for i := 1 to highI do
+  begin
+    inc(p);
+    if p.x < l then l := p.x;
+    if p.x > r then r := p.x;
+    if p.y < t then t := p.y;
+    if p.y > b then b := p.y;
+  end;
+  result := RectD(l, t, r, b);
+end;
+//------------------------------------------------------------------------------
+
+function GetBounds(const path: TPathD): TRect;
+var
+  recD: TRectD;
+begin
+  recD := GetBoundsD(path);
+  Result := Rect(recD);
+end;
+//------------------------------------------------------------------------------
+
+function GetBounds(const paths: TPathsD): TRect;
+var
+  recD: TRectD;
+begin
+  recD := GetBoundsD(paths);
+  Result := Rect(recD);
+end;
+//------------------------------------------------------------------------------
+
+function PrePendPoint(const pt: TPointD; const p: TPathD): TPathD;
+var
+  len: integer;
+begin
+  len := Length(p);
+  SetLength(Result, len +1);
+  Result[0] := pt;
+  if len > 0 then Move(p[0], Result[1], len * SizeOf(TPointD));
+end;
+//------------------------------------------------------------------------------
+
+function PrePendPoints(const pt1, pt2: TPointD; const p: TPathD): TPathD;
+var
+  len: integer;
+begin
+  len := Length(p);
+  SetLength(Result, len +2);
+  Result[0] := pt1;
+  Result[1] := pt2;
+  if len > 0 then Move(p[0], Result[2], len * SizeOf(TPointD));
+end;
+//------------------------------------------------------------------------------
+
+function GetPointInQuadBezier(const a,b,c: TPointD; t: double): TPointD;
+var
+  omt: double;
+begin
+  if t > 1 then t := 1
+  else if t < 0 then t := 0;
+  omt := 1 - t;
+  Result.X := a.X*omt*omt + b.X*2*omt*t + c.X*t*t;
+  Result.Y := a.Y*omt*omt + b.Y*2*omt*t + c.Y*t*t;
+end;
+//------------------------------------------------------------------------------
+
+function FlattenQBezier(const firstPt: TPointD; const pts: TPathD;
+  tolerance: double = 0.0): TPathD; overload;
+begin
+  if tolerance <= 0.0 then tolerance := BezierTolerance;
+  Result := FlattenQBezier(PrePendPoint(firstPt, pts), tolerance);
+end;
+//------------------------------------------------------------------------------
+
+function FlattenQBezier(const pts: TPathD; tolerance: double = 0.0): TPathD;
+var
+  i, highI: integer;
+  p: TPathD;
+begin
+  Result := nil;
+  highI := high(pts);
+  if highI < 0 then Exit;
+  if (highI < 2) or Odd(highI) then
+    raise Exception.Create(rsInvalidQBezier);
+  if tolerance <= 0.0 then tolerance := BezierTolerance;
+  setLength(Result, 1);
+  Result[0] := pts[0];
+  for i := 0 to (highI div 2) -1 do
+  begin
+    if PointsEqual(pts[i*2], pts[i*2+1]) and
+      PointsEqual(pts[i*2+1], pts[i*2+2]) then
+    begin
+      AppendPoint(Result, pts[i*2]);
+      AppendPoint(Result, pts[i*2 +2]);
+    end else
+    begin
+      p := FlattenQBezier(pts[i*2], pts[i*2+1], pts[i*2+2], tolerance);
+      AppendPath(Result, Copy(p, 1, Length(p) -1));
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function FlattenQBezier(const pt1, pt2, pt3: TPointD;
+  tolerance: double = 0.0): TPathD;
+var
+  resultCnt, resultLen: integer;
+
+  procedure AddPoint(const pt: TPointD);
+  begin
+    if resultCnt = resultLen then
+    begin
+      inc(resultLen, BuffSize);
+      setLength(result, resultLen);
+    end;
+    result[resultCnt] := pt;
+    inc(resultCnt);
+  end;
+
+  procedure DoCurve(const p1, p2, p3: TPointD);
+  var
+    p12, p23, p123: TPointD;
+  begin
+    if (abs(p1.x + p3.x - 2 * p2.x) +
+      abs(p1.y + p3.y - 2 * p2.y) < tolerance) then
+    begin
+      AddPoint(p3);
+    end else
+    begin
+      P12.X := (P1.X + P2.X) * 0.5;
+      P12.Y := (P1.Y + P2.Y) * 0.5;
+      P23.X := (P2.X + P3.X) * 0.5;
+      P23.Y := (P2.Y + P3.Y) * 0.5;
+      P123.X := (P12.X + P23.X) * 0.5;
+      P123.Y := (P12.Y + P23.Y) * 0.5;
+      DoCurve(p1, p12, p123);
+      DoCurve(p123, p23, p3);
+    end;
+  end;
+
+begin
+  resultLen := 0; resultCnt := 0;
+  if tolerance <= 0.0 then tolerance := BezierTolerance;
+  AddPoint(pt1);
+  if ((pt1.X = pt2.X) and (pt1.Y = pt2.Y)) or
+    ((pt2.X = pt3.X) and (pt2.Y = pt3.Y)) then
+  begin
+    AddPoint(pt3)
+  end else
+    DoCurve(pt1, pt2, pt3);
+  SetLength(result, resultCnt);
+end;
+//------------------------------------------------------------------------------
+
+function GetPointInCubicBezier(const a,b,c,d: TPointD; t: double): TPointD;
+var
+  omt: double;
+begin
+  if t > 1 then t := 1
+  else if t < 0 then t := 0;
+  omt := 1 - t;
+  Result.X := a.X*omt*omt*omt +b.X*3*omt*omt*t +c.X*3*omt*t*t +d.X*t*t*t;
+  Result.Y := a.Y*omt*omt*omt +b.Y*3*omt*omt*t +c.Y*3*omt*t*t +d.Y*t*t*t;
+end;
+//------------------------------------------------------------------------------
+
+function FlattenCBezier(const firstPt: TPointD; const pts: TPathD;
+  tolerance: double = 0.0): TPathD; overload;
+begin
+    Result := FlattenCBezier(PrePendPoint(firstPt, pts), tolerance);
+end;
+//------------------------------------------------------------------------------
+
+function FlattenCBezier(const pts: TPathD; tolerance: double = 0.0): TPathD;
+var
+  i, len: integer;
+  p: TPathD;
+begin
+  Result := nil;
+  len := Length(pts) -1;
+  if len < 0 then Exit;
+  if (len < 3) or (len mod 3 <> 0) then
+    raise Exception.Create(rsInvalidCBezier);
+  if tolerance <= 0.0 then tolerance := BezierTolerance;
+  setLength(Result, 1);
+  Result[0] := pts[0];
+  for i := 0 to (len div 3) -1 do
+  begin
+    if PointsEqual(pts[i*3], pts[i*3+1]) and
+      PointsEqual(pts[i*3+2], pts[i*3+3]) then
+    begin
+      AppendPoint(Result, pts[i*3]);
+      AppendPoint(Result, pts[i*3 +3]);
+    end else
+    begin
+      p := FlattenCBezier(pts[i*3], pts[i*3+1],
+        pts[i*3+2], pts[i*3+3], tolerance);
+      AppendPath(Result, Copy(p, 1, Length(p) -1));
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function FlattenCBezier(const pt1, pt2, pt3, pt4: TPointD;
+  tolerance: double = 0.0): TPathD;
+var
+  resultCnt, resultLen: integer;
+
+  procedure AddPoint(const pt: TPointD);
+  begin
+    if resultCnt = resultLen then
+    begin
+      inc(resultLen, BuffSize);
+      setLength(result, resultLen);
+    end;
+    result[resultCnt] := pt;
+    inc(resultCnt);
+  end;
+
+  procedure DoCurve(const p1, p2, p3, p4: TPointD);
+  var
+    p12, p23, p34, p123, p234, p1234: TPointD;
+  begin
+    if  ((abs(p1.x +p3.x - 2*p2.x)  < tolerance) and
+        (abs(p2.x +p4.x - 2*p3.x) < tolerance)) and
+        ((abs(p1.y +p3.y - 2*p2.y)  < tolerance) and
+        (abs(p2.y +p4.y - 2*p3.y) < tolerance)) then
+    begin
+      AddPoint(p4);
+    end else
+    begin
+      p12.X := (p1.X + p2.X) / 2;
+      p12.Y := (p1.Y + p2.Y) / 2;
+      p23.X := (p2.X + p3.X) / 2;
+      p23.Y := (p2.Y + p3.Y) / 2;
+      p34.X := (p3.X + p4.X) / 2;
+      p34.Y := (p3.Y + p4.Y) / 2;
+      p123.X := (p12.X + p23.X) / 2;
+      p123.Y := (p12.Y + p23.Y) / 2;
+      p234.X := (p23.X + p34.X) / 2;
+      p234.Y := (p23.Y + p34.Y) / 2;
+      p1234.X := (p123.X + p234.X) / 2;
+      p1234.Y := (p123.Y + p234.Y) / 2;
+      DoCurve(p1, p12, p123, p1234);
+      DoCurve(p1234, p234, p34, p4);
+    end;
+  end;
+
+begin
+  result := nil;
+  resultLen := 0; resultCnt := 0;
+  if tolerance <= 0.0 then tolerance := BezierTolerance;
+  AddPoint(pt1);
+  if ValueAlmostZero(pt1.X - pt2.X) and ValueAlmostZero(pt1.Y - pt2.Y) and
+    ValueAlmostZero(pt3.X - pt4.X) and ValueAlmostZero(pt3.Y - pt4.Y) then
+  begin
+    AddPoint(pt4)
+  end else
+    DoCurve(pt1, pt2, pt3, pt4);
+  SetLength(result,resultCnt);
+end;
+//------------------------------------------------------------------------------
+
+function ReflectPoint(const pt, pivot: TPointD): TPointD;
+begin
+  Result.X := pivot.X + (pivot.X - pt.X);
+  Result.Y := pivot.Y + (pivot.Y - pt.Y);
+end;
+//------------------------------------------------------------------------------
+
+function FlattenCSpline(const priorCtrlPt, startPt: TPointD;
+  const pts: TPathD; tolerance: double = 0.0): TPathD;
+var
+  p: TPathD;
+  len: integer;
+begin
+  len := Length(pts);
+  SetLength(p, len + 2);
+  p[0] := startPt;
+  p[1] := ReflectPoint(priorCtrlPt, startPt);
+  if len > 0 then
+    Move(pts[0], p[2], len * SizeOf(TPointD));
+  Result := FlattenCSpline(p, tolerance);
+end;
+//------------------------------------------------------------------------------
+
+function FlattenCSpline(const pts: TPathD; tolerance: double = 0.0): TPathD;
+var
+  resultCnt, resultLen: integer;
+
+  procedure AddPoint(const pt: TPointD);
+  begin
+    if resultCnt = resultLen then
+    begin
+      inc(resultLen, BuffSize);
+      setLength(result, resultLen);
+    end;
+    result[resultCnt] := pt;
+    inc(resultCnt);
+  end;
+
+  procedure DoCurve(const p1, p2, p3, p4: TPointD);
+  var
+    p12, p23, p34, p123, p234, p1234: TPointD;
+  begin
+    if (abs(p1.x + p3.x - 2*p2.x) + abs(p2.x + p4.x - 2*p3.x) +
+      abs(p1.y + p3.y - 2*p2.y) + abs(p2.y + p4.y - 2*p3.y)) < tolerance then
+    begin
+      if resultCnt = length(result) then
+        setLength(result, length(result) +BuffSize);
+      result[resultCnt] := p4;
+      inc(resultCnt);
+    end else
+    begin
+      p12.X := (p1.X + p2.X) / 2;
+      p12.Y := (p1.Y + p2.Y) / 2;
+      p23.X := (p2.X + p3.X) / 2;
+      p23.Y := (p2.Y + p3.Y) / 2;
+      p34.X := (p3.X + p4.X) / 2;
+      p34.Y := (p3.Y + p4.Y) / 2;
+      p123.X := (p12.X + p23.X) / 2;
+      p123.Y := (p12.Y + p23.Y) / 2;
+      p234.X := (p23.X + p34.X) / 2;
+      p234.Y := (p23.Y + p34.Y) / 2;
+      p1234.X := (p123.X + p234.X) / 2;
+      p1234.Y := (p123.Y + p234.Y) / 2;
+      DoCurve(p1, p12, p123, p1234);
+      DoCurve(p1234, p234, p34, p4);
+    end;
+  end;
+
+var
+  i, len: integer;
+  p: PPointD;
+  pt1,pt2,pt3,pt4: TPointD;
+begin
+  result := nil;
+  len := Length(pts); resultLen := 0; resultCnt := 0;
+  if (len < 4) then Exit;
+  if tolerance <= 0.0 then tolerance := BezierTolerance;
+  //ignore incomplete trailing control points
+  if Odd(len) then dec(len);
+  p := @pts[0];
+  AddPoint(p^);
+  pt1 := p^; inc(p);
+  pt2 := p^; inc(p);
+  for i := 0 to (len shr 1) - 2 do
+  begin
+    pt3 := p^; inc(p);
+    pt4 := p^; inc(p);
+    DoCurve(pt1, pt2, pt3, pt4);
+    pt1 := pt4;
+    pt2 := ReflectPoint(pt3, pt1);
+  end;
+  SetLength(result,resultCnt);
+end;
+//------------------------------------------------------------------------------
+
+function FlattenQSpline(const priorCtrlPt, startPt: TPointD;
+  const pts: TPathD; tolerance: double = 0.0): TPathD;
+var
+  p: TPathD;
+  len: integer;
+begin
+  len := Length(pts);
+  SetLength(p, len + 2);
+  p[0] := startPt;
+  p[1] := ReflectPoint(priorCtrlPt, startPt);
+  if len > 0 then
+    Move(pts[0], p[2], len * SizeOf(TPointD));
+  Result := FlattenQSpline(p, tolerance);
+end;
+//------------------------------------------------------------------------------
+
+function FlattenQSpline(const pts: TPathD; tolerance: double = 0.0): TPathD;
+var
+  resultCnt, resultLen: integer;
+
+  procedure AddPoint(const pt: TPointD);
+  begin
+    if resultCnt = resultLen then
+    begin
+      inc(resultLen, BuffSize);
+      setLength(result, resultLen);
+    end;
+    result[resultCnt] := pt;
+    inc(resultCnt);
+  end;
+
+  procedure DoCurve(const p1, p2, p3: TPointD);
+  var
+    p12, p23, p123: TPointD;
+  begin
+    if (abs(p1.x + p3.x - 2 * p2.x) +
+      abs(p1.y + p3.y - 2 * p2.y) < tolerance) then
+    begin
+      AddPoint(p3);
+    end else
+    begin
+      P12.X := (P1.X + P2.X) * 0.5;
+      P12.Y := (P1.Y + P2.Y) * 0.5;
+      P23.X := (P2.X + P3.X) * 0.5;
+      P23.Y := (P2.Y + P3.Y) * 0.5;
+      P123.X := (P12.X + P23.X) * 0.5;
+      P123.Y := (P12.Y + P23.Y) * 0.5;
+      DoCurve(p1, p12, p123);
+      DoCurve(p123, p23, p3);
+    end;
+  end;
+
+var
+  i, len: integer;
+  p: PPointD;
+  pt1, pt2, pt3: TPointD;
+begin
+  result := nil;
+  len := Length(pts);
+  if (len < 3) then Exit;
+  resultLen := 0;
+  resultCnt := 0;
+  if tolerance <= 0.0 then tolerance := BezierTolerance;
+  p := @pts[0];
+  AddPoint(p^);
+  pt1 := p^; inc(p);
+  pt2 := p^; inc(p);
+  for i := 0 to len - 3 do
+  begin
+    pt3 := p^; inc(p);
+    DoCurve(pt1, pt2, pt3);
+    pt1 := pt3;
+    pt2 := ReflectPoint(pt2, pt1);
+  end;
+  SetLength(result,resultCnt);
+end;
+//------------------------------------------------------------------------------
+
+function MakePath(const pts: array of double): TPathD;
+var
+  i, j, len: Integer;
+  x,y: double;
+begin
+  Result := nil;
+  len := length(pts) div 2;
+  if len = 0 then Exit;
+  setlength(Result, len);
+  Result[0].X := pts[0];
+  Result[0].Y := pts[1];
+  j := 0;
+  for i := 1 to len -1 do
+  begin
+    x := pts[i*2];
+    y := pts[i*2 +1];
+    inc(j);
+    Result[j].X := x;
+    Result[j].Y := y;
+  end;
+  setlength(Result, j+1);
+end;
+//------------------------------------------------------------------------------
+
+end.

+ 77 - 0
components/Image32/source/Img32.inc

@@ -0,0 +1,77 @@
+//NO_STORAGE is experimental 
+//Allows file system storage of layered objects etc 
+//Must be disabled to compile the experimental 'CtrlDemo' in Examples
+{$DEFINE NO_STORAGE}   
+
+//USING_VCL_LCL - using either Delphi or Lazarus Component Libraries
+//Adds a few extra library features (eg copying to and from TBitmap objects)
+//Enabled is recommended unless you're compiling console applications.
+{$DEFINE USING_VCL_LCL} 
+
+{$IFDEF FPC}
+  {$MODE DELPHI}
+  {$DEFINE ABSTRACT_CLASSES}
+  {$DEFINE RECORD_METHODS}
+  {$DEFINE PBYTE}
+  {$DEFINE UITYPES}
+  {$DEFINE NESTED_TYPES}
+  {$IFNDEF DEBUG}
+    {$DEFINE INLINE}
+  {$ENDIF}
+  {$DEFINE DELPHI_PNG}
+  {$IFDEF WINDOWS}
+    {$DEFINE MSWINDOWS}
+  {$ENDIF}
+{$ELSE}
+  {$IF COMPILERVERSION < 15}
+    Your version of Delphi is not supported (Image32 requires Delphi version 7 or above)
+  {$IFEND}  
+  {$IFDEF CPUX86}
+      {$DEFINE ASM_X86}                         //caution: do not define in FPC
+  {$ENDIF}  
+  {$IF COMPILERVERSION >= 17}                   //Delphi 2005
+    {$IFNDEF DEBUG}
+      {$DEFINE INLINE}                            //added inlining
+    {$ENDIF}
+    {$DEFINE NESTED_TYPES}                        //added nested types & nested constants
+    {$IF COMPILERVERSION >= 18}                 //Delphi 2006
+      {$DEFINE ABSTRACT_CLASSES}                  //added abstract classes
+      {$DEFINE REPORTMEMORYLEAKS}                 //added ReportMemoryLeaksOnShutdown
+      {$WARN SYMBOL_PLATFORM OFF}
+      {$DEFINE SETSIZE}                           //added TBitmap.SetSize
+      {$IF COMPILERVERSION >= 18.5}             //Delphi 2007
+        {$DEFINE RECORD_METHODS}                  //added records with methods
+        {$DEFINE DELPHI_PNG}                      //added PNG support
+        {$DEFINE DELPHI_GIF}                      //added GIF support
+        {$DEFINE MAINFORMONTASKBAR}               //added TApplication.MainFormOnTaskbar
+        {$if CompilerVersion >= 20}             //Delphi 2009
+          {$DEFINE PBYTE}                         //added PByte
+          {$DEFINE CHARINSET}                     //added CharInSet function
+          {$DEFINE EXIT_PARAM}                    //added Exit(value)
+          {$DEFINE ALPHAFORMAT}                   //added TBitmap.AlphaFormat property
+          {$IF COMPILERVERSION >= 21}           //Delphi 2010
+            {$DEFINE GESTURES}                    //added screen gesture support
+            {$IF COMPILERVERSION >= 23}         //DelphiXE2
+              {$IF declared(FireMonkeyVersion)}   //defined in FMX.Types
+                {$DEFINE FMX}
+              {$IFEND}        
+              {$DEFINE USES_NAMESPACES}
+              {$DEFINE FORMATSETTINGS}
+              {$DEFINE TROUNDINGMODE}   
+              {$DEFINE UITYPES}                   //added UITypes unit
+              {$DEFINE XPLAT_GENERICS}            //reasonable cross-platform & generics support
+              {$DEFINE STYLESERVICES}             //added StyleServices unit
+              {$IF COMPILERVERSION >= 24}       //DelphiXE3
+                {$LEGACYIFEND ON}
+                {$DEFINE ZEROBASEDSTR}
+                {$IF COMPILERVERSION >= 25}     //DelphiXE4
+                  {$LEGACYIFEND ON}               //avoids compiler warning
+                {$IFEND}
+              {$IFEND}
+            {$IFEND}
+          {$IFEND}
+        {$IFEND}
+      {$IFEND}
+    {$IFEND}
+  {$IFEND}
+{$ENDIF}

+ 3556 - 0
components/Image32/source/Img32.pas

@@ -0,0 +1,3556 @@
+unit Img32;
+
+(*******************************************************************************
+* Author    :  Angus Johnson                                                   *
+* Version   :  4.3                                                             *
+* Date      :  27 September 2022                                               *
+* Website   :  http://www.angusj.com                                           *
+* Copyright :  Angus Johnson 2019-2022                                         *
+*                                                                              *
+* Purpose   :  The core module of the Image32 library                          *
+*                                                                              *
+* License   :  Use, modification & distribution is subject to                  *
+*              Boost Software License Ver 1                                    *
+*              http://www.boost.org/LICENSE_1_0.txt                            *
+*******************************************************************************)
+
+interface
+
+{$I Img32.inc}
+
+uses
+  Types, SysUtils, Classes,
+  {$IFDEF MSWINDOWS} Windows,{$ENDIF}
+  {$IFDEF USING_VCL_LCL}
+    {$IFDEF USES_NAMESPACES} Vcl.Graphics, Vcl.Forms,
+    {$ELSE}Graphics, Forms,
+    {$ENDIF}
+  {$ENDIF}
+  {$IFDEF XPLAT_GENERICS}
+    Generics.Collections, Generics.Defaults, Character,
+  {$ENDIF}
+  {$IFDEF UITYPES} UITypes,{$ENDIF} Math;
+
+type
+  TRect = Types.TRect;
+  TColor32 = type Cardinal;
+
+  TPointD = record
+    X, Y: double;
+  end;
+
+const
+  clNone32     = TColor32($00000000);
+  clAqua32     = TColor32($FF00FFFF);
+  clBlack32    = TColor32($FF000000);
+  clBlue32     = TColor32($FF0000FF);
+  clFuchsia32  = TColor32($FFFF00FF);
+  clGray32     = TColor32($FF808080);
+  clGreen32    = TColor32($FF008000);
+  clGrey32     = TColor32($FF808080);
+  clLime32     = TColor32($FF00FF00);
+  clMaroon32   = TColor32($FF800000);
+  clNavy32     = TColor32($FF000080);
+  clOlive32    = TColor32($FF7F7F00);
+  clOrange32   = TColor32($FFFF7F00);
+  clPurple32   = TColor32($FF7F00FF);
+  clRed32      = TColor32($FFFF0000);
+  clSilver32   = TColor32($FFC0C0C0);
+  clTeal32     = TColor32($FF007F7F);
+  clWhite32    = TColor32($FFFFFFFF);
+  clYellow32   = TColor32($FFFFFF00);
+
+  //custom gray colors
+  clDarkGray32 = TColor32($FF505050);
+  clDarkGrey32 = TColor32($FF505050);
+  //clGray32   = TColor32($FF808080);
+  //clSilver32 = TColor32($FFC0C0C0);
+  clLiteGray32 = TColor32($FFD3D3D3);
+  clLiteGrey32 = TColor32($FFD3D3D3);
+  clPaleGray32 = TColor32($FFE0E0E0);
+  clPaleGrey32 = TColor32($FFE0E0E0);
+  clDarkBtn32  = TColor32($FFE8E8E8);
+  clBtnFace32  = TColor32($FFF0F0F0);
+  clLiteBtn32  = TColor32($FFF8F8F8);
+
+{$IFDEF ZEROBASEDSTR}
+  {$ZEROBASEDSTRINGS OFF}
+{$ENDIF}
+
+{$IFNDEF MSWINDOWS}
+  RT_BITMAP = PChar(2);
+{$ENDIF}
+
+type
+  TClipboardPriority = (cpLow, cpMedium, cpHigh);
+
+  PColor32 = ^TColor32;
+  TArrayOfColor32 = array of TColor32;
+  TArrayOfArrayOfColor32 = array of TArrayOfColor32;
+  TArrayOfInteger = array of Integer;
+  TArrayOfWord = array of WORD;
+  TArrayOfByte = array of Byte;
+
+  TImg32Notification = (inStateChange, inDestroy);
+
+  //A INotifyRecipient receives change notifications though a property
+  //interface from a single NotifySender (eg a Font property).
+  //A NotifySender can send change notificatons to multiple NotifyRecipients
+  //(eg where multiple object use the same font property). NotifyRecipients can
+  //still receive change notificatons from mulitple NotifySenders, but it
+  //must use a separate property for each NotifySender. (Also there's little
+  //benefit in using INotifySender and INotifyRecipient interfaces where there
+  //will only be one receiver - eg scroll - scrolling window.)
+
+  INotifyRecipient = interface
+    ['{95F50C62-D321-46A4-A42C-8E9D0E3149B5}']
+   procedure ReceiveNotification(Sender: TObject; notify: TImg32Notification);
+  end;
+  TRecipients = array of INotifyRecipient;
+
+  INotifySender = interface
+    ['{52072382-8B2F-481D-BE0A-E1C0A216B03E}']
+    procedure AddRecipient(recipient: INotifyRecipient);
+    procedure DeleteRecipient(recipient: INotifyRecipient);
+  end;
+
+  TInterfacedObj = class(TObject, IInterface)
+  public
+  {$IFDEF FPC}
+    function  _AddRef: Integer;
+      {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
+    function  _Release: Integer;
+      {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
+    function QueryInterface(
+      {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} iid : tguid;
+      out obj) : longint;
+      {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
+  {$ELSE}
+    function  _AddRef: Integer; stdcall;
+    function  _Release: Integer; stdcall;
+    function  QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
+  {$ENDIF}
+  end;
+
+  TImage32 = class;
+  TImageFormatClass = class of TImageFormat;
+
+  //TImageFormat: Abstract base class for loading and saving images in TImage32.<br>
+  //This class is overridden to provide support for separate
+  //file storage formats (eg BMP, PNG, GIF & JPG).<br>
+  //Derived classes register with TImage32 using TImage32.RegisterImageFormatClass.
+  TImageFormat = class
+    class function IsValidImageStream(stream: TStream): Boolean; virtual; abstract;
+    procedure SaveToStream(stream: TStream; img32: TImage32); virtual; abstract;
+    function SaveToFile(const filename: string; img32: TImage32): Boolean; virtual;
+    function LoadFromStream(stream: TStream; img32: TImage32): Boolean; virtual; abstract;
+    function LoadFromFile(const filename: string; img32: TImage32): Boolean; virtual;
+    class function CanCopyToClipboard: Boolean; virtual;
+    class function CopyToClipboard(img32: TImage32): Boolean; virtual; abstract;
+    class function CanPasteFromClipboard: Boolean; virtual; abstract;
+    class function PasteFromClipboard(img32: TImage32): Boolean; virtual; abstract;
+  end;
+
+  TBlendFunction = function(bgColor, fgColor: TColor32): TColor32;
+
+  TCompareFunction = function(master, current: TColor32; data: integer): Boolean;
+  TCompareFunctionEx = function(master, current: TColor32): Byte;
+
+  TTileFillStyle = (tfsRepeat, tfsMirrorHorz, tfsMirrorVert, tfsRotate180);
+
+  TResamplerFunction = function(img: TImage32; x256, y256: integer): TColor32;
+
+  TImage32 = class(TObject)
+  private
+    fWidth: integer;
+    fHeight: Integer;
+    fResampler: integer;
+    fIsPremultiplied: Boolean;
+    fColorCount: integer;
+    fPixels: TArrayOfColor32;
+    fOnChange: TNotifyEvent;
+    fOnResize: TNotifyEvent;
+    fUpdateCnt: integer;
+    fAntiAliased: Boolean;
+    fNotifyBlocked: Boolean;
+    function GetPixel(x,y: Integer): TColor32;
+    procedure SetPixel(x,y: Integer; color: TColor32);
+    function GetIsBlank: Boolean;
+    function GetIsEmpty: Boolean;
+    function GetPixelBase: PColor32;
+    function GetPixelRow(row: Integer): PColor32;
+    procedure NearestNeighborResize(newWidth, newHeight: Integer);
+    procedure ResamplerResize(newWidth, newHeight: Integer);
+    procedure RotateLeft90;
+    procedure RotateRight90;
+    procedure Rotate180;
+    function GetColorCount: Integer;
+    function GetHasTransparency: Boolean;
+    function GetBounds: TRect;
+    function GetMidPoint: TPointD;
+  protected
+    function  RectHasTransparency(rec: TRect): Boolean;
+    function  CopyPixels(rec: TRect): TArrayOfColor32;
+    //CopyInternal: Internal routine (has no scaling or bounds checking)
+    procedure CopyInternal(src: TImage32;
+      const srcRec, dstRec: TRect; blendFunc: TBlendFunction);
+    procedure  Changed; virtual;
+    procedure  Resized; virtual;
+    property   UpdateCount: integer read fUpdateCnt;
+  public
+    constructor Create(width: Integer = 0; height: Integer = 0); overload;
+    constructor Create(src: TImage32); overload;
+    constructor Create(src: TImage32; const srcRec: TRect); overload;
+    destructor Destroy; override;
+    procedure BeginUpdate;
+    procedure EndUpdate;
+    procedure BlockNotify;
+    procedure UnblockNotify;
+
+    procedure Assign(src: TImage32);
+    procedure AssignTo(dst: TImage32);
+    //SetSize: Erases any current image, and fills with the specified color.
+    procedure SetSize(newWidth, newHeight: Integer; color: TColor32 = 0);
+    //Resize: is similar to Scale() in that it won't eraze the existing
+    //image. Depending on the stretchImage parameter it will either stretch
+    //or crop the image. Don't confuse Resize() with SetSize(), as the latter
+    //does erase the image.
+    procedure Resize(newWidth, newHeight: Integer; stretchImage: Boolean = true);
+    //ScaleToFit: The new image will be scaled to fit within 'rec'
+    procedure ScaleToFit(width, height: integer);
+    //ScaleToFitCentered: The new image will be scaled and also centred
+    procedure ScaleToFitCentered(width, height: integer); overload;
+    procedure ScaleToFitCentered(const rect: TRect); overload;
+    procedure Scale(s: double); overload;
+    procedure Scale(sx, sy: double); overload;
+
+    function Copy(src: TImage32; srcRec, dstRec: TRect): Boolean;
+    //CopyBlend: Copies part or all of another image (src) on top of the
+    //existing image. If no blend function is provided, then the function
+    //will behave exactly as the Copy function above. However, when a blend
+    //function is specified, that function will determine how the images will
+    //be blended. If srcRec and dstRec have different widths or heights,
+    //then the image in srcRec will also be stretched to fit dstRec.
+    function CopyBlend(src: TImage32; srcRec, dstRec: TRect;
+      blendFunc: TBlendFunction = nil): Boolean;
+
+{$IFDEF MSWINDOWS}
+    //CopyFromDC: Copies an image from a Windows device context, erasing
+    //any current image in TImage32. (eg copying from TBitmap.canvas.handle)
+    procedure CopyFromDC(srcDc: HDC; const srcRect: TRect);
+    //CopyToDc: Copies the image into a Windows device context
+    procedure CopyToDc(dstDc: HDC; x: Integer = 0; y: Integer = 0;
+      transparent: Boolean = true); overload;
+    procedure CopyToDc(const srcRect: TRect; dstDc: HDC;
+      x: Integer = 0; y: Integer = 0; transparent: Boolean = true); overload;
+    procedure CopyToDc(const srcRect, dstRect: TRect; dstDc: HDC;
+      transparent: Boolean = true); overload;
+{$ENDIF}
+{$IFDEF USING_VCL_LCL}
+    procedure CopyFromBitmap(bmp: TBitmap);
+    procedure CopyToBitmap(bmp: TBitmap);
+{$ENDIF}
+    function CopyToClipBoard: Boolean;
+    class function CanPasteFromClipBoard: Boolean;
+    function PasteFromClipBoard: Boolean;
+    procedure Crop(const rec: TRect);
+    //SetBackgroundColor: Assumes the current image is semi-transparent.
+    procedure SetBackgroundColor(bgColor: TColor32);
+    procedure Clear(color: TColor32 = 0); overload;
+    procedure Clear(const rec: TRect; color: TColor32 = 0); overload;
+    procedure FillRect(rec: TRect; color: TColor32);
+
+    procedure ConvertToBoolMask(reference: TColor32;
+      tolerance: integer; colorFunc: TCompareFunction;
+      maskBg: TColor32 = clWhite32; maskFg: TColor32 = clBlack32);
+    procedure ConvertToAlphaMask(reference: TColor32;
+      colorFunc: TCompareFunctionEx);
+
+    procedure FlipVertical;
+    procedure FlipHorizontal;
+    procedure PreMultiply;
+    //SetAlpha: Sets 'alpha' to the alpha byte of every pixel in the image
+    procedure SetAlpha(alpha: Byte);
+    procedure ReduceOpacity(opacity: Byte); overload;
+    procedure ReduceOpacity(opacity: Byte; rec: TRect); overload;
+    //SetRGB: Sets the RGB channels leaving the alpha channel unchanged
+    procedure SetRGB(rgbColor: TColor32); overload;
+    procedure SetRGB(rgbColor: TColor32; rec: TRect); overload;
+    //Grayscale: Only changes color channels. The alpha channel is untouched.
+    procedure Grayscale;
+    procedure InvertColors;
+    procedure InvertAlphas;
+    procedure AdjustHue(percent: Integer);         //ie +/- 100%
+    procedure AdjustLuminance(percent: Integer);   //ie +/- 100%
+    procedure AdjustSaturation(percent: Integer);  //ie +/- 100%
+
+    //CropTransparentPixels: Trims transparent edges until each edge contains
+    //at least one opaque or semi-opaque pixel.
+    function CropTransparentPixels: TRect;
+    procedure Rotate(angleRads: double);
+    //RotateRect: Rotates part of an image, but also clips those parts of the
+    //rotated image that fall outside rec. The eraseColor parameter indicates
+    //the color to fill those uncovered pixels in rec following rotation.
+    procedure RotateRect(const rec: TRect;
+      angleRads: double; eraseColor: TColor32 = 0);
+    procedure Skew(dx,dy: double);
+
+    //ScaleAlpha: Scales the alpha byte of every pixel by the specified amount.
+    procedure ScaleAlpha(scale: double);
+    class procedure RegisterImageFormatClass(ext: string;
+      bm32ExClass: TImageFormatClass; clipPriority: TClipboardPriority);
+    class function GetImageFormatClass(const ext: string): TImageFormatClass; overload;
+    class function GetImageFormatClass(stream: TStream): TImageFormatClass; overload;
+    class function IsRegisteredFormat(const ext: string): Boolean;
+    function SaveToFile(filename: string): Boolean;
+    function SaveToStream(stream: TStream; const FmtExt: string): Boolean;
+    function LoadFromFile(const filename: string): Boolean;
+    function LoadFromStream(stream: TStream): Boolean;
+    function LoadFromResource(const resName: string; resType: PChar): Boolean;
+
+    //properties ...
+
+    property AntiAliased: Boolean read fAntiAliased write fAntiAliased;
+    property Width: Integer read fWidth;
+    property Height: Integer read fHeight;
+    property Bounds: TRect read GetBounds;
+    property IsBlank: Boolean read GetIsBlank;
+    property IsEmpty: Boolean read GetIsEmpty;
+    property IsPreMultiplied: Boolean read fIsPremultiplied;
+    property MidPoint: TPointD read GetMidPoint;
+    property Pixel[x,y: Integer]: TColor32 read GetPixel write SetPixel;
+    property Pixels: TArrayOfColor32 read fPixels;
+    property PixelBase: PColor32 read GetPixelBase;
+    property PixelRow[row: Integer]: PColor32 read GetPixelRow;
+    property ColorCount: Integer read GetColorCount;
+    //HasTransparency: Returns true if any pixel's alpha byte < 255.
+    property HasTransparency: Boolean read GetHasTransparency;
+    //Resampler: is used in scaling and rotation transforms
+    property Resampler: integer read fResampler write fResampler;
+    property OnChange: TNotifyEvent read fOnChange write fOnChange;
+    property OnResize: TNotifyEvent read fOnResize write fOnResize;
+  end;
+
+  TImageList32 = class
+  private
+{$IFDEF XPLAT_GENERICS}
+    fList: TList<TImage32>;
+{$ELSE}
+    fList: TList;
+{$ENDIF}
+    fIsImageOwner: Boolean;
+    function GetImage(index: integer): TImage32;
+    procedure SetImage(index: integer; img: TIMage32);
+    function GetLast: TImage32;
+  public
+    constructor Create;
+    destructor Destroy; override;
+    procedure Clear;
+    function Count: integer;
+    procedure Add(image: TImage32); overload;
+    function Add(width, height: integer): TImage32; overload;
+    procedure Insert(index: integer; image: TImage32);
+    procedure Move(currentIndex, newIndex: integer);
+    procedure Delete(index: integer);
+    property Image[index: integer]: TImage32 read GetImage write SetImage; default;
+    property IsImageOwner: Boolean read fIsImageOwner write fIsImageOwner;
+    property Last: TImage32 read GetLast;
+  end;
+
+  PARGB = ^TARGB;
+  TARGB = packed record
+    case boolean of
+      false: (B: Byte; G: Byte; R: Byte; A: Byte);
+      true : (Color: TColor32);
+  end;
+  TArrayOfARGB = array of TARGB;
+  PArgbArray = ^TArrayOfARGB;
+
+  THsl = packed record
+    hue  : byte;
+    sat  : byte;
+    lum  : byte;
+    alpha: byte;
+  end;
+  PHsl = ^THsl;
+  TArrayofHSL = array of THsl;
+
+  TTriState = (tsUnknown = 0, tsYes = 1, tsChecked = 1, tsNo = 2, tsUnchecked = 2);
+
+  PPointD = ^TPointD;
+  TPathD = array of TPointD;       //nb: watch for ambiguity with Clipper.pas
+  TPathsD = array of TPathD;       //nb: watch for ambiguity with Clipper.pas
+  TArrayOfPathsD = array of TPathsD;
+
+  TArrayOfDouble = array of double;
+  TArrayOfString = array of string;
+
+  TRectD = {$IFDEF RECORD_METHODS} record {$ELSE} object {$ENDIF}
+    {$IFNDEF RECORD_METHODS}
+    Left, Top, Right, Bottom: Double;
+    function TopLeft: TPointD;
+    function BottomRight: TPointD;
+    {$ENDIF}
+    function IsEmpty: Boolean;
+    function Width: double;
+    function Height: double;
+    //Normalize: Returns True if swapping top & bottom or left & right
+    function Normalize: Boolean;
+    function Contains(const Pt: TPoint): Boolean; overload;
+    function Contains(const Pt: TPointD): Boolean; overload;
+    function MidPoint: TPointD;
+    {$IFDEF RECORD_METHODS}
+    case Integer of
+      0: (Left, Top, Right, Bottom: Double);
+      1: (TopLeft, BottomRight: TPointD);
+    {$ENDIF}
+  end;
+
+  {$IFNDEF PBYTE}
+  PByte = type PChar;
+  {$ENDIF}
+
+  //BLEND FUNCTIONS ( see TImage32.CopyBlend() )
+
+  //BlendToOpaque: Blends a semi-transparent image onto an opaque background
+  function BlendToOpaque(bgColor, fgColor: TColor32): TColor32;
+  //BlendToAlpha: Blends two semi-transparent images (slower than BlendToOpaque)
+  function BlendToAlpha(bgColor, fgColor: TColor32): TColor32;
+  //BlendMask: Whereever the mask is, preserves the background
+  function BlendMask(bgColor, alphaMask: TColor32): TColor32;
+  function BlendAltMask(bgColor, alphaMask: TColor32): TColor32;
+  function BlendDifference(color1, color2: TColor32): TColor32;
+  function BlendSubtract(bgColor, fgColor: TColor32): TColor32;
+  function BlendLighten(bgColor, fgColor: TColor32): TColor32;
+  function BlendDarken(bgColor, fgColor: TColor32): TColor32;
+  function BlendInvertedMask(bgColor, alphaMask: TColor32): TColor32;
+  //BlendBlueChannel: typically useful for white color masks
+  function BlendBlueChannel(bgColor, blueMask: TColor32): TColor32;
+
+  //COMPARE COLOR FUNCTIONS (ConvertToBoolMask, FloodFill, Vectorize etc.)
+
+  function CompareRGB(master, current: TColor32; tolerance: Integer): Boolean;
+  function CompareHue(master, current: TColor32; tolerance: Integer): Boolean;
+  function CompareAlpha(master, current: TColor32; tolerance: Integer): Boolean;
+
+  //CompareEx COLOR FUNCTIONS (see ConvertToAlphaMask)
+  function CompareRgbEx(master, current: TColor32): Byte;
+  function CompareAlphaEx(master, current: TColor32): Byte;
+
+  //MISCELLANEOUS FUNCTIONS ...
+
+  function GetBoolMask(img: TImage32; reference: TColor32;
+    compareFunc: TCompareFunction; tolerance: Integer): TArrayOfByte;
+
+  function GetByteMask(img: TImage32; reference: TColor32;
+    compareFunc: TCompareFunctionEx): TArrayOfByte;
+
+  {$IFDEF MSWINDOWS}
+  //Color32: Converts a Graphics.TColor value into a TColor32 value.
+  function Color32(rgbColor: Integer): TColor32; overload;
+  {$ENDIF}
+  function Color32(a, r, g, b: Byte): TColor32; overload;
+
+  //RGBColor: Converts a TColor32 value into a COLORREF value
+  function RGBColor(color: TColor32): Cardinal;
+  function InvertColor(color: TColor32): TColor32;
+
+  //RgbToHsl: See https://en.wikipedia.org/wiki/HSL_and_HSV
+  function RgbToHsl(color: TColor32): THsl;
+  //HslToRgb: See https://en.wikipedia.org/wiki/HSL_and_HSV
+  function HslToRgb(hslColor: THsl): TColor32;
+  function AdjustHue(color: TColor32; percent: Integer): TColor32;
+  function ArrayOfColor32ToArrayHSL(const clr32Arr: TArrayOfColor32): TArrayofHSL;
+  function ArrayOfHSLToArrayColor32(const hslArr: TArrayofHSL): TArrayOfColor32;
+
+  function GetAlpha(color: TColor32): Byte;  {$IFDEF INLINE} inline; {$ENDIF}
+
+  function PointD(const X, Y: Double): TPointD; overload;
+  function PointD(const pt: TPoint): TPointD; overload;
+
+  function RectD(left, top, right, bottom: double): TRectD; overload;
+  function RectD(const rec: TRect): TRectD; overload;
+
+  function ClampByte(val: Integer): byte; overload; {$IFDEF INLINE} inline; {$ENDIF}
+  function ClampByte(val: double): byte; overload; {$IFDEF INLINE} inline; {$ENDIF}
+  function ClampRange(val, min, max: Integer): Integer; overload;
+  function ClampRange(val, min, max: double): double; overload;
+  function IncPColor32(pc: Pointer; cnt: Integer): PColor32;
+
+  procedure NormalizeAngle(var angle: double; tolerance: double = Pi/360);
+  function GrayScale(color: TColor32): TColor32;
+
+  //DPIAware: Useful for DPIAware sizing of images and their container controls.
+  //It scales values relative to the display's resolution (PixelsPerInch).
+  //See https://docs.microsoft.com/en-us/windows/desktop/hidpi/high-DPIAware-desktop-application-development-on-windows
+  function DPIAware(val: Integer): Integer; overload; {$IFDEF INLINE} inline; {$ENDIF}
+  function DPIAware(val: double): double; overload; {$IFDEF INLINE} inline; {$ENDIF}
+  function DPIAware(const pt: TPoint): TPoint; overload;
+  function DPIAware(const pt: TPointD): TPointD; overload;
+  function DPIAware(const rec: TRect): TRect; overload;
+  function DPIAware(const rec: TRectD): TRectD; overload;
+
+{$IFDEF MSWINDOWS}
+  {$IFDEF FPC}
+  function AlphaBlend(DC: HDC; p2, p3, p4, p5: Integer;
+    DC6: HDC; p7, p8, p9, p10: Integer; p11: Windows.TBlendFunction): BOOL;
+    stdcall; external 'msimg32.dll' name 'AlphaBlend';
+  {$ENDIF}
+{$ENDIF}
+
+  //CreateResourceStream: handles both numeric and string names and types
+  function CreateResourceStream(const resName: string;
+    resType: PChar): TResourceStream;
+
+  function GetResampler(id: integer): TResamplerFunction;
+  function RegisterResampler(func: TResamplerFunction; const name: string): integer;
+  procedure GetResamplerList(stringList: TStringList);
+
+const
+  TwoPi = Pi *2;
+  angle0   = 0;
+  angle1   = Pi/180;
+  angle15  = Pi /12;
+  angle30  = angle15 *2;
+  angle45  = angle15 *3;
+  angle60  = angle15 *4;
+  angle75  = angle15 *5;
+  angle90  = Pi /2;
+  angle105 = Pi - angle75;
+  angle120 = Pi - angle60;
+  angle135 = Pi - angle45;
+  angle150 = Pi - angle30;
+  angle165 = Pi - angle15;
+  angle180 = Pi;
+  angle195 = Pi + angle15;
+  angle210 = Pi + angle30;
+  angle225 = Pi + angle45;
+  angle240 = Pi + angle60;
+  angle255 = Pi + angle75;
+  angle270 = TwoPi - angle90;
+  angle285 = TwoPi - angle75;
+  angle300 = TwoPi - angle60;
+  angle315 = TwoPi - angle45;
+  angle330 = TwoPi - angle30;
+  angle345 = TwoPi - angle15;
+  angle360 = TwoPi;
+
+var
+  ClockwiseRotationIsAnglePositive: Boolean = true;
+
+  //Resampling function identifiers (initialized in Img32.Resamplers)
+  rNearestResampler : integer;
+  rBilinearResampler: integer;
+  rBicubicResampler : integer;
+
+  DefaultResampler: Integer = 0;
+
+  //Both MulTable and DivTable are used in blend functions
+  //MulTable[a,b] = a * b / 255
+  MulTable: array [Byte,Byte] of Byte;
+  //DivTable[a,b] = a * 255/b (for a &lt;= b)
+  DivTable: array [Byte,Byte] of Byte;
+
+  dpiAware1   : integer = 1;
+  DpiAwareOne : double  = 1.0;
+
+  //AND BECAUSE OLDER DELPHI COMPILERS (OLDER THAN D2006)
+  //DON'T SUPPORT RECORD METHODS
+  procedure RectWidthHeight(const rec: TRect; out width, height: Integer);
+  {$IFDEF INLINE} inline; {$ENDIF}
+  function RectWidth(const rec: TRect): Integer;
+  {$IFDEF INLINE} inline; {$ENDIF}
+  function RectHeight(const rec: TRect): Integer;
+  {$IFDEF INLINE} inline; {$ENDIF}
+
+  function IsEmptyRect(const rec: TRect): Boolean; overload;
+  {$IFDEF INLINE} inline; {$ENDIF}
+  function IsEmptyRect(const rec: TRectD): Boolean; overload;
+  {$IFDEF INLINE} inline; {$ENDIF}
+
+  function SwapRedBlue(color: TColor32): TColor32; overload;
+  procedure SwapRedBlue(color: PColor32; count: integer); overload;
+
+  function MulBytes(b1, b2: Byte) : Byte;
+
+implementation
+
+uses
+  Img32.Vector, Img32.Resamplers, Img32.Transform;
+
+resourcestring
+  rsImageTooLarge = 'Image32 error: the image is too large.';
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+const
+  div255 : Double = 1 / 255;
+type
+  TByteArray = array[0..MaxInt -1] of Byte;
+  PByteArray = ^TByteArray;
+
+  TImgFmtRec = record
+    Fmt: string;
+    SortOrder: TClipboardPriority;
+    Obj: TImageFormatClass;
+  end;
+  PImgFmtRec = ^TImgFmtRec;
+
+  TResamplerObj = class
+    id: integer;
+    name: string;
+    func: TResamplerFunction;
+  end;
+
+var
+{$IFDEF XPLAT_GENERICS}
+  ImageFormatClassList: TList<PImgFmtRec>; //list of supported file extensions
+  ResamplerList: TList<TResamplerObj>;     //list of resampler functions
+{$ELSE}
+  ImageFormatClassList: TList;
+  ResamplerList: TList;
+{$ENDIF}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+procedure CreateImageFormatList;
+begin
+  if Assigned(ImageFormatClassList) then Exit;
+
+{$IFDEF XPLAT_GENERICS}
+  ImageFormatClassList := TList<PImgFmtRec>.Create;
+{$ELSE}
+  ImageFormatClassList := TList.Create;
+{$ENDIF}
+end;
+//------------------------------------------------------------------------------
+
+function FMod(const ANumerator, ADenominator: Double): Double;
+begin
+  Result := ANumerator - Trunc(ANumerator / ADenominator) * ADenominator;
+end;
+//------------------------------------------------------------------------------
+
+procedure NormalizeAngle(var angle: double; tolerance: double = Pi/360);
+var
+  aa: double;
+begin
+  angle := FMod(angle, angle360);
+  if angle < -Angle180 then angle := angle + angle360
+  else if angle > angle180 then angle := angle - angle360;
+
+  aa := Abs(angle);
+  if aa < tolerance then angle := 0
+  else if aa > angle180 - tolerance then angle := angle180
+  else if (aa < angle90 - tolerance) or (aa > angle90 + tolerance) then Exit
+  else if angle < 0 then angle := -angle90
+  else angle := angle90;
+end;
+//------------------------------------------------------------------------------
+
+function SwapRedBlue(color: TColor32): TColor32;
+var
+  c: array[0..3] of byte absolute color;
+  r: array[0..3] of byte absolute Result;
+begin
+  result := color;
+  r[0] := c[2];
+  r[2] := c[0];
+end;
+//------------------------------------------------------------------------------
+
+procedure SwapRedBlue(color: PColor32; count: integer);
+var
+  i: integer;
+begin
+  for i := 1 to count do
+  begin
+    color^ := SwapRedBlue(color^);
+    inc(color);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function MulBytes(b1, b2: Byte) : Byte; {$IFDEF INLINE} inline; {$ENDIF}
+begin
+  Result := MulTable[b1, b2];
+end;
+//------------------------------------------------------------------------------
+
+function ImageFormatClassListSort(item1, item2: Pointer): integer;
+var
+  imgFmtRec1: PImgFmtRec absolute item1;
+  imgFmtRec2: PImgFmtRec absolute item2;
+begin
+  Result := Integer(imgFmtRec1.SortOrder) - Integer(imgFmtRec2.SortOrder);
+end;
+//------------------------------------------------------------------------------
+
+function ClampByte(val: Integer): byte;
+begin
+  if val < 0 then result := 0
+  else if val > 255 then result := 255
+  else result := val;
+end;
+//------------------------------------------------------------------------------
+
+function ClampByte(val: double): byte;
+begin
+  if val <= 0 then result := 0
+  else if val >= 255 then result := 255
+  else result := Round(val);
+end;
+//------------------------------------------------------------------------------
+
+//------------------------------------------------------------------------------
+// Blend functions - used by TImage32.CopyBlend()
+//------------------------------------------------------------------------------
+
+function BlendToOpaque(bgColor, fgColor: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute fgColor;
+  fw,bw: PByteArray;
+begin
+  if fg.A = 0 then Result := bgColor
+  else if fg.A = 255 then Result := fgColor
+  else
+  begin
+    //assuming bg.A = 255, use just fg.A for color weighting
+    res.A := 255;
+    fw := PByteArray(@MulTable[fg.A]);     //ie weight of foreground
+    bw := PByteArray(@MulTable[not fg.A]); //ie weight of foreground
+    res.R := fw[fg.R] + bw[bg.R];
+    res.G := fw[fg.G] + bw[bg.G];
+    res.B := fw[fg.B] + bw[bg.B];
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function BlendToAlpha(bgColor, fgColor: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute fgColor;
+  fgWeight: byte;
+  R, InvR: PByteArray;
+begin
+  //(see https://en.wikipedia.org/wiki/Alpha_compositing)
+  if (bg.A = 0) or (fg.A = 255) then Result := fgColor
+  else if fg.A = 0 then Result := bgColor
+  else
+  begin
+    //combine alphas ...
+    res.A := not MulTable[not fg.A, not bg.A];
+    fgWeight := DivTable[fg.A, res.A]; //fgWeight = amount foreground color
+                                       //contibutes to total (result) color
+
+    R     := PByteArray(@MulTable[fgWeight]);      //ie weight of foreground
+    InvR  := PByteArray(@MulTable[not fgWeight]);  //ie weight of foreground
+    res.R := R[fg.R] + InvR[bg.R];
+    res.G := R[fg.G] + InvR[bg.G];
+    res.B := R[fg.B] + InvR[bg.B];
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function BlendMask(bgColor, alphaMask: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute alphaMask;
+begin
+  Result := bgColor;
+  res.A := MulTable[bg.A, fg.A];
+  if res.A = 0 then Result := 0;
+end;
+//------------------------------------------------------------------------------
+
+function BlendAltMask(bgColor, alphaMask: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute alphaMask;
+begin
+  Result := bgColor;
+  res.A := MulTable[bg.A, 255-fg.A];
+  if res.A = 0 then Result := 0;
+end;
+//------------------------------------------------------------------------------
+
+function BlendDifference(color1, color2: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute color1;
+  fg: TARGB absolute color2;
+begin
+  if fg.A = 0 then Result := color1
+  else if bg.A = 0 then Result := color2
+  else
+  begin
+    res.A := (((fg.A xor 255) * (bg.A xor 255)) shr 8) xor 255;
+    res.R := Abs(fg.R - bg.R);
+    res.G := Abs(fg.G - bg.G);
+    res.B := Abs(fg.B - bg.B);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function BlendSubtract(bgColor, fgColor: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute fgColor;
+begin
+  if fg.A = 0 then Result := bgColor
+  else if bg.A = 0 then Result := fgColor
+  else
+  begin
+    res.A := (((fg.A xor 255) * (bg.A xor 255)) shr 8) xor 255;
+    res.R := ClampByte(fg.R - bg.R);
+    res.G := ClampByte(fg.G - bg.G);
+    res.B := ClampByte(fg.B - bg.B);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function BlendLighten(bgColor, fgColor: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute fgColor;
+begin
+  if fg.A = 0 then Result := bgColor
+  else if bg.A = 0 then Result := fgColor
+  else
+  begin
+    res.A := (((fg.A xor 255) * (bg.A xor 255)) shr 8) xor 255;
+    res.R := Max(fg.R, bg.R);
+    res.G := Max(fg.G, bg.G);
+    res.B := Max(fg.B, bg.B);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function BlendDarken(bgColor, fgColor: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute fgColor;
+begin
+  if fg.A = 0 then Result := bgColor
+  else if bg.A = 0 then Result := fgColor
+  else
+  begin
+    res.A := (((fg.A xor 255) * (bg.A xor 255)) shr 8) xor 255;
+    res.R := Min(fg.R, bg.R);
+    res.G := Min(fg.G, bg.G);
+    res.B := Min(fg.B, bg.B);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function BlendBlueChannel(bgColor, blueMask: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute blueMask;
+begin
+  Result := bgColor;
+  res.A := MulTable[bg.A, fg.B];
+end;
+//------------------------------------------------------------------------------
+
+function BlendInvertedMask(bgColor, alphaMask: TColor32): TColor32;
+var
+  res: TARGB absolute Result;
+  bg: TARGB absolute bgColor;
+  fg: TARGB absolute alphaMask;
+begin
+  Result := bgColor;
+  res.A := MulTable[bg.A, 255 - fg.A];
+  if res.A < 2 then Result := 0;
+end;
+
+//------------------------------------------------------------------------------
+// Compare functions (see ConvertToBoolMask, FloodFill & Vectorize)
+//------------------------------------------------------------------------------
+
+function CompareRGB(master, current: TColor32; tolerance: Integer): Boolean;
+var
+  mast: TARGB absolute master;
+  curr: TARGB absolute current;
+begin
+  if curr.A < $80 then
+    Result := false
+  else if (master and $FFFFFF) = (current and $FFFFFF) then
+    Result := true
+  else if tolerance = 0 then
+    Result := false
+  else result :=
+    (Abs(curr.R - mast.R) <= tolerance) and
+    (Abs(curr.G - mast.G) <= tolerance) and
+    (Abs(curr.B - mast.B) <= tolerance);
+end;
+//------------------------------------------------------------------------------
+
+function CompareAlpha(master, current: TColor32; tolerance: Integer): Boolean;
+var
+  mast: TARGB absolute master;
+  curr: TARGB absolute current;
+begin
+  if mast.A = curr.A then Result := true
+  else if tolerance = 0 then Result := false
+  else result := Abs(curr.A - mast.A) <= tolerance;
+end;
+//------------------------------------------------------------------------------
+
+function CompareHue(master, current: TColor32; tolerance: Integer): Boolean;
+var
+  curr, mast: THsl;
+  val: Integer;
+begin
+  if TARGB(current).A < $80 then
+  begin
+    Result := false;
+    Exit;
+  end;
+  curr := RgbToHsl(current);
+  mast := RgbToHsl(master);
+  if curr.hue > mast.hue then
+  begin
+    val := curr.hue - mast.hue;
+    if val > 127 then val := mast.hue - curr.hue + 255;
+  end else
+  begin
+    val := mast.hue - curr.hue;
+    if val > 127 then val := curr.hue - mast.hue + 255;
+  end;
+  result := val <= tolerance;
+end;
+
+//------------------------------------------------------------------------------
+// CompareEx functions (see ConvertToAlphaMask)
+//------------------------------------------------------------------------------
+
+function CompareRgbEx(master, current: TColor32): Byte;
+var
+  mast: TARGB absolute master;
+  curr: TARGB absolute current;
+  res: Cardinal;
+begin
+  res := Sqr(mast.R - curr.R) + Sqr(mast.G - curr.G) + Sqr(mast.B - curr.B);
+  if res >= 65025 then result := 255
+  else result := Round(Sqrt(res));
+end;
+//------------------------------------------------------------------------------
+
+function CompareAlphaEx(master, current: TColor32): Byte;
+var
+  mast: TARGB absolute master;
+  curr: TARGB absolute current;
+begin
+  Result := abs(mast.A - curr.A);
+end;
+
+//------------------------------------------------------------------------------
+// Miscellaneous functions ...
+//------------------------------------------------------------------------------
+
+function IsAlphaChar(c: Char): Boolean;
+begin
+  Result := ((c >= 'A') and (c <= 'Z')) or ((c >= 'a') and (c <= 'z'));
+end;
+//------------------------------------------------------------------------------
+
+procedure RectWidthHeight(const rec: TRect; out width, height: Integer);
+begin
+  width := rec.Right - rec.Left;
+  height := rec.Bottom - rec.Top;
+end;
+//------------------------------------------------------------------------------
+
+function RectWidth(const rec: TRect): Integer;
+begin
+  Result := rec.Right - rec.Left;
+end;
+//------------------------------------------------------------------------------
+
+function RectHeight(const rec: TRect): Integer;
+begin
+  Result := rec.Bottom - rec.Top;
+end;
+//------------------------------------------------------------------------------
+
+function IsEmptyRect(const rec: TRect): Boolean;
+begin
+  Result := (rec.Right <= rec.Left) or (rec.Bottom <= rec.Top);
+end;
+//------------------------------------------------------------------------------
+
+function IsEmptyRect(const rec: TRectD): Boolean;
+begin
+  Result := (rec.Right <= rec.Left) or (rec.Bottom <= rec.Top);
+end;
+//------------------------------------------------------------------------------
+
+function InvertColor(color: TColor32): TColor32;
+var
+  c: TARGB absolute color;
+  r: TARGB absolute Result;
+begin
+  r.A := c.A;
+  r.R := 255 - c.R;
+  r.G := 255 - c.G;
+  r.B := 255 - c.B;
+end;
+//------------------------------------------------------------------------------
+
+function GetAlpha(color: TColor32): Byte;
+begin
+  Result := Byte(color shr 24);
+end;
+//------------------------------------------------------------------------------
+
+function RGBColor(color: TColor32): Cardinal;
+var
+  c  : TARGB absolute color;
+  res: TARGB absolute Result;
+begin
+  res.R := c.B; res.G := c.G; res.B := c.R; res.A := 0;
+end;
+//------------------------------------------------------------------------------
+
+function Color32(a, r, g, b: Byte): TColor32;
+var
+  res: TARGB absolute Result;
+begin
+  res.A := a; res.R := r; res.G := g; res.B := b;
+end;
+//------------------------------------------------------------------------------
+
+{$IFDEF MSWINDOWS}
+function Color32(rgbColor: Integer): TColor32;
+var
+  res: TARGB absolute Result;
+begin
+  if rgbColor < 0 then
+    result := GetSysColor(rgbColor and $FFFFFF) else
+    result := rgbColor;
+  res.A := res.B; res.B := res.R; res.R := res.A; //byte swap
+  res.A := 255;
+end;
+//------------------------------------------------------------------------------
+
+function Get32bitBitmapInfoHeader(width, height: Integer): TBitmapInfoHeader;
+begin
+  FillChar(Result, sizeof(Result), #0);
+  Result.biSize := sizeof(TBitmapInfoHeader);
+  Result.biWidth := width;
+  Result.biHeight := height;
+  Result.biPlanes := 1;
+  Result.biBitCount := 32;
+  Result.biSizeImage := width * height * SizeOf(TColor32);
+  Result.biCompression := BI_RGB;
+end;
+//------------------------------------------------------------------------------
+{$ENDIF}
+
+function DPIAware(val: Integer): Integer;
+begin
+  result := Round(val * DpiAwareOne);
+end;
+//------------------------------------------------------------------------------
+
+function DPIAware(val: double): double;
+begin
+  result := val * DpiAwareOne;
+end;
+//------------------------------------------------------------------------------
+
+function DPIAware(const pt: TPoint): TPoint;
+begin
+  result.X := Round(pt.X * DpiAwareOne);
+  result.Y := Round(pt.Y * DpiAwareOne);
+end;
+//------------------------------------------------------------------------------
+
+function DPIAware(const pt: TPointD): TPointD;
+begin
+  result.X := pt.X * DpiAwareOne;
+  result.Y := pt.Y * DpiAwareOne;
+end;
+//------------------------------------------------------------------------------
+
+function DPIAware(const rec: TRect): TRect;
+begin
+  result.Left := Round(rec.Left * DpiAwareOne);
+  result.Top := Round(rec.Top * DpiAwareOne);
+  result.Right := Round(rec.Right * DpiAwareOne);
+  result.Bottom := Round(rec.Bottom * DpiAwareOne);
+end;
+//------------------------------------------------------------------------------
+
+function DPIAware(const rec: TRectD): TRectD;
+begin
+  result.Left := rec.Left * DpiAwareOne;
+  result.Top := rec.Top * DpiAwareOne;
+  result.Right := rec.Right * DpiAwareOne;
+  result.Bottom := rec.Bottom * DpiAwareOne;
+end;
+//------------------------------------------------------------------------------
+
+function GrayScale(color: TColor32): TColor32;
+var
+  c: TARGB absolute color;
+  r: TARGB absolute result;
+  g: Byte;
+begin
+  //https://www.w3.org/TR/AERT/#color-contrast
+  g := ClampByte(0.299 * c.R + 0.587 * c.G + 0.114 * c.B);
+  r.A := c.A;
+  r.R := g; r.G := g; r.B := g;
+end;
+//------------------------------------------------------------------------------
+
+function ClampRange(val, min, max: Integer): Integer;
+begin
+  if val < min then result := min
+  else if val > max then result := max
+  else result := val;
+end;
+//------------------------------------------------------------------------------
+
+function ClampRange(val, min, max: double): double;
+begin
+  if val < min then result := min
+  else if val > max then result := max
+  else result := val;
+end;
+//------------------------------------------------------------------------------
+
+procedure ScaleRect(var rec: TRect; x,y: double);
+begin
+  rec.Right := rec.Left + Round((rec.Right - rec.Left) * x);
+  rec.Bottom := rec.Top + Round((rec.Bottom - rec.Top) * y);
+end;
+//------------------------------------------------------------------------------
+
+function IncPColor32(pc: Pointer; cnt: Integer): PColor32;
+begin
+  result := PColor32(PByte(pc) + cnt * SizeOf(TColor32));
+end;
+//------------------------------------------------------------------------------
+
+function PointD(const X, Y: Double): TPointD;
+begin
+  Result.X := X;
+  Result.Y := Y;
+end;
+//------------------------------------------------------------------------------
+
+function PointD(const pt: TPoint): TPointD;
+begin
+  Result.X := pt.X;
+  Result.Y := pt.Y;
+end;
+//------------------------------------------------------------------------------
+
+function GetBoolMask(img: TImage32; reference: TColor32;
+  compareFunc: TCompareFunction; tolerance: Integer): TArrayOfByte;
+var
+  i: integer;
+  pa: PByte;
+  pc: PColor32;
+begin
+  result := nil;
+  if not assigned(img) or img.IsEmpty then Exit;
+  if not Assigned(compareFunc) then compareFunc := CompareRGB;
+  SetLength(Result, img.Width * img.Height);
+  pa := @Result[0];
+  pc := img.PixelBase;
+  for i := 0 to img.Width * img.Height -1 do
+  begin
+    if compareFunc(reference, pc^, tolerance) then
+  {$IFDEF PBYTE}
+      pa^ := 1 else
+      pa^ := 0;
+  {$ELSE}
+      pa^ := #1 else
+      pa^ := #0;
+  {$ENDIF}
+    inc(pc); inc(pa);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function GetAlphaEx(master, current: TColor32): Byte;
+{$IFDEF INLINE} inline; {$ENDIF}
+var
+  curr: TARGB absolute current;
+begin
+  result := curr.A; //nb: 'master' is ignored
+end;
+//------------------------------------------------------------------------------
+
+function GetByteMask(img: TImage32; reference: TColor32;
+  compareFunc: TCompareFunctionEx): TArrayOfByte;
+var
+  i: integer;
+  pa: PByte;
+  pc: PColor32;
+begin
+  result := nil;
+  if not assigned(img) or img.IsEmpty then Exit;
+  if not Assigned(compareFunc) then compareFunc := GetAlphaEx;
+  SetLength(Result, img.Width * img.Height);
+  pa := @Result[0];
+  pc := img.PixelBase;
+  for i := 0 to img.Width * img.Height -1 do
+  begin
+    {$IFDEF PBYTE}
+    pa^ := compareFunc(reference, pc^);
+    {$ELSE}
+    pa^ := Char(compareFunc(reference, pc^));
+    {$ENDIF}
+    inc(pc); inc(pa);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function RgbToHsl(color: TColor32): THsl;
+var
+  rgba: TARGB absolute color;
+  hsl: THsl absolute result;
+  r,g,b: byte;
+  maxRGB, minRGB, mAdd, mSub: Integer;
+begin
+  //https://en.wikipedia.org/wiki/HSL_and_HSV and
+  //http://en.wikipedia.org/wiki/HSL_color_space
+{$IF DEFINED(ANDROID)}
+  color := SwapRedBlue(color);
+{$IFEND}
+
+  r := rgba.R; g := rgba.G; b := rgba.B;
+  maxRGB := Max(r, Max(g, b));
+  minRGB := Min(r, Min(g, b));
+  mAdd := maxRGB + minRGB;
+  hsl.lum := mAdd shr 1;
+  hsl.alpha := rgba.A;
+
+  if maxRGB = minRGB then
+  begin
+    hsl.hue := 0; //hsl.hue is undefined when gray
+    hsl.sat := 0;
+    Exit;
+  end;
+
+  mSub := maxRGB - minRGB;
+  if mAdd <= 255 then
+    hsl.sat := DivTable[mSub, mAdd] else
+    hsl.sat := DivTable[mSub, 511 - mAdd];
+
+  mSub := mSub * 6;
+  if r = maxRGB then
+  begin
+    if g >= b then
+      hsl.hue := (g - b) * 255 div mSub else
+      hsl.hue := 255 - ((b - g) * 255 div mSub);
+  end
+  else if G = maxRGB then
+  begin
+    if b > r then
+      hsl.hue := 85 + (b - r) * 255 div mSub else
+      hsl.hue := 85 - (r - b)  * 255 div mSub;
+  end else
+  begin
+    if r > g then
+      hsl.hue := 170 + (r - g)  * 255 div mSub else
+      hsl.hue := 170 - (g - r)  * 255 div mSub;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function HslToRgb(hslColor: THsl): TColor32;
+var
+  rgba: TARGB absolute result;
+  hsl: THsl absolute hslColor;
+  c, x, m, a: Integer;
+begin
+  //formula from https://www.rapidtables.com/convert/color/hsl-to-rgb.html
+  c := (255 - abs(2 * hsl.lum - 255)) * hsl.sat div 255;
+  a := (hsl.hue mod 85) * 6 - 255;
+  x := c * (255 - abs(a)) div 255;
+  m := hsl.lum - c div 2;
+  rgba.A := hsl.alpha;
+  case (hsl.hue * 6) shr 8 of
+    0: begin rgba.R := c + m; rgba.G := x + m; rgba.B := 0 + m; end;
+    1: begin rgba.R := x + m; rgba.G := c + m; rgba.B := 0 + m; end;
+    2: begin rgba.R := 0 + m; rgba.G := c + m; rgba.B := x + m; end;
+    3: begin rgba.R := 0 + m; rgba.G := x + m; rgba.B := c + m; end;
+    4: begin rgba.R := x + m; rgba.G := 0 + m; rgba.B := c + m; end;
+    5: begin rgba.R := c + m; rgba.G := 0 + m; rgba.B := x + m; end;
+  end;
+{$IF DEFINED(ANDROID)}
+  Result := SwapRedBlue(Result);
+{$IFEND}
+end;
+//------------------------------------------------------------------------------
+
+function AdjustHue(color: TColor32; percent: Integer): TColor32;
+var
+  hsl: THsl;
+begin
+  percent := percent mod 100;
+  if percent < 0 then inc(percent, 100);
+  hsl := RgbToHsl(color);
+  hsl.hue := (hsl.hue + Round(percent*255/100)) mod 256;
+  result := HslToRgb(hsl);
+end;
+//------------------------------------------------------------------------------
+
+function ArrayOfColor32ToArrayHSL(const clr32Arr: TArrayOfColor32): TArrayofHSL;
+var
+  i, len: Integer;
+begin
+  len := length(clr32Arr);
+  setLength(result, len);
+  for i := 0 to len -1 do
+    result[i] := RgbToHsl(clr32Arr[i]);
+end;
+//------------------------------------------------------------------------------
+
+function ArrayOfHSLToArrayColor32(const hslArr: TArrayofHSL): TArrayOfColor32;
+var
+  i, len: Integer;
+begin
+  len := length(hslArr);
+  setLength(result, len);
+  for i := 0 to len -1 do
+    result[i] := HslToRgb(hslArr[i]);
+end;
+//------------------------------------------------------------------------------
+
+function NameToId(Name: PChar): Longint;
+begin
+  if Cardinal(PWord(Name)) < 30 then
+  begin
+    Result := Cardinal(PWord(Name))
+  end else
+  begin
+    if Name^ = '#' then inc(Name);
+    Result := StrToIntDef(Name, 0);
+    if Result > 65535 then Result := 0;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function CreateResourceStream(const resName: string;
+  resType: PChar): TResourceStream;
+var
+  nameId, typeId: Cardinal;
+begin
+  Result := nil;
+  typeId := NameToId(resType);
+  if (typeId > 0) then resType := PChar(typeId)
+  else if (resType = 'BMP') then resType := RT_BITMAP;
+
+  nameId := NameToId(PChar(resName));
+  if nameId > 0 then
+  begin
+    if FindResource(hInstance, PChar(nameId), resType) <> 0 then
+      Result := TResourceStream.CreateFromID(hInstance, nameId, resType);
+  end else
+  begin
+    if FindResource(hInstance, PChar(resName), resType) <> 0 then
+      Result := TResourceStream.Create(hInstance, PChar(resName), resType);
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TRectD methods (and helpers)
+//------------------------------------------------------------------------------
+
+function TRectD.IsEmpty: Boolean;
+begin
+  result := (right <= left) or (bottom <= top);
+end;
+//------------------------------------------------------------------------------
+
+function TRectD.Width: double;
+begin
+  result := Max(0, right - left);
+end;
+//------------------------------------------------------------------------------
+
+function TRectD.Height: double;
+begin
+  result := Max(0, bottom - top);
+end;
+//------------------------------------------------------------------------------
+
+function TRectD.MidPoint: TPointD;
+begin
+  Result.X := (Right + Left)/2;
+  Result.Y := (Bottom + Top)/2;
+end;
+//------------------------------------------------------------------------------
+
+{$IFNDEF RECORD_METHODS}
+function TRectD.TopLeft: TPointD;
+begin
+  Result.X := Left;
+  Result.Y := Top;
+end;
+//------------------------------------------------------------------------------
+
+function TRectD.BottomRight: TPointD;
+begin
+  Result.X := Right;
+  Result.Y := Bottom;
+end;
+//------------------------------------------------------------------------------
+{$ENDIF}
+
+function TRectD.Normalize: Boolean;
+var
+  d: double;
+begin
+  Result := false;
+  if Left > Right then
+  begin
+    d := Left;
+    Left := Right;
+    Right := d;
+    Result := True;
+  end;
+  if Top > Bottom then
+  begin
+    d := Top;
+    Top := Bottom;
+    Bottom := d;
+    Result := True;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TRectD.Contains(const Pt: TPoint): Boolean;
+begin
+  Result := (pt.X >= Left) and (pt.X < Right) and
+    (pt.Y >= Top) and (pt.Y < Bottom);
+end;
+//------------------------------------------------------------------------------
+
+function TRectD.Contains(const Pt: TPointD): Boolean;
+begin
+  Result := (pt.X >= Left) and (pt.X < Right) and
+    (pt.Y >= Top) and (pt.Y < Bottom);
+end;
+//------------------------------------------------------------------------------
+
+function RectD(left, top, right, bottom: double): TRectD;
+begin
+  result.Left := left;
+  result.Top := top;
+  result.Right := right;
+  result.Bottom := bottom;
+end;
+//------------------------------------------------------------------------------
+
+function RectD(const rec: TRect): TRectD;
+begin
+  with rec do
+  begin
+    result.Left := left;
+    result.Top := top;
+    result.Right := right;
+    result.Bottom := bottom;
+  end;
+end;
+
+//------------------------------------------------------------------------------
+// TImage32 methods
+//------------------------------------------------------------------------------
+
+constructor TImage32.Create(width: Integer; height: Integer);
+begin
+  fAntiAliased := true;
+  fResampler := DefaultResampler;
+  fwidth := Max(0, width);
+  fheight := Max(0, height);
+  SetLength(fPixels, fwidth * fheight);
+end;
+//------------------------------------------------------------------------------
+
+constructor TImage32.Create(src: TImage32);
+begin
+  Assign(src);
+end;
+//------------------------------------------------------------------------------
+
+constructor TImage32.Create(src: TImage32; const srcRec: TRect);
+var
+  rec: TRect;
+begin
+  fAntiAliased := src.AntiAliased;
+  fResampler := src.fResampler;
+  types.IntersectRect(rec, src.Bounds, srcRec);
+  RectWidthHeight(rec, fWidth, fHeight);
+  SetLength(fPixels, fWidth * fHeight);
+  if (fWidth = 0) or (fheight = 0) then Exit;
+  fPixels := src.CopyPixels(srcRec);
+end;
+//------------------------------------------------------------------------------
+
+destructor TImage32.Destroy;
+begin
+  fPixels := nil;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+class function TImage32.IsRegisteredFormat(const ext: string): Boolean;
+begin
+  result := Assigned(TImage32.GetImageFormatClass(ext));
+end;
+//------------------------------------------------------------------------------
+
+class procedure TImage32.RegisterImageFormatClass(ext: string;
+  bm32ExClass: TImageFormatClass; clipPriority: TClipboardPriority);
+var
+  i: Integer;
+  imgFmtRec: PImgFmtRec;
+  isNewFormat: Boolean;
+begin
+  if not Assigned(ImageFormatClassList) then CreateImageFormatList;
+
+  if (ext = '') or (ext = '.') then Exit;
+  if (ext[1] = '.') then Delete(ext, 1,1);
+  if not IsAlphaChar(ext[1]) then Exit;
+  isNewFormat := true;
+
+  // avoid duplicates but still allow overriding
+  for i := 0 to imageFormatClassList.count -1 do
+  begin
+    imgFmtRec := PImgFmtRec(imageFormatClassList[i]);
+    if SameText(imgFmtRec.Fmt, ext) then
+    begin
+      imgFmtRec.Obj := bm32ExClass; // replace prior class
+      if imgFmtRec.SortOrder = clipPriority then
+        Exit; // re-sorting isn't required
+      imgFmtRec.SortOrder := clipPriority;
+      isNewFormat := false;
+      Break;
+    end;
+  end;
+
+  if isNewFormat then
+  begin
+    new(imgFmtRec);
+    imgFmtRec.Fmt := ext;
+    imgFmtRec.SortOrder := clipPriority;
+    imgFmtRec.Obj := bm32ExClass;
+    ImageFormatClassList.Add(imgFmtRec);
+  end;
+
+  // Sort with lower priority before higher.
+  // Sorting here is arguably inefficient but, with so few
+  // entries, this inefficiency will be inconsequential.
+
+{$IFDEF XPLAT_GENERICS}
+  ImageFormatClassList.Sort(TComparer<PImgFmtRec>.Construct(
+      function(const imgFmtRec1, imgFmtRec2: PImgFmtRec): Integer
+      begin
+        Result := Integer(imgFmtRec1.SortOrder) - Integer(imgFmtRec2.SortOrder);
+      end));
+{$ELSE}
+  ImageFormatClassList.Sort(ImageFormatClassListSort);
+{$ENDIF}
+end;
+//------------------------------------------------------------------------------
+
+class function TImage32.GetImageFormatClass(const ext: string): TImageFormatClass;
+var
+  i: Integer;
+  pattern: string;
+  imgFmtRec: PImgFmtRec;
+begin
+  Result := nil;
+  pattern := ext;
+  if (pattern = '')  or (pattern = '.') then Exit;
+  if pattern[1] = '.' then Delete(pattern, 1,1);
+
+  //try for highest priority first
+  for i := imageFormatClassList.count -1 downto 0 do
+  begin
+    imgFmtRec := PImgFmtRec(imageFormatClassList[i]);
+    if not SameText(imgFmtRec.Fmt, pattern) then Continue;
+    Result := imgFmtRec.Obj;
+    break;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+class function TImage32.GetImageFormatClass(stream: TStream): TImageFormatClass;
+var
+  i: integer;
+begin
+  Result := nil;
+  for i := 0 to imageFormatClassList.count -1 do
+    with PImgFmtRec(imageFormatClassList[i])^ do
+      if Obj.IsValidImageStream(stream) then
+      begin
+        Result := Obj;
+        break;
+      end;
+end;
+//------------------------------------------------------------------------------
+
+
+procedure TImage32.Assign(src: TImage32);
+begin
+  if assigned(src) then
+    src.AssignTo(self);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.AssignTo(dst: TImage32);
+begin
+  if dst = self then Exit;
+  dst.BeginUpdate;
+  try
+    dst.fResampler := fResampler;
+    dst.fIsPremultiplied := fIsPremultiplied;
+    dst.fAntiAliased := fAntiAliased;
+    dst.fColorCount := 0;
+    try
+      dst.SetSize(Width, Height);
+      if (Width > 0) and (Height > 0) then
+        move(fPixels[0], dst.fPixels[0], Width * Height * SizeOf(TColor32));
+    except
+      dst.SetSize(0,0);
+    end;
+  finally
+    dst.EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Changed;
+begin
+  if fUpdateCnt <> 0 then Exit;
+  fColorCount := 0;
+  if Assigned(fOnChange) then fOnChange(Self);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Resized;
+begin
+  if fUpdateCnt <> 0 then Exit
+  else if Assigned(fOnResize) then fOnResize(Self)
+  else Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.BeginUpdate;
+begin
+  if fNotifyBlocked then Exit;
+  inc(fUpdateCnt);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.EndUpdate;
+begin
+  if fNotifyBlocked then Exit;
+  dec(fUpdateCnt);
+  if fUpdateCnt = 0 then Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.BlockNotify;
+begin
+  if fUpdateCnt <> 0 then Exit;
+  inc(fUpdateCnt);
+  fNotifyBlocked := true;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.UnblockNotify;
+begin
+  if not fNotifyBlocked then Exit;
+  dec(fUpdateCnt);
+  fNotifyBlocked := false;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.SetBackgroundColor(bgColor: TColor32);
+var
+  i: Integer;
+  pc: PColor32;
+begin
+  pc := Pixelbase;
+  for i := 0 to high(fPixels) do
+  begin
+    pc^ := BlendToOpaque(bgColor, pc^);
+     inc(pc);
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Clear(color: TColor32);
+var
+  i: Integer;
+  pc: PColor32;
+begin
+  fIsPremultiplied := false;
+  if IsEmpty then Exit;
+  if color = clNone32 then
+    FillChar(fPixels[0], Width * Height * SizeOf(TColor32), 0)
+  else
+  begin
+    pc := PixelBase;
+    for i := 0 to Width * Height -1 do
+    begin
+      pc^ := color;
+      inc(pc);
+    end;
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Clear(const rec: TRect; color: TColor32 = 0);
+begin
+  FillRect(rec, color);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.FillRect(rec: TRect; color: TColor32);
+var
+  i,j, rw: Integer;
+  c: PColor32;
+begin
+  Types.IntersectRect(rec, rec, bounds);
+  if IsEmptyRect(rec) then Exit;
+  rw := RectWidth(rec);
+  c := @Pixels[rec.Top * Width + rec.Left];
+  for i := rec.Top to rec.Bottom -1 do
+  begin
+    for j := 1 to rw do
+    begin
+      c^ := color;
+      inc(c);
+    end;
+    inc(c, Width - rw);
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.RectHasTransparency(rec: TRect): Boolean;
+var
+  i,j, rw: Integer;
+  c: PARGB;
+begin
+  Result := True;
+  Types.IntersectRect(rec, rec, bounds);
+  if IsEmptyRect(rec) then Exit;
+  rw := RectWidth(rec);
+  c := @Pixels[rec.Top * Width + rec.Left];
+  for i := rec.Top to rec.Bottom -1 do
+  begin
+    for j := 1 to rw do
+    begin
+      if c.A < 254 then Exit;
+      inc(c);
+    end;
+    inc(c, Width - rw);
+  end;
+  Result := False;
+end;
+//------------------------------------------------------------------------------
+
+procedure CheckBlendFill(pc: PColor32; color: TColor32);
+{$IFDEF INLINE} inline; {$ENDIF}
+begin
+  if not assigned(pc) then Exit;
+  pc^ := BlendToAlpha(pc^, color);
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.CopyPixels(rec: TRect): TArrayOfColor32;
+var
+  i, clipW, w,h: Integer;
+  pSrc, pDst, pDst2: PColor32;
+  recClipped: TRect;
+begin
+  RectWidthHeight(rec, w,h);
+  setLength(result, w * h);
+
+  if w * h = 0 then Exit;
+  Types.IntersectRect(recClipped, rec, Bounds);
+  //if recClipped is wholely outside the bounds of the image ...
+  if IsEmptyRect(recClipped) then
+  begin
+    //rec is considered valid even when completely outside the image bounds,
+    //and so when that happens we simply return a fully transparent image ...
+    FillChar(Result[0], w * h * SizeOf(TColor32), 0);
+    Exit;
+  end;
+
+  //if recClipped is wholely within the bounds of the image ...
+  if RectsEqual(recClipped, rec) then
+  begin
+    pDst := @Result[0];
+    pSrc := @fPixels[recClipped.Top * Width + rec.Left];
+    for i := recClipped.Top to recClipped.Bottom -1 do
+    begin
+      Move(pSrc^, pDst^, w * SizeOf(TColor32));
+      inc(pSrc, Width); inc(pDst, w);
+    end;
+    Exit;
+  end;
+
+  //a part of 'rec' must be outside the bounds of the image ...
+
+  pDst := @Result[0];
+  for i := rec.Top to -1 do
+  begin
+    FillChar(pDst^, w * SizeOf(TColor32), 0);
+    inc(pDst, w);
+  end;
+  pSrc := @fPixels[recClipped.Top * Width + Max(0,rec.Left)];
+  if (rec.Left < 0) or (rec.Right > Width) then
+  begin
+    clipW := RectWidth(recClipped);
+    pDst2 := IncPColor32(pDst, -Min(0, rec.Left));
+    for i := recClipped.Top to recClipped.Bottom -1 do
+    begin
+      //when rec.left < 0 or rec.right > width it's simplest to
+      //start with a prefilled row of transparent pixels
+      FillChar(pDst^, w * SizeOf(TColor32), 0);
+      Move(pSrc^, pDst2^, clipW * SizeOf(TColor32));
+      inc(pDst, w); inc(pDst2, w); inc(pSrc, Width);
+    end;
+  end else
+  begin
+    //things are simpler when there's no part of 'rec' is
+    //outside the image, at least not on the left or right sides ...
+    for i := recClipped.Top to recClipped.Bottom -1 do
+    begin
+      Move(pSrc^, pDst^, w * SizeOf(TColor32));
+      inc(pSrc, Width); inc(pDst, w);
+    end;
+  end;
+  for i := Height to rec.Bottom -1 do
+  begin
+    FillChar(pDst^, w * SizeOf(TColor32), 0);
+    inc(pDst, w);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Crop(const rec: TRect);
+var
+  newPixels: TArrayOfColor32;
+  w,h: integer;
+begin
+  RectWidthHeight(rec, w, h);
+  if (w = Width) and (h = Height) then Exit;
+  newPixels := CopyPixels(rec);
+  BlockNotify;
+  try
+    SetSize(w, h);
+    if not IsEmptyRect(rec) then
+      fPixels := newPixels;
+  finally
+    UnblockNotify;
+  end;
+  Resized;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.GetBounds: TRect;
+begin
+  result := Types.Rect(0, 0, Width, Height);
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.GetMidPoint: TPointD;
+begin
+  Result := PointD(fWidth * 0.5, fHeight * 0.5);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.SetSize(newWidth, newHeight: Integer; color: TColor32);
+begin
+  //very large images are usually due to a bug
+  if (newWidth > 20000) or (newHeight > 20000) then
+    raise Exception.Create(rsImageTooLarge);
+  fwidth := Max(0, newWidth);
+  fheight := Max(0, newHeight);
+  fPixels := nil; //forces a blank image
+  SetLength(fPixels, fwidth * fheight);
+  fIsPremultiplied := false;
+  if color > 0 then
+  begin
+    BlockNotify;
+    Clear(color);
+    UnblockNotify;
+  end;
+  Resized;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Resize(newWidth, newHeight: Integer; stretchImage: Boolean);
+var
+  tmp: TImage32;
+  rec: TRect;
+begin
+
+  if (newWidth <= 0) or (newHeight <= 0) then
+  begin
+    SetSize(0, 0);
+    Exit;
+  end
+  else if (newWidth = fwidth) and (newHeight = fheight) then
+  begin
+    Exit
+  end
+  else if IsEmpty then
+  begin
+    SetSize(newWidth, newHeight);
+    Exit;
+  end;
+
+  BlockNotify;
+  try
+    if stretchImage then
+    begin
+      if fResampler = 0 then
+        NearestNeighborResize(newWidth, newHeight)
+      else
+        ResamplerResize(newWidth, newHeight);
+    end else
+    begin
+      tmp := TImage32.create(self);
+      try
+        rec := Bounds;
+        SetSize(newWidth, newHeight, clNone32);
+        Copy(tmp, rec, rec); //this will clip as required.
+      finally
+        tmp.Free;
+      end;
+    end;
+  finally
+    UnblockNotify;
+  end;
+  Resized;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.NearestNeighborResize(newWidth, newHeight: Integer);
+var
+  x, y, srcY: Integer;
+  scaledXi, scaledYi: TArrayOfInteger;
+  tmp: TArrayOfColor32;
+  pc: PColor32;
+begin
+  //this NearestNeighbor code is slightly more efficient than
+  //the more general purpose one in Img32.Resamplers
+
+  if (newWidth = fWidth) and (newHeight = fHeight) then Exit;
+  SetLength(tmp, newWidth * newHeight * SizeOf(TColor32));
+
+  //get scaled X & Y values once only (storing them in lookup arrays) ...
+  SetLength(scaledXi, newWidth);
+  for x := 0 to newWidth -1 do
+    scaledXi[x] := Floor(x * fWidth / newWidth);
+  SetLength(scaledYi, newHeight);
+  for y := 0 to newHeight -1 do
+    scaledYi[y] := Floor(y * fHeight / newHeight);
+
+  pc := @tmp[0];
+  for y := 0 to newHeight - 1 do
+  begin
+    srcY := scaledYi[y];
+    if (srcY < 0) or (srcY >= fHeight) then Continue;
+    for x := 0 to newWidth - 1 do
+    begin
+      pc^ := fPixels[scaledXi[x] + srcY * fWidth];
+      inc(pc);
+    end;
+  end;
+
+  fPixels := tmp;
+  fwidth := newWidth;
+  fheight := newHeight;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.ResamplerResize(newWidth, newHeight: Integer);
+var
+  mat: TMatrixD;
+begin
+  mat := IdentityMatrix;
+  MatrixScale(mat, newWidth/fWidth, newHeight/fHeight);
+  AffineTransformImage(self, mat);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Scale(s: double);
+begin
+  Scale(s, s);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Scale(sx, sy: double);
+begin
+  //sx := Min(sx, 100); sy := Min(sy, 100);
+  if (sx > 0) and (sy > 0) then
+    ReSize(Round(width * sx), Round(height * sy));
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.ScaleToFit(width, height: integer);
+var
+  sx, sy: double;
+begin
+  if IsEmpty or (width <= 0) or (height <= 0) then Exit;
+  sx := width / self.Width;
+  sy := height / self.Height;
+  if sx <= sy then
+    Scale(sx) else
+    Scale(sy);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.ScaleToFitCentered(const rect: TRect);
+begin
+  ScaleToFitCentered(RectWidth(rect), RectHeight(rect));
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.ScaleToFitCentered(width, height: integer);
+var
+  sx, sy: double;
+  tmp: TImage32;
+  rec2: TRect;
+begin
+  if IsEmpty or (width <= 0) or (height <= 0) or
+    ((width = self.Width) and (height = self.Height)) then Exit;
+
+  sx := width / self.Width;
+  sy := height / self.Height;
+  BlockNotify;
+  try
+    if sx <= sy then
+    begin
+      Scale(sx);
+      if height = self.Height then Exit;
+      rec2 := Bounds;
+      Types.OffsetRect(rec2, 0, (height - self.Height) div 2);
+      tmp := TImage32.Create(self);
+      try
+        SetSize(width, height);
+        CopyInternal(tmp, tmp.Bounds, rec2, nil);
+      finally
+        tmp.Free;
+      end;
+    end else
+    begin
+      Scale(sy);
+      if width = self.Width then Exit;
+      rec2 := Bounds;
+      Types.OffsetRect(rec2, (width - self.Width) div 2, 0);
+      tmp := TImage32.Create(self);
+      try
+        SetSize(width, height);
+        CopyInternal(tmp, tmp.Bounds, rec2, nil);
+      finally
+        tmp.Free;
+      end;
+    end;
+  finally
+    UnblockNotify;
+  end;
+  Resized;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.RotateLeft90;
+var
+  x,y, xx: Integer;
+  src, dst: PColor32;
+  tmp: TImage32;
+begin
+  if IsEmpty then Exit;
+
+  BeginUpdate;
+  tmp := TImage32.create(Self);
+  try
+    SetSize(Height, Width);
+    xx := (width - 1) * Height;
+    dst := PixelBase;
+    for y := 0 to Height -1 do
+    begin
+      src := @tmp.Pixels[xx + y];
+      for x := 0 to Width -1 do
+      begin
+        dst^ := src^;
+        inc(dst); dec(src, Height);
+      end;
+    end;
+  finally
+    tmp.Free;
+    EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.RotateRight90;
+var
+  x,y: Integer;
+  src, dst: PColor32;
+  tmp: TImage32;
+begin
+  if IsEmpty then Exit;
+
+  BeginUpdate;
+  tmp := TImage32.create(Self);
+  try
+    SetSize(Height, Width);
+    dst := PixelBase;
+    for y := 0 to Height -1 do
+    begin
+      src := @tmp.Pixels[Height -1 - y];
+      for x := 0 to Width -1 do
+      begin
+        dst^ := src^;
+        inc(dst); inc(src, Height);
+      end;
+    end;
+  finally
+    tmp.Free;
+    EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Rotate180;
+var
+  x,y: Integer;
+  src, dst: PColor32;
+  tmp: TImage32;
+begin
+  if IsEmpty then Exit;
+  tmp := TImage32.create(Self);
+  try
+    dst := PixelBase;
+    src := @tmp.Pixels[Width * Height -1];
+    for y := 0 to Height -1 do
+    begin
+      for x := 0 to Width -1 do
+      begin
+        dst^ := src^;
+        inc(dst); dec(src);
+      end;
+    end;
+  finally
+    tmp.Free;
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.GetColorCount: Integer;
+var
+  allColors: PByteArray;
+  i: Integer;
+  c: PColor32;
+const
+  cube256 = 256 * 256 * 256;
+begin
+  result := 0;
+  if IsEmpty then Exit;
+  if fColorCount > 0 then
+  begin
+    result := fColorCount;
+    Exit;
+  end;
+  //because 'allColors' uses quite a chunk of memory, it's
+  //allocated on the heap rather than the stack
+  allColors := AllocMem(cube256); //nb: zero initialized
+  try
+    c := PixelBase;
+    for i := 0 to Width * Height -1 do
+    begin
+      //ignore colors with signifcant transparency
+      if GetAlpha(c^)  > $80 then
+        allColors[c^ and $FFFFFF] := 1;
+      inc(c);
+    end;
+    for i := 0 to cube256 -1 do
+      if allColors[i] = 1 then inc(Result);
+  finally
+    FreeMem(allColors);
+  end;
+  fColorCount := Result; //avoids repeating the above unnecessarily
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.GetHasTransparency: Boolean;
+var
+  i: Integer;
+  pc: PARGB;
+begin
+  result := true;
+  If IsEmpty then Exit;
+  pc := PARGB(PixelBase);
+  for i := 0 to Width * Height -1 do
+  begin
+    if pc.A < 255 then Exit;
+    inc(pc);
+  end;
+  result := false;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.SaveToFile(filename: string): Boolean;
+var
+  fileFormatClass: TImageFormatClass;
+begin
+  result := false;
+  if IsEmpty or (length(filename) < 5) then Exit;
+  //use the process's current working directory if no path supplied ...
+  if ExtractFilePath(filename) = '' then
+    filename := GetCurrentDir + '\'+ filename;
+  fileFormatClass := GetImageFormatClass(ExtractFileExt(filename));
+  if assigned(fileFormatClass) then
+    with fileFormatClass.Create do
+    try
+      result := SaveToFile(filename, self);
+    finally
+      free;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.SaveToStream(stream: TStream; const FmtExt: string): Boolean;
+var
+  fileFormatClass: TImageFormatClass;
+begin
+  result := false;
+  fileFormatClass := GetImageFormatClass(FmtExt);
+  if assigned(fileFormatClass) then
+    with fileFormatClass.Create do
+    try
+      SaveToStream(stream, self);
+      result := true;
+    finally
+      free;
+    end;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.LoadFromFile(const filename: string): Boolean;
+var
+  stream: TFileStream;
+begin
+  Result := false;
+  if not FileExists(filename) then Exit;
+
+  stream := TFileStream.Create(filename, fmOpenRead or fmShareDenyNone);
+  try
+    result := LoadFromStream(stream);
+  finally
+    stream.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.LoadFromStream(stream: TStream): Boolean;
+var
+  ifc: TImageFormatClass;
+begin
+  ifc := GetImageFormatClass(stream);
+  Result := Assigned(ifc);
+  if not Result then Exit;
+
+  with ifc.Create do
+  try
+    result := LoadFromStream(stream, self);
+  finally
+    free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.GetPixel(x, y: Integer): TColor32;
+begin
+  if (x < 0) or (x >= Width) or (y < 0) or (y >= Height) then
+    result := clNone32 else
+    result := fPixels[y * width + x];
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.SetPixel(x,y: Integer; color: TColor32);
+begin
+  if (x < 0) or (x >= Width) or (y < 0) or (y >= Height) then Exit;
+  fPixels[y * width + x] := color;
+  //nb: no notify event here
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.GetIsBlank: Boolean;
+var
+  i: integer;
+  pc: PARGB;
+begin
+  result := IsEmpty;
+  if result then Exit;
+  pc := PARGB(PixelBase);
+  for i := 0 to width * height -1 do
+  begin
+    if pc.A > 0 then Exit;
+    inc(pc);
+  end;
+  result := true;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.GetIsEmpty: Boolean;
+begin
+  result := fPixels = nil;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.GetPixelBase: PColor32;
+begin
+  if IsEmpty then result := nil
+  else result := @fPixels[0];
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.GetPixelRow(row: Integer): PColor32;
+begin
+  if IsEmpty then result := nil
+  else result := @fPixels[row * Width];
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.CopyInternal(src: TImage32;
+  const srcRec, dstRec: TRect; blendFunc: TBlendFunction);
+var
+  i, j, srcRecWidth, srcRecHeight: Integer;
+  s, d: PColor32;
+begin
+  // occasionally, due to rounding, srcRec and dstRec
+  // don't have exactly the same widths and heights, so ...
+  srcRecWidth :=
+    Min(srcRec.Right - srcRec.Left, dstRec.Right - dstRec.Left);
+  srcRecHeight :=
+    Min(srcRec.Bottom - srcRec.Top, dstRec.Bottom - dstRec.Top);
+
+  s := @src.Pixels[srcRec.Top * src.Width + srcRec.Left];
+  d := @Pixels[dstRec.top * Width + dstRec.Left];
+
+  if assigned(blendFunc) then
+    for i := srcRec.Top to srcRec.Top + srcRecHeight -1 do
+    begin
+      for j := 1 to srcRecWidth do
+      begin
+        d^ := blendFunc(d^, s^);
+        inc(s); inc(d);
+      end;
+      inc(s, src.Width - srcRecWidth);
+      inc(d, Width - srcRecWidth);
+    end
+  else
+    //simply overwrite src with dst (ie without blending)
+    for i := srcRec.Top to srcRec.Top + srcRecHeight -1 do
+    begin
+      move(s^, d^, srcRecWidth * SizeOf(TColor32));
+      inc(s, src.Width);
+      inc(d, Width);
+    end;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.Copy(src: TImage32; srcRec, dstRec: TRect): Boolean;
+begin
+  Result := CopyBlend(src, srcRec, dstRec, nil);
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.CopyBlend(src: TImage32; srcRec, dstRec: TRect;
+  blendFunc: TBlendFunction): Boolean;
+var
+  tmp: TImage32;
+  srcRecClipped, dstRecClipped, r: TRect;
+  scaleX, scaleY: double;
+  w,h, dstW,dstH, srcW,srcH: integer;
+begin
+  result := false;
+  if IsEmptyRect(srcRec) or IsEmptyRect(dstRec) then Exit;
+  Types.IntersectRect(srcRecClipped, srcRec, src.Bounds);
+
+  //get the scaling amount (if any) before
+  //dstRec might be adjusted due to clipping ...
+  RectWidthHeight(dstRec, dstW, dstH);
+  RectWidthHeight(srcRec, srcW, srcH);
+
+  //watching out for insignificant scaling
+  if Abs(dstW - srcW) < 2 then
+     scaleX := 1 else
+     scaleX := dstW / srcW;
+  if Abs(dstH - srcH) < 2 then
+     scaleY := 1 else
+     scaleY := dstH / srcH;
+
+  //check if the source rec has been clipped ...
+  if not RectsEqual(srcRecClipped, srcRec) then
+  begin
+    if IsEmptyRect(srcRecClipped) then Exit;
+    //the source has been clipped so clip the destination too ...
+    RectWidthHeight(srcRecClipped, w, h);
+    RectWidthHeight(srcRec, srcW, srcH);
+    ScaleRect(dstRec, w / srcW, h / srcH);
+    Types.OffsetRect(dstRec,
+      srcRecClipped.Left - srcRec.Left,
+      srcRecClipped.Top - srcRec.Top);
+  end;
+
+  if (scaleX <> 1.0) or (scaleY <> 1.0) then
+  begin
+    //scale source (tmp) to the destination then call CopyBlend() again ...
+    tmp := TImage32.Create(src, srcRecClipped);
+    try
+      tmp.Scale(scaleX, scaleY);
+      result := CopyBlend(tmp, tmp.Bounds, dstRec, blendFunc);
+    finally
+      tmp.Free;
+    end;
+    Exit;
+  end;
+
+  Types.IntersectRect(dstRecClipped, dstRec, Bounds);
+  if IsEmptyRect(dstRecClipped) then Exit;
+
+  //there's no scaling if we get here, but further clipping may be needed if
+  //the destination rec is partially outside the destination image's bounds
+
+  if not RectsEqual(dstRecClipped, dstRec) then
+  begin
+    //the destination rec has been clipped so clip the source too ...
+    RectWidthHeight(dstRecClipped, w, h);
+    RectWidthHeight(dstRec, dstW, dstH);
+    ScaleRect(srcRecClipped, w / dstW, h / dstH);
+    Types.OffsetRect(srcRecClipped,
+      dstRecClipped.Left - dstRec.Left,
+      dstRecClipped.Top - dstRec.Top);
+  end;
+
+  //when copying to self and srcRec & dstRec overlap then
+  //copy srcRec to a temporary image and use it as the source ...
+  if (src = self) and Types.IntersectRect(r, srcRecClipped, dstRecClipped) then
+  begin
+    tmp := TImage32.Create(self, srcRecClipped);
+    try
+      result := src.CopyBlend(tmp, tmp.Bounds, dstRecClipped, blendFunc);
+    finally
+      tmp.Free;
+    end;
+    Exit;
+  end;
+
+  CopyInternal(src, srcRecClipped, dstRecClipped, blendFunc);
+  result := true;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.LoadFromResource(const resName: string; resType: PChar): Boolean;
+var
+  resStream: TResourceStream;
+begin
+  resStream := CreateResourceStream(resName, resType);
+  try
+    Result := assigned(resStream) and
+      LoadFromStream(resStream);
+  finally
+    resStream.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+{$IFDEF MSWINDOWS}
+procedure TImage32.CopyFromDC(srcDc: HDC; const srcRect: TRect);
+var
+  bi: TBitmapInfoHeader;
+  bm, oldBm: HBitmap;
+  dc, memDc: HDC;
+  pixels: Pointer;
+  w,h: integer;
+begin
+  BeginUpdate;
+  try
+    RectWidthHeight(srcRect, w,h);
+    SetSize(w, h);
+    bi := Get32bitBitmapInfoHeader(w, h);
+    dc := GetDC(0);
+    memDc := CreateCompatibleDC(dc);
+    try
+      bm := CreateDIBSection(dc,
+        PBITMAPINFO(@bi)^, DIB_RGB_COLORS, pixels, 0, 0);
+      if bm = 0 then Exit;
+      try
+        oldBm := SelectObject(memDc, bm);
+        BitBlt(memDc, 0, 0, w, h, srcDc, srcRect.Left,srcRect.Top, SRCCOPY);
+        Move(pixels^, fPixels[0], w * h * sizeOf(TColor32));
+        SelectObject(memDc, oldBm);
+      finally
+        DeleteObject(bm);
+      end;
+    finally
+      DeleteDc(memDc);
+      ReleaseDc(0, dc);
+    end;
+    if IsBlank then SetAlpha(255);
+    FlipVertical;
+  finally
+    EndUpdate;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.CopyToDc(dstDc: HDC; x,y: Integer; transparent: Boolean);
+begin
+  CopyToDc(Bounds, Types.Rect(x,y, x+Width, y+Height),
+    dstDc, transparent);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.CopyToDc(const srcRect: TRect; dstDc: HDC;
+  x: Integer = 0; y: Integer = 0; transparent: Boolean = true);
+var
+  recW, recH: integer;
+begin
+  RectWidthHeight(srcRect, recW, recH);
+  CopyToDc(srcRect, Types.Rect(x,y, x+recW, y+recH), dstDc, transparent);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.CopyToDc(const srcRect, dstRect: TRect;
+  dstDc: HDC; transparent: Boolean = true);
+var
+  i, x,y, wSrc ,hSrc, wDest, hDest: integer;
+  rec: TRect;
+  bi: TBitmapInfoHeader;
+  bm, oldBm: HBitmap;
+  dibBits: Pointer;
+  pc: PARGB;
+  memDc: HDC;
+  isTransparent: Boolean;
+  bf: BLENDFUNCTION;
+begin
+  Types.IntersectRect(rec, srcRect, Bounds);
+  if IsEmpty or IsEmptyRect(rec) or IsEmptyRect(dstRect) then Exit;
+  RectWidthHeight(rec, wSrc, hSrc);
+  RectWidthHeight(dstRect, wDest, hDest);
+  x := dstRect.Left;
+  y := dstRect.Top;
+  inc(x, rec.Left - srcRect.Left);
+  inc(y, rec.Top - srcRect.Top);
+
+  bi := Get32bitBitmapInfoHeader(wSrc, hSrc);
+
+  isTransparent := transparent and RectHasTransparency(srcRect);
+  memDc := CreateCompatibleDC(0);
+  try
+    bm := CreateDIBSection(memDc, PBITMAPINFO(@bi)^,
+      DIB_RGB_COLORS, dibBits, 0, 0);
+    if bm = 0 then Exit;
+
+    try
+      //copy Image to dibBits (with vertical flip)
+      pc := dibBits;
+      for i := rec.Bottom -1 downto rec.Top do
+      begin
+        Move(Pixels[i * Width + rec.Left], pc^, wSrc * SizeOf(TColor32));
+        inc(pc, wSrc);
+      end;
+
+      oldBm := SelectObject(memDC, bm);
+      if isTransparent then
+      begin
+
+        //premultiplied alphas are required when alpha blending
+        pc := dibBits;
+        for i := 0 to wSrc * hSrc -1 do
+        begin
+          if pc.A > 0 then
+          begin
+            pc.R  := MulTable[pc.R, pc.A];
+            pc.G  := MulTable[pc.G, pc.A];
+            pc.B  := MulTable[pc.B, pc.A];
+          end else
+            pc.Color := 0;
+          inc(pc);
+        end;
+
+        bf.BlendOp := AC_SRC_OVER;
+        bf.BlendFlags := 0;
+        bf.SourceConstantAlpha := 255;
+        bf.AlphaFormat := AC_SRC_ALPHA;
+        AlphaBlend(dstDc, x,y, wDest,hDest, memDC, 0,0, wSrc,hSrc, bf);
+      end
+      else if (wDest = wSrc) and (hDest = hSrc) then
+        BitBlt(dstDc, x,y, wSrc, hSrc, memDc, 0,0, SRCCOPY)
+      else
+        StretchBlt(dstDc, x,y, wDest, hDest, memDc, 0,0, wSrc,hSrc, SRCCOPY);
+
+      SelectObject(memDC, oldBm);
+    finally
+      DeleteObject(bm);
+    end;
+  finally
+    DeleteDc(memDc);
+  end;
+end;
+//------------------------------------------------------------------------------
+{$ENDIF}
+
+function TImage32.CopyToClipBoard: Boolean;
+var
+  i: Integer;
+  formatClass: TImageFormatClass;
+begin
+  //Sadly with CF_DIB (and even CF_DIBV5) clipboard formats, transparency is
+  //usually lost, so we'll copy all available formats including CF_PNG, that
+  //is if it's registered.
+  result := not IsEmpty;
+  if not result then Exit;
+  result := false;
+
+  for i := ImageFormatClassList.Count -1 downto 0 do
+  begin
+    formatClass := PImgFmtRec(ImageFormatClassList[i]).Obj;
+    if not formatClass.CanCopyToClipboard then Continue;
+    with formatClass.Create do
+    try
+      result := CopyToClipboard(self);
+    finally
+      free;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+class function TImage32.CanPasteFromClipBoard: Boolean;
+var
+  i: Integer;
+  formatClass: TImageFormatClass;
+begin
+  result := false;
+  for i := ImageFormatClassList.Count -1 downto 0 do
+  begin
+    formatClass := PImgFmtRec(ImageFormatClassList[i]).Obj;
+    if formatClass.CanPasteFromClipboard then
+    begin
+      result := true;
+      Exit;
+    end;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.PasteFromClipBoard: Boolean;
+var
+  i: Integer;
+  formatClass: TImageFormatClass;
+begin
+  result := false;
+  for i := ImageFormatClassList.Count -1 downto 0 do
+  begin
+    formatClass := PImgFmtRec(ImageFormatClassList[i]).Obj;
+    if not formatClass.CanPasteFromClipboard then Continue;
+
+    with formatClass.Create do
+    try
+      result := PasteFromClipboard(self);
+      if not Result then Continue;
+    finally
+      free;
+    end;
+    Changed;
+    Break;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+{$IFDEF USING_VCL_LCL}
+procedure TImage32.CopyFromBitmap(bmp: TBitmap);
+var
+  savedPF: TPixelFormat;
+{$IFNDEF MSWINDOWS}
+  i: integer;
+  pxDst, pxSrc: PColor32;
+{$ENDIF}
+begin
+  if not Assigned(bmp) then Exit;
+  savedPF := bmp.PixelFormat;
+  bmp.PixelFormat := pf32bit;
+  SetSize(bmp.Width, bmp.Height);
+{$IFDEF MSWINDOWS}
+  GetBitmapBits(bmp.Handle, Width * Height * 4, PixelBase);
+{$ELSE}
+  for i := 0 to bmp.Height -1 do
+  begin
+    pxSrc := bmp.ScanLine[i];
+    pxDst := PixelRow[i];
+    Move(pxSrc^, pxDst^, bmp.Width * SizeOf(TColor32));
+  end;
+{$ENDIF}
+  bmp.PixelFormat := savedPF;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.CopyToBitmap(bmp: TBitmap);
+{$IFNDEF MSWINDOWS}
+var
+  i: integer;
+  pxDst, pxSrc: PColor32;
+{$ENDIF}
+begin
+  if not Assigned(bmp) then Exit;
+  bmp.PixelFormat := pf32bit;
+  bmp.Width := Width;
+  bmp.Height := Height;
+{$IFDEF MSWINDOWS}
+  {$IFNDEF FPC}
+  {$IFDEF ALPHAFORMAT}
+  bmp.AlphaFormat := afDefined;
+  {$ENDIF}
+  {$ENDIF}
+  SetBitmapBits(bmp.Handle, Width * Height * 4, PixelBase);
+{$ELSE}
+  for i := 0 to bmp.Height -1 do
+  begin
+    pxDst := bmp.ScanLine[i];
+    pxSrc := PixelRow[i];
+    Move(pxSrc^, pxDst^, bmp.Width * SizeOf(TColor32));
+  end;
+{$ENDIF}
+end;
+//------------------------------------------------------------------------------
+{$ENDIF}
+
+procedure TImage32.ConvertToBoolMask(reference: TColor32; tolerance: integer;
+  colorFunc: TCompareFunction; maskBg: TColor32; maskFg: TColor32);
+var
+  i: Integer;
+  mask: TArrayOfByte;
+  c: PColor32;
+  b: PByte;
+begin
+  if IsEmpty then Exit;
+  mask := GetBoolMask(self, reference, colorFunc, tolerance);
+  c := PixelBase;
+  b := @mask[0];
+  for i := 0 to Width * Height -1 do
+  begin
+  {$IFDEF PBYTE}
+    if b^ = 0 then c^ := maskBg else c^ := maskFg;
+  {$ELSE}
+    if b^ = #0 then c^ := maskBg else c^ := maskFg;
+  {$ENDIF}
+    inc(c); inc(b);
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.ConvertToAlphaMask(reference: TColor32;
+  colorFunc: TCompareFunctionEx);
+var
+  i: Integer;
+  mask: TArrayOfByte;
+  c: PColor32;
+  b: PByte;
+begin
+  if IsEmpty then Exit;
+  mask := GetByteMask(self, reference, colorFunc);
+  c := PixelBase;
+  b := @mask[0];
+  for i := 0 to Width * Height -1 do
+  begin
+  {$IFDEF PBYTE}
+    c^ := b^ shl 24;
+  {$ELSE}
+    c^ := Ord(b^) shl 24;
+  {$ENDIF}
+    inc(c); inc(b);
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.FlipVertical;
+var
+  i: Integer;
+  a: TArrayOfColor32;
+  src, dst: PColor32;
+begin
+  if IsEmpty then Exit;
+  SetLength(a, fWidth * fHeight);
+  src := @fPixels[(height-1) * width];
+  dst := @a[0];
+  for i := 0 to fHeight -1 do
+  begin
+    move(src^, dst^, fWidth * SizeOf(TColor32));
+    dec(src, fWidth); inc(dst, fWidth);
+  end;
+  fPixels := a;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.FlipHorizontal;
+var
+  i,j, widthLess1: Integer;
+  a: TArrayOfColor32;
+  row: PColor32;
+begin
+  if IsEmpty then Exit;
+  SetLength(a, fWidth);
+  widthLess1 := fWidth -1;
+  row := @fPixels[(height-1) * width]; //top row
+  for i := 0 to fHeight -1 do
+  begin
+    move(row^, a[0], fWidth * SizeOf(TColor32));
+    for j := 0 to widthLess1 do
+    begin
+      row^ := a[widthLess1 - j];
+      inc(row);
+    end;
+    dec(row, fWidth *2);
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.PreMultiply;
+var
+  i: Integer;
+  c: PARGB;
+begin
+  if IsEmpty or fIsPremultiplied then Exit;
+  fIsPremultiplied := true;
+  c := PARGB(PixelBase);
+  for i := 0 to Width * Height -1 do
+  begin
+    if (c.A = 0) then c.Color := 0
+    else if (c.A < 255) then
+    begin
+      c.R  := MulTable[c.R, c.A];
+      c.G  := MulTable[c.G, c.A];
+      c.B  := MulTable[c.B, c.A];
+    end;
+    inc(c);
+  end;
+  //nb: no OnChange notify event here
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.SetRGB(rgbColor: TColor32);
+var
+  rgb: TARGB absolute rgbColor;
+  r,g,b: Byte;
+  i: Integer;
+  pc: PARGB;
+begin
+  //this method leaves the alpha channel untouched
+  if IsEmpty then Exit;
+  r := rgb.R; g := rgb.G; b := rgb.B;
+  pc := PARGB(PixelBase);
+  for i := 0 to Width * Height -1 do
+    if pc.A = 0 then
+    begin
+      pc.Color := 0;
+      inc(pc);
+    end else
+    begin
+      pc.R := r;
+      pc.G := g;
+      pc.B := b;
+      inc(pc);
+    end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.SetRGB(rgbColor: TColor32; rec: TRect);
+var
+  rgb: TARGB absolute rgbColor;
+  r,g,b: Byte;
+  i,j, dx: Integer;
+  pc: PARGB;
+begin
+  Types.IntersectRect(rec, rec, bounds);
+  if IsEmptyRect(rec) then Exit;
+  r := rgb.R; g := rgb.G; b := rgb.B;
+  pc := PARGB(PixelBase);
+  inc(pc, rec.Left);
+  dx := Width - RectWidth(rec);
+  for i := rec.Top to rec.Bottom -1 do
+  begin
+    for j := rec.Left to rec.Right -1 do
+    begin
+      pc.R := r;
+      pc.G := g;
+      pc.B := b;
+      inc(pc);
+    end;
+    inc(pc, dx);
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.SetAlpha(alpha: Byte);
+var
+  i: Integer;
+  c: PARGB;
+begin
+  //this method only changes the alpha channel
+  if IsEmpty then Exit;
+  c := PARGB(PixelBase);
+  for i := 0 to Width * Height -1 do
+  begin
+    c.A := alpha;
+    inc(c);
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.ReduceOpacity(opacity: Byte);
+var
+  i: Integer;
+  c: PARGB;
+begin
+  if opacity = 255 then Exit;
+  c := PARGB(PixelBase);
+  for i := 0 to Width * Height -1 do
+  begin
+    c.A := MulTable[c.A, opacity];
+    inc(c);
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.ReduceOpacity(opacity: Byte; rec: TRect);
+var
+  i,j, rw: Integer;
+  c: PARGB;
+begin
+  Types.IntersectRect(rec, rec, bounds);
+  if IsEmptyRect(rec) then Exit;
+  rw := RectWidth(rec);
+  c := @Pixels[rec.Top * Width + rec.Left];
+  for i := rec.Top to rec.Bottom -1 do
+  begin
+    for j := 1 to rw do
+    begin
+      c.A := MulTable[c.A, opacity];
+      inc(c);
+    end;
+    inc(c, Width - rw);
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Grayscale;
+begin
+  AdjustSaturation(-100);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.InvertColors;
+var
+  pc: PARGB;
+  i: Integer;
+begin
+  pc := PARGB(PixelBase);
+  for i := 0 to Width * Height -1 do
+  begin
+    pc.R := 255 - pc.R;
+    pc.G := 255 - pc.G;
+    pc.B := 255 - pc.B;
+    inc(pc);
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.InvertAlphas;
+var
+  pc: PARGB;
+  i: Integer;
+begin
+  pc := PARGB(PixelBase);
+  for i := 0 to Width * Height -1 do
+  begin
+    pc.A := 255 - pc.A;
+    inc(pc);
+  end;
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.AdjustHue(percent: Integer);
+var
+  i: Integer;
+  tmpImage: TArrayofHSL;
+  lut: array [byte] of byte;
+begin
+  percent := percent mod 100;
+  if percent < 0 then inc(percent, 100);
+  percent := Round(percent * 255 / 100);
+  if (percent = 0) or IsEmpty then Exit;
+  for i := 0 to 255 do lut[i] := (i + percent) mod 255;
+  tmpImage := ArrayOfColor32ToArrayHSL(fPixels);
+  for i := 0 to high(tmpImage) do
+    tmpImage[i].hue := lut[ tmpImage[i].hue ];
+  fPixels := ArrayOfHSLToArrayColor32(tmpImage);
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.AdjustLuminance(percent: Integer);
+var
+  i: Integer;
+  tmpImage: TArrayofHSL;
+  pc: double;
+  lut: array [byte] of byte;
+begin
+  if (percent = 0) or IsEmpty then Exit;
+  percent := percent mod 101;
+  pc := percent / 100;
+  if pc > 0 then
+    for i := 0 to 255 do lut[i] := Round(i + (255 - i) * pc)
+  else
+    for i := 0 to 255 do lut[i] := Round(i + (i * pc));
+
+  tmpImage := ArrayOfColor32ToArrayHSL(fPixels);
+  for i := 0 to high(tmpImage) do
+    tmpImage[i].lum := lut[ tmpImage[i].lum ];
+  fPixels := ArrayOfHSLToArrayColor32(tmpImage);
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.AdjustSaturation(percent: Integer);
+var
+  i: Integer;
+  tmpImage: TArrayofHSL;
+  lut: array [byte] of byte;
+  pc: double;
+begin
+  if (percent = 0) or IsEmpty then Exit;
+  percent := percent mod 101;
+  pc := percent / 100;
+  if pc > 0 then
+    for i := 0 to 255 do lut[i] := Round(i + (255 - i) * pc)
+  else
+    for i := 0 to 255 do lut[i] := Round(i + (i * pc));
+
+  tmpImage := ArrayOfColor32ToArrayHSL(fPixels);
+  for i := 0 to high(tmpImage) do
+    tmpImage[i].sat := lut[ tmpImage[i].sat ];
+  fPixels := ArrayOfHSLToArrayColor32(tmpImage);
+  Changed;
+end;
+//------------------------------------------------------------------------------
+
+function TImage32.CropTransparentPixels: TRect;
+var
+  x,y, x1,x2,y1,y2: Integer;
+  found: Boolean;
+begin
+  y1 := 0; y2 := 0;
+  found := false;
+  for y := 0 to Height -1 do
+  begin
+    for x := 0 to Width -1 do
+      if TARGB(fPixels[y * Width + x]).A > 0 then
+      begin
+        y1 := y;
+        found := true;
+        break;
+      end;
+    if found then break;
+  end;
+
+  if not found then
+  begin
+    SetSize(0, 0);
+    Exit;
+  end;
+
+  found := false;
+  for y := Height -1 downto 0 do
+  begin
+    for x := 0 to Width -1 do
+      if TARGB(fPixels[y * Width + x]).A > 0 then
+      begin
+        y2 := y;
+        found := true;
+        break;
+      end;
+    if found then break;
+  end;
+
+  x1 := Width; x2 := 0;
+  for y := y1 to y2 do
+    for x := 0 to Width -1 do
+      if TARGB(fPixels[y * Width + x]).A > 0 then
+      begin
+        if x < x1 then x1 := x;
+        if x > x2 then x2 := x;
+      end;
+
+  Result := Types.Rect(x1, y1, x2+1, y2+1);
+  Crop(Result);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Rotate(angleRads: double);
+var
+  rec: TRectD;
+  mat: TMatrixD;
+begin
+  if not ClockwiseRotationIsAnglePositive then
+    angleRads := -angleRads;
+
+  //nb: There's no point rotating about a specific point
+  //since the rotated image will be recentered.
+
+  NormalizeAngle(angleRads);
+  if IsEmpty or (angleRads = 0) then Exit;
+
+  if angleRads = angle180 then
+  begin
+    Rotate180; //because we've excluded 0 & 360 deg angles
+  end
+  else if angleRads = angle90 then
+  begin
+    RotateRight90;
+  end
+  else if angleRads = -angle90 then
+  begin
+    RotateLeft90;
+  end else
+  begin
+    mat := IdentityMatrix;
+    MatrixTranslate(mat, Width/2, Height/2);
+    rec := RectD(Bounds);
+    rec := GetRotatedRectBounds(rec, angleRads);
+    MatrixRotate(mat, NullPointD, angleRads);
+    MatrixTranslate(mat, rec.Width/2, rec.Height/2);
+    AffineTransformImage(self, mat);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.RotateRect(const rec: TRect;
+  angleRads: double; eraseColor: TColor32 = 0);
+var
+  tmp: TImage32;
+  rec2: TRect;
+  recWidth, recHeight: integer;
+begin
+  recWidth := rec.Right - rec.Left;
+  recHeight := rec.Bottom - rec.Top;
+  //create a tmp image with a copy of the pixels inside rec ...
+  tmp := TImage32.Create(self, rec);
+  try
+    tmp.Rotate(angleRads);
+    //since rotating also resizes, get a centered
+    //(clipped) rect of the rotated pixels ...
+    rec2.Left := (tmp.Width - recWidth) div 2;
+    rec2.Top := (tmp.Height - recHeight) div 2;
+    rec2.Right := rec2.Left + recWidth;
+    rec2.Bottom := rec2.Top + recHeight;
+    //finally move the rotated rec back to the image ...
+    FillRect(rec, eraseColor);
+    CopyBlend(tmp, rec2, rec);
+  finally
+    tmp.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.Skew(dx,dy: double);
+var
+  mat: TMatrixD;
+begin
+  if IsEmpty or ((dx = 0) and (dy = 0)) then Exit;
+  //limit skewing to twice the image's width and/or height
+  dx := ClampRange(dx, -2.0, 2.0);
+  dy := ClampRange(dy, -2.0, 2.0);
+  mat := IdentityMatrix;
+  MatrixSkew(mat, dx, dy);
+  AffineTransformImage(self, mat);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImage32.ScaleAlpha(scale: double);
+var
+  i: Integer;
+  pb: PARGB;
+begin
+  pb := PARGB(PixelBase);
+  for i := 0 to Width * Height - 1 do
+  begin
+    pb.A := ClampByte(Round(pb.A * scale));
+    inc(pb);
+  end;
+  Changed;
+end;
+
+//------------------------------------------------------------------------------
+// TImageList32
+//------------------------------------------------------------------------------
+
+constructor TImageList32.Create;
+begin
+{$IFDEF XPLAT_GENERICS}
+  fList := TList<TImage32>.Create;
+{$ELSE}
+  fList := TList.Create;
+{$ENDIF}
+  fIsImageOwner := true;
+end;
+//------------------------------------------------------------------------------
+
+destructor TImageList32.Destroy;
+begin
+  Clear;
+  fList.Free;
+  inherited;
+end;
+//------------------------------------------------------------------------------
+
+function TImageList32.Count: integer;
+begin
+  result := fList.Count;
+end;
+//------------------------------------------------------------------------------
+
+procedure TImageList32.Clear;
+var
+  i: integer;
+begin
+  if IsImageOwner then
+    for i := 0 to fList.Count -1 do
+      TImage32(fList[i]).Free;
+  fList.Clear;
+end;
+//------------------------------------------------------------------------------
+
+function TImageList32.GetImage(index: integer): TImage32;
+begin
+  result := TImage32(fList[index]);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImageList32.SetImage(index: integer; img: TIMage32);
+begin
+  if fIsImageOwner then TImage32(fList[index]).Free;
+  fList[index] := img;
+end;
+//------------------------------------------------------------------------------
+
+function TImageList32.GetLast: TImage32;
+begin
+  if Count = 0 then Result := nil
+  else Result := TImage32(fList[Count -1]);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImageList32.Add(image: TImage32);
+begin
+  fList.Add(image);
+end;
+//------------------------------------------------------------------------------
+
+function TImageList32.Add(width, height: integer): TImage32;
+begin
+  Result := TImage32.create(width, height);
+  fList.Add(Result);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImageList32.Insert(index: integer; image: TImage32);
+begin
+  fList.Insert(index, image);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImageList32.Move(currentIndex, newIndex: integer);
+begin
+  fList.Move(currentIndex, newIndex);
+end;
+//------------------------------------------------------------------------------
+
+procedure TImageList32.Delete(index: integer);
+begin
+  if fIsImageOwner then TImage32(fList[index]).Free;
+  fList.Delete(index);
+end;
+
+//------------------------------------------------------------------------------
+// TImageFormat methods
+//------------------------------------------------------------------------------
+
+function TImageFormat.LoadFromFile(const filename: string;
+  img32: TImage32): Boolean;
+var
+  fs: TFileStream;
+begin
+  result := FileExists(filename);
+  if not result then Exit;
+  fs := TFileStream.Create(filename, fmOpenRead or fmShareDenyWrite);
+  try
+    Result := LoadFromStream(fs, img32);
+  finally
+    fs.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function TImageFormat.SaveToFile(const filename: string;
+  img32: TImage32): Boolean;
+var
+  fs: TFileStream;
+begin
+  result := (pos('.', filename) = 1) or
+    DirectoryExists(ExtractFilePath(filename));
+  if not result then Exit;
+
+  fs := TFileStream.Create(filename, fmCreate);
+  try
+    SaveToStream(fs, img32);
+  finally
+    fs.Free;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+class function TImageFormat.CanCopyToClipboard: Boolean;
+begin
+  Result := false;
+end;
+
+//------------------------------------------------------------------------------
+// TInterfacedObj
+//------------------------------------------------------------------------------
+
+{$IFDEF FPC}
+function TInterfacedObj._AddRef: Integer;
+  {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
+begin
+  Result := -1;
+end;
+//------------------------------------------------------------------------------
+
+function TInterfacedObj._Release: Integer;
+  {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
+begin
+  Result := -1;
+end;
+//------------------------------------------------------------------------------
+
+function TInterfacedObj.QueryInterface(
+  {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} iid : tguid;
+  out obj) : longint;
+begin
+  if GetInterface(IID, Obj) then Result := 0
+  else Result := E_NOINTERFACE;
+end;
+
+{$ELSE}
+
+function TInterfacedObj._AddRef: Integer; stdcall;
+begin
+  Result := -1;
+end;
+//------------------------------------------------------------------------------
+
+function TInterfacedObj._Release: Integer; stdcall;
+begin
+  Result := -1;
+end;
+//------------------------------------------------------------------------------
+
+function TInterfacedObj.QueryInterface(const IID: TGUID;
+  out Obj): HResult;
+begin
+  if GetInterface(IID, Obj) then Result := 0
+  else Result := E_NOINTERFACE;
+end;
+{$ENDIF}
+
+//------------------------------------------------------------------------------
+// Initialization and Finalization functions
+//------------------------------------------------------------------------------
+
+procedure MakeBlendTables;
+var
+  i,j: Integer;
+begin
+  for j := 0 to 255 do MulTable[0, j] := 0;
+  for i := 0 to 255 do MulTable[i, 0] := 0;
+  for j := 0 to 255 do DivTable[0, j] := 0;
+  for i := 0 to 255 do DivTable[i, 0] := 0;
+  for i := 1 to 255 do
+    for j := 1 to 255 do
+    begin
+      MulTable[i, j] := Round(i * j * div255);
+      if i >= j then
+        DivTable[i, j] := 255 else
+        DivTable[i, j] := Round(i * $FF / j);
+    end;
+end;
+//------------------------------------------------------------------------------
+
+{$IFDEF MSWINDOWS}
+procedure GetScreenScale;
+var
+  dc: HDC;
+  ScreenPixelsY: integer;
+begin
+  dc := GetDC(0);
+  try
+    ScreenPixelsY := GetDeviceCaps(dc, LOGPIXELSY);
+    DpiAwareOne := ScreenPixelsY / 96;
+  finally
+    ReleaseDC(0, dc);
+  end;
+  dpiAware1   := Round(DpiAwareOne);
+end;
+{$ENDIF}
+//------------------------------------------------------------------------------
+
+{$IFDEF USING_VCL_LCL}
+procedure GetScreenScale2;
+begin
+  DpiAwareOne := Screen.PixelsPerInch / 96;
+  dpiAware1   := Round(DpiAwareOne);
+end;
+{$ENDIF}
+//------------------------------------------------------------------------------
+
+procedure CleanUpImageFormatClassList;
+var
+  i: integer;
+begin
+  for i := ImageFormatClassList.Count -1 downto 0 do
+    Dispose(PImgFmtRec(ImageFormatClassList[i]));
+  ImageFormatClassList.Free;
+end;
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+procedure CreateResamplerList;
+begin
+{$IFDEF XPLAT_GENERICS}
+  ResamplerList := TList<TResamplerObj>.Create;
+{$ELSE}
+  ResamplerList := TList.Create;
+{$ENDIF}
+end;
+//------------------------------------------------------------------------------
+
+function GetResampler(id: integer): TResamplerFunction;
+var
+  i: integer;
+begin
+  result := nil;
+  if not Assigned(ResamplerList) then Exit;
+
+  for i := ResamplerList.Count -1 downto 0 do
+    if TResamplerObj(ResamplerList[i]).id = id then
+  begin
+    Result := TResamplerObj(ResamplerList[i]).func;
+    Break;
+  end;
+end;
+//------------------------------------------------------------------------------
+
+function RegisterResampler(func: TResamplerFunction; const name: string): integer;
+var
+  resampleObj: TResamplerObj;
+begin
+  if not Assigned(ResamplerList) then
+    CreateResamplerList;
+
+  resampleObj := TResamplerObj.Create;
+  Result := ResamplerList.Add(resampleObj) +1;
+  resampleObj.id := Result;
+  resampleObj.name := name;
+  resampleObj.func := func;
+end;
+//------------------------------------------------------------------------------
+
+procedure GetResamplerList(stringList: TStringList);
+var
+  i: integer;
+  resampleObj: TResamplerObj;
+begin
+  stringList.Clear;
+  stringList.Capacity := ResamplerList.Count;
+  for i := 0 to ResamplerList.Count -1 do
+  begin
+    resampleObj := ResamplerList[i];
+    stringList.AddObject(resampleObj.name, resampleObj);
+  end;
+end;
+//------------------------------------------------------------------------------
+
+procedure CleanUpResamplerClassList;
+var
+  i: integer;
+begin
+  if not Assigned(ResamplerList) then Exit;
+  for i := ResamplerList.Count -1 downto 0 do
+    TResamplerObj(ResamplerList[i]).Free;
+  ResamplerList.Free;
+end;
+//------------------------------------------------------------------------------
+
+initialization
+  CreateImageFormatList;
+  MakeBlendTables;
+
+{$IFDEF MSWINDOWS}
+  GetScreenScale;
+{$ELSE}
+  {$IFDEF USING_VCL_LCL}
+  GetScreenScale2;
+  {$ENDIF}
+{$ENDIF}
+
+finalization
+  CleanUpImageFormatClassList;
+  CleanUpResamplerClassList;
+
+end.