Skip to content

Commit 82a2cfa

Browse files
ver. 3.4.1
- Added single-instance behavior: opening a SVG file now reuses the running editor instead of starting a new one. - Added Settings option to remember and reopen files from the last session.
1 parent e68cc44 commit 82a2cfa

32 files changed

Lines changed: 595 additions & 211 deletions

Ext/SVGIconImageList/Image32/source/Img32.SVG.Core.pas

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
(*******************************************************************************
44
* Author : Angus Johnson *
55
* Version : 4.9 *
6-
* Date : 15 December 2025 *
6+
* Date : 8 April 2026 *
77
* Website : https://www.angusj.com *
8-
* Copyright : Angus Johnson 2019-2025 *
8+
* Copyright : Angus Johnson 2019-2026 *
99
* *
1010
* Purpose : Essential structures and functions to read SVG files *
1111
* *
@@ -244,9 +244,14 @@ TSvgParser = class
244244
skipComma: Boolean; out val: double): Boolean;
245245
function ParseNextNumEx(var c: PUTF8Char; endC: PUTF8Char; skipComma: Boolean;
246246
out val: double; out unitType: TUnitType): Boolean;
247-
function GetHash(c: PUTF8Char; len: nativeint): cardinal; overload;
248-
function GetHash(const name: UTF8String): cardinal; overload; {$IFDEF INLINE} inline; {$ENDIF}
249-
function GetHashCaseSensitive(name: PUTF8Char; nameLen: integer): cardinal;
247+
248+
function GetHashCaseSensitive(c: PUTF8Char; len: nativeint): cardinal; overload;
249+
function GetHashCaseInsensitive(c: PUTF8Char; len: nativeint): cardinal; overload;
250+
function GetHashCaseSensitive(const name: UTF8String): cardinal; overload; {$IFDEF INLINE} inline; {$ENDIF}
251+
function GetHashCaseInsensitive(const name: UTF8String): cardinal; overload; {$IFDEF INLINE} inline; {$ENDIF}
252+
253+
// function GetHashCaseSensitive(name: PUTF8Char; nameLen: integer): cardinal; overload;
254+
// function GetHashCaseInsensitive(name: PUTF8Char; nameLen: integer): cardinal; overload;
250255
function ExtractRef(const href: UTF8String): UTF8String;
251256
function IsNumPending(var c: PUTF8Char;
252257
endC: PUTF8Char; ignoreComma: Boolean): Boolean;
@@ -295,6 +300,7 @@ TSvgParser = class
295300
TSetOfUTF8Char = set of UTF8Char;
296301

297302
function CharInSet(chr: UTF8Char; const chrs: TSetOfUTF8Char): Boolean;
303+
{$IFDEF INLINE} inline; {$ENDIF}
298304
function DecodeUtf8ToUnicode(const utf8: UTF8String): UnicodeString;
299305

