Skip to content

Instantly share code, notes, and snippets.

@ktwrd
Last active August 5, 2025 04:40
Show Gist options
  • Save ktwrd/77bee806f77d1510f1e6dad99a43d7c9 to your computer and use it in GitHub Desktop.
Save ktwrd/77bee806f77d1510f1e6dad99a43d7c9 to your computer and use it in GitHub Desktop.
Eto.Forms: SVG (to Bitmap) to Icon Converter
/*
SVG (to PNG) to ICO Converter for Eto.Forms
Originally written by: Kate Ward <[email protected]>
Depends on:
- Eto.Forms 2.9.x or later
- Svg 3.4.7
Generated SVG Icons are cached in "IconCache".
You can modifiy this code so you can have a standalone function for generating
an Eto.Drawing.Bitmap from an SVG (the first for loop in CreateIconFromSvg).
At some point, this will go in my shared library, but as a new project
for Eto.Forms specifically.
Copyright 2025 Kate Ward <[email protected]>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
using Eto.Drawing;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using ImageFormat = System.Drawing.Imaging.ImageFormat;
namespace kate.shared.EtoForms;
public static class ResourceHelper
{
private readonly static Dictionary<string, Icon> IconCache = [];
private static MemoryStream CreateIconFromSvg(MemoryStream svgStream, Size[] sizes)
{
var pngParts = new MemoryStream[sizes.Length];
sizes = sizes.OrderByDescending(e => e.Width).ToArray();
for (int i = 0; i < sizes.Length; i++)
{
svgStream.Seek(0, SeekOrigin.Begin);
var doc = Svg.SvgDocument.Open<Svg.SvgDocument>(svgStream);
var bm = doc.Draw(sizes[i].Width, sizes[i].Height);
var ms = new MemoryStream();
bm.Save(ms, ImageFormat.Png);
ms.Seek(0, SeekOrigin.Begin);
bm.Dispose();
pngParts[i] = ms;
}
var resultStream = new MemoryStream();
var sizeCountAsUShort = Convert.ToUInt16(sizes.Length);
var sizeCountAsBytes = BitConverter.GetBytes(sizeCountAsUShort);
// .ico header
// source: https://en.wikipedia.org/wiki/ICO_(file_format)#Headers
var header = new byte[6];
header[0] = 0x00;
header[1] = 0x00;
header[2] = 0x01;
header[3] = 0x00;
header[4] = sizeCountAsBytes[0];
header[5] = sizeCountAsBytes[1];
resultStream.Write(header.AsSpan());
// headers are always written next to eachother.
// image data can be anywhere in the file after the headers
// source: https://en.wikipedia.org/wiki/ICO_(file_format)#Structure_of_image_directory
var fakePosition = resultStream.Position + (pngParts.Length * 16);
for (int i = 0; i < pngParts.Length; i++)
{
var part = pngParts[i];
var width = Convert.ToByte(sizes[i].Width >= 256 ? 0 : sizes[i].Width);
var height = Convert.ToByte(sizes[i].Height >= 256 ? 0 : sizes[i].Height);
var partSize = BitConverter.GetBytes(Convert.ToUInt32(part.Length));
var imageStart = BitConverter.GetBytes(Convert.ToUInt32(fakePosition));
var partHeader = new byte[]
{
width, height,
0x00, 0x00,
0x01, 0x00,
0x20, 0x00, // assume 32bits per-pixel
partSize[0], partSize[1],
partSize[2], partSize[3],
imageStart[0], imageStart[1],
imageStart[2], imageStart[3]
};
resultStream.Write(partHeader);
fakePosition += part.Length;
}
// just write the generated png files as-is after the headers.
// no extra fluff required, since we're not going to use
// the BMP format (uses too much space compared to png)
// source: https://en.wikipedia.org/wiki/ICO_(file_format)#PNG_format
for (int i = 0; i < pngParts.Length; i++)
{
var part = pngParts[i];
part.CopyTo(resultStream);
part.Dispose();
}
resultStream.Seek(0, SeekOrigin.Begin);
return resultStream;
}
private static Icon GetSvgIcon(string name, Size[] sizes, params Assembly[] assemblies)
{
// max image frame size for an ico file is 256 (actual value in hex is 0x00) since
// the width/height are stored as 2 bytes in total.
// source: https://en.wikipedia.org/wiki/ICO_(file_format)#Structure_of_image_directory
if (sizes.Any(e => e.Width > 256 || e.Height > 256))
throw new ArgumentException("One or more sizes has a width or height greater than 256");
lock (IconCache)
{
var keyItems = new string[2 + (sizes.Length * 2)];
keyItems[0] = name;
keyItems[1] = "svg-to-icon";
for (int i = 0; i < sizes.Length; i++)
{
var b = (i + 1) * 2;
keyItems[b] = sizes[i].Width.ToString();
keyItems[b + 1] = sizes[i].Height.ToString();
}
var key = string.Join("\n",
name,
"svg-to-icon",
keyItems);
if (!IconCache.TryGetValue(key, out var svgIco))
{
var svgStream = new MemoryStream();
Stream? s = null;
foreach (var asm in assemblies)
{
s = asm.GetManifestResourceStream(name);
if (s != null) break;
}
if (s == null) throw new InvalidOperationException($"Could not find resource \"{name}\" in any assemblies");
s.CopyTo(svgStream);
s.Dispose();
var resultStream = CreateIconFromSvg(svgStream, sizes);
var icon = new Icon(resultStream);
svgIco = icon;
IconCache[key] = svgIco;
}
return svgIco;
}
}
public static Icon GetSvgIcon(string name)
=> GetSvgIcon(name, [
new(16, 16),
new(32, 32),
new(64, 64),
new(128, 128),
], typeof(ReflectionHelper).Assembly);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment