diff --git a/SharpEmf.sln b/SharpEmf.sln
index 1bbe612..6e5e9a2 100644
--- a/SharpEmf.sln
+++ b/SharpEmf.sln
@@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9E1188D3
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEmf.UnitTests", "tests\SharpEmf.UnitTests\SharpEmf.UnitTests.csproj", "{2BF9EA6A-0AF1-495E-BAF1-0E91150CD885}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEmf.Svg", "src\SharpEmf.Svg\SharpEmf.Svg.csproj", "{207AD2F5-163F-4E66-B6EF-28FDE0CB06B9}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -29,9 +31,14 @@ Global
{2BF9EA6A-0AF1-495E-BAF1-0E91150CD885}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2BF9EA6A-0AF1-495E-BAF1-0E91150CD885}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2BF9EA6A-0AF1-495E-BAF1-0E91150CD885}.Release|Any CPU.Build.0 = Release|Any CPU
+ {207AD2F5-163F-4E66-B6EF-28FDE0CB06B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {207AD2F5-163F-4E66-B6EF-28FDE0CB06B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {207AD2F5-163F-4E66-B6EF-28FDE0CB06B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {207AD2F5-163F-4E66-B6EF-28FDE0CB06B9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{76508D82-8D2E-440C-9690-A2CB9F050A21} = {35AA0FA2-85E0-4264-9FD8-8CE5ACCAEBA7}
{2BF9EA6A-0AF1-495E-BAF1-0E91150CD885} = {9E1188D3-00AC-4D61-9626-4DCBA561FE88}
+ {207AD2F5-163F-4E66-B6EF-28FDE0CB06B9} = {35AA0FA2-85E0-4264-9FD8-8CE5ACCAEBA7}
EndGlobalSection
EndGlobal
diff --git a/SharpEmf.sln.DotSettings b/SharpEmf.sln.DotSettings
index 9b7b738..9bede4d 100644
--- a/SharpEmf.sln.DotSettings
+++ b/SharpEmf.sln.DotSettings
@@ -113,6 +113,7 @@
True
True
True
+ True
True
True
True
diff --git a/src/SharpEmf.Svg/BitmapUtils.cs b/src/SharpEmf.Svg/BitmapUtils.cs
new file mode 100644
index 0000000..95b3ad7
--- /dev/null
+++ b/src/SharpEmf.Svg/BitmapUtils.cs
@@ -0,0 +1,79 @@
+using SharpEmf.WmfTypes.Bitmap;
+using SkiaSharp;
+
+namespace SharpEmf.Svg;
+
+internal static class BitmapUtils
+{
+ public static unsafe string DibToPngBase64(byte[] dibData, BitmapInfoHeader bitmapHeader)
+ {
+ // TODO: this assumes that the DIB is 24-bit RGB, handle other cases
+ var rgbaData = DibToRgba(dibData, bitmapHeader);
+
+ using var bitmap = new SKBitmap();
+
+ var width = bitmapHeader.Width;
+ var height = bitmapHeader.Height;
+
+ fixed(byte* rgbaDataPtr = rgbaData)
+ {
+ bitmap.InstallPixels(new SKImageInfo(width, height, SKColorType.Rgba8888, SKAlphaType.Unpremul), (nint)rgbaDataPtr, width * 4);
+ }
+
+ using var pngData = bitmap.Encode(SKEncodedImageFormat.Png, 100);
+ var resultBase64Str = Convert.ToBase64String(pngData.Span);
+
+ return resultBase64Str;
+ }
+
+ public static unsafe byte[] DibToRgba(byte[] dibData, BitmapInfoHeader bitmapHeader)
+ {
+ var usedBytes = (ushort)bitmapHeader.BitCount / 8 * bitmapHeader.Width;
+ // DIB data is padded to the nearest DWORD (4-byte) boundary
+ var padding = RoundUpToNearestMultipleOf4(usedBytes) - usedBytes;
+
+ var rgbaStride = bitmapHeader.Width * 4;
+
+ byte[] rgbaData = new byte[rgbaStride * bitmapHeader.Height];
+
+ fixed (byte* dibDataPtr = dibData)
+ {
+ fixed (byte* rgbaDataPtr = rgbaData)
+ {
+ var dibDataPtr2 = dibDataPtr;
+ var rgbaDataPtr2 = rgbaDataPtr;
+
+ for (int y = 0; y < bitmapHeader.Height; y++)
+ {
+ // Due to the fact that emf arrays after reading are reversed, padding skipping is done before the X-row loop
+ // Bytes order in EMF file: B, G, R, PADDED, PADDED, B, G, R, PADDED, PADDED, ...
+ // After reversing: PADDED, PADDED, R, G, B, PADDED, PADDED, R, G, B, ...
+ dibDataPtr2 += padding;
+
+ for (int x = 0; x < bitmapHeader.Width; x++)
+ {
+ var r = *dibDataPtr2;
+ var g = *(dibDataPtr2 + 1);
+ var b = *(dibDataPtr2 + 2);
+ const byte a = 0xFF;
+
+ *rgbaDataPtr2 = r;
+ *(rgbaDataPtr2 + 1) = g;
+ *(rgbaDataPtr2 + 2) = b;
+ *(rgbaDataPtr2 + 3) = a;
+
+ dibDataPtr2 += 3;
+ rgbaDataPtr2 += 4;
+ }
+ }
+ }
+ }
+
+ return rgbaData;
+ }
+
+ private static int RoundUpToNearestMultipleOf4(int num)
+ {
+ return (num + 3) / 4 * 4;
+ }
+}
\ No newline at end of file
diff --git a/src/SharpEmf.Svg/EmfState.cs b/src/SharpEmf.Svg/EmfState.cs
new file mode 100644
index 0000000..62c2416
--- /dev/null
+++ b/src/SharpEmf.Svg/EmfState.cs
@@ -0,0 +1,19 @@
+using SharpEmf.Enums;
+using SharpEmf.WmfTypes;
+
+namespace SharpEmf.Svg;
+
+internal class EmfState
+{
+ public PointL WindowOrigin { get; set; }
+ public PointL ViewportOrigin { get; set; }
+ public SizeL WindowExtent { get; set; }
+ public SizeL ViewportExtent { get; set; }
+ public MapMode MapMode { get; set; }
+ public GraphicsObject[] ObjectTable { get; set; }
+ public PlaybackDeviceContext CurrentPlaybackDeviceContext { get; } = new();
+ public float Scaling { get; set; }
+
+ public bool InPath { get; set; }
+
+}
\ No newline at end of file
diff --git a/src/SharpEmf.Svg/EmfSvgWriter.cs b/src/SharpEmf.Svg/EmfSvgWriter.cs
new file mode 100644
index 0000000..90a1d08
--- /dev/null
+++ b/src/SharpEmf.Svg/EmfSvgWriter.cs
@@ -0,0 +1,95 @@
+using System.Text;
+using SharpEmf.Records.Bitmap;
+using SharpEmf.Records.Drawing;
+using SharpEmf.Records.ObjectCreation;
+using SharpEmf.Records.ObjectManipulation;
+using SharpEmf.Records.PathBracket;
+using SharpEmf.Records.State;
+using SharpEmf.Svg.RecordsProcessing;
+
+namespace SharpEmf.Svg;
+
+public static class EmfSvgWriter
+{
+ public static string ConvertToSvg(EnhancedMetafile emf)
+ {
+ var sb = new StringBuilder();
+ var state = new EmfState();
+
+ EmfControlRecordsHandlers.HandleHeaderRecord(sb, state, emf.Header);
+
+ foreach (var record in emf.Records)
+ {
+ switch (record)
+ {
+ case EmrSetMapMode setMapMode:
+ EmfStateRecordsHandlers.HandleSetMapModeRecord(state, setMapMode);
+ break;
+ case EmrSetBkMode setBkMode:
+ EmfStateRecordsHandlers.HandleSetBkModeRecord(state, setBkMode);
+ break;
+ case EmrSetWindowOrgEx setWindowOrgEx:
+ EmfStateRecordsHandlers.HandleSetWindowOrgExRecord(state, setWindowOrgEx);
+ break;
+ case EmrSetViewportOrgEx setViewportOrgEx:
+ EmfStateRecordsHandlers.HandleSetViewportOrgExRecord(state, setViewportOrgEx);
+ break;
+ case EmrSetWindowExtEx setWindowExtEx:
+ EmfStateRecordsHandlers.HandleSetWindowExtExRecord(state, setWindowExtEx);
+ break;
+ case EmrSetViewportExtEx setViewportExtEx:
+ EmfStateRecordsHandlers.HandleSetViewportExtExRecord(state, setViewportExtEx);
+ break;
+ case EmrSetPolyfillMode setPolyfillMode:
+ EmfStateRecordsHandlers.HandleSetPolyfillMode(state, setPolyfillMode);
+ break;
+ case EmrCreateBrushIndirect createBrushIndirect:
+ EmfObjectCreationRecordsHandlers.HandleCreateBrushIndirect(state, createBrushIndirect);
+ break;
+ case EmrSelectObject selectObject:
+ EmfObjectManipulationRecordsHandlers.HandleSelectObject(state, selectObject);
+ break;
+ case EmrDeleteObject deleteObject:
+ EmfObjectManipulationRecordsHandlers.HandleDeleteObject(state, deleteObject);
+ break;
+ case EmrExtCreatePen extCreatePen:
+ EmfObjectCreationRecordsHandlers.HandleExtCreatePen(state, extCreatePen);
+ break;
+ case EmrPolyPolygon16 polyPolygon16:
+ EmfDrawingRecordsHandlers.HandlePolyPolygon16(sb, state, polyPolygon16);
+ break;
+ case EmrBeginPath:
+ EmfPathBracketRecordsHandlers.HandleBeginPath(sb, state);
+ break;
+ case EmrEndPath:
+ EmfPathBracketRecordsHandlers.HandleEndPath(sb, state);
+ break;
+ case EmrMoveToEx moveToEx:
+ EmfStateRecordsHandlers.HandleMoveToEx(sb, state, moveToEx);
+ break;
+ case EmrPolyBezierTo16 polyBezierTo16:
+ EmfDrawingRecordsHandlers.HandlePolybezierTo16(sb, state, polyBezierTo16);
+ break;
+ case EmrCloseFigure:
+ EmfPathBracketRecordsHandlers.HandleCloseFigure(sb, state);
+ break;
+ case EmrStretchDiBits stretchDiBits:
+ EmfBitmapRecordsHandlers.HandleStretchDIBits(sb, state, stretchDiBits);
+ break;
+ case EmrSetTextColor setTextColor:
+ EmfStateRecordsHandlers.HandleSetTextColor(state, setTextColor);
+ break;
+ case EmrExtTextOutW extTextOutW:
+ EmfDrawingRecordsHandlers.HandleExtTextOutW(sb, state, extTextOutW);
+ break;
+ default:
+ Console.WriteLine($"Skipped EMF to SVG conversion of record with type: {(Enum.IsDefined(record.Type) ? record.Type : record.Type.ToString("X"))}");
+ break;
+ }
+ }
+
+ sb.AppendLine("");
+ sb.AppendLine("");
+ return sb.ToString();
+ }
+}
\ No newline at end of file
diff --git a/src/SharpEmf.Svg/GraphicsObject.cs b/src/SharpEmf.Svg/GraphicsObject.cs
new file mode 100644
index 0000000..20ac09f
--- /dev/null
+++ b/src/SharpEmf.Svg/GraphicsObject.cs
@@ -0,0 +1,10 @@
+using SharpEmf.Objects;
+
+namespace SharpEmf.Svg;
+
+internal struct GraphicsObject
+{
+ public GraphicsObjectType Type { get; set; }
+ public LogBrushEx LogBrush { get; set; }
+ public LogPenEx LogPen { get; set; }
+}
\ No newline at end of file
diff --git a/src/SharpEmf.Svg/GraphicsObjectType.cs b/src/SharpEmf.Svg/GraphicsObjectType.cs
new file mode 100644
index 0000000..8a2f961
--- /dev/null
+++ b/src/SharpEmf.Svg/GraphicsObjectType.cs
@@ -0,0 +1,9 @@
+namespace SharpEmf.Svg;
+
+internal enum GraphicsObjectType
+{
+ Unknown,
+ Brush,
+ Pen,
+ // TODO: add more stock object (font, color space, palette)
+}
\ No newline at end of file
diff --git a/src/SharpEmf.Svg/Helpers.cs b/src/SharpEmf.Svg/Helpers.cs
new file mode 100644
index 0000000..674b025
--- /dev/null
+++ b/src/SharpEmf.Svg/Helpers.cs
@@ -0,0 +1,142 @@
+using SharpEmf.Enums;
+
+namespace SharpEmf.Svg;
+
+internal static class Helpers
+{
+ public static (double X, double Y) GetScalingForCurrentMapMode(this EmfState state)
+ {
+ double scalingX;
+ double scalingY;
+ double windowOrgX = 0.0;
+ double windowOrgY = 0.0;
+ double viewPortOrgX = 0.0;
+ double viewPortOrgY = 0.0;
+ switch (state.MapMode)
+ {
+ case MapMode.MM_TEXT:
+ scalingX = 1.0;
+ scalingY = 1.0;
+ break;
+ // case MapMode.MM_LOMETRIC:
+ // // convert to 0.1 mm to pixel and invert Y
+ // scalingX = states->pxPerMm * 0.1 * 1;
+ // scalingY = states->pxPerMm * 0.1 * -1;
+ // break;
+ // case MapMode.MM_HIMETRIC:
+ // // convert to 0.01 mm to pixel and invert Y
+ // scalingX = states->pxPerMm * 0.01 * 1;
+ // scalingY = states->pxPerMm * 0.01 * -1;
+ // break;
+ // case MapMode.MM_LOENGLISH:
+ // // convert to 0.01 inch to pixel and invert Y
+ // scalingX = states->pxPerMm * 0.01 * mmPerInch * 1;
+ // scalingY = states->pxPerMm * 0.01 * mmPerInch * -1;
+ // break;
+ // case MapMode.MM_HIENGLISH:
+ // // convert to 0.001 inch to pixel and invert Y
+ // scalingX = states->pxPerMm * 0.001 * mmPerInch * 1;
+ // scalingY = states->pxPerMm * 0.001 * mmPerInch * -1;
+ // break;
+ // case MapMode.MM_TWIPS:
+ // // convert to 1 twips to pixel and invert Y
+ // scalingX = states->pxPerMm / 1440 * mmPerInch * 1;
+ // scalingY = states->pxPerMm / 1440 * mmPerInch * -1;
+ // break;
+ // case MapMode.MM_ISOTROPIC:
+ // if (states->windowExSet && states->viewPortExSet)
+ // {
+ // scalingX = states->viewPortExX / states->windowExX;
+ // }
+ // else
+ // {
+ // scalingX = 1.0;
+ // }
+ //
+ // scalingY = scalingX;
+ // windowOrgX = states->windowOrgX;
+ // windowOrgY = states->windowOrgY;
+ // viewPortOrgX = states->viewPortOrgX;
+ // viewPortOrgY = states->viewPortOrgY;
+ // break;
+ case MapMode.MM_ANISOTROPIC:
+ scalingX = (double)state.ViewportExtent.Cx / state.WindowExtent.Cx;
+ scalingY = (double)state.ViewportExtent.Cy / state.WindowExtent.Cy;
+
+ break;
+ default:
+ scalingX = 1.0;
+ scalingY = 1.0;
+ break;
+ }
+ return (scalingX, scalingY);
+ }
+
+ public static (double X, double Y) ScalePointForCurrentMapMode(this EmfState state, double x, double y)
+ {
+ double scalingX;
+ double scalingY;
+ double windowOrgX = 0.0;
+ double windowOrgY = 0.0;
+ double viewPortOrgX = 0.0;
+ double viewPortOrgY = 0.0;
+ switch (state.MapMode)
+ {
+ case MapMode.MM_TEXT:
+ scalingX = 1.0;
+ scalingY = 1.0;
+ break;
+ // case MapMode.MM_LOMETRIC:
+ // // convert to 0.1 mm to pixel and invert Y
+ // scalingX = states->pxPerMm * 0.1 * 1;
+ // scalingY = states->pxPerMm * 0.1 * -1;
+ // break;
+ // case MapMode.MM_HIMETRIC:
+ // // convert to 0.01 mm to pixel and invert Y
+ // scalingX = states->pxPerMm * 0.01 * 1;
+ // scalingY = states->pxPerMm * 0.01 * -1;
+ // break;
+ // case MapMode.MM_LOENGLISH:
+ // // convert to 0.01 inch to pixel and invert Y
+ // scalingX = states->pxPerMm * 0.01 * mmPerInch * 1;
+ // scalingY = states->pxPerMm * 0.01 * mmPerInch * -1;
+ // break;
+ // case MapMode.MM_HIENGLISH:
+ // // convert to 0.001 inch to pixel and invert Y
+ // scalingX = states->pxPerMm * 0.001 * mmPerInch * 1;
+ // scalingY = states->pxPerMm * 0.001 * mmPerInch * -1;
+ // break;
+ // case MapMode.MM_TWIPS:
+ // // convert to 1 twips to pixel and invert Y
+ // scalingX = states->pxPerMm / 1440 * mmPerInch * 1;
+ // scalingY = states->pxPerMm / 1440 * mmPerInch * -1;
+ // break;
+ // case MapMode.MM_ISOTROPIC:
+ // if (states->windowExSet && states->viewPortExSet)
+ // {
+ // scalingX = states->viewPortExX / states->windowExX;
+ // }
+ // else
+ // {
+ // scalingX = 1.0;
+ // }
+ //
+ // scalingY = scalingX;
+ // windowOrgX = states->windowOrgX;
+ // windowOrgY = states->windowOrgY;
+ // viewPortOrgX = states->viewPortOrgX;
+ // viewPortOrgY = states->viewPortOrgY;
+ // break;
+ case MapMode.MM_ANISOTROPIC:
+ scalingX = (double)state.ViewportExtent.Cx / state.WindowExtent.Cx;
+ scalingY = (double)state.ViewportExtent.Cy / state.WindowExtent.Cy;
+
+ break;
+ default:
+ scalingX = 1.0;
+ scalingY = 1.0;
+ break;
+ }
+ return ((x - windowOrgX) * scalingX + viewPortOrgX, (y - windowOrgY) * scalingY + viewPortOrgY);
+ }
+}
\ No newline at end of file
diff --git a/src/SharpEmf.Svg/PlaybackDeviceContext.cs b/src/SharpEmf.Svg/PlaybackDeviceContext.cs
new file mode 100644
index 0000000..adce73c
--- /dev/null
+++ b/src/SharpEmf.Svg/PlaybackDeviceContext.cs
@@ -0,0 +1,15 @@
+using SharpEmf.Enums;
+using SharpEmf.Objects;
+
+namespace SharpEmf.Svg;
+
+internal class PlaybackDeviceContext
+{
+ public BackgroundMode BkMode { get; set; }
+ public PolygonFillMode PolyFillMode { get; set; }
+ public LogBrushEx SelectedBrush { get; set; }
+ public LogPenEx SelectedPen { get; set; }
+ public ColorRef TextColor { get; set; }
+ // TODO: should this be static?
+ public static int ClipId { get; set; } = 1;
+}
\ No newline at end of file
diff --git a/src/SharpEmf.Svg/RecordsProcessing/EmfBitmapRecordsHandlers.cs b/src/SharpEmf.Svg/RecordsProcessing/EmfBitmapRecordsHandlers.cs
new file mode 100644
index 0000000..513fab1
--- /dev/null
+++ b/src/SharpEmf.Svg/RecordsProcessing/EmfBitmapRecordsHandlers.cs
@@ -0,0 +1,19 @@
+using System.Text;
+using SharpEmf.Records.Bitmap;
+
+namespace SharpEmf.Svg.RecordsProcessing;
+
+internal static class EmfBitmapRecordsHandlers
+{
+ public static void HandleStretchDIBits(StringBuilder svgSb, EmfState state, EmrStretchDiBits stretchDiBits)
+ {
+ var bitmapBase64 = BitmapUtils.DibToPngBase64(stretchDiBits.BitsSrc, stretchDiBits.BmiHeader);
+
+ var x = stretchDiBits.XDest;
+ var y = stretchDiBits.YDest;
+ var width = stretchDiBits.CXDest;
+ var height = stretchDiBits.CYDest;
+
+ svgSb.AppendLine($"");
+ }
+}
\ No newline at end of file
diff --git a/src/SharpEmf.Svg/RecordsProcessing/EmfClippingRecordsHandlers.cs b/src/SharpEmf.Svg/RecordsProcessing/EmfClippingRecordsHandlers.cs
new file mode 100644
index 0000000..dcc1cdb
--- /dev/null
+++ b/src/SharpEmf.Svg/RecordsProcessing/EmfClippingRecordsHandlers.cs
@@ -0,0 +1,26 @@
+using System.Text;
+using SharpEmf.Enums;
+using SharpEmf.Records.Clipping;
+
+namespace SharpEmf.Svg.RecordsProcessing;
+
+internal static class EmfClippingRecordsHandlers
+{
+ public static void HandleEmrSelectClipPath(StringBuilder svgSb, EmfState state, EmrSelectClipPath selectClipPath)
+ {
+ if (state.InPath) return;
+
+
+ svgSb.AppendLine("");
+ svgSb.AppendLine($"");
+
+ switch (selectClipPath.RegionMode)
+ {
+ case RegionMode.RGN_AND:
+ break;
+ }
+
+ svgSb.AppendLine("");
+ svgSb.AppendLine("");
+ }
+}
\ No newline at end of file
diff --git a/src/SharpEmf.Svg/RecordsProcessing/EmfControlRecordsHandlers.cs b/src/SharpEmf.Svg/RecordsProcessing/EmfControlRecordsHandlers.cs
new file mode 100644
index 0000000..ed47b41
--- /dev/null
+++ b/src/SharpEmf.Svg/RecordsProcessing/EmfControlRecordsHandlers.cs
@@ -0,0 +1,26 @@
+using System.Text;
+using SharpEmf.Records.Control.Header;
+
+namespace SharpEmf.Svg.RecordsProcessing;
+
+internal static class EmfControlRecordsHandlers
+{
+ public static void HandleHeaderRecord(StringBuilder svgSb, EmfState state, EmfMetafileHeader header)
+ {
+ var width = header.Bounds.Right - header.Bounds.Left;
+ var height = header.Bounds.Bottom - header.Bounds.Top;
+ var gTransform = $"translate({-header.Bounds.Left},{-header.Bounds.Top})";
+
+ state.Scaling = width / MathF.Abs(header.Bounds.Right - header.Bounds.Left);
+
+ // TODO: +1 is a hack to make the object table start at index 1
+ state.ObjectTable = new GraphicsObject[header.Handles + 1];
+
+ svgSb.AppendLine(
+ $"""
+
+