unit SVG;

interface

uses
  SysUtils, Types, UITypes, Classes, Graphics, Generics.Defaults,
  Generics.Collections, DoublePoint;

const
  SVGLengthUnits: array[0..9] of string =
    ('', 'em', 'ex', 'px', 'in', 'cm', 'mm', 'pt', 'pc', '%');

type
  TTagKind = (tkStart, tkEnd, tkSelfClose, tkWithChild, tkOptionalWithChild,
    tkWithCDATA, tkComment);

  TTransform = record
  strict private
    FData: string;
  public
    class operator Multiply(const L, R: TTransform): TTransform;
    class operator Implicit(const ATransform: TTransform): string;
    class operator Explicit(const S: string): TTransform;
  end;

function Translation(const X: Single; const Y: Single = 0.0): TTransform; overload;
function Translation(const X: TVectorD): TTransform; overload;
function Scaling(const X: Single): TTransform; overload;
function Scaling(const X, Y: Single): TTransform; overload;
function Rotation(const Theta: Single; const X: Single = 0.0;
  const Y: Single = 0.0): TTransform;
function RotationDeg(const Theta: Single; const X: Single = 0.0;
  const Y: Single = 0.0): TTransform;
function RotationRad(const Theta: Single; const X: Single = 0.0;
  const Y: Single = 0.0): TTransform;
function SkewX(const Theta: Single): TTransform;
function SkewY(const Theta: Single): TTransform;
function Matrix(const A, B, C, D, E, F: Single): TTransform;
function Identity: TTransform;

