├── .gitignore
├── LICENSE
├── README.md
├── SkiaSharpVisualizer.sln
├── SkiaSharpVisualizer
├── .vsextension
│ └── string-resources.json
├── SkiaSharpVisualizer.csproj
├── SkiaSharpVisualizerControl.cs
├── SkiaSharpVisualizerControl.xaml
├── SkiaSharpVisualizerDataContext.cs
├── SkiaSharpVisualizerExtension.cs
└── SkiaSharpVisualizerProvider.cs
└── SkiaSharpVisualizerSource
├── SkiaSharpVisualizerSource.cs
└── SkiaSharpVisualizerSource.csproj
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.tlog
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
298 | *.vbp
299 |
300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
301 | *.dsw
302 | *.dsp
303 |
304 | # Visual Studio 6 technical files
305 | *.ncb
306 | *.aps
307 |
308 | # Visual Studio LightSwitch build output
309 | **/*.HTMLClient/GeneratedArtifacts
310 | **/*.DesktopClient/GeneratedArtifacts
311 | **/*.DesktopClient/ModelManifest.xml
312 | **/*.Server/GeneratedArtifacts
313 | **/*.Server/ModelManifest.xml
314 | _Pvt_Extensions
315 |
316 | # Paket dependency manager
317 | .paket/paket.exe
318 | paket-files/
319 |
320 | # FAKE - F# Make
321 | .fake/
322 |
323 | # CodeRush personal settings
324 | .cr/personal
325 |
326 | # Python Tools for Visual Studio (PTVS)
327 | __pycache__/
328 | *.pyc
329 |
330 | # Cake - Uncomment if you are using it
331 | # tools/**
332 | # !tools/packages.config
333 |
334 | # Tabs Studio
335 | *.tss
336 |
337 | # Telerik's JustMock configuration file
338 | *.jmconfig
339 |
340 | # BizTalk build output
341 | *.btp.cs
342 | *.btm.cs
343 | *.odx.cs
344 | *.xsd.cs
345 |
346 | # OpenCover UI analysis results
347 | OpenCover/
348 |
349 | # Azure Stream Analytics local run output
350 | ASALocalRun/
351 |
352 | # MSBuild Binary and Structured Log
353 | *.binlog
354 |
355 | # NVidia Nsight GPU debugger configuration file
356 | *.nvuser
357 |
358 | # MFractors (Xamarin productivity tool) working folder
359 | .mfractor/
360 |
361 | # Local History for Visual Studio
362 | .localhistory/
363 |
364 | # Visual Studio History (VSHistory) files
365 | .vshistory/
366 |
367 | # BeatPulse healthcheck temp database
368 | healthchecksdb
369 |
370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
371 | MigrationBackup/
372 |
373 | # Ionide (cross platform F# VS Code tools) working folder
374 | .ionide/
375 |
376 | # Fody - auto-generated XML schema
377 | FodyWeavers.xsd
378 |
379 | # VS Code files for those working on multiple tools
380 | .vscode/*
381 | !.vscode/settings.json
382 | !.vscode/tasks.json
383 | !.vscode/launch.json
384 | !.vscode/extensions.json
385 | *.code-workspace
386 |
387 | # Local History for Visual Studio Code
388 | .history/
389 |
390 | # Windows Installer files from build outputs
391 | *.cab
392 | *.msi
393 | *.msix
394 | *.msm
395 | *.msp
396 |
397 | # JetBrains Rider
398 | *.sln.iml
399 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 MapLarge, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SkiaSharpVisualizer
2 | A Visual Studio debugger extension for viewing [SkiaSharp](https://github.com/mono/SkiaSharp) bitmaps and images.
3 |
4 | ## Building
5 | Designed for Visual Studio 2022, simply update to at least 17.9 and install the "Visual Studio extension development" workload. There are no external requirements beyond the SkiaSharp nuget package and Visual Studio SDK. After building the solution, run the generated .vsix file to install.
6 |
7 | ## How to Use
8 | The extension adds a new UI item to view SkiaSharp SKBitmap, SKImage, and SKSurface objects.
9 | 
10 |
11 | When viewing, you will see a tool window containing a graphical preview of the image.
12 | 
13 |
14 | The stretch option will make the image fill the entire dialog space.
15 | 
16 |
17 | The bordered option will add an indicator border around the image so you can figure out the boundary of an image with transparency.
18 | 
19 |
20 | The "Open in External Viewer" button will launch the default viewer for PNG files.
21 | 
22 |
23 | ## Contributing
24 | PRs and suggestions are welcome, or you can fork this project and make your own enhancements.
25 |
--------------------------------------------------------------------------------
/SkiaSharpVisualizer.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.9.34728.123
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SkiaSharpVisualizer", "SkiaSharpVisualizer\SkiaSharpVisualizer.csproj", "{1E8D3F65-F34F-41FF-8711-1D8D73350487}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SkiaSharpVisualizerSource", "SkiaSharpVisualizerSource\SkiaSharpVisualizerSource.csproj", "{B5993999-1646-452B-85EC-ED5155DA370C}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {1E8D3F65-F34F-41FF-8711-1D8D73350487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {1E8D3F65-F34F-41FF-8711-1D8D73350487}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {1E8D3F65-F34F-41FF-8711-1D8D73350487}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {1E8D3F65-F34F-41FF-8711-1D8D73350487}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {B5993999-1646-452B-85EC-ED5155DA370C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {B5993999-1646-452B-85EC-ED5155DA370C}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {B5993999-1646-452B-85EC-ED5155DA370C}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {B5993999-1646-452B-85EC-ED5155DA370C}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {A6B6DE13-3B7C-4285-863C-E9DB2B513A00}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/SkiaSharpVisualizer/.vsextension/string-resources.json:
--------------------------------------------------------------------------------
1 | {
2 | "SkiaSharpVisualizer.SkiaSharpVisualizerProvider.SKBitmap.DisplayName": "SKBitmap Visualizer",
3 | "SkiaSharpVisualizer.SkiaSharpVisualizerProvider.SKImage.DisplayName": "SKImage Visualizer",
4 | "SkiaSharpVisualizer.SkiaSharpVisualizerProvider.SKSurface.DisplayName": "SKSurface Visualizer"
5 | }
6 |
--------------------------------------------------------------------------------
/SkiaSharpVisualizer/SkiaSharpVisualizer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0-windows
4 | enable
5 | enable
6 | 1.0.3
7 | 1.0.3
8 | SkiaSharp Visualizer
9 | MapLarge
10 | https://github.com/MapLarge/SkiaSharpVisualizer
11 | https://github.com/MapLarge/SkiaSharpVisualizer
12 | git
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | PreserveNewest
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | MSBuild:Compile
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/SkiaSharpVisualizer/SkiaSharpVisualizerControl.cs:
--------------------------------------------------------------------------------
1 | namespace SkiaSharpVisualizer;
2 |
3 | using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers;
4 | using Microsoft.VisualStudio.Extensibility.UI;
5 | using System.Runtime.Serialization;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | ///
10 | /// Remote user control to visualize the value.
11 | ///
12 | internal partial class SkiaSharpVisualizerControl : RemoteUserControl {
13 |
14 | public SkiaSharpVisualizerDataContext? TypedContext => this.DataContext as SkiaSharpVisualizerDataContext;
15 |
16 | public SkiaSharpVisualizerControl(VisualizerTarget visualizerTarget)
17 | : base(dataContext: new SkiaSharpVisualizerDataContext(visualizerTarget)) {
18 | }
19 |
20 | public override Task GetXamlAsync(CancellationToken cancellationToken) {
21 | return base.GetXamlAsync(cancellationToken);
22 | }
23 | public override Task ControlLoadedAsync(CancellationToken cancellationToken) {
24 | return base.ControlLoadedAsync(cancellationToken);
25 | }
26 |
27 | protected override void Dispose(bool disposing) {
28 | if (disposing) {
29 | TypedContext?.Dispose();
30 | }
31 | base.Dispose(disposing);
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/SkiaSharpVisualizer/SkiaSharpVisualizerControl.xaml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/SkiaSharpVisualizer/SkiaSharpVisualizerDataContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.Extensibility;
2 | using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers;
3 | using Microsoft.VisualStudio.Extensibility.UI;
4 | using Microsoft.VisualStudio.RpcContracts.DebuggerVisualizers;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Runtime.Serialization;
9 | using System.Text;
10 | using System.Threading.Tasks;
11 |
12 | namespace SkiaSharpVisualizer {
13 |
14 | [DataContract]
15 | public class SkiaSharpVisualizerDataContext : NotifyPropertyChangedObject, IDisposable {
16 |
17 | private readonly VisualizerTarget visualizerTarget;
18 | private SkiaSharpVisualizerDataSource? _model;
19 |
20 | public SkiaSharpVisualizerDataContext(VisualizerTarget visualizerTarget) {
21 | this.visualizerTarget = visualizerTarget;
22 | visualizerTarget.StateChanged += this.OnStateChangedAsync;
23 |
24 | this.OpenExternalCommand = new OpenExternalCommand(this);
25 | }
26 |
27 | [DataMember]
28 | public SkiaSharpVisualizerDataSource? Model {
29 | get => _model;
30 | set {
31 | SetProperty(ref this._model, value);
32 | RaiseNotifyPropertyChangedEvent(nameof(Width));
33 | RaiseNotifyPropertyChangedEvent(nameof(Height));
34 | }
35 | }
36 | [DataMember]
37 | public int Width => Model?.width ?? 0;
38 | [DataMember]
39 | public int Height => Model?.height ?? 0;
40 |
41 | private string? _filePath;
42 | [DataMember]
43 | public string? FilePath {
44 | get => _filePath;
45 | set {
46 | SetProperty(ref _filePath, value);
47 | }
48 | }
49 |
50 | private bool _isStretched;
51 | [DataMember]
52 | public bool IsStretched {
53 | get => _isStretched;
54 | set {
55 | SetProperty(ref _isStretched, value);
56 | RaiseNotifyPropertyChangedEvent(nameof(ImageStretch));
57 | }
58 | }
59 | [DataMember]
60 | public string ImageStretch => _isStretched ? "Uniform" : "None";
61 |
62 | private bool _isBordered;
63 | [DataMember]
64 | public bool IsBordered {
65 | get => _isBordered;
66 | set {
67 | SetProperty(ref _isBordered, value);
68 | RaiseNotifyPropertyChangedEvent(nameof(BorderThickness));
69 | }
70 | }
71 | [DataMember]
72 | public int BorderThickness => _isBordered ? 3 : 0;
73 |
74 | private const int MAXFILEPATHS = 5;
75 | private SortedDictionary byteFilePaths = new();
76 | private SortedDictionary byteLastAccess = new();
77 |
78 | [DataMember]
79 | public IAsyncCommand OpenExternalCommand { get; }
80 |
81 | #if DEBUG
82 | private readonly List failedToDeleteFiles = new();
83 | #endif
84 |
85 | private async Task GetRequestAsync(VisualizerTargetStateNotification args) {
86 | switch (args) {
87 | case VisualizerTargetStateNotification.Available:
88 | case VisualizerTargetStateNotification.ValueUpdated:
89 | return await visualizerTarget.ObjectSource.RequestDataAsync(jsonSerializer: null, CancellationToken.None);
90 | case VisualizerTargetStateNotification.Unavailable:
91 | return null;
92 | default:
93 | throw new NotSupportedException("Unexpected visualizer target state notification");
94 | }
95 | }
96 |
97 | private async Task OnStateChangedAsync(object? sender, VisualizerTargetStateNotification args) {
98 | var dataSource = await GetRequestAsync(args);
99 |
100 | CleanupUsedFilePaths();
101 |
102 | //No data.
103 | var pngBase64 = dataSource?.pngBase64;
104 | if (string.IsNullOrWhiteSpace(pngBase64)) {
105 | ResetBindings();
106 | return;
107 | }
108 |
109 | var pngBytes = System.Convert.FromBase64String(pngBase64);
110 | try {
111 | //Is this the same image we've shown already?
112 | if (byteFilePaths.TryGetValue(pngBase64, out var fp) && System.IO.File.Exists(fp) && System.IO.File.ReadAllBytes(fp).SequenceEqual(pngBytes)) {
113 | this.byteLastAccess[pngBase64] = DateTimeOffset.Now;
114 | this.FilePath = fp;
115 | this.Model = dataSource;
116 | return;
117 | }
118 | } catch {
119 | //Ignore any lookup errors.
120 | }
121 |
122 | //Using a BitmapSource on the data context is not serializable cross-process, so we will write the png to a temp file since binding to a url works.
123 | try {
124 | var tmpFilePath = System.IO.Path.ChangeExtension(System.IO.Path.GetTempFileName(), "png");
125 | await System.IO.File.WriteAllBytesAsync(tmpFilePath, pngBytes);
126 |
127 | this.byteFilePaths[pngBase64] = tmpFilePath;
128 | this.byteLastAccess[pngBase64] = DateTimeOffset.Now;
129 | this.FilePath = tmpFilePath;
130 | this.Model = dataSource;
131 | } catch {
132 | //Something terrible happened.
133 | ResetBindings();
134 | }
135 | }
136 |
137 | private void CleanupUsedFilePaths() {
138 | //Once we hit the max limit, remove the oldest tracked file.
139 | if (byteLastAccess.Count < MAXFILEPATHS) {
140 | return;
141 | }
142 |
143 | var oldestFile = byteLastAccess.First();
144 | if (!byteFilePaths.TryGetValue(oldestFile.Key, out var filePath)) {
145 | //Shouldn't happen.
146 | byteLastAccess.Remove(oldestFile.Key);
147 | return;
148 | }
149 |
150 | byteFilePaths.Remove(oldestFile.Key);
151 | byteLastAccess.Remove(oldestFile.Key);
152 | try {
153 | //Make an attempt to remove the file.
154 | System.IO.File.Delete(filePath);
155 | } catch {
156 | //Ignore IO errors
157 | #if DEBUG
158 | failedToDeleteFiles.Add(filePath);
159 | #endif
160 | }
161 | }
162 | private void ResetBindings() {
163 | this.FilePath = null;
164 | this.Model = null;
165 | }
166 | private void RemoveAllFiles() {
167 | foreach (var kvp in byteFilePaths) {
168 | try {
169 | //Make an attempt to remove the file. VS locks them for a while, so we might not get them all.
170 | System.IO.File.Delete(kvp.Value);
171 | } catch {
172 | //Ignore IO errors
173 | #if DEBUG
174 | failedToDeleteFiles.Add(kvp.Value);
175 | #endif
176 | }
177 | }
178 |
179 | this.byteFilePaths.Clear();
180 | this.byteLastAccess.Clear();
181 | }
182 |
183 | public void Dispose() {
184 | visualizerTarget.StateChanged -= this.OnStateChangedAsync;
185 | this.visualizerTarget.Dispose();
186 |
187 | this.ResetBindings();
188 | this.RemoveAllFiles();
189 | }
190 |
191 | }
192 |
193 | public class OpenExternalCommand : NotifyPropertyChangedObject, IAsyncCommand {
194 |
195 | private bool executeFailed = false;
196 | public bool CanExecute => !executeFailed && !string.IsNullOrWhiteSpace(context.FilePath);
197 |
198 | private readonly SkiaSharpVisualizerDataContext context;
199 | public OpenExternalCommand(SkiaSharpVisualizerDataContext context) {
200 | this.context = context;
201 | this.context.PropertyChanged += Context_PropertyChanged;
202 | }
203 |
204 | private void Context_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) {
205 | switch (e.PropertyName) {
206 | case nameof(context.FilePath):
207 | this.RaiseNotifyPropertyChangedEvent("CanExecute");
208 | break;
209 | }
210 | }
211 |
212 | public Task ExecuteAsync(object? parameter, IClientContext clientContext, CancellationToken cancellationToken) {
213 | var filePath = parameter as string;
214 | if (string.IsNullOrWhiteSpace(filePath)) {
215 | return Task.CompletedTask;
216 | }
217 |
218 | try {
219 | //Need UseShellExecute to run an image file.
220 | var info = new System.Diagnostics.ProcessStartInfo(filePath);
221 | info.UseShellExecute = true;
222 | using var _ = System.Diagnostics.Process.Start(info);
223 | } catch {
224 | //Hopefully doesn't happen.
225 | this.executeFailed = true;
226 | this.RaiseNotifyPropertyChangedEvent("CanExecute");
227 | }
228 | return Task.CompletedTask;
229 | }
230 | }
231 |
232 | }
233 |
--------------------------------------------------------------------------------
/SkiaSharpVisualizer/SkiaSharpVisualizerExtension.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.VisualStudio.Extensibility;
3 | using System.Reflection;
4 |
5 | namespace SkiaSharpVisualizer {
6 | ///
7 | /// Extension entrypoint for the VisualStudio.Extensibility extension.
8 | ///
9 | [VisualStudioContribution]
10 | public class SkiaSharpVisualizerExtension : Extension {
11 | ///
12 | public override ExtensionConfiguration ExtensionConfiguration => new() {
13 | Metadata = new(
14 | id: "SkiaSharpVisualizer.c3655891-53aa-416e-981c-17ea9a969e58",
15 | version: this.ExtensionAssemblyVersion,
16 | publisherName: "MapLarge",
17 | displayName: "SkiaSharp Visualizer",
18 | description: "Debugger visualizers for SkiaSharp images."),
19 | };
20 |
21 | ///
22 | protected override void InitializeServices(IServiceCollection serviceCollection) {
23 | base.InitializeServices(serviceCollection);
24 |
25 | // You can configure dependency injection here by adding services to the serviceCollection.
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/SkiaSharpVisualizer/SkiaSharpVisualizerProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.Extensibility;
2 | using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers;
3 | using Microsoft.VisualStudio.Extensibility.UI;
4 | using Microsoft.VisualStudio.RpcContracts.DebuggerVisualizers;
5 | using Microsoft.VisualStudio.RpcContracts.RemoteUI;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Runtime.Serialization;
10 | using System.Text;
11 | using System.Threading.Tasks;
12 | using System.Windows.Media.Imaging;
13 |
14 | namespace SkiaSharpVisualizer {
15 |
16 | [VisualStudioContribution]
17 | public class SkiaSharpVisualizerProvider : DebuggerVisualizerProvider {
18 |
19 | public SkiaSharpVisualizerProvider(SkiaSharpVisualizerExtension extension, VisualStudioExtensibility extensibility) : base(extension, extensibility) {
20 |
21 | }
22 |
23 | ///
24 | public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new DebuggerVisualizerProviderConfiguration(
25 | new VisualizerTargetType("%SkiaSharpVisualizer.SkiaSharpVisualizerProvider.SKBitmap.DisplayName%", typeof(SkiaSharp.SKBitmap)),
26 | new VisualizerTargetType("%SkiaSharpVisualizer.SkiaSharpVisualizerProvider.SKImage.DisplayName%", typeof(SkiaSharp.SKImage)),
27 | new VisualizerTargetType("%SkiaSharpVisualizer.SkiaSharpVisualizerProvider.SKSurface.DisplayName%", typeof(SkiaSharp.SKSurface))
28 | ) {
29 | VisualizerObjectSourceType = new(typeof(SkiaSharpVisualizerSource)),
30 | Style = VisualizerStyle.ToolWindow
31 | };
32 |
33 | ///
34 | public override async Task CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken) {
35 | // The control will be in charge of calling the RequestDataAsync method from the visualizer object source and disposing of the visualizer target.
36 | return await Task.FromResult(new SkiaSharpVisualizerControl(visualizerTarget));
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/SkiaSharpVisualizerSource/SkiaSharpVisualizerSource.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.DebuggerVisualizers;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Text;
6 |
7 | namespace SkiaSharpVisualizer {
8 |
9 | public class SkiaSharpVisualizerDataSource {
10 |
11 | ///
12 | /// PNG image bytes encoded to Base64.
13 | ///
14 | public string pngBase64;
15 | ///
16 | /// Width of the image.
17 | ///
18 | public int width;
19 | ///
20 | /// Height of the image.
21 | ///
22 | public int height;
23 |
24 | }
25 |
26 | public class SkiaSharpVisualizerSource : VisualizerObjectSource {
27 |
28 | public override void GetData(object target, Stream outgoingData) {
29 | //Get raw bitmap bytes and serialize those.
30 | //If something terrible happens, return null.
31 | SkiaSharpVisualizerDataSource ds;
32 | switch (target) {
33 | case SkiaSharp.SKBitmap bitmap:
34 | ds = GetBitmapDataSource(bitmap);
35 | break;
36 | case SkiaSharp.SKImage image:
37 | ds = GetBitmapDataSource(image);
38 | break;
39 | case SkiaSharp.SKSurface surface:
40 | using (var snapshot = surface.Snapshot()) {
41 | ds = GetBitmapDataSource(snapshot);
42 | }
43 | break;
44 | default:
45 | throw new NotImplementedException(target.GetType().FullName);
46 | }
47 | SerializeAsJson(outgoingData, ds);
48 | }
49 |
50 | private SkiaSharpVisualizerDataSource GetBitmapDataSource(SkiaSharp.SKBitmap bitmap) {
51 | return new SkiaSharpVisualizerDataSource {
52 | pngBase64 = Convert.ToBase64String(SavePngBytes(bitmap)),
53 | width = bitmap.Width,
54 | height = bitmap.Height,
55 | };
56 | }
57 | private SkiaSharpVisualizerDataSource GetBitmapDataSource(SkiaSharp.SKImage image) {
58 | return new SkiaSharpVisualizerDataSource {
59 | pngBase64 = Convert.ToBase64String(SavePngBytes(image)),
60 | width = image.Width,
61 | height = image.Height,
62 | };
63 | }
64 |
65 | ///
66 | /// Encodes the provided Skia bitmap as a PNG and returns the bytes. Do not use while drawing with a canvas.
67 | ///
68 | ///
69 | ///
70 | public static byte[] SavePngBytes(SkiaSharp.SKBitmap bitmap) {
71 | return SaveImageBytes(bitmap, SkiaSharp.SKEncodedImageFormat.Png, 100);
72 | }
73 |
74 | ///
75 | /// Encodes the provided Skia bitmap as a PNG and returns the bytes. Do not use while drawing with a canvas.
76 | ///
77 | ///
78 | ///
79 | ///
80 | ///
81 | public static byte[] SaveImageBytes(SkiaSharp.SKBitmap bitmap, SkiaSharp.SKEncodedImageFormat imageFormat, int imageQuality) {
82 | if (bitmap == null)
83 | throw new ArgumentNullException(nameof(bitmap));
84 |
85 | using (var ms = new System.IO.MemoryStream()) {
86 | using (var skStream = new SkiaSharp.SKManagedWStream(ms, false)) {
87 | using (var pixmap = bitmap.PeekPixels()) {
88 | pixmap.Encode(skStream, imageFormat, imageQuality);
89 | }
90 | return ms.ToArray();
91 | }
92 | }
93 | }
94 |
95 | ///
96 | /// Encodes the provided Skia bitmap as a PNG and returns the bytes. Do not use while drawing with a canvas.
97 | ///
98 | ///
99 | ///
100 | public static byte[] SavePngBytes(SkiaSharp.SKImage image) {
101 | return SavePngBytes(image, SkiaSharp.SKEncodedImageFormat.Png, 100);
102 | }
103 |
104 | ///
105 | /// Encodes the provided Skia bitmap as a PNG and returns the bytes. Do not use while drawing with a canvas.
106 | ///
107 | ///
108 | ///
109 | ///
110 | ///
111 | public static byte[] SavePngBytes(SkiaSharp.SKImage image, SkiaSharp.SKEncodedImageFormat imageFormat, int imageQuality) {
112 | if (image == null)
113 | throw new ArgumentNullException(nameof(image));
114 |
115 | using (var ms = new System.IO.MemoryStream()) {
116 | SaveToStream(image, ms, imageFormat, imageQuality);
117 | return ms.ToArray();
118 | }
119 | }
120 |
121 | ///
122 | /// Write the Skia image into the provided stream.
123 | ///
124 | ///
125 | ///
126 | public static void SaveToStream(SkiaSharp.SKImage image, System.IO.Stream stream) {
127 | SaveToStream(image, stream, SkiaSharp.SKEncodedImageFormat.Png, 100);
128 | }
129 | ///
130 | /// Write the Skia image into the provided stream.
131 | ///
132 | ///
133 | ///
134 | ///
135 | ///
136 | public static void SaveToStream(SkiaSharp.SKImage image, System.IO.Stream stream, SkiaSharp.SKEncodedImageFormat imageFormat, int imageQuality) {
137 | if (image == null)
138 | throw new ArgumentNullException(nameof(image));
139 | if (stream == null)
140 | throw new ArgumentNullException(nameof(stream));
141 | if (!stream.CanWrite)
142 | throw new ArgumentException("Stream is not writable.", nameof(stream));
143 |
144 | switch (imageFormat) {
145 | case SkiaSharp.SKEncodedImageFormat.Webp:
146 | var opts = new SkiaSharp.SKWebpEncoderOptions(SkiaSharp.SKWebpEncoderCompression.Lossless, imageQuality);
147 | using (var bitmap = SkiaSharp.SKBitmap.FromImage(image))
148 | using (var skStream = new SkiaSharp.SKManagedWStream(stream, false))
149 | using (var pixmap = bitmap.PeekPixels()) {
150 | pixmap.Encode(skStream, opts);
151 | }
152 | break;
153 | default:
154 | using (var encoded = image.Encode(imageFormat, imageQuality)) {
155 | encoded.SaveTo(stream);
156 | }
157 | break;
158 | }
159 | }
160 |
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/SkiaSharpVisualizerSource/SkiaSharpVisualizerSource.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | SkiaSharpVisualizer
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------