Last active
August 5, 2025 04:40
-
-
Save ktwrd/77bee806f77d1510f1e6dad99a43d7c9 to your computer and use it in GitHub Desktop.
Eto.Forms: SVG (to Bitmap) to Icon Converter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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