type
  TSVGBuilder = class;

  TTag = record
  strict private type
    TContentType = (ctText, ctXML);
  strict private var
    FKind: TTagKind;
    FName: string;
    FAttribs: TArray<TPair<string, string>>;
    FContent: string;
    FContentType: TContentType;
    FSVG: TSVGBuilder;
  public
    class operator Implicit(const ATag: TTag): string;
    function Associate(ASVG: TSVGBuilder): TTag;
    function Name(const ATagName: string): TTag;
    function StartTag: TTag;
    function EndTag: TTag;
    function SelfClosed: TTag;
    function ContentText(const AContent: string): TTag;
    function ContentXML(const AContent: string): TTag;
    function CDATA(const AContent: string): TTag;
    function Attrib(const AName, AValue: string): TTag; overload;
    function Attrib(const AName: string; const AValue: Integer): TTag; overload;
    function Attrib(const AName: string; const AValue: Single): TTag; overload;
    function Attrib(const AName: string; const AValue: Double): TTag; overload;
    function AttribIf(const AName, AValue: string; ACondition: Boolean): TTag; overload;
    function AttribIf(const AName, AValue: string): TTag; overload;
    function AttribIf(const AName: string; const AValue: Integer;
      ACondition: Boolean): TTag; overload;
    function AttribIf(const AName: string; const AValue: Single;
      ACondition: Boolean): TTag; overload;
    function AttribIf(const AName: string; const AValue: Double;
      ACondition: Boolean): TTag; overload;
    function Attribs(AAttribs: TDictionary<string, string>): TTag; overload;
    function Attribs(AAttribs: TList<TPair<string, string>>): TTag; overload;
    function Attribs(AAttribs: TArray<TPair<string, string>>): TTag; overload;
    procedure Append(const AOpName: string = '');
    procedure AppendIf(const AOpName: string);
    function ID(const AID: string): TTag;
    function &Class(const AClassName: string): TTag;
    function Style(const AStyle: string): TTag;
    function x(const Ax: string): TTag; overload;
    function x(const Ax: Integer): TTag; overload;
    function x(const Ax: Single): TTag; overload;
    function x(const Ax: Double): TTag; overload;
    function y(const Ay: string): TTag; overload;
    function y(const Ay: Integer): TTag; overload;
    function y(const Ay: Single): TTag; overload;
    function y(const Ay: Double): TTag; overload;
    function Width(const AWidth: string): TTag; overload;
    function Width(const AWidth: Integer): TTag; overload;
    function Width(const AWidth: Single): TTag; overload;
    function Width(const AWidth: Double): TTag; overload;
    function Height(const AHeight: string): TTag; overload;
    function Height(const AHeight: Integer): TTag; overload;
    function Height(const AHeight: Single): TTag; overload;
    function Height(const AHeight: Double): TTag; overload;
    function Transform(const ATransform: TTransform): TTag;
    function TransformIf(const ATransform: TTransform; ACondition: Boolean): TTag;
    function Fill(const AFill: string): TTag; overload;
    function Fill(const AFill: TColor): TTag; overload;
    function FillIf(const AFill: string; ACondition: Boolean): TTag; overload;
    function FillIf(const AFill: TColor; ACondition: Boolean): TTag; overload;
    function FillOpacity(const AFillOpacity: string): TTag; overload;
    function FillOpacity(const AFillOpacity: Single): TTag; overload;
    function Opacity(const AOpacity: string): TTag; overload;
    function Opacity(const AOpacity: Single): TTag; overload;
    function Stroke(const AStroke: string): TTag; overload;
    function Stroke(const AStroke: TColor): TTag; overload;
    function StrokeIf(const AStroke: string; ACondition: Boolean): TTag; overload;
    function StrokeIf(const AStroke: TColor; ACondition: Boolean): TTag; overload;
    function StrokeWidth(const AStrokeWidth: string): TTag; overload;
    function StrokeWidth(const AStrokeWidth: Integer): TTag; overload;
    function StrokeWidth(const AStrokeWidth: Single): TTag; overload;
    function StrokeWidth(const AStrokeWidth: Double): TTag; overload;
    function StrokeWidthIf(const AStrokeWidth: string; ACondition: Boolean): TTag; overload;
    function StrokeWidthIf(const AStrokeWidth: Integer; ACondition: Boolean): TTag; overload;
    function StrokeWidthIf(const AStrokeWidth: Single; ACondition: Boolean): TTag; overload;
    function StrokeWidthIf(const AStrokeWidth: Double; ACondition: Boolean): TTag; overload;
    function StrokeWidthPx(const AStrokeWidth: Integer): TTag; overload;
    function StrokeWidthPx(const AStrokeWidth: Single): TTag; overload;
    function TextAnchor(const AAnchor: string): TTag; overload;
    function TextAnchor(AAnchor: TAlignment): TTag; overload;
    function DominantBaseline(const AAnchor: string): TTag; overload;
    function DominantBaseline(AAnchor: TVerticalAlignment): TTag; overload;
    function MarkerStart(const AMarker: string): TTag;
    function MarkerEnd(const AMarker: string): TTag;
    function MarkerMid(const AMarker: string): TTag;
    function A_Begin(const ABegin: string): TTag; overload;
    function A_Begin(const ABegin: Integer): TTag; overload;
    function A_Begin(const ABegin: Single): TTag; overload;
    function A_End(const AEnd: string): TTag; overload;
    function A_End(const AEnd: Integer): TTag; overload;
    function A_End(const AEnd: Single): TTag; overload;
    function A_Duration(const ADuration: string): TTag; overload;
    function A_Duration(const ADuration: Integer): TTag; overload;
    function A_Duration(const ADuration: Single): TTag; overload;
    function A_RepeatCount(const ARepeatCount: string): TTag; overload;
    function A_RepeatCount(const ARepeatCount: Integer): TTag; overload;
    function A_RepeatIndefinitely: TTag;
    function A_RepeatDuration(const ARepeatDuration: string): TTag; overload;
    function A_RepeatDuration(const ARepeatDuration: Integer): TTag; overload;
    function A_RepeatDuration(const ARepeatDuration: Single): TTag; overload;
    function A_Values(const AValues: string): TTag; overload;
    function A_Values(const AValues: array of Integer): TTag; overload;
    function A_Values(const AValues: array of Single): TTag; overload;
    function A_KeyTimes(const AKeyTimes: string): TTag; overload;
    function A_KeyTimes(const AKeyTimes: array of Integer): TTag; overload;
    function A_KeyTimes(const AKeyTimes: array of Single): TTag; overload;
    function A_From(const AFrom: string): TTag; overload;
    function A_From(const AFrom: Integer): TTag; overload;
    function A_From(const AFrom: Single): TTag; overload;
    function A_To(const ATo: string): TTag; overload;
    function A_To(const ATo: Integer): TTag; overload;
    function A_To(const ATo: Single): TTag; overload;
    function FontFamily(const AFamilies: string): TTag;
    function FontSize(const ASize: string): TTag; overload;
    function FontSize(const ASize: Integer): TTag; overload;
    function FontSize(const ASize: Single): TTag; overload;
    function FontStretch(const AStretch: string): TTag;
    function FontStyle(const AStyle: string): TTag;
    function FontItalic: TTag;
    function FontOblique: TTag;
    function FontVariant(const AVariant: string): TTag;
    function FontWeight(const AWeight: string): TTag;
    function FontBold: TTag;
    function FontBolder: TTag;
    function FontLighter: TTag;
    function Font(AFont: TFont): TTag;
    function RescalePoints(const XFactor, YFactor: Double): TTag;
    function Clone: TTag;
  end;

  TCSSBuilder = record
  private type
    TRule = string;
    TRuleSet = record
      Selector: string;
      Rules: TArray<TRule>;
    end;
  strict private var
    RuleSets: TArray<TRuleSet>;
  public
    class operator Implicit(const ACSS: TCSSBuilder): string;
    function RuleSet(const ASelector: string; const ARules: array of TRule): TCSSBuilder;
  end;

  ESVGException = class(Exception);

  TTransformType = (ttTranslation, ttScaling, ttRotation, ttSkewX, ttSkewY);

  TViewBox = record
    Xmin,
    Ymin,
    Width,
    Height: Single;
    class operator Implicit(const AViewBox: TViewBox): string;
    constructor Create(const Xmin, Ymin, Width, Height: Integer); overload;
    constructor Create(const Xmin, Ymin, Width, Height: Single); overload;
    constructor Create(const Xmin, Ymin, Width, Height, ScaleX, ScaleY: Single); overload;
    function Xmax: Single; inline;
    function Ymax: Single; inline;
    function Valid: Boolean; inline;
  end;

  TSVGBuilder = class
  strict private
  type
    TNodeType = (ntDefs, ntSymbol, ntMarker, ntGroup, ntSwitch, ntMetadata);
  var
    FSvgVersion: string;
    FWidth, FHeight: string;
    FStretch: Boolean;
    FViewBox: TViewBox;
    FLanguage, FTitle, FDescription: string;
    FLineBreak: string;
    FDefs: TDictionary<string, string>;
    FParts: TList<string>;
    FStack: TStack<TNodeType>;
    FDefines: TDictionary<string, Pointer>;
    FDefID: string;
    FNamespaces: TDictionary<string, string>;
    function GetAsXML: string;
    procedure StructureError;
    function GetAspectRatio: Double;
    procedure EndDefinition;
  private
    FAbstract: Boolean;
    procedure AddTag(const ATag: TTag);
  public
    function EndTag(const ATagName: string = ''): TTag;
    function OptionalTag(const ATagName: string = ''): TTag;
    function StartTag(const ATagName: string = ''): TTag;
    function Tag(const ATagName: string = ''): TTag;
    function XmlComment(const AComment: string = ''): TTag;
  public
    constructor Create(AAbstract: Boolean = False); virtual;
    destructor Destroy; override;
    procedure DefineNamespace(const APrefix, AURL: string);
    procedure Define(const AName: string);
    function Undefine(const AName: string): Boolean;
    function Line(const x1, y1, x2, y2: string): TTag; overload;
    function Line(const x1, y1, x2, y2: Integer): TTag; overload;
    function Line(const x1, y1, x2, y2: Single): TTag; overload;
    function Line(const x1, y1, x2, y2: Double): TTag; overload;
    function Line(const P, Q: TPoint): TTag; overload;
    function Line(const P, Q: TPointF): TTag; overload;
    function Line(const P, Q: TPointD): TTag; overload;
    function PolyLine(const APoints: string): TTag; overload;
    function PolyLine(const APoints: array of TPoint): TTag; overload;
    function PolyLine(const APoints: array of TPointF): TTag; overload;
    function PolyLine(const APoints: array of TPointD): TTag; overload;
    function Polygon(const APoints: string): TTag; overload;
    function Polygon(const APoints: array of TPoint): TTag; overload;
    function Polygon(const APoints: array of TPointF): TTag; overload;
    function Polygon(const APoints: array of TPointD): TTag; overload;
    function Rect(const x, y, w, h: string): TTag; overload;
    function Rect(const x, y, w, h: Single): TTag; overload;
    function Rect(const x, y, w, h: Double): TTag; overload;
    function Rect(const R: TRect): TTag; overload;
    function Rect(const R: TRectF): TTag; overload;
    function Rect(const R: TRectD): TTag; overload;
    function Circle(const x, y, r: string): TTag; overload;
    function Circle(const x, y, r: Integer): TTag; overload;
    function Circle(const x, y, r: Single): TTag; overload;
    function Circle(const x, y, r: Double): TTag; overload;
    function Circle(const x, y: Single; const r: string): TTag; overload;
    function Circle(const x: TPoint; const r: Integer): TTag; overload;
    function Circle(const x: TPointF; const r: Single): TTag; overload;
    function Circle(const x: TPointD; const r: Double): TTag; overload;
    function Circle(const r: Integer): TTag; overload;
    function Circle(const r: Single): TTag; overload;
    function Circle(const r: Double): TTag; overload;
    function Ellipse(const x, y, rx, ry: string): TTag; overload;
    function Ellipse(const x, y, rx, ry: Integer): TTag; overload;
    function Ellipse(const x, y, rx, ry: Single): TTag; overload;
    function Ellipse(const x, y, rx, ry: Double): TTag; overload;
    function Ellipse(const x: TPoint; const rx, ry: Integer): TTag; overload;
    function Ellipse(const x: TPointF; const rx, ry: Single): TTag; overload;
    function Ellipse(const x: TPointD; const rx, ry: Double): TTag; overload;
    function Ellipse2(const x, y, rx, ry: string): TTag; overload;
    function Ellipse2(const x, y, rx, ry: Integer): TTag; overload;
    function Ellipse2(const x, y, rx, ry: Single): TTag; overload;
    function Ellipse2(const x, y, rx, ry: Double): TTag; overload;
    function Ellipse2(const x: TPoint; const rx, ry: Integer): TTag; overload;
    function Ellipse2(const x: TPointF; const rx, ry: Single): TTag; overload;
    function Ellipse2(const x: TPointD; const rx, ry: Double): TTag; overload;
    function Path(const d: string; const APathLength: Single = 0.0): TTag;
    function Arc(const x, y, rx, ry, t0, t1: Double): TTag; overload;
    function Arc(const X: TPointD; const rx, ry, t0, t1: Double): TTag; overload;
    function Sector(const x, y, rx, ry, t0, t1: Double): TTag; overload;
    function Sector(const X: TPointD; const rx, ry, t0, t1: Double): TTag; overload;
    function Text(const S: string = ''): TTag; overload;
    function Text(const x, y: string; const S: string = ''): TTag; overload;
    function Text(const x, y: Integer; const S: string = ''): TTag; overload;
    function Text(const x, y: Single; const S: string = ''): TTag; overload;
    function Text(const x, y: Double; const S: string = ''): TTag; overload;
    function Text(const x: TPoint; const S: string = ''): TTag; overload;
    function Text(const x: TPointF; const S: string = ''): TTag; overload;
    function Text(const x: TPointD; const S: string = ''): TTag; overload;
    function TextPath(const APathRef: string; const S: string = '';
      const AStartOffset: string = ''): TTag;
    function TextSpan(const S: string = ''): TTag;
    function Link(const ref: string): TTag;
    function Image(const x, y, w, h, ref: string): TTag; overload;
    function Image(const x, y, w, h: Integer; const ref: string): TTag; overload;
    function Image(const x, y, w, h: Single; const ref: string): TTag; overload;
    function Image(const x, y, w, h: Double; const ref: string): TTag; overload;
    function Image(const x: TPoint; const s: TSize; const ref: string): TTag; overload;
    function Image(const x: TPointF; const s: TSizeF; const ref: string): TTag; overload;
    function Image(const R: TRect; const ref: string): TTag; overload;
    function Image(const R: TRectF; const ref: string): TTag; overload;
    function Image(const R: TRectD; const ref: string): TTag; overload;
    function HasID(const ID: string): Boolean;
    function BeginSymbol(const AViewBox: string = ''): TTag;
    function EndSymbol: TTag;
    function DefSymbol(const ID: string; const AViewBox: string = ''): TTag;
    procedure EndDefSymbol;
    function BeginMarker(const AViewBox, ARefX, ARefY, AMarkerWidth,
      AMarkerHeight: string; const AStrokeWidth: Boolean = True): TTag; overload;
    function BeginMarker(const AViewBox: string; ARefX, ARefY, AMarkerWidth,
      AMarkerHeight: Integer; const AStrokeWidth: Boolean = True): TTag; overload;
    function BeginMarker(const AViewBox: string; ARefX, ARefY, AMarkerWidth,
      AMarkerHeight: Single; const AStrokeWidth: Boolean = True): TTag; overload;
    function BeginMarker(const AViewBox: string; ARefX, ARefY, AMarkerWidth,
      AMarkerHeight: Double; const AStrokeWidth: Boolean = True): TTag; overload;
    function EndMarker: TTag;
    function DefMarker(const ID, AViewBox, ARefX, ARefY, AMarkerWidth,
      AMarkerHeight: string; const AStrokeWidth: Boolean = True): TTag; overload;
    function DefMarker(const ID, AViewBox: string; ARefX, ARefY, AMarkerWidth,
      AMarkerHeight: Integer; const AStrokeWidth: Boolean = True): TTag; overload;
    function DefMarker(const ID, AViewBox: string; ARefX, ARefY, AMarkerWidth,
      AMarkerHeight: Single; const AStrokeWidth: Boolean = True): TTag; overload;
    function DefMarker(const ID, AViewBox: string; ARefX, ARefY, AMarkerWidth,
      AMarkerHeight: Double; const AStrokeWidth: Boolean = True): TTag; overload;
    procedure EndDefMarker;
    function Use(const ref: string): TTag; overload;
    function Use(const x, y, w, h, ref: string): TTag; overload;
    function Use(const x, y, ref: string): TTag; overload;
    function Use(const x, y, w, h: Integer; const ref: string): TTag; overload;
    function Use(const x, y: Integer; const ref: string): TTag; overload;
    function Use(const x, y: Single; const ref: string): TTag; overload;
    function Use(const x, y: Double; const ref: string): TTag; overload;
    function Use(const x, y, w, h: Single; const ref: string): TTag; overload;
    function Use(const x, y, w, h: Double; const ref: string): TTag; overload;
    function Use(const X: TPoint; const S: TSize; const ref: string): TTag; overload;
    function Use(const X: TPoint; const ref: string): TTag; overload;
    function Use(const X: TPointF; const S: TSizeF; const ref: string): TTag; overload;
    function Use(const X: TPointF; const ref: string): TTag; overload;
    function Use(const X: TPointD; const S: TSizeD; const ref: string): TTag; overload;
    function Use(const X: TPointD; const ref: string): TTag; overload;
    function Animate(const AAttributeName: string = '';
      const AAttributeType: string = ''): TTag;
    function AnimateMotion(const APathData: string = '';
      const ARotate: string = ''): TTag;
    function AnimateTransform(const AType: TTransformType): TTag;
    function AnimateSet(const AAttributeName, ATo: string): TTag;
    function MPath(const APathName: string): TTag;
    function BeginSwitch: TTag;
    function EndSwitch: TTag;
    function Style(const CSS: string): TTag;
    function Script(const AScript: string): TTag;
    function BeginGroup: TTag;
    function GroupTitle(const ATitle: string): TTag;
    function GroupDescription(const ADesc: string): TTag;
    function EndGroup: TTag;
    function BeginDefs: TTag;
    function EndDefs: TTag;
    function BeginMetadata: TTag;
    function EndMetadata: TTag;
    function SourceComment(const AText: string): TTag;
    procedure SaveToFile(const AFileName: TFileName);
    property AsXML: string read GetAsXML;
    property LineBreak: string read FLineBreak write FLineBreak;
    property SVGVersion: string read FSvgVersion write FSvgVersion;
    property Width: string read FWidth write FWidth;
    property Height: string read FHeight write FHeight;
    property Stretch: Boolean read FStretch write FStretch;
    property AspectRatio: Double read GetAspectRatio;
    property ViewBox: TViewBox read FViewBox write FViewBox;
    property Language: string read FLanguage write FLanguage;
    property Title: string read FTitle write FTitle;
    property Description: string read FDescription write FDescription;
  end;

function CSS: TCSSBuilder;

function px(const AValue: Integer): string; overload;
function px(const AValue: Single): string; overload;
function px(const AValue: Double): string; overload;

implementation

uses
  Math, StrUtils, IOUtils, ASColors, ASNum;

type
  PTagKind = ^TTagKind;

var
  InvFS: TFormatSettings;

function AddNamespace(AAttribs: TDictionary<string, string>;
  const ANamespace: string): TArray<TPair<string, string>>; overload;
begin
  Result := AAttribs.ToArray;
  for var i := 0 to High(Result) do
    if not Result[i].Key.Contains(':') then
      Result[i].Key := ANamespace + ':' + Result[i].Key;
end;

function AddNamespace(AAttribs: TList<TPair<string, string>>;
  const ANamespace: string): TArray<TPair<string, string>>; overload;
begin
  Result := AAttribs.ToArray;
  for var i := 0 to High(Result) do
    if not Result[i].Key.Contains(':') then
      Result[i].Key := ANamespace + ':' + Result[i].Key;
end;

function AddNamespace(AAttribs: TArray<TPair<string, string>>;
  const ANamespace: string): TArray<TPair<string, string>>; overload;
begin
  Result := Copy(AAttribs);
  for var i := 0 to High(Result) do
    if not Result[i].Key.Contains(':') then
      Result[i].Key := ANamespace + ':' + Result[i].Key;
end;

function CSS: TCSSBuilder;
begin
  Result := Default(TCSSBuilder);
end;

function px(const AValue: Integer): string;
begin
  Result := AValue.ToString + 'px';
end;

function px(const AValue: Single): string;
begin
  Result := FormatFloat('0.###', AValue, InvFS) + 'px';
end;

function px(const AValue: Double): string;
begin
  Result := FormatFloat('0.###', AValue, InvFS) + 'px';
end;

function IntegerArrayToStringArray(const AValues: array of Integer): TArray<string>;
begin
  SetLength(Result, Length(AValues));
  for var i := 0 to High(AValues) do
    Result[i] := AValues[i].ToString;
end;

function SingleArrayToStringArray(const AValues: array of Single): TArray<string>;
begin
  SetLength(Result, Length(AValues));
  for var i := 0 to High(AValues) do
    Result[i] := AValues[i].ToString(InvFS);
end;

function PointToString(const APoint: TPoint): string; overload;
begin
  with APoint do
    Result := Format('%d,%d', [X, Y])
end;

function PointToString(const APoint: TPointF): string; overload;
begin
  with APoint do
    Result := Format('%g,%g', [X, Y], InvFS)
end;

function PointToString(const APoint: TPointD): string; overload;
begin
  with APoint do
    Result := Format('%g,%g', [X, Y], InvFS)
end;

function PointArrayToStringArray(const APoints: array of TPoint): TArray<string>; overload;
begin
  SetLength(Result, Length(APoints));
  for var i := 0 to High(APoints) do
    Result[i] := PointToString(APoints[i]);
end;

function PointArrayToStringArray(const APoints: array of TPointF): TArray<string>; overload;
begin
  SetLength(Result, Length(APoints));
  for var i := 0 to High(APoints) do
    Result[i] := PointToString(APoints[i]);
end;

function PointArrayToStringArray(const APoints: array of TPointD): TArray<string>; overload;
begin
  SetLength(Result, Length(APoints));
  for var i := 0 to High(APoints) do
    Result[i] := PointToString(APoints[i]);
end;

function ParsePointString(const Data: string): TPointD;
begin
  var CoordinateStrings := Data.Split([',']);
  if Length(CoordinateStrings) <> 2 then
    raise ESVGException.Create('Invalid point string.');
  if
    not TryStrToFloat(CoordinateStrings[0].Trim, Result.X, InvFS)
      or
    not TryStrToFloat(CoordinateStrings[1].Trim, Result.Y, InvFS)
  then
    raise ESVGException.Create('Invalid point string.');
end;

function SVGPointArrayParse(const Data: string): TArray<TPointD>;
begin
  var PointStrings := Data.Split([#32], TStringSplitOptions.ExcludeEmpty);
  SetLength(Result, Length(PointStrings));
  for var i := 0 to High(Result) do
    Result[i] := ParsePointString(PointStrings[i]);
end;

{ TTag }

procedure TTag.Append(const AOpName: string);
begin
  if Assigned(FSVG) then
  begin
    FSVG.AddTag(Self);
    if not AOpName.IsEmpty then
      FSVG.Define(AOpName);
  end;
end;

procedure TTag.AppendIf(const AOpName: string);
begin
  if Assigned(FSVG) then
    if FSVG.Undefine(AOpName) then
      FSVG.AddTag(Self);
end;

function TTag.Associate(ASVG: TSVGBuilder): TTag;
begin
  if not ASVG.FAbstract then
    FSVG := ASVG;
  Result := Self;
end;

function TTag.Attrib(const AName, AValue: string): TTag;
begin
  for var i := Low(FAttribs) to High(FAttribs) do
    if FAttribs[i].Key = AName then
    begin
      if AName = 'class' then
        FAttribs[i].Value := FAttribs[i].Value + IfThen(not FAttribs[i].Value.IsEmpty, #32) + AValue.Replace(#32, '-')
      else if AName = 'style' then
        FAttribs[i].Value := FAttribs[i].Value + IfThen(not FAttribs[i].Value.IsEmpty, '; ') + AValue.TrimRight([#32, ';'])
      else
        FAttribs[i].Value := AValue;
      Exit(Self);
    end;
  SetLength(FAttribs, Succ(Length(FAttribs)));
  FAttribs[High(FAttribs)].Key := AName;
  FAttribs[High(FAttribs)].Value := AValue;
  Result := Self;
end;

function TTag.AttribIf(const AName, AValue: string; ACondition: Boolean): TTag;
begin
  if ACondition then
    Result := Attrib(AName, AValue)
  else
    Result := Self;
end;

function TTag.Attrib(const AName: string; const AValue: Integer): TTag;
begin
  Result := Attrib(AName, AValue.ToString);
end;

function TTag.Attrib(const AName: string; const AValue: Single): TTag;
begin
  Result := Attrib(AName, AValue.ToString(InvFS));
end;

function TTag.Attrib(const AName: string; const AValue: Double): TTag;
begin
  Result := Attrib(AName, AValue.ToString(InvFS));
end;

function TTag.AttribIf(const AName: string; const AValue: Integer;
  ACondition: Boolean): TTag;
begin
  Result := AttribIf(AName, AValue.ToString, ACondition);
end;

function TTag.AttribIf(const AName: string; const AValue: Single;
  ACondition: Boolean): TTag;
begin
  Result := AttribIf(AName, AValue.ToString(InvFS), ACondition);
end;

function TTag.AttribIf(const AName: string; const AValue: Double;
  ACondition: Boolean): TTag;
begin
  Result := AttribIf(AName, AValue.ToString(InvFS), ACondition);
end;

function TTag.AttribIf(const AName, AValue: string): TTag;
begin
  Result := AttribIf(AName, AValue, not AValue.IsEmpty);
end;

function TTag.Attribs(AAttribs: TArray<TPair<string, string>>): TTag;
begin

  var i := Length(FAttribs);
  SetLength(FAttribs, Length(FAttribs) + Length(AAttribs));
  for var a in AAttribs do
  begin
    FAttribs[i].Key := a.Key;
    FAttribs[i].Value := a.Value;
    Inc(i);
  end;

  Result := Self;

end;

function TTag.Attribs(AAttribs: TList<TPair<string, string>>): TTag;
begin

  var i := Length(FAttribs);
  SetLength(FAttribs, Length(FAttribs) + AAttribs.Count);
  for var a in AAttribs do
  begin
    FAttribs[i].Key := a.Key;
    FAttribs[i].Value := a.Value;
    Inc(i);
  end;

  Result := Self;

end;

function TTag.Attribs(AAttribs: TDictionary<string, string>): TTag;
begin

  var i := Length(FAttribs);
  SetLength(FAttribs, Length(FAttribs) + AAttribs.Count);
  for var a in AAttribs do
  begin
    FAttribs[i].Key := a.Key;
    FAttribs[i].Value := a.Value;
    Inc(i);
  end;

  Result := Self;

end;

function TTag.A_Begin(const ABegin: string): TTag;
begin
  Result := AttribIf('begin', ABegin);
end;

function TTag.A_Begin(const ABegin: Integer): TTag;
begin
  Result := A_Begin(ABegin.ToString + 's');
end;

function TTag.A_Begin(const ABegin: Single): TTag;
begin
  Result := A_Begin(ABegin.ToString(InvFS) + 's');
end;

function TTag.A_End(const AEnd: string): TTag;
begin
  Result := AttribIf('end', AEnd);
end;

function TTag.A_End(const AEnd: Integer): TTag;
begin
  Result := A_End(AEnd.ToString + 's');
end;

function TTag.A_End(const AEnd: Single): TTag;
begin
  Result := A_End(AEnd.ToString(InvFS) + 's');
end;

function TTag.A_Duration(const ADuration: Single): TTag;
begin
  Result := A_Duration(ADuration.ToString(InvFS) + 's');
end;

function TTag.A_Duration(const ADuration: Integer): TTag;
begin
  Result := A_Duration(ADuration.ToString + 's');
end;

function TTag.A_Duration(const ADuration: string): TTag;
begin
  Result := AttribIf('dur', ADuration);
end;

function TTag.A_From(const AFrom: Single): TTag;
begin
  Result := A_From(AFrom.ToString(InvFS));
end;

function TTag.A_From(const AFrom: Integer): TTag;
begin
  Result := A_From(AFrom.ToString);
end;

function TTag.A_From(const AFrom: string): TTag;
begin
  Result := AttribIf('from', AFrom);
end;

function TTag.A_To(const ATo: Single): TTag;
begin
  Result := A_To(ATo.ToString(InvFS));
end;

function TTag.A_To(const ATo: Integer): TTag;
begin
  Result := A_To(ATo.ToString);
end;

function TTag.A_To(const ATo: string): TTag;
begin
  Result := AttribIf('To', ATo);
end;

function TTag.A_RepeatCount(const ARepeatCount: Integer): TTag;
begin
  Result := A_RepeatCount(ARepeatCount.ToString);
end;

function TTag.A_RepeatDuration(const ARepeatDuration: string): TTag;
begin
  Result := AttribIf('repeatDur', ARepeatDuration);
end;

function TTag.A_RepeatDuration(const ARepeatDuration: Integer): TTag;
begin
  Result := A_RepeatDuration(ARepeatDuration.ToString + 's');
end;

function TTag.A_RepeatDuration(const ARepeatDuration: Single): TTag;
begin
  Result := A_RepeatDuration(ARepeatDuration.ToString(InvFS) + 's');
end;

function TTag.A_RepeatIndefinitely: TTag;
begin
  Result := A_RepeatCount('indefinite');
end;

function TTag.A_Values(const AValues: array of Single): TTag;
begin
  Result := A_Values(string.Join(';', SingleArrayToStringArray(AValues)));
end;

function TTag.A_Values(const AValues: array of Integer): TTag;
begin
  Result := A_Values(string.Join(';', IntegerArrayToStringArray(AValues)));
end;

function TTag.A_Values(const AValues: string): TTag;
begin
  Result := AttribIf('values', AValues);
end;

function TTag.A_KeyTimes(const AKeyTimes: array of Single): TTag;
begin
  Result := A_KeyTimes(string.Join(';', SingleArrayToStringArray(AKeyTimes)));
end;

function TTag.A_KeyTimes(const AKeyTimes: array of Integer): TTag;
begin
  Result := A_KeyTimes(string.Join(';', IntegerArrayToStringArray(AKeyTimes)));
end;

function TTag.A_KeyTimes(const AKeyTimes: string): TTag;
begin
  Result := AttribIf('keyTimes', AKeyTimes);
end;

function TTag.A_RepeatCount(const ARepeatCount: string): TTag;
begin
  Result := AttribIf('repeatCount', ARepeatCount);
end;

function TTag.EndTag: TTag;
begin
  FKind := tkEnd;
  Result := Self;
end;

function TTag.Fill(const AFill: string): TTag;
begin
  Result := AttribIf('fill', AFill);
end;

function TTag.Fill(const AFill: TColor): TTag;
begin
  Result := Fill(ColorToHex(AFill));
end;

function TTag.FillIf(const AFill: TColor; ACondition: Boolean): TTag;
begin
  Result := AttribIf('fill', ColorToHex(AFill), ACondition);
end;

function TTag.FillIf(const AFill: string; ACondition: Boolean): TTag;
begin
  Result := AttribIf('fill', AFill, ACondition);
end;

function TTag.FillOpacity(const AFillOpacity: Single): TTag;
begin
  Result := FillOpacity(AFillOpacity.ToString(InvFS));
end;

function TTag.FontSize(const ASize: string): TTag;
begin
  Result := AttribIf('font-size', ASize);
end;

function TTag.FontSize(const ASize: Integer): TTag;
begin
  Result := FontSize(ASize.ToString);
end;

function TTag.FontStretch(const AStretch: string): TTag;
begin
  Result := AttribIf('font-stretch', AStretch);
end;

function TTag.FontStyle(const AStyle: string): TTag;
begin
  Result := AttribIf('font-style', AStyle);
end;

function TTag.FontVariant(const AVariant: string): TTag;
begin
  Result := AttribIf('font-variant', AVariant);
end;

function TTag.FontWeight(const AWeight: string): TTag;
begin
  Result := AttribIf('font-weight', AWeight);
end;

function TTag.Height(const AHeight: string): TTag;
begin
  Result := AttribIf('height', AHeight);
end;

function TTag.Height(const AHeight: Integer): TTag;
begin
  Result := AttribIf('height', AHeight.ToString);
end;

function TTag.Height(const AHeight: Single): TTag;
begin
  Result := AttribIf('height', AHeight.ToString(InvFS));
end;

function TTag.Height(const AHeight: Double): TTag;
begin
  Result := AttribIf('height', AHeight.ToString(InvFS));
end;

function TTag.Font(AFont: TFont): TTag;
begin
  if AFont = nil then
    Exit(Self);
  Result := FontFamily(AFont.Name);
  if AFont.Size > 0 then
    Result := Result.FontSize(AFont.Size.ToString + 'pt')
  else
    Result := Result.FontSize(AFont.Height);
  if AFont.Color <> clNone then
    Result := Result.Fill(AFont.Color);
  if fsItalic in AFont.Style then
    Result := Result.FontItalic;
  if fsBold in AFont.Style then
    Result := Result.FontBold;
end;

function TTag.FontBold: TTag;
begin
  Result := FontWeight('bold');
end;

function TTag.FontBolder: TTag;
begin
  Result := FontWeight('bolder');
end;

function TTag.FontFamily(const AFamilies: string): TTag;
begin
  Result := AttribIf('font-family', AFamilies);
end;

function TTag.FontItalic: TTag;
begin
  Result := FontStyle('italic');
end;

function TTag.FontLighter: TTag;
begin
  Result := FontWeight('lighter');
end;

function TTag.FontOblique: TTag;
begin
  Result := FontStyle('oblique');
end;

function TTag.FillOpacity(const AFillOpacity: string): TTag;
begin
  Result := AttribIf('fill-opacity', AFillOpacity);
end;

function XmlEscape(const S: string): string;
begin
  Result := S
    .Replace('''', '&apos;')
    .Replace('"', '&quot;')
    .Replace('&', '&amp;')
    .Replace('<', '&lt;')
    .Replace('>', '&gt;')
end;

function CdataEscape(const S: string): string;
begin
  Result := S.Replace(']]>', ']]]]><![CDATA[>')
end;

function TTag.ID(const AID: string): TTag;
begin
  Result := AttribIf('id', AID)
end;

class operator TTag.Implicit(const ATag: TTag): string;
begin
  if ATag.FKind = tkComment then
    Exit('<!-- ' + ATag.FContent.Replace('--', #$2013) + ' -->');
  if (ATag.FKind = tkOptionalWithChild) and ATag.FContent.IsEmpty then
    Exit('');
  Result := '<' + IfThen(ATag.FKind = tkEnd, '/') + ATag.FName;
  if ATag.FKind <> tkEnd then
    for var p in ATag.FAttribs do
      Result := Result + #32 + p.Key + '="' + XmlEscape(p.Value) + '"';
  case ATag.FKind of
    tkStart, tkEnd:
      Result := Result + '>';
    tkSelfClose:
      Result := Result + ' />';
    tkWithChild, tkOptionalWithChild:
      case ATag.FContentType of
        ctText:
          Result := Result + '>' + XmlEscape(ATag.FContent) + '</' + ATag.FName + '>';
        ctXML:
          Result := Result + '>' + ATag.FContent + '</' + ATag.FName + '>';
      end;
    tkWithCDATA:
      Result := Result + '><![CDATA[' + CdataEscape(ATag.FContent) + ']]></' + ATag.FName + '>';
  else
    raise Exception.Create('Invalid tag kind.');
  end;
end;

function MarkerAttribValue(const S: string): string;
begin
  if S.IsEmpty or (S = 'none') or (S = 'inherit') or (S.StartsWith('url(#')) then
    Result := S
  else
    Result := 'url(#' + S + ')'
end;

function TTag.MarkerEnd(const AMarker: string): TTag;
begin
  Result := AttribIf('marker-end', MarkerAttribValue(AMarker))
end;

function TTag.MarkerMid(const AMarker: string): TTag;
begin
  Result := AttribIf('marker-mid', MarkerAttribValue(AMarker))
end;

function TTag.MarkerStart(const AMarker: string): TTag;
begin
  Result := AttribIf('marker-start', MarkerAttribValue(AMarker))
end;

function TTag.Name(const ATagName: string): TTag;
begin
  FName := ATagName;
  Result := Self;
end;

function TTag.Opacity(const AOpacity: Single): TTag;
begin
  Result := Opacity(AOpacity.ToString(InvFS));
end;

function TTag.RescalePoints(const XFactor, YFactor: Double): TTag;
begin
  Result := Clone;
  with Result do
    for var i := 0 to High(FAttribs) do
      if FAttribs[i].Key = 'points' then
      begin
        var LPoints := SVGPointArrayParse(FAttribs[i].Value);
        for var j := 0 to High(LPoints) do
        begin
          LPoints[j].x := XFactor * LPoints[j].x;
          LPoints[j].y := YFactor * LPoints[j].y;
        end;
        FAttribs[i].Value := string.Join(#32, PointArrayToStringArray(LPoints));
        Break;
      end
      else if FAttribs[i].Key = 'rx' then
        FAttribs[i].Value := (StrToFloat(FAttribs[i].Value, InvFS) * XFactor).ToString(InvFS)
      else if FAttribs[i].Key = 'ry' then
        FAttribs[i].Value := (StrToFloat(FAttribs[i].Value, InvFS) * YFactor).ToString(InvFS)
      else if (FAttribs[i].Key = 'r') and SameValue(XFactor, YFactor) then
        FAttribs[i].Value := (StrToFloat(FAttribs[i].Value, InvFS) * XFactor).ToString(InvFS)
      else if FAttribs[i].Key = 'r' then
      begin
        var Radius := StrToFloat(FAttribs[i].Value, InvFS);
        FAttribs[i].Key := 'rx';
        FAttribs[i].Value := (Radius * XFactor).ToString(InvFS);
        FName := 'ellipse';
        Exit(Attrib('ry', (Radius * YFactor).ToString(InvFS)));
      end;
end;

function TTag.Opacity(const AOpacity: string): TTag;
begin
  Result := AttribIf('opacity', AOpacity);
end;

function TTag.SelfClosed: TTag;
begin
  FKind := tkSelfClose;
  Result := Self;
end;

function TTag.StartTag: TTag;
begin
  FKind := tkStart;
  Result := Self;
end;

function TTag.Stroke(const AStroke: string): TTag;
begin
  Result := AttribIf('stroke', AStroke);
end;

function TTag.Stroke(const AStroke: TColor): TTag;
begin
  Result := Stroke(ColorToHex(AStroke));
end;

function TTag.StrokeIf(const AStroke: TColor; ACondition: Boolean): TTag;
begin
  Result := StrokeIf(ColorToHex(AStroke), ACondition);
end;

function TTag.StrokeIf(const AStroke: string; ACondition: Boolean): TTag;
begin
  Result := AttribIf('stroke', AStroke, ACondition);
end;

function TTag.StrokeWidth(const AStrokeWidth: Single): TTag;
begin
  Result := StrokeWidth(AStrokeWidth.ToString(InvFS));
end;

function TTag.StrokeWidthPx(const AStrokeWidth: Single): TTag;
begin
  Result := StrokeWidth(px(AStrokeWidth));
end;

function TTag.StrokeWidthPx(const AStrokeWidth: Integer): TTag;
begin
  Result := StrokeWidth(px(AStrokeWidth));
end;

function TTag.StrokeWidth(const AStrokeWidth: Integer): TTag;
begin
  Result := StrokeWidth(AStrokeWidth.ToString);
end;

function TTag.StrokeWidth(const AStrokeWidth: string): TTag;
begin
  Result := AttribIf('stroke-width', AStrokeWidth);
end;

function TTag.ContentText(const AContent: string): TTag;
begin
  if FKind <> tkOptionalWithChild then
    FKind := tkWithChild;
  FContent := AContent;
  FContentType := ctText;
  Result := Self;
end;

function TTag.ContentXML(const AContent: string): TTag;
begin
  if FKind <> tkOptionalWithChild then
    FKind := tkWithChild;
  FContent := AContent;
  FContentType := ctXML;
  Result := Self;
end;

function TTag.DominantBaseline(AAnchor: TVerticalAlignment): TTag;
const
  BaselineValues: array[TVerticalAlignment] of string
    = ('text-before-edge', 'text-after-edge', 'central');
begin
  Result := DominantBaseline(BaselineValues[AAnchor]);
end;

function TTag.DominantBaseline(const AAnchor: string): TTag;
begin
  Result := AttribIf('dominant-baseline', AAnchor);
end;

function TTag.CDATA(const AContent: string): TTag;
begin
  FKind := tkWithCDATA;
  FContent := AContent;
  Result := Self;
end;

function TTag.&Class(const AClassName: string): TTag;
begin
  Result := AttribIf('class', AClassName)
end;

function TTag.Clone: TTag;
begin
  Result := Default(TTag);
  Result.FKind := Self.FKind;
  Result.FName := Self.FName;
  Result.FAttribs := Copy(Self.FAttribs);
  Result.FContent := Self.FContent;
  Result.FContentType := Self.FContentType;
  Result.FSVG := Self.FSVG;
end;

function TTag.Style(const AStyle: string): TTag;
begin
  Result := AttribIf('style', AStyle)
end;

function TTag.TextAnchor(const AAnchor: string): TTag;
begin
  Result := AttribIf('text-anchor', AAnchor);
end;

function TTag.Transform(const ATransform: TTransform): TTag;
begin
  Result := AttribIf('transform', ATransform);
end;

function TTag.TransformIf(const ATransform: TTransform;
  ACondition: Boolean): TTag;
begin
  Result := AttribIf('transform', ATransform, ACondition);
end;

function TTag.Width(const AWidth: string): TTag;
begin
  Result := AttribIf('width', AWidth);
end;

function TTag.Width(const AWidth: Integer): TTag;
begin
  Result := AttribIf('width', AWidth.ToString);
end;

function TTag.Width(const AWidth: Single): TTag;
begin
  Result := AttribIf('width', AWidth.ToString(InvFS));
end;

function TTag.Width(const AWidth: Double): TTag;
begin
  Result := AttribIf('width', AWidth.ToString(InvFS));
end;

function TTag.x(const Ax: string): TTag;
begin
  Result := AttribIf('x', Ax);
end;

function TTag.x(const Ax: Integer): TTag;
begin
  Result := AttribIf('x', Ax.ToString);
end;

function TTag.x(const Ax: Single): TTag;
begin
  Result := AttribIf('x', Ax.ToString(InvFS));
end;

function TTag.x(const Ax: Double): TTag;
begin
  Result := AttribIf('x', Ax.ToString(InvFS));
end;

function TTag.y(const Ay: string): TTag;
begin
  Result := AttribIf('y', Ay);
end;

function TTag.y(const Ay: Integer): TTag;
begin
  Result := AttribIf('y', Ay.ToString);
end;

function TTag.y(const Ay: Single): TTag;
begin
  Result := AttribIf('y', Ay.ToString(InvFS));
end;

function TTag.y(const Ay: Double): TTag;
begin
  Result := AttribIf('y', Ay.ToString(InvFS));
end;

function TTag.TextAnchor(AAnchor: TAlignment): TTag;
const
  Anchors: array[TAlignment] of string = ('start', 'end', 'middle');
begin
  Result := TextAnchor(Anchors[AAnchor]);
end;

function TTag.FontSize(const ASize: Single): TTag;
begin
  Result := FontSize(ASize.ToString(InvFS));
end;

function TTag.StrokeWidth(const AStrokeWidth: Double): TTag;
begin
  Result := StrokeWidth(AStrokeWidth.ToString(InvFS));
end;

function TTag.StrokeWidthIf(const AStrokeWidth: Integer;
  ACondition: Boolean): TTag;
begin
  Result := StrokeWidthIf(AStrokeWidth.ToString, ACondition);
end;

function TTag.StrokeWidthIf(const AStrokeWidth: string;
  ACondition: Boolean): TTag;
begin
  Result := AttribIf('stroke-width', AStrokeWidth, ACondition);
end;

function TTag.StrokeWidthIf(const AStrokeWidth: Single;
  ACondition: Boolean): TTag;
begin
  Result := StrokeWidthIf(AStrokeWidth.ToString(InvFS), ACondition);
end;

function TTag.StrokeWidthIf(const AStrokeWidth: Double;
  ACondition: Boolean): TTag;
begin
  Result := StrokeWidthIf(AStrokeWidth.ToString(InvFS), ACondition);
end;

{ TSVGBuilder }

function TSVGBuilder.Circle(const x, y, r: string): TTag;
begin
  Result := Tag('circle')
    .AttribIf('cx', x)
    .AttribIf('cy', y)
    .AttribIf('r', r)
end;

function TSVGBuilder.Circle(const x, y, r: Integer): TTag;
begin
  Result := Circle(x.ToString, y.ToString, r.ToString);
end;

function TSVGBuilder.Circle(const x, y, r: Single): TTag;
begin
  Result := Circle(x.ToString(InvFS), y.ToString(InvFS), r.ToString(InvFS));
end;

function TSVGBuilder.Circle(const x: TPoint; const r: Integer): TTag;
begin
  Result := Circle(x.X, x.Y, r);
end;

function TSVGBuilder.Circle(const x, y, r: Double): TTag;
begin
  Result := Circle(x.ToString(InvFS), y.ToString(InvFS), r.ToString(InvFS));
end;

constructor TSVGBuilder.Create(AAbstract: Boolean = False);
begin
  FLineBreak := #13#10;
  FSvgVersion := '1.1';
  FWidth := '10cm';
  FHeight := '10cm';
  FParts := TList<string>.Create;
  FStack := TStack<TNodeType>.Create;
  FDefines := TDictionary<string, Pointer>.Create;
  FDefs := TDictionary<string, string>.Create;
  FNamespaces := TDictionary<string, string>.Create;
  FAbstract := AAbstract;
end;

procedure TSVGBuilder.Define(const AName: string);
begin
  FDefines.AddOrSetValue(AName, nil);
end;

procedure TSVGBuilder.DefineNamespace(const APrefix, AURL: string);
begin
  if FNamespaces.ContainsKey(APrefix) then
    raise ESVGException.CreateFmt('XML namespace "%s" redeclared.', [APrefix]);
  FNamespaces.Add(APrefix, AURL);
end;

function TSVGBuilder.DefMarker(const ID, AViewBox, ARefX, ARefY, AMarkerWidth,
  AMarkerHeight: string; const AStrokeWidth: Boolean): TTag;
begin
  if not FDefID.IsEmpty then
    StructureError;
  FDefs.Add(ID, ''); // raises on conflict
  FDefID := ID;
  Result := BeginMarker(AViewBox, ARefX, ARefY, AMarkerWidth, AMarkerHeight, AStrokeWidth).ID(ID);
end;

function TSVGBuilder.DefMarker(const ID, AViewBox: string; ARefX, ARefY,
  AMarkerWidth, AMarkerHeight: Integer; const AStrokeWidth: Boolean): TTag;
begin
  if not FDefID.IsEmpty then
    StructureError;
  FDefs.Add(ID, ''); // raises on conflict
  FDefID := ID;
  Result := BeginMarker(AViewBox, ARefX, ARefY, AMarkerWidth, AMarkerHeight, AStrokeWidth).ID(ID);
end;

function TSVGBuilder.DefMarker(const ID, AViewBox: string; ARefX, ARefY,
  AMarkerWidth, AMarkerHeight: Single; const AStrokeWidth: Boolean): TTag;
begin
  if not FDefID.IsEmpty then
    StructureError;
  FDefs.Add(ID, ''); // raises on conflict
  FDefID := ID;
  Result := BeginMarker(AViewBox, ARefX, ARefY, AMarkerWidth, AMarkerHeight, AStrokeWidth).ID(ID);
end;

function TSVGBuilder.DefMarker(const ID, AViewBox: string; ARefX, ARefY,
  AMarkerWidth, AMarkerHeight: Double; const AStrokeWidth: Boolean): TTag;
begin
  if not FDefID.IsEmpty then
    StructureError;
  FDefs.Add(ID, ''); // raises on conflict
  FDefID := ID;
  Result := BeginMarker(AViewBox, ARefX, ARefY, AMarkerWidth, AMarkerHeight, AStrokeWidth).ID(ID);
end;

function TSVGBuilder.DefSymbol(const ID, AViewBox: string): TTag;
begin
  if not FDefID.IsEmpty then
    StructureError;
  FDefs.Add(ID, ''); // raises on conflict
  FDefID := ID;
  Result := BeginSymbol(AViewBox).ID(ID);
end;

destructor TSVGBuilder.Destroy;
begin
  FNamespaces.Free;
  FDefs.Free;
  FDefines.Free;
  FStack.Free;
  FParts.Free;
  inherited;
end;

function TSVGBuilder.GetAspectRatio: Double;
const
  Units: array[0..8] of string = ('em', 'ex', 'px', 'in', 'cm', 'mm', 'pt', 'pc', '%');
var
  WUnit, HUnit: string;
  W, H: string;
  WValue, HValue: Double;
begin
  W := Width.Trim;
  for var U in Units do
    if W.EndsWith(U, True) then
    begin
      Delete(W, W.Length - U.Length + 1, U.Length);
      WUnit := U;
      Break;
    end;
  H := Height.Trim;
  for var U in Units do
    if H.EndsWith(U, True) then
    begin
      Delete(H, H.Length - U.Length + 1, U.Length);
      HUnit := U;
      Break;
    end;
  W := W.Trim;
  H := H.Trim;
  if
    (WUnit = HUnit) and (WUnit <> '%') and
    TryStrToFloat(W, WValue, InvFS) and
    TryStrToFloat(H, HValue, InvFS) and
    (WValue > 0.0) and (HValue > 0.0)
  then
    Result := WValue / HValue
  else
    raise ESVGException.Create('Couldn''t determine aspect ratio.');
end;

function TSVGBuilder.GetAsXML: string;
var
  Lines: TList<string>;

  procedure Line(const AText: string);
  begin
    for var l in AText.Split([#13#10]) do
      Lines.Add(l);
  end;

  procedure OptionalLine(const AText: string);
  begin
    if not AText.IsEmpty then
      Lines.Add(AText);
  end;

begin

  if FStack.Count > 0 then
    StructureError;

  if not FDefID.IsEmpty then
    StructureError;

  Lines := TList<string>.Create;
  try

    Line('<?xml version="1.0" encoding="UTF-8" standalone="no"?>');

    Line(
      StartTag('svg')
        .Attrib('xmlns', 'http://www.w3.org/2000/svg')
        .Attrib('xmlns:xlink', 'http://www.w3.org/1999/xlink')
        .AttribIf('version', FSvgVersion)
        .AttribIf('xml:lang', FLanguage)
        .AttribIf('width', FWidth)
        .AttribIf('height', FHeight)
        .AttribIf('preserveAspectRatio', 'none', FStretch)
        .AttribIf('viewBox', FViewBox)
        .Attribs(AddNamespace(FNamespaces, 'xmlns'))
    );

    OptionalLine(OptionalTag('title').ContentText(FTitle));
    OptionalLine(OptionalTag('desc').ContentText(FDescription));

    if FDefs.Count > 0 then
    begin
      Line(BeginDefs);
      for var s in FDefs do
        OptionalLine(s.Value);
      Line(EndDefs);
    end;

    for var s in FParts do
      OptionalLine(s);

    Line(EndTag('svg'));

    Result := string.Join(FLineBreak, Lines.ToArray);

  finally
    Lines.Free;
  end;

  if FStack.Count > 0 then
    StructureError;

end;

function TSVGBuilder.GroupDescription(const ADesc: string): TTag;
begin
  Result := OptionalTag('desc').ContentText(ADesc);
end;

function TSVGBuilder.GroupTitle(const ATitle: string): TTag;
begin
  Result := OptionalTag('title').ContentText(ATitle);
end;

function TSVGBuilder.HasID(const ID: string): Boolean;
begin
  Result := FDefs.ContainsKey(ID);
end;

function TSVGBuilder.Image(const x: TPoint; const s: TSize;
  const ref: string): TTag;
begin
  Result := Image(x.X, x.Y, s.Width, s.Height, ref);
end;

function TSVGBuilder.Image(const x, y, w, h: Single; const ref: string): TTag;
begin
  Result := Image(x.ToString(InvFS), y.ToString(InvFS), w.ToString(InvFS),
    h.ToString(InvFS), ref)
end;

function TSVGBuilder.Image(const x, y, w, h: Integer; const ref: string): TTag;
begin
  Result := Image(x.ToString, y.ToString, w.ToString, h.ToString, ref)
end;

function TSVGBuilder.Image(const x, y, w, h, ref: string): TTag;
begin
  Result := Tag('image')
    .AttribIf('x', x)
    .AttribIf('y', y)
    .AttribIf('width', w)
    .AttribIf('height', h)
    .AttribIf('xlink:href', ref)
end;

function TSVGBuilder.Line(const x1, y1, x2, y2: Single): TTag;
begin
  Result := Line(x1.ToString(InvFS), y1.ToString(InvFS),
    x2.ToString(InvFS), y2.ToString(InvFS));
end;

function TSVGBuilder.Line(const x1, y1, x2, y2: Integer): TTag;
begin
  Result := Line(x1.ToString, y1.ToString, x2.ToString, y2.ToString);
end;

function TSVGBuilder.Line(const x1, y1, x2, y2: string): TTag;
begin
  Result := Tag('line')
    .AttribIf('x1', x1)
    .AttribIf('y1', y1)
    .AttribIf('x2', x2)
    .AttribIf('y2', y2)
end;

function TSVGBuilder.Tag(const ATagName: string = ''): TTag;
begin
  Result := Default(TTag).Name(ATagName).Associate(Self).SelfClosed;
end;

function TSVGBuilder.Text(const x, y: Single; const S: string): TTag;
begin
  Result := Text(x.ToString(InvFS), y.ToString(InvFS), S);
end;

function TSVGBuilder.Text(const x, y: Integer; const S: string): TTag;
begin
  Result := Text(x.ToString, y.ToString, S);
end;

function TSVGBuilder.Text(const x, y, S: string): TTag;
begin
  Result := Tag('text')
    .AttribIf('x', x)
    .AttribIf('y', y)
    .ContentText(S)
end;

function TSVGBuilder.OptionalTag(const ATagName: string = ''): TTag;
begin
  Result := Default(TTag).Name(ATagName).Associate(Self);
  PTagKind(@Result)^ := tkOptionalWithChild;
end;

function TSVGBuilder.Polygon(const APoints: string): TTag;
begin
  Result := Tag('polygon')
    .AttribIf('points', APoints)
end;

function TSVGBuilder.Polygon(const APoints: array of TPoint): TTag;
begin
  Result := Polygon(string.Join(#32, PointArrayToStringArray(APoints)));
end;

function TSVGBuilder.Path(const d: string; const APathLength: Single): TTag;
begin
  Result := Tag('path')
    .AttribIf('d', d)
    .AttribIf('pathLength', APathLength.ToString(InvFS), APathLength <> 0.0)
end;

function TSVGBuilder.Polygon(const APoints: array of TPointF): TTag;
begin
  Result := Polygon(string.Join(#32, PointArrayToStringArray(APoints)));
end;

function TSVGBuilder.PolyLine(const APoints: array of TPointF): TTag;
begin
  Result := PolyLine(string.Join(#32, PointArrayToStringArray(APoints)));
end;

function TSVGBuilder.Rect(const R: TRectF): TTag;
begin
  Result := Rect(R.Left.ToString(InvFS), R.Top.ToString(InvFS),
    R.Width.ToString(InvFS), R.Height.ToString(InvFS));
end;

function TSVGBuilder.Rect(const R: TRect): TTag;
begin
  Result := Rect(R.Left.ToString, R.Top.ToString, R.Width.ToString, R.Height.ToString);
end;

function TSVGBuilder.Rect(const x, y, w, h: string): TTag;
begin
  Result := Tag('rect')
    .AttribIf('x', x)
    .AttribIf('y', y)
    .AttribIf('width', w)
    .AttribIf('height', h)
end;

function TSVGBuilder.PolyLine(const APoints: array of TPoint): TTag;
begin
  Result := PolyLine(string.Join(#32, PointArrayToStringArray(APoints)));
end;

function TSVGBuilder.PolyLine(const APoints: string): TTag;
begin
  Result := Tag('polyline')
    .AttribIf('points', APoints);
end;

procedure TSVGBuilder.SaveToFile(const AFileName: TFileName);
begin
  TFile.WriteAllText(AFileName, AsXML, TEncoding.UTF8);
end;

function TSVGBuilder.Script(const AScript: string): TTag;
begin
  Result := Tag('script').Attrib('type', 'text/javascript')
    .CDATA(FLineBreak + AScript + FLineBreak);
end;

function TSVGBuilder.Sector(const X: TPointD; const rx, ry, t0,
  t1: Double): TTag;
begin
  Result := Sector(X.X, X.Y, rx, ry, t0, t1);
end;

function ArcDist(a, b: Double): Double;
begin
  a := rmod(a, TwoPi);
  b := rmod(b, TwoPi);
  if b = a then
    Result := 0
  else if a < b then
    Result := b - a
  else
    Result := TwoPi - a + b;
end;

function TSVGBuilder.Sector(const x, y, rx, ry, t0, t1: Double): TTag;
begin
  var P := TPointD.Create(x + rx*cos(t0), y - ry*sin(t0));
  var Q := TPointD.Create(x + rx*cos(t1), y - ry*sin(t1));
  var d := Format('M%g,%g L%g,%g A%g,%g 0 %d,%d %g,%g Z',
    [x, y, P.X, P.Y, rx, ry, Ord(ArcDist(t0, t1) >= Pi), 0, Q.X, Q.Y], InvFS);
  Result := Path(d);
end;

function TSVGBuilder.SourceComment(const AText: string): TTag;
begin
  Result := XmlComment(AText);
end;

function TSVGBuilder.StartTag(const ATagName: string = ''): TTag;
begin
  Result := Default(TTag).Name(ATagName).Associate(Self).StartTag;
end;

procedure TSVGBuilder.StructureError;
begin
  raise ESVGException.Create('Invalid SVG structure.');
end;

function TSVGBuilder.Style(const CSS: string): TTag;
begin
  Result := Tag('style').Attrib('type', 'text/css')
    .CDATA(FLineBreak + CSS + FLineBreak);
end;

procedure TSVGBuilder.AddTag(const ATag: TTag);
begin
  if not FDefID.IsEmpty then
  begin
    var s: string;
    if FDefs.TryGetValue(FDefID, s) then
    begin
      s := s + IfThen(not s.IsEmpty, #13#10) + ATag;
      FDefs[FDefID] := s;
    end
    else
      StructureError
  end
  else
    FParts.Add(ATag);
end;

function TSVGBuilder.Ellipse(const x, y, rx, ry: string): TTag;
begin
  Result := Tag('ellipse')
    .AttribIf('cx', x)
    .AttribIf('cy', y)
    .AttribIf('rx', rx)
    .AttribIf('ry', ry)
end;

function TSVGBuilder.Ellipse(const x, y, rx, ry: Integer): TTag;
begin
  Result := Ellipse(x.ToString, y.ToString,
    rx.ToString, ry.ToString);
end;

function TSVGBuilder.Ellipse(const x, y, rx, ry: Single): TTag;
begin
  Result := Ellipse(x.ToString(InvFS), y.ToString(InvFS),
    rx.ToString(InvFS), ry.ToString(InvFS));
end;

function TSVGBuilder.Ellipse(const x: TPoint; const rx, ry: Integer): TTag;
begin
  Result := Ellipse(x.X, x.Y, rx, ry);
end;

function TSVGBuilder.Ellipse(const x: TPointF; const rx, ry: Single): TTag;
begin
  Result := Ellipse(x.X, x.Y, rx, ry);
end;

procedure TSVGBuilder.EndDefinition;
begin
  FDefID := '';
end;

procedure TSVGBuilder.EndDefMarker;
begin
  EndMarker.Append;
  EndDefinition;
end;

function TSVGBuilder.EndDefs: TTag;
begin
  Result := EndTag('defs');
  if FStack.Pop <> ntDefs then
    StructureError;
end;

procedure TSVGBuilder.EndDefSymbol;
begin
  EndSymbol.Append;
  EndDefinition;
end;

function TSVGBuilder.EndGroup: TTag;
begin
  Result := EndTag('g');
  if FStack.Pop <> ntGroup then
    StructureError;
end;

function TSVGBuilder.EndMarker: TTag;
begin
  Result := EndTag('marker');
  if FStack.Pop <> ntMarker then
    StructureError;
end;

function TSVGBuilder.EndMetadata: TTag;
begin
  Result := EndTag('metadata');
  if FStack.Pop <> ntMetadata then
    StructureError;
end;

function TSVGBuilder.EndSwitch: TTag;
begin
  Result := EndTag('switch');
  if FStack.Pop <> ntSwitch then
    StructureError;
end;

function TSVGBuilder.EndSymbol: TTag;
begin
  Result := EndTag('symbol');
  if FStack.Pop <> ntSymbol then
    StructureError;
end;

function TSVGBuilder.EndTag(const ATagName: string = ''): TTag;
begin
  Result := Default(TTag).Name(ATagName).Associate(Self).EndTag;
end;

function TSVGBuilder.XmlComment(const AComment: string = ''): TTag;
begin
  Result := Default(TTag).Associate(Self).ContentText(AComment);
  PTagKind(@Result)^ := tkComment;
end;

function TSVGBuilder.Animate(const AAttributeName, AAttributeType: string): TTag;
begin
  Result := Tag('animate')
    .AttribIf('attributeType', AAttributeType)
    .AttribIf('attributeName', AAttributeName);
end;

function TSVGBuilder.AnimateMotion(const APathData, ARotate: string): TTag;
begin
  Result := Tag('animateMotion')
    .AttribIf('path', APathData)
    .AttribIf('rotate', ARotate);
end;

function TSVGBuilder.AnimateSet(const AAttributeName, ATo: string): TTag;
begin
  Result := Tag('set')
    .AttribIf('attributeName', AAttributeName)
    .AttribIf('to', ATo)
end;

function TSVGBuilder.AnimateTransform(const AType: TTransformType): TTag;
const
  TransformTypes: array[TTransformType] of string = ('translate', 'scale',
    'rotate', 'skewX', 'skewY');
begin
  if not InRange(Ord(AType), Ord(Low(TTransformType)), Ord(High(TTransformType))) then
    raise ESVGException.Create('Invalid transform type.');
  Result := Tag('animateTransform')
    .Attrib('attributeName', 'transform')
    .Attrib('attributeType', 'XML')
    .Attrib('type', TransformTypes[AType])
end;

function TSVGBuilder.Arc(const X: TPointD; const rx, ry, t0, t1: Double): TTag;
begin
  Result := Arc(X.X, X.Y, rx, ry, t0, t1);
end;

function TSVGBuilder.Arc(const x, y, rx, ry, t0, t1: Double): TTag;
begin
  var P := TPointD.Create(x + rx*cos(t0), y - ry*sin(t0));
  var Q := TPointD.Create(x + rx*cos(t1), y - ry*sin(t1));
  var d := Format('M%g,%g A%g,%g 0 %d,%d %g,%g',
    [P.X, P.Y, rx, ry, Ord(ArcDist(t0, t1) >= Pi), 0, Q.X, Q.Y], InvFS);
  Result := Path(d);
end;

function TSVGBuilder.BeginDefs: TTag;
begin
  FStack.Push(ntDefs);
  Result := StartTag('defs');
end;

function TSVGBuilder.BeginGroup: TTag;
begin
  FStack.Push(ntGroup);
  Result := StartTag('g')
end;

function TSVGBuilder.BeginMarker(const AViewBox: string; ARefX, ARefY,
  AMarkerWidth, AMarkerHeight: Integer; const AStrokeWidth: Boolean): TTag;
begin
  Result := BeginMarker(AViewBox, ARefX.ToString, ARefY.ToString,
    AMarkerWidth.ToString, AMarkerHeight.ToString, AStrokeWidth)
end;

function TSVGBuilder.BeginMarker(const AViewBox, ARefX, ARefY, AMarkerWidth,
  AMarkerHeight: string; const AStrokeWidth: Boolean): TTag;
const
  MarkerUnits: array[Boolean] of string = ('userSpaceOnUse', 'strokeWidth');
begin
  FStack.Push(ntMarker);
  Result := StartTag('marker')
    .AttribIf('viewBox', AViewBox)
    .AttribIf('refX', ARefX)
    .AttribIf('refY', ARefY)
    .Attrib  ('markerUnits', MarkerUnits[AStrokeWidth])
    .AttribIf('markerWidth', AMarkerWidth)
    .AttribIf('markerHeight', AMarkerHeight)
end;

function TSVGBuilder.BeginSwitch: TTag;
begin
  FStack.Push(ntSwitch);
  Result := StartTag('switch');
end;

function TSVGBuilder.BeginSymbol(const AViewBox: string): TTag;
begin
  FStack.Push(ntSymbol);
  Result := StartTag('symbol')
    .AttribIf('viewBox', AViewBox)
end;

function TSVGBuilder.Circle(const x: TPointF; const r: Single): TTag;
begin
  Result := Circle(x.X, x.Y, r);
end;

function TSVGBuilder.Circle(const x, y: Single; const r: string): TTag;
begin
  Result := Circle(x.ToString(InvFS), y.ToString(InvFS), r);
end;

function TSVGBuilder.Line(const P, Q: TPoint): TTag;
begin
  Result := Line(P.X, P.Y, Q.X, Q.Y);
end;

function TSVGBuilder.Line(const P, Q: TPointF): TTag;
begin
  Result := Line(P.X, P.Y, Q.X, Q.Y);
end;

function TSVGBuilder.Link(const ref: string): TTag;
begin
  Result := Tag('a').AttribIf('xlink:href', ref);
end;

function TSVGBuilder.MPath(const APathName: string): TTag;
begin
  Result := Tag('mpath').Attrib('xlink:href', '#' + APathName)
end;

function TSVGBuilder.Text(const x: TPointF; const S: string): TTag;
begin
  Result := Text(x.X, x.Y, S);
end;

function TSVGBuilder.Text(const x: TPointD; const S: string): TTag;
begin
  Result := Text(x.X, x.Y, S);
end;

function TSVGBuilder.Text(const S: string): TTag;
begin
  Result := Tag('text')
    .ContentText(S)
end;

function TSVGBuilder.Text(const x, y: Double; const S: string): TTag;
begin
  Result := Text(x.ToString(InvFS), y.ToString(InvFS), S);
end;

function TSVGBuilder.TextPath(const APathRef, S, AStartOffset: string): TTag;
begin
  Result := Tag('textPath')
    .AttribIf('xlink:href', '#' + APathRef, not APathRef.IsEmpty)
    .AttribIf('startOffset', AStartOffset)
    .ContentText(S)
end;

function TSVGBuilder.TextSpan(const S: string): TTag;
begin
  Result := Tag('tspan').ContentText(S);
end;

function TSVGBuilder.Use(const x, y, w, h: Integer; const ref: string): TTag;
begin
  Result := Use(x.ToString, y.ToString, w.ToString, h.ToString, ref)
end;

function TSVGBuilder.Use(const x, y, w, h, ref: string): TTag;
begin
  Result := Tag('use')
    .AttribIf('x', x)
    .AttribIf('y', y)
    .AttribIf('width', w)
    .AttribIf('height', h)
    .AttribIf('xlink:href', ref);
end;

function TSVGBuilder.Text(const x: TPoint; const S: string): TTag;
begin
  Result := Text(x.X, x.Y, S);
end;

function TSVGBuilder.Use(const x, y, w, h: Single; const ref: string): TTag;
begin
  Result := Use(x.ToString(InvFS), y.ToString(InvFS), w.ToString(InvFS),
    h.ToString(InvFS), ref)
end;

function TSVGBuilder.Image(const x: TPointF; const s: TSizeF;
  const ref: string): TTag;
begin
  Result := Image(x.X, x.Y, s.Width, s.Height, ref);
end;

function TSVGBuilder.Image(const x, y, w, h: Double; const ref: string): TTag;
begin
  Result := Image(x.ToString(InvFS), y.ToString(InvFS), w.ToString(InvFS),
    h.ToString(InvFS), ref)
end;

function TSVGBuilder.BeginMarker(const AViewBox: string; ARefX, ARefY,
  AMarkerWidth, AMarkerHeight: Single; const AStrokeWidth: Boolean): TTag;
begin
  Result := BeginMarker(AViewBox, ARefX.ToString(InvFS), ARefY.ToString(InvFS),
    AMarkerWidth.ToString(InvFS), AMarkerHeight.ToString(InvFS), AStrokeWidth)
end;

function TSVGBuilder.BeginMetadata: TTag;
begin
  FStack.Push(ntMetadata);
  Result := StartTag('metadata');
end;

function TSVGBuilder.Polygon(const APoints: array of TPointD): TTag;
begin
  Result := Polygon(string.Join(#32, PointArrayToStringArray(APoints)));
end;

function TSVGBuilder.PolyLine(const APoints: array of TPointD): TTag;
begin
  Result := PolyLine(string.Join(#32, PointArrayToStringArray(APoints)));
end;

function TSVGBuilder.Rect(const R: TRectD): TTag;
begin
  Result := Rect(R.Left.ToString(InvFS), R.Top.ToString(InvFS),
    R.Width.ToString(InvFS), R.Height.ToString(InvFS));
end;

function TSVGBuilder.Rect(const x, y, w, h: Single): TTag;
begin
  Result := Rect(x.ToString(InvFS), y.ToString(InvFS),
    w.ToString(InvFS), h.ToString(InvFS));
end;

function TSVGBuilder.Rect(const x, y, w, h: Double): TTag;
begin
  Result := Rect(x.ToString(InvFS), y.ToString(InvFS),
    w.ToString(InvFS), h.ToString(InvFS));
end;

function TSVGBuilder.Line(const P, Q: TPointD): TTag;
begin
  Result := Line(P.X, P.Y, Q.X, Q.Y);
end;

function TSVGBuilder.Line(const x1, y1, x2, y2: Double): TTag;
begin
  Result := Line(x1.ToString(InvFS), y1.ToString(InvFS),
    x2.ToString(InvFS), y2.ToString(InvFS));
end;

function TSVGBuilder.Ellipse(const x: TPointD; const rx, ry: Double): TTag;
begin
  Result := Ellipse(x.X, x.Y, rx, ry);
end;

function TSVGBuilder.Ellipse2(const x, y, rx, ry: Single): TTag;
begin
  if SameValue(rx, ry) then
    Result := Circle(x, y, rx)
  else
    Result := Ellipse(x, y, rx, ry);
end;

function TSVGBuilder.Ellipse2(const x, y, rx, ry: Integer): TTag;
begin
  if SameValue(rx, ry) then
    Result := Circle(x, y, rx)
  else
    Result := Ellipse(x, y, rx, ry);
end;

function TSVGBuilder.Ellipse2(const x, y, rx, ry: string): TTag;
begin
  if rx = ry then
    Result := Circle(x, y, rx)
  else
    Result := Ellipse(x, y, rx, ry);
end;

function TSVGBuilder.Ellipse2(const x: TPointF; const rx, ry: Single): TTag;
begin
  if SameValue(rx, ry) then
    Result := Circle(x, rx)
  else
    Result := Ellipse(x, rx, ry);
end;

function TSVGBuilder.Ellipse2(const x: TPoint; const rx, ry: Integer): TTag;
begin
  if SameValue(rx, ry) then
    Result := Circle(x, rx)
  else
    Result := Ellipse(x, rx, ry);
end;

function TSVGBuilder.Ellipse2(const x, y, rx, ry: Double): TTag;
begin
  if SameValue(rx, ry) then
    Result := Circle(x, y, rx)
  else
    Result := Ellipse(x, y, rx, ry);
end;

function TSVGBuilder.Ellipse(const x, y, rx, ry: Double): TTag;
begin
  Result := Ellipse(x.ToString(InvFS), y.ToString(InvFS),
    rx.ToString(InvFS), ry.ToString(InvFS));
end;

function TSVGBuilder.Circle(const x: TPointD; const r: Double): TTag;
begin
  Result := Circle(x.X, x.Y, r);
end;

function TSVGBuilder.Undefine(const AName: string): Boolean;
begin
  Result := FDefines.ContainsKey(AName);
  if Result then
    FDefines.Remove(AName);
end;

function TSVGBuilder.Use(const ref: string): TTag;
begin
  Result := Tag('use')
    .AttribIf('xlink:href', ref);
end;

function TSVGBuilder.Use(const x, y, ref: string): TTag;
begin
  Result := Use(x, y, '', '', ref);
end;

function TSVGBuilder.Use(const x, y, w, h: Double; const ref: string): TTag;
begin
  Result := Use(x.ToString(InvFS), y.ToString(InvFS), w.ToString(InvFS),
    h.ToString(InvFS), ref)
end;

function TSVGBuilder.Image(const R: TRect; const ref: string): TTag;
begin
  Result := Image(R.Left, R.Top, R.Width, R.Height, ref);
end;

function TSVGBuilder.Image(const R: TRectF; const ref: string): TTag;
begin
  Result := Image(R.Left, R.Top, R.Width, R.Height, ref);
end;

function TSVGBuilder.Image(const R: TRectD; const ref: string): TTag;
begin
  Result := Image(R.Left, R.Top, R.Width, R.Height, ref);
end;

function TSVGBuilder.Use(const X: TPoint; const S: TSize;
  const ref: string): TTag;
begin
  Result := Use(X.X, X.Y, S.Width, S.Height, ref);
end;

function TSVGBuilder.Use(const X: TPoint; const ref: string): TTag;
begin
  Result := Use(X.X, X.Y, ref);
end;

function TSVGBuilder.Use(const x, y: Double; const ref: string): TTag;
begin
  Result := Use(x.ToString(InvFS), y.ToString(InvFS), ref);
end;

function TSVGBuilder.Use(const x, y: Single; const ref: string): TTag;
begin
  Result := Use(x.ToString(InvFS), y.ToString(InvFS), ref);
end;

function TSVGBuilder.Use(const x, y: Integer; const ref: string): TTag;
begin
  Result := Use(x.ToString, y.ToString, ref)
end;

function TSVGBuilder.Use(const X: TPointF; const S: TSizeF;
  const ref: string): TTag;
begin
  Result := Use(X.X, X.Y, S.Width, S.Height, ref);
end;

function TSVGBuilder.Use(const X: TPointF; const ref: string): TTag;
begin
  Result := Use(X.X, X.Y, ref);
end;

function TSVGBuilder.Use(const X: TPointD; const S: TSizeD;
  const ref: string): TTag;
begin
  Result := Use(X.X, X.Y, S.Width, S.Height, ref);
end;

function TSVGBuilder.Use(const X: TPointD; const ref: string): TTag;
begin
  Result := Use(X.X, X.Y, ref);
end;

function TSVGBuilder.Ellipse2(const x: TPointD; const rx, ry: Double): TTag;
begin
  if SameValue(rx, ry) then
    Result := Circle(x, rx)
  else
    Result := Ellipse(x, rx, ry);
end;

function TSVGBuilder.Circle(const r: Integer): TTag;
begin
  Result := Tag('circle')
    .Attrib('r', r.ToString);
end;

function TSVGBuilder.Circle(const r: Single): TTag;
begin
  Result := Tag('circle')
    .Attrib('r', r.ToString(InvFS));
end;

function TSVGBuilder.Circle(const r: Double): TTag;
begin
  Result := Tag('circle')
    .Attrib('r', r.ToString(InvFS));
end;

function TSVGBuilder.BeginMarker(const AViewBox: string; ARefX, ARefY,
  AMarkerWidth, AMarkerHeight: Double; const AStrokeWidth: Boolean): TTag;
begin
  Result := BeginMarker(AViewBox, ARefX.ToString(InvFS), ARefY.ToString(InvFS),
    AMarkerWidth.ToString(InvFS), AMarkerHeight.ToString(InvFS), AStrokeWidth)
end;

{ TTransform }

class operator TTransform.Explicit(const S: string): TTransform;
begin
  Result.FData := S;
end;

class operator TTransform.Implicit(const ATransform: TTransform): string;
begin
  Result := ATransform.FData;
end;

class operator TTransform.Multiply(const L, R: TTransform): TTransform;
begin
  if L.FData.IsEmpty then
    Result := R
  else
    Result := TTransform(L.FData + #32 + R.FData)
end;

function Matrix(const A, B, C, D, E, F: Single): TTransform;
begin
  Result := TTransform(Format('matrix(%g %g %g %g %g %g)', [A, B, C, D, E, F], InvFS));
end;

function Identity: TTransform;
begin
  Result := Default(TTransform);
end;

function Rotation(const Theta, X, Y: Single): TTransform;
begin
  if (X <> 0.0) or (Y <> 0.0) then
    Result := TTransform(Format('rotate(%g %g,%g)', [Theta, X, Y], InvFS))
  else
    Result := TTransform(Format('rotate(%g)', [Theta], InvFS));
end;

function RotationDeg(const Theta, X, Y: Single): TTransform;
begin
  Result := Rotation(Theta, X, Y)
end;

function RotationRad(const Theta, X, Y: Single): TTransform;
begin
  Result := Rotation(180 * Theta / Pi, X, Y)
end;

function Scaling(const X, Y: Single): TTransform;
begin
  Result := TTransform(Format('scale(%g,%g)', [X, Y], InvFS));
end;

function SkewX(const Theta: Single): TTransform;
begin
  Result := TTransform(Format('skewX(%g)', [Theta], InvFS));
end;

function SkewY(const Theta: Single): TTransform;
begin
  Result := TTransform(Format('skewY(%g)', [Theta], InvFS));
end;

function Scaling(const X: Single): TTransform;
begin
  Result := TTransform(Format('scale(%g)', [X], InvFS));
end;

function Translation(const X, Y: Single): TTransform;
begin
  if Y <> 0.0 then
    Result := TTransform(Format('translate(%g,%g)', [X, Y], InvFS))
  else
    Result := TTransform(Format('translate(%g)', [X], InvFS))
end;

function Translation(const X: TVectorD): TTransform; overload;
begin
  Result := Translation(X.X, X.Y);
end;

{ TViewBox }

constructor TViewBox.Create(const Xmin, Ymin, Width, Height: Single);
begin
  Self.Xmin := Xmin;
  Self.Ymin := Ymin;
  Self.Width := Width;
  Self.Height := Height;
end;

constructor TViewBox.Create(const Xmin, Ymin, Width, Height: Integer);
begin
  Self.Xmin := Xmin;
  Self.Ymin := Ymin;
  Self.Width := Width;
  Self.Height := Height;
end;

constructor TViewBox.Create(const Xmin, Ymin, Width, Height, ScaleX,
  ScaleY: Single);
begin
  Self.Xmin := Xmin * ScaleX;
  Self.Ymin := Ymin * ScaleY;
  Self.Width := Width * ScaleX;
  Self.Height := Height * ScaleY;
end;

class operator TViewBox.Implicit(const AViewBox: TViewBox): string;
begin
  Result := Format('%g %g %g %g',
    [AViewBox.Xmin, AViewBox.Ymin, AViewBox.Width, AViewBox.Height], InvFS);
end;

function TViewBox.Valid: Boolean;
begin
  Result := (Width > 0) and (Height > 0);
end;

function TViewBox.Xmax: Single;
begin
  Result := Xmin + Width;
end;

function TViewBox.Ymax: Single;
begin
  Result := Ymin + Height;
end;

{ TCSSBuilder }

class operator TCSSBuilder.Implicit(const ACSS: TCSSBuilder): string;
begin
  Result := '';
  for var i := 0 to High(ACSS.RuleSets) do
  begin
    Result := Result + IfThen(not Result.IsEmpty, #13#10) + ACSS.RuleSets[i].Selector + ' {';
    for var j := 0 to High(ACSS.RuleSets[i].Rules) do
      Result := Result + #13#10#32#32 + ACSS.RuleSets[i].Rules[j].TrimRight([#32, ';']) + ';';
    Result := Result + #13#10 + '}';
  end;
end;

function TCSSBuilder.RuleSet(const ASelector: string;
  const ARules: array of TRule): TCSSBuilder;
begin
  SetLength(RuleSets, Succ(Length(RuleSets)));
  RuleSets[High(RuleSets)].Selector := ASelector;
  SetLength(RuleSets[High(RuleSets)].Rules, Length(ARules));
  for var i := Low(ARules) to High(ARules) do
    RuleSets[High(RuleSets)].Rules[i] := ARules[i];
  Result := Self;
end;

initialization
  InvFS := TFormatSettings.Invariant;

end.