300306
const
@@ -599,7 +605,8 @@ function Match(c: PUTF8Char; const compare: UTF8String): Boolean;
599605
Result := false;
600606
for i := 1 to Length(compare) do
601607
begin
602-
if LowerCaseTable[c[i - 1]] <> compare[i] then Exit;
608+
//if LowerCaseTable[c[i - 1]] <> compare[i] then Exit;
609+
if c[i - 1] <> compare[i] then Exit;
603610
end;
604611
Result := true;
605612
end;
@@ -616,7 +623,8 @@ function Match(const compare1, compare2: UTF8String): Boolean;
616623
c1 := @compare1[1]; c2 := @compare2[1];
617624
for i := 1 to len do
618625
begin
619-
if LowerCaseTable[c1[i - 1]] <> LowerCaseTable[c2[i - 1]] then Exit;
626+
//if LowerCaseTable[c1[i - 1]] <> LowerCaseTable[c2[i - 1]] then Exit;
627+
if c1[i - 1] <> c2[i - 1] then Exit;
620628
end;
621629
Result := true;
622630
end;
@@ -888,7 +896,7 @@ function ParseNextWordHash(var c: PUTF8Char; endC: PUTF8Char; out hash: cardinal
888896
end;
889897
end;
890898
c := cc;
891-
hash := GetHash(c2, cc - c2);
899+
hash := GetHashCaseSensitive(c2, cc - c2);
892900
Result := True;
893901
end;
894902
//------------------------------------------------------------------------------
@@ -912,7 +920,7 @@ function ParseNextWordHash(c, endC: PUTF8Char): cardinal;
912920
else break;
913921
end;
914922
end;
915-
Result := GetHash(c2, c - c2);
923+
Result := GetHashCaseInsensitive(c2, c - c2);
916924
end;
917925
//------------------------------------------------------------------------------
918926

@@ -935,7 +943,7 @@ function ParseNextWordExHash(var c: PUTF8Char; endC: PUTF8Char;
935943
inc(c);
936944
c2 := cc;
937945
while (cc < endC) and (cc^ <> quote) do inc(cc);
938-
hash := GetHash(c2, cc - c2);
946+
hash := GetHashCaseSensitive(c2, cc - c2);
939947
inc(cc);
940948
end else
941949
begin
@@ -952,7 +960,7 @@ function ParseNextWordExHash(var c: PUTF8Char; endC: PUTF8Char;
952960
'A'..'Z', 'a'..'z', '-', '_': inc(cc);
953961
else break;
954962
end;
955-
hash := GetHash(c2, cc - c2);
963+
hash := GetHashCaseSensitive(c2, cc - c2);
956964
end;
957965
c := cc;
958966
Result := True;
@@ -974,7 +982,7 @@ function ParseNameLength(c: PUTF8Char; endC: PUTF8Char): PUTF8Char;
974982
//------------------------------------------------------------------------------
975983

976984
{$OVERFLOWCHECKS OFF}
977-
function GetHash(c: PUTF8Char; len: nativeint): cardinal;
985+
function GetHashCaseSensitive(c: PUTF8Char; len: nativeint): cardinal;
978986
var
979987
i: integer;
980988
begin
@@ -983,7 +991,8 @@ function GetHash(c: PUTF8Char; len: nativeint): cardinal;
983991
if c = nil then Exit;
984992
for i := 1 to len do
985993
begin
986-
Result := (Result + Ord(LowerCaseTable[c^]));
994+
//Result := (Result + Ord(LowerCaseTable[c^]));
995+
Result := Result + Ord(c^);
987996
Result := Result + (Result shl 10);
988997
Result := Result xor (Result shr 6);
989998
inc(c);
@@ -992,40 +1001,46 @@ function GetHash(c: PUTF8Char; len: nativeint): cardinal;
9921001
Result := Result xor (Result shr 11);
9931002
Result := Result + (Result shl 15);
9941003
end;
995-
{$IFDEF OVERFLOWCHECKS_ENABLED}
996-
{$OVERFLOWCHECKS ON}
997-
{$ENDIF}
9981004
//------------------------------------------------------------------------------
9991005

1000-
function GetHash(const name: UTF8String): cardinal;
1001-
begin
1002-
// skip function call by directly casting it to Pointer
1003-
Result := GetHash(PUTF8Char(Pointer(name)), Length(name));
1004-
end;
1005-
//------------------------------------------------------------------------------
1006-
1007-
{$OVERFLOWCHECKS OFF}
1008-
function GetHashCaseSensitive(name: PUTF8Char; nameLen: integer): cardinal;
1006+
function GetHashCaseInsensitive(c: PUTF8Char; len: nativeint): cardinal;
10091007
var
10101008
i: integer;
10111009
begin
1010+
//https://en.wikipedia.org/wiki/Jenkins_hash_function
10121011
Result := 0;
1013-
for i := 1 to nameLen do
1012+
if c = nil then Exit;
1013+
for i := 1 to len do
10141014
begin
1015-
Result := (Result + Ord(name^));
1015+
Result := (Result + Ord(LowerCaseTable[c^]));
10161016
Result := Result + (Result shl 10);
10171017
Result := Result xor (Result shr 6);
1018-
inc(name);
1018+
inc(c);
10191019
end;
10201020
Result := Result + (Result shl 3);
10211021
Result := Result xor (Result shr 11);
10221022
Result := Result + (Result shl 15);
10231023
end;
1024+
10241025
{$IFDEF OVERFLOWCHECKS_ENABLED}
10251026
{$OVERFLOWCHECKS ON}
10261027
{$ENDIF}
10271028
//------------------------------------------------------------------------------
10281029

1030+
function GetHashCaseSensitive(const name: UTF8String): cardinal;
1031+
begin
1032+
// skip function call by directly casting it to Pointer
1033+
Result := GetHashCaseSensitive(PUTF8Char(Pointer(name)), Length(name));
1034+
end;
1035+
//------------------------------------------------------------------------------
1036+
1037+
function GetHashCaseInsensitive(const name: UTF8String): cardinal;
1038+
begin
1039+
// skip function call by directly casting it to Pointer
1040+
Result := GetHashCaseInsensitive(PUTF8Char(Pointer(name)), Length(name));
1041+
end;
1042+
//------------------------------------------------------------------------------
1043+
10291044
function ParseNextWordHashed(var c: PUTF8Char; endC: PUTF8Char): cardinal;
10301045
var
10311046
c2: PUTF8Char;
@@ -1035,7 +1050,7 @@ function ParseNextWordHashed(var c: PUTF8Char; endC: PUTF8Char): cardinal;
10351050
c := ParseNameLength(c2, endC);
10361051
len := c - c2;
10371052
if len <= 0 then Result := 0
1038-
else Result := GetHash(c2, len);
1053+
else Result := GetHashCaseInsensitive(c2, len);
10391054
end;
10401055
//------------------------------------------------------------------------------
10411056

@@ -1555,7 +1570,7 @@ function IsKnownEntity(owner: TSvgParser;
15551570
inc(c); //skip ampersand.
15561571
c2 := c; c3 := c;
15571572
c3 := ParseNameLength(c3, endC);
1558-
entity := owner.FindEntity(GetHash(c2, c3 - c2));
1573+
entity := owner.FindEntity(GetHashCaseInsensitive(c2, c3 - c2));
15591574
Result := (c3^ = ';') and Assigned(entity);
15601575
//nb: increments 'c' only if the entity is found.
15611576
if Result then c := c3 +1 else dec(c);
@@ -2168,7 +2183,7 @@ class function TXmlEl.ParseAttribName(c, endC: PUTF8Char; attrib: PSvgAttrib): P
21682183
Delete(attrib.name, 1, i);
21692184
end;
21702185

2171-
attrib.hash := GetHash(attrib.Name);
2186+
attrib.hash := GetHashCaseInsensitive(attrib.Name);
21722187
end;
21732188
//------------------------------------------------------------------------------
21742189

@@ -2346,7 +2361,7 @@ procedure TXmlEl.ParseStyleAttribute(const style: UTF8String);
23462361
attrib := NewSvgAttrib();
23472362
attrib.name := styleName;
23482363
attrib.value := styleVal;
2349-
attrib.hash := GetHash(attrib.name);
2364+
attrib.hash := GetHashCaseInsensitive(attrib.name);
23502365
attribs.Add(attrib);
23512366
end;
23522367
end;
@@ -2484,7 +2499,7 @@ function TXmlEl.ParseContent(var c: PUTF8Char; endC: PUTF8Char): Boolean;
24842499
while (cc < endC) and (cc^ <> '<') do inc(cc);
24852500
child := TSvgXmlEl.Create(owner);
24862501
child.name := 'tspan';
2487-
child.hash := GetHash('tspan');
2502+
child.hash := GetHashCaseInsensitive('tspan');
24882503
child.selfClosed := true; ////////////////////// :)))
24892504
childs.Add(child);
24902505
ToUTF8String(c, cc, child.text, sitPreserve);
@@ -2602,7 +2617,7 @@ procedure TSvgXmlEl.Clear;
26022617
function TSvgXmlEl.ParseHeader(var c: PUTF8Char; endC: PUTF8Char): Boolean;
26032618
begin
26042619
Result := inherited ParseHeader(c, endC);
2605-
if Result then hash := GetHash(name);
2620+
if Result then hash := GetHashCaseInsensitive(name);
26062621
end;
26072622
//------------------------------------------------------------------------------
26082623

@@ -3200,7 +3215,7 @@ procedure TClassStylesList.Preallocate(AdditionalItemCount: Integer);
32003215
function TClassStylesList.FindItemIndex(const Name: UTF8String): Integer;
32013216
begin
32023217
Result := -1;
3203-
FNameHash := GetHash(Name);
3218+
FNameHash := GetHashCaseInsensitive(Name);
32043219
if FMod <> 0 then
32053220
begin
32063221
Result := FBuckets[(FNameHash and $7FFFFFFF) mod FMod];
@@ -3294,7 +3309,7 @@ constructor TColorConstList.Create(Colors: PColorConst; Count: Integer);
32943309
Item := @FItems[I];
32953310
Item.Data := Colors; // link the constant to the ColorConstMapItem
32963311
Inc(Colors);
3297-
Item.Hash := GetHash(Item.Data.ColorName); // case-insensitive
3312+
Item.Hash := GetHashCaseInsensitive(Item.Data.ColorName);
32983313
Bucket := @FBuckets[(Cardinal(Item.Hash) and $7FFFFFFF) mod FMod];
32993314
Item.Next := Bucket^;
33003315
Bucket^ := Item;
@@ -3307,7 +3322,7 @@ function TColorConstList.GetColorValue(const ColorName: UTF8String; var Color: T
33073322
Hash: Cardinal;
33083323
Item: PColorConstMapItem;
33093324
begin
3310-
Hash := GetHash(ColorName);
3325+
Hash := GetHashCaseInsensitive(ColorName);
33113326
Item := FBuckets[(Cardinal(Hash) and $7FFFFFFF) mod FMod];
33123327
while (Item <> nil) and
33133328
not IsSameAsciiUTF8String(Item.Data.ColorName, ColorName) do

Ext/SVGIconImageList/Image32/source/Img32.SVG.Path.pas

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
(*******************************************************************************
44
* Author : Angus Johnson *
55
* Version : 4.9 *
6-
* Date : 9 August 2025 *
6+
* Date : 8 April 2026 *
77
* Website : https://www.angusj.com *
8-
* Copyright : Angus Johnson 2019-2025 *
8+
* Copyright : Angus Johnson 2019-2026 *
99
* *
1010
* Purpose : Essential structures and functions to read SVG Path elements *
1111
* *
@@ -1261,11 +1261,36 @@ function TSvgSubPath.AddZSeg(const endPt, firstPt: TPointD): TSvgZSegment;
12611261
var
12621262
i: integer;
12631263
begin
1264+
// If the previous command is missing required coordinate data, then the
1265+
// initial point is used to fill in the missing data (but only for the last
1266+
// point or points).
1267+
// https://www.w3.org/TR/2015/WD-SVG2-20150915/paths.html#PathDataClosePathCommand
1268+
12641269
i := fSegsCount;
12651270
if i = Length(fSegs) then
12661271
GrowSegs;
12671272
inc(fSegsCount);
12681273

1274+
if (i > 0) then
1275+
begin
1276+
if (fSegs[i-1] is TSvgCSegment) then
1277+
begin
1278+
while (Length(TSvgCSegment(fSegs[i-1]).fCtrlPts) < 3) do
1279+
AppendPoint(TSvgCSegment(fSegs[i-1]).fCtrlPts, GetFirstPt);
1280+
end
1281+
else if (fSegs[i-1] is TSvgSSegment) then
1282+
begin
1283+
while (Length(TSvgCSegment(fSegs[i-1]).fCtrlPts) < 2) do
1284+
AppendPoint(TSvgCSegment(fSegs[i-1]).fCtrlPts, GetFirstPt);
1285+
end
1286+
else if (fSegs[i-1] is TSvgQSegment) then
1287+
begin
1288+
while (Length(TSvgCSegment(fSegs[i-1]).fCtrlPts) < 2) do
1289+
AppendPoint(TSvgCSegment(fSegs[i-1]).fCtrlPts, GetFirstPt);
1290+
end;
1291+
end;
1292+
1293+
12691294
Result := TSvgZSegment.Create(self, i, endPt);
12701295
fSegs[i] := Result;
12711296
NewPointDArray(Result.fCtrlPts, 1, True);
@@ -1497,7 +1522,7 @@ procedure TSvgPath.Parse(const value: UTF8String);
14971522
firstPt : TPointD;
14981523
lastPt : TPointD;
14991524
currPt : TPointD;
1500-
pt2, pt3 : TPointD;
1525+
pt2 : TPointD;
15011526
angle : double;
15021527
sweepCW : integer;
15031528
largeArc : integer;
@@ -1524,25 +1549,31 @@ procedure TSvgPath.Parse(const value: UTF8String);
15241549
begin
15251550
// Count the numbers before the next segment type char
15261551
ptCap := 0;
1552+
dec(endC); // trailing double-quote.
15271553
while c < endC do
15281554
begin
1529-
// skip whitespaces
1555+
1556+
// valid separators are whitespace and/or a comma
1557+
// https://www.w3.org/TR/SVG2/paths.html
1558+
while (c < endC) and (c^ <= space) do
1559+
inc(c);
1560+
if (c < endC) and (c^ = comma) then
1561+
inc(c);
15301562
while (c < endC) and (c^ <= space) do
15311563
inc(c);
15321564

1533-
if c >= endC then
1534-
break;
1565+
if (c >= endC) or
1566+
(not CharInSet(c^, ['0'..'9', '-', '.'])) then
1567+
break;
1568+
inc(c);
15351569

1536-
case c^ of
1537-
'0'..'9', '-', '.', 'E', 'e':
1538-
begin
1539-
while (c < endC) and (c^ > space) do
1540-
inc(c);
1541-
Inc(ptCap);
1542-
end;
1543-
else
1544-
Break;
1545-
end;
1570+
// nb: a minus char in the following loop
1571+
// must belong to the following value
1572+
while (c < endC) and
1573+
CharInSet(c^, ['0'..'9', '.', 'E', 'e']) do
1574+
inc(c);
1575+
1576+
Inc(ptCap);
15461577
end;
15471578
ptCap := ptCap div 2; // two numbers are one point
15481579
SetLength(pts, ptCap);
@@ -1675,17 +1706,15 @@ procedure TSvgPath.Parse(const value: UTF8String);
16751706
stCBezier:
16761707
begin
16771708
AllocEstimatedPtsCount(c, endC);
1709+
16781710
while IsNumPending(c, endC, true) and
1679-
Parse2Num(c, endC, pt2, lastPt) and
1680-
Parse2Num(c, endC, pt3, lastPt) and
16811711
Parse2Num(c, endC, currPt, lastPt) do
16821712
begin
1683-
AddPt(pt2);
1684-
AddPt(pt3);
16851713
AddPt(currPt);
1686-
if isRelative then lastPt := currPt;
1714+
if isRelative and (ptCnt mod 3 = 0) then
1715+
lastPt := currPt;
16871716
end;
1688-
if Length(pts) <> ptCnt then
1717+
if Length(pts) > ptCnt then
16891718
SetLength(pts, ptCnt);
16901719
currSubPath.AddSeg(stCBezier, firstPt, pts);
16911720
end;

0 commit comments

Comments
 (0)