diff --git a/Source/SVGImage/DotNetProjects.SVGImage.csproj b/Source/SVGImage/DotNetProjects.SVGImage.csproj index 69bd12b..02886db 100644 --- a/Source/SVGImage/DotNetProjects.SVGImage.csproj +++ b/Source/SVGImage/DotNetProjects.SVGImage.csproj @@ -52,12 +52,12 @@ $(DefineConstants);DOTNET40;NETFULL $(DefineConstants);DOTNET45;NETFULL $(DefineConstants);DOTNET46;NETFULL - $(DefineConstants);DOTNET47;NETFULL - $(DefineConstants);DOTNET48;NETFULL - $(DefineConstants);NETCORE - $(DefineConstants);NETCORE;NETNEXT - $(DefineConstants);NETCORE;NETNEXT - $(DefineConstants);NETCORE;NETNEXT + $(DefineConstants);DOTNET47;DPI_AWARE;NETFULL + $(DefineConstants);DOTNET48;DPI_AWARE;NETFULL + $(DefineConstants);NETCORE;DPI_AWARE + $(DefineConstants);NETCORE;DPI_AWARE;NETNEXT + $(DefineConstants);NETCORE;DPI_AWARE;NETNEXT + $(DefineConstants);NETCORE;DPI_AWARE;NETNEXT True SVGImage.snk diff --git a/Source/SVGImage/SVG/SVGImage.cs b/Source/SVGImage/SVG/SVGImage.cs index 26463c3..2f9c6a7 100644 --- a/Source/SVGImage/SVG/SVGImage.cs +++ b/Source/SVGImage/SVG/SVGImage.cs @@ -71,7 +71,7 @@ public enum eSizeType DependencyProperty.Register("UriSource", typeof(Uri), typeof(SVGImage), new FrameworkPropertyMetadata(null, OnUriSourceChanged)); - public static DependencyProperty SizeTypeProperty = DependencyProperty.Register("SizeType", + public static readonly DependencyProperty SizeTypeProperty = DependencyProperty.Register("SizeType", typeof(eSizeType), typeof(SVGImage), new FrameworkPropertyMetadata(eSizeType.ContentToSizeNoStretch, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, new PropertyChangedCallback(OnSizeTypeChanged))); @@ -84,7 +84,7 @@ public enum eSizeType public static readonly DependencyProperty FileSourceProperty = DependencyProperty.Register("FileSource", typeof(string), typeof(SVGImage), new PropertyMetadata(OnFileSourceChanged)); - public static DependencyProperty ImageSourcePoperty = DependencyProperty.Register("ImageSource", + public static readonly DependencyProperty ImageSourcePoperty = DependencyProperty.Register("ImageSource", typeof(Drawing), typeof(SVGImage), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, new PropertyChangedCallback(OnImageSourceChanged))); @@ -247,6 +247,11 @@ public Uri BaseUri } } + + + /// + /// Rerenders if CustomBrushesPropertyChanged, OverrideStrokeWidthPropertyChanged, OverrideStrokeColorPropertyChanged, OverrideFillColorPropertyChanged, OverrideColorPropertyChanged + /// public void ReRenderSvg() { if (_render != null) @@ -364,7 +369,17 @@ protected override void OnInitialized(EventArgs e) _render.OverrideStrokeWidth = OverrideStrokeWidth; _render.UseAnimations = this.UseAnimations; - _loadImage(_render); + //This is to prevent double rendering because setting CustomBrushes = brushesFromSVG; invokes ReRenderSvg + //Not sure if it has any side effects + if (!String.IsNullOrEmpty(FileSource)) + { + _render.LoadDrawingWithoutRender(FileSource); + } + else + { + _loadImage(_render); + } + //_loadImage(_render); _loadImage = null; var brushesFromSVG = new Dictionary(); foreach (var server in _render.SVG.PaintServers.GetServers()) diff --git a/Source/SVGImage/SVG/SVGRender.cs b/Source/SVGImage/SVG/SVGRender.cs index 814d360..7039d93 100644 --- a/Source/SVGImage/SVG/SVGRender.cs +++ b/Source/SVGImage/SVG/SVGRender.cs @@ -65,6 +65,15 @@ public DrawingGroup LoadDrawing(string filename) return this.CreateDrawing(this.SVG); } + internal void LoadDrawingWithoutRender(string filename) + { + this.SVG = new SVG(filename, ExternalFileLoader); + } + internal void LoadDrawingWithoutRender(Stream stream) + { + this.SVG = new SVG(stream, ExternalFileLoader); + } + public DrawingGroup LoadXmlDrawing(string fileXml) { this.SVG = new SVG(this.ExternalFileLoader); @@ -430,13 +439,13 @@ internal DrawingGroup LoadGroup(IList elements, Rect? viewBox, bool isSwi } if (shape is TextShape textShape) { - GeometryGroup gp = TextRender.BuildTextGeometry(textShape); + TextRender textRender2 = new TextRender(); + GeometryGroup gp = textRender2.BuildTextGeometry(textShape); if (gp != null) { foreach (Geometry gm in gp.Children) { - TextSpan tspan = TextRender.GetElement(gm); - if (tspan != null) + if (TextRenderBase.GetElement(gm) is TextShapeBase tspan) { var di = this.NewDrawingItem(tspan, gm); AddDrawingToGroup(grp, shape, di); diff --git a/Source/SVGImage/SVG/Shapes/CharacterLayout.cs b/Source/SVGImage/SVG/Shapes/CharacterLayout.cs new file mode 100644 index 0000000..9abd558 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/CharacterLayout.cs @@ -0,0 +1,48 @@ +using System; + +namespace SVGImage.SVG.Shapes +{ + /// + /// Represents a per-character layout result. + /// + public class CharacterLayout + { + private CharacterLayout() + { + // Default constructor for array creation + } + public CharacterLayout(char character) + { + Character = character; + } + public char Character { get; set; } = '\0'; + public int GlobalIndex { get; set; } + public double X { get; set; } = 0; + public double Y { get; set; } = 0; + public double DX { get; set; } = Double.NaN; + public double DY { get; set; } = Double.NaN; + public double Rotation { get; set; } = Double.NaN; + public bool Hidden { get; set; } = false; + public bool Addressable { get; set; } = true; + public bool Middle { get; set; } = false; + public bool AnchoredChunk { get; set; } = false; + /// + /// Not used, part of the SVG 2.0 spec. + /// + internal bool FirstCharacterInResolvedDescendant { get; set; } + /// + /// The character redefines the X position for anteceding characters. + /// + public bool DoesPositionX { get; internal set; } + /// + /// The character redefines the Y position for anteceding characters. + /// + public bool DoesPositionY { get; internal set; } + + + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/CircleShape.cs b/Source/SVGImage/SVG/Shapes/CircleShape.cs index 91ba9c4..86e0167 100644 --- a/Source/SVGImage/SVG/Shapes/CircleShape.cs +++ b/Source/SVGImage/SVG/Shapes/CircleShape.cs @@ -8,15 +8,8 @@ namespace SVGImage.SVG.Shapes public sealed class CircleShape : Shape { - private static Fill DefaultFill = null; - public CircleShape(SVG svg, XmlNode node) : base(svg, node) { - if (DefaultFill == null) - { - DefaultFill = Fill.CreateDefault(svg, "black"); - } - Rect? box = svg.ViewBox; this.CX = XmlUtil.AttrValue(node, "cx", 0, box.HasValue ? box.Value.Width : svg.Size.Width); @@ -27,14 +20,9 @@ public CircleShape(SVG svg, XmlNode node) : base(svg, node) diagRef = Math.Sqrt(box.Value.Width * box.Value.Width + box.Value.Height * box.Value.Height) / Math.Sqrt(2); this.R = XmlUtil.AttrValue(node, "r", 0, diagRef); } - - public override Fill Fill + protected override Fill DefaultFill() { - get { - Fill f = base.Fill; - if (f == null) f = DefaultFill; - return f; - } + return Fill.CreateDefault(Svg, "black"); } public double CX { get; set; } diff --git a/Source/SVGImage/SVG/Shapes/EllipseShape.cs b/Source/SVGImage/SVG/Shapes/EllipseShape.cs index b72a992..e07ea22 100644 --- a/Source/SVGImage/SVG/Shapes/EllipseShape.cs +++ b/Source/SVGImage/SVG/Shapes/EllipseShape.cs @@ -8,15 +8,9 @@ namespace SVGImage.SVG.Shapes public sealed class EllipseShape : Shape { - private static Fill DefaultFill = null; - public EllipseShape(SVG svg, XmlNode node) : base(svg, node) - { - if (DefaultFill == null) - { - DefaultFill = Fill.CreateDefault(svg, "black"); - } + { Rect? box = svg.ViewBox; @@ -30,13 +24,9 @@ public EllipseShape(SVG svg, XmlNode node) this.RY = XmlUtil.AttrValue(node, "ry", 0, diagRef); } - public override Fill Fill + protected override Fill DefaultFill() { - get { - Fill f = base.Fill; - if (f == null) f = DefaultFill; - return f; - } + return Fill.CreateDefault(Svg, "black"); } public double CX { get; set; } diff --git a/Source/SVGImage/SVG/Shapes/Group.cs b/Source/SVGImage/SVG/Shapes/Group.cs index f82986b..0eba3af 100644 --- a/Source/SVGImage/SVG/Shapes/Group.cs +++ b/Source/SVGImage/SVG/Shapes/Group.cs @@ -88,6 +88,7 @@ private Shape AddToList(SVG svg, XmlNode childnode, Shape parent, bool isDefinit else if (nodeName == SVGTags.sAnimateTransform) retVal = new AnimateTransform(svg, childnode, parent); else if (nodeName == SVGTags.sText) + //retVal = new TextShape(svg, childnode, parent); retVal = new TextShape(svg, childnode, parent); else if (nodeName == SVGTags.sLinearGradient) { diff --git a/Source/SVGImage/SVG/Shapes/ITextChild.cs b/Source/SVGImage/SVG/Shapes/ITextChild.cs new file mode 100644 index 0000000..80df697 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/ITextChild.cs @@ -0,0 +1,11 @@ +namespace SVGImage.SVG.Shapes +{ + public interface ITextChild : ITextNode + { + Shape Parent { get; set; } + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/ITextNode.cs b/Source/SVGImage/SVG/Shapes/ITextNode.cs new file mode 100644 index 0000000..c9d0da2 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/ITextNode.cs @@ -0,0 +1,15 @@ +namespace SVGImage.SVG.Shapes +{ + public interface ITextNode + { + CharacterLayout GetFirstCharacter(); + CharacterLayout GetLastCharacter(); + string GetText(); + int GetLength(); + CharacterLayout[] GetCharacters(); + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/LengthAdjustment.cs b/Source/SVGImage/SVG/Shapes/LengthAdjustment.cs new file mode 100644 index 0000000..aef7fb8 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/LengthAdjustment.cs @@ -0,0 +1,19 @@ +namespace SVGImage.SVG.Shapes +{ + public enum LengthAdjustment + { + None, + /// + /// Indicates that only the advance values are adjusted. The glyphs themselves are not stretched or compressed. + /// + Spacing, + /// + /// Indicates that the advance values are adjusted and the glyphs themselves stretched or compressed in one axis (i.e., a direction parallel to the inline-base direction). + /// + SpacingAndGlyphs + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/LengthContext.cs b/Source/SVGImage/SVG/Shapes/LengthContext.cs new file mode 100644 index 0000000..e478af2 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/LengthContext.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +namespace SVGImage.SVG.Shapes +{ + public class LengthContext + { + public Shape Owner { get; set; } + public LengthUnit Unit { get; set; } + private static readonly Dictionary _unitMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + {"em", LengthUnit.em}, + {"ex", LengthUnit.ex}, + {"ch", LengthUnit.ch}, + {"rem", LengthUnit.rem}, + {"vw", LengthUnit.vw}, + {"vh", LengthUnit.vh}, + {"vmin", LengthUnit.vmin}, + {"vmax", LengthUnit.vmax}, + {"cm", LengthUnit.cm}, + {"mm", LengthUnit.mm}, + {"Q", LengthUnit.Q}, + {"in", LengthUnit.Inches}, + {"pc", LengthUnit.pc}, + {"pt", LengthUnit.pt}, + {"px", LengthUnit.px}, + }; + + public LengthContext(Shape owner, LengthUnit unit) + { + Owner = owner; + Unit = unit; + } + + public static LengthUnit Parse(string text, LengthOrientation orientation = LengthOrientation.None) + { + if (String.IsNullOrEmpty(text)) + { + return LengthUnit.Number; + } + string trimmed = text.Trim(); + if(trimmed == "%") + { + switch (orientation) + { + case LengthOrientation.Horizontal: + return LengthUnit.PercentWidth; + case LengthOrientation.Vertical: + return LengthUnit.PercentHeight; + default: + return LengthUnit.PercentDiagonal; + } + } + if(_unitMap.TryGetValue(trimmed, out LengthUnit unit)) + { + return unit; + } + return LengthUnit.Unknown; + } + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/LengthOrientation.cs b/Source/SVGImage/SVG/Shapes/LengthOrientation.cs new file mode 100644 index 0000000..351cbb2 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/LengthOrientation.cs @@ -0,0 +1,13 @@ +namespace SVGImage.SVG.Shapes +{ + public enum LengthOrientation + { + None, + Horizontal, + Vertical, + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs new file mode 100644 index 0000000..4b9ca9b --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs @@ -0,0 +1,194 @@ +using System; + +namespace SVGImage.SVG.Shapes +{ + using System.Text.RegularExpressions; + + public struct LengthPercentageOrNumber + { + private static readonly Regex _lengthRegex = new Regex(@"(?\d+(?:\.\d+)?)\s*(?%|\w+)?", RegexOptions.Compiled | RegexOptions.Singleline); + private readonly LengthContext _context; + private readonly double _value; + /// + /// Represents a length, percentage, or number value that has been resolved based on the context. + /// + public double Value => ResolveValue(); + + + private static double ResolveAbsoluteValue(double value, LengthContext context) + { + switch (context.Unit) + { + case LengthUnit.cm: + return value * 35.43; + case LengthUnit.mm: + return value * 3.54; + case LengthUnit.Q: + return value * 3.54 / 4d; + case LengthUnit.Inches: + return value * 90d; + case LengthUnit.pc: + return value * 15d; + case LengthUnit.pt: + return value * 1.25; + case LengthUnit.px: + return value * 90d / 96d; + case LengthUnit.Unknown: + case LengthUnit.Number: + default: + return value; + } + } + private static double ResolveViewboxValue(double value, LengthContext context) + { + double height; + double width; + if (context.Owner.Svg.ViewBox.HasValue) + { + height = context.Owner.Svg.ViewBox.Value.Height; + width = context.Owner.Svg.ViewBox.Value.Width; + } + else + { + height = context.Owner.Svg.Size.Height; + width = context.Owner.Svg.Size.Width; + } + switch (context.Unit) + { + case LengthUnit.Percent: + throw new NotSupportedException($"Percent without specific orientation is not supported. Use ${LengthUnit.PercentWidth}, ${LengthUnit.PercentHeight}, or ${LengthUnit.PercentDiagonal} instead."); + case LengthUnit.PercentDiagonal: + return (value / 100d) * Math.Sqrt(Math.Pow(width, 2d) + Math.Pow(height, 2d)); + case LengthUnit.vw: + case LengthUnit.PercentWidth: + return (value / 100d) * width; + case LengthUnit.vh: + case LengthUnit.PercentHeight: + return (value / 100d) * height; + case LengthUnit.vmin: + return (value / 100d) * Math.Min(width, height); + case LengthUnit.vmax: + return (value / 100d) * Math.Max(width, height); + case LengthUnit.Unknown: + case LengthUnit.Number: + default: + return value; + } + } + private static double ResolveRelativeValue(double value, LengthContext context) + { + switch (context.Unit) + { + case LengthUnit.em: + return value * context.Owner.TextStyle.FontSize; + case LengthUnit.ex: + return value * context.Owner.TextStyle.GetTypeface().XHeight; + case LengthUnit.ch: + var glyphTypeface = context.Owner.TextStyle.GetGlyphTypeface(); + return value * glyphTypeface.AdvanceWidths[glyphTypeface.CharacterToGlyphMap['0']]; + case LengthUnit.rem: + return value * context.Owner.GetRoot().TextStyle.FontSize; + case LengthUnit.Unknown: + case LengthUnit.Number: + default: + return value; + } + } + private double ResolveValue() + { + if (_context == null) + { + return _value; // No context, return raw value + + } + + switch (_context.Unit) + { + case LengthUnit.Percent: + case LengthUnit.PercentWidth: + case LengthUnit.PercentHeight: + case LengthUnit.PercentDiagonal: + case LengthUnit.vw: + case LengthUnit.vh: + case LengthUnit.vmin: + case LengthUnit.vmax: + return ResolveViewboxValue(_value, _context); + case LengthUnit.em: + case LengthUnit.ex: + case LengthUnit.ch: + case LengthUnit.rem: + return ResolveRelativeValue(_value, _context); + case LengthUnit.cm: + case LengthUnit.mm: + case LengthUnit.Q: + case LengthUnit.Inches: + case LengthUnit.pc: + case LengthUnit.pt: + case LengthUnit.px: + return ResolveAbsoluteValue(_value, _context); + case LengthUnit.Unknown: + case LengthUnit.Number: + default: + return _value; + } + } + /// + /// Represents a length that may have a value that is context dependent. + /// + /// The numerical part of the length that is related to the + /// If , units will be ignored + public LengthPercentageOrNumber(double value, LengthContext context) + { + _context = context; + _value = value; + } + /// + /// Parses a string representation of a length, percentage, or number value into a instance. + /// + /// + /// The element that the length is associated with. + /// + /// A string representation of a length + /// Used to establish the context of the length. + /// Should be for inherntly horizontal values like 'x' and 'dx'. + /// Should be for inherntly vertical values like 'y' and 'dy'. + /// Should be for other values. + /// + /// + /// A instance that represents the parsed value. + /// + /// + /// Thrown when the provided value is not a valid length, percentage, or number. + /// + public static LengthPercentageOrNumber Parse(Shape owner, string value, LengthOrientation orientation = LengthOrientation.None) + { + var lengthMatch = _lengthRegex.Match(value.Trim()); + if(!lengthMatch.Success || !Double.TryParse(lengthMatch.Groups["Value"].Value, out double d)) + { + throw new ArgumentException($"Invalid length/percentage/number value: {value}"); + } + LengthContext context; + if (lengthMatch.Groups["Unit"].Success) + { + string unitStr = lengthMatch.Groups["Unit"].Value; + LengthUnit unit = LengthContext.Parse(unitStr, orientation); + if (unit == LengthUnit.Unknown) + { + throw new ArgumentException($"Unknown length unit: {unitStr}"); + } + context = new LengthContext(owner, unit); + } + else + { + // Default to pixels if no unit is specified + context = new LengthContext(owner, LengthUnit.px); + } + return new LengthPercentageOrNumber(d, context); + } + + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs new file mode 100644 index 0000000..0bddebf --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; + +namespace SVGImage.SVG.Shapes +{ + using System.Linq; + using System.Text.RegularExpressions; + using System.Collections; + + public class LengthPercentageOrNumberList : IList + { + private readonly Shape _owner; + private List _list = new List(); + private readonly LengthOrientation _orientation; + private static readonly Regex _splitRegex = new Regex(@"\b(?:,|\s*,?\s+)\b", RegexOptions.Compiled); + private LengthPercentageOrNumberList(Shape owner, LengthOrientation orientation = LengthOrientation.None) + { + _owner = owner; + _orientation = orientation; + } + /// + /// Creates a new instance of LengthPercentageOrNumberList with the specified owner and orientation. + /// + /// + /// The element that the length list is associated with. + /// + /// A string representation of a length list + /// Used to establish the context of the lengths. + /// Should be for inherntly horizontal values like 'x' and 'dx'. + /// Should be for inherntly vertical values like 'y' and 'dy'. + /// Should be for other values. + /// + public LengthPercentageOrNumberList(Shape owner, string value, LengthOrientation orientation = LengthOrientation.None) : this(owner, orientation) + { + Parse(value); + } + private void Parse(string value) + { + string[] list = _splitRegex.Split(value.Trim()); + + if (list.Any(string.IsNullOrEmpty)) + { + throw new ArgumentException("Invalid length/percentage/number list: " + value); + } + _list = list.Select(s=>LengthPercentageOrNumber.Parse(_owner, s, _orientation)).ToList(); + } + + public static LengthPercentageOrNumberList Empty(Shape owner, LengthOrientation orientation = LengthOrientation.None) + { + return new LengthPercentageOrNumberList(owner, orientation); + } + + + public LengthPercentageOrNumber this[int index] { get => ((IList)_list)[index]; set => ((IList)_list)[index] = value; } + + public int Count => ((ICollection)_list).Count; + + public bool IsReadOnly => ((ICollection)_list).IsReadOnly; + + public void Add(LengthPercentageOrNumber item) + { + //Remove units because Child elements do not inherit the relative values as specified for their parent; they inherit the computed values. + var strippedContext = new LengthPercentageOrNumber(item.Value, new LengthContext(_owner, LengthUnit.Number)); + ((ICollection)_list).Add(strippedContext); + } + + public void Clear() + { + ((ICollection)_list).Clear(); + } + + public bool Contains(LengthPercentageOrNumber item) + { + return ((ICollection)_list).Contains(item); + } + + public void CopyTo(LengthPercentageOrNumber[] array, int arrayIndex) + { + ((ICollection)_list).CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)_list).GetEnumerator(); + } + + public int IndexOf(LengthPercentageOrNumber item) + { + return ((IList)_list).IndexOf(item); + } + + public void Insert(int index, LengthPercentageOrNumber item) + { + //Remove units because Child elements do not inherit the relative values as specified for their parent; they inherit the computed values. + var strippedContext = new LengthPercentageOrNumber(item.Value, new LengthContext(_owner, LengthUnit.Number)); + ((ICollection)_list).Add(strippedContext); + ((IList)_list).Insert(index, strippedContext); + } + + public bool Remove(LengthPercentageOrNumber item) + { + return ((ICollection)_list).Remove(item); + } + + public void RemoveAt(int index) + { + ((IList)_list).RemoveAt(index); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_list).GetEnumerator(); + } + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/LengthUnit.cs b/Source/SVGImage/SVG/Shapes/LengthUnit.cs new file mode 100644 index 0000000..c1e1c7b --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/LengthUnit.cs @@ -0,0 +1,123 @@ +namespace SVGImage.SVG.Shapes +{ + /// + /// + /// + /// + /// Child elements do not inherit the relative values as specified for their parent; they inherit the computed values. + /// Values from to are 1 less than as defined in the SVG 1.1 SVG_LENGTHTYPE specification. This is so can be used as the default. + /// + public enum LengthUnit + { + /// + /// A unit was detected but not recgonized. + /// + /// + Unknown = -1, + /// + /// No unit specified, interpreted as a value in pixels. + /// + /// + Number, + + /// + /// Percent is relative to least distant viewbox dimensions. + /// If the length is inherently horizontal, like "dx", then the percentage is relative to the least distant viewbox width. + /// If the length is inherently vertical, like "dy", then the percentage is relative to the least distant viewbox height. + /// Otherwise the percentage is relative to the least distant viewbox diagonal. + /// + /// + /// Setting a unit of may be converted into , , or + /// + Percent, + + /// + /// Relative to font size of the element + /// + em, + + /// + /// Relative to x-height of the element’s font + /// + ex, + + /// + /// pixels 1px = 1/96th of 1in + /// + px, + /// + /// centimeters 1cm = 96px/2.54 + /// + cm, + + /// + /// millimeters 1mm = 1/10th of 1cm + /// + mm, + + /// + /// inches 1in = 2.54cm = 96px + /// + Inches, + + /// + /// points 1pt = 1/72nd of 1in + /// + pt, + + /// + /// picas 1pc = 1/6th of 1in + /// + pc, + + /// + /// quarter-millimeters 1Q = 1/40th of 1cm + /// + Q, + + /// + /// Relative to character advance of the “0” (ZERO, U+0030) glyph in the element’s font + /// + ch, + + /// + /// Relative to font size of the root element + /// + rem, + + /// + /// Relative to 1% of viewport’s width + /// + vw, + + /// + /// Relative to 1% of viewport’s height + /// + vh, + + /// + /// Relative to 1% of viewport’s smaller dimension + /// + vmin, + + /// + /// Relative to 1% of viewport’s larger dimension + /// + vmax, + + /// + /// Percentage is relative to the least distant viewbox width. + /// + PercentWidth, + + /// + /// Percentage is relative to the least distant viewbox height. + /// + PercentHeight, + + /// + /// Percentage is relative to the least distant viewbox diagonal + /// + PercentDiagonal, + } +} diff --git a/Source/SVGImage/SVG/Shapes/PathShape.cs b/Source/SVGImage/SVG/Shapes/PathShape.cs index 33ca7c6..35b58e5 100644 --- a/Source/SVGImage/SVG/Shapes/PathShape.cs +++ b/Source/SVGImage/SVG/Shapes/PathShape.cs @@ -7,30 +7,17 @@ namespace SVGImage.SVG public sealed class PathShape : Shape { - static Fill DefaultFill = null; // http://apike.ca/prog_svg_paths.html public PathShape(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) { - if (DefaultFill == null) - { - DefaultFill = Fill.CreateDefault(svg, "black"); - } - this.ClosePath = false; string path = XmlUtil.AttrValue(node, "d", string.Empty); this.Data = path; } - - public override Fill Fill + protected override Fill DefaultFill() { - get - { - Fill f = base.Fill; - if (f == null) - f = DefaultFill; - return f; - } + return Fill.CreateDefault(Svg, "black"); } public bool ClosePath { get; private set; } diff --git a/Source/SVGImage/SVG/Shapes/PolygonShape.cs b/Source/SVGImage/SVG/Shapes/PolygonShape.cs index 0e82bb5..777924f 100644 --- a/Source/SVGImage/SVG/Shapes/PolygonShape.cs +++ b/Source/SVGImage/SVG/Shapes/PolygonShape.cs @@ -8,15 +8,9 @@ namespace SVGImage.SVG.Shapes public sealed class PolygonShape : Shape { - private static Fill DefaultFill = null; - public PolygonShape(SVG svg, XmlNode node) : base(svg, node) { - if (DefaultFill == null) - { - DefaultFill = Fill.CreateDefault(svg, "black"); - } string points = XmlUtil.AttrValue(node, SVGTags.sPoints, string.Empty); var split = new StringSplitter(points); @@ -30,14 +24,9 @@ public PolygonShape(SVG svg, XmlNode node) public Point[] Points { get; private set; } - public override Fill Fill + protected override Fill DefaultFill() { - get - { - Fill f = base.Fill; - if (f == null) f = DefaultFill; - return f; - } + return Fill.CreateDefault(Svg, "black"); } } } diff --git a/Source/SVGImage/SVG/Shapes/RectangleShape.cs b/Source/SVGImage/SVG/Shapes/RectangleShape.cs index 2144443..a360fe3 100644 --- a/Source/SVGImage/SVG/Shapes/RectangleShape.cs +++ b/Source/SVGImage/SVG/Shapes/RectangleShape.cs @@ -6,24 +6,14 @@ namespace SVGImage.SVG.Shapes public sealed class RectangleShape : Shape { - private static Fill DefaultFill = null; public RectangleShape(SVG svg, XmlNode node) : base(svg, node) { - if (DefaultFill == null) - { - DefaultFill = Fill.CreateDefault(svg, "black"); - } } - public override Fill Fill + protected override Fill DefaultFill() { - get - { - Fill f = base.Fill; - if (f == null) f = DefaultFill; - return f; - } + return Fill.CreateDefault(Svg, "black"); } public double X { get; set; } diff --git a/Source/SVGImage/SVG/Shapes/Shape.cs b/Source/SVGImage/SVG/Shapes/Shape.cs index 9bc707f..7d45c29 100644 --- a/Source/SVGImage/SVG/Shapes/Shape.cs +++ b/Source/SVGImage/SVG/Shapes/Shape.cs @@ -7,10 +7,19 @@ namespace SVGImage.SVG.Shapes { using Utils; - using Filters; - + using Filters; + using System.Linq; + using System.Text.RegularExpressions; + using System.Windows.Shapes; + using System.Windows.Markup; + using System.Diagnostics; + using System.Text; + using System.Collections; + using System.Reflection; + public class Shape : ClipArtElement { + private static readonly Regex _whiteSpaceRegex = new Regex(@"\s+"); protected Fill m_fill; protected Stroke m_stroke; @@ -22,8 +31,10 @@ public class Shape : ClipArtElement internal Clip m_clip = null; internal Geometry geometryElement; + private readonly SVG _svg; + public SVG Svg => _svg; - public bool Display = true; + public bool Display { get; private set; } = true; //Used during render internal Shape RealParent; @@ -33,27 +44,45 @@ public Shape(SVG svg, XmlNode node) : this(svg, node, null) { } + public Shape GetRoot() + { + Shape root = this; + while (root.Parent != null) + { + root = root.Parent; + } + return root; + } + + public Shape(SVG svg, XmlNode node, Shape parent) : base(node) { + _svg = svg; this.Opacity = 1; this.Parent = parent; this.ParseAtStart(svg, node); if (node != null) { - foreach (XmlAttribute attr in node.Attributes) - this.Parse(svg, attr.Name, attr.Value); + _ = GetTextStyle(svg); // Ensure TextStyle is initialized + foreach (XmlAttribute attr in node.Attributes) + { + this.Parse(svg, attr.Name, attr.Value); + } } ParseLocalStyle(svg); } public Shape(SVG svg, List attrs, Shape parent) : base(null) { + _svg = svg; this.Opacity = 1; this.Parent = parent; if (attrs != null) { - foreach (StyleItem attr in attrs) - this.Parse(svg, attr.Name, attr.Value); + foreach (StyleItem attr in attrs) + { + this.Parse(svg, attr.Name, attr.Value); + } } ParseLocalStyle(svg); } @@ -74,53 +103,65 @@ internal Clip Clip public Visibility Visibility { get; set; } - public virtual Stroke Stroke - { - get - { - if (this.m_stroke != null) return this.m_stroke; - var parent = this.Parent; - while (parent != null) - { - if (this.Parent.Stroke != null) return parent.Stroke; - parent = parent.Parent; - } - return null; - } - set - { - m_stroke = value; - } + protected virtual Stroke DefaultStroke() + { + var parent = this.Parent; + while (parent != null) + { + if (parent.Stroke != null) + { + return parent.Stroke; + } + + parent = parent.Parent; + } + return null; + } + + public Stroke Stroke + { + get => m_stroke ?? DefaultStroke(); + set => m_stroke = value; } - public virtual Fill Fill - { - get - { - if (this.m_fill != null) return this.m_fill; - var parent = this.Parent; - while (parent != null) - { - if (parent.Fill != null) return parent.Fill; - parent = parent.Parent; - } - return null; - } - set - { - m_fill = value; - } + protected virtual Fill DefaultFill() + { + var parent = this.Parent; + while (parent != null) + { + if (parent.Fill != null) + { + return parent.Fill; + } + + parent = parent.Parent; + } + return null; + } + + public Fill Fill + { + get => m_fill ?? DefaultFill(); + set => m_fill = value; } public virtual TextStyle TextStyle { get { - if (this.m_textstyle != null) return this.m_textstyle; + if (this.m_textstyle != null) + { + return this.m_textstyle; + } + var parent = this.Parent; while (parent != null) { - if (parent.m_textstyle != null) return parent.m_textstyle; + if (parent.m_textstyle != null) + { + return parent.m_textstyle; + } + parent = parent.Parent; } return null; @@ -144,9 +185,11 @@ protected virtual void ParseAtStart(SVG svg, XmlNode node) if (node != null) { var name = node.Name; - if (name.Contains(":")) - name = name.Split(':')[1]; - + if (name.Contains(':')) + { + name = name.Split(':')[1]; + } + if (svg.m_styles.TryGetValue(name, out var attributes)) { foreach (var xmlAttribute in attributes) @@ -155,14 +198,11 @@ protected virtual void ParseAtStart(SVG svg, XmlNode node) } } - if (!string.IsNullOrEmpty(this.Id)) - { - if (svg.m_styles.TryGetValue("#" + this.Id, out attributes)) - { - foreach (var xmlAttribute in attributes) - { - Parse(svg, xmlAttribute.Name, xmlAttribute.Value); - } + if (!string.IsNullOrEmpty(this.Id) && svg.m_styles.TryGetValue("#" + this.Id, out attributes)) + { + foreach (var xmlAttribute in attributes) + { + Parse(svg, xmlAttribute.Name, xmlAttribute.Value); } } } @@ -170,16 +210,22 @@ protected virtual void ParseAtStart(SVG svg, XmlNode node) protected virtual void ParseLocalStyle(SVG svg) { - if (!string.IsNullOrEmpty(this.m_localStyle)) - foreach (StyleItem item in StyleParser.SplitStyle(svg, this.m_localStyle)) - this.Parse(svg, item.Name, item.Value); + if (!string.IsNullOrEmpty(this.m_localStyle)) + { + foreach (StyleItem item in StyleParser.SplitStyle(svg, this.m_localStyle)) + { + this.Parse(svg, item.Name, item.Value); + } + } } protected virtual void Parse(SVG svg, string name, string value) { - if (name.Contains(":")) - name = name.Split(':')[1]; - + if (name.Contains(':')) + { + name = name.Split(':')[1]; + } + if (name == SVGTags.sDisplay && value == "none") { this.Display = false; @@ -253,13 +299,19 @@ protected virtual void Parse(SVG svg, string name, string value) if (name == SVGTags.sStrokeLinecap) { if (Enum.TryParse(value, true, out var parsed)) - this.GetStroke(svg).LineCap = parsed; + { + this.GetStroke(svg).LineCap = parsed; + } + return; } if (name == SVGTags.sStrokeLinejoin) { if (Enum.TryParse(value, true, out var parsed)) - this.GetStroke(svg).LineJoin = parsed; + { + this.GetStroke(svg).LineJoin = parsed; + } + return; } if (name == SVGTags.sFilterProperty) @@ -268,7 +320,11 @@ protected virtual void Parse(SVG svg, string name, string value) { Shape result; string id = ShapeUtil.ExtractBetween(value, '(', ')'); - if (id.Length > 0 && id[0] == '#') id = id.Substring(1); + if (id.Length > 0 && id[0] == '#') + { + id = id.Substring(1); + } + svg.m_shapes.TryGetValue(id, out result); this.Filter = result as Filter; return; @@ -281,7 +337,11 @@ protected virtual void Parse(SVG svg, string name, string value) { Shape result; string id = ShapeUtil.ExtractBetween(value, '(', ')'); - if (id.Length > 0 && id[0] == '#') id = id.Substring(1); + if (id.Length > 0 && id[0] == '#') + { + id = id.Substring(1); + } + svg.m_shapes.TryGetValue(id, out result); this.m_clip = result as Clip; return; @@ -306,7 +366,10 @@ protected virtual void Parse(SVG svg, string name, string value) if (name == SVGTags.sFillRule) { if (Enum.TryParse(value, true, out var parsed)) - this.GetFill(svg).FillRule = parsed; + { + this.GetFill(svg).FillRule = parsed; + } + return; } if (name == SVGTags.sOpacity) @@ -341,21 +404,58 @@ protected virtual void Parse(SVG svg, string name, string value) } if (name == SVGTags.sTextDecoration) { - TextDecoration t = new TextDecoration(); - if (value == "none") return; - if (value == "underline") t.Location = TextDecorationLocation.Underline; - if (value == "overline") t.Location = TextDecorationLocation.OverLine; - if (value == "line-through") t.Location = TextDecorationLocation.Strikethrough; - TextDecorationCollection tt = new TextDecorationCollection(); - tt.Add(t); - this.GetTextStyle(svg).TextDecoration = tt; + if (value == "none") + { + return; + } + var textDecorations = _whiteSpaceRegex.Split(value); + TextDecorationCollection tt = new TextDecorationCollection(); + if (textDecorations.Length == 0 || textDecorations.Any(td => td.Equals("none", StringComparison.OrdinalIgnoreCase))) + { + // If "none" is explicitly set, set TextDecoration to empty collection. + // This distinguishes it from not setting TextDecoration at all, in which case it defaults to inherit. + this.GetTextStyle(svg).TextDecoration = tt; + return; + } + foreach (var textDecoration in textDecorations) + { + TextDecoration t = new TextDecoration(); + if (value == "underline") + { + t.Location = TextDecorationLocation.Underline; + } + else if (value == "overline") + { + t.Location = TextDecorationLocation.OverLine; + } + else if (value == "line-through") + { + t.Location = TextDecorationLocation.Strikethrough; + } + tt.Add(t); + } + + this.GetTextStyle(svg).TextDecoration = tt; + return; } if (name == SVGTags.sTextAnchor) { - if (value == "start") this.GetTextStyle(svg).TextAlignment = TextAlignment.Left; - if (value == "middle") this.GetTextStyle(svg).TextAlignment = TextAlignment.Center; - if (value == "end") this.GetTextStyle(svg).TextAlignment = TextAlignment.Right; + if (value == "start") + { + this.GetTextStyle(svg).TextAlignment = TextAlignment.Left; + } + + if (value == "middle") + { + this.GetTextStyle(svg).TextAlignment = TextAlignment.Center; + } + + if (value == "end") + { + this.GetTextStyle(svg).TextAlignment = TextAlignment.Right; + } + return; } if (name == "word-spacing") @@ -380,19 +480,31 @@ protected virtual void Parse(SVG svg, string name, string value) private Stroke GetStroke(SVG svg) { - if (this.m_stroke == null) this.m_stroke = new Stroke(svg); + if (this.m_stroke == null) + { + this.m_stroke = new Stroke(svg); + } + return this.m_stroke; } protected Fill GetFill(SVG svg) { - if (this.m_fill == null) this.m_fill = new Fill(svg); + if (this.m_fill == null) + { + this.m_fill = new Fill(svg); + } + return this.m_fill; } protected TextStyle GetTextStyle(SVG svg) { - if (this.m_textstyle == null) this.m_textstyle = new TextStyle(svg, this); + if (this.m_textstyle == null) + { + this.m_textstyle = new TextStyle( this); + } + return this.m_textstyle; } @@ -400,5 +512,9 @@ public override string ToString() { return this.GetType().Name + " (" + (Id ?? "") + ")"; } - } + } + + + + } diff --git a/Source/SVGImage/SVG/Shapes/Text.cs b/Source/SVGImage/SVG/Shapes/Text.cs index 1a3a7c8..4ba2e0a 100644 --- a/Source/SVGImage/SVG/Shapes/Text.cs +++ b/Source/SVGImage/SVG/Shapes/Text.cs @@ -6,13 +6,14 @@ namespace SVGImage.SVG { using Utils; using Shapes; + using System.Linq; - public sealed class TextShape : Shape + /// + /// The original TextShape class. + /// + public sealed class TextShape2 : Shape { - private static Fill DefaultFill = null; - private static Stroke DefaultStroke = null; - - public TextShape(SVG svg, XmlNode node, Shape parent) + public TextShape2(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) { this.X = XmlUtil.AttrValue(node, "x", 0); @@ -22,48 +23,27 @@ public TextShape(SVG svg, XmlNode node, Shape parent) // check for tSpan tag if (node.InnerXml.IndexOf("<") >= 0) this.TextSpan = this.ParseTSpan(svg, node.InnerXml); - if (DefaultFill == null) - { - DefaultFill = Fill.CreateDefault(svg, "black"); - } - if (DefaultStroke == null) - { - DefaultStroke = Stroke.CreateDefault(svg, 0.1); - } } public double X { get; set; } public double Y { get; set; } public string Text { get; set; } - public TextSpan TextSpan {get; private set;} + public TextSpan2 TextSpan { get; private set; } - public override Fill Fill - { - get - { - Fill f = base.Fill; - if (f == null) - f = DefaultFill; - return f; - } + protected override Fill DefaultFill() + { + return Fill.CreateDefault(Svg, "black"); } - - public override Stroke Stroke - { - get - { - Stroke f = base.Stroke; - if (f == null) - f = DefaultStroke; - return f; - } + protected override Stroke DefaultStroke() + { + return Stroke.CreateDefault(Svg, 0.1); } - TextSpan ParseTSpan(SVG svg, string tspanText) + TextSpan2 ParseTSpan(SVG svg, string tspanText) { try { - return TextSpan.Parse(svg, tspanText, this); + return TextSpan2.Parse(svg, tspanText, this); } catch { @@ -71,8 +51,7 @@ TextSpan ParseTSpan(SVG svg, string tspanText) } } } - - public class TextSpan : Shape + public class TextSpan2 : Shape { public enum eElementType { @@ -80,39 +59,39 @@ public enum eElementType Text, } - public override System.Windows.Media.Transform Transform - { - get {return this.Parent.Transform; } + public override System.Windows.Media.Transform Transform + { + get { return this.Parent.Transform; } } - public eElementType ElementType {get; private set;} - public List Attributes {get; set;} - public List Children {get; private set;} - public int StartIndex {get; set;} - public string Text {get; set;} - public TextSpan End {get; set;} - public TextSpan(SVG svg, Shape parent, string text) : base(svg, (XmlNode)null, parent) + public eElementType ElementType { get; private set; } + public List Attributes { get; set; } + public List Children { get; private set; } + public int StartIndex { get; set; } + public string Text { get; set; } + public TextSpan2 End { get; set; } + public TextSpan2(SVG svg, Shape parent, string text) : base(svg, (XmlNode)null, parent) { this.ElementType = eElementType.Text; this.Text = text; } - public TextSpan(SVG svg, Shape parent, eElementType eType, List attrs) + public TextSpan2(SVG svg, Shape parent, eElementType eType, List attrs) : base(svg, attrs, parent) { this.ElementType = eType; this.Text = string.Empty; - this.Children = new List(); + this.Children = new List(); } public override string ToString() { return this.Text; } - static TextSpan NextTag(SVG svg, TextSpan parent, string text, ref int curPos) + static TextSpan2 NextTag(SVG svg, TextSpan2 parent, string text, ref int curPos) { int start = text.IndexOf("<", curPos); if (start < 0) return null; - int end = text.IndexOf(">", start+1); + int end = text.IndexOf(">", start + 1); if (end < 0) throw new Exception("Start '<' with no end '>'"); @@ -127,11 +106,11 @@ static TextSpan NextTag(SVG svg, TextSpan parent, string text, ref int curPos) if (attrstart > 0) { attrstart += 5; - while (attrstart < tagtext.Length-1) + while (attrstart < tagtext.Length - 1) attrs.Add(StyleItem.ReadNextAttr(tagtext, ref attrstart)); } - - TextSpan tag = new TextSpan(svg, parent, eElementType.Tag, attrs); + + TextSpan2 tag = new TextSpan2(svg, parent, eElementType.Tag, attrs); tag.StartIndex = start; tag.Text = text.Substring(start, end - start); if (tag.Text.IndexOf("<", 1) > 0) @@ -141,28 +120,28 @@ static TextSpan NextTag(SVG svg, TextSpan parent, string text, ref int curPos) return tag; } - static TextSpan Parse(SVG svg, string text, ref int curPos, TextSpan parent, TextSpan curTag) + static TextSpan2 Parse(SVG svg, string text, ref int curPos, TextSpan2 parent, TextSpan2 curTag) { - TextSpan tag = curTag; + TextSpan2 tag = curTag; if (tag == null) tag = NextTag(svg, parent, text, ref curPos); while (curPos < text.Length) { int prevPos = curPos; - TextSpan next = NextTag(svg, tag, text, ref curPos); + TextSpan2 next = NextTag(svg, tag, text, ref curPos); if (next == null && curPos < text.Length) { // remaining pure text string s = text.Substring(curPos, text.Length - curPos); - tag.Children.Add(new TextSpan(svg, tag, s)); + tag.Children.Add(new TextSpan2(svg, tag, s)); return tag; } - if (next != null && next.StartIndex-prevPos > 0) + if (next != null && next.StartIndex - prevPos > 0) { // pure text between tspan elements - int diff = next.StartIndex-prevPos; + int diff = next.StartIndex - prevPos; string s = text.Substring(prevPos, diff); - tag.Children.Add(new TextSpan(svg, tag, s)); + tag.Children.Add(new TextSpan2(svg, tag, s)); } if (next.Text.StartsWith(" + /// This is an unfinished scaffold for resolving the 'textLength' attribute in SVG text elements. + /// I was using the SVG 2.0 specification as a reference, but it is not complete, and it is confusing. + /// + internal class TextLengthResolver + { + private readonly CharacterLayout[] _result; + private readonly bool _horizontal; + private readonly double[] _resolveDx; + private readonly double[] _resolveDy; + + public TextLengthResolver(CharacterLayout[] result, bool horizontal, double[] resolveDx, double[] resolveDy) + { + _result = result; + _horizontal = horizontal; + _resolveDx = resolveDx; + _resolveDy = resolveDy; + } + + + /// + /// Define resolved descendant node as a descendant of node with a valid ‘textLength’ attribute that is not itself a descendant node of a descendant node that has a valid ‘textLength’ attribute + /// + /// + /// + /// + private bool IsResolvedDescendantNode(ITextNode textNode, ITextNode descendant) + { + if (textNode is TextShapeBase textShape) + { + if (textShape.TextLength != null && !textShape.Children.Any(c => c is TextShapeBase child && child.TextLength != null)) + { + return true; + } + foreach (var child in textShape.Children) + { + if (IsResolvedDescendantNode(child, descendant)) + { + return true; + } + } + } + return false; + } + public void ResolveTextLength(ITextNode textNode, string text, ref bool in_text_path) + { + if (textNode is TextShapeBase textNodeBase) + { + foreach (var child in textNodeBase.Children) + { + ResolveTextLength(child, text, ref in_text_path); + } + } + if (textNode is TextSpan textSpan && textSpan.TextLength != null && !IsResolvedDescendantNode(textNode, textSpan)) + { + // Calculate total advance width + double totalAdvance = 0; + for (int i = 0; i < _result.Length; i++) + { + if (_result[i].Addressable) + { + totalAdvance += _resolveDx[i]; + } + } + // Calculate scaling factor + double scaleFactor = textSpan.TextLength.Value.Value / totalAdvance; + // Apply scaling to dx and dy + for (int i = 0; i < _result.Length; i++) + { + if (_result[i].Addressable) + { + _resolveDx[i] *= scaleFactor; + _resolveDy[i] *= scaleFactor; + } + } + } + } + + private static double GetAdvance(CharacterLayout characterLayout) + { + throw new NotImplementedException(); + } + + public void ResolveTextSpanTextLength(TextSpan textSpan, string text, ref bool in_text_path) + { + double a = Double.PositiveInfinity; + double b = Double.NegativeInfinity; + int i = textSpan.GetFirstCharacter().GlobalIndex; + int j = textSpan.GetLastCharacter().GlobalIndex; + for (int k = i; k <= j; k++) + { + if (!_result[k].Addressable) + { + continue; + } + if (_result[k].Character == '\r' || _result[k].Character == '\n') + { + //No adjustments due to ‘textLength’ are made to a node with a forced line break. + return; + } + double pos = _horizontal ? _result[k].X : _result[k].Y; + double advance = GetAdvance(_result[k]); //This advance will be negative for RTL horizontal text. + + a = Math.Min(Math.Min(a, pos), pos + advance); + b = Math.Max(Math.Max(b, pos), pos + advance); + + } + if (!Double.IsPositiveInfinity(a)) + { + double delta = textSpan.TextLength.Value.Value - (b - a); + int n = 0;// textSpan.GetTypographicCharacterCount(); + int resolvedDescendantNodes = GetResolvedDescendantNodeCount(textSpan, ref n); + n += resolvedDescendantNodes - 1;//Each resolved descendant node is treated as if it were a single typographic character in this context. + var δ = delta / n; + double shift = 0; + for (int k = i; k <= j; k++) + { + if (_horizontal) + { + _result[k].X += shift; + } + else + { + _result[k].Y += shift; + } + if (!_result[k].Middle && IsNotACharacterInAResolvedDescendantNodeOtherThanTheFirstCharacter(textSpan, _result[k])) + { + shift += δ; + } + } + } + } + + internal int GetResolvedDescendantNodeCount(ITextNode node, ref int n) + { + int resolvedDescendantNodes = 0; + if (node is TextSpan textSpan) + { + n = textSpan.Children.Count; + for (int c = 0; c < textSpan.Children.Count; c++) + { + if (textSpan.Children[c].GetText() is string ccontent) + { + n += String.IsNullOrEmpty(ccontent) ? 0 : ccontent.Length; + } + else + { + _result[n].FirstCharacterInResolvedDescendant = true; + resolvedDescendantNodes++; + } + } + } + else if (node is TextString textString) + { + n = textString.GetLength(); + } + return resolvedDescendantNodes; + } + private static bool IsNotACharacterInAResolvedDescendantNodeOtherThanTheFirstCharacter(ITextNode textNode, CharacterLayout character) + { + throw new NotImplementedException("This method needs to be implemented based on the specific rules for resolved descendant nodes."); + } + + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/TextPath.cs b/Source/SVGImage/SVG/Shapes/TextPath.cs new file mode 100644 index 0000000..e31749f --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/TextPath.cs @@ -0,0 +1,20 @@ +using System; +using System.Xml; + +namespace SVGImage.SVG.Shapes +{ + /// + /// A placeholder class for TextPath. + /// + internal class TextPath : TextShapeBase, ITextChild + { + protected TextPath(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) + { + throw new NotImplementedException("TextPath is not yet implemented."); + } + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/TextRender.cs b/Source/SVGImage/SVG/Shapes/TextRender.cs new file mode 100644 index 0000000..cdd1a12 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/TextRender.cs @@ -0,0 +1,187 @@ +using System.Collections.Generic; +using System.Windows.Media; +using System.Windows; + +namespace SVGImage.SVG.Shapes +{ + using Utils; + using System.Linq; + using System.Windows.Markup; + + /// + /// This class is responsible for rendering text shapes in SVG. + /// + public sealed partial class TextRender : TextRenderBase + { + public override GeometryGroup BuildTextGeometry(TextShape text) + { + + using(TextRenderState state = new TextRenderState()) + { + if (!state.Setup(text)) + { + return null; // No characters to render + } + return CreateGeometry(text, state); + } + } + private static GeometryGroup CreateGeometry(TextShape root, TextRenderState state) + { + state.Resolve(root); + + List textStrings = new List(); + TextStyleStack textStyleStacks = new TextStyleStack(); + PopulateTextStrings(textStrings, root, textStyleStacks); + GeometryGroup mainGeometryGroup = new GeometryGroup(); + var baselineOrigin = new Point(root.X.FirstOrDefault().Value, root.Y.FirstOrDefault().Value); + var isSideways = root.WritingMode == WritingMode.HorizontalTopToBottom; + foreach (TextString textString in textStrings) + { + GeometryGroup geometryGroup = new GeometryGroup(); + var textStyle = textString.TextStyle; + Typeface font = textString.TextStyle.GetTypeface(); + if (CreateRun(textString, state, font, isSideways, baselineOrigin, out Point newBaseline) is GlyphRun run) + { + var runGeometry = run.BuildGeometry(); + geometryGroup.Children.Add(runGeometry); + if (textStyle.TextDecoration != null && textStyle.TextDecoration.Count > 0) + { + GetTextDecorations(geometryGroup, textStyle, font, baselineOrigin, out List backgroundDecorations, out List foregroundDecorations); + foreach (var decoration in backgroundDecorations) + { + //Underline and OverLine should be drawn behind the text + geometryGroup.Children.Insert(0, new RectangleGeometry(decoration)); + } + foreach (var decoration in foregroundDecorations) + { + //Strikethrough should be drawn on top of the text + geometryGroup.Children.Add(new RectangleGeometry(decoration)); + } + } + mainGeometryGroup.Children.Add(geometryGroup); + } + baselineOrigin = newBaseline; + } + + mainGeometryGroup.Transform = root.Transform; + return mainGeometryGroup; + } + + /// + /// + /// + /// + /// Not perfect, the lines are not continuous across multiple text strings. + /// + /// + /// + /// + /// + /// + /// + private static void GetTextDecorations(GeometryGroup geometryGroup, TextStyle textStyle, Typeface font, Point baselineOrigin, out List backgroundDecorations, out List foregroundDecorations) + { + backgroundDecorations = new List(); + foregroundDecorations = new List(); + double decorationPos = 0; + double decorationThickness = 0; + double fontSize = textStyle.FontSize; + double baselineY = baselineOrigin.Y; + foreach(TextDecorationLocation textDecorationLocation in textStyle.TextDecoration.Select(td=>td.Location)) + { + if (textDecorationLocation == TextDecorationLocation.Strikethrough) + { + decorationPos = baselineY - (font.StrikethroughPosition * fontSize); + decorationThickness = font.StrikethroughThickness * fontSize; + Rect bounds = new Rect(geometryGroup.Bounds.Left, decorationPos, geometryGroup.Bounds.Width, decorationThickness); + foregroundDecorations.Add(bounds); + } + else if (textDecorationLocation == TextDecorationLocation.Underline) + { + decorationPos = baselineY - (font.UnderlinePosition * fontSize); + decorationThickness = font.UnderlineThickness * fontSize; + Rect bounds = new Rect(geometryGroup.Bounds.Left, decorationPos, geometryGroup.Bounds.Width, decorationThickness); + backgroundDecorations.Add(bounds); + } + else if (textDecorationLocation == TextDecorationLocation.OverLine) + { + decorationPos = baselineY - fontSize; + decorationThickness = font.StrikethroughThickness * fontSize; + Rect bounds = new Rect(geometryGroup.Bounds.Left, decorationPos, geometryGroup.Bounds.Width, decorationThickness); + backgroundDecorations.Add(bounds); + } + } + } + + private static GlyphRun CreateRun(TextString textString, TextRenderState state, Typeface font, bool isSideways, Point baselineOrigin, out Point newBaseline) + { + var textStyle = textString.TextStyle; + var characterInfos = textString.GetCharacters(); + if(characterInfos is null ||characterInfos.Length == 0) + { + newBaseline = baselineOrigin; + return null; + } + if (!font.TryGetGlyphTypeface(out var glyphFace)) + { + newBaseline = baselineOrigin; + return null; + } + string deviceFontName = null; + IList clusterMap = null; + IList caretStops = null; + XmlLanguage language = null; + var glyphOffsets = characterInfos.Select(c => new Point(c.DX, -c.DY)).ToList(); + var renderingEmSize = textStyle.FontSize; + var characters = characterInfos.Select(c => c.Character).ToArray(); + var glyphIndices = characters.Select(c => glyphFace.CharacterToGlyphMap[c]).ToList(); + var advanceWidths = glyphIndices.Select(c => glyphFace.AdvanceWidths[c] * renderingEmSize).ToArray(); + + + if (characterInfos[0].DoesPositionX) + { + baselineOrigin.X = characterInfos[0].X; + } + if (characterInfos[0].DoesPositionY) + { + baselineOrigin.Y = characterInfos[0].Y; + } + + GlyphRun run = new GlyphRun(glyphFace, state.BidiLevel, isSideways, renderingEmSize, +#if !DOTNET40 && !DOTNET45 && !DOTNET46 + (float)DpiUtil.PixelsPerDip, +#endif + glyphIndices, baselineOrigin, advanceWidths, glyphOffsets, characters, deviceFontName, clusterMap, caretStops, language); + + var newX = baselineOrigin.X + advanceWidths.Sum(); + var newY = baselineOrigin.Y ; + + newBaseline = new Point(newX, newY); + return run; + } + + + + private static void PopulateTextStrings(List textStrings, ITextNode node, TextStyleStack textStyleStacks) + { + if(node is TextShapeBase span) + { + textStyleStacks.Push(span.TextStyle); + foreach (var child in span.Children) + { + PopulateTextStrings(textStrings, child, textStyleStacks); + } + _ = textStyleStacks.Pop(); + } + else if(node is TextString textString) + { + textString.TextStyle = textStyleStacks.Peek(); + textStrings.Add(textString); + } + } + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/TextRenderBase.cs b/Source/SVGImage/SVG/Shapes/TextRenderBase.cs new file mode 100644 index 0000000..ccec0cf --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/TextRenderBase.cs @@ -0,0 +1,24 @@ +using System.Windows.Media; +using System.Windows; + +namespace SVGImage.SVG.Shapes +{ + public abstract class TextRenderBase + { + public abstract GeometryGroup BuildTextGeometry(TextShape text); + public static DependencyProperty TSpanElementProperty = DependencyProperty.RegisterAttached( + "TSpanElement", typeof(ITextNode), typeof(DependencyObject)); + public static void SetElement(DependencyObject obj, ITextNode value) + { + obj.SetValue(TSpanElementProperty, value); + } + public static ITextNode GetElement(DependencyObject obj) + { + return (ITextNode)obj.GetValue(TSpanElementProperty); + } + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/TextRenderState.cs b/Source/SVGImage/SVG/Shapes/TextRenderState.cs new file mode 100644 index 0000000..a4484d3 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/TextRenderState.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace SVGImage.SVG.Shapes +{ + public sealed partial class TextRender + { + /// + /// Represents the state of the text rendering process, including character layout and position resolution. + /// This class is responsible for setting up the text layout, resolving positions and transformations, and applying them to the characters. + /// + private sealed class TextRenderState : IDisposable + { + private bool _disposedValue; + private CharacterLayout[] _characters; + private double[] _resolvedX; + private double[] _resolvedY; + private double[] _resolvedDx; + private double[] _resolvedDy; + private double[] _resolvedRotate; + private int[] _xBaseIndicies; + private int[] _yBaseIndicies; + public int BidiLevel { get; private set; } = 0; + private void ResetState() + { + _characters = null; + _resolvedX = null; + _resolvedY = null; + _resolvedDx = null; + _resolvedDy = null; + _resolvedRotate = null; + _xBaseIndicies = null; + _yBaseIndicies = null; + } + /// + /// Initializes the text render state with the root TextShape. + /// + /// + /// The root TextShape to process. This should contain all the text nodes and their children. + /// + /// + /// Returns true if the setup was successful, false if there were no characters to process. + /// + public bool Setup(TextShape root) + { + string text = root.GetText(); + int globalIndex = 0; + SetGlobalIndicies(root, ref globalIndex); + _characters = root.GetCharacters(); + SetFlagsAndAssignInitialPositions(root, text); + if (_characters.Length == 0) + { + return false; + } + return true; + } + /// + /// Creates an array of a specified length, filled with a specified element. + /// + /// + /// The type of the element to fill the array with. Must be a struct type. + /// + /// + /// The length of the array to create. + /// + /// + /// The element to fill the array with. This should be a value type (struct). + /// + /// + /// Returns an array of type T, filled with the specified element. + /// + private static T[] CreateRepeatedArray(int count, T element) where T : struct + { + var result = new T[count]; + for (int i = 0; i < count; i++) + { + result[i] = element; + } + return result; + } + /// + /// Initializes the arrays used for resolving text positions and transformations. + /// + /// + /// The length of the arrays to initialize. This should match the number of characters in the text. + /// + private void InitializeResolveArrays(int length) + { + _xBaseIndicies = CreateRepeatedArray(length, -1); + _yBaseIndicies = CreateRepeatedArray(length, -1); + if (length > 0) + { + _xBaseIndicies[0] = 0; + _yBaseIndicies[0] = 0; + } + _resolvedX = CreateRepeatedArray(length, double.NaN); + _resolvedY = CreateRepeatedArray(length, double.NaN); + _resolvedDx = CreateRepeatedArray(length, 0d); + _resolvedDy = CreateRepeatedArray(length, 0d); + _resolvedRotate = CreateRepeatedArray(length, double.NaN); + } + + /// + /// Recursively resolves the text span's position and offset values. + /// + /// + /// The TextShapeBase to resolve. This should contain the text span's position and offset values. + /// + public void Resolve(TextShapeBase textSpan) + { + string text = textSpan.GetText(); + InitializeResolveArrays(text.Length); + ResolveInternal(textSpan); + ApplyResolutions(); + } + + /// + /// Recursively resolves the text span's position and offset values. + /// + /// + /// The TextShapeBase to resolve. This should contain the text span's position and offset values. + /// + private void ResolveInternal(TextShapeBase textSpan) + { + int index = textSpan.GetFirstCharacter().GlobalIndex; + LengthPercentageOrNumberList x = textSpan.X; + LengthPercentageOrNumberList y = textSpan.Y; + LengthPercentageOrNumberList dx = textSpan.DX; + LengthPercentageOrNumberList dy = textSpan.DY; + List rotate = textSpan.Rotate; + + var arrays = new List>(); + arrays.Add(new Tuple(x, _resolvedX)); + arrays.Add(new Tuple(y, _resolvedY)); + arrays.Add(new Tuple(dx, _resolvedDx)); + arrays.Add(new Tuple(dy, _resolvedDy)); + + foreach (var tuple in arrays) + { + var list = tuple.Item1; + var resolvedArray = tuple.Item2; + for (int i = 0; i < list.Count; i++) + { + // Check if the index is within bounds of the resolved array + if (index + i >= resolvedArray.Length) + { + break; + } + resolvedArray[index + i] = list[i].Value; + } + } + + for (int i = 0; i < rotate.Count; i++) + { + // Check if the index is within bounds of the resolved array + if (index + i >= _resolvedRotate.Length) + { + break; + } + _resolvedRotate[index + i] = rotate[i]; + } + foreach (var child in textSpan.Children.OfType()) + { + ResolveInternal(child); + } + } + /// + /// Recursively sets the global indices for each character in the text node. + /// + /// + /// The text node to process. This should be a or containing characters. + /// + /// + /// A reference to the global index counter. This will be incremented for each character processed. + /// + private static void SetGlobalIndicies(ITextNode textNode, ref int globalIndex) + { + if (textNode is TextShapeBase textNodeBase) + { + foreach (var child in textNodeBase.Children) + { + SetGlobalIndicies(child, ref globalIndex); + } + } + else if (textNode is TextString textString) + { + foreach (var c in textString.Characters) + { + c.GlobalIndex = globalIndex++; + } + } + } + /// + /// Fills in gaps in the resolved position and rotation arrays. + /// + private void FillInGaps() + { + FillInGaps(_resolvedX, 0d, _xBaseIndicies); + FillInGaps(_resolvedY, 0d, _yBaseIndicies); + FillInGaps(_resolvedRotate, 0d); + } + /// + /// Fills in gaps in the specified list with the initial value or the last known value. + /// + /// + /// The list to fill in gaps for. This should be an array of doubles. + /// + /// + /// The initial value to use for filling gaps. If null, the first value in the list will be used, even if it is NaN. + /// + /// + /// An optional array of base indices to track the last known index for each position in the list. + /// + private static void FillInGaps(double[] list, double? initialValue = null, int[] baseIndicies = null) + { + if (list == null || list.Length == 0) + { + return; + } + if (Double.IsNaN(list[0]) && initialValue.HasValue && !Double.IsNaN(initialValue.Value)) + { + list[0] = initialValue.Value; + } + double current = list[0]; + int currentBaseIndex = 0; + for (int i = 1; i < list.Length; i++) + { + if (Double.IsNaN(list[i])) + { + list[i] = current; + } + else + { + current = list[i]; + currentBaseIndex = i; + } + if (baseIndicies != null) + { + baseIndicies[i] = currentBaseIndex; + } + } + } + /// + /// Applies the resolved positions and transformations to the characters. + /// + private void ApplyResolutions() + { + FillInGaps(); + for (int i = 0; i < _characters.Length; i++) + { + int xBaseIndex = _xBaseIndicies[i]; + int yBaseIndex = _yBaseIndicies[i]; + _characters[i].X = _resolvedX[xBaseIndex]; + _characters[i].Y = _resolvedY[yBaseIndex]; + _characters[i].DX = _resolvedDx.Skip(xBaseIndex).Take(i - xBaseIndex + 1).Sum(); + _characters[i].DY = _resolvedDy.Skip(yBaseIndex).Take(i - yBaseIndex + 1).Sum(); + _characters[i].Rotation = _resolvedRotate[i]; + _characters[i].DoesPositionX = _xBaseIndicies[_characters[i].GlobalIndex] == _characters[i].GlobalIndex; + _characters[i].DoesPositionY = _yBaseIndicies[_characters[i].GlobalIndex] == _characters[i].GlobalIndex; + } + + } + /// + /// Preliminary, Need Implementation + /// + /// + /// + private static bool IsNonRenderedCharacter(char c) + { + // Check for non-rendered characters like zero-width space, etc. + return char.IsControl(c) || char.IsWhiteSpace(c) && c != ' '; + } + + private static bool IsBidiControlCharacter(char c) + { + // Check for Bidi control characters + return c == '\u2066' || c == '\u2067' || c == '\u2068' || c == '\u2069' || + c == '\u200E' || c == '\u200F' || c == '\u202A' || c == '\u202B' || c == '\u202C' || c == '\u202D' || c == '\u202E'; + } + + /// + /// discarded during layout due to being a collapsed white space character, a soft hyphen character, collapsed segment break, or a bidi control character + /// + /// + /// + private static bool WasDiscardedDuringLayout(char c) + { + return IsBidiControlCharacter(c) || + c == '\u00AD' || // Soft hyphen + c == '\u200B' || // Zero-width space + c == '\u200C' || // Zero-width non-joiner + c == '\u200D' || // Zero-width joiner + char.IsWhiteSpace(c) && c != ' '; // Collapsed whitespace characters + } + + private static bool IsAddressable(int index, char c, int beginningCharactersTrimmed, int startOfTrimmedEnd) + { + return !IsNonRenderedCharacter(c) && + !WasDiscardedDuringLayout(c) && + index >= beginningCharactersTrimmed && + index < startOfTrimmedEnd; + } + private static readonly Regex _trimmedWhitespace = new Regex(@"(?^\s*).*(?\s*$)", RegexOptions.Compiled | RegexOptions.Singleline); + private static readonly Regex _lineStarts = new Regex(@"^ *\S", RegexOptions.Compiled | RegexOptions.Multiline); + private static bool IsTypographicCharacter(char c, int index, string text) + { + return !IsNonRenderedCharacter(c); //It's not clear what a typographic character is in this context, so we assume all non-rendered characters are not typographic. + } + private static HashSet GetLineBeginnings(string text) + { + HashSet lineStartIndicies = new HashSet(); + var lineStarts = _lineStarts.Matches(text); + foreach (Match lineStart in lineStarts) + { + lineStartIndicies.Add(lineStart.Index + lineStart.Length - 1); + } + return lineStartIndicies; + } + public void SetFlagsAndAssignInitialPositions(TextShape root, string text) + { + var trimmedText = _trimmedWhitespace.Match(text); + int beginningCharactersTrimmed = trimmedText.Groups["Start"].Success ? trimmedText.Groups["Start"].Length : 0; + int endingCharactersTrimmed = trimmedText.Groups["End"].Success ? trimmedText.Groups["End"].Length : 0; + int startOfTrimmedEnd = text.Length - endingCharactersTrimmed; + var lineBeginnings = GetLineBeginnings(text); + for (int i = 0; i < _characters.Length; i++) + { + var c = _characters[i]; + bool isTypographic = IsTypographicCharacter(text[i], i, text); + c.Addressable = IsAddressable(i, text[i], beginningCharactersTrimmed, startOfTrimmedEnd); + c.Middle = i > 0 && isTypographic; + c.AnchoredChunk = lineBeginnings.Contains(i); + } + } + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + ResetState(); + } + + _disposedValue = true; + } + } + + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } + + } +} diff --git a/Source/SVGImage/SVG/Shapes/TextShape.cs b/Source/SVGImage/SVG/Shapes/TextShape.cs new file mode 100644 index 0000000..da28c7f --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/TextShape.cs @@ -0,0 +1,17 @@ +using System.Xml; + +namespace SVGImage.SVG.Shapes +{ + public class TextShape : TextShapeBase + { + public TextShape(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) + { + + } + + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/TextShapeBase.cs b/Source/SVGImage/SVG/Shapes/TextShapeBase.cs new file mode 100644 index 0000000..00e2053 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/TextShapeBase.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Xml; + +namespace SVGImage.SVG.Shapes +{ + using System.Linq; + using System.Diagnostics; + using System.Text; + + [DebuggerDisplay("{DebugDisplayText}")] + public class TextShapeBase: Shape, ITextNode + { + protected TextShapeBase(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) + { + } + + private string DebugDisplayText => GetDebugDisplayText(new StringBuilder()); + private string GetDebugDisplayText(StringBuilder sb) + { + if (Children.Count == 0) + { + return ""; + } + foreach(var child in Children) + { + if (child is TextString textString) + { + sb.Append(textString.Text); + } + else if (child is TextSpan textSpan) + { + sb.Append('('); + textSpan.GetDebugDisplayText(sb); + sb.Append(')'); + } + } + + return sb.ToString(); + } + + + + public LengthPercentageOrNumberList X { get; protected set; } + public LengthPercentageOrNumberList Y { get; protected set; } + public LengthPercentageOrNumberList DX { get; protected set; } + public LengthPercentageOrNumberList DY { get; protected set; } + public WritingMode WritingMode { get; set; } + public List Attributes { get; set; } = new List(); + public List Rotate { get; protected set; } = new List(); + public LengthPercentageOrNumber? TextLength { get; set; } + public LengthAdjustment LengthAdjust { get; set; } = LengthAdjustment.Spacing; + public List Children { get; } = new List(); + public CharacterLayout FirstCharacter => GetFirstCharacter(); + public CharacterLayout LastCharacter => GetLastCharacter(); + public string Text => GetText(); + public int Length => GetLength(); + + + + public CharacterLayout[] GetCharacters() + { + return Children.SelectMany(c => c.GetCharacters()).ToArray(); + } + + public CharacterLayout GetFirstCharacter() + { + foreach(var child in Children) + { + if (child.GetFirstCharacter() is CharacterLayout firstChar) + { + return firstChar; + } + } + throw new InvalidOperationException("No characters found in text node."); + } + public CharacterLayout GetLastCharacter() + { + for (int i = Children.Count - 1; i >= 0; i--) + { + if (Children[i].GetLastCharacter() is CharacterLayout LastChar) + { + return LastChar; + } + } + throw new InvalidOperationException("No characters found in text node."); + } + + public int GetLength() + { + return Children.Sum(c => c.GetLength()); + } + + public string GetText() + { + return string.Concat(Children.Select(c => c.GetText())); + } + + protected override void ParseAtStart(SVG svg, XmlNode node) + { + base.ParseAtStart(svg, node); + + foreach (XmlAttribute attr in node.Attributes) + { + switch (attr.Name) + { + case "x": + X = new LengthPercentageOrNumberList(this, attr.Value, LengthOrientation.Horizontal); + break; + case "y": + Y = new LengthPercentageOrNumberList(this, attr.Value, LengthOrientation.Vertical); + break; + case "dx": + DX = new LengthPercentageOrNumberList(this, attr.Value, LengthOrientation.Horizontal); + break; + case "dy": + DY = new LengthPercentageOrNumberList(this, attr.Value, LengthOrientation.Vertical); + break; + case "rotate": + Rotate = attr.Value.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(v => double.Parse(v)).ToList(); + break; + case "textLength": + TextLength = LengthPercentageOrNumber.Parse(this, attr.Value); + break; + case "lengthAdjust": + LengthAdjust = Enum.TryParse(attr.Value, true, out LengthAdjustment adj) ? adj : LengthAdjustment.Spacing; + break; + case "writing-mode": + switch (attr.Value) + { + case "horizontal-tb": + case "lr": + case "lr-tb": + case "rl": + case "rl-tb": + WritingMode = WritingMode.HorizontalTopToBottom; + break; + case "vertical-rl": + case "tb": + case "tb-rl": + WritingMode = WritingMode.VerticalRightToLeft; + break; + case "vertical-lr": + WritingMode = WritingMode.VerticalLeftToRight; + break; + default: + Debug.WriteLine($"Unknown writing-mode: {attr.Value}"); + break; + } + break; + default: + Attributes.Add(new StyleItem(attr.Name, attr.Value)); + break; + } + } + if(X is null) + { + X = LengthPercentageOrNumberList.Empty(this, LengthOrientation.Horizontal); + } + if (Y is null) + { + Y = LengthPercentageOrNumberList.Empty(this, LengthOrientation.Vertical); + } + if (DX is null) + { + DX = LengthPercentageOrNumberList.Empty(this, LengthOrientation.Horizontal); + } + if (DY is null) + { + DY = LengthPercentageOrNumberList.Empty(this, LengthOrientation.Vertical); + } + + ParseChildren(svg, node); + } + + protected void ParseChildren(SVG svg, XmlNode node) + { + foreach (XmlNode child in node.ChildNodes) + { + if (child.NodeType == XmlNodeType.Text || child.NodeType == XmlNodeType.CDATA) + { + var text = child.InnerText.Trim(); + if (!string.IsNullOrWhiteSpace(text)) + { + Children.Add(new TextString(this, text)); + } + } + else if (child.NodeType == XmlNodeType.Element && child.Name == "tspan") + { + var span = new TextSpan(svg, child, this); + Children.Add(span); + } + // Future support for , , etc. could go here + } + } + + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/TextSpan.cs b/Source/SVGImage/SVG/Shapes/TextSpan.cs new file mode 100644 index 0000000..c6cd990 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/TextSpan.cs @@ -0,0 +1,17 @@ +using System.Xml; + +namespace SVGImage.SVG.Shapes +{ + public class TextSpan : TextShapeBase, ITextChild + { + + public TextSpan(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) + { + } + + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/TextString.cs b/Source/SVGImage/SVG/Shapes/TextString.cs new file mode 100644 index 0000000..7b0af19 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/TextString.cs @@ -0,0 +1,68 @@ +namespace SVGImage.SVG.Shapes +{ + using System.Linq; + using System.Text.RegularExpressions; + using System.Diagnostics; + + [DebuggerDisplay("{Text}")] + /// + /// The leaf of the Text tree + /// + public class TextString : ITextChild + { + private static readonly Regex _trimmedWhitespace = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.Singleline); + /// + /// Represents the layout info for the string of text owned by this TextString. + /// + public CharacterLayout[] Characters { get; set; } + public Shape Parent { get; set; } + /// + /// The index of this TextString in the root TextShape. This is set by the . + /// + public int Index { get; internal set; } + public TextString(Shape parent, string text) + { + Parent = parent; + string trimmed = _trimmedWhitespace.Replace(text.Trim(), " "); + Characters = new CharacterLayout[trimmed.Length]; + for(int i = 0; i < trimmed.Length; i++) + { + var c = trimmed[i]; + Characters[i] = new CharacterLayout(c); + } + } + public CharacterLayout GetFirstCharacter() + { + return Characters.FirstOrDefault(); + } + public CharacterLayout GetLastCharacter() + { + return Characters.LastOrDefault(); + } + public CharacterLayout FirstCharacter => GetFirstCharacter(); + public CharacterLayout LastCharacter => GetLastCharacter(); + public string Text => GetText(); + public int Length => GetLength(); + + public TextStyle TextStyle { get; internal set; } + + public string GetText() + { + return new string(Characters.Select(c => c.Character).ToArray()); + } + + public int GetLength() + { + return Characters.Length; + } + + public CharacterLayout[] GetCharacters() + { + return Characters; + } + } + + + + +} diff --git a/Source/SVGImage/SVG/Shapes/WritingMode.cs b/Source/SVGImage/SVG/Shapes/WritingMode.cs new file mode 100644 index 0000000..9f2f342 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/WritingMode.cs @@ -0,0 +1,41 @@ +namespace SVGImage.SVG.Shapes +{ + /// + /// The ‘writing-mode’ property specifies whether the initial inline-progression-direction for a ‘text’ element shall be left-to-right, right-to-left, or top-to-bottom. + /// The ‘writing-mode’ property applies only to ‘text’ elements; the property is ignored for ‘tspan’, ‘tref’, ‘altGlyph’ and ‘textPath’ sub-elements. + /// (Note that the inline-progression-direction can change within a ‘text’ element due to the Unicode bidirectional algorithm and properties ‘direction’ and ‘unicode-bidi’. + /// For more on bidirectional text, see Relationship with bidirectionality.) + /// + public enum WritingMode + { + /// + /// Inherits the writing mode from the parent element. + /// + None = 0, + /// + /// This value defines a top-to-bottom block flow direction. Both the writing mode and the typographic mode are horizontal. + /// + /// + /// Set for atrributes with values 'horizontal-tb', 'lr', 'lr-tb', 'rl', and 'rl-tb'. + /// + HorizontalTopToBottom, + /// + /// This value defines a right-to-left block flow direction. Both the writing mode and the typographic mode are vertical. + /// + /// + /// Set for atrributes with values 'vertical-rl', 'tb-rl', 'tb'. + /// + VerticalRightToLeft, + /// + /// This value defines a left-to-right block flow direction. Both the writing mode and the typographic mode are vertical. + /// + /// + /// Set for atrributes with value 'vertical-lr'. + /// + VerticalLeftToRight, + } + + + + +} diff --git a/Source/SVGImage/SVG/TextRender.cs b/Source/SVGImage/SVG/TextRender2.cs similarity index 63% rename from Source/SVGImage/SVG/TextRender.cs rename to Source/SVGImage/SVG/TextRender2.cs index 8ce868f..b9289d9 100644 --- a/Source/SVGImage/SVG/TextRender.cs +++ b/Source/SVGImage/SVG/TextRender2.cs @@ -2,15 +2,15 @@ using System.Windows.Media; using System.Windows; using System.Reflection; +using SVGImage.SVG.Shapes; +using System.Linq; +using SVGImage.SVG.Utils; namespace SVGImage.SVG { - static class TextRender + public class TextRender2 : TextRenderBase { - private static int dpiX = 0; - private static int dpiY = 0; - - static public GeometryGroup BuildTextGeometry(TextShape shape) + public override GeometryGroup BuildTextGeometry(TextShape shape) { return BuildGlyphRun(shape, 0, 0); } @@ -20,60 +20,45 @@ static public GeometryGroup BuildTextGeometry(TextShape shape) static GeometryGroup BuildGlyphRun(TextShape shape, double xoffset, double yoffset) { GeometryGroup gp = new GeometryGroup(); - double totalwidth = 0; - if (shape.TextSpan == null) - { - string txt = shape.Text; - gp.Children.Add(BuildGlyphRun(shape.TextStyle, txt, shape.X, shape.Y, ref totalwidth)); - return gp; - } return BuildTextSpan(shape); } static GeometryGroup BuildTextSpan(TextShape shape) { - double x = shape.X; - double y = shape.Y; + double x = shape.X.FirstOrDefault().Value; + double y = shape.Y.FirstOrDefault().Value; GeometryGroup gp = new GeometryGroup(); - BuildTextSpan(gp, shape.TextStyle, shape.TextSpan, ref x, ref y); + BuildTextSpan(gp, shape.TextStyle, shape, ref x, ref y); return gp; } - public static DependencyProperty TSpanElementProperty = DependencyProperty.RegisterAttached( - "TSpanElement", typeof(TextSpan), typeof(DependencyObject)); - public static void SetElement(DependencyObject obj, TextSpan value) - { - obj.SetValue(TSpanElementProperty, value); - } - public static TextSpan GetElement(DependencyObject obj) - { - return (TextSpan)obj.GetValue(TSpanElementProperty); - } + - static void BuildTextSpan(GeometryGroup gp, TextStyle textStyle, - TextSpan tspan, ref double x, ref double y) + static void BuildTextSpan(GeometryGroup gp, TextStyle textStyle, + TextShapeBase tspan, ref double x, ref double y) { - foreach (TextSpan child in tspan.Children) + foreach (ITextNode child in tspan.Children) { - if (child.ElementType == TextSpan.eElementType.Text) + if (child is TextString textString) { - string txt = child.Text; + string txt = textString.Text; double totalwidth = 0; double baseline = y; - if (child.TextStyle.BaseLineShift == "sub") - baseline += child.TextStyle.FontSize * 0.5; /* * cap height ? fontSize*/; - if (child.TextStyle.BaseLineShift == "super") - baseline -= tspan.TextStyle.FontSize + (child.TextStyle.FontSize * 0.25)/*font.CapsHeight * fontSize*/; + if (textString.TextStyle.BaseLineShift == "sub") + baseline += textString.TextStyle.FontSize * 0.5; /* * cap height ? fontSize*/; + if (textString.TextStyle.BaseLineShift == "super") + baseline -= tspan.TextStyle.FontSize + (textString.TextStyle.FontSize * 0.25)/*font.CapsHeight * fontSize*/; - Geometry gm = BuildGlyphRun(child.TextStyle, txt, x, baseline, ref totalwidth); - TextRender.SetElement(gm, child); + Geometry gm = BuildGlyphRun(textString.TextStyle, txt, x, baseline, ref totalwidth); + TextRender2.SetElement(gm, textString); gp.Children.Add(gm); x += totalwidth; - continue; } - if (child.ElementType == TextSpan.eElementType.Tag) - BuildTextSpan(gp, textStyle, child, ref x, ref y); + else if (child is TextShapeBase childTspan) + { + BuildTextSpan(gp, textStyle, childTspan, ref x, ref y); + } } } @@ -82,19 +67,10 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double if (string.IsNullOrEmpty(text)) return new GeometryGroup(); - if (dpiX == 0 || dpiY == 0) - { - var sysPara = typeof(SystemParameters); - var dpiXProperty = sysPara.GetProperty("DpiX", BindingFlags.NonPublic | BindingFlags.Static); - var dpiYProperty = sysPara.GetProperty("Dpi", BindingFlags.NonPublic | BindingFlags.Static); - - dpiX = (int)dpiXProperty.GetValue(null, null); - dpiY = (int)dpiYProperty.GetValue(null, null); - } double fontSize = textStyle.FontSize; GlyphRun glyphs = null; - Typeface font = new Typeface(new FontFamily(textStyle.FontFamily), - textStyle.Fontstyle, + Typeface font = new Typeface(new FontFamily(textStyle.FontFamily), + textStyle.Fontstyle, textStyle.Fontweight, FontStretch.FromOpenTypeStretch(9), new FontFamily("Arial Unicode MS")); @@ -102,12 +78,10 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double double baseline = y; if (font.TryGetGlyphTypeface(out glyphFace)) { -#if DOTNET40 || DOTNET45 || DOTNET46 - glyphs = new GlyphRun(); +#if DPI_AWARE + glyphs = new GlyphRun((float)DpiUtil.PixelsPerDip); #else - var dpiScale = new DpiScale(dpiX, dpiY); - - glyphs = new GlyphRun((float)dpiScale.PixelsPerDip); + glyphs = new GlyphRun(); #endif ((System.ComponentModel.ISupportInitialize)glyphs).BeginInit(); glyphs.GlyphTypeface = glyphFace; @@ -116,7 +90,7 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double List glyphIndices = new List(); List advanceWidths = new List(); totalwidth = 0; - char[] charsToSkip = new char[] {'\t', '\r', '\n'}; + char[] charsToSkip = new char[] { '\t', '\r', '\n' }; for (int i = 0; i < text.Length; ++i) { char textchar = text[i]; @@ -124,15 +98,15 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double //if (charsToSkip.Any(item => item == codepoint)) // continue; ushort glyphIndex; - if (glyphFace.CharacterToGlyphMap.TryGetValue(codepoint, out glyphIndex) == false) + if (!glyphFace.CharacterToGlyphMap.TryGetValue(codepoint, out glyphIndex)) continue; textChars.Add(textchar); double glyphWidth = glyphFace.AdvanceWidths[glyphIndex]; glyphIndices.Add(glyphIndex); advanceWidths.Add(glyphWidth * fontSize + textStyle.LetterSpacing); if (char.IsWhiteSpace(textchar)) - advanceWidths[advanceWidths.Count-1] += textStyle.WordSpacing; - totalwidth += advanceWidths[advanceWidths.Count-1]; + advanceWidths[advanceWidths.Count - 1] += textStyle.WordSpacing; + totalwidth += advanceWidths[advanceWidths.Count - 1]; } glyphs.Characters = textChars.ToArray(); glyphs.GlyphIndices = glyphIndices.ToArray(); @@ -171,7 +145,7 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double } if (textStyle.TextDecoration[0].Location == TextDecorationLocation.OverLine) { - decorationPos = baseline - fontSize; + decorationPos = baseline - fontSize; decorationThickness = font.StrikethroughThickness * fontSize; } Rect bounds = new Rect(gp.Bounds.Left, decorationPos, gp.Bounds.Width, decorationThickness); @@ -181,4 +155,4 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double return gp; } } -} +} \ No newline at end of file diff --git a/Source/SVGImage/SVG/TextStyle.cs b/Source/SVGImage/SVG/TextStyle.cs index e56de7f..f343e14 100644 --- a/Source/SVGImage/SVG/TextStyle.cs +++ b/Source/SVGImage/SVG/TextStyle.cs @@ -2,52 +2,153 @@ namespace SVGImage.SVG { + using global::SVGImage.SVG.Utils; using Shapes; - + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; + using System.Text.RegularExpressions; + using System.Windows.Media; public sealed class TextStyle { + private static readonly FontResolver _fontResolver = new FontResolver(0); + //This should be configurable in some way. + private static readonly TextStyle _defaults = new TextStyle() + { + FontFamily = "Arial Unicode MS, Verdana", + FontSize = 12, + Fontweight = FontWeights.Normal, + Fontstyle = FontStyles.Normal, + TextAlignment = System.Windows.TextAlignment.Left, + WordSpacing = 0, + LetterSpacing = 0, + BaseLineShift = string.Empty, + }; + private string _fontFamily; + private double? _fontSize; + private FontWeight? _fontweight; + private FontStyle? _fontstyle; + private TextAlignment? _textAlignment; + private double? _wordSpacing; + private double? _letterSpacing; + private string _baseLineShift; + public TextStyle(TextStyle aCopy) { this.Copy(aCopy); } - public TextStyle(SVG svg, Shape owner) + private TextStyle() + { + + } + + public TextStyle(Shape owner) { - this.FontFamily = "Arial Unicode MS, Verdana"; - this.FontSize = 12; - this.Fontweight = FontWeights.Normal; - this.Fontstyle = FontStyles.Normal; - this.TextAlignment = System.Windows.TextAlignment.Left; - this.WordSpacing = 0; - this.LetterSpacing = 0; - this.BaseLineShift = string.Empty; if (owner.Parent != null) + { this.Copy(owner.Parent.TextStyle); + } + } + + public Typeface GetTypeface() + { + var fontFamily = _fontResolver.ResolveFontFamily(FontFamily); + return new Typeface(fontFamily, + Fontstyle, + Fontweight, + FontStretch.FromOpenTypeStretch(9), + new FontFamily(_defaults.FontFamily)); } - public string FontFamily {get; set;} - public double FontSize {get; set;} - public FontWeight Fontweight {get; set;} - public FontStyle Fontstyle {get; set;} - public TextDecorationCollection TextDecoration {get; set;} - public TextAlignment TextAlignment {get; set;} - public double WordSpacing {get; set;} - public double LetterSpacing {get; set;} - public string BaseLineShift {get; set;} - - public void Copy(TextStyle aCopy) + public GlyphTypeface GetGlyphTypeface() + { + var typeface = GetTypeface(); + GlyphTypeface glyphTypeface; + if (typeface.TryGetGlyphTypeface(out glyphTypeface)) + { + return glyphTypeface; + } + return null; + } + + public string FontFamily { get => _fontFamily ?? _defaults.FontFamily; set => _fontFamily = value; } + public double FontSize { get => _fontSize ?? _defaults.FontSize; set => _fontSize = value; } + public FontWeight Fontweight { get => _fontweight ?? _defaults.Fontweight; set => _fontweight = value; } + public FontStyle Fontstyle { get => _fontstyle ?? _defaults.Fontstyle; set => _fontstyle = value; } + public TextDecorationCollection TextDecoration { get; set; } + public TextAlignment TextAlignment { get => _textAlignment ?? _defaults.TextAlignment; set => _textAlignment = value; } + public double WordSpacing { get => _wordSpacing ?? _defaults.WordSpacing; set => _wordSpacing = value; } + public double LetterSpacing { get => _letterSpacing ?? _defaults.LetterSpacing; set => _letterSpacing = value; } + public string BaseLineShift { get => _baseLineShift ?? _defaults.BaseLineShift; set => _baseLineShift = value; } + + private void Copy(TextStyle aCopy) { if (aCopy == null) return; - this.FontFamily = aCopy.FontFamily; - this.FontSize = aCopy.FontSize; - this.Fontweight = aCopy.Fontweight; - this.Fontstyle = aCopy.Fontstyle;; - this.TextAlignment = aCopy.TextAlignment; - this.WordSpacing = aCopy.WordSpacing; - this.LetterSpacing = aCopy.LetterSpacing; - this.BaseLineShift = aCopy.BaseLineShift; - } - + this._fontFamily = aCopy._fontFamily; + this._fontSize = aCopy._fontSize; + this._fontweight = aCopy._fontweight; + this._fontstyle = aCopy._fontstyle; + this._textAlignment = aCopy._textAlignment; + this._wordSpacing = aCopy._wordSpacing; + this._letterSpacing = aCopy._letterSpacing; + this._baseLineShift = aCopy._baseLineShift; + } + + public static TextStyle Merge(TextStyle baseStyle, TextStyle overrides) + { + var result = new TextStyle(); + result._fontFamily = overrides._fontFamily ?? baseStyle._fontFamily; + result._fontSize = overrides._fontSize ?? baseStyle._fontSize; + result._fontweight = overrides._fontweight ?? baseStyle._fontweight; + result._fontstyle = overrides._fontstyle ?? baseStyle._fontstyle; + result._textAlignment = overrides._textAlignment ?? baseStyle._textAlignment; + result._wordSpacing = overrides._wordSpacing ?? baseStyle._wordSpacing; + result._letterSpacing = overrides._letterSpacing ?? baseStyle._letterSpacing; + result._baseLineShift = overrides._baseLineShift ?? baseStyle._baseLineShift; + if (overrides.TextDecoration != null) + { + //None was explicitly set + if (overrides.TextDecoration.Count <= 0) + { + result.TextDecoration = new TextDecorationCollection(); + } + //Copy overrides + else if (baseStyle.TextDecoration == null || baseStyle.TextDecoration.Count <= 0) + { + result.TextDecoration = new TextDecorationCollection(overrides.TextDecoration.Select(CopyTextDecoration)); + } + //merge with base style + else + { + var merged = new List(); + merged.AddRange(baseStyle.TextDecoration.Select(CopyTextDecoration)); + merged.AddRange(overrides.TextDecoration.Select(CopyTextDecoration)); + result.TextDecoration = new TextDecorationCollection(merged); + } + } + else if (baseStyle.TextDecoration != null) + { + result.TextDecoration = new TextDecorationCollection(baseStyle.TextDecoration.Select(CopyTextDecoration)); + } + else + { + result.TextDecoration = null; + } + return result; + } + /// + /// Does not clone the TextDecorationCollection, but creates a new instance with the same properties. This prevents issues with shared references in the TextDecorationCollection. + /// + /// The TextDecoration to copy. + /// + /// A new TextDecoration instance with the same properties as the original. + /// + private static TextDecoration CopyTextDecoration(TextDecoration textDecoration) + { + return new TextDecoration(textDecoration.Location, textDecoration.Pen, textDecoration.PenOffset, textDecoration.PenOffsetUnit, textDecoration.PenThicknessUnit); + } } } diff --git a/Source/SVGImage/SVG/Utils/DpiUtil.cs b/Source/SVGImage/SVG/Utils/DpiUtil.cs new file mode 100644 index 0000000..7bb404f --- /dev/null +++ b/Source/SVGImage/SVG/Utils/DpiUtil.cs @@ -0,0 +1,49 @@ +using System.Windows; +using System.Reflection; + +namespace SVGImage.SVG.Utils +{ + + public static class DpiUtil + { + static DpiUtil() + { + try + { + var sysPara = typeof(SystemParameters); + var dpiXProperty = sysPara.GetProperty("DpiX", BindingFlags.NonPublic | BindingFlags.Static); + var dpiYProperty = sysPara.GetProperty("Dpi", BindingFlags.NonPublic | BindingFlags.Static); + DpiX = (int)dpiXProperty.GetValue(null, null); + DpiX = (int)dpiYProperty.GetValue(null, null); + } + catch + { + DpiX = 96; + DpiY = 96; + } +#if DPI_AWARE + DpiScale = new DpiScale(DpiX / 96.0, DpiY / 96.0); +#endif + } + + public static int DpiX { get; private set; } + public static int DpiY { get; private set; } +#if DPI_AWARE + public static DpiScale DpiScale { get; private set; } +#endif + public static double PixelsPerDip => GetPixelsPerDip(); + + public static double GetPixelsPerDip() + { + return DpiY / 96.0; + } + + + + + } + + + + +} diff --git a/Source/SVGImage/SVG/Utils/EnumerableExtensions.cs b/Source/SVGImage/SVG/Utils/EnumerableExtensions.cs new file mode 100644 index 0000000..9bab667 --- /dev/null +++ b/Source/SVGImage/SVG/Utils/EnumerableExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace SVGImage.SVG.Utils +{ + internal static class EnumerableExtensions + { + public static int IndexOfFirst(this IEnumerable source, Func predicate) + { + int i = 0; + foreach (var item in source) + { + if (predicate(item)) + { + return i; + } + i++; + } + return -1; // Not found + } + } + + + + +} diff --git a/Source/SVGImage/SVG/Utils/FontResolver.cs b/Source/SVGImage/SVG/Utils/FontResolver.cs new file mode 100644 index 0000000..a64db0b --- /dev/null +++ b/Source/SVGImage/SVG/Utils/FontResolver.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Windows.Media; + +namespace SVGImage.SVG.Utils +{ + + /// + /// A utility class that resolves font families based on requested font names. + /// + public class FontResolver + { + private readonly ConcurrentDictionary _fontCache = new ConcurrentDictionary(); + private readonly Dictionary _availableFonts; + private readonly Dictionary _normalizedFontNameMap; + + /// + /// A utility class that resolves font families based on requested font names. + /// + /// Maximum Levenshtein distance to consider a match. If set to ≤ 0, Levenshtein matching is disabled. + public FontResolver(int maxLevenshteinDistance = 0) + { + _availableFonts = Fonts.SystemFontFamilies + .Select(ff => new { NormalName = ff.Source, Family = ff }) + .ToDictionary(x => x.NormalName, x => x.Family, StringComparer.OrdinalIgnoreCase); + + _normalizedFontNameMap = _availableFonts.Keys + .ToDictionary( + name => Normalize(name), + name => name, + StringComparer.OrdinalIgnoreCase); + MaxLevenshteinDistance = maxLevenshteinDistance; + } + /// + /// Maximum Levenshtein distance to consider a match. + /// If set to ≤ 0, Levenshtein matching is disabled. + /// + public int MaxLevenshteinDistance { get; set; } = 0; + + /// + /// Attempts to a font family based on the requested font name. + /// + /// The name of the font to resolve. + /// + /// A if a match is found, otherwise null. + /// + /// + /// Thrown when the requested font name is null or empty. + /// + public FontFamily ResolveFontFamily(string requestedFontName) + { + if (string.IsNullOrWhiteSpace(requestedFontName)) + { + throw new ArgumentException("Font name cannot be null or empty.", nameof(requestedFontName)); + } + + if (_fontCache.TryGetValue(requestedFontName, out var cachedFont)) + { + return cachedFont; + } + + // 1. Exact match + if (_availableFonts.TryGetValue(requestedFontName, out var exactMatch)) + { + _fontCache[requestedFontName] = exactMatch; + return exactMatch; + } + + // 2. Normalized match + string normalizedRequested = Normalize(requestedFontName); + if (_normalizedFontNameMap.TryGetValue(normalizedRequested, out var normalizedMatchName) && + _availableFonts.TryGetValue(normalizedMatchName, out var normalizedMatch)) + { + _fontCache[requestedFontName] = normalizedMatch; + return normalizedMatch; + } + + // 3. Substring match + var substringMatch = _availableFonts + .FirstOrDefault(kv => Normalize(kv.Key).Contains(normalizedRequested)); + if (substringMatch.Value != null) + { + _fontCache[requestedFontName] = substringMatch.Value; + return substringMatch.Value; + } + + // 4. Levenshtein match (optional but slow) + if ( MaxLevenshteinDistance > 0) + { + var bestMatch = _availableFonts + .Select(kv => new + { + FontName = kv.Key, + Font = kv.Value, + Distance = Levenshtein(normalizedRequested, Normalize(kv.Key)) + }) + .OrderBy(x => x.Distance) + .FirstOrDefault(); + + if (bestMatch != null && bestMatch.Distance <= 4) + { + _fontCache[requestedFontName] = bestMatch.Font; + return bestMatch.Font; + } + } + + + + // 5. No match + _fontCache[requestedFontName] = null; + return null; + } + + /// + /// Matches spaces, hyphens, and underscores + /// + private static readonly Regex _normalizationRegex = new Regex(@"[\s\-_]", RegexOptions.Compiled); + + /// + /// Remove spaces, hyphens, underscores, and make lowercase + /// + /// The font name to normalize. + /// + /// A normalized version of the font name, with spaces, hyphens, and underscores removed, and all characters in lowercase. + /// + private static string Normalize(string fontName) + { + if (fontName is null) + { + return string.Empty; + } + return _normalizationRegex.Replace(fontName, String.Empty).ToLowerInvariant(); + + } + + private static int[,] CreateDistanceMatrix(int length1, int length2) + { + var matrix = new int[length1 + 1, length2 + 1]; + for (int i = 0; i <= length1; i++) + { + matrix[i, 0] = i; + } + for (int j = 0; j <= length2; j++) + { + matrix[0, j] = j; + } + return matrix; + } + + /// + /// Calculates the Levenshtein distance between two strings. + /// The Levenshtein distance is a measure of the difference between two sequences. + /// It is defined as the minimum number of single-character edits (insertions, deletions, or substitutions) + /// + /// The first string to compare. + /// The second string to compare. + /// + /// The Levenshtein distance between the two strings. + /// + private static int Levenshtein(string string1, string string2) + { + if (string1 == string2) + { + return 0; + } + + if (string.IsNullOrEmpty(string1)) + { + return string2.Length; + } + + if (string.IsNullOrEmpty(string2)) + { + return string1.Length; + } + + + int[,] distanceMatrix = CreateDistanceMatrix(string1.Length, string2.Length); + + for (int i = 1; i <= string1.Length; i++) + { + for (int j = 1; j <= string2.Length; j++) + { + int cost = string1[i - 1] == string2[j - 1] ? 0 : 1; + + distanceMatrix[i, j] = Math.Min( + Math.Min(distanceMatrix[i - 1, j] + 1, // deletion + distanceMatrix[i, j - 1] + 1), // insertion + distanceMatrix[i - 1, j - 1] + cost); // substitution + } + } + + return distanceMatrix[string1.Length, string2.Length]; + } + + } + +} diff --git a/Source/SVGImage/SVG/Utils/TextStyleStack.cs b/Source/SVGImage/SVG/Utils/TextStyleStack.cs new file mode 100644 index 0000000..f864651 --- /dev/null +++ b/Source/SVGImage/SVG/Utils/TextStyleStack.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace SVGImage.SVG.Utils +{ + internal sealed class TextStyleStack + { + private readonly Stack _stack = new Stack(); + internal void Push(TextStyle textStyle) + { + if (textStyle == null) + { + throw new ArgumentNullException(nameof(textStyle), $"{nameof(textStyle)} cannot be null."); + } + if (_stack.Count == 0) + { + _stack.Push(textStyle); + return; + } + _stack.Push(TextStyle.Merge(_stack.Peek(), textStyle)); + } + + internal TextStyle Pop() + { + return _stack.Pop(); + } + internal TextStyle Peek() + { + return _stack.Peek(); + } + } + + + + +}