mirror of
https://github.com/doublecmd/doublecmd.git
synced 2026-06-21 09:58:13 +00:00
3163 lines
87 KiB
ObjectPascal
3163 lines
87 KiB
ObjectPascal
unit Img32.SVG.Core;
|
|
|
|
(*******************************************************************************
|
|
* Author : Angus Johnson *
|
|
* Version : 4.7 *
|
|
* Date : 12 January 2025 *
|
|
* Website : http://www.angusj.com *
|
|
* Copyright : Angus Johnson 2019-2025 *
|
|
* *
|
|
* Purpose : Essential structures and functions to read SVG files *
|
|
* *
|
|
* License : Use, modification & distribution is subject to *
|
|
* Boost Software License Ver 1 *
|
|
* http://www.boost.org/LICENSE_1_0.txt *
|
|
*******************************************************************************)
|
|
|
|
interface
|
|
|
|
{$I Img32.inc}
|
|
|
|
uses
|
|
SysUtils, Classes, Types, Math, StrUtils,
|
|
{$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 GetRectD(relSizeX, relSizeY: 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, staJustify);
|
|
TSpacesInText = (sitUndefined, sitIgnore, sitPreserve);
|
|
|
|
UTF8Strings = array of UTF8String;
|
|
|
|
TSVGFontInfo = record
|
|
family : TFontFamily;
|
|
familyNames : UTF8Strings;
|
|
size : double;
|
|
spacing : double;
|
|
spacesInText : TSpacesInText;
|
|
textLength : double;
|
|
italic : TSvgItalicSyle;
|
|
weight : Integer;
|
|
align : TSvgTextAlign;
|
|
decoration : TFontDecoration;
|
|
baseShift : TValue;
|
|
end;
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// TClassStylesList: Map that stores CSS selectors with their styles
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
PClassStyleListItem = ^TClassStyleListItem;
|
|
TClassStyleListItem = record //used internally by TClassStylesList
|
|
Hash : Cardinal;
|
|
Next : Integer;
|
|
Name : UTF8String;
|
|
Style : UTF8String;
|
|
end;
|
|
|
|
TClassStylesList = class
|
|
private
|
|
FNameHash: Cardinal;
|
|
FItems: array of TClassStyleListItem;
|
|
FBuckets: TArrayOfInteger;
|
|
FCount: Integer;
|
|
FMod: Cardinal;
|
|
procedure Grow(NewCapacity: Integer = -1);
|
|
function FindItemIndex(const Name: UTF8String): Integer;
|
|
public
|
|
procedure Preallocate(AdditionalItemCount: Integer);
|
|
procedure AddAppendStyle(const Name, Style: UTF8String);
|
|
function GetStyle(const Name: 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;
|
|
class function ParseAttribName(c, endC: PUTF8Char; attrib: PSvgAttrib): PUTF8Char; {$IFDEF CLASS_STATIC}static;{$ENDIF}
|
|
class function ParseAttribValue(c, endC: PUTF8Char; attrib: PSvgAttrib): PUTF8Char; {$IFDEF CLASS_STATIC}static;{$ENDIF}
|
|
class function ParseAttribNameAndValue(c, endC: PUTF8Char; attrib: PSvgAttrib): PUTF8Char; {$IFDEF CLASS_STATIC}static;{$ENDIF}
|
|
function ParseAttributes(var c: PUTF8Char; endC: PUTF8Char): Boolean; virtual;
|
|
procedure ParseStyleAttribute(const style: UTF8String);
|
|
property Attrib[index: integer]: PSvgAttrib read GetAttrib;
|
|
property AttribCount: integer read GetAttribCount;
|
|
end;
|
|
|
|
TDocTypeEl = class(TXmlEl)
|
|
private
|
|
function SkipWord(c, endC: PUTF8Char): PUTF8Char;
|
|
function ParseEntities(var c, endC: PUTF8Char): Boolean;
|
|
public
|
|
function ParseAttributes(var c: PUTF8Char; endC: PUTF8Char): Boolean; override;
|
|
end;
|
|
|
|
TSvgXmlEl = 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 ParseUtf8Stream;
|
|
public
|
|
classStyles : TClassStylesList;
|
|
xmlHeader : TXmlEl;
|
|
docType : TDocTypeEl;
|
|
svgTree : TSvgXmlEl;
|
|
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 ParseNextWordHash(var c: PUTF8Char; endC: PUTF8Char;
|
|
out hash: cardinal): Boolean; overload;
|
|
function ParseNextWordHash(c, endC: PUTF8Char): cardinal; overload;
|
|
function ParseNextWordExHash(var c: PUTF8Char; endC: PUTF8Char;
|
|
out hash: cardinal): Boolean;
|
|
function ParseNextNum(var c: PUTF8Char; endC: PUTF8Char;
|
|
skipComma: Boolean; out val: double): Boolean;
|
|
function ParseNextNumEx(var c: PUTF8Char; endC: PUTF8Char; skipComma: Boolean;
|
|
out val: double; out unitType: TUnitType): Boolean;
|
|
function GetHash(c: PUTF8Char; len: nativeint): cardinal; overload;
|
|
function GetHash(const name: UTF8String): cardinal; overload; {$IFDEF INLINE} inline; {$ENDIF}
|
|
function GetHashCaseSensitive(name: PUTF8Char; nameLen: integer): cardinal;
|
|
function ExtractRef(const href: UTF8String): UTF8String;
|
|
function IsNumPending(var c: PUTF8Char;
|
|
endC: PUTF8Char; ignoreComma: Boolean): Boolean;
|
|
function UTF8StringToColor32(const value: UTF8String; var color: TColor32): Boolean;
|
|
function ScaleDashArray(const dblArray: TArrayOfDouble; scale: double): TArrayOfDouble;
|
|
function Match(c: PUTF8Char; const compare: UTF8String): Boolean; overload;
|
|
function Match(const compare1, compare2: UTF8String): Boolean; overload;
|
|
function PosEx(const subStr: utf8String; const text: Utf8String; startIdx: integer = 1): integer;
|
|
procedure ToUTF8String(c, endC: PUTF8Char; var S: UTF8String;
|
|
spacesInText: TSpacesInText = sitUndefined);
|
|
function TrimMultiSpacesUtf8(const text: Utf8String): Utf8String;
|
|
function TrimMultiSpacesUnicode(const text: UnicodeString): UnicodeString;
|
|
function ConvertNewlines(const s: UTF8String): UTF8String; overload;
|
|
function ConvertNewlines(const s: UnicodeString): UnicodeString; overload;
|
|
function StripNewlines(const s: UTF8String): UTF8String; overload;
|
|
function StripNewlines(const s: UnicodeString): UnicodeString; overload;
|
|
procedure ToAsciiLowerUTF8String(c, endC: PUTF8Char; var S: UTF8String);
|
|
procedure ToTrimmedUTF8String(c, endC: PUTF8Char; var S: UTF8String);
|
|
function IsSameUTF8String(const S1, S2: UTF8String): Boolean;
|
|
|
|
//special parsing functions //////////////////////////////////////////
|
|
procedure ParseStyleElementContent(const value: UTF8String; stylesList: TClassStylesList);
|
|
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 SkipBlanksEx(c: PUTF8Char; endC: PUTF8Char): PUTF8Char; {$IFDEF INLINE} inline; {$ENDIF}
|
|
function SkipBlanksAndComma(c, endC: PUTF8Char): PUTF8Char; {$IFDEF INLINE} inline; {$ENDIF}
|
|
|
|
function GetCommaSeparatedArray(const str: UTF8String): UTF8Strings;
|
|
function TrimQuotes(const str: UTF8String): UTF8String;
|
|
|
|
procedure ConvertUnicodeToUtf8(memStream: TMemoryStream);
|
|
|
|
function GetScale(src, dst: double): double;
|
|
function GetScaleForBestFit(srcW, srcH, dstW, dstH: double): double;
|
|
|
|
function Base64Decode(const str: PAnsiChar; len: integer; memStream: TMemoryStream): Boolean;
|
|
|
|
type
|
|
TSetOfUTF8Char = set of UTF8Char;
|
|
|
|
function CharInSet(chr: UTF8Char; const chrs: TSetOfUTF8Char): Boolean;
|
|
function DecodeUtf8ToUnicode(const utf8: UTF8String): UnicodeString;
|
|
|
|
const
|
|
clInvalid = $00010001;
|
|
clCurrent = $00010002;
|
|
sqrt2 = 1.4142135623731;
|
|
quote = '''';
|
|
dquote = '"';
|
|
space = #32;
|
|
comma = ',';
|
|
SvgDecimalSeparator = '.'; //do not localize
|
|
|
|
{$I Img32.SVG.HashConsts.inc}
|
|
|
|
var
|
|
LowerCaseTable : array[#0..#$FF] of UTF8Char;
|
|
|
|
implementation
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Color Constant HashMap
|
|
//------------------------------------------------------------------------------
|
|
|
|
type
|
|
PColorConst = ^TColorConst;
|
|
TColorConst = record
|
|
ColorName : UTF8String;
|
|
ColorValue: TColor32;
|
|
end;
|
|
|
|
PPColorConstMapItem = ^PColorConstMapItem;
|
|
PColorConstMapItem = ^TColorConstMapItem;
|
|
TColorConstMapItem = record
|
|
Hash: Cardinal;
|
|
Next: PColorConstMapItem;
|
|
Data: PColorConst;
|
|
end;
|
|
|
|
PColorConstMapItemArray = ^TColorConstMapItemArray;
|
|
TColorConstMapItemArray = array[0..MaxInt div SizeOf(TColorConstMapItem) - 1] of TColorConstMapItem;
|
|
|
|
TColorConstList = class(TObject)
|
|
private
|
|
FItems: array of TColorConstMapItem;
|
|
FBuckets: array of PColorConstMapItem;
|
|
FCount: Integer;
|
|
FMod: Cardinal;
|
|
public
|
|
constructor Create(Colors: PColorConst; Count: Integer);
|
|
function GetColorValue(const ColorName: UTF8String; var Color: TColor32): Boolean;
|
|
end;
|
|
|
|
var
|
|
ColorConstList : TColorConstList;
|
|
|
|
const
|
|
buffSize = 8;
|
|
|
|
//include hashed html entity constants
|
|
{$I Img32.SVG.HtmlHashConsts.inc}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Base64 (MIME) Encode & Decode and other encoding functions ...
|
|
//------------------------------------------------------------------------------
|
|
|
|
type
|
|
PFourChars = ^TFourChars;
|
|
TFourChars = record
|
|
c1: ansichar;
|
|
c2: ansichar;
|
|
c3: ansichar;
|
|
c4: ansichar;
|
|
end;
|
|
|
|
function Chr64ToVal(c: ansiChar): integer; {$IFDEF INLINE} inline; {$ENDIF}
|
|
begin
|
|
case c of
|
|
'+': result := 62;
|
|
'/': result := 63;
|
|
'0'..'9': result := ord(c) + 4;
|
|
'A'..'Z': result := ord(c) -65;
|
|
'a'..'z': result := ord(c) -71;
|
|
else Raise Exception.Create('Corrupted MIME encoded text');
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function FrstChr(c: PFourChars): ansichar; {$IFDEF INLINE} inline; {$ENDIF}
|
|
begin
|
|
result := ansichar(Chr64ToVal(c.c1) shl 2 or Chr64ToVal(c.c2) shr 4);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ScndChr(c: PFourChars): ansichar; {$IFDEF INLINE} inline; {$ENDIF}
|
|
begin
|
|
result := ansichar(Chr64ToVal(c.c2) shl 4 or Chr64ToVal(c.c3) shr 2);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ThrdChr(c: PFourChars): ansichar; {$IFDEF INLINE} inline; {$ENDIF}
|
|
begin
|
|
result := ansichar( Chr64ToVal(c.c3) shl 6 or Chr64ToVal(c.c4) );
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function Base64Decode(const str: PAnsiChar; len: integer; memStream: TMemoryStream): Boolean;
|
|
var
|
|
i, j, extra: integer;
|
|
Chars4: PFourChars;
|
|
dst: PAnsiChar;
|
|
begin
|
|
result := false;
|
|
if (len = 0) or (len mod 4 > 0) or not Assigned(memStream) then exit;
|
|
if str[len-2] = '=' then extra := 2
|
|
else if str[len-1] = '=' then extra := 1
|
|
else extra := 0;
|
|
memStream.SetSize(LongInt((len div 4 * 3) - extra));
|
|
dst := memStream.Memory;
|
|
Chars4 := @str[0];
|
|
i := 0;
|
|
try
|
|
for j := 1 to (len div 4) -1 do
|
|
begin
|
|
dst[i] := FrstChr(Chars4);
|
|
dst[i+1] := ScndChr(Chars4);
|
|
dst[i+2] := ThrdChr(Chars4);
|
|
inc(pbyte(Chars4),4);
|
|
inc(i,3);
|
|
end;
|
|
dst[i] := FrstChr(Chars4);
|
|
if extra < 2 then dst[i+1] := ScndChr(Chars4);
|
|
if extra < 1 then dst[i+2] := ThrdChr(Chars4);
|
|
except
|
|
Exit;
|
|
end;
|
|
Result := true;
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Miscellaneous functions ...
|
|
//------------------------------------------------------------------------------
|
|
|
|
function NewSvgAttrib(): PSvgAttrib; {$IFDEF INLINE} inline; {$ENDIF}
|
|
begin
|
|
// New(Result) uses RTTI to initialize the UTF8String fields to nil.
|
|
// By allocating zero'ed memory we can achieve that much faster.
|
|
Result := AllocMem(SizeOf(TSvgAttrib));
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
procedure DisposeSvgAttrib(attrib: PSvgAttrib); {$IFDEF INLINE} inline; {$ENDIF}
|
|
begin
|
|
// Dispose(Result) uses RTTI to set the UTF8String fields to nil.
|
|
// By clearing them outself we can achieve that much faster.
|
|
attrib.name := '';
|
|
attrib.value := '';
|
|
FreeMem(attrib);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function GetScale(src, dst: double): double;
|
|
begin
|
|
Result := dst / src;
|
|
if (SameValue(Result, 1, 0.00001)) then Result := 1;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function GetScaleForBestFit(srcW, srcH, dstW, dstH: double): double;
|
|
var
|
|
sx,sy: double;
|
|
begin
|
|
sx := dstW / srcW;
|
|
sy := dstH / srcH;
|
|
if sy < sx then sx := sy;
|
|
if (SameValue(sx, 1, 0.00001)) then
|
|
Result := 1 else
|
|
Result := sx;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
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 IsSameAsciiUTF8String(const S1, S2: UTF8String): Boolean;
|
|
var
|
|
Len: Integer;
|
|
I: Integer;
|
|
Ch1, Ch2: UTF8Char;
|
|
begin
|
|
Len := Length(S1);
|
|
Result := Len = Length(S2);
|
|
if Result then
|
|
begin
|
|
Result := False;
|
|
I := 1;
|
|
while True do
|
|
begin
|
|
if I > Len then
|
|
Break;
|
|
|
|
Ch1 := S1[I];
|
|
Ch2 := S2[I];
|
|
if Ch1 = Ch2 then
|
|
begin
|
|
Inc(I);
|
|
Continue;
|
|
end;
|
|
|
|
case Ch1 of
|
|
'A'..'Z', 'a'..'z':
|
|
ch1 := UTF8Char(Ord(ch1) xor $20); // toggle upper/lower
|
|
end;
|
|
|
|
if Ch1 <> Ch2 then
|
|
Exit;
|
|
Inc(I);
|
|
end;
|
|
Result := True;
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function IsSameUTF8StringSlow(const S1, S2: UTF8String): Boolean;
|
|
begin
|
|
Result := AnsiSameText(string(S1), string(S2));
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function IsSameUTF8String(const S1, S2: UTF8String): Boolean;
|
|
var
|
|
Len: Integer;
|
|
I: Integer;
|
|
Ch1, Ch2: UTF8Char;
|
|
begin
|
|
Len := Length(S1);
|
|
Result := Len = Length(S2);
|
|
if Result then
|
|
begin
|
|
Result := False;
|
|
I := 1;
|
|
Ch1 := #0;
|
|
Ch2 := #0;
|
|
while True do
|
|
begin
|
|
if I > Len then
|
|
Break;
|
|
|
|
Ch1 := S1[I];
|
|
Ch2 := S2[I];
|
|
if Ch1 = Ch2 then
|
|
begin
|
|
Inc(I);
|
|
Continue;
|
|
end;
|
|
|
|
case Ch1 of
|
|
'A'..'Z', 'a'..'z':
|
|
ch1 := UTF8Char(Ord(ch1) xor $20); // toggle upper/lower
|
|
end;
|
|
|
|
if Ch1 <> Ch2 then
|
|
Break;
|
|
Inc(I);
|
|
end;
|
|
if Ch1 = Ch2 then
|
|
Result := True
|
|
else if (Ord(Ch1) or Ord(Ch2)) and $80 <> 0 then // we found non-matching, non-ASCII characters
|
|
Result := IsSameUTF8StringSlow(S1, S2);
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function CharInSet(chr: UTF8Char; const chrs: TSetOfUTF8Char): Boolean;
|
|
begin
|
|
Result := chr in chrs;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
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[i - 1]] <> compare[i] then Exit;
|
|
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[i - 1]] <> LowerCaseTable[c2[i - 1]] then Exit;
|
|
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] <= space) do dec(len);
|
|
while (i <= len) and (str[i] <= space) do inc(i);
|
|
for j := i + 1 to len do
|
|
if (str[j] <= space) and (str[j -1] > space) then inc(spcCnt);
|
|
SetLength(Result, spcCnt +1);
|
|
for k := 0 to spcCnt do
|
|
begin
|
|
j := i;
|
|
while (j <= len) and (str[j] > space) do inc(j);
|
|
SetLength(Result[k], j -i);
|
|
if j > i then
|
|
Move(str[i], Result[k][1], j -i);
|
|
while (j <= len) and (str[j] <= space) do inc(j);
|
|
i := j;
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function TrimQuotes(const str: UTF8String): UTF8String;
|
|
var
|
|
i, len: integer;
|
|
savedQuote: UTF8Char;
|
|
begin
|
|
len := Length(str);
|
|
i := 1;
|
|
while (i < len) and (str[i] <= space) do inc(i);
|
|
if (i < len) and (str[i] in [quote, dquote]) then
|
|
begin
|
|
savedQuote := str[i];
|
|
inc(i);
|
|
while (len > i) and (str[len] <= space) do dec(len);
|
|
if (len = i) or (str[len] <> savedQuote) then
|
|
Result := str else // oops!
|
|
Result := Copy(str, i, len - i);
|
|
end
|
|
else
|
|
Result := str
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function GetCommaSeparatedArray(const str: UTF8String): UTF8Strings;
|
|
var
|
|
i,j,k, cnt, len: integer;
|
|
begin
|
|
// precondition: commas CANNOT be embedded
|
|
len := Length(str);
|
|
cnt := 1;
|
|
for i := 1 to len do
|
|
if (str[i] = comma) then inc(cnt);
|
|
SetLength(Result, cnt);
|
|
j := 0;
|
|
k := 1;
|
|
for i := 1 to len do
|
|
begin
|
|
if (str[i] <> comma) then Continue;
|
|
Result[j] := TrimQuotes(Copy(str, k, i-k));
|
|
inc(j);
|
|
k := i + 1;
|
|
end;
|
|
if len >= k then
|
|
Result[j] := TrimQuotes(Copy(str, k, len-k +1));
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function GetXmlEncoding(memory: Pointer; len: integer): TSvgEncoding;
|
|
var
|
|
p, p1: PUTF8Char;
|
|
begin
|
|
Result := eUnknown;
|
|
if (len < 4) or not Assigned(memory) then Exit;
|
|
p := PUTF8Char(memory);
|
|
p1 := (p + 1);
|
|
case p^ of
|
|
#$EF: if (p1^ = #$BB) then
|
|
if ((p +2)^ = #$BF) then
|
|
Result := eUtf8 else
|
|
Exit;
|
|
#$FF: if (p1^ = #$FE) or (p1^ = #0) then
|
|
Result := eUnicodeLE;
|
|
#$FE: if (p1^ = #$FF) then
|
|
Result := eUnicodeBE;
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function SkipBlanks(var c: PUTF8Char; endC: PUTF8Char): Boolean;
|
|
var
|
|
cc: PUTF8Char;
|
|
begin
|
|
cc := c;
|
|
if (cc < endC) and (cc^ <= space) then
|
|
begin
|
|
inc(cc);
|
|
while (cc < endC) and (cc^ <= space) do inc(cc);
|
|
c := cc;
|
|
end;
|
|
Result := (cc < endC);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function SkipBlanksEx(c: PUTF8Char; endC: PUTF8Char): PUTF8Char;
|
|
begin
|
|
while (c < endC) and (c^ <= space) do inc(c);
|
|
Result := c;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function SkipBlanksAndComma(c, endC: PUTF8Char): PUTF8Char;
|
|
begin
|
|
Result := SkipBlanksEx(c, endC);
|
|
if (Result >= endC) or (Result^ <> ',') then Exit;
|
|
Result := SkipBlanksEx(Result + 1, endC);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function SkipStyleBlanks(c, endC: PUTF8Char): PUTF8Char;
|
|
var
|
|
inComment: Boolean;
|
|
ch: UTF8Char;
|
|
begin
|
|
//style content may include multi-line comment blocks
|
|
inComment := false;
|
|
while (c < endC) do
|
|
begin
|
|
ch := c^;
|
|
if inComment then
|
|
begin
|
|
if (ch = '*') and ((c +1)^ = '/') then
|
|
begin
|
|
inComment := false;
|
|
inc(c);
|
|
end;
|
|
end
|
|
else if (ch > space) then
|
|
begin
|
|
inComment := (ch = '/') and ((c +1)^ = '*');
|
|
if not inComment then break;
|
|
inc(c);
|
|
end;
|
|
inc(c);
|
|
end;
|
|
Result := c;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function IsDigit(c: UTF8Char): Boolean; {$IFDEF INLINE} inline; {$ENDIF}
|
|
begin
|
|
case c of
|
|
'0'..'9': Result := True;
|
|
else Result := False;
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function IsQuoteChar(c: UTF8Char): Boolean; {$IFDEF INLINE} inline; {$ENDIF}
|
|
begin
|
|
Result := (c = quote) or (c = dquote);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function IsAlpha(c: UTF8Char): Boolean; {$IFDEF INLINE} inline; {$ENDIF}
|
|
begin
|
|
case c of
|
|
'A'..'Z', 'a'..'z': Result := True;
|
|
else Result := False;
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseStyleNameLen(c, endC: PUTF8Char): PUTF8Char;
|
|
var
|
|
c2: PUTF8Char;
|
|
begin
|
|
Result := c;
|
|
//nb: style names may start with a hyphen
|
|
c2 := Result;
|
|
if (c2^ = '-') then inc(c2);
|
|
if not IsAlpha(c2^) then Exit;
|
|
|
|
Result := c2 + 1;
|
|
while Result < endC do
|
|
begin
|
|
case Result^ of
|
|
'0'..'9', 'A'..'Z', 'a'..'z', '-': inc(Result);
|
|
else break;
|
|
end;
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseNextWord(var c: PUTF8Char; endC: PUTF8Char; out word: UTF8String): Boolean;
|
|
var
|
|
c2, cc: PUTF8Char;
|
|
begin
|
|
cc := SkipBlanksAndComma(c, endC);
|
|
if cc >= endC then
|
|
begin
|
|
c := cc;
|
|
Result := False;
|
|
Exit;
|
|
end;
|
|
|
|
c2 := cc;
|
|
while cc < endC do
|
|
begin
|
|
case cc^ of
|
|
'A'..'Z', 'a'..'z': inc(cc);
|
|
else break;
|
|
end;
|
|
end;
|
|
c := cc;
|
|
ToUTF8String(c2, cc, word);
|
|
Result := True;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseNextWordHash(var c: PUTF8Char; endC: PUTF8Char; out hash: cardinal): Boolean;
|
|
var
|
|
c2, cc: PUTF8Char;
|
|
begin
|
|
cc := SkipBlanksAndComma(c, endC);
|
|
if cc >= endC then
|
|
begin
|
|
c := cc;
|
|
hash := 0;
|
|
Result := False;
|
|
Exit;
|
|
end;
|
|
|
|
c2 := cc;
|
|
while cc < endC do
|
|
begin
|
|
case cc^ of
|
|
'A'..'Z', 'a'..'z': inc(cc);
|
|
else break;
|
|
end;
|
|
end;
|
|
c := cc;
|
|
hash := GetHash(c2, cc - c2);
|
|
Result := True;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseNextWordHash(c, endC: PUTF8Char): cardinal;
|
|
var
|
|
c2: PUTF8Char;
|
|
begin
|
|
c := SkipBlanksAndComma(c, endC);
|
|
if c >= endC then
|
|
begin
|
|
Result := 0;
|
|
Exit;
|
|
end;
|
|
|
|
c2 := c;
|
|
while c < endC do
|
|
begin
|
|
case c^ of
|
|
'A'..'Z', 'a'..'z': inc(c);
|
|
else break;
|
|
end;
|
|
end;
|
|
Result := GetHash(c2, c - c2);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseNextWordExHash(var c: PUTF8Char; endC: PUTF8Char;
|
|
out hash: cardinal): Boolean;
|
|
var
|
|
c2, cc: PUTF8Char;
|
|
begin
|
|
cc := SkipBlanksAndComma(c, endC);
|
|
if cc >= endC then
|
|
begin
|
|
c := cc;
|
|
hash := 0;
|
|
Result := False;
|
|
Exit;
|
|
end;
|
|
|
|
if cc^ = quote then
|
|
begin
|
|
inc(c);
|
|
c2 := cc;
|
|
while (cc < endC) and (cc^ <> quote) do inc(cc);
|
|
hash := GetHash(c2, cc - c2);
|
|
inc(cc);
|
|
end else
|
|
begin
|
|
if not IsAlpha(cc^) then
|
|
begin
|
|
hash := 0;
|
|
Result := False;
|
|
Exit;
|
|
end;
|
|
c2 := cc;
|
|
inc(cc);
|
|
while cc < endC do
|
|
case cc^ of
|
|
'A'..'Z', 'a'..'z', '-', '_': inc(cc);
|
|
else break;
|
|
end;
|
|
hash := GetHash(c2, cc - c2);
|
|
end;
|
|
c := cc;
|
|
Result := True;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseNameLength(c: PUTF8Char; endC: PUTF8Char): PUTF8Char;
|
|
begin
|
|
inc(c);
|
|
while c < endC do
|
|
begin
|
|
case c^ of
|
|
'0'..'9', 'A'..'Z', 'a'..'z', '_', ':', '-': inc(c);
|
|
else break;
|
|
end;
|
|
end;
|
|
Result := c;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
{$PUSH}{$Q-}{$R-}
|
|
function GetHash(c: PUTF8Char; len: nativeint): cardinal;
|
|
var
|
|
i: integer;
|
|
begin
|
|
//https://en.wikipedia.org/wiki/Jenkins_hash_function
|
|
Result := 0;
|
|
if c = nil then Exit;
|
|
for i := 1 to len 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;
|
|
{$POP}
|
|
//------------------------------------------------------------------------------
|
|
|
|
function GetHash(const name: UTF8String): cardinal;
|
|
begin
|
|
// skip function call by directly casting it to Pointer
|
|
Result := GetHash(PUTF8Char(Pointer(name)), Length(name));
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
{$PUSH}{$Q-}{$R-}
|
|
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;
|
|
{$POP}
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseNextWordHashed(var c: PUTF8Char; endC: PUTF8Char): cardinal;
|
|
var
|
|
c2: PUTF8Char;
|
|
len: integer;
|
|
begin
|
|
c2 := c;
|
|
c := ParseNameLength(c2, endC);
|
|
len := c - c2;
|
|
if len <= 0 then Result := 0
|
|
else Result := GetHash(c2, len);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseExpDigits(c, endC: PUTF8Char; out val: Integer): PUTF8Char; {$IFDEF INLINE} inline; {$ENDIF}
|
|
var
|
|
v32: Cardinal;
|
|
Digit: Integer;
|
|
begin
|
|
Result := c;
|
|
v32 := 0;
|
|
while Result < endC do
|
|
begin
|
|
Digit := Integer(Ord(Result^)) - Ord('0');
|
|
if Cardinal(Digit) >= 10 then break;
|
|
{$IFDEF FPC} // Something Delphi can optimize but FPC can't (yet?)
|
|
v32 := (v32 shl 3) + (v32 shl 1) + Cardinal(Digit); // Delphi's code is even better than this
|
|
{$ELSE}
|
|
v32 := v32 * 10 + Cardinal(Digit);
|
|
{$ENDIF FPC}
|
|
inc(Result);
|
|
end;
|
|
val := v32;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseDigitsToDouble(c, endC: PUTF8Char; out val: double): PUTF8Char;
|
|
var
|
|
v32: Cardinal;
|
|
v64: Int64;
|
|
Digit: Integer;
|
|
blockEndC: PUTF8Char;
|
|
begin
|
|
// skip leading zeros
|
|
while (c < endC) and (c^ = '0') do inc(c);
|
|
|
|
// Use Int32 first as it is fast for 64bit and 32bit CPUs
|
|
Result := c;
|
|
v32 := 0;
|
|
|
|
blockEndC := c + 9; // log10(2^31) = 9.33
|
|
if blockEndC > endC then
|
|
blockEndC := endC;
|
|
while Result < blockEndC do
|
|
begin
|
|
Digit := Integer(Ord(Result^)) - Ord('0');
|
|
if Cardinal(Digit) >= 10 then break;
|
|
{$IFDEF FPC} // Something Delphi can optimize but FPC can't (yet?)
|
|
v32 := (v32 shl 3) + (v32 shl 1) + Cardinal(Digit);
|
|
{$ELSE}
|
|
v32 := v32 * 10 + Cardinal(Digit);
|
|
{$ENDIF FPC}
|
|
inc(Result);
|
|
end;
|
|
|
|
if (Result < endC) and (Result >= blockEndC) then
|
|
begin
|
|
v64 := v32;
|
|
|
|
blockEndC := c + 18; // log10(2^63) = 18.96
|
|
if blockEndC > endC then
|
|
blockEndC := endC;
|
|
while Result < blockEndC do
|
|
begin
|
|
Digit := Integer(Ord(Result^)) - Ord('0');
|
|
if Cardinal(Digit) >= 10 then break;
|
|
{$IF (SizeOf(Pointer) = 4) or defined(FPC)} // neither Delphi 32bit nor FPC can optimize this
|
|
v64 := (v64 shl 3) + (v64 shl 1) + Cardinal(Digit);
|
|
{$ELSE}
|
|
v64 := v64 * 10 + Cardinal(Digit);
|
|
{$IFEND}
|
|
inc(Result);
|
|
end;
|
|
|
|
val := v64;
|
|
// Use Double for the remaining digits and loose precision (we are beyong 16 digits anyway)
|
|
if (Result < endC) and (Result >= blockEndC) then
|
|
begin
|
|
while Result < endC do
|
|
begin
|
|
Digit := Integer(Ord(Result^)) - Ord('0');
|
|
if Cardinal(Digit) >= 10 then break;
|
|
val := val * 10 + Digit;
|
|
inc(Result);
|
|
end;
|
|
end;
|
|
end
|
|
else
|
|
val := v32;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseNextNumEx(var c: PUTF8Char; endC: PUTF8Char; skipComma: Boolean;
|
|
out val: double; out unitType: TUnitType): Boolean;
|
|
const
|
|
Power10: array[0..18] of Double = (
|
|
1E0, 1E1, 1E2, 1E3, 1E4, 1E5, 1E6, 1E7, 1E8, 1E9,
|
|
1E10, 1E11, 1E12, 1E13, 1E14, 1E15, 1E16, 1E17, 1E18
|
|
);
|
|
Power10Reciprocal: array[0..18] of Double = (
|
|
1/1E0, 1/1E1, 1/1E2, 1/1E3, 1/1E4, 1/1E5, 1/1E6, 1/1E7, 1/1E8, 1/1E9,
|
|
1/1E10, 1/1E11, 1/1E12, 1/1E13, 1/1E14, 1/1E15, 1/1E16, 1/1E17, 1/1E18
|
|
);
|
|
var
|
|
exp: integer;
|
|
isNeg, expIsNeg: Boolean;
|
|
start, decStart, cc: PUTF8Char;
|
|
decimals: Double;
|
|
begin
|
|
Result := false;
|
|
unitType := utNumber;
|
|
|
|
cc := c;
|
|
|
|
//skip white space +/- single comma
|
|
if skipComma then
|
|
begin
|
|
while (cc < endC) and (cc^ <= space) do inc(cc);
|
|
if (cc^ = ',') then inc(cc);
|
|
end;
|
|
while (cc < endC) and (cc^ <= space) do inc(cc);
|
|
if (cc = endC) then
|
|
begin
|
|
c := cc;
|
|
Exit;
|
|
end;
|
|
|
|
exp := Invalid; expIsNeg := false;
|
|
isNeg := cc^ = '-';
|
|
if isNeg then inc(cc);
|
|
|
|
start := cc;
|
|
|
|
// Use fast parsing
|
|
cc := ParseDigitsToDouble(cc, endC, val);
|
|
if cc < endC then
|
|
begin
|
|
// Decimals
|
|
if Ord(cc^) = Ord(SvgDecimalSeparator) then
|
|
begin
|
|
inc(cc);
|
|
decStart := cc;
|
|
cc := ParseDigitsToDouble(cc, endC, decimals);
|
|
if cc > decStart then
|
|
begin
|
|
if cc - decStart <= 18 then
|
|
val := val + (decimals * Power10Reciprocal[(cc - decStart)])
|
|
else
|
|
val := val + (decimals * Power(10, -(cc - decStart)))
|
|
end;
|
|
end;
|
|
|
|
// Exponent
|
|
if (cc < endC) and ((cc^ = 'e') or (cc^ = 'E')) then
|
|
begin
|
|
case (cc+1)^ of
|
|
'-', '0'..'9':
|
|
begin
|
|
inc(cc);
|
|
if cc^ = '-' then
|
|
begin
|
|
expIsNeg := true;
|
|
inc(cc);
|
|
end;
|
|
cc := ParseExpDigits(cc, endC, exp);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
Result := cc > start;
|
|
if not Result then
|
|
begin
|
|
c := cc;
|
|
Exit;
|
|
end;
|
|
|
|
if isNeg then val := -val;
|
|
if IsValid(exp) then
|
|
begin
|
|
if exp <= 18 then
|
|
begin
|
|
if expIsNeg then
|
|
val := val * Power10Reciprocal[exp] else
|
|
val := val * Power10[exp];
|
|
end
|
|
else
|
|
begin
|
|
if expIsNeg then
|
|
val := val * Power(10, -exp) else
|
|
val := val * Power(10, exp);
|
|
end;
|
|
end;
|
|
|
|
//https://oreillymedia.github.io/Using_SVG/guide/units.html
|
|
case cc^ of
|
|
'%':
|
|
begin
|
|
inc(cc);
|
|
unitType := utPercent;
|
|
end;
|
|
'c': //convert cm to pixels
|
|
if ((cc+1)^ = 'm') then
|
|
begin
|
|
inc(cc, 2);
|
|
unitType := utCm;
|
|
end;
|
|
'd': //ignore deg
|
|
if ((cc+1)^ = 'e') and ((cc+2)^ = 'g') then
|
|
begin
|
|
inc(cc, 3);
|
|
unitType := utDegree;
|
|
end;
|
|
'e': //convert cm to pixels
|
|
if ((cc+1)^ = 'm') then
|
|
begin
|
|
inc(cc, 2);
|
|
unitType := utEm;
|
|
end
|
|
else if ((cc+1)^ = 'x') then
|
|
begin
|
|
inc(cc, 2);
|
|
unitType := utEx;
|
|
end;
|
|
'i': //convert inchs to pixels
|
|
if ((cc+1)^ = 'n') then
|
|
begin
|
|
inc(cc, 2);
|
|
unitType := utInch;
|
|
end;
|
|
'm': //convert mm to pixels
|
|
if ((cc+1)^ = 'm') then
|
|
begin
|
|
inc(cc, 2);
|
|
unitType := utMm;
|
|
end;
|
|
'p':
|
|
case (cc+1)^ of
|
|
'c':
|
|
begin
|
|
inc(cc, 2);
|
|
unitType := utPica;
|
|
end;
|
|
't':
|
|
begin
|
|
inc(cc, 2);
|
|
unitType := utPt;
|
|
end;
|
|
'x':
|
|
begin
|
|
inc(cc, 2);
|
|
unitType := utPixel;
|
|
end;
|
|
end;
|
|
'r': //convert radian angles to degrees
|
|
if Match(cc, 'rad') then
|
|
begin
|
|
inc(cc, 3);
|
|
unitType := utRadian;
|
|
end;
|
|
end;
|
|
c := cc;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseNextNum(var c: PUTF8Char; endC: PUTF8Char;
|
|
skipComma: Boolean; out val: double): Boolean;
|
|
var
|
|
tmp: TValue;
|
|
begin
|
|
tmp.Init;
|
|
Result := ParseNextNumEx(c, endC, skipComma, tmp.rawVal, tmp.unitType);
|
|
val := tmp.GetValue(1, 1);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ExtractRef(const href: UTF8String): UTF8String; {$IFDEF INLINE} inline; {$ENDIF}
|
|
var
|
|
c, c2, endC: PUTF8Char;
|
|
begin
|
|
c := PUTF8Char(href);
|
|
endC := c + Length(href);
|
|
if Match(c, 'url(') then
|
|
begin
|
|
inc(c, 4);
|
|
dec(endC); // avoid trailing ')'
|
|
end;
|
|
if c^ = '#' then inc(c);
|
|
c2 := c;
|
|
while (c < endC) and (c^ <> ')') do inc(c);
|
|
ToUTF8String(c2, c, Result);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ParseNextChar(var c: PUTF8Char; endC: PUTF8Char): UTF8Char;
|
|
var
|
|
cc: PUTF8Char;
|
|
begin
|
|
cc := SkipBlanksEx(c, endC);
|
|
if cc >= endC then
|
|
Result := #0
|
|
else
|
|
begin
|
|
Result := cc^;
|
|
c := cc + 1;
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
procedure ToTrimmedUTF8String(c, endC: PUTF8Char; var S: UTF8String);
|
|
var
|
|
len: integer;
|
|
begin
|
|
// trim left
|
|
while (c < endC) and (c^ <= space) do Inc(c);
|
|
// trim right
|
|
while (endC > c) and (endC[-1] <= space) do Dec(endC);
|
|
|
|
len := endC - c;
|
|
SetLength(S, len);
|
|
if len = 0 then Exit;
|
|
Move(c^, PUTF8Char(S)^, len * SizeOf(UTF8Char));
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function PosEx(const subStr: UTF8String; const text: Utf8String; startIdx: integer): integer;
|
|
var
|
|
i, maxI, len, subStrLen: integer;
|
|
begin
|
|
len := Length(Text);
|
|
subStrLen := Length(subStr);
|
|
maxI := len - subStrLen +1;
|
|
for i := Max(1, startIdx) to maxI do
|
|
begin
|
|
if (text[i] <> subStr[1]) or
|
|
not CompareMem(@text[i], @subStr[1], subStrLen) then Continue;
|
|
Result := i;
|
|
Exit;
|
|
end;
|
|
Result := 0;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ReversePosEx(utf8: utf8Char;
|
|
const text: Utf8String; startIdx: integer): integer; overload;
|
|
{$IFDEF INLINE} inline; {$ENDIF}
|
|
begin
|
|
Result := Max(0, Min(Length(text), startidx));
|
|
while (Result > 0) and (text[Result] <> utf8) do Dec(Result);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function TrimMultiSpacesUtf8(const text: Utf8String): Utf8String;
|
|
var
|
|
i, len: integer;
|
|
begin
|
|
Result := text;
|
|
len := Length(Result);
|
|
for i := 1 to len do
|
|
if Result[i] < #32 then Result[i] := #32;
|
|
i := ReversePosEx(space, Result, len);
|
|
while i > 1 do
|
|
begin
|
|
Dec(i);
|
|
while (i > 0) and (Result[i] = space) do
|
|
begin
|
|
Delete(Result, i, 1);
|
|
Dec(i);
|
|
end;
|
|
i := ReversePosEx(space, Result, i);
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ReversePosEx(c: WideChar;
|
|
const text: UnicodeString; startIdx: integer): integer; overload;
|
|
{$IFDEF INLINE} inline; {$ENDIF}
|
|
begin
|
|
Result := Max(0, Min(Length(text), startidx));
|
|
while (Result > 0) and (text[Result] <> c) do Dec(Result);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function TrimMultiSpacesUnicode(const text: UnicodeString): UnicodeString;
|
|
var
|
|
i, len: integer;
|
|
begin
|
|
Result := text;
|
|
len := Length(Result);
|
|
for i := 1 to len do
|
|
if Result[i] < #32 then Result[i] := #32;
|
|
i := ReversePosEx(space, Result, len);
|
|
while i > 1 do
|
|
begin
|
|
Dec(i);
|
|
while (i > 0) and (Result[i] = space) do
|
|
begin
|
|
Delete(Result, i, 1);
|
|
Dec(i);
|
|
end;
|
|
i := ReversePosEx(space, Result, i);
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function StripNewlines(const s: UTF8String): UTF8String;
|
|
var
|
|
i: integer;
|
|
begin
|
|
Result := s;
|
|
i := Length(Result);
|
|
while i > 0 do
|
|
begin
|
|
if Result[i] < space then Delete(Result, i, 1);
|
|
Dec(i);
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function StripNewlines(const s: UnicodeString): UnicodeString;
|
|
var
|
|
i: integer;
|
|
begin
|
|
Result := s;
|
|
i := Length(Result);
|
|
while i > 0 do
|
|
begin
|
|
if Result[i] < space then Delete(Result, i, 1);
|
|
Dec(i);
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ConvertNewlines(const s: UTF8String): UTF8String; overload;
|
|
var
|
|
i: integer;
|
|
begin
|
|
Result := s;
|
|
i := Length(Result);
|
|
while i > 0 do
|
|
begin
|
|
if Result[i] < space then
|
|
begin
|
|
if Result[i] = #10 then
|
|
Result[i] := space else
|
|
Delete(Result, i, 1);
|
|
end;
|
|
Dec(i);
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function ConvertNewlines(const s: UnicodeString): UnicodeString; overload;
|
|
var
|
|
i: integer;
|
|
begin
|
|
Result := s;
|
|
i := Length(Result);
|
|
while i > 0 do
|
|
begin
|
|
if Result[i] < space then
|
|
begin
|
|
if Result[i] = #10 then
|
|
Result[i] := space else
|
|
Delete(Result, i, 1);
|
|
end;
|
|
Dec(i);
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
procedure ToUTF8String(c, endC: PUTF8Char;
|
|
var S: UTF8String; spacesInText: TSpacesInText);
|
|
var
|
|
len: integer;
|
|
begin
|
|
len := endC - c;
|
|
SetLength(S, len);
|
|
if len = 0 then Exit;
|
|
Move(c^, PUTF8Char(S)^, len * SizeOf(UTF8Char));
|
|
if spacesInText <> sitPreserve then
|
|
S := TrimMultiSpacesUtf8(S);
|
|
S := ConvertNewlines(S);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
procedure ToAsciiLowerUTF8String(c, endC: PUTF8Char; var S: UTF8String);
|
|
// Reads a UTF8String and converts all upper case 'A'..'Z' to lower case 'a'..'z'
|
|
var
|
|
len: integer;
|
|
p: PUTF8Char;
|
|
ch: UTF8Char;
|
|
begin
|
|
len := endC - c;
|
|
SetLength(S, len);
|
|
if len = 0 then Exit;
|
|
|
|
// Use a pointer arithmetic trick to run forward by using a negative index
|
|
p := PUTF8Char(S) + len;
|
|
len := -len;
|
|
while len < 0 do
|
|
begin
|
|
ch := endC[len];
|
|
case ch of
|
|
'A'..'Z':
|
|
ch := UTF8Char(Byte(ch) or $20);
|
|
end;
|
|
p[len] := ch;
|
|
inc(len);
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function IsKnownEntity(owner: TSvgParser;
|
|
var c: PUTF8Char; endC: PUTF8Char; out entity: PSvgAttrib): boolean;
|
|
var
|
|
c2, c3: PUTF8Char;
|
|
begin
|
|
inc(c); //skip ampersand.
|
|
c2 := c; c3 := c;
|
|
c3 := ParseNameLength(c3, endC);
|
|
entity := owner.FindEntity(GetHash(c2, c3 - c2));
|
|
Result := (c3^ = ';') and Assigned(entity);
|
|
//nb: increments 'c' only if the entity is found.
|
|
if Result then c := c3 +1 else dec(c);
|
|
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;
|
|
ToUTF8String(c2, c, quotStr);
|
|
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 IsDigit(c2^);
|
|
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;
|
|
MatrixMultiply2(mat, Result);
|
|
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 := tfSansSerif;
|
|
hSerif : fontInfo.family := tfSerif;
|
|
hMonospace : fontInfo.family := tfMonospace;
|
|
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;
|
|
ch: UTF8Char;
|
|
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
|
|
ch := c^;
|
|
case ch of
|
|
'a'..'f':
|
|
val := val * 16 + Ord(ch) - 87;
|
|
'A'..'F':
|
|
val := val * 16 + Ord(ch) - 55;
|
|
'0'..'9':
|
|
val := val * 16 + Ord(ch) - 48;
|
|
else
|
|
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 (c < endC) and (c^ <> ')') and ParseNextNumEx(c, endC, true, vals[3], mus[3]) then
|
|
alpha := 255 else //stops further alpha adjustment
|
|
vals[3] := 255;
|
|
if ParseNextChar(c, endC) <> ')' then Exit;
|
|
for i := 0 to 3 do if IsFraction(vals[i]) then
|
|
vals[i] := vals[i] * 255;
|
|
color := ClampByte(Integer(Round(vals[3]))) shl 24 +
|
|
ClampByte(Integer(Round(vals[0]))) shl 16 +
|
|
ClampByte(Integer(Round(vals[1]))) shl 8 +
|
|
ClampByte(Integer(Round(vals[2])));
|
|
end
|
|
else if (c^ = '#') then //#RRGGBB or #RGB
|
|
begin
|
|
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
|
|
if not ColorConstList.GetColorValue(value, color) then
|
|
Exit;
|
|
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 ScaleDashArray(const dblArray: TArrayOfDouble; scale: double): TArrayOfDouble;
|
|
var
|
|
i, len: integer;
|
|
begin
|
|
len := Length(dblArray);
|
|
SetLength(Result, len);
|
|
if len = 0 then Exit;
|
|
|
|
for i := 0 to len -1 do
|
|
Result[i] := dblArray[i] * scale;
|
|
|
|
if Odd(len) then
|
|
begin
|
|
SetLength(Result, len *2);
|
|
Move(Result[0], Result[len], len * SizeOf(double));
|
|
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 UTF8String;
|
|
|
|
procedure AddName(const name: UTF8String);
|
|
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);
|
|
|
|
c := SkipBlanksEx(c, endC);
|
|
if c >= endC then Exit;
|
|
|
|
if Match(c, '<![cdata[') then inc(c, 9);
|
|
|
|
while True do
|
|
begin
|
|
c := SkipStyleBlanks(c, endC);
|
|
if c >= endC then Break;
|
|
|
|
case c^ of
|
|
SvgDecimalSeparator, '#', 'A'..'Z', 'a'..'z': ;
|
|
else break;
|
|
end;
|
|
|
|
//get one or more class names for each pending style
|
|
c2 := c;
|
|
c := ParseNameLength(c, endC);
|
|
ToAsciiLowerUTF8String(c2, c, aclassName);
|
|
|
|
AddName(aclassName);
|
|
|
|
c := SkipStyleBlanks(c, endC);
|
|
if (c < endC) and (c^ = ',') then
|
|
begin
|
|
inc(c);
|
|
Continue;
|
|
end;
|
|
if len = 0 then break;
|
|
|
|
//now get the style
|
|
if (c >= endC) or (c^ <> '{') then Break;
|
|
inc(c);
|
|
c2 := c;
|
|
while (c < endC) and (c^ <> '}') do inc(c);
|
|
if (c = endC) then break;
|
|
ToTrimmedUTF8String(c2, c, aStyle);
|
|
if aStyle <> '' then
|
|
begin
|
|
stylesList.Preallocate(len);
|
|
//finally, for each class name add (or append) this style
|
|
for i := 0 to len - 1 do
|
|
stylesList.AddAppendStyle(names[i], aStyle);
|
|
end;
|
|
// Reset the used names array length, so we can reuse it to reduce the amount of SetLength calls
|
|
len := 0;
|
|
inc(c);
|
|
end;
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// 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
|
|
DisposeSvgAttrib(PSvgAttrib(attribs.List[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
|
|
c2 := SkipBlanksEx(c, endC);
|
|
c := ParseNameLength(c2, endC);
|
|
ToAsciiLowerUTF8String(c2, c, name);
|
|
|
|
//load the class's style (ie undotted style) if found.
|
|
style := owner.classStyles.GetStyle(name);
|
|
if style <> '' then ParseStyleAttribute(style);
|
|
Result := ParseAttributes(c, endC);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
class function TXmlEl.ParseAttribName(c: PUTF8Char;
|
|
endC: PUTF8Char; attrib: PSvgAttrib): PUTF8Char;
|
|
begin
|
|
Result := SkipBlanksEx(c, endC);
|
|
if Result >= endC then Exit;
|
|
c := Result;
|
|
Result := ParseNameLength(Result, endC);
|
|
ToUTF8String(c, Result, attrib.Name);
|
|
attrib.hash := GetHash(attrib.Name);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
class function TXmlEl.ParseAttribValue(c, endC: PUTF8Char;
|
|
attrib: PSvgAttrib): PUTF8Char;
|
|
// Parse: [Whitespaces] "=" [Whitespaces] ("'" | "\"") <string> ("'" | "\"")
|
|
var
|
|
quoteChar: UTF8Char;
|
|
c2: PUTF8Char;
|
|
begin
|
|
Result := endC;
|
|
|
|
// ParseNextChar:
|
|
c := SkipBlanksEx(c, endC);
|
|
if (c >= endC) or (c^ <> '=') then Exit;
|
|
inc(c); // '=' parsed
|
|
|
|
// ParseQuoteChar:
|
|
c := SkipBlanksEx(c, endC);
|
|
if c >= endC then Exit;
|
|
quoteChar := c^;
|
|
if not (quoteChar in [quote, dquote]) then Exit;
|
|
inc(c); // quote parsed
|
|
|
|
//trim leading and trailing spaces in the actual value
|
|
c := SkipBlanksEx(c, endC);
|
|
// find value end
|
|
Result := c;
|
|
while (Result < endC) and (Result^ <> quoteChar) do inc(Result);
|
|
c2 := Result;
|
|
while (c2 > c) and ((c2 -1)^ <= space) do dec(c2);
|
|
|
|
ToUTF8String(c, c2, attrib.value, sitPreserve);
|
|
inc(Result); //skip end quote
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
class function TXmlEl.ParseAttribNameAndValue(c, endC: PUTF8Char; attrib: PSvgAttrib): PUTF8Char;
|
|
begin
|
|
Result := ParseAttribName(c, endC, attrib);
|
|
if (Result < endC) then
|
|
Result := ParseAttribValue(Result, endC, attrib);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
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
|
|
case c^ of
|
|
'/', '?':
|
|
begin
|
|
inc(c);
|
|
if (c^ <> '>') then Exit; //error
|
|
selfClosed := true;
|
|
inc(c);
|
|
Result := true;
|
|
break;
|
|
end;
|
|
'>':
|
|
begin
|
|
inc(c);
|
|
Result := true;
|
|
break;
|
|
end;
|
|
'x':
|
|
if Match(c, 'xml:') then
|
|
begin
|
|
inc(c, 4); //ignore xml: prefixes
|
|
end;
|
|
end;
|
|
|
|
attrib := NewSvgAttrib();
|
|
c := ParseAttribNameAndValue(c, endC, attrib);
|
|
if c >= endC then
|
|
begin
|
|
DisposeSvgAttrib(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 True do
|
|
begin
|
|
c := SkipStyleBlanks(c, endC);
|
|
if c >= endC then Break;
|
|
|
|
c2 := c;
|
|
c := ParseStyleNameLen(c, endC);
|
|
ToUTF8String(c2, c, styleName);
|
|
if styleName = '' then Break;
|
|
|
|
// ParseNextChar
|
|
c := SkipStyleBlanks(c, endC);
|
|
if (c >= endC) or (c^ <> ':') then Break; //syntax check
|
|
inc(c);
|
|
|
|
c := SkipBlanksEx(c, endC);
|
|
if c >= endC then Break;
|
|
|
|
c2 := c;
|
|
inc(c);
|
|
while (c < endC) and (c^ <> ';') do inc(c);
|
|
ToTrimmedUTF8String(c2, c, styleVal);
|
|
inc(c);
|
|
|
|
attrib := NewSvgAttrib();
|
|
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 IsTextAreaTbreak(var c: PUTF8Char; endC: PUTF8Char): Boolean;
|
|
const
|
|
// https://www.w3.org/TR/SVGTiny12/text.html#tbreakElement
|
|
tbreak: PUTF8Char = '<tbreak/>';
|
|
begin
|
|
Result := (c + 9 < endC) and CompareMem(c, tbreak, 9);
|
|
if Result then inc(c, 8);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function TXmlEl.ParseContent(var c: PUTF8Char; endC: PUTF8Char): Boolean;
|
|
var
|
|
child : TSvgXmlEl;
|
|
entity : PSvgAttrib;
|
|
c2, cc : PUTF8Char;
|
|
tmpC, tmpEndC : PUTF8Char;
|
|
begin
|
|
Result := false;
|
|
// note: don't trim spaces at the start of text content.
|
|
// Text space trimming will be done later IF and when required.
|
|
while (hash = hText) or (hash = hTSpan) or
|
|
(hash = hTextArea) or SkipBlanks(c, endC) do
|
|
begin
|
|
if (c^ = '<') then
|
|
begin
|
|
inc(c);
|
|
case c^ of
|
|
'!':
|
|
begin
|
|
cc := c;
|
|
if Match(cc, '!--') then //start comment
|
|
begin
|
|
inc(cc, 3);
|
|
while (cc < endC) and ((cc^ <> '-') or
|
|
not Match(cc, '-->')) do inc(cc); //end comment
|
|
inc(cc, 3);
|
|
end else
|
|
begin
|
|
//it's very likely <![CDATA[
|
|
c2 := cc - 1;
|
|
if Match(cc, '![cdata[') then
|
|
begin
|
|
while (cc < endC) and ((cc^ <> ']') or not Match(cc, ']]>')) do
|
|
inc(cc);
|
|
ToUTF8String(c2, cc, text);
|
|
inc(cc, 3);
|
|
if (hash = hStyle) then
|
|
ParseStyleElementContent(text, owner.classStyles);
|
|
end else
|
|
begin
|
|
while (cc < endC) and (cc^ <> '<') do inc(cc);
|
|
ToUTF8String(c2, cc, text);
|
|
end;
|
|
end;
|
|
c := cc;
|
|
end;
|
|
'/', '?':
|
|
begin
|
|
//element closing tag
|
|
cc := c;
|
|
inc(cc);
|
|
if Match(cc, name) then
|
|
begin
|
|
inc(cc, Length(name));
|
|
//very rarely there's a space before '>'
|
|
cc := SkipBlanksEx(cc, endC);
|
|
Result := cc^ = '>';
|
|
inc(cc);
|
|
end;
|
|
c := cc;
|
|
Exit;
|
|
end;
|
|
else
|
|
begin
|
|
//starting a new element
|
|
child := TSvgXmlEl.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
|
|
// assume this is text content, and because text can also be mixed
|
|
// with any number of nested <tspan> elements, always put text
|
|
// content inside a pseudo 'self closed' <tspan> element
|
|
cc := c;
|
|
while (cc < endC) and (cc^ <> '<') do inc(cc);
|
|
child := TSvgXmlEl.Create(owner);
|
|
child.name := 'tspan';
|
|
child.hash := GetHash('tspan');
|
|
child.selfClosed := true; ////////////////////// :)))
|
|
childs.Add(child);
|
|
ToUTF8String(c, cc, child.text, sitPreserve);
|
|
c := cc;
|
|
end
|
|
else if (hash = hTextArea) then
|
|
begin
|
|
// also assume this is text content, but don't create
|
|
// pseudo <tspan> elements inside <textarea> elements
|
|
cc := c;
|
|
while (cc < endC) and
|
|
((cc^ <> '<') or IsTextAreaTbreak(cc, endC)) do inc(cc);
|
|
ToUTF8String(c, cc, text, sitPreserve);
|
|
c := cc;
|
|
end else
|
|
begin
|
|
cc := c;
|
|
while (cc < endC) and (cc^ <> '<') do inc(cc);
|
|
ToUTF8String(c, cc, text);
|
|
c := cc;
|
|
//if <style> element then load styles into owner.classStyles
|
|
if (hash = hStyle) then
|
|
ParseStyleElementContent(text, owner.classStyles);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// TDocTypeEl
|
|
//------------------------------------------------------------------------------
|
|
|
|
function TDocTypeEl.SkipWord(c, endC: PUTF8Char): PUTF8Char;
|
|
begin
|
|
while (c < endC) and (c^ > space) do inc(c);
|
|
inc(c);
|
|
Result := 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);
|
|
attrib := NewSvgAttrib();
|
|
|
|
c := ParseAttribName(c, endC, attrib);
|
|
if c >= endC then break;
|
|
|
|
SkipBlanks(c, endC);
|
|
if not IsQuoteChar(c^) 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 DisposeSvgAttrib(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 c := SkipWord(c, endC);
|
|
end;
|
|
end;
|
|
Result := (c < endC) and (c^ = '>');
|
|
inc(c);
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// TSvgTreeEl
|
|
//------------------------------------------------------------------------------
|
|
|
|
constructor TSvgXmlEl.Create(owner: TSvgParser);
|
|
begin
|
|
inherited Create(owner);
|
|
selfClosed := false;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
procedure TSvgXmlEl.Clear;
|
|
var
|
|
i: integer;
|
|
begin
|
|
for i := 0 to childs.Count -1 do
|
|
TSvgXmlEl(childs[i]).free;
|
|
childs.Clear;
|
|
inherited;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function TSvgXmlEl.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
|
|
begin
|
|
Result := PSvgAttrib(docType.attribs.List[i]);
|
|
if Result.hash = hash then 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;
|
|
//------------------------------------------------------------------------------
|
|
|
|
procedure ConvertUnicodeToUtf8(memStream: TMemoryStream);
|
|
var
|
|
i, len: LongInt;
|
|
encoding: TSvgEncoding;
|
|
s: UnicodeString;
|
|
wc: PWord;
|
|
utf8: UTF8String;
|
|
begin
|
|
memStream.Position := 0;
|
|
encoding := GetXmlEncoding(memStream.Memory, memStream.Size);
|
|
case encoding of
|
|
eUnicodeLE, eUnicodeBE: ;
|
|
else Exit;
|
|
end;
|
|
SetLength(s, memStream.Size div 2);
|
|
Move(memStream.Memory^, s[1], memStream.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);
|
|
memStream.SetSize(len);
|
|
Move(utf8[1], memStream.Memory^, len);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function TSvgParser.LoadFromStream(stream: TStream): Boolean;
|
|
begin
|
|
Clear;
|
|
Result := true;
|
|
try
|
|
svgStream.LoadFromStream(stream);
|
|
// very few SVG files are unicode encoded, almost all are Utf8
|
|
// so it's more efficient to parse them all as Utf8 encoded files
|
|
ConvertUnicodeToUtf8(svgStream);
|
|
ParseUtf8Stream;
|
|
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.ParseUtf8Stream;
|
|
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 := TSvgXmlEl.Create(self);
|
|
if svgTree.ParseHeader(c, endC) and
|
|
not svgTree.selfClosed then
|
|
svgTree.ParseContent(c, endC);
|
|
break;
|
|
end;
|
|
inc(c);
|
|
end;
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Miscellaneous functions
|
|
//------------------------------------------------------------------------------
|
|
|
|
function DecodeUtf8ToUnicode(const utf8: UTF8String): UnicodeString;
|
|
var
|
|
i,j, len: Integer;
|
|
c, cp: Cardinal;
|
|
codePoints: TArrayOfCardinal;
|
|
begin
|
|
Result := '';
|
|
if utf8 = '' then Exit;
|
|
len := Length(utf8);
|
|
|
|
// first decode utf8String to codepoints
|
|
SetLength(codePoints, len);
|
|
i := 1;
|
|
j := 0;
|
|
while i <= len do
|
|
begin
|
|
c := Ord(utf8[i]);
|
|
if c and $80 = 0 then // c < 128
|
|
begin
|
|
codePoints[j] := c;
|
|
inc(i); inc(j);
|
|
end
|
|
else if c and $E0 = $C0 then
|
|
begin
|
|
if i = len then break;
|
|
codePoints[j] := (c and $1F) shl 6 + (Ord(utf8[i+1]) and $3F);
|
|
inc(i, 2); inc(j);
|
|
end
|
|
else if c and $F0 = $E0 then
|
|
begin
|
|
if i > len - 2 then break;
|
|
codePoints[j] := (c and $F) shl 12 +
|
|
((Ord(utf8[i+1]) and $3F) shl 6) + ((Ord(utf8[i+2]) and $3F));
|
|
inc(i, 3); inc(j);
|
|
end else
|
|
begin
|
|
if (i > len - 3) or (c shr 3 <> $1E) then break;
|
|
codePoints[j] := (c and $7) shl 18 + ((Ord(utf8[i+1]) and $3F) shl 12) +
|
|
((Ord(utf8[i+2]) and $3F) shl 6) + (Ord(utf8[i+3]) and $3F);
|
|
inc(i, 4); inc(j);
|
|
end;
|
|
end;
|
|
len := j; // there are now 'j' valid codepoints
|
|
j := 0;
|
|
|
|
// make room in the result for surrogate paired chars, and
|
|
// convert codepoints into the result (a Utf16 string)
|
|
SetLength(Result, len *2);
|
|
for i := 0 to len -1 do
|
|
begin
|
|
inc(j);
|
|
cp := codePoints[i];
|
|
if (cp < $D7FF) or (cp = $E000) or (cp = $FFFF) then
|
|
begin
|
|
Result[j] := WideChar(cp);
|
|
end else if (cp > $FFFF) and (cp < $110000) then
|
|
begin
|
|
Dec(cp, $10000);
|
|
Result[j] := WideChar($D800 + (cp shr 10));
|
|
inc(j);
|
|
Result[j] := WideChar($DC00 + (cp and $3FF));
|
|
end;
|
|
end;
|
|
SetLength(Result, j);
|
|
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 // already checked by TValue.GetValue, the only function calling this code
|
|
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.GetRectD(relSizeX, relSizeY: double; assumeRelValBelow: Double): TRectD;
|
|
begin
|
|
if not left.IsValid then
|
|
Result.Left := 0 else
|
|
Result.Left := left.GetValue(relSizeX, assumeRelValBelow);
|
|
|
|
if not top.IsValid then
|
|
Result.Top := 0 else
|
|
Result.Top := top.GetValue(relSizeY, assumeRelValBelow);
|
|
|
|
Result.Right := Result.Left + width.GetValue(relSizeX, assumeRelValBelow);
|
|
Result.Bottom := Result.Top + height.GetValue(relSizeY, 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
|
|
//------------------------------------------------------------------------------
|
|
|
|
procedure TClassStylesList.Grow(NewCapacity: Integer);
|
|
var
|
|
Len, I: Integer;
|
|
Index: Integer;
|
|
begin
|
|
Len := Length(FItems);
|
|
if NewCapacity < 0 then
|
|
begin
|
|
if Len < 5 then
|
|
Len := 5
|
|
else
|
|
Len := Len * 2;
|
|
end
|
|
else if NewCapacity <= Len then
|
|
Exit
|
|
else
|
|
Len := NewCapacity;
|
|
|
|
SetLength(FItems, Len);
|
|
FMod := Cardinal(Len);
|
|
if not Odd(FMod) then
|
|
Inc(FMod);
|
|
SetLengthUninit(FBuckets, FMod);
|
|
FillChar(FBuckets[0], FMod * SizeOf(FBuckets[0]), $FF);
|
|
|
|
// Rehash
|
|
for I := 0 to FCount - 1 do
|
|
begin
|
|
Index := (FItems[I].Hash and $7FFFFFFF) mod FMod;
|
|
FItems[I].Next := FBuckets[Index];
|
|
FBuckets[Index] := I;
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
procedure TClassStylesList.Preallocate(AdditionalItemCount: Integer);
|
|
begin
|
|
if AdditionalItemCount > 2 then
|
|
Grow(Length(FItems) + AdditionalItemCount);
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function TClassStylesList.FindItemIndex(const Name: UTF8String): Integer;
|
|
begin
|
|
Result := -1;
|
|
FNameHash := GetHash(Name);
|
|
if FMod <> 0 then
|
|
begin
|
|
Result := FBuckets[(FNameHash and $7FFFFFFF) mod FMod];
|
|
while (Result <> -1) and
|
|
((FItems[Result].Hash <> FNameHash) or
|
|
not IsSameUTF8String(FItems[Result].Name, Name)) do
|
|
Result := FItems[Result].Next;
|
|
end;
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
procedure TClassStylesList.AddAppendStyle(const Name, Style: UTF8String);
|
|
var
|
|
Index: Integer;
|
|
Item: PClassStyleListItem;
|
|
Bucket: PInteger;
|
|
begin
|
|
Index := FindItemIndex(Name);
|
|
if Index <> -1 then
|
|
begin
|
|
Item := @FItems[Index];
|
|
if (Item.Style <> '') and (Item.Style[Length(Item.Style)] <> ';') then
|
|
Item.Style := Item.Style + ';' + Style
|
|
else
|
|
Item.Style := Item.Style + Style;
|
|
end
|
|
else
|
|
begin
|
|
if FCount = Length(FItems) then
|
|
Grow;
|
|
Index := FCount;
|
|
Inc(FCount);
|
|
|
|
Bucket := @FBuckets[(FNameHash and $7FFFFFFF) mod FMod];
|
|
Item := @FItems[Index];
|
|
Item.Next := Bucket^;
|
|
Item.Hash := FNameHash;
|
|
Item.Name := Name;
|
|
Item.Style := style;
|
|
Bucket^ := Index;
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function TClassStylesList.GetStyle(const Name: UTF8String): UTF8String;
|
|
var
|
|
Index: Integer;
|
|
begin
|
|
if FCount = 0 then
|
|
Result := ''
|
|
else
|
|
begin
|
|
Index := FindItemIndex(Name);
|
|
if Index <> -1 then
|
|
Result := FItems[Index].Style
|
|
else
|
|
Result := '';
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
procedure TClassStylesList.Clear;
|
|
begin
|
|
FCount := 0;
|
|
FMod := 0;
|
|
FItems := nil;
|
|
FBuckets := nil;
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// TColorConstList
|
|
//------------------------------------------------------------------------------
|
|
|
|
constructor TColorConstList.Create(Colors: PColorConst; Count: Integer);
|
|
var
|
|
I: Integer;
|
|
Bucket: PPColorConstMapItem;
|
|
Item: PColorConstMapItem;
|
|
begin
|
|
inherited Create;
|
|
FCount := Count;
|
|
SetLength(FItems, FCount);
|
|
|
|
FMod := FCount * 2 + 1; // gives us 3 color constants as max. bucket depth
|
|
SetLength(FBuckets, FMod);
|
|
|
|
// Initialize FItems[] and fill the buckets
|
|
for I := 0 to Count - 1 do
|
|
begin
|
|
Item := @FItems[I];
|
|
Item.Data := Colors; // link the constant to the ColorConstMapItem
|
|
Inc(Colors);
|
|
Item.Hash := GetHash(Item.Data.ColorName); // case-insensitive
|
|
Bucket := @FBuckets[(Cardinal(Item.Hash) and $7FFFFFFF) mod FMod];
|
|
Item.Next := Bucket^;
|
|
Bucket^ := Item;
|
|
end;
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
function TColorConstList.GetColorValue(const ColorName: UTF8String; var Color: TColor32): Boolean;
|
|
var
|
|
Hash: Cardinal;
|
|
Item: PColorConstMapItem;
|
|
begin
|
|
Hash := GetHash(ColorName);
|
|
Item := FBuckets[(Cardinal(Hash) and $7FFFFFFF) mod FMod];
|
|
while (Item <> nil) and
|
|
not IsSameAsciiUTF8String(Item.Data.ColorName, ColorName) do
|
|
Item := Item.Next;
|
|
if Item <> nil then
|
|
begin
|
|
Color := Item.Data.ColorValue;
|
|
Result := True;
|
|
end
|
|
else
|
|
Result := False;
|
|
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;
|
|
{$I Img32.SVG.HtmlColorConsts.inc}
|
|
begin
|
|
ColorConstList := TColorConstList.Create(@ColorConsts, Length(ColorConsts));
|
|
end;
|
|
//------------------------------------------------------------------------------
|
|
|
|
procedure CleanupColorConstList;
|
|
begin
|
|
FreeAndNil(ColorConstList);
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
//------------------------------------------------------------------------------
|
|
|
|
initialization
|
|
MakeLowerCaseTable;
|
|
MakeColorConstList;
|
|
|
|
finalization
|
|
CleanupColorConstList;
|
|
end.
|