Skip to content

Commit 8a0fbcf

Browse files
authored
Feat: Add WinUIEssential.Svg nuget to support advanced svg rendering
* Test adding rust resvg library and a dedicated project * Finished svg render component backend * Finished svg sample code * Add SvgImageSource to readme
1 parent 55b799c commit 8a0fbcf

29 files changed

Lines changed: 1799 additions & 1 deletion

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,4 @@ coverage
433433
*.tsbuildinfo
434434
vcpkg_installed
435435
/.claude/settings.local.json
436+
*.exe

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ For UWP: [![](https://img.shields.io/nuget/v/WinUIEssential.UWP?label=WinUIEssen
88

99
For WinUI3 (Windows App SDK): [![](https://img.shields.io/nuget/v/WinUIEssential.WinUI3?label=WinUIEssential.WinUI3)](https://www.nuget.org/packages/WinUIEssential.WinUI3)
1010

11+
WinUI3 Svg: [![](https://img.shields.io/nuget/v/WinUIEssential.WinUI3.Svg?label=WinUIEssential.WinUI3.Svg)](https://www.nuget.org/packages/WinUIEssential.WinUI3.Svg)
12+
1113

1214
## Example Gallery
1315
[Download from Microsoft Store!](https://apps.microsoft.com/detail/9PCC690BCMT9?hl=en-us&gl=US&ocid=pdpshare)
@@ -130,6 +132,7 @@ You can reference Github Action for detailed build steps.
130132
|RevealFocusPanel | :x: | :white_check_mark: | Control
131133
|TenMica | :x: | :white_check_mark: | WinRT component
132134
|WindowedContentDialog | :x: | :white_check_mark: | Control
135+
|SvgImageSource | :x: | :white_check_mark: | WinRT component
133136

134137
*means additional settings required, see the sections for info
135138

@@ -1197,4 +1200,27 @@ Windows.Foundation.IAsyncOperation<Microsoft.UI.Xaml.Controls.ContentDialogResul
11971200

11981201
|Smoke|Blur|
11991202
|-----|----|
1200-
|![](assets/windowed-dialog-smoke.png)|![](assets/windowed-dialog-blur.png)
1203+
|![](assets/windowed-dialog-smoke.png)|![](assets/windowed-dialog-blur.png)
1204+
1205+
## SvgImageSource
1206+
**This component is in a dedicated `WinUIEssential.WinUI3.Svg` package.**
1207+
1208+
This component is based on [the great resvg project](https://github.com/linebender/resvg).
1209+
It is meant to be a drop-in replacement for the WinUI3's built-in `SvgImageSource`, but offers much better rendering capabilities.
1210+
The WinUI3's built-in `SvgImageSource` has these [known limitations](https://learn.microsoft.com/en-us/windows/win32/direct2d/svg-support), one of the most limiting one being:
1211+
**unable to render `<text>`**.
1212+
1213+
Using this component, you get the majority of svg1.1 features supported (download the WinEssentials.WinUI3Example app from the store and see for yourself), plus easier sizing control,
1214+
supporting binding the rasterization size directly with the `Image` control.
1215+
1216+
Example usage:
1217+
```xml
1218+
<Image x:Name="SvgUrlImage">
1219+
<Image.Source>
1220+
<svg:SvgImageSource BindSizeTo="{x:Bind SvgUrlImage}" UriSource="https://img.shields.io/nuget/v/WinUIEssential.WinUI3?label=WinUIEssential.WinUI3" />
1221+
</Image.Source>
1222+
</Image>
1223+
```
1224+
1225+
![](assets/svg1.png)
1226+
![](assets/svg-compare.png)

SharedExampleComponent/SharedExampleComponent.vcxitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<ClInclude Include="$(MSBuildThisFileDirectory)GithubRequest.h" />
3939
<ClInclude Include="$(MSBuildThisFileDirectory)NamedUrl.h" />
4040
<ClInclude Include="$(MSBuildThisFileDirectory)RepoInfo.h" />
41+
<ClInclude Include="$(MSBuildThisFileDirectory)TestSvgUrls.hpp" />
4142
<ClInclude Include="$(MSBuildThisFileDirectory)ThemeToggleButton.xaml.h" />
4243
</ItemGroup>
4344
<ItemGroup>

SharedExampleComponent/SharedExampleComponent.vcxitems.filters

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
<ClInclude Include="$(MSBuildThisFileDirectory)ThemeToggleButton.xaml.h">
5858
<Filter>ThemeToggleButton</Filter>
5959
</ClInclude>
60+
<ClInclude Include="$(MSBuildThisFileDirectory)TestSvgUrls.hpp" />
6061
</ItemGroup>
6162
<ItemGroup>
6263
<Midl Include="$(MSBuildThisFileDirectory)Editor.idl">

SharedExampleComponent/TestSvgUrls.hpp

Lines changed: 441 additions & 0 deletions
Large diffs are not rendered by default.

WinUI3Example/MainWindow.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,7 @@
867867
<NavigationViewItem Content="Toasts" Tag="{essential:TypeName Type=local:ToastPage}" />
868868
<NavigationViewItem Content="IInitializeWithWindowHelper" Tag="{essential:TypeName Type=local:InitializeWithWindowHelperPage}" />
869869
<NavigationViewItem Content="ScopedButtonDisabler" Tag="{essential:TypeName Type=local:ScopedButtonDisablerPage}" />
870+
<NavigationViewItem Content="Svg" Tag="{essential:TypeName Type=local:SvgPage}" />
870871
<NavigationViewItem Content="*Experiments*" Tag="{essential:TypeName Type=local:ExperimentPage}" />
871872
</NavigationView.MenuItems>
872873
<Grid>

WinUI3Example/SvgPage.idl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace WinUI3Example
2+
{
3+
[default_interface]
4+
runtimeclass SvgPage : Microsoft.UI.Xaml.Controls.Page
5+
{
6+
SvgPage();
7+
Windows.Foundation.Collections.IVector<Object> Svgs{ get; };
8+
9+
static Windows.Foundation.Uri GetPngFromSvgUrl(Object svgUrl);
10+
static Microsoft.UI.Xaml.Media.ImageSource GetSvgImageSourceFromString(String svgContent);
11+
}
12+
}

WinUI3Example/SvgPage.xaml

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<Page
3+
x:Class="WinUI3Example.SvgPage"
4+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
5+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
6+
xmlns:Foundation="using:Windows.Foundation"
7+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
8+
xmlns:essential="using:WinUI3Package"
9+
xmlns:local="using:WinUI3Example"
10+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
11+
xmlns:svg="using:WinUI3Package.Svg"
12+
mc:Ignorable="d">
13+
14+
<StackPanel Style="{StaticResource ControlExamplesStackPanelStyle}">
15+
<local:ControlExample HeaderText="Paste in svg urls">
16+
<Grid ColumnSpacing="8">
17+
<Grid.ColumnDefinitions>
18+
<ColumnDefinition Width="*" />
19+
<ColumnDefinition Width="*" />
20+
</Grid.ColumnDefinitions>
21+
22+
<essential:GroupBox Header="WinUIEssential.Svg">
23+
<Image
24+
x:Name="SvgUrlImage"
25+
MinWidth="500"
26+
MinHeight="500">
27+
<Image.Source>
28+
<svg:SvgImageSource BindSizeTo="{x:Bind SvgUrlImage}" UriSource="{x:Bind SvgUrl.Text, Mode=OneWay}" />
29+
</Image.Source>
30+
</Image>
31+
</essential:GroupBox>
32+
33+
<essential:GroupBox Grid.Column="1" Header="WinUI3 Image">
34+
<Image MinWidth="500" MinHeight="500">
35+
<Image.Source>
36+
<SvgImageSource UriSource="{x:Bind SvgUrl.Text, Mode=OneWay}" />
37+
</Image.Source>
38+
</Image>
39+
</essential:GroupBox>
40+
</Grid>
41+
<local:ControlExample.Options>
42+
<TextBox
43+
x:Name="SvgUrl"
44+
Width="400"
45+
PlaceholderText="Paste svg url here to test the rendering effect"
46+
Text="https://img.shields.io/nuget/v/WinUIEssential.WinUI3?label=WinUIEssential.WinUI3"
47+
TextWrapping="Wrap" />
48+
</local:ControlExample.Options>
49+
50+
<local:ControlExample.Xaml>
51+
<local:CodeSource>
52+
<local:CodeSource.Code>
53+
<x:String xml:space="preserve">xmlns:svg=&quot;using:WinUI3Package.Svg&quot;
54+
&lt;Image
55+
x:Name=&quot;SvgUrlImage&quot;
56+
MinWidth=&quot;500&quot;
57+
MinHeight=&quot;500&quot;&gt;
58+
&lt;Image.Source&gt;
59+
&lt;svg:SvgImageSource BindSizeTo=&quot;{x:Bind SvgUrlImage}&quot; UriSource=&quot;https://img.shields.io/nuget/v/WinUIEssential.WinUI3?label=WinUIEssential.WinUI3 /&gt;
60+
&lt;/Image.Source&gt;
61+
&lt;/Image&gt;
62+
63+
&lt;!-- Drop-in replacement for winui3&apos;s built-in SvgImageSource --&gt;
64+
&lt;Image MinWidth=&quot;500&quot; MinHeight=&quot;500&quot;&gt;
65+
&lt;Image.Source&gt;
66+
&lt;SvgImageSource UriSource=&quot;https://img.shields.io/nuget/v/WinUIEssential.WinUI3?label=WinUIEssential.WinUI3 /&gt;
67+
&lt;/Image.Source&gt;
68+
&lt;/Image&gt;</x:String>
69+
</local:CodeSource.Code>
70+
</local:CodeSource>
71+
</local:ControlExample.Xaml>
72+
</local:ControlExample>
73+
74+
<local:ControlExample HeaderText="Paste in svg strings">
75+
<Grid ColumnSpacing="8">
76+
<Grid.ColumnDefinitions>
77+
<ColumnDefinition Width="*" />
78+
<ColumnDefinition Width="*" />
79+
</Grid.ColumnDefinitions>
80+
81+
<essential:GroupBox Header="WinUI3Essential.Svg">
82+
<Image
83+
x:Name="SvgStringImage"
84+
MinWidth="500"
85+
MinHeight="500">
86+
<Image.Source>
87+
<svg:SvgImageSource BindSizeTo="{x:Bind SvgStringImage}" StringSource="{x:Bind SvgString.Text, Mode=OneWay}" />
88+
</Image.Source>
89+
</Image>
90+
</essential:GroupBox>
91+
92+
<essential:GroupBox Grid.Column="1" Header="WinUI3 Image">
93+
<!-- Native SvgImageSource does not support rendering from a string, you have to use a Converter or functional binding -->
94+
<Image
95+
MinWidth="500"
96+
MinHeight="500"
97+
Source="{x:Bind local:SvgPage.GetSvgImageSourceFromString(SvgString.Text), Mode=OneWay}" />
98+
</essential:GroupBox>
99+
</Grid>
100+
101+
102+
<local:ControlExample.Options>
103+
<TextBox
104+
x:Name="SvgString"
105+
Width="400"
106+
AcceptsReturn="True"
107+
PlaceholderText="Paste svg string here to test the rendering effect"
108+
TextWrapping="Wrap">
109+
<TextBox.Text>
110+
<x:String xml:space="preserve">&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;168&quot; height=&quot;20&quot; role=&quot;img&quot; aria-label=&quot;WinUIEssential.UWP: v1.3.3&quot;&gt;
111+
&lt;title&gt;WinUIEssential.UWP: v1.3.3&lt;/title&gt;
112+
&lt;filter id=&quot;blur&quot;&gt;
113+
&lt;feGaussianBlur in=&quot;SourceGraphic&quot; stdDeviation=&quot;16&quot;/&gt;
114+
&lt;/filter&gt;
115+
&lt;linearGradient id=&quot;s&quot; x2=&quot;0&quot; y2=&quot;100%&quot;&gt;
116+
&lt;stop offset=&quot;0&quot; stop-color=&quot;#bbb&quot; stop-opacity=&quot;.1&quot;/&gt;
117+
&lt;stop offset=&quot;1&quot; stop-opacity=&quot;.1&quot;/&gt;
118+
&lt;/linearGradient&gt;
119+
&lt;clipPath id=&quot;r&quot;&gt;
120+
&lt;rect width=&quot;168&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;#fff&quot;/&gt;
121+
&lt;/clipPath&gt;
122+
&lt;g clip-path=&quot;url(#r)&quot;&gt;
123+
&lt;rect width=&quot;123&quot; height=&quot;20&quot; fill=&quot;#555&quot;/&gt;
124+
&lt;rect x=&quot;123&quot; width=&quot;45&quot; height=&quot;20&quot; fill=&quot;#007ec6&quot;/&gt;
125+
&lt;rect width=&quot;168&quot; height=&quot;20&quot; fill=&quot;url(#s)&quot;/&gt;
126+
&lt;/g&gt;
127+
&lt;g fill=&quot;#fff&quot; text-anchor=&quot;middle&quot; font-family=&quot;Verdana,Geneva,DejaVu Sans,sans-serif&quot; text-rendering=&quot;geometricPrecision&quot; font-size=&quot;110&quot;&gt;
128+
&lt;text aria-hidden=&quot;true&quot; x=&quot;625&quot; y=&quot;150&quot; fill=&quot;#010101&quot; fill-opacity=&quot;.80&quot; filter=&quot;url(#blur)&quot; transform=&quot;scale(.1)&quot; textLength=&quot;1130&quot;&gt;WinUIEssential.UWP&lt;/text&gt;
129+
&lt;text aria-hidden=&quot;true&quot; x=&quot;625&quot; y=&quot;150&quot; fill=&quot;#010101&quot; fill-opacity=&quot;.3&quot; transform=&quot;scale(.1)&quot; textLength=&quot;1130&quot;&gt;WinUIEssential.UWP&lt;/text&gt;
130+
&lt;text x=&quot;625&quot; y=&quot;140&quot; transform=&quot;scale(.1)&quot; fill=&quot;#fff&quot; textLength=&quot;1130&quot;&gt;WinUIEssential.UWP&lt;/text&gt;
131+
&lt;text aria-hidden=&quot;true&quot; x=&quot;1445&quot; y=&quot;150&quot; fill=&quot;#010101&quot; fill-opacity=&quot;.80&quot; filter=&quot;url(#blur)&quot; transform=&quot;scale(.1)&quot; textLength=&quot;350&quot;&gt;v1.3.3&lt;/text&gt;
132+
&lt;text aria-hidden=&quot;true&quot; x=&quot;1445&quot; y=&quot;150&quot; fill=&quot;#010101&quot; fill-opacity=&quot;.3&quot; transform=&quot;scale(.1)&quot; textLength=&quot;350&quot;&gt;v1.3.3&lt;/text&gt;
133+
&lt;text x=&quot;1445&quot; y=&quot;140&quot; transform=&quot;scale(.1)&quot; fill=&quot;#fff&quot; textLength=&quot;350&quot;&gt;v1.3.3&lt;/text&gt;
134+
&lt;/g&gt;
135+
&lt;/svg&gt;</x:String>
136+
</TextBox.Text>
137+
</TextBox>
138+
</local:ControlExample.Options>
139+
140+
<local:ControlExample.Xaml>
141+
<local:CodeSource>
142+
<local:CodeSource.Code>
143+
<x:String xml:space="preserve">xmlns:svg=&quot;using:WinUI3Package.Svg&quot;
144+
&lt;!-- Binding to a svg string is also supported --&gt;
145+
&lt;Image
146+
x:Name=&quot;SvgStringImage&quot;
147+
MinWidth=&quot;500&quot;
148+
MinHeight=&quot;500&quot;&gt;
149+
&lt;Image.Source&gt;
150+
&lt;svg:SvgImageSource BindSizeTo=&quot;{x:Bind SvgStringImage}&quot; StringSource=&quot;{x:Bind SvgString.Text, Mode=OneWay}&quot; /&gt;
151+
&lt;/Image.Source&gt;
152+
&lt;/Image&gt;</x:String>
153+
</local:CodeSource.Code>
154+
</local:CodeSource>
155+
</local:ControlExample.Xaml>
156+
</local:ControlExample>
157+
158+
<local:ControlExample HeaderText="WinUI3Package.Svg vs WinUI3 built-in Image vs expected png result">
159+
160+
<ListView
161+
x:Name="SvgListView"
162+
Grid.Column="0"
163+
MaxHeight="3000"
164+
ItemsSource="{x:Bind Svgs}"
165+
Tag="Svg">
166+
<ListView.ItemTemplate>
167+
<DataTemplate x:DataType="Foundation:Uri">
168+
<StackPanel Orientation="Horizontal" Spacing="8">
169+
<StackPanel Spacing="8">
170+
<Image
171+
Width="200"
172+
Height="200"
173+
Stretch="Uniform">
174+
<Image.Source>
175+
<svg:SvgImageSource UriSource="{Binding}" />
176+
</Image.Source>
177+
</Image>
178+
<TextBlock Text="{Binding}" />
179+
</StackPanel>
180+
181+
<StackPanel Spacing="8">
182+
<Image
183+
Width="200"
184+
Height="200"
185+
Source="{Binding}"
186+
Stretch="Uniform" />
187+
</StackPanel>
188+
189+
<StackPanel Spacing="8">
190+
<Image
191+
Width="200"
192+
Height="200"
193+
Stretch="Uniform">
194+
<Image.Source>
195+
<BitmapImage UriSource="{x:Bind local:SvgPage.GetPngFromSvgUrl((Foundation:Uri))}" />
196+
</Image.Source>
197+
</Image>
198+
<TextBlock Text="{x:Bind local:SvgPage.GetPngFromSvgUrl((Foundation:Uri))}" />
199+
</StackPanel>
200+
</StackPanel>
201+
202+
</DataTemplate>
203+
</ListView.ItemTemplate>
204+
</ListView>
205+
</local:ControlExample>
206+
207+
</StackPanel>
208+
</Page>

WinUI3Example/SvgPage.xaml.cpp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#include "pch.h"
2+
#include "SvgPage.xaml.h"
3+
#if __has_include("SvgPage.g.cpp")
4+
#include "SvgPage.g.cpp"
5+
#endif
6+
7+
#include <winrt/Microsoft.UI.Xaml.Media.Imaging.h>
8+
#include "TestSvgUrls.hpp"
9+
10+
11+
// To learn more about WinUI, the WinUI project structure,
12+
// and more about our project templates, see: http://aka.ms/winui-project-info.
13+
14+
namespace winrt::WinUI3Example::implementation
15+
{
16+
winrt::Windows::Foundation::Collections::IVector<winrt::Windows::Foundation::IInspectable> SvgPage::Svgs()
17+
{
18+
if (m_svgs)
19+
return m_svgs;
20+
21+
std::vector<winrt::Windows::Foundation::IInspectable> transformed;
22+
transformed.reserve(std::size(TestSvgUrls::urls));
23+
std::transform(
24+
std::begin(TestSvgUrls::urls),
25+
std::end(TestSvgUrls::urls),
26+
std::back_inserter(transformed),
27+
[](auto url) {return winrt::Windows::Foundation::Uri{ url }; }
28+
);
29+
m_svgs = winrt::single_threaded_vector(std::move(transformed));
30+
return m_svgs;
31+
}
32+
33+
34+
winrt::Windows::Foundation::Uri SvgPage::GetPngFromSvgUrl(winrt::Windows::Foundation::IInspectable const& item)
35+
{
36+
auto uri = item.as<winrt::Windows::Foundation::Uri>();
37+
std::wstring s{ uri.RawUri() };
38+
if (auto p = s.find(L"/svg/"); p != std::wstring::npos)
39+
s.replace(p, 5, L"/png/");
40+
if (auto e = s.rfind(L".svg"); e != std::wstring::npos)
41+
s.replace(e, 4, L".png");
42+
return winrt::Windows::Foundation::Uri{ winrt::hstring{ s } };
43+
}
44+
45+
winrt::Microsoft::UI::Xaml::Media::ImageSource SvgPage::GetSvgImageSourceFromString(winrt::hstring const& svgContent)
46+
{
47+
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source;
48+
if (svgContent.empty())
49+
return source;
50+
51+
[](auto source, auto svgContent)->winrt::fire_and_forget
52+
{
53+
winrt::Windows::Storage::Streams::InMemoryRandomAccessStream stream;
54+
winrt::Windows::Storage::Streams::DataWriter writer{ stream };
55+
writer.UnicodeEncoding(winrt::Windows::Storage::Streams::UnicodeEncoding::Utf8);
56+
writer.WriteString(svgContent);
57+
co_await writer.StoreAsync();
58+
writer.DetachStream();
59+
stream.Seek(0);
60+
source.SetSourceAsync(stream);
61+
}(source, svgContent);
62+
63+
return source;
64+
}
65+
}

WinUI3Example/SvgPage.xaml.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#pragma once
2+
3+
#include "SvgPage.g.h"
4+
5+
namespace winrt::WinUI3Example::implementation
6+
{
7+
struct SvgPage : SvgPageT<SvgPage>
8+
{
9+
SvgPage() = default;
10+
11+
winrt::Windows::Foundation::Collections::IVector<winrt::Windows::Foundation::IInspectable> Svgs();
12+
13+
static winrt::Windows::Foundation::Uri GetPngFromSvgUrl(winrt::Windows::Foundation::IInspectable const& svgUrl);
14+
static winrt::Microsoft::UI::Xaml::Media::ImageSource GetSvgImageSourceFromString(winrt::hstring const& svgContent);
15+
private:
16+
winrt::Windows::Foundation::Collections::IVector<winrt::Windows::Foundation::IInspectable> m_svgs{ nullptr };
17+
};
18+
}
19+
20+
namespace winrt::WinUI3Example::factory_implementation
21+
{
22+
struct SvgPage : SvgPageT<SvgPage, implementation::SvgPage>
23+
{
24+
};
25+
}

0 commit comments

Comments
 (0)