Skip to content

Commit 49bab6d

Browse files
committed
Fixed some rendering issues with GDI+ and Direct2D renderers.
1 parent 4f97bdd commit 49bab6d

File tree

6 files changed

+132
-83
lines changed

6 files changed

+132
-83
lines changed

SvgFileType/Extensions/SvgElementExtensions.cs

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
// Use of this source code is governed by GNU General Public License (GPL-2.0) that can be found in the COPYING file.
33

44
using System;
5+
using System.Globalization;
56
using System.IO;
67
using System.Reflection;
78
using System.Text;
9+
using System.Xml;
810
using Svg;
911

1012
namespace SvgFileTypePlugin.Extensions;
@@ -51,16 +53,29 @@ public static string GetXML_QuotedFuncIRIHack(this SvgElement svg)
5153
{
5254
ArgumentNullException.ThrowIfNull(svg);
5355

54-
return svg.GetXML().Replace(""", string.Empty);
56+
using InvariantUtf8StringWriter writer = new InvariantUtf8StringWriter();
57+
XmlWriterSettings xmlWriterSettings = new() { Encoding = Encoding.UTF8 };
58+
using (XmlWriter xmlWriter = new CustomXmlWriter(XmlWriter.Create(writer, xmlWriterSettings)))
59+
{
60+
svg.Write(xmlWriter);
61+
xmlWriter.Flush();
62+
}
63+
writer.Flush();
64+
return writer.ToString();
5565
}
5666

57-
public static Stream GetXMLAsStream(this SvgElement svg)
67+
public static void WriteXML_QuotedFuncIRIHack(this SvgElement svg, Stream output)
5868
{
5969
ArgumentNullException.ThrowIfNull(svg);
6070

61-
string xml = svg.GetXML_QuotedFuncIRIHack();
62-
byte[] bytes = Encoding.UTF8.GetBytes(xml);
63-
return new MemoryStream(bytes);
71+
using InvariantUtf8StreamWriter writer = new InvariantUtf8StreamWriter(output);
72+
XmlWriterSettings xmlWriterSettings = new() { Encoding = Encoding.UTF8 };
73+
using (XmlWriter xmlWriter = new CustomXmlWriter(XmlWriter.Create(writer, xmlWriterSettings)))
74+
{
75+
svg.Write(xmlWriter);
76+
xmlWriter.Flush();
77+
}
78+
writer.Flush();
6479
}
6580

6681
private static T CreateGetterDelegate<T>(string propertyName) where T : Delegate
@@ -71,4 +86,56 @@ private static T CreateGetterDelegate<T>(string propertyName) where T : Delegate
7186
?? throw new MissingMemberException(nameof(SvgElement), propertyName);
7287
return getter.CreateDelegate<T>();
7388
}
89+
90+
private sealed class InvariantUtf8StreamWriter(Stream stream) : StreamWriter(stream, Encoding.UTF8, leaveOpen: true)
91+
{
92+
private readonly Stream stream = stream;
93+
94+
public override Encoding Encoding => Encoding.UTF8;
95+
public override IFormatProvider FormatProvider => CultureInfo.InvariantCulture;
96+
97+
protected override void Dispose(bool disposing)
98+
{
99+
base.Dispose(disposing);
100+
if (disposing)
101+
stream.Position = 0;
102+
}
103+
}
104+
105+
private sealed class InvariantUtf8StringWriter() : StringWriter(CultureInfo.InvariantCulture)
106+
{
107+
public override Encoding Encoding => Encoding.UTF8;
108+
}
109+
110+
private sealed class CustomXmlWriter(XmlWriter writer) : XmlWriter
111+
{
112+
private readonly XmlWriter writer = writer;
113+
public override WriteState WriteState => writer.WriteState;
114+
public override void Flush() => writer.Flush();
115+
public override string? LookupPrefix(string ns) => writer.LookupPrefix(ns);
116+
public override void WriteBase64(byte[] buffer, int index, int count) => writer.WriteBase64(buffer, index, count);
117+
public override void WriteCData(string? text) => writer.WriteCData(text);
118+
public override void WriteCharEntity(char ch) => writer.WriteCharEntity(ch);
119+
public override void WriteChars(char[] buffer, int index, int count) => writer.WriteChars(buffer, index, count);
120+
public override void WriteComment(string? text) => writer.WriteComment(text);
121+
public override void WriteDocType(string name, string? pubid, string? sysid, string? subset) => writer.WriteDocType(name, pubid, sysid, subset);
122+
public override void WriteEndAttribute() => writer.WriteEndAttribute();
123+
public override void WriteEndDocument() => writer.WriteEndDocument();
124+
public override void WriteEndElement() => writer.WriteEndElement();
125+
public override void WriteEntityRef(string name) => writer.WriteEntityRef(name);
126+
public override void WriteFullEndElement() => writer.WriteFullEndElement();
127+
public override void WriteProcessingInstruction(string name, string? text) => writer.WriteProcessingInstruction(name, text);
128+
public override void WriteRaw(char[] buffer, int index, int count) => writer.WriteRaw(buffer, index, count);
129+
public override void WriteRaw(string data) => writer.WriteRaw(data);
130+
public override void WriteStartAttribute(string? prefix, string localName, string? ns) => writer.WriteStartAttribute(prefix, localName, ns);
131+
public override void WriteStartDocument() => writer.WriteStartDocument();
132+
public override void WriteStartDocument(bool standalone) => writer.WriteStartDocument(standalone);
133+
public override void WriteStartElement(string? prefix, string localName, string? ns) => writer.WriteStartElement(prefix, localName, ns);
134+
public override void WriteString(string? text)
135+
{
136+
writer.WriteString(text?.Replace("\"", string.Empty).Replace("\'", string.Empty));
137+
}
138+
public override void WriteSurrogateCharEntity(char lowChar, char highChar) => writer.WriteSurrogateCharEntity(lowChar, highChar);
139+
public override void WriteWhitespace(string? ws) => writer.WriteWhitespace(ws);
140+
}
74141
}

SvgFileType/Extensions/SvgUseExtensions.cs

Lines changed: 34 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -10,77 +10,49 @@ namespace SvgFileTypePlugin.Extensions;
1010

1111
internal static class SvgUseExtensions
1212
{
13-
// Most attributes on use do not override those already on the element
14-
// referenced by use. (This differs from how CSS style attributes override
15-
// those set 'earlier' in the cascade). Only the attributes x, y, width,
16-
// height and href on the use element will override those set on the
17-
// referenced element.However, any other attributes not set on the referenced
18-
// element will be applied to the use element.
19-
private static readonly HashSet<string> UseOverrides = new(StringComparer.OrdinalIgnoreCase)
20-
{ "x", "y", "width", "height", "href", "xlink:href" };
21-
22-
public static SvgElement? CopyReferencedRootElement(this SvgUse use)
13+
public static SvgElement? GetCopyOfReferencedElement(this SvgUse use)
2314
{
2415
ArgumentNullException.ThrowIfNull(use);
16+
if (use.OwnerDocument is null)
17+
throw new ArgumentException("Use element does not have owner document.", nameof(use));
2518

26-
SvgElement? refElem = GetReferencedElement(use);
27-
if (refElem == null)
28-
return null;
2919
List<SvgTransform> transforms = [];
30-
while (refElem is SvgUse use2)
31-
{
32-
AddTransforms(refElem.Transforms);
33-
refElem = GetReferencedElement(use2);
34-
}
35-
if (refElem == null)
36-
{
37-
transforms.Clear();
38-
return null;
39-
}
40-
AddTransforms(use.Transforms);
41-
SvgElement copy = refElem.DeepCopy();
42-
copy.Transforms ??= [];
43-
copy.Transforms.AddRange(transforms);
44-
CopyAttributes(use, copy);
45-
return copy;
46-
47-
void AddTransforms(SvgTransformCollection col)
48-
{
49-
if (col != null && col.Count > 0)
50-
transforms.AddRange(col);
51-
}
52-
53-
SvgElement? GetReferencedElement(SvgUse use)
20+
SvgElement? referenced = use;
21+
while (referenced is SvgUse parentUse && parentUse?.ReferencedElement is Uri uri)
5422
{
55-
if (use.OwnerDocument == null) { throw new InvalidOperationException("use element doesn't have an owner document."); }
56-
if (use == null)
57-
return null;
58-
Uri uri = use.ReferencedElement;
59-
if (uri == null)
23+
string href = uri.ToString().TrimStart('#');
24+
if (href.Length < 1)
6025
return null;
61-
string id = uri.ToString().Trim();
62-
if (id.Length < 2 || id == null)
63-
return null;
64-
id = id[1..];
65-
return use.OwnerDocument.GetElementById(id);
26+
if (parentUse.Transforms is not null && parentUse.Transforms.Count > 0)
27+
transforms.AddRange(parentUse.Transforms);
28+
referenced = use.OwnerDocument.GetElementById(href);
6629
}
30+
if (referenced is null)
31+
return null;
32+
SvgElement copied = referenced.DeepCopy();
33+
use.CopyOverridedAttributes(copied);
34+
copied.Transforms ??= [];
35+
copied.Transforms.AddRange(transforms);
36+
return copied;
37+
}
6738

68-
void CopyAttributes(SvgUse fromUse, SvgElement toElem)
69-
{
70-
SvgAttributeCollection? refAttrs = toElem.GetAttributes();
71-
SvgAttributeCollection? fromUseAttrs = fromUse.GetAttributes();
39+
private static void CopyOverridedAttributes(this SvgUse use, SvgElement target)
40+
{
41+
SvgAttributeCollection targetAttributes = target.GetAttributes();
42+
SvgAttributeCollection sourceAttributes = use.GetAttributes();
7243

73-
if (fromUseAttrs is not null && refAttrs is not null)
74-
{
75-
foreach (KeyValuePair<string, object> useAttr in fromUseAttrs)
76-
{
77-
string key = useAttr.Key;
78-
if (UseOverrides.Contains(key))
79-
{
80-
refAttrs[key] = useAttr.Value;
81-
}
82-
}
83-
}
44+
foreach (KeyValuePair<string, object> attribute in sourceAttributes)
45+
{
46+
string key = attribute.Key.ToLowerInvariant();
47+
48+
// Most attributes on use do not override those already on the element
49+
// referenced by use. (This differs from how CSS style attributes override
50+
// those set 'earlier' in the cascade). Only the attributes x, y, width,
51+
// height and href on the use element will override those set on the
52+
// referenced element.However, any other attributes not set on the referenced
53+
// element will be applied to the use element.
54+
if (key is "x" or "y" or "width" or "height" or "href" or "xlink:href")
55+
targetAttributes[key] = attribute.Value;
8456
}
8557
}
8658
}

SvgFileType/Import/Direct2DSvgRenderer.cs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,15 @@ protected override Document GetFlatDocument(string svgdata, SvgImportConfig conf
3535
try
3636
{
3737
SvgDocument svgdoc = SvgDocument.FromSvg<SvgDocument>(svgdata);
38-
using IDisposable _ = svgdoc.UseSetRasterDimensions(config);
39-
40-
SizeFloat viewport = new SizeFloat(width, height);
4138
IDirect2DFactory d2d = Services.Get<IDirect2DFactory>(); // Don't dispose this! It's singleton.
4239
using IBitmap<ColorBgra32> sbitmap = surface.CreateSharedBitmap();
4340
using IBitmap<ColorPbgra32> pbitmap = sbitmap.CreatePremultipliedAdapter(PremultipliedAdapterOptions.UnPremultiplyOnDispose);
4441
using IDeviceContext dc = d2d.CreateBitmapDeviceContext(pbitmap);
45-
using Stream stream = svgdoc.GetXMLAsStream();
46-
using ISvgDocument svg = dc.CreateSvgDocument(stream, viewport);
42+
using MemoryStream stream = new MemoryStream();
43+
svgdoc.WriteXML_QuotedFuncIRIHack(stream);
44+
using ISvgDocument svg = dc.CreateSvgDocument(stream, viewportSize: new SizeFloat(width, height));
4745
using DrawingScope _1 = dc.UseBeginDraw();
46+
dc.Transform = CalculateTransform(svgsize: new SizeF(svgdoc.Width, svgdoc.Height), config);
4847
dc.Clear();
4948
dc.DrawSvgDocument(svg);
5049
}
@@ -190,11 +189,26 @@ private void RenderSvgDocument(SvgElement element, IDeviceContext dc)
190189
else
191190
{
192191
SvgDocument clone = element.OwnerDocument.RemoveInvisibleAndNonTextElements();
193-
using Stream stream = clone.GetXMLAsStream();
192+
using MemoryStream stream = new MemoryStream();
193+
clone.WriteXML_QuotedFuncIRIHack(stream);
194194
using ISvgDocument partial = dc.CreateSvgDocument(stream, dc.Size);
195195
using DrawingScope _ = dc.UseBeginDraw();
196196
dc.Clear();
197197
dc.DrawSvgDocument(partial);
198198
}
199199
}
200+
201+
private static Matrix3x2Float CalculateTransform(SizeF svgsize, SvgImportConfig config, float tolerance = 1f)
202+
{
203+
float ratioX, ratioY;
204+
ratioX = config.RasterWidth / svgsize.Width * tolerance;
205+
ratioY = config.PreserveAspectRatio
206+
? ratioX
207+
: config.RasterHeight / svgsize.Height * tolerance;
208+
return new Matrix3x2Float()
209+
{
210+
M11 = ratioX,
211+
M22 = ratioY
212+
};
213+
}
200214
}

SvgFileType/Import/GdipSvgRenderer.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ protected override Document GetFlatDocument(string svgdata, SvgImportConfig conf
2727
using MemoryFailPoint _1 = GetMemoryFailPoint(config.RasterWidth, config.RasterHeight, 2);
2828

2929
ResetProgress(elements.Count);
30-
//ResetProgress(1);
3130
using Bitmap bmp = new Bitmap(config.RasterWidth, config.RasterHeight);
3231
using (Graphics g = Graphics.FromImage(bmp))
3332
{
@@ -43,8 +42,6 @@ protected override Document GetFlatDocument(string svgdata, SvgImportConfig conf
4342
RenderSvgDocument(element, g, config);
4443
IncrementProgress();
4544
}
46-
//RenderSvgDocument(svg, g, config);
47-
//IncrementProgress();
4845
}
4946
Document document = Document.FromImage(bmp);
5047
document.SetDpi(config.Ppi);
@@ -145,7 +142,6 @@ private void RenderSvgDocument(SvgElement element, Graphics graphics, SvgImportC
145142
else
146143
{
147144
SvgDocument clone = element.OwnerDocument.RemoveInvisibleAndNonTextElements();
148-
using IDisposable _ = clone.UseSetRasterDimensions(config);
149145
clone.Draw(graphics);
150146
}
151147
}

SvgFileType/Import/SvgImport.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public static Document Load(Stream stream)
2525

2626
// We are using resvg which gives the best result in my tests.
2727
// For testing purposes, I also have implemented GDI+ and Direct2D based SVG renderers as well.
28+
// Renderers: resvg, gdip or gdiplus or gdi+, direct2d or d2d
2829
const string rendererName = "resvg";
2930

3031
string svgdata = Open(stream);

SvgFileType/Import/SvgRenderer2.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,19 @@ protected virtual void RenderSvgUseElement(SvgUse use, Action<SvgElement> render
7474
ArgumentNullException.ThrowIfNull(use);
7575
ArgumentNullException.ThrowIfNull(renderAction);
7676

77-
SvgElement? referencedRoot = use.CopyReferencedRootElement();
78-
if (referencedRoot is null)
77+
if (use.GetCopyOfReferencedElement() is not SvgElement referencedElement)
7978
return;
80-
referencedRoot.Visibility = "visible";
79+
referencedElement.Visibility = "visible";
8180
use.Visibility = "hidden";
8281
SvgElementCollection children = use.Parent.Children;
83-
children.AddAndForceUniqueID(referencedRoot);
82+
children.AddAndForceUniqueID(referencedElement);
8483
try
8584
{
86-
renderAction(referencedRoot);
85+
renderAction(referencedElement);
8786
}
8887
finally
8988
{
90-
children.Remove(referencedRoot);
89+
children.Remove(referencedElement);
9190
}
9291
}
9392

0 commit comments

Comments
 (0)