{ Reads an SVG Document License: The same modified LGPL as the Free Pascal RTL See the file COPYING.modifiedLGPL for more details AUTHORS: Felipe Monteiro de Carvalho } unit svgvectorialreader; {$mode objfpc}{$H+} {$define SVG_MERGE_LAYER_STYLES} interface uses Classes, SysUtils, math, fpimage, fpcanvas, laz2_xmlread, laz2_dom, fgl, fpvectorial, fpvutils, lazutf8, TypInfo; type TSVGTokenType = ( // moves sttMoveTo, sttRelativeMoveTo, // Close Path sttClosePath, // lines sttLineTo, sttRelativeLineTo, sttHorzLineTo, sttRelativeHorzLineTo, sttVertLineTo, sttRelativeVertLineTo, // cubic beziers sttBezierTo, sttRelativeBezierTo, sttSmoothBezierTo, sttRelativeSmoothBezierTo, // quadratic beziers sttQuadraticBezierTo, sttRelativeQuadraticBezierTo, // Elliptic curves sttEllipticArcTo, sttRelativeEllipticArcTo, // numbers sttFloatValue); TSVGToken = class TokenType: TSVGTokenType; Value: Float; end; TSVGTokenList = specialize TFPGList; { TSVGPathTokenizer } TSVGPathTokenizer = class public FPointSeparator, FCommaSeparator: TFormatSettings; Tokens: TSVGTokenList; constructor Create; Destructor Destroy; override; procedure AddToken(AStr: string); procedure TokenizePathString(AStr: string); function DebugOutTokensAsString: string; end; { TvSVGVectorialReader } TvSVGVectorialReader = class(TvCustomVectorialReader) private FPointSeparator, FCommaSeparator: TFormatSettings; FSVGPathTokenizer: TSVGPathTokenizer; FLayerStylesKeys, FLayerStylesValues: TFPList; // of TStringList; function ReadSVGColor(AValue: string): TFPColor; function ReadSVGStyle(AValue: string; ADestEntity: TvEntityWithPen; AUseFillAsPen: Boolean = False): TvSetPenBrushAndFontElements; function ReadSVGStyleToStyleLists(AValue: string; AStyleKeys, AStyleValues: TStringList): TvSetPenBrushAndFontElements; function ReadSVGPenStyleWithKeyAndValue(AKey, AValue: string; ADestEntity: TvEntityWithPen): TvSetPenBrushAndFontElements; function ReadSVGBrushStyleWithKeyAndValue(AKey, AValue: string; ADestEntity: TvEntityWithPenAndBrush): TvSetPenBrushAndFontElements; function ReadSVGFontStyleWithKeyAndValue(AKey, AValue: string; ADestEntity: TvEntityWithPenBrushAndFont): TvSetPenBrushAndFontElements; function IsAttributeFromStyle(AStr: string): Boolean; procedure ApplyLayerStyles(ADestEntity: TvEntity); // procedure ReadEntityFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); procedure ReadCircleFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); procedure ReadEllipseFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); procedure ReadLayerFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); procedure ReadLineFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); procedure ReadPathFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); procedure ReadPathFromString(AStr: string; AData: TvVectorialPage; ADoc: TvVectorialDocument); procedure ReadNextPathCommand(ACurTokenType: TSVGTokenType; var i: Integer; var CurX, CurY: Double; AData: TvVectorialPage; ADoc: TvVectorialDocument); procedure ReadPointsFromString(AStr: string; AData: TvVectorialPage; ADoc: TvVectorialDocument; AClosePath: Boolean); procedure ReadPolyFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); procedure ReadRectFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); procedure ReadTextFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); procedure ReadUseFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); function StringWithUnitToFloat(AStr: string): Double; procedure ConvertSVGCoordinatesToFPVCoordinates( const AData: TvVectorialPage; const ASrcX, ASrcY: Double; var ADestX, ADestY: Double); procedure ConvertSVGDeltaToFPVDelta( const AData: TvVectorialPage; const ASrcX, ASrcY: Double; var ADestX, ADestY: Double); public { General reading methods } constructor Create; override; Destructor Destroy; override; procedure ReadFromStream(AStream: TStream; AData: TvVectorialDocument); override; procedure ReadFromXML(Doc: TXMLDocument; AData: TvVectorialDocument); end; implementation const // SVG requires hardcoding a DPI value // The Opera Browser and Inkscape use 90 DPI, so we follow that // 1 Inch = 25.4 milimiters // 90 inches per pixel = (1 / 90) * 25.4 = 0.2822 // FLOAT_MILIMETERS_PER_PIXEL = 0.3528; // DPI 72 = 1 / 72 inches per pixel FLOAT_MILIMETERS_PER_PIXEL = 0.2822; // DPI 90 = 1 / 90 inches per pixel FLOAT_PIXELS_PER_MILIMETER = 3.5433; // DPI 90 = 1 / 90 inches per pixel { TSVGPathTokenizer } constructor TSVGPathTokenizer.Create; begin inherited Create; FPointSeparator := DefaultFormatSettings; FPointSeparator.DecimalSeparator := '.'; FPointSeparator.ThousandSeparator := '#';// disable the thousand separator Tokens := TSVGTokenList.Create; end; destructor TSVGPathTokenizer.Destroy; begin Tokens.Free; inherited Destroy; end; procedure TSVGPathTokenizer.AddToken(AStr: string); var lToken: TSVGToken; lStr: string; begin lToken := TSVGToken.Create; lStr := Trim(AStr); if lStr = '' then Exit; // Moves if lStr[1] = 'M' then lToken.TokenType := sttMoveTo else if lStr[1] = 'm' then lToken.TokenType := sttRelativeMoveTo // Close Path else if lStr[1] = 'Z' then lToken.TokenType := sttClosePath else if lStr[1] = 'z' then lToken.TokenType := sttClosePath // Lines else if lStr[1] = 'L' then lToken.TokenType := sttLineTo else if lStr[1] = 'l' then lToken.TokenType := sttRelativeLineTo else if lStr[1] = 'H' then lToken.TokenType := sttHorzLineTo else if lStr[1] = 'h' then lToken.TokenType := sttRelativeHorzLineTo else if lStr[1] = 'V' then lToken.TokenType := sttVertLineTo else if lStr[1] = 'v' then lToken.TokenType := sttRelativeVertLineTo // cubic Bézier curve commands else if lStr[1] = 'C' then lToken.TokenType := sttBezierTo else if lStr[1] = 'c' then lToken.TokenType := sttRelativeBezierTo else if lStr[1] = 'S' then lToken.TokenType := sttSmoothBezierTo else if lStr[1] = 's' then lToken.TokenType := sttRelativeSmoothBezierTo // quadratic beziers else if lStr[1] = 'Q' then lToken.TokenType := sttQuadraticBezierTo else if lStr[1] = 'q' then lToken.TokenType := sttRelativeQuadraticBezierTo // Elliptic curves else if lStr[1] = 'A' then lToken.TokenType := sttEllipticArcTo else if lStr[1] = 'a' then lToken.TokenType := sttRelativeEllipticArcTo else begin lToken.TokenType := sttFloatValue; lToken.Value := StrToFloat(AStr, FPointSeparator); end; // Sometimes we get a command glued to a value, for example M150 if (lToken.TokenType <> sttFloatValue) and (Length(lStr) > 1) then begin Tokens.Add(lToken); lToken.TokenType := sttFloatValue; lStr := Copy(AStr, 2, Length(AStr)); lToken.Value := StrToFloat(lStr, FPointSeparator); end; Tokens.Add(lToken); end; procedure TSVGPathTokenizer.TokenizePathString(AStr: string); const Str_Space: Char = ' '; Str_Comma: Char = ','; var i: Integer; lTmpStr: string = ''; lState: Integer; lFirstTmpStrChar, lCurChar: Char; begin lState := 0; i := 1; while i <= Length(AStr) do begin case lState of 0: // Adding to the tmp string begin lCurChar := AStr[i]; if lCurChar = Str_Space then begin lState := 1; AddToken(lTmpStr); lTmpStr := ''; end else if lCurChar = Str_Comma then begin AddToken(lTmpStr); lTmpStr := ''; end else begin // Check for a break, from letter to number if (Length(lTmpStr) >= 1) then begin lFirstTmpStrChar := lTmpStr[1]; if ((lFirstTmpStrChar in ['a'..'z', 'A'..'Z']) and not (lCurChar in ['a'..'z', 'A'..'Z'])) or (not (lFirstTmpStrChar in ['a'..'z', 'A'..'Z']) and (lCurChar in ['a'..'z', 'A'..'Z'])) then begin AddToken(lTmpStr); lTmpStr := ''; Continue; end; end; lTmpStr := lTmpStr + lCurChar; end; Inc(i); end; 1: // Removing spaces begin if AStr[i] <> Str_Space then lState := 0 else Inc(i); end; end; end; // If there is a token still to be added, add it now if (lState = 0) and (lTmpStr <> '') then AddToken(lTmpStr); end; function TSVGPathTokenizer.DebugOutTokensAsString: string; var i: Integer; begin for i := 0 to Tokens.Count-1 do Result := Result + GetEnumName(TypeInfo(TSVGTokenType), integer(Tokens.Items[i].TokenType)) + Format('(%f) ', [Tokens.Items[i].Value]); end; { Example of a supported SVG image: } { TvSVGVectorialReader } function TvSVGVectorialReader.ReadSVGColor(AValue: string): TFPColor; var lValue, lStr: string; lStrings: TStringList; i: Integer; begin Result := colBlack; lValue := Trim(LowerCase(AValue)); // Support for rgb(255,255,0) if (Length(lValue) > 3) and (Copy(lValue, 0, 3) = 'rgb') then begin lStrings := TStringList.Create; try lStr := Copy(lValue, 5, Length(lValue)-5); lStrings.Delimiter := ','; lStrings.DelimitedText := lStr; if lStrings.Count = 3 then begin Result.Red := StrToInt(lStrings.Strings[0]) * $101; Result.Green := StrToInt(lStrings.Strings[1]) * $101; Result.Blue := StrToInt(lStrings.Strings[2]) * $101; end else raise Exception.Create(Format('[TvSVGVectorialReader.ReadSVGColor] An unexpected number of channels was found: %d', [lStrings.Count])); finally lStrings.Free; end; Exit; end; // Support for RGB hex // ex: #0000ff // Another wierd valid variant: #000 if (Length(lValue) > 1) and (lValue[1] = '#') then begin lStr := Copy(lValue, 2, 2); Result.Red := StrToInt('$'+lStr)*$101; lStr := Copy(lValue, 4, 2); if lStr = '' then Result.Green := 0 else Result.Green := StrToInt('$'+lStr)*$101; lStr := Copy(lValue, 6, 2); if lStr = '' then Result.Blue := 0 else Result.Blue := StrToInt('$'+lStr)*$101; Exit; end; // Support for named colors // List here: http://www.december.com/html/spec/colorsvghex.html case lValue of 'black': Result := colBlack; 'navy': Result.Blue := $8080; 'darkblue':Result.Blue := $8B8B; 'mediumblue':Result.Blue := $CDCD; 'blue': Result := colBlue; 'darkgreen':Result.Green := $6464; 'green': Result.Green := $8080; 'teal': begin Result.Green := $8080; Result.Blue := $8080; end; 'darkcyan': begin Result.Green := $8B8B; Result.Blue := $8B8B; end; 'deepskyblue': begin Result.Green := $BFBF; Result.Blue := $FFFF; end; 'darkturquoise': begin Result.Green := $CECE; Result.Blue := $D1D1; end; 'mediumspringgreen': begin Result.Green := $FAFA; Result.Blue := $9A9A; end; 'lime': Result := colGreen; 'springgreen': begin Result.Green := $FFFF; Result.Blue := $7F7F; end; 'cyan': Result := colCyan; 'aqua': Result := colCyan; 'midnightblue': begin Result.Red := $1919; Result.Green := $1919; Result.Blue := $7070; end; 'dodgerblue': begin Result.Red := $1E1E; Result.Green := $9090; Result.Blue := $FFFF; end; 'lightseagreen': begin Result.Red := $2020; Result.Green := $B2B2; Result.Blue := $AAAA; end; 'forestgreen': begin Result.Red := $2222; Result.Green := $8B8B; Result.Blue := $2222; end; 'seagreen': begin Result.Red := $2E2E; Result.Green := $8B8B; Result.Blue := $5757; end; 'darkslategray', 'darkslategrey': begin Result.Red := $2F2F; Result.Green := $4F4F; Result.Blue := $4F4F; end; 'limegreen': begin Result.Red := $3232; Result.Green := $CDCD; Result.Blue := $3232; end; 'mediumseagreen': begin Result.Red := $3C3C; Result.Green := $CBCB; Result.Blue := $7171; end; 'turquoise': begin Result.Red := $4040; Result.Green := $E0E0; Result.Blue := $D0D0; end; 'royalblue': begin Result.Red := $4141; Result.Green := $6969; Result.Blue := $E1E1; end; 'steelblue': begin Result.Red := $4646; Result.Green := $8282; Result.Blue := $B4B4; end; 'darkslateblue': begin Result.Red := $4848; Result.Green := $3D3D; Result.Blue := $8B8B; end; 'mediumturquoise': begin Result.Red := $4848; Result.Green := $D1D1; Result.Blue := $CCCC; end; { indigo #4B0082 darkolivegreen #556B2F cadetblue #5F9EA0 cornflowerblue #6495ED mediumaquamarine #66CDAA dimgrey #696969 dimgray #696969 slateblue #6A5ACD olivedrab #6B8E23 slategrey #708090 slategray #708090 lightslategray(Hex3) #778899 lightslategrey(Hex3) #778899 mediumslateblue #7B68EE lawngreen #7CFC00 chartreuse #7FFF00 } 'aquamarine': begin Result.Red := $7F7F; Result.Green := $FFFF; Result.Blue := $D4D4; end; 'maroon': Result.Red := $8080; 'purple': Result := colPurple; 'olive': Result := colOlive; 'gray', 'grey': Result := colGray; 'skyblue': begin Result.Red := $8787; Result.Green := $CECE; Result.Blue := $EBEB; end; 'lightskyblue': begin Result.Red := $8787; Result.Green := $CECE; Result.Blue := $FAFA; end; 'blueviolet': begin Result.Red := $8A8A; Result.Green := $2B2B; Result.Blue := $E2E2; end; 'darkred': Result.Red := $8B8B; 'darkmagenta': begin Result.Red := $8B8B; Result.Blue := $8B8B; end; { saddlebrown #8B4513 darkseagreen #8FBC8F lightgreen #90EE90 mediumpurple #9370DB darkviolet #9400D3 palegreen #98FB98 darkorchid #9932CC yellowgreen #9ACD32 sienna #A0522D brown #A52A2A darkgray #A9A9A9 darkgrey #A9A9A9 lightblue #ADD8E6 greenyellow #ADFF2F paleturquoise #AFEEEE lightsteelblue #B0C4DE powderblue #B0E0E6 firebrick #B22222 darkgoldenrod #B8860B mediumorchid #BA55D3 rosybrown #BC8F8F darkkhaki #BDB76B } 'silver': Result := colSilver; 'mediumvioletred': begin Result.Red := $C7C7; Result.Green := $1515; Result.Blue := $8585; end; 'indianred': begin Result.Red := $CDCD; Result.Green := $5C5C; Result.Blue := $5C5C; end; 'peru': begin Result.Red := $CDCD; Result.Green := $8585; Result.Blue := $3F3F; end; 'chocolate': begin Result.Red := $D2D2; Result.Green := $6969; Result.Blue := $1E1E; end; { tan #D2B48C lightgray #D3D3D3 lightgrey #D3D3D3 thistle #D8BFD8 orchid #DA70D6 goldenrod #DAA520 palevioletred #DB7093 crimson #DC143C gainsboro #DCDCDC plum #DDA0DD burlywood #DEB887 lightcyan #E0FFFF lavender #E6E6FA } 'darksalmon': begin Result.Red := $E9E9; Result.Green := $9696; Result.Blue := $7A7A; end; 'violet': begin Result.Red := $EEEE; Result.Green := $8282; Result.Blue := $EEEE; end; 'palegoldenrod': begin Result.Red := $EEEE; Result.Green := $E8E8; Result.Blue := $AAAA; end; 'lightcoral': begin Result.Red := $F0F0; Result.Green := $8080; Result.Blue := $8080; end; 'khaki': begin Result.Red := $F0F0; Result.Green := $E6E6; Result.Blue := $8C8C; end; 'aliceblue': begin Result.Red := $F0F0; Result.Green := $F8F8; Result.Blue := $FFFF; end; 'honeydew': begin Result.Red := $F0F0; Result.Green := $FFFF; Result.Blue := $F0F0; end; 'azure': begin Result.Red := $F0F0; Result.Green := $FFFF; Result.Blue := $FFFF; end; 'sandybrown': begin Result.Red := $F4F4; Result.Green := $A4A4; Result.Blue := $6060; end; { wheat #F5DEB3 beige #F5F5DC whitesmoke #F5F5F5 mintcream #F5FFFA ghostwhite #F8F8FF salmon #FA8072 antiquewhite #FAEBD7 linen #FAF0E6 lightgoldenrodyellow #FAFAD2 oldlace #FDF5E6 } 'red': Result := colRed; 'fuchsia': Result := colFuchsia; 'magenta': Result := colMagenta; { deeppink #FF1493 orangered #FF4500 tomato #FF6347 hotpink #FF69B4 coral #FF7F50 darkorange #FF8C00 lightsalmon #FFA07A orange #FFA500 lightpink #FFB6C1 pink #FFC0CB gold #FFD700 peachpuff #FFDAB9 navajowhite #FFDEAD moccasin #FFE4B5 bisque #FFE4C4 mistyrose #FFE4E1 blanchedalmond #FFEBCD papayawhip #FFEFD5 lavenderblush #FFF0F5 seashell #FFF5EE cornsilk #FFF8DC lemonchiffon #FFFACD floralwhite #FFFAF0 } 'snow': begin Result.Red := $FFFF; Result.Green := $FAFA; Result.Blue := $FAFA; end; 'yellow': Result := colYellow; 'lightyellow': begin Result.Red := $FFFF; Result.Green := $FEFE; end; 'ivory': begin Result.Red := $FFFF; Result.Green := $FFFF; Result.Blue := $F0F0; end; 'white': Result := colWhite; end; end; // style="fill:none;stroke:black;stroke-width:3" function TvSVGVectorialReader.ReadSVGStyle(AValue: string; ADestEntity: TvEntityWithPen; AUseFillAsPen: Boolean = False): TvSetPenBrushAndFontElements; var lStr, lStyleKeyStr, lStyleValueStr: String; lStrings: TStringList; i, lPosEqual: Integer; begin Result := []; if AValue = '' then Exit; // Now split using ";" separator lStrings := TStringList.Create; try lStrings.Delimiter := ';'; lStrings.DelimitedText := LowerCase(AValue); for i := 0 to lStrings.Count-1 do begin lStr := lStrings.Strings[i]; lPosEqual := Pos(':', lStr); lStyleKeyStr := Copy(lStr, 0, lPosEqual-1); lStyleValueStr := Copy(lStr, lPosEqual+1, Length(lStr)); ReadSVGPenStyleWithKeyAndValue(lStyleKeyStr, lStyleValueStr, ADestEntity); if AUseFillAsPen and (lStyleKeyStr = 'fill') then Result := Result + ReadSVGPenStyleWithKeyAndValue('stroke', lStyleValueStr, ADestEntity) else if ADestEntity is TvText then Result := Result + ReadSVGFontStyleWithKeyAndValue(lStyleKeyStr, lStyleValueStr, ADestEntity as TvText) else if ADestEntity is TvEntityWithPenAndBrush then Result := Result + ReadSVGBrushStyleWithKeyAndValue(lStyleKeyStr, lStyleValueStr, ADestEntity as TvEntityWithPenAndBrush); end; finally lStrings.Free; end; end; // style="fill:none;stroke:black;stroke-width:3" function TvSVGVectorialReader.ReadSVGStyleToStyleLists(AValue: string; AStyleKeys, AStyleValues: TStringList): TvSetPenBrushAndFontElements; var lStr, lStyleKeyStr, lStyleValueStr: String; lStrings: TStringList; i, lPosEqual: Integer; begin Result := []; if AValue = '' then Exit; // Now split using ";" separator lStrings := TStringList.Create; try lStrings.Delimiter := ';'; lStrings.DelimitedText := LowerCase(AValue); for i := 0 to lStrings.Count-1 do begin lStr := lStrings.Strings[i]; lPosEqual := Pos(':', lStr); lStyleKeyStr := Copy(lStr, 0, lPosEqual-1); lStyleValueStr := Copy(lStr, lPosEqual+1, Length(lStr)); AStyleKeys.Add(lStyleKeyStr); AStyleValues.Add(lStyleValueStr); end; finally lStrings.Free; end; end; function TvSVGVectorialReader.ReadSVGPenStyleWithKeyAndValue(AKey, AValue: string; ADestEntity: TvEntityWithPen): TvSetPenBrushAndFontElements; var OldAlpha: Word; begin Result := []; if AKey = 'stroke' then begin // We store and restore the old alpha to support the "-opacity" element OldAlpha := ADestEntity.Pen.Color.Alpha; if ADestEntity.Pen.Style = psClear then ADestEntity.Pen.Style := psSolid; if AValue = 'none' then ADestEntity.Pen.Style := fpcanvas.psClear else begin ADestEntity.Pen.Color := ReadSVGColor(AValue); ADestEntity.Pen.Color.Alpha := OldAlpha; end; Result := Result + [spbfPenColor, spbfPenStyle]; end else if AKey = 'stroke-width' then begin ADestEntity.Pen.Width := Round(StringWithUnitToFloat(AValue)); Result := Result + [spbfPenWidth]; end else if AKey = 'stroke-opacity' then begin ADestEntity.Pen.Color.Alpha := StrToInt(AValue)*$101 end else if AKey = 'stroke-linecap' then begin {case LowerCase(AValue) of 'butt': 'round': 'square': ADestEntity.Pen; end;} end; end; function TvSVGVectorialReader.ReadSVGBrushStyleWithKeyAndValue(AKey, AValue: string; ADestEntity: TvEntityWithPenAndBrush): TvSetPenBrushAndFontElements; var OldAlpha: Word; begin Result := []; if AKey = 'fill' then begin // We store and restore the old alpha to support the "-opacity" element OldAlpha := ADestEntity.Brush.Color.Alpha; if ADestEntity.Brush.Style = bsClear then ADestEntity.Brush.Style := bsSolid; if AValue = 'none' then ADestEntity.Brush.Style := fpcanvas.bsClear else begin ADestEntity.Brush.Color := ReadSVGColor(AValue); ADestEntity.Brush.Color.Alpha := OldAlpha; end; Result := Result + [spbfBrushColor, spbfBrushStyle]; end else if AKey = 'fill-opacity' then ADestEntity.Brush.Color.Alpha := StrToInt(AValue)*$101; end; function TvSVGVectorialReader.ReadSVGFontStyleWithKeyAndValue(AKey, AValue: string; ADestEntity: TvEntityWithPenBrushAndFont): TvSetPenBrushAndFontElements; begin Result := []; // SVG text uses "fill" to indicate the pen color of the text, very unintuitive as // "fill" is usually for brush in other elements if AKey = 'fill' then begin ADestEntity.Font.Color := ReadSVGColor(AValue); Result := Result + [spbfFontColor]; end // But sometimes SVG also uses stroke! Oh no... else if AKey = 'stroke' then begin ADestEntity.Font.Color := ReadSVGColor(AValue); Result := Result + [spbfFontColor]; end else if AKey = 'fill-opacity' then ADestEntity.Font.Color.Alpha := StrToInt(AValue)*$101 else if AKey = 'font-size' then begin ADestEntity.Font.Size := Round(StringWithUnitToFloat(AValue)); Result := Result + [spbfFontSize]; end else if AKey = 'font-family' then ADestEntity.Font.Name := AValue else if AKey = 'font-weight' then begin case LowerCase(AValue) of 'bold': ADestEntity.Font.Bold := True; end; end; end; function TvSVGVectorialReader.IsAttributeFromStyle(AStr: string): Boolean; begin Result := (AStr = 'stroke') or (AStr = 'stroke-width') or (AStr = 'stroke-dasharray') or (AStr = 'stroke-opacity') or (AStr = 'stroke-linecap') or // brush (AStr = 'fill') or (AStr = 'fill-opacity') or // font (AStr = 'font-size') or (AStr = 'fill-family') or (AStr = 'font-weight'); end; procedure TvSVGVectorialReader.ApplyLayerStyles(ADestEntity: TvEntity); var lStringsKeys, lStringsValues: TStringList; i, j: Integer; begin for i := 0 to FLayerStylesKeys.Count-1 do begin lStringsKeys := TStringList(FLayerStylesKeys.Items[i]); lStringsValues := TStringList(FLayerStylesValues.Items[i]); for j := 0 to lStringsKeys.Count-1 do begin if ADestEntity is TvEntityWithPen then ReadSVGPenStyleWithKeyAndValue(lStringsKeys.Strings[j], lStringsValues.Strings[j], ADestEntity as TvEntityWithPen); if ADestEntity is TvEntityWithPenAndBrush then ReadSVGBrushStyleWithKeyAndValue(lStringsKeys.Strings[j], lStringsValues.Strings[j], ADestEntity as TvEntityWithPenAndBrush); if ADestEntity is TvEntityWithPenBrushAndFont then ReadSVGFontStyleWithKeyAndValue(lStringsKeys.Strings[j], lStringsValues.Strings[j], ADestEntity as TvEntityWithPenBrushAndFont); end; end; end; procedure TvSVGVectorialReader.ReadEntityFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); var lEntityName: DOMString; begin lEntityName := LowerCase(ANode.NodeName); case lEntityName of 'circle': ReadCircleFromNode(ANode, AData, ADoc); 'ellipse': ReadEllipseFromNode(ANode, AData, ADoc); 'g': ReadLayerFromNode(ANode, AData, ADoc); 'line': ReadLineFromNode(ANode, AData, ADoc); 'path': ReadPathFromNode(ANode, AData, ADoc); 'polygon', 'polyline': ReadPolyFromNode(ANode, AData, ADoc); 'rect': ReadRectFromNode(ANode, AData, ADoc); 'text': ReadTextFromNode(ANode, AData, ADoc); 'use': ReadUseFromNode(ANode, AData, ADoc); end; end; procedure TvSVGVectorialReader.ReadCircleFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); var cx, cy, cr, dtmp: double; lCircle: TvCircle; i: Integer; lNodeName: DOMString; begin cx := 0.0; cy := 0.0; cr := 0.0; lCircle := TvCircle.Create; // SVG entities start without any pen drawing, but with a black brush lCircle.Pen.Style := psClear; lCircle.Brush.Style := bsSolid; lCircle.Brush.Color := colBlack; // read the attributes for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'cx' then cx := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'cy' then cy := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'r' then cr := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'id' then lCircle.Name := UTF16ToUTF8(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'style' then ReadSVGStyle(ANode.Attributes.Item[i].NodeValue, lCircle) else if IsAttributeFromStyle(lNodeName) then begin ReadSVGPenStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lCircle); ReadSVGBrushStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lCircle); end; end; ConvertSVGDeltaToFPVDelta( AData, cx, cy, lCircle.X, lCircle.Y); ConvertSVGDeltaToFPVDelta( AData, cr, 0, lCircle.Radius, dtmp); AData.AddEntity(lCircle); end; procedure TvSVGVectorialReader.ReadEllipseFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); var cx, cy, crx, cry: double; lEllipse: TvEllipse; i: Integer; lNodeName: DOMString; begin cx := 0.0; cy := 0.0; crx := 0.0; cry := 0.0; lEllipse := TvEllipse.Create; // SVG entities start without any pen drawing, but with a black brush lEllipse.Pen.Style := psClear; lEllipse.Brush.Style := bsSolid; lEllipse.Brush.Color := colBlack; // read the attributes for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'cx' then cx := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'cy' then cy := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'rx' then crx := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'ry' then cry := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'id' then lEllipse.Name := UTF16ToUTF8(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'style' then ReadSVGStyle(ANode.Attributes.Item[i].NodeValue, lEllipse) else if IsAttributeFromStyle(lNodeName) then begin ReadSVGPenStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lEllipse); ReadSVGBrushStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lEllipse); end; end; ConvertSVGDeltaToFPVDelta( AData, cx, cy, lEllipse.X, lEllipse.Y); ConvertSVGDeltaToFPVDelta( AData, crx, cry, lEllipse.HorzHalfAxis, lEllipse.VertHalfAxis); AData.AddEntity(lEllipse); end; procedure TvSVGVectorialReader.ReadLayerFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); var lNodeName: DOMString; lLayerName: string = ''; lCurNode, lLayerNameNode: TDOMNode; lLayer, lParentLayer: TvLayer; i: Integer; {$ifdef SVG_MERGE_LAYER_STYLES} lLayerStyleKeys, lLayerStyleValues: TStringList; {$endif} begin // Store the style of this layer in the list {$ifdef SVG_MERGE_LAYER_STYLES} lLayerStyleKeys := TStringList.Create; lLayerStyleValues := TStringList.Create; FLayerStylesKeys.Add(lLayerStyleKeys); FLayerStylesValues.Add(lLayerStyleValues); {$endif} // first attribute reader, there is a second one for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'id' then lLayerName := UTF16ToUTF8(ANode.Attributes.Item[i].NodeValue); end; lParentLayer := AData.GetCurrentLayer(); lLayer := AData.AddLayerAndSetAsCurrent(lLayerName); // attribute reading again after getting the object for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'style' then begin {$ifdef SVG_MERGE_LAYER_STYLES} ReadSVGStyleToStyleLists(ANode.Attributes.Item[i].NodeValue, lLayerStyleKeys, lLayerStyleValues); {$else} lLayer.SetPenBrushAndFontElements += ReadSVGStyle(ANode.Attributes.Item[i].NodeValue, lLayer) {$endif} end else if IsAttributeFromStyle(lNodeName) then begin {$ifdef SVG_MERGE_LAYER_STYLES} lLayerStyleKeys.Add(lNodeName); lLayerStyleValues.Add(UTF16ToUTF8(ANode.Attributes.Item[i].NodeValue)); {$else} lLayer.SetPenBrushAndFontElements += ReadSVGPenStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lLayer); lLayer.SetPenBrushAndFontElements += ReadSVGBrushStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lLayer); {$endif} end; end; lCurNode := ANode.FirstChild; while Assigned(lCurNode) do begin ReadEntityFromNode(lCurNode, AData, ADoc); lCurNode := lCurNode.NextSibling; end; {$ifdef SVG_MERGE_LAYER_STYLES} // Now remove the style from this layer FLayerStylesKeys.Remove(lLayerStyleKeys); lLayerStyleKeys.Free; FLayerStylesValues.Remove(lLayerStyleValues); lLayerStyleValues.Free; {$endif} // Set the current layer to the parent node, // or else items read next will be put as children of this layer AData.SetCurrentLayer(lParentLayer); end; procedure TvSVGVectorialReader.ReadLineFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); var x1, y1, x2, y2: double; vx1, vy1, vx2, vy2: double; i: Integer; lNodeName: DOMString; lPath: TPath; lStyleStr, lStrokeStr, lStrokeWidthStr: DOMString; begin x1 := 0.0; y1 := 0.0; x2 := 0.0; y2 := 0.0; // read the attributes for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'x1' then x1 := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'y1' then y1 := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'x2' then x2 := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'y2' then y2 := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'style' then lStyleStr := ANode.Attributes.Item[i].NodeValue else if lNodeName = 'stroke' then lStrokeStr := ANode.Attributes.Item[i].NodeValue else if lNodeName = 'stroke-width' then lStrokeWidthStr := ANode.Attributes.Item[i].NodeValue; end; ConvertSVGCoordinatesToFPVCoordinates( AData, x1, y1, vx1, vy1); ConvertSVGCoordinatesToFPVCoordinates( AData, x2, y2, vx2, vy2); AData.StartPath(); AData.AddMoveToPath(vx1, vy1); AData.AddLineToPath(vx2, vy2); lPath := AData.EndPath(); // Add the pen/brush ReadSVGStyle(lStyleStr, lPath); ReadSVGStyle(lStrokeStr, lPath); ReadSVGStyle(lStrokeWidthStr, lPath); end; procedure TvSVGVectorialReader.ReadPathFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); var lNodeName, lDStr: WideString; i: Integer; lPath: TPath; begin for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'd' then lDStr := ANode.Attributes.Item[i].NodeValue; end; AData.StartPath(); ReadPathFromString(UTF8Encode(lDStr), AData, ADoc); lPath := AData.EndPath(); // Add default SVG pen/brush lPath.Pen.Style := psClear; lPath.Brush.Color := colBlack; lPath.Brush.Style := bsSolid; // Apply the layer style ApplyLayerStyles(lPath); // Add the pen/brush/name for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'id' then lPath.Name := UTF16ToUTF8(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'style' then ReadSVGStyle(ANode.Attributes.Item[i].NodeValue, lPath) else if IsAttributeFromStyle(lNodeName) then begin ReadSVGPenStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lPath); ReadSVGBrushStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lPath); end; end; end; // Documentation: http://www.w3.org/TR/SVG/paths.html procedure TvSVGVectorialReader.ReadPathFromString(AStr: string; AData: TvVectorialPage; ADoc: TvVectorialDocument); var i: Integer; X, Y, X2, Y2, X3, Y3: Double; CurX, CurY: Double; lCurTokenType, lLastCommandToken: TSVGTokenType; lDebugStr: String; lTmpTokenType: TSVGTokenType; begin FSVGPathTokenizer.Tokens.Clear; FSVGPathTokenizer.TokenizePathString(AStr); //lDebugStr := FSVGPathTokenizer.DebugOutTokensAsString(); CurX := 0; CurY := 0; lLastCommandToken := sttFloatValue; i := 0; while i < FSVGPathTokenizer.Tokens.Count do begin lCurTokenType := FSVGPathTokenizer.Tokens.Items[i].TokenType; if not (lCurTokenType = sttFloatValue) then begin lLastCommandToken := lCurTokenType; ReadNextPathCommand(lCurTokenType, i, CurX, CurY, AData, ADoc); end else begin lTmpTokenType := lLastCommandToken; if lLastCommandToken = sttMoveTo then lTmpTokenType := sttLineTo; if lLastCommandToken = sttRelativeMoveTo then lTmpTokenType := sttRelativeLineTo; Dec(i);// because there is command token ReadNextPathCommand(lTmpTokenType, i, CurX, CurY, AData, ADoc); end; end; end; procedure TvSVGVectorialReader.ReadNextPathCommand(ACurTokenType: TSVGTokenType; var i: Integer; var CurX, CurY: Double; AData: TvVectorialPage; ADoc: TvVectorialDocument); var X, Y, X2, Y2, X3, Y3: Double; lCurTokenType: TSVGTokenType; lDebugStr: String; begin lCurTokenType := ACurTokenType; // -------------- // Moves // -------------- if lCurTokenType in [sttMoveTo, sttRelativeMoveTo] then begin X := FSVGPathTokenizer.Tokens.Items[i+1].Value; Y := FSVGPathTokenizer.Tokens.Items[i+2].Value; ConvertSVGCoordinatesToFPVCoordinates(AData, X, Y, X, Y); // take care of relative or absolute if lCurTokenType = sttRelativeMoveTo then begin CurX := CurX + X; CurY := CurY + Y; end else begin CurX := X; CurY := Y; end; AData.AddMoveToPath(CurX, CurY); Inc(i, 3); end // -------------- // Close Path // -------------- else if lCurTokenType = sttClosePath then begin // Get the first point AData.GetTmpPathStartPos(X, Y); // And repeat it CurX := X; CurY := Y; AData.AddLineToPath(CurX, CurY); Inc(i, 3); end // -------------- // Lines // -------------- else if lCurTokenType in [sttLineTo, sttRelativeLineTo, sttHorzLineTo, sttRelativeHorzLineTo, sttVertLineTo, sttRelativeVertLineTo] then begin X := FSVGPathTokenizer.Tokens.Items[i+1].Value; if not (lCurTokenType in [sttHorzLineTo, sttRelativeHorzLineTo, sttVertLineTo, sttRelativeVertLineTo]) then Y := FSVGPathTokenizer.Tokens.Items[i+2].Value; // "l" LineTo uses relative coordenates in SVG if lCurTokenType in [sttRelativeLineTo, sttRelativeHorzLineTo, sttRelativeVertLineTo] then begin ConvertSVGDeltaToFPVDelta(AData, X, Y, X, Y); CurX := CurX + X; CurY := CurY + Y; end else begin ConvertSVGCoordinatesToFPVCoordinates(AData, X, Y, X, Y); CurX := X; CurY := Y; end; // horizontal and vertical line corrections if lCurTokenType in [sttHorzLineTo, sttRelativeHorzLineTo] then Y := 0 else if lCurTokenType in [sttVertLineTo, sttRelativeVertLineTo] then begin Y := X; X := 0; end; AData.AddLineToPath(CurX, CurY); if not (lCurTokenType in [sttHorzLineTo, sttRelativeHorzLineTo, sttVertLineTo, sttRelativeVertLineTo]) then Inc(i, 3) else Inc(i, 2); end // -------------- // Cubic Bezier // -------------- else if lCurTokenType in [sttBezierTo, sttRelativeBezierTo, sttSmoothBezierTo, sttRelativeSmoothBezierTo] then begin if lCurTokenType in [sttBezierTo, sttRelativeBezierTo] then begin X2 := FSVGPathTokenizer.Tokens.Items[i+1].Value; Y2 := FSVGPathTokenizer.Tokens.Items[i+2].Value; X3 := FSVGPathTokenizer.Tokens.Items[i+3].Value; Y3 := FSVGPathTokenizer.Tokens.Items[i+4].Value; X := FSVGPathTokenizer.Tokens.Items[i+5].Value; Y := FSVGPathTokenizer.Tokens.Items[i+6].Value; end else begin X2 := CurX; Y2 := CurY; X3 := FSVGPathTokenizer.Tokens.Items[i+1].Value; Y3 := FSVGPathTokenizer.Tokens.Items[i+2].Value; X := FSVGPathTokenizer.Tokens.Items[i+3].Value; Y := FSVGPathTokenizer.Tokens.Items[i+4].Value; end; // Careful that absolute coordinates require using ConvertSVGCoordinatesToFPVCoordinates if lCurTokenType in [sttRelativeBezierTo, sttRelativeSmoothBezierTo] then begin ConvertSVGDeltaToFPVDelta(AData, X2, Y2, X2, Y2); ConvertSVGDeltaToFPVDelta(AData, X3, Y3, X3, Y3); ConvertSVGDeltaToFPVDelta(AData, X, Y, X, Y); end else begin ConvertSVGCoordinatesToFPVCoordinates(AData, X2, Y2, X2, Y2); ConvertSVGCoordinatesToFPVCoordinates(AData, X3, Y3, X3, Y3); ConvertSVGCoordinatesToFPVCoordinates(AData, X, Y, X, Y); end; if lCurTokenType = sttRelativeBezierTo then begin AData.AddBezierToPath(X2 + CurX, Y2 + CurY, X3 + CurX, Y3 + CurY, X + CurX, Y + CurY); CurX := CurX + X; CurY := CurY + Y; end else begin AData.AddBezierToPath(X2, Y2, X3, Y3, X, Y); CurX := X; CurY := Y; end; if lCurTokenType in [sttBezierTo, sttRelativeBezierTo] then Inc(i, 7) else Inc(i, 5); end // -------------- // Quadratic Bezier // -------------- else if lCurTokenType in [sttQuadraticBezierTo, sttRelativeQuadraticBezierTo] then begin X2 := FSVGPathTokenizer.Tokens.Items[i+1].Value; Y2 := FSVGPathTokenizer.Tokens.Items[i+2].Value; X := FSVGPathTokenizer.Tokens.Items[i+3].Value; Y := FSVGPathTokenizer.Tokens.Items[i+4].Value; // Careful that absolute coordinates require using ConvertSVGCoordinatesToFPVCoordinates if lCurTokenType in [sttRelativeQuadraticBezierTo] then begin ConvertSVGDeltaToFPVDelta(AData, X2, Y2, X2, Y2); ConvertSVGDeltaToFPVDelta(AData, X, Y, X, Y); end else begin ConvertSVGCoordinatesToFPVCoordinates(AData, X2, Y2, X2, Y2); ConvertSVGCoordinatesToFPVCoordinates(AData, X, Y, X, Y); end; if lCurTokenType = sttRelativeQuadraticBezierTo then begin AData.AddBezierToPath(X2 + CurX, Y2 + CurY, X2 + CurX, Y2 + CurY, X + CurX, Y + CurY); CurX := CurX + X; CurY := CurY + Y; end else begin AData.AddBezierToPath(X2, Y2, X2, Y2, X, Y); CurX := X; CurY := Y; end; Inc(i, 5); end // -------------- // Elliptical arcs // -------------- else if lCurTokenType in [sttEllipticArcTo, sttRelativeEllipticArcTo] then begin {X2 := FSVGPathTokenizer.Tokens.Items[i+1].Value; Y2 := FSVGPathTokenizer.Tokens.Items[i+2].Value; X := FSVGPathTokenizer.Tokens.Items[i+3].Value; Y := FSVGPathTokenizer.Tokens.Items[i+4].Value; // Careful that absolute coordinates require using ConvertSVGCoordinatesToFPVCoordinates if lCurTokenType in [sttRelativeQuadraticBezierTo] then begin ConvertSVGDeltaToFPVDelta(AData, X2, Y2, X2, Y2); ConvertSVGDeltaToFPVDelta(AData, X, Y, X, Y); end else begin ConvertSVGCoordinatesToFPVCoordinates(AData, X2, Y2, X2, Y2); ConvertSVGCoordinatesToFPVCoordinates(AData, X, Y, X, Y); end; if lCurTokenType = sttRelativeQuadraticBezierTo then begin AData.AddBezierToPath(X2 + CurX, Y2 + CurY, X2 + CurX, Y2 + CurY, X + CurX, Y + CurY); CurX := CurX + X; CurY := CurY + Y; end else begin AData.AddBezierToPath(X2, Y2, X2, Y2, X, Y); CurX := X; CurY := Y; end; } Inc(i, 8); end else begin Inc(i); end; end; procedure TvSVGVectorialReader.ReadPointsFromString(AStr: string; AData: TvVectorialPage; ADoc: TvVectorialDocument; AClosePath: Boolean); var i: Integer; X, Y: Double; FirstPtX, FirstPtY, CurX, CurY: Double; begin FSVGPathTokenizer.Tokens.Clear; FSVGPathTokenizer.TokenizePathString(AStr); CurX := 0; CurY := 0; if FSVGPathTokenizer.Tokens.Count <= 2 then raise Exception.Create('[TvSVGVectorialReader.ReadPointsFromString] There are too few points in the element'); // The first point CurX := FSVGPathTokenizer.Tokens.Items[0].Value; CurY := FSVGPathTokenizer.Tokens.Items[1].Value; ConvertSVGCoordinatesToFPVCoordinates(AData, CurX, CurY, CurX, CurY); FirstPtX := CurX; FirstPtY := CurY; AData.AddMoveToPath(CurX, CurY); // Now all other points i := 2; while i < FSVGPathTokenizer.Tokens.Count do begin X := FSVGPathTokenizer.Tokens.Items[i].Value; Y := FSVGPathTokenizer.Tokens.Items[i+1].Value; ConvertSVGDeltaToFPVDelta(AData, X, Y, CurX, CurY); AData.AddLineToPath(CurX, CurY); Inc(i, 2); end; // and if we want, close the path if AClosePath then AData.AddLineToPath(FirstPtX, FirstPtY); end; // polygon and polyline are very similar procedure TvSVGVectorialReader.ReadPolyFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); var lPointsStr: string = ''; i: Integer; lNodeName: DOMString; lPath: TPath; lIsPolygon: Boolean = False; begin lIsPolygon := LowerCase(ANode.NodeName) = 'polygon'; // first get the points for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'points' then lPointsStr := ANode.Attributes.Item[i].NodeValue; end; AData.StartPath(); ReadPointsFromString(lPointsStr, AData, ADoc, lIsPolygon); lPath := AData.EndPath(); // now read the other attributes for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'id' then lPath.Name := UTF16ToUTF8(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'style' then ReadSVGStyle(ANode.Attributes.Item[i].NodeValue, lPath) else if IsAttributeFromStyle(lNodeName) then begin ReadSVGPenStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lPath); ReadSVGBrushStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lPath); end; end; end; // procedure TvSVGVectorialReader.ReadRectFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); var lx, ly, cx, cy, lrx, lry: double; lRect: TvRectangle; i: Integer; lNodeName: DOMString; begin lx := 0.0; ly := 0.0; cx := 0.0; cy := 0.0; lrx := 0.0; lry := 0.0; lRect := TvRectangle.Create; // SVG entities start without any pen drawing, but with a black brush lRect.Pen.Style := psClear; lRect.Brush.Style := bsSolid; lRect.Brush.Color := colBlack; // read the attributes for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'x' then lx := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'y' then ly := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'rx' then lrx := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'ry' then lry := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'width' then cx := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'height' then cy := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'id' then lRect.Name := UTF16ToUTF8(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'style' then ReadSVGStyle(ANode.Attributes.Item[i].NodeValue, lRect) else if IsAttributeFromStyle(lNodeName) then begin ReadSVGPenStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lRect); ReadSVGBrushStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lRect); end; end; ConvertSVGDeltaToFPVDelta( AData, lx, ly, lRect.X, lRect.Y); ConvertSVGDeltaToFPVDelta( AData, cx, cy, lRect.CX, lRect.CY); ConvertSVGDeltaToFPVDelta( AData, lrx, lry, lRect.RX, lRect.RY); lRect.RX := Abs(lRect.RX) * 2; lRect.RY := Abs(lRect.RY) * 2; AData.AddEntity(lRect); end; procedure TvSVGVectorialReader.ReadTextFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); var lTextStr: string = ''; lx, ly: double; lText: TvText; i: Integer; lNodeName: DOMString; lCurNode: TDOMNode; begin lx := 0.0; ly := 0.0; lText := TvText.Create; // read the attributes for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'x' then lx := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'y' then ly := StringWithUnitToFloat(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'id' then lText.Name := UTF16ToUTF8(ANode.Attributes.Item[i].NodeValue) else if lNodeName = 'style' then ReadSVGStyle(ANode.Attributes.Item[i].NodeValue, lText, True) else if IsAttributeFromStyle(lNodeName) then ReadSVGFontStyleWithKeyAndValue(lNodeName, ANode.Attributes.Item[i].NodeValue, lText); end; // The text contents are inside as a child text, not as a attribute // ex: I love SVG if Anode.FirstChild <> nil then lTextStr := Anode.FirstChild.NodeValue; // Add the first line lText.Value.Add(lTextStr); // Set the coordinates ConvertSVGDeltaToFPVDelta( AData, lx, ly, lText.X, lText.Y); // Now add other lines, which appear as another line // Example: // Several lines: // First line // Second line // // These other lines can be positioned, so they need to appear as independent TvText elements { lCurNode := Anode.FirstChild; while lCurNode <> nil do begin lNodeName := LowerCase(lCurNode.NodeName); if lNodeName <> 'tspan' then Continue; ReadTextFromNode(lCurNode, AData, ADoc); lCurNode := lCurNode.NextSibling; end;} // Finalization AData.AddEntity(lText); end; procedure TvSVGVectorialReader.ReadUseFromNode(ANode: TDOMNode; AData: TvVectorialPage; ADoc: TvVectorialDocument); begin end; function TvSVGVectorialReader.StringWithUnitToFloat(AStr: string): Double; var UnitStr, ValueStr: string; Len: Integer; LastChar: Char; begin if AStr = '' then Exit(0.0); // Check the unit Len := Length(AStr); UnitStr := Copy(AStr, Len-1, 2); LastChar := AStr[Len]; if UnitStr = 'mm' then begin ValueStr := Copy(AStr, 1, Len-2); Result := StrToFloat(ValueStr, FPointSeparator); end else if UnitStr = 'cm' then begin ValueStr := Copy(AStr, 1, Len-2); Result := StrToFloat(ValueStr, FPointSeparator) * 10; end else if UnitStr = 'px' then begin ValueStr := Copy(AStr, 1, Len-2); Result := StrToFloat(ValueStr, FPointSeparator); end else if LastChar = '%' then begin ValueStr := Copy(AStr, 1, Len-1); Result := StrToInt(ValueStr); end else // If there is no unit, just use StrToFloat begin Result := StrToFloat(AStr, FPointSeparator); end; end; procedure TvSVGVectorialReader.ConvertSVGCoordinatesToFPVCoordinates( const AData: TvVectorialPage; const ASrcX, ASrcY: Double; var ADestX,ADestY: Double); begin ADestX := ASrcX * FLOAT_MILIMETERS_PER_PIXEL; ADestY := AData.Height - ASrcY * FLOAT_MILIMETERS_PER_PIXEL; end; procedure TvSVGVectorialReader.ConvertSVGDeltaToFPVDelta( const AData: TvVectorialPage; const ASrcX, ASrcY: Double; var ADestX, ADestY: Double); begin ADestX := ASrcX * FLOAT_MILIMETERS_PER_PIXEL; ADestY := - ASrcY * FLOAT_MILIMETERS_PER_PIXEL; end; constructor TvSVGVectorialReader.Create; begin inherited Create; FPointSeparator := DefaultFormatSettings; FPointSeparator.DecimalSeparator := '.'; FPointSeparator.ThousandSeparator := '#';// disable the thousand separator FSVGPathTokenizer := TSVGPathTokenizer.Create; FLayerStylesKeys := TFPList.Create; FLayerStylesValues := TFPList.Create; end; destructor TvSVGVectorialReader.Destroy; begin FLayerStylesKeys.Free; FLayerStylesValues.Free; FSVGPathTokenizer.Free; inherited Destroy; end; procedure TvSVGVectorialReader.ReadFromStream(AStream: TStream; AData: TvVectorialDocument); var Doc: TXMLDocument; begin try // Read in xml file from the stream ReadXMLFile(Doc, AStream); ReadFromXML(Doc, AData); finally // finally, free the document Doc.Free; end; end; procedure TvSVGVectorialReader.ReadFromXML(Doc: TXMLDocument; AData: TvVectorialDocument); var lCurNode: TDOMNode; lPage: TvVectorialPage; {$ifdef SVG_MERGE_LAYER_STYLES} lLayerStyleKeys, lLayerStyleValues: TStringList; {$endif} lNodeName: DOMString; ANode: TDOMElement; i: Integer; begin // ---------------- // Read the properties of the tag // ---------------- AData.Width := StringWithUnitToFloat(Doc.DocumentElement.GetAttribute('width')); AData.Height := StringWithUnitToFloat(Doc.DocumentElement.GetAttribute('height')); {$ifdef SVG_MERGE_LAYER_STYLES} FLayerStylesKeys.Clear; FLayerStylesValues.Clear; lLayerStyleKeys := TStringList.Create; lLayerStyleValues := TStringList.Create; FLayerStylesKeys.Add(lLayerStyleKeys); FLayerStylesValues.Add(lLayerStyleValues); {$endif} ANode := Doc.DocumentElement; for i := 0 to ANode.Attributes.Length - 1 do begin lNodeName := ANode.Attributes.Item[i].NodeName; if lNodeName = 'style' then begin {$ifdef SVG_MERGE_LAYER_STYLES} ReadSVGStyleToStyleLists(ANode.Attributes.Item[i].NodeValue, lLayerStyleKeys, lLayerStyleValues); {$endif} end else if IsAttributeFromStyle(lNodeName) then begin {$ifdef SVG_MERGE_LAYER_STYLES} lLayerStyleKeys.Add(lNodeName); lLayerStyleValues.Add(UTF16ToUTF8(ANode.Attributes.Item[i].NodeValue)); {$endif} end; end; // ---------------- // Now process the elements // ---------------- lCurNode := Doc.DocumentElement.FirstChild; lPage := AData.AddPage(); lPage.Width := AData.Width; lPage.Height := AData.Height; while Assigned(lCurNode) do begin ReadEntityFromNode(lCurNode, lPage, AData); lCurNode := lCurNode.NextSibling; end; // ---------------- // Remove the memory of the styles // ---------------- {$ifdef SVG_MERGE_LAYER_STYLES} // Now remove the style from this layer FLayerStylesKeys.Remove(lLayerStyleKeys); lLayerStyleKeys.Free; FLayerStylesValues.Remove(lLayerStyleValues); lLayerStyleValues.Free; {$endif} end; initialization RegisterVectorialReader(TvSVGVectorialReader, vfSVG); end.