diff --git a/Samples/Example/Example.csproj b/Samples/Example/Example.csproj index def0b7f..cc2f2ba 100644 --- a/Samples/Example/Example.csproj +++ b/Samples/Example/Example.csproj @@ -43,9 +43,11 @@ + + diff --git a/Samples/Example/Images/acid1_TextOnly.svg b/Samples/Example/Images/acid1_TextOnly.svg new file mode 100644 index 0000000..0ef7b64 --- /dev/null +++ b/Samples/Example/Images/acid1_TextOnly.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + Hello World! + + Hello + World! + + + + + + + + \ No newline at end of file diff --git a/Samples/Example/Images/boldgreen.svg b/Samples/Example/Images/boldgreen.svg new file mode 100644 index 0000000..e9edc0f --- /dev/null +++ b/Samples/Example/Images/boldgreen.svg @@ -0,0 +1,13 @@ + + + + + + + + BOLD + + + + \ No newline at end of file diff --git a/Source/SVGImage/DotNetProjects.SVGImage.csproj b/Source/SVGImage/DotNetProjects.SVGImage.csproj index 02886db..414995a 100644 --- a/Source/SVGImage/DotNetProjects.SVGImage.csproj +++ b/Source/SVGImage/DotNetProjects.SVGImage.csproj @@ -29,7 +29,7 @@ images\dotnetprojects.png svg wpf svg-icons svg-to-png svg-to-xaml svgimage svgimage-control Readme.md - true + 5.1.0 diff --git a/Source/SVGImage/SVG/PaintServers/PaintServerManager.cs b/Source/SVGImage/SVG/PaintServers/PaintServerManager.cs index d5dc1cc..b1159af 100644 --- a/Source/SVGImage/SVG/PaintServers/PaintServerManager.cs +++ b/Source/SVGImage/SVG/PaintServers/PaintServerManager.cs @@ -160,10 +160,11 @@ public static Color ParseHexColor(string value) newval |= (@int & 0x00000f); u = newval; } - else { - u = Convert.ToUInt64(value.Substring(start), 16); - } - + else + { + u = Convert.ToUInt64(value.Substring(start), 16); + } + byte a = (byte)((u & 0xff000000) >> 24); byte r = (byte)((u & 0x00ff0000) >> 16); byte g = (byte)((u & 0x0000ff00) >> 8); @@ -223,10 +224,12 @@ private int ParseColorNumber(string value) { if (value.EndsWith("%")) { - var nr = int.Parse(value.Substring(0, value.Length - 1)); + var nr = double.Parse(value.Substring(0, value.Length - 1)); if (nr < 0) - nr = 255 - nr; - return nr * 255 / 100; + nr = 255 - nr; //TODO: what is this trying to do? + var result = (int)Math.Round((nr * 255) / 100); + + return MathUtil.Clamp(result, 0, 255); } return int.Parse(value); diff --git a/Source/SVGImage/SVG/SVGRender.cs b/Source/SVGImage/SVG/SVGRender.cs index 7039d93..24b1c64 100644 --- a/Source/SVGImage/SVG/SVGRender.cs +++ b/Source/SVGImage/SVG/SVGRender.cs @@ -3,6 +3,7 @@ using System.Xml; using System.Linq; using System.Collections.Generic; +using SVGImage.SVG.Utils; using System.Windows; using System.Windows.Media; @@ -115,7 +116,8 @@ public DrawingGroup LoadDrawing(Stream stream) public DrawingGroup CreateDrawing(SVG svg) { - return this.LoadGroup(svg.Elements, svg.ViewBox, false); + var drawingGroup = this.LoadGroup(svg.Elements, svg.ViewBox, false); + return drawingGroup; } public DrawingGroup CreateDrawing(Shape shape) @@ -443,9 +445,9 @@ internal DrawingGroup LoadGroup(IList elements, Rect? viewBox, bool isSwi GeometryGroup gp = textRender2.BuildTextGeometry(textShape); if (gp != null) { - foreach (Geometry gm in gp.Children) + foreach (Geometry gm in GetStyledSpans(gp)) { - if (TextRenderBase.GetElement(gm) is TextShapeBase tspan) + if (TextRender.GetElement(gm) is TextShapeBase tspan) { var di = this.NewDrawingItem(tspan, gm); AddDrawingToGroup(grp, shape, di); @@ -480,6 +482,31 @@ internal DrawingGroup LoadGroup(IList elements, Rect? viewBox, bool isSwi return grp; } + private static IEnumerable GetStyledSpans(Geometry geometry) + { + if (geometry is GeometryGroup gg) + { + if (!(TextRender.GetElement(gg) is null)) + { + yield return geometry; + } + else + { + foreach (var g in gg.Children) + { + foreach (var subg in GetStyledSpans(g)) + { + yield return subg; + } + } + } + } + else + { + yield return geometry; + } + } + private void AddDrawingToGroup(DrawingGroup grp, Shape shape, Drawing drawing) { if (shape.Clip != null || shape.Transform != null || shape.Filter != null) diff --git a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs index 4b9ca9b..a255c51 100644 --- a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs +++ b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs @@ -6,7 +6,7 @@ namespace SVGImage.SVG.Shapes public struct LengthPercentageOrNumber { - private static readonly Regex _lengthRegex = new Regex(@"(?\d+(?:\.\d+)?)\s*(?%|\w+)?", RegexOptions.Compiled | RegexOptions.Singleline); + private static readonly Regex _lengthRegex = new Regex(@"(?-?\d+(?:\.\d+)?)\s*(?%|\w+)?", RegexOptions.Compiled | RegexOptions.Singleline); private readonly LengthContext _context; private readonly double _value; /// @@ -180,12 +180,12 @@ public static LengthPercentageOrNumber Parse(Shape owner, string value, LengthOr } else { - // Default to pixels if no unit is specified - context = new LengthContext(owner, LengthUnit.px); + // Default to Number if no unit is specified + context = new LengthContext(owner, LengthUnit.Number); } return new LengthPercentageOrNumber(d, context); } - + } diff --git a/Source/SVGImage/SVG/Shapes/Shape.cs b/Source/SVGImage/SVG/Shapes/Shape.cs index 7d45c29..a44f6ed 100644 --- a/Source/SVGImage/SVG/Shapes/Shape.cs +++ b/Source/SVGImage/SVG/Shapes/Shape.cs @@ -125,6 +125,11 @@ public Stroke Stroke } protected virtual Fill DefaultFill() + { + return null; + } + + protected virtual Fill GetParentFill() { var parent = this.Parent; while (parent != null) @@ -141,7 +146,7 @@ protected virtual Fill DefaultFill() public Fill Fill { - get => m_fill ?? DefaultFill(); + get => m_fill ?? GetParentFill() ?? DefaultFill(); set => m_fill = value; } diff --git a/Source/SVGImage/SVG/Shapes/TextRender.cs b/Source/SVGImage/SVG/Shapes/TextRender.cs index cdd1a12..03fb88c 100644 --- a/Source/SVGImage/SVG/Shapes/TextRender.cs +++ b/Source/SVGImage/SVG/Shapes/TextRender.cs @@ -7,6 +7,9 @@ namespace SVGImage.SVG.Shapes using Utils; using System.Linq; using System.Windows.Markup; + using System; + using System.Reflection; + using System.Windows.Documents; /// /// This class is responsible for rendering text shapes in SVG. @@ -25,6 +28,117 @@ public override GeometryGroup BuildTextGeometry(TextShape text) return CreateGeometry(text, state); } } + + + + public static readonly DependencyProperty TextSpanTextStyleProperty = DependencyProperty.RegisterAttached( + "TextSpanTextStyle", typeof(TextStyle), typeof(DependencyObject)); + private static void SetTextSpanTextStyle(DependencyObject obj, TextStyle value) + { + obj.SetValue(TextSpanTextStyleProperty, value); + } + public static TextStyle GetTextSpanTextStyle(DependencyObject obj) + { + return (TextStyle)obj.GetValue(TextSpanTextStyleProperty); + } + + private sealed class TextChunk + { + public List GlyphRuns { get; set; } = new List(); + public Dictionary> BackgroundDecorations { get; set; } = new Dictionary>(); + public Dictionary> ForegroundDecorations { get; set; } = new Dictionary>(); + public Dictionary GlyphRunBounds { get; set; } = new Dictionary(); + public Dictionary TextStyles { get; set; } = new Dictionary(); + public Dictionary TextContainers { get; set; } = new Dictionary(); + public TextAlignment TextAlignment { get; set; } + + public GeometryGroup BuildGeometry() + { + double alignmentOffset = GetAlignmentOffset(); + bool nonZeroAlignmentOffset = !alignmentOffset.IsNearlyZero(); + GeometryGroup geometryGroup = new GeometryGroup(); + foreach (var glyphRun in GlyphRuns) + { + var runGeometry = !nonZeroAlignmentOffset ? glyphRun.BuildGeometry() : glyphRun.CreateOffsetRun(alignmentOffset, 0).BuildGeometry(); + geometryGroup.Children.Add(runGeometry); + if (TextStyles.TryGetValue(glyphRun, out TextStyle textStyle)) + { + TextRender.SetTextSpanTextStyle(runGeometry, textStyle); + } + if (TextContainers.TryGetValue(glyphRun, out TextShapeBase textContainer)) + { + TextRender.SetElement(runGeometry, textContainer); + } + if (BackgroundDecorations.TryGetValue(glyphRun, out List backgroundDecorations)) + { + foreach (var decoration in backgroundDecorations) + { + if (nonZeroAlignmentOffset) + { + decoration.Offset(alignmentOffset, 0); + } + //Underline and OverLine should be drawn behind the text + geometryGroup.Children.Insert(0, new RectangleGeometry(decoration)); + } + } + if (ForegroundDecorations.TryGetValue(glyphRun, out List foregroundDecorations)) + { + foreach (var decoration in foregroundDecorations) + { + if (nonZeroAlignmentOffset) + { + decoration.Offset(alignmentOffset, 0); + } + //Strikethrough should be drawn on top of the text + geometryGroup.Children.Add(new RectangleGeometry(decoration)); + } + } + } + return geometryGroup; + } + + private Rect GetBoundingBox() + { + if (GlyphRunBounds.Count == 0) + { + return Rect.Empty; + } + Rect boundingBox = GlyphRunBounds.First().Value; + foreach (var kvp in GlyphRunBounds) + { + boundingBox.Union(kvp.Value); + } + return boundingBox; + } + + private double GetAlignmentOffset() + { + if(TextAlignment == TextAlignment.Left) + { + return 0; // No offset needed for left alignment + } + var boundingBox = GetBoundingBox(); + double totalWidth = boundingBox.Width; + double alignmentOffset = 0; + switch (TextAlignment) + { + case TextAlignment.Left: + break; + case TextAlignment.Right: + alignmentOffset = totalWidth; + break; + case TextAlignment.Center: + alignmentOffset = totalWidth / 2d; + break; + case TextAlignment.Justify: + // Justify is not implemented + break; + default: + break; + } + return alignmentOffset; + } + } private static GeometryGroup CreateGeometry(TextShape root, TextRenderState state) { state.Resolve(root); @@ -35,33 +149,57 @@ private static GeometryGroup CreateGeometry(TextShape root, TextRenderState stat GeometryGroup mainGeometryGroup = new GeometryGroup(); var baselineOrigin = new Point(root.X.FirstOrDefault().Value, root.Y.FirstOrDefault().Value); var isSideways = root.WritingMode == WritingMode.HorizontalTopToBottom; + TextAlignment currentTextAlignment = root.TextStyle.TextAlignment; + List textChunks = new List(); + bool newTextChunk = false; + TextChunk currentTextChunk = null; 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) + if (CreateRun(textString, state, font, isSideways, baselineOrigin, out Point newBaseline, out newTextChunk, ref currentTextAlignment) is GlyphRun run) { + if (newTextChunk) + { + if(currentTextChunk != null) + { + // Add the current text chunk to the list + textChunks.Add(currentTextChunk); + } + currentTextChunk = new TextChunk(); + currentTextChunk.TextAlignment = currentTextAlignment; + } var runGeometry = run.BuildGeometry(); - geometryGroup.Children.Add(runGeometry); + currentTextChunk.GlyphRuns.Add(run); + currentTextChunk.TextStyles[run] = textStyle; + currentTextChunk.GlyphRunBounds[run] = runGeometry.Bounds; + currentTextChunk.TextContainers[run] = (TextShapeBase)textString.Parent; if (textStyle.TextDecoration != null && textStyle.TextDecoration.Count > 0) { - GetTextDecorations(geometryGroup, textStyle, font, baselineOrigin, out List backgroundDecorations, out List foregroundDecorations); - foreach (var decoration in backgroundDecorations) + GetTextDecorations(runGeometry, textStyle, font, baselineOrigin, out List backgroundDecorations, out List foregroundDecorations); + if(backgroundDecorations.Count > 0) { - //Underline and OverLine should be drawn behind the text - geometryGroup.Children.Insert(0, new RectangleGeometry(decoration)); + currentTextChunk.BackgroundDecorations[run] = backgroundDecorations; } - foreach (var decoration in foregroundDecorations) + if (foregroundDecorations.Count > 0) { - //Strikethrough should be drawn on top of the text - geometryGroup.Children.Add(new RectangleGeometry(decoration)); + currentTextChunk.ForegroundDecorations[run] = foregroundDecorations; } } - mainGeometryGroup.Children.Add(geometryGroup); } baselineOrigin = newBaseline; } + textChunks.Add(currentTextChunk); + + foreach(var textChunk in textChunks) + { + if (textChunk.GlyphRuns.Count == 0) + { + continue; // No glyphs to render in this chunk + } + GeometryGroup geometryGroup = textChunk.BuildGeometry(); + mainGeometryGroup.Children.Add(geometryGroup); + } mainGeometryGroup.Transform = root.Transform; return mainGeometryGroup; @@ -73,13 +211,13 @@ private static GeometryGroup CreateGeometry(TextShape root, TextRenderState stat /// /// 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) + private static void GetTextDecorations(Geometry geometry, TextStyle textStyle, Typeface font, Point baselineOrigin, out List backgroundDecorations, out List foregroundDecorations) { backgroundDecorations = new List(); foregroundDecorations = new List(); @@ -93,28 +231,29 @@ private static void GetTextDecorations(GeometryGroup geometryGroup, TextStyle te { decorationPos = baselineY - (font.StrikethroughPosition * fontSize); decorationThickness = font.StrikethroughThickness * fontSize; - Rect bounds = new Rect(geometryGroup.Bounds.Left, decorationPos, geometryGroup.Bounds.Width, decorationThickness); + Rect bounds = new Rect(geometry.Bounds.Left, decorationPos, geometry.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); + Rect bounds = new Rect(geometry.Bounds.Left, decorationPos, geometry.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); + Rect bounds = new Rect(geometry.Bounds.Left, decorationPos, geometry.Bounds.Width, decorationThickness); backgroundDecorations.Add(bounds); } } } - private static GlyphRun CreateRun(TextString textString, TextRenderState state, Typeface font, bool isSideways, Point baselineOrigin, out Point newBaseline) + private static GlyphRun CreateRun(TextString textString, TextRenderState state, Typeface font, bool isSideways, Point baselineOrigin, out Point newBaseline, out bool newTextChunk, ref TextAlignment currentTextAlignment) { + newTextChunk = textString.FirstCharacter.GlobalIndex == 0; var textStyle = textString.TextStyle; var characterInfos = textString.GetCharacters(); if(characterInfos is null ||characterInfos.Length == 0) @@ -135,31 +274,64 @@ private static GlyphRun CreateRun(TextString textString, TextRenderState state, 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(); - + var advanceWidths = WrapInThousandthOfEmRealDoubles(renderingEmSize, glyphIndices.Select(c => glyphFace.AdvanceWidths[c] * renderingEmSize).ToArray()); if (characterInfos[0].DoesPositionX) { baselineOrigin.X = characterInfos[0].X; + newTextChunk = true; } if (characterInfos[0].DoesPositionY) { baselineOrigin.Y = characterInfos[0].Y; + newTextChunk = true; } + else if(textString.TextStyle.TextAlignment != currentTextAlignment) + { + newTextChunk = true; + } + + double baselineShift = 0; + baselineShift += BaselineHelper.EstimateBaselineShiftValue(textStyle, textString.Parent); + //if (textStyle.BaseLineShift == "sub") + //{ + // baselineShift += textStyle.FontSize * 0.5; /* * cap height ? fontSize*/ + //} + //else if (textStyle.BaseLineShift == "super") + //{ + // baselineShift -= textStyle.FontSize + (textStyle.FontSize * 0.25)/*font.CapsHeight * fontSize*/; + //} + + double totalWidth = advanceWidths.Sum(); + 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(); + glyphIndices, new Point(baselineOrigin.X, baselineOrigin.Y + baselineShift), advanceWidths, glyphOffsets, characters, deviceFontName, clusterMap, caretStops, language); + + var newX = baselineOrigin.X + totalWidth; var newY = baselineOrigin.Y ; newBaseline = new Point(newX, newY); return run; } - + + private static readonly Type _thousandthOfEmRealDoublesType = Type.GetType("MS.Internal.TextFormatting.ThousandthOfEmRealDoubles, PresentationCore, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"); + private static readonly ConstructorInfo _thousandthOfEmRealDoublesConstructor = _thousandthOfEmRealDoublesType?.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(double), typeof(IList)}, null); + + /// + /// Microsoft's GlyphRun converts the advance widths to thousandths of an em when using EndInit. + /// This wrapper method is used to compare apples to apples + /// + /// + /// + /// + private static IList WrapInThousandthOfEmRealDoubles(double renderingEmSize, IList advanceWidths) + { + return (IList)_thousandthOfEmRealDoublesConstructor?.Invoke(new object[] { renderingEmSize, advanceWidths }) ?? advanceWidths; + } private static void PopulateTextStrings(List textStrings, ITextNode node, TextStyleStack textStyleStacks) diff --git a/Source/SVGImage/SVG/Shapes/TextRenderBase.cs b/Source/SVGImage/SVG/Shapes/TextRenderBase.cs index ccec0cf..47c619b 100644 --- a/Source/SVGImage/SVG/Shapes/TextRenderBase.cs +++ b/Source/SVGImage/SVG/Shapes/TextRenderBase.cs @@ -7,14 +7,14 @@ 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) + "TSpanElement", typeof(TextShapeBase), typeof(DependencyObject)); + public static void SetElement(DependencyObject obj, TextShapeBase value) { obj.SetValue(TSpanElementProperty, value); } - public static ITextNode GetElement(DependencyObject obj) + public static TextShapeBase GetElement(DependencyObject obj) { - return (ITextNode)obj.GetValue(TSpanElementProperty); + return (TextShapeBase)obj.GetValue(TSpanElementProperty); } } diff --git a/Source/SVGImage/SVG/Shapes/TextShape.cs b/Source/SVGImage/SVG/Shapes/TextShape.cs index da28c7f..29e5b59 100644 --- a/Source/SVGImage/SVG/Shapes/TextShape.cs +++ b/Source/SVGImage/SVG/Shapes/TextShape.cs @@ -1,4 +1,5 @@ -using System.Xml; +using System.Linq; +using System.Xml; namespace SVGImage.SVG.Shapes { @@ -6,7 +7,87 @@ public class TextShape : TextShapeBase { public TextShape(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) { - + CollapseWhitespaceBetweenTextNodes(this); + } + private static bool EndsWithWhitespace(string text) + { + return !string.IsNullOrEmpty(text) && char.IsWhiteSpace(text[text.Length - 1]); + } + + private static bool StartsWithWhitespace(string text) + { + return !string.IsNullOrEmpty(text) && char.IsWhiteSpace(text[0]); + } + private static TextString GetFirstLeaf(ITextNode node) + { + if (node is TextString leaf) + { + return leaf; + } + + if (node is TextShapeBase container) + { + foreach (var child in container.Children) + { + var result = GetFirstLeaf(child); + if (!(result is null)) + { + return result; + } + } + } + + return null; + } + + private static TextString GetLastLeaf(ITextNode node) + { + if (node is TextString leaf) + { + return leaf; + } + + if (node is TextShapeBase container) + { + for (int i = container.Children.Count - 1; i >= 0; i--) + { + var result = GetLastLeaf(container.Children[i]); + if (!(result is null)) + { + return result; + } + } + } + + return null; + } + + private static void CollapseWhitespaceBetweenTextNodes(TextShape root) + { + TextString previousLeaf = null; + CollapseWhitespaceBetweenTextNodes(root, ref previousLeaf); + } + + private static void CollapseWhitespaceBetweenTextNodes(ITextNode node, ref TextString previousLeaf) + { + if (node is TextString currentLeaf) + { + if (!(previousLeaf is null) && + EndsWithWhitespace(previousLeaf.Text) && + StartsWithWhitespace(currentLeaf.Text)) + { + currentLeaf.Characters = currentLeaf.Characters.Skip(1).ToArray(); + } + + previousLeaf = currentLeaf; + } + else if (node is TextShapeBase container) + { + foreach (var child in container.Children) + { + CollapseWhitespaceBetweenTextNodes(child, ref previousLeaf); + } + } } } diff --git a/Source/SVGImage/SVG/Shapes/TextShapeBase.cs b/Source/SVGImage/SVG/Shapes/TextShapeBase.cs index 00e2053..43e5b7b 100644 --- a/Source/SVGImage/SVG/Shapes/TextShapeBase.cs +++ b/Source/SVGImage/SVG/Shapes/TextShapeBase.cs @@ -7,6 +7,7 @@ namespace SVGImage.SVG.Shapes using System.Linq; using System.Diagnostics; using System.Text; + using System.Text.RegularExpressions; [DebuggerDisplay("{DebugDisplayText}")] public class TextShapeBase: Shape, ITextNode @@ -39,7 +40,15 @@ private string GetDebugDisplayText(StringBuilder sb) return sb.ToString(); } - + + protected override Fill DefaultFill() + { + return Fill.CreateDefault(Svg, "black"); + } + protected override Stroke DefaultStroke() + { + return Stroke.CreateDefault(Svg, 0.1); + } public LengthPercentageOrNumberList X { get; protected set; } public LengthPercentageOrNumberList Y { get; protected set; } @@ -173,6 +182,7 @@ protected override void ParseAtStart(SVG svg, XmlNode node) ParseChildren(svg, node); } + private static readonly Regex _trimmedWhitespace = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.Singleline); protected void ParseChildren(SVG svg, XmlNode node) { @@ -180,7 +190,7 @@ protected void ParseChildren(SVG svg, XmlNode node) { if (child.NodeType == XmlNodeType.Text || child.NodeType == XmlNodeType.CDATA) { - var text = child.InnerText.Trim(); + var text = _trimmedWhitespace.Replace(child.InnerText, " "); if (!string.IsNullOrWhiteSpace(text)) { Children.Add(new TextString(this, text)); diff --git a/Source/SVGImage/SVG/Shapes/TextString.cs b/Source/SVGImage/SVG/Shapes/TextString.cs index 7b0af19..4dc907f 100644 --- a/Source/SVGImage/SVG/Shapes/TextString.cs +++ b/Source/SVGImage/SVG/Shapes/TextString.cs @@ -23,7 +23,7 @@ public class TextString : ITextChild public TextString(Shape parent, string text) { Parent = parent; - string trimmed = _trimmedWhitespace.Replace(text.Trim(), " "); + string trimmed = _trimmedWhitespace.Replace(text, " "); Characters = new CharacterLayout[trimmed.Length]; for(int i = 0; i < trimmed.Length; i++) { diff --git a/Source/SVGImage/SVG/TextRender2.cs b/Source/SVGImage/SVG/TextRender2.cs index b9289d9..537121d 100644 --- a/Source/SVGImage/SVG/TextRender2.cs +++ b/Source/SVGImage/SVG/TextRender2.cs @@ -51,7 +51,7 @@ static void BuildTextSpan(GeometryGroup gp, TextStyle textStyle, baseline -= tspan.TextStyle.FontSize + (textString.TextStyle.FontSize * 0.25)/*font.CapsHeight * fontSize*/; Geometry gm = BuildGlyphRun(textString.TextStyle, txt, x, baseline, ref totalwidth); - TextRender2.SetElement(gm, textString); + TextRender2.SetElement(gm, (TextShapeBase)textString.Parent); gp.Children.Add(gm); x += totalwidth; } diff --git a/Source/SVGImage/SVG/Utils/BaselineHelper.cs b/Source/SVGImage/SVG/Utils/BaselineHelper.cs new file mode 100644 index 0000000..0896a16 --- /dev/null +++ b/Source/SVGImage/SVG/Utils/BaselineHelper.cs @@ -0,0 +1,66 @@ +using SVGImage.SVG.Shapes; +using System; + +namespace SVGImage.SVG.Utils +{ + internal static class BaselineHelper + { + public static LengthPercentageOrNumber EstimateBaselineShift(Shape shape) + { + return EstimateBaselineShift(shape.TextStyle, shape); + } + /// + /// The purpose of this method is to allow TextStrings which are not shapes themselves to use the same logic as TextShapes to estimate the baseline shift. + /// They can use this method to estimate the baseline shift based on the TextStyle of the TextString's parent Shape. + /// + /// + /// + /// + public static LengthPercentageOrNumber EstimateBaselineShift(TextStyle textStyle, Shape shape) + { + if (String.IsNullOrEmpty( textStyle.BaseLineShift) || textStyle.BaseLineShift == "baseline") + { + return new LengthPercentageOrNumber(0d, new LengthContext(shape, LengthUnit.Number)); + } + else if (textStyle.BaseLineShift == "sub") + { + //Based on previous estimation + return new LengthPercentageOrNumber( textStyle.FontSize * 0.5, new LengthContext(shape, LengthUnit.Number)); + } + else if (textStyle.BaseLineShift == "super") + { + //Based on previous estimation + return new LengthPercentageOrNumber((-1) * (textStyle.FontSize + (textStyle.FontSize * 0.25)), new LengthContext(shape, LengthUnit.Number)); + } + else if(textStyle.BaseLineShift.EndsWith("%") && Double.TryParse(textStyle.BaseLineShift.Substring(0, textStyle.BaseLineShift.Length - 1), out double d)) + { + try + { + //The computed value of the property is this percentage multiplied by the computed "line-height" of the ‘text’ element. + //for the purposes of processing the ‘font’ property in SVG, 'line-height' is assumed to be equal the value for property ‘font-size’ + return new LengthPercentageOrNumber(d, new LengthContext(shape, LengthUnit.rem)); + } + catch + { + //Continue + } + } + try + { + return LengthPercentageOrNumber.Parse(shape, textStyle.BaseLineShift, LengthOrientation.Vertical); + } + catch + { + return new LengthPercentageOrNumber(0d, new LengthContext(shape, LengthUnit.Number)); + } + } + public static double EstimateBaselineShiftValue(Shape shape) + { + return EstimateBaselineShift(shape).Value; + } + public static double EstimateBaselineShiftValue(TextStyle textStyle, Shape shape) + { + return EstimateBaselineShift(textStyle, shape).Value; + } + } +} diff --git a/Source/SVGImage/SVG/Utils/DrawingGroupSerializer.cs b/Source/SVGImage/SVG/Utils/DrawingGroupSerializer.cs new file mode 100644 index 0000000..db0478e --- /dev/null +++ b/Source/SVGImage/SVG/Utils/DrawingGroupSerializer.cs @@ -0,0 +1,39 @@ +using System; +using System.Windows.Media; +using System.Windows.Markup; +using System.IO; +using System.Xml; + +namespace SVGImage.SVG.Utils +{ + internal static class DrawingGroupSerializer + { + public static string SerializeToXaml(DrawingGroup drawingGroup) + { + if (drawingGroup is null) + { + throw new ArgumentNullException(nameof(drawingGroup)); + } + + // Freezing can help ensure serialization works without exceptions + if (drawingGroup.CanFreeze && !drawingGroup.IsFrozen) + { + drawingGroup.Freeze(); + } + + var settings = new XmlWriterSettings + { + Indent = true, + IndentChars = " ", + OmitXmlDeclaration = true + }; + + using (var stringWriter = new StringWriter()) + using (var xmlWriter = XmlWriter.Create(stringWriter, settings)) + { + XamlWriter.Save(drawingGroup, xmlWriter); + return stringWriter.ToString(); + } + } + } +} diff --git a/Source/SVGImage/SVG/Utils/FontResolver.cs b/Source/SVGImage/SVG/Utils/FontResolver.cs index a64db0b..f753675 100644 --- a/Source/SVGImage/SVG/Utils/FontResolver.cs +++ b/Source/SVGImage/SVG/Utils/FontResolver.cs @@ -13,7 +13,7 @@ namespace SVGImage.SVG.Utils /// public class FontResolver { - private readonly ConcurrentDictionary _fontCache = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _fontCache = new ConcurrentDictionary(); private readonly Dictionary _availableFonts; private readonly Dictionary _normalizedFontNameMap; @@ -43,7 +43,7 @@ public FontResolver(int maxLevenshteinDistance = 0) /// /// Attempts to a font family based on the requested font name. /// - /// The name of the font to resolve. + /// The name of the font or fonts to resolve. Multiple fonts should be separated by commas /// /// A if a match is found, otherwise null. /// @@ -51,6 +51,27 @@ public FontResolver(int maxLevenshteinDistance = 0) /// Thrown when the requested font name is null or empty. /// public FontFamily ResolveFontFamily(string requestedFontName) + { + var fontList = requestedFontName + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(font => ResolveFontFamilyInternal(font.Trim())) + .Where(font => font != null); + + var resolvedFontNames = String.Join(",", fontList); + return new FontFamily(resolvedFontNames); + } + + /// + /// 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. + /// + private string ResolveFontFamilyInternal(string requestedFontName) { if (string.IsNullOrWhiteSpace(requestedFontName)) { @@ -65,8 +86,8 @@ public FontFamily ResolveFontFamily(string requestedFontName) // 1. Exact match if (_availableFonts.TryGetValue(requestedFontName, out var exactMatch)) { - _fontCache[requestedFontName] = exactMatch; - return exactMatch; + _fontCache[requestedFontName] = exactMatch.Source; + return exactMatch.Source; } // 2. Normalized match @@ -74,8 +95,8 @@ public FontFamily ResolveFontFamily(string requestedFontName) if (_normalizedFontNameMap.TryGetValue(normalizedRequested, out var normalizedMatchName) && _availableFonts.TryGetValue(normalizedMatchName, out var normalizedMatch)) { - _fontCache[requestedFontName] = normalizedMatch; - return normalizedMatch; + _fontCache[requestedFontName] = normalizedMatch.Source; + return normalizedMatch.Source; } // 3. Substring match @@ -83,8 +104,8 @@ public FontFamily ResolveFontFamily(string requestedFontName) .FirstOrDefault(kv => Normalize(kv.Key).Contains(normalizedRequested)); if (substringMatch.Value != null) { - _fontCache[requestedFontName] = substringMatch.Value; - return substringMatch.Value; + _fontCache[requestedFontName] = substringMatch.Value.Source; + return substringMatch.Value.Source; } // 4. Levenshtein match (optional but slow) @@ -102,16 +123,14 @@ public FontFamily ResolveFontFamily(string requestedFontName) if (bestMatch != null && bestMatch.Distance <= 4) { - _fontCache[requestedFontName] = bestMatch.Font; - return bestMatch.Font; + _fontCache[requestedFontName] = bestMatch.Font.Source; + return bestMatch.Font.Source; } } - - // 5. No match - _fontCache[requestedFontName] = null; - return null; + _fontCache[requestedFontName] = requestedFontName; + return requestedFontName; } /// diff --git a/Source/SVGImage/SVG/Utils/GlyphRunUtil.cs b/Source/SVGImage/SVG/Utils/GlyphRunUtil.cs new file mode 100644 index 0000000..7a17079 --- /dev/null +++ b/Source/SVGImage/SVG/Utils/GlyphRunUtil.cs @@ -0,0 +1,34 @@ +using System; +using System.Windows; +using System.Windows.Media; + +namespace SVGImage.SVG.Utils +{ + internal static class GlyphRunUtil + { + public static GlyphRun CreateOffsetRun(this GlyphRun value, double xOffset, double yOffset) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + return new GlyphRun( + value.GlyphTypeface, + value.BidiLevel, + value.IsSideways, + value.FontRenderingEmSize, +#if DPI_AWARE + value.PixelsPerDip, +#endif + value.GlyphIndices, + new Point( value.BaselineOrigin.X + xOffset, value.BaselineOrigin.Y + yOffset), + value.AdvanceWidths, + value.GlyphOffsets, + value.Characters, + value.DeviceFontName, + value.ClusterMap, + value.CaretStops, + value.Language); + } + } +} diff --git a/Source/SVGImage/SVG/Utils/MathUtils.cs b/Source/SVGImage/SVG/Utils/MathUtils.cs new file mode 100644 index 0000000..3e47909 --- /dev/null +++ b/Source/SVGImage/SVG/Utils/MathUtils.cs @@ -0,0 +1,40 @@ +using SVGImage.SVG.Shapes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Media; + +namespace SVGImage.SVG.Utils +{ + internal static class MathUtil + { + public static bool IsNearlyZero(this double value, double epsilon = Double.Epsilon) + { + return Math.Abs(value) < epsilon; + } + public static bool IsNearlyEqual(this double a, double b, double epsilon = Double.Epsilon) + { + return Math.Abs(a - b) < epsilon; + } + public static int Clamp(int value, int min, int max) + { + if (min > max) + { + throw new ArgumentException("min must be less than or equal to max", nameof(min)); + } + + if (value < min) + { + return min; + } + else if (value > max) + { + return max; + } + + return value; + } + } +} diff --git a/Source/SVGImage/SVG/Utils/SubAndSuperScriptHelper.cs b/Source/SVGImage/SVG/Utils/SubAndSuperScriptHelper.cs new file mode 100644 index 0000000..1735c13 --- /dev/null +++ b/Source/SVGImage/SVG/Utils/SubAndSuperScriptHelper.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Text; + +namespace SVGImage.SVG.Utils +{ + internal static class SubAndSuperScriptHelper + { + private static readonly Dictionary _subscriptMap = new Dictionary + { + {'0', '₀'}, {'1', '₁'}, {'2', '₂'}, {'3', '₃'}, {'4', '₄'}, + {'5', '₅'}, {'6', '₆'}, {'7', '₇'}, {'8', '₈'}, {'9', '₉'}, + {'+', '₊'}, {'-', '₋'}, {'=', '₌'} + }; + private static readonly Dictionary _superscriptMap = new Dictionary + { + {'0', '⁰'}, {'1', '¹'}, {'2', '²'}, {'3', '³'}, {'4', '⁴'}, + {'5', '⁵'}, {'6', '⁶'}, {'7', '⁷'}, {'8', '⁸'}, {'9', '⁹'}, + {'+', '⁺'}, {'-', '⁻'}, {'=', '⁼'} + }; + public static string ToSubscript(this string input) + { + return Convert(input, _subscriptMap); + } + public static string ToSuperscript(this string input) + { + return Convert(input, _superscriptMap); + } + + private static string Convert(string input, Dictionary map) + { + var sb = new StringBuilder(); + foreach (var c in input) + { + sb.Append(map.TryGetValue(c, out var mappedChar) ? mappedChar : c); + } + return sb.ToString(); + } + } +}