Commit 780b5150 authored by Tebjan Halm's avatar Tebjan Halm
Browse files

Merge pull request #93 from erdomke/master

Initial Filter, Svg Font, and Text on a Path Support
parents 1bc4c0f3 4200d302
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Svg
{
[SvgElement("font-face-src")]
public class SvgFontFaceSrc : SvgElement
{
public override SvgElement DeepCopy()
{
return base.DeepCopy<SvgFontFaceSrc>();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Svg
{
[SvgElement("font-face-uri")]
public class SvgFontFaceUri : SvgElement
{
private Uri _referencedElement;
[SvgAttribute("href", SvgAttributeAttribute.XLinkNamespace)]
public virtual Uri ReferencedElement
{
get { return this._referencedElement; }
set { this._referencedElement = value; }
}
public override SvgElement DeepCopy()
{
return DeepCopy<SvgFontFaceUri>();
}
public override SvgElement DeepCopy<T>()
{
var newObj = base.DeepCopy<T>() as SvgFontFaceUri;
newObj.ReferencedElement = this.ReferencedElement;
return newObj;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Svg.Pathing;
using System.Drawing.Drawing2D;
namespace Svg
{
[SvgElement("glyph")]
public class SvgGlyph : SvgVisualElement
{
private GraphicsPath _path;
/// <summary>
/// Gets or sets a <see cref="SvgPathSegmentList"/> of path data.
/// </summary>
[SvgAttribute("d")]
public SvgPathSegmentList PathData
{
get { return this.Attributes.GetAttribute<SvgPathSegmentList>("d"); }
set { this.Attributes["d"] = value; }
}
[SvgAttribute("glyph-name")]
public virtual string GlyphName
{
get { return this.Attributes["glyph-name"] as string; }
set { this.Attributes["glyph-name"] = value; }
}
[SvgAttribute("horiz-adv-x")]
public float HorizAdvX
{
get { return (this.Attributes["horiz-adv-x"] == null ? this.Parents.OfType<SvgFont>().First().HorizAdvX : (float)this.Attributes["horiz-adv-x"]); }
set { this.Attributes["horiz-adv-x"] = value; }
}
[SvgAttribute("unicode")]
public string Unicode
{
get { return this.Attributes["unicode"] as string; }
set { this.Attributes["unicode"] = value; }
}
[SvgAttribute("vert-adv-y")]
public float VertAdvY
{
get { return (this.Attributes["vert-adv-y"] == null ? this.Parents.OfType<SvgFont>().First().VertAdvY : (float)this.Attributes["vert-adv-y"]); }
set { this.Attributes["vert-adv-y"] = value; }
}
[SvgAttribute("vert-origin-x")]
public float VertOriginX
{
get { return (this.Attributes["vert-origin-x"] == null ? this.Parents.OfType<SvgFont>().First().VertOriginX : (float)this.Attributes["vert-origin-x"]); }
set { this.Attributes["vert-origin-x"] = value; }
}
[SvgAttribute("vert-origin-y")]
public float VertOriginY
{
get { return (this.Attributes["vert-origin-y"] == null ? this.Parents.OfType<SvgFont>().First().VertOriginY : (float)this.Attributes["vert-origin-y"]); }
set { this.Attributes["vert-origin-y"] = value; }
}
/// <summary>
/// Gets the <see cref="GraphicsPath"/> for this element.
/// </summary>
public override GraphicsPath Path(ISvgRenderer renderer)
{
if (this._path == null || this.IsPathDirty)
{
_path = new GraphicsPath();
foreach (SvgPathSegment segment in this.PathData)
{
segment.AddToPath(_path);
}
this.IsPathDirty = false;
}
return _path;
}
/// <summary>
/// Gets or sets a value to determine if anti-aliasing should occur when the element is being rendered.
/// </summary>
protected override bool RequiresSmoothRendering
{
get { return true; }
}
/// <summary>
/// Gets the bounds of the element.
/// </summary>
/// <value>The bounds.</value>
public override System.Drawing.RectangleF Bounds
{
get { return this.Path(null).GetBounds(); }
}
/// <summary>
/// Initializes a new instance of the <see cref="SvgGlyph"/> class.
/// </summary>
public SvgGlyph()
{
var pathData = new SvgPathSegmentList();
this.Attributes["d"] = pathData;
}
public override SvgElement DeepCopy()
{
return DeepCopy<SvgGlyph>();
}
public override SvgElement DeepCopy<T>()
{
var newObj = base.DeepCopy<T>() as SvgGlyph;
foreach (var pathData in this.PathData)
newObj.PathData.Add(pathData.Clone());
return newObj;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Svg
{
public abstract class SvgKern : SvgElement
{
[SvgAttribute("g1")]
public string Glyph1
{
get { return this.Attributes["g1"] as string; }
set { this.Attributes["g1"] = value; }
}
[SvgAttribute("g2")]
public string Glyph2
{
get { return this.Attributes["g2"] as string; }
set { this.Attributes["g2"] = value; }
}
[SvgAttribute("u1")]
public string Unicode1
{
get { return this.Attributes["u1"] as string; }
set { this.Attributes["u1"] = value; }
}
[SvgAttribute("u2")]
public string Unicode2
{
get { return this.Attributes["u2"] as string; }
set { this.Attributes["u2"] = value; }
}
[SvgAttribute("k")]
public float Kerning
{
get { return (this.Attributes["k"] == null ? 0 : (float)this.Attributes["k"]); }
set { this.Attributes["k"] = value; }
}
}
[SvgElement("vkern")]
public class SvgVerticalKern : SvgKern
{
public override SvgElement DeepCopy()
{
return base.DeepCopy<SvgVerticalKern>();
}
}
[SvgElement("hkern")]
public class SvgHorizontalKern : SvgKern
{
public override SvgElement DeepCopy()
{
return base.DeepCopy<SvgHorizontalKern>();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Svg
{
[SvgElement("missing-glyph")]
public class SvgMissingGlyph : SvgGlyph
{
[SvgAttribute("glyph-name")]
public override string GlyphName
{
get { return this.Attributes["glyph-name"] as string ?? "__MISSING_GLYPH__"; }
set { this.Attributes["glyph-name"] = value; }
}
}
}
......@@ -11,33 +11,14 @@ using System.Linq;
namespace Svg
{
public enum XmlSpaceHandling
{
@default,
preserve
}
public abstract class SvgTextBase : SvgVisualElement
{
private SvgUnitCollection _x = new SvgUnitCollection();
private SvgUnitCollection _y = new SvgUnitCollection();
private SvgUnitCollection _dy = new SvgUnitCollection();
private SvgUnitCollection _dx = new SvgUnitCollection();
private SvgUnit _letterSpacing;
private SvgUnit _wordSpacing;
private static readonly SvgRenderer _stringMeasure;
private XmlSpaceHandling _space = XmlSpaceHandling.@default;
/// <summary>
/// Initializes the <see cref="SvgTextBase"/> class.
/// </summary>
static SvgTextBase()
{
Bitmap bitmap = new Bitmap(1, 1);
_stringMeasure = SvgRenderer.FromImage(bitmap);
_stringMeasure.TextRenderingHint = TextRenderingHint.AntiAlias;
}
protected SvgUnitCollection _x = new SvgUnitCollection();
protected SvgUnitCollection _y = new SvgUnitCollection();
protected SvgUnitCollection _dy = new SvgUnitCollection();
protected SvgUnitCollection _dx = new SvgUnitCollection();
private string _rotate;
private List<float> _rotations = new List<float>();
/// <summary>
/// Gets or sets the text to be rendered.
......@@ -66,6 +47,12 @@ namespace Svg
set { this.Attributes["baseline-shift"] = value; this.IsPathDirty = true; }
}
public override XmlSpaceHandling SpaceHandling
{
get { return base.SpaceHandling; }
set { base.SpaceHandling = value; this.IsPathDirty = true; }
}
/// <summary>
/// Gets or sets the X.
/// </summary>
......@@ -142,14 +129,56 @@ namespace Svg
}
}
/// <summary>
/// Gets or sets the rotate.
/// </summary>
/// <value>The rotate.</value>
[SvgAttribute("rotate")]
public virtual string Rotate
{
get { return this._rotate; }
set
{
if (_rotate != value)
{
this._rotate = value;
this._rotations.Clear();
this._rotations.AddRange(from r in _rotate.Split(new char[] {',', ' ', '\r', '\n', '\t'}, StringSplitOptions.RemoveEmptyEntries) select float.Parse(r));
this.IsPathDirty = true;
OnAttributeChanged(new AttributeEventArgs { Attribute = "rotate", Value = value });
}
}
}
/// <summary>
/// The pre-calculated length of the text
/// </summary>
[SvgAttribute("textLength")]
public virtual SvgUnit TextLength
{
get { return (this.Attributes["textLength"] == null ? SvgUnit.None : (SvgUnit)this.Attributes["textLength"]); }
set { this.Attributes["textLength"] = value; this.IsPathDirty = true; }
}
/// <summary>
/// Gets or sets the text anchor.
/// </summary>
/// <value>The text anchor.</value>
[SvgAttribute("lengthAdjust")]
public virtual SvgTextLengthAdjust LengthAdjust
{
get { return (this.Attributes["lengthAdjust"] == null) ? SvgTextLengthAdjust.spacing : (SvgTextLengthAdjust)this.Attributes["lengthAdjust"]; }
set { this.Attributes["lengthAdjust"] = value; this.IsPathDirty = true; }
}
/// <summary>
/// Specifies spacing behavior between text characters.
/// </summary>
[SvgAttribute("letter-spacing")]
public virtual SvgUnit LetterSpacing
{
get { return this._letterSpacing; }
set { this._letterSpacing = value; this.IsPathDirty = true; }
get { return (this.Attributes["letter-spacing"] == null ? SvgUnit.None : (SvgUnit)this.Attributes["letter-spacing"]); }
set { this.Attributes["letter-spacing"] = value; this.IsPathDirty = true; }
}
/// <summary>
......@@ -158,8 +187,8 @@ namespace Svg
[SvgAttribute("word-spacing")]
public virtual SvgUnit WordSpacing
{
get { return this._wordSpacing; }
set { this._wordSpacing = value; this.IsPathDirty = true; }
get { return (this.Attributes["word-spacing"] == null ? SvgUnit.None : (SvgUnit)this.Attributes["word-spacing"]); }
set { this.Attributes["word-spacing"] = value; this.IsPathDirty = true; }
}
/// <summary>
......@@ -215,9 +244,9 @@ namespace Svg
/// <summary>
/// Renders the <see cref="SvgElement"/> and contents to the specified <see cref="Graphics"/> object.
/// </summary>
/// <param name="renderer">The <see cref="SvgRenderer"/> object to render to.</param>
/// <param name="renderer">The <see cref="ISvgRenderer"/> object to render to.</param>
/// <remarks>Necessary to make sure that any internal tspan elements get rendered as well</remarks>
protected override void Render(SvgRenderer renderer)
protected override void Render(ISvgRenderer renderer)
{
if ((this.Path(renderer) != null) && this.Visible && this.Displayable)
{
......@@ -245,185 +274,138 @@ namespace Svg
}
}
private GraphicsPath _path;
protected class NodeBounds
internal virtual IEnumerable<ISvgNode> GetContentNodes()
{
public float xOffset { get; set; }
public SizeF Bounds { get; set; }
public ISvgNode Node { get; set; }
return (this.Nodes == null || this.Nodes.Count < 1 ? this.Children.OfType<ISvgNode>() : this.Nodes);
}
protected class BoundsData
{
private List<NodeBounds> _nodes = new List<NodeBounds>();
public IList<NodeBounds> Nodes
protected virtual GraphicsPath GetBaselinePath(ISvgRenderer renderer)
{
get { return _nodes; }
return null;
}
public SizeF Bounds { get; set; }
}
protected BoundsData GetTextBounds(SvgRenderer renderer)
protected virtual float GetAuthorPathLength()
{
var font = GetFont(renderer);
SvgTextBase innerText;
SizeF stringBounds;
float totalHeight = 0;
float totalWidth = 0;
return 0;
}
var result = new BoundsData();
var nodes = (from n in this.Nodes
where (n is SvgContentNode || n is SvgTextBase) && !string.IsNullOrEmpty(n.Content)
select n).ToList();
private GraphicsPath _path;
if (nodes.FirstOrDefault() is SvgContentNode && _x.Count > 1)
/// <summary>
/// Gets the <see cref="GraphicsPath"/> for this element.
/// </summary>
/// <value></value>
public override System.Drawing.Drawing2D.GraphicsPath Path(ISvgRenderer renderer)
{
string ch;
var content = nodes.First() as SvgContentNode;
nodes.RemoveAt(0);
int posCount = Math.Min(content.Content.Length, _x.Count);
var text = PrepareText(content.Content, false, (nodes.Count > 1 && nodes[1] is SvgTextBase));
// Make sure the path is always null if there is no text
//if there is a TSpan inside of this text element then path should not be null (even if this text is empty!)
var nodes = this.GetContentNodes().ToList();
if (nodes.Count < 1) return _path = null;
if (nodes.Count == 1 && nodes[0] is SvgContentNode &&
(string.IsNullOrEmpty(nodes[0].Content) || nodes[0].Content.Trim().Length < 1)) return _path = null;
for (var i = 0; i < posCount; i++)
{
ch = (i == posCount - 1 ? text.Substring(i) : text.Substring(i, 1));
stringBounds = _stringMeasure.MeasureString(ch, font);
totalHeight = Math.Max(totalHeight, stringBounds.Height);
result.Nodes.Add(new NodeBounds()
if (_path == null || this.IsPathDirty)
{
Bounds = stringBounds,
Node = new SvgContentNode() { Content = ch },
xOffset = (i == 0 ? 0 : _x[i].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this) -
_x[0].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this))
});
renderer = (renderer ?? SvgRenderer.FromNull());
this.SetPath(new TextDrawingState(renderer, this));
}
return _path;
}
ISvgNode node;
for (var i = 0; i < nodes.Count; i++)
private void SetPath(TextDrawingState state)
{
SetPath(state, true);
}
/// <summary>
/// Sets the path on this element and all child elements. Uses the state
/// object to track the state of the drawing
/// </summary>
/// <param name="state">State of the drawing operation</param>
private void SetPath(TextDrawingState state, bool doMeasurements)
{
node = nodes[i];
lock (_stringMeasure)
SvgTextBase inner;
TextDrawingState newState;
TextDrawingState origState = null;
bool alignOnBaseline = state.BaselinePath != null && (this.TextAnchor == SvgTextAnchor.Middle || this.TextAnchor == SvgTextAnchor.End);
if (doMeasurements)
{
innerText = node as SvgTextBase;
if (innerText == null)
if (this.TextLength != SvgUnit.None)
{
stringBounds = _stringMeasure.MeasureString(PrepareText(node.Content,
i > 0 && nodes[i - 1] is SvgTextBase,
i < nodes.Count - 1 && nodes[i + 1] is SvgTextBase), font);
result.Nodes.Add(new NodeBounds() { Bounds = stringBounds, Node = node, xOffset = totalWidth });
origState = state.Clone();
}
else
else if (alignOnBaseline)
{
stringBounds = innerText.GetTextBounds(renderer).Bounds;
result.Nodes.Add(new NodeBounds() { Bounds = stringBounds, Node = node, xOffset = totalWidth });
if (innerText.Dx.Count == 1) totalWidth += innerText.Dx[0].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this);
origState = state.Clone();
state.BaselinePath = null;
}
totalHeight = Math.Max(totalHeight, stringBounds.Height);
totalWidth += stringBounds.Width;
}
foreach (var node in GetContentNodes())
{
inner = node as SvgTextBase;
if (inner == null)
{
if (!string.IsNullOrEmpty(node.Content)) state.DrawString(PrepareText(node.Content));
}
else
{
newState = new TextDrawingState(state, inner);
inner.SetPath(newState);
state.NumChars += newState.NumChars;
state.Current = newState.Current;
}
result.Bounds = new SizeF(totalWidth, totalHeight);
return result;
}
protected float _calcX = 0;
protected float _calcY = 0;
var path = state.GetPath() ?? new GraphicsPath();
/// <summary>
/// Gets the <see cref="GraphicsPath"/> for this element.
/// </summary>
/// <value></value>
public override System.Drawing.Drawing2D.GraphicsPath Path(SvgRenderer renderer)
// Apply any text length adjustments
if (doMeasurements)
{
// Make sure the path is always null if there is no text
//if there is a TSpan inside of this text element then path should not be null (even if this text is empty!)
if ((string.IsNullOrEmpty(this.Text) || this.Text.Trim().Length < 1) && this.Children.Where(x => x is SvgTextSpan).Select(x => x as SvgTextSpan).Count() == 0)
return _path = null;
//NOT SURE WHAT THIS IS ABOUT - Path gets created again anyway - WTF?
// When an empty string is passed to GraphicsPath, it rises an InvalidArgumentException...
if (_path == null || this.IsPathDirty)
if (this.TextLength != SvgUnit.None)
{
renderer = (renderer ?? SvgRenderer.FromNull());
// Measure the overall bounds of all the text
var boundsData = GetTextBounds(renderer);
var font = GetFont(renderer);
SvgTextBase innerText;
float x = (_x.Count < 1 ? _calcX : _x[0].ToDeviceValue(renderer, UnitRenderingType.HorizontalOffset, this)) +
(_dx.Count < 1 ? 0 : _dx[0].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this));
float y = (_y.Count < 1 ? _calcY : _y[0].ToDeviceValue(renderer, UnitRenderingType.VerticalOffset, this)) +
(_dy.Count < 1 ? 0 : _dy[0].ToDeviceValue(renderer, UnitRenderingType.Vertical, this));
_path = new GraphicsPath();
_path.StartFigure();
// Determine the location of the start point
switch (this.TextAnchor)
var bounds = path.GetBounds();
var specLength = this.TextLength.ToDeviceValue(state.Renderer, UnitRenderingType.Horizontal, this);
var actLength = bounds.Width;
var diff = (actLength - specLength);
if (Math.Abs(diff) > 1.5)
{
case SvgTextAnchor.Middle:
x -= (boundsData.Bounds.Width / 2);
break;
case SvgTextAnchor.End:
x -= boundsData.Bounds.Width;
break;
if (this.LengthAdjust == SvgTextLengthAdjust.spacing)
{
origState.LetterSpacingAdjust = -1 * diff / (state.NumChars - origState.NumChars - 1);
SetPath(origState, false);
return;
}
try
else
{
renderer.Boundable(new FontBoundable(font));
switch (this.BaselineShift)
using (var matrix = new Matrix())
{
case null:
case "":
case "baseline":
case "inherit":
// do nothing
break;
case "sub":
y += new SvgUnit(SvgUnitType.Ex, 1).ToDeviceValue(renderer, UnitRenderingType.Vertical, this);
break;
case "super":
y -= new SvgUnit(SvgUnitType.Ex, 1).ToDeviceValue(renderer, UnitRenderingType.Vertical, this);
break;
default:
var convert = new SvgUnitConverter();
var shift = (SvgUnit)convert.ConvertFromInvariantString(this.BaselineShift);
y -= shift.ToDeviceValue(renderer, UnitRenderingType.Vertical, this);
break;
matrix.Translate(-1 * bounds.X, 0, MatrixOrder.Append);
matrix.Scale(specLength / actLength, 1, MatrixOrder.Append);
matrix.Translate(bounds.X, 0, MatrixOrder.Append);
path.Transform(matrix);
}
}
finally
{
renderer.PopBoundable();
}
NodeBounds data;
var yCummOffset = 0.0f;
for (var i = 0; i < boundsData.Nodes.Count; i++)
}
else if (alignOnBaseline)
{
data = boundsData.Nodes[i];
innerText = data.Node as SvgTextBase;
if (innerText == null)
var bounds = path.GetBounds();
if (this.TextAnchor == SvgTextAnchor.Middle)
{
// Minus FontSize because the x/y coords mark the bottom left, not bottom top.
DrawString(renderer, _path, x + data.xOffset, y - boundsData.Bounds.Height, font,
PrepareText(data.Node.Content, i > 0 && boundsData.Nodes[i - 1].Node is SvgTextBase,
i < boundsData.Nodes.Count - 1 && boundsData.Nodes[i + 1].Node is SvgTextBase));
origState.StartOffsetAdjust = -1 * bounds.Width / 2;
}
else
{
innerText._calcX = x + data.xOffset;
innerText._calcY = y + yCummOffset;
if (innerText.Dy.Count == 1) yCummOffset += innerText.Dy[0].ToDeviceValue(renderer, UnitRenderingType.Vertical, this);
origState.StartOffsetAdjust = -1 * bounds.Width;
}
SetPath(origState, false);
return;
}
}
_path.CloseFigure();
_path = path;
this.IsPathDirty = false;
}
return _path;
}
private static readonly Regex MultipleSpaces = new Regex(@" {2,}", RegexOptions.Compiled);
......@@ -432,67 +414,19 @@ namespace Svg
/// </summary>
/// <param name="value">Text to be prepared</param>
/// <returns>Prepared text</returns>
protected string PrepareText(string value, bool leadingSpace, bool trailingSpace)
protected string PrepareText(string value)
{
if (_space == XmlSpaceHandling.preserve)
if (this.SpaceHandling == XmlSpaceHandling.preserve)
{
return value.Replace('\t', ' ').Replace("\r\n", " ").Replace('\r', ' ').Replace('\n', ' ');
}
else
{
var convValue = MultipleSpaces.Replace(value.Replace("\r", "").Replace("\n", "").Replace('\t', ' '), " ");
//if (!leadingSpace) convValue = convValue.TrimStart();
//if (!trailingSpace) convValue = convValue.TrimEnd();
return convValue;
}
}
/// <summary>
/// Draws a string on a path at a specified location and with a specified font.
/// </summary>
internal void DrawString(SvgRenderer renderer, GraphicsPath path, float x, float y, Font font, string text)
{
PointF location = new PointF(x, y);
// No way to do letter-spacing or word-spacing, so do manually
if (this.LetterSpacing.Value > 0.0f || this.WordSpacing.Value > 0.0f)
{
// Cut up into words, or just leave as required
string[] words = (this.WordSpacing.Value > 0.0f) ? text.Split(' ') : new string[] { text };
float wordSpacing = this.WordSpacing.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this);
float letterSpacing = this.LetterSpacing.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this);
float start = x;
foreach (string word in words)
{
// Only do if there is line spacing, just write the word otherwise
if (this.LetterSpacing.Value > 0.0f)
{
char[] characters = word.ToCharArray();
foreach (char currentCharacter in characters)
{
path.AddString(currentCharacter.ToString(), font.FontFamily, (int)font.Style, font.Size, location, StringFormat.GenericTypographic);
location = new PointF(path.GetBounds().Width + start + letterSpacing, location.Y);
}
}
else
{
path.AddString(word, font.FontFamily, (int)font.Style, font.Size, location, StringFormat.GenericTypographic);
}
// Move the location of the word to be written along
location = new PointF(path.GetBounds().Width + start + wordSpacing, location.Y);
}
}
else
{
if (!string.IsNullOrEmpty(text))
{
path.AddString(text, font.FontFamily, (int)font.Style, font.Size, location, StringFormat.GenericTypographic);
}
}
}
[SvgAttribute("onchange")]
public event EventHandler<StringArg> Change;
......@@ -511,6 +445,23 @@ namespace Svg
}
}
//private static GraphicsPath GetPath(string text, Font font)
//{
// var fontMetrics = (from c in text.Distinct()
// select new { Char = c, Metrics = Metrics(c, font) }).
// ToDictionary(c => c.Char, c=> c.Metrics);
// // Measure each character and check the metrics against the overall metrics of rendering
// // an entire word with kerning.
//}
//private static RectangleF Metrics(char c, Font font)
//{
// var path = new GraphicsPath();
// path.AddString(c.ToString(), font.FontFamily, (int)font.Style, font.Size, new Point(0, 0), StringFormat.GenericTypographic);
// return path.GetBounds();
//}
#if Net4
public override void RegisterEvents(ISvgEventCaller caller)
{
......@@ -534,11 +485,17 @@ namespace Svg
private class FontBoundable : ISvgBoundable
{
private Font _font;
private IFontDefn _font;
private float _width = 1;
public FontBoundable(Font font)
public FontBoundable(IFontDefn font)
{
_font = font;
}
public FontBoundable(IFontDefn font, float width)
{
_font = font;
_width = width;
}
public PointF Location
......@@ -548,7 +505,7 @@ namespace Svg
public SizeF Size
{
get { return new SizeF(1, _font.Size); }
get { return new SizeF(_width, _font.Size); }
}
public RectangleF Bounds
......@@ -556,5 +513,408 @@ namespace Svg
get { return new RectangleF(this.Location, this.Size); }
}
}
private class TextDrawingState
{
private float _xAnchor = float.MinValue;
private IList<GraphicsPath> _anchoredPaths = new List<GraphicsPath>();
private GraphicsPath _currPath = null;
private GraphicsPath _finalPath = null;
private float _authorPathLength = 0;
public GraphicsPath BaselinePath { get; set; }
public PointF Current { get; set; }
public SvgTextBase Element { get; set; }
public float LetterSpacingAdjust { get; set; }
public int NumChars { get; set; }
public TextDrawingState Parent { get; set; }
public ISvgRenderer Renderer { get; set; }
public float StartOffsetAdjust { get; set; }
private TextDrawingState() { }
public TextDrawingState(ISvgRenderer renderer, SvgTextBase element)
{
this.Element = element;
this.Renderer = renderer;
this.Current = PointF.Empty;
_xAnchor = 0;
this.BaselinePath = element.GetBaselinePath(renderer);
_authorPathLength = element.GetAuthorPathLength();
}
public TextDrawingState(TextDrawingState parent, SvgTextBase element)
{
this.Element = element;
this.Renderer = parent.Renderer;
this.Parent = parent;
this.Current = parent.Current;
this.BaselinePath = element.GetBaselinePath(parent.Renderer) ?? parent.BaselinePath;
var currPathLength = element.GetAuthorPathLength();
_authorPathLength = currPathLength == 0 ? parent._authorPathLength : currPathLength;
}
public GraphicsPath GetPath()
{
FlushPath();
return _finalPath;
}
public TextDrawingState Clone()
{
var result = new TextDrawingState();
result._anchoredPaths = this._anchoredPaths.ToList();
result.BaselinePath = this.BaselinePath;
result._xAnchor = this._xAnchor;
result.Current = this.Current;
result.Element = this.Element;
result.NumChars = this.NumChars;
result.Parent = this.Parent;
result.Renderer = this.Renderer;
return result;
}
public void DrawString(string value)
{
// Get any defined anchors
var xAnchors = GetValues(value.Length, e => e._x, UnitRenderingType.HorizontalOffset);
var yAnchors = GetValues(value.Length, e => e._y, UnitRenderingType.VerticalOffset);
using (var font = this.Element.GetFont(this.Renderer))
{
var fontBaselineHeight = font.Ascent(this.Renderer);
PathStatistics pathStats = null;
var pathScale = 1.0;
if (BaselinePath != null)
{
pathStats = new PathStatistics(BaselinePath.PathData);
if (_authorPathLength > 0) pathScale = _authorPathLength / pathStats.TotalLength;
}
// Get all of the offsets (explicit and defined by spacing)
IList<float> xOffsets;
IList<float> yOffsets;
IList<float> rotations;
float baselineShift = 0.0f;
try
{
this.Renderer.SetBoundable(new FontBoundable(font, (float)(pathStats == null ? 1 : pathStats.TotalLength)));
xOffsets = GetValues(value.Length, e => e._dx, UnitRenderingType.Horizontal);
yOffsets = GetValues(value.Length, e => e._dy, UnitRenderingType.Vertical);
if (StartOffsetAdjust != 0.0f)
{
if (xOffsets.Count < 1)
{
xOffsets.Add(StartOffsetAdjust);
}
else
{
xOffsets[0] += StartOffsetAdjust;
}
}
if (this.Element.LetterSpacing.Value != 0.0f || this.Element.WordSpacing.Value != 0.0f || this.LetterSpacingAdjust != 0.0f)
{
var spacing = this.Element.LetterSpacing.ToDeviceValue(this.Renderer, UnitRenderingType.Horizontal, this.Element) + this.LetterSpacingAdjust;
var wordSpacing = this.Element.WordSpacing.ToDeviceValue(this.Renderer, UnitRenderingType.Horizontal, this.Element);
if (this.Parent == null && this.NumChars == 0 && xOffsets.Count < 1) xOffsets.Add(0);
for (int i = (this.Parent == null && this.NumChars == 0 ? 1 : 0); i < value.Length; i++)
{
if (i >= xOffsets.Count)
{
xOffsets.Add(spacing + (char.IsWhiteSpace(value[i]) ? wordSpacing : 0));
}
else
{
xOffsets[i] += spacing + (char.IsWhiteSpace(value[i]) ? wordSpacing : 0);
}
}
}
rotations = GetValues(value.Length, e => e._rotations);
// Calculate Y-offset due to baseline shift. Don't inherit the value so that it is not accumulated multiple times.
var baselineShiftText = this.Element.Attributes.GetAttribute<string>("baseline-shift");
switch (baselineShiftText)
{
case null:
case "":
case "baseline":
case "inherit":
// do nothing
break;
case "sub":
baselineShift = new SvgUnit(SvgUnitType.Ex, 1).ToDeviceValue(this.Renderer, UnitRenderingType.Vertical, this.Element);
break;
case "super":
baselineShift = -1 * new SvgUnit(SvgUnitType.Ex, 1).ToDeviceValue(this.Renderer, UnitRenderingType.Vertical, this.Element);
break;
default:
var convert = new SvgUnitConverter();
var shiftUnit = (SvgUnit)convert.ConvertFromInvariantString(baselineShiftText);
baselineShift = -1 * shiftUnit.ToDeviceValue(this.Renderer, UnitRenderingType.Vertical, this.Element);
break;
}
if (baselineShift != 0.0f)
{
if (yOffsets.Any())
{
yOffsets[0] += baselineShift;
}
else
{
yOffsets.Add(baselineShift);
}
}
}
finally
{
this.Renderer.PopBoundable();
}
// NOTE: Assuming a horizontal left-to-right font
// Render absolutely positioned items in the horizontal direction
var yPos = Current.Y;
for (int i = 0; i < xAnchors.Count - 1; i++)
{
FlushPath();
_xAnchor = xAnchors[i] + (xOffsets.Count > i ? xOffsets[i] : 0);
EnsurePath();
yPos = (yAnchors.Count > i ? yAnchors[i] : yPos) + (yOffsets.Count > i ? yOffsets[i] : 0);
DrawStringOnCurrPath(value[i].ToString(), font, new PointF(_xAnchor, yPos),
fontBaselineHeight, (rotations.Count > i ? rotations[i] : rotations.LastOrDefault()));
}
// Render any remaining characters
var renderChar = 0;
var xPos = this.Current.X;
if (xAnchors.Any())
{
FlushPath();
renderChar = xAnchors.Count - 1;
xPos = xAnchors.Last();
_xAnchor = xPos;
}
EnsurePath();
// Render individual characters as necessary
var lastIndividualChar = renderChar + Math.Max(Math.Max(Math.Max(Math.Max(xOffsets.Count, yOffsets.Count), yAnchors.Count), rotations.Count) - renderChar - 1, 0);
if (rotations.LastOrDefault() != 0.0f || pathStats != null) lastIndividualChar = value.Length;
if (lastIndividualChar > renderChar)
{
var charBounds = font.MeasureCharacters(this.Renderer, value.Substring(renderChar, Math.Min(lastIndividualChar + 1, value.Length) - renderChar));
PointF pathPoint;
float rotation;
float halfWidth;
for (int i = renderChar; i < lastIndividualChar; i++)
{
xPos += (float)pathScale * (xOffsets.Count > i ? xOffsets[i] : 0) + (charBounds[i - renderChar].X - (i == renderChar ? 0 : charBounds[i - renderChar - 1].X));
yPos = (yAnchors.Count > i ? yAnchors[i] : yPos) + (yOffsets.Count > i ? yOffsets[i] : 0);
if (pathStats == null)
{
DrawStringOnCurrPath(value[i].ToString(), font, new PointF(xPos, yPos),
fontBaselineHeight, (rotations.Count > i ? rotations[i] : rotations.LastOrDefault()));
}
else
{
xPos = Math.Max(xPos, 0);
halfWidth = charBounds[i - renderChar].Width / 2;
if (pathStats.OffsetOnPath(xPos + halfWidth))
{
pathStats.LocationAngleAtOffset(xPos + halfWidth, out pathPoint, out rotation);
pathPoint = new PointF((float)(pathPoint.X - halfWidth * Math.Cos(rotation * Math.PI / 180) - (float)pathScale * yPos * Math.Sin(rotation * Math.PI / 180)),
(float)(pathPoint.Y - halfWidth * Math.Sin(rotation * Math.PI / 180) + (float)pathScale * yPos * Math.Cos(rotation * Math.PI / 180)));
DrawStringOnCurrPath(value[i].ToString(), font, pathPoint, fontBaselineHeight, rotation);
}
}
}
// Add the kerning to the next character
if (lastIndividualChar < value.Length)
{
xPos += charBounds[charBounds.Count - 1].X - charBounds[charBounds.Count - 2].X;
}
else
{
xPos += charBounds.Last().Width;
}
}
// Render the string normally
if (lastIndividualChar < value.Length)
{
xPos += (xOffsets.Count > lastIndividualChar ? xOffsets[lastIndividualChar] : 0);
yPos = (yAnchors.Count > lastIndividualChar ? yAnchors[lastIndividualChar] : yPos) +
(yOffsets.Count > lastIndividualChar ? yOffsets[lastIndividualChar] : 0);
DrawStringOnCurrPath(value.Substring(lastIndividualChar), font, new PointF(xPos, yPos),
fontBaselineHeight, rotations.LastOrDefault());
var bounds = font.MeasureString(this.Renderer, value.Substring(lastIndividualChar));
xPos += bounds.Width;
}
NumChars += value.Length;
// Undo any baseline shift. This is not persisted, unlike normal vertical offsets.
this.Current = new PointF(xPos, yPos - baselineShift);
}
}
private void DrawStringOnCurrPath(string value, IFontDefn font, PointF location, float fontBaselineHeight, float rotation)
{
var drawPath = _currPath;
if (rotation != 0.0f) drawPath = new GraphicsPath();
font.AddStringToPath(this.Renderer, drawPath, value, new PointF(location.X, location.Y - fontBaselineHeight));
if (rotation != 0.0f && drawPath.PointCount > 0)
{
using (var matrix = new Matrix())
{
matrix.Translate(-1 * location.X, -1 * location.Y, MatrixOrder.Append);
matrix.Rotate(rotation, MatrixOrder.Append);
matrix.Translate(location.X, location.Y, MatrixOrder.Append);
drawPath.Transform(matrix);
_currPath.AddPath(drawPath, false);
}
}
}
private void EnsurePath()
{
if (_currPath == null)
{
_currPath = new GraphicsPath();
_currPath.StartFigure();
var currState = this;
while (currState != null && currState._xAnchor <= float.MinValue)
{
currState = currState.Parent;
}
currState._anchoredPaths.Add(_currPath);
}
}
private void FlushPath()
{
if (_currPath != null)
{
_currPath.CloseFigure();
// Abort on empty paths (e.g. rendering a space)
if (_currPath.PointCount < 1)
{
_anchoredPaths.Clear();
_xAnchor = float.MinValue;
_currPath = null;
return;
}
if (_xAnchor > float.MinValue)
{
float minX = float.MaxValue;
float maxX = float.MinValue;
RectangleF bounds;
foreach (var path in _anchoredPaths)
{
bounds = path.GetBounds();
if (bounds.Left < minX) minX = bounds.Left;
if (bounds.Right > maxX) maxX = bounds.Right;
}
var xOffset = 0f; //_xAnchor - minX;
switch (Element.TextAnchor)
{
case SvgTextAnchor.Middle:
xOffset -= (maxX - minX) / 2;
break;
case SvgTextAnchor.End:
xOffset -= (maxX - minX);
break;
}
if (xOffset != 0)
{
using (var matrix = new Matrix())
{
matrix.Translate(xOffset, 0);
foreach (var path in _anchoredPaths)
{
path.Transform(matrix);
}
}
}
_anchoredPaths.Clear();
_xAnchor = float.MinValue;
}
if (_finalPath == null)
{
_finalPath = _currPath;
}
else
{
_finalPath.AddPath(_currPath, false);
}
_currPath = null;
}
}
private IList<float> GetValues(int maxCount, Func<SvgTextBase, IEnumerable<float>> listGetter)
{
var currState = this;
int charCount = 0;
var results = new List<float>();
int resultCount = 0;
while (currState != null)
{
charCount += currState.NumChars;
results.AddRange(listGetter.Invoke(currState.Element).Skip(charCount).Take(maxCount));
if (results.Count > resultCount)
{
maxCount -= results.Count - resultCount;
charCount += results.Count - resultCount;
resultCount = results.Count;
}
if (maxCount < 1) return results;
currState = currState.Parent;
}
return results;
}
private IList<float> GetValues(int maxCount, Func<SvgTextBase, IEnumerable<SvgUnit>> listGetter, UnitRenderingType renderingType)
{
var currState = this;
int charCount = 0;
var results = new List<float>();
int resultCount = 0;
while (currState != null)
{
charCount += currState.NumChars;
results.AddRange(listGetter.Invoke(currState.Element).Skip(charCount).Take(maxCount).Select(p => p.ToDeviceValue(currState.Renderer, renderingType, currState.Element)));
if (results.Count > resultCount)
{
maxCount -= results.Count - resultCount;
charCount += results.Count - resultCount;
resultCount = results.Count;
}
if (maxCount < 1) return results;
currState = currState.Parent;
}
return results;
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Diagnostics;
namespace Svg
{
/// <summary>
/// The <see cref="SvgText"/> element defines a graphics element consisting of text.
/// </summary>
[SvgElement("textPath")]
public class SvgTextPath : SvgTextBase
{
private Uri _referencedPath;
public override SvgUnitCollection Dx
{
get { return null; }
set { /* do nothing */ }
}
[SvgAttribute("startOffset")]
public virtual SvgUnit StartOffset
{
get { return (_dx.Count < 1 ? SvgUnit.None : _dx[0]); }
set
{
if (_dx.Count < 1)
{
_dx.Add(value);
}
else
{
_dx[0] = value;
}
}
}
[SvgAttribute("method")]
public virtual SvgTextPathMethod Method
{
get { return (this.Attributes["method"] == null ? SvgTextPathMethod.align : (SvgTextPathMethod)this.Attributes["method"]); }
set { this.Attributes["method"] = value; }
}
[SvgAttribute("spacing")]
public virtual SvgTextPathSpacing Spacing
{
get { return (this.Attributes["spacing"] == null ? SvgTextPathSpacing.exact : (SvgTextPathSpacing)this.Attributes["spacing"]); }
set { this.Attributes["spacing"] = value; }
}
[SvgAttribute("href", SvgAttributeAttribute.XLinkNamespace)]
public virtual Uri ReferencedPath
{
get { return this._referencedPath; }
set { this._referencedPath = value; }
}
protected override GraphicsPath GetBaselinePath(ISvgRenderer renderer)
{
var path = this.OwnerDocument.IdManager.GetElementById(this.ReferencedPath) as SvgVisualElement;
if (path == null) return null;
var pathData = (GraphicsPath)path.Path(renderer).Clone();
if (path.Transforms.Count > 0)
{
Matrix transformMatrix = new Matrix(1, 0, 0, 1, 0, 0);
foreach (var transformation in path.Transforms)
{
transformMatrix.Multiply(transformation.Matrix);
}
pathData.Transform(transformMatrix);
}
return pathData;
}
protected override float GetAuthorPathLength()
{
var path = this.OwnerDocument.IdManager.GetElementById(this.ReferencedPath) as SvgPath;
if (path == null) return 0;
return path.PathLength;
}
public override SvgElement DeepCopy()
{
return base.DeepCopy<SvgTextPath>();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Svg
{
[SvgElement("tref")]
public class SvgTextRef : SvgTextBase
{
private Uri _referencedElement;
[SvgAttribute("href", SvgAttributeAttribute.XLinkNamespace)]
public virtual Uri ReferencedElement
{
get { return this._referencedElement; }
set { this._referencedElement = value; }
}
internal override IEnumerable<ISvgNode> GetContentNodes()
{
var refText = this.OwnerDocument.IdManager.GetElementById(this.ReferencedElement) as SvgTextBase;
if (refText == null)
{
return base.GetContentNodes();
}
else
{
return refText.GetContentNodes();
}
}
public override SvgElement DeepCopy()
{
return DeepCopy<SvgTextRef>();
}
public override SvgElement DeepCopy<T>()
{
var newObj = base.DeepCopy<T>() as SvgTextRef;
newObj.X = this.X;
newObj.Y = this.Y;
newObj.Dx = this.Dx;
newObj.Dy = this.Dy;
newObj.Text = this.Text;
newObj.ReferencedElement = this.ReferencedElement;
return newObj;
}
}
}
......@@ -18,14 +18,14 @@ namespace Svg
/// </summary>
SvgTransformCollection Transforms { get; set; }
/// <summary>
/// Applies the required transforms to <see cref="SvgRenderer"/>.
/// Applies the required transforms to <see cref="ISvgRenderer"/>.
/// </summary>
/// <param name="renderer">The <see cref="SvgRenderer"/> to be transformed.</param>
void PushTransforms(SvgRenderer renderer);
/// <param name="renderer">The <see cref="ISvgRenderer"/> to be transformed.</param>
void PushTransforms(ISvgRenderer renderer);
/// <summary>
/// Removes any previously applied transforms from the specified <see cref="SvgRenderer"/>.
/// Removes any previously applied transforms from the specified <see cref="ISvgRenderer"/>.
/// </summary>
/// <param name="renderer">The <see cref="SvgRenderer"/> that should have transforms removed.</param>
void PopTransforms(SvgRenderer renderer);
/// <param name="renderer">The <see cref="ISvgRenderer"/> that should have transforms removed.</param>
void PopTransforms(ISvgRenderer renderer);
}
}
\ No newline at end of file
......@@ -27,7 +27,7 @@ namespace Svg.Transforms
{
get
{
System.Drawing.Drawing2D.Matrix matrix = new System.Drawing.Drawing2D.Matrix();
var matrix = new System.Drawing.Drawing2D.Matrix();
matrix.Scale(this.X, this.Y);
return matrix;
}
......@@ -35,6 +35,7 @@ namespace Svg.Transforms
public override string WriteToString()
{
if (this.X == this.Y) return string.Format(CultureInfo.InvariantCulture, "scale({0})", this.X);
return string.Format(CultureInfo.InvariantCulture, "scale({0}, {1})", this.X, this.Y);
}
......
......@@ -54,5 +54,9 @@ namespace Svg.Transforms
}
#endregion
public override string ToString()
{
return WriteToString();
}
}
}
\ No newline at end of file
......@@ -109,5 +109,11 @@ namespace Svg.Transforms
}
return result;
}
public override string ToString()
{
if (this.Count < 1) return string.Empty;
return (from t in this select t.ToString()).Aggregate((p,c) => p + " " + c);
}
}
}
......@@ -5,6 +5,7 @@ using System.Windows.Forms;
using System.Drawing;
using System.IO;
using Svg;
using System.Diagnostics;
namespace SvgW3CTestRunner
{
......@@ -17,25 +18,39 @@ namespace SvgW3CTestRunner
{
InitializeComponent();
// ignore tests pertaining to javascript or xml reading
var files = (from f in (from g in Directory.GetFiles(_svgBasePath)
var passes = File.ReadAllLines(_svgBasePath + @"..\PassingTests.txt").ToDictionary((f) => f, (f) => true);
var files = (from f in
(from g in Directory.GetFiles(_svgBasePath)
select Path.GetFileName(g))
where !f.StartsWith("animate-") && !f.StartsWith("conform-viewer") &&
!f.Contains("-dom-") && !f.StartsWith("linking-") && !f.StartsWith("interact-")
!f.Contains("-dom-") && !f.StartsWith("linking-") && !f.StartsWith("interact-") &&
!f.StartsWith("script-")
orderby f
select (object)f);
files = files.Where((f) => !passes.ContainsKey((string)f)).Union(Enumerable.Repeat((object)"## PASSING ##", 1)).Union(files.Where((f) => passes.ContainsKey((string)f)));
lstFiles.Items.AddRange(files.ToArray());
}
private void lstFiles_SelectedIndexChanged(object sender, EventArgs e)
{
var fileName = lstFiles.SelectedItem.ToString();
if (fileName.StartsWith("#")) return;
try
{
Debug.Print(fileName);
var doc = SvgDocument.Open(_svgBasePath + fileName);
if (fileName.StartsWith("__"))
{
picSvg.Image = doc.Draw();
}
else
{
var img = new Bitmap(480, 360);
doc.Draw(img);
picSvg.Image = img;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
......
color-prof-01-f.svg
color-prop-01-b.svg
color-prop-02-f.svg
color-prop-03-t.svg
color-prop-04-t.svg
color-prop-05-t.svg
coords-coord-01-t.svg
coords-coord-02-t.svg
coords-trans-01-b.svg
coords-trans-02-t.svg
coords-trans-03-t.svg
coords-trans-04-t.svg
coords-trans-05-t.svg
coords-trans-06-t.svg
coords-trans-07-t.svg
coords-trans-08-t.svg
coords-trans-09-t.svg
coords-trans-10-f.svg
coords-trans-11-f.svg
coords-trans-12-f.svg
coords-trans-13-f.svg
coords-trans-14-f.svg
coords-transformattr-01-f.svg
coords-transformattr-02-f.svg
coords-transformattr-03-f.svg
coords-transformattr-04-f.svg
coords-transformattr-05-f.svg
coords-units-01-b.svg
coords-units-02-b.svg
coords-units-03-b.svg
coords-viewattr-01-b.svg
coords-viewattr-02-b.svg
coords-viewattr-03-b.svg
masking-path-01-b.svg
masking-path-02-b.svg
masking-path-04-b.svg
masking-path-05-f.svg
masking-path-06-b.svg
masking-path-03-b.svg
pservers-grad-01-b.svg
pservers-grad-02-b.svg
pservers-grad-03-b.svg
pservers-grad-04-b.svg
pservers-grad-06-b.svg
pservers-grad-07-b.svg
pservers-grad-08-b.svg
pservers-grad-09-b.svg
pservers-grad-10-b.svg
pservers-grad-11-b.svg
pservers-grad-12-b.svg
pservers-grad-14-b.svg
pservers-grad-15-b.svg
pservers-grad-16-b.svg
pservers-grad-17-b.svg
pservers-grad-18-b.svg
pservers-grad-20-b.svg
pservers-grad-22-b.svg
pservers-grad-23-f.svg
pservers-grad-24-f.svg
pservers-grad-stops-01-f.svg
pservers-pattern-01-b.svg
pservers-pattern-02-f.svg
pservers-pattern-03-f.svg
pservers-pattern-04-f.svg
pservers-pattern-05-f.svg
pservers-pattern-06-f.svg
pservers-pattern-07-f.svg
pservers-pattern-08-f.svg
pservers-pattern-09-f.svg
render-elems-01-t.svg
render-elems-02-t.svg
render-elems-03-t.svg
render-elems-06-t.svg
render-elems-07-t.svg
render-elems-08-t.svg
render-groups-03-t.svg
shapes-circle-01-t.svg
shapes-circle-02-t.svg
shapes-ellipse-01-t.svg
shapes-ellipse-02-t.svg
shapes-ellipse-03-f.svg
shapes-grammar-01-f.svg
shapes-intro-01-t.svg
shapes-intro-02-f.svg
shapes-line-01-t.svg
shapes-line-02-f.svg
shapes-polygon-01-t.svg
shapes-polygon-02-t.svg
shapes-polygon-03-t.svg
shapes-polyline-01-t.svg
shapes-polyline-02-t.svg
shapes-rect-01-t.svg
shapes-rect-02-t.svg
shapes-rect-03-t.svg
shapes-rect-04-f.svg
shapes-rect-06-f.svg
shapes-rect-07-f.svg
struct-frag-06-t.svg
struct-group-01-t.svg
struct-group-02-b.svg
paths-data-01-t.svg
paths-data-02-t.svg
paths-data-03-f.svg
paths-data-04-t.svg
paths-data-05-t.svg
paths-data-06-t.svg
paths-data-07-t.svg
paths-data-08-t.svg
paths-data-09-t.svg
paths-data-12-t.svg
paths-data-13-t.svg
paths-data-14-t.svg
paths-data-15-t.svg
paths-data-17-f.svg
paths-data-18-f.svg
paths-data-19-f.svg
paths-data-20-f.svg
struct-image-01-t.svg
struct-image-04-t.svg
struct-image-06-t.svg
masking-path-13-f.svg
masking-path-14-f.svg
painting-control-01-f.svg
painting-control-03-f.svg
painting-fill-01-t.svg
painting-fill-02-t.svg
painting-fill-03-t.svg
painting-fill-04-t.svg
painting-fill-05-b.svg
painting-stroke-01-t.svg
struct-use-05-b.svg
styling-class-01-f.svg
styling-css-01-b.svg
styling-css-03-b.svg
styling-css-07-f.svg
text-align-01-b.svg
text-align-02-b.svg
text-align-03-b.svg
text-fonts-01-t.svg
text-fonts-02-t.svg
text-fonts-03-t.svg
text-fonts-05-f.svg
text-intro-04-t.svg
text-intro-06-t.svg
text-path-01-b.svg
text-path-02-b.svg
text-spacing-01-b.svg
text-text-01-b.svg
text-text-04-t.svg
text-text-05-t.svg
text-text-07-t.svg
text-text-09-t.svg
text-text-10-t.svg
text-text-11-t.svg
text-tref-01-b.svg
text-tspan-01-b.svg
text-ws-01-t.svg
text-ws-02-t.svg
text-ws-03-t.svg
painting-stroke-02-t.svg
coords-viewattr-04-f.svg
fonts-desc-01-t.svg
fonts-elem-05-t.svg
fonts-elem-06-t.svg
paths-data-10-t.svg
paths-data-16-t.svg
shapes-rect-05-f.svg
styling-css-02-b.svg
styling-css-04-f.svg
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment