Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions SharpEmf.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
1 change: 1 addition & 0 deletions SharpEmf.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=SELECTOBJECT/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=SETABORTPROC/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=SETBKMODE/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=setbrushorgex/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=SETCOLORTABLE/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=SETCOPYCOUNT/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=SETDIBSCALING/@EntryIndexedValue">True</s:Boolean>
Expand Down
79 changes: 79 additions & 0 deletions src/SharpEmf.Svg/BitmapUtils.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
19 changes: 19 additions & 0 deletions src/SharpEmf.Svg/EmfState.cs
Original file line number Diff line number Diff line change
@@ -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; }

}
95 changes: 95 additions & 0 deletions src/SharpEmf.Svg/EmfSvgWriter.cs
Original file line number Diff line number Diff line change
@@ -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("</g>");
sb.AppendLine("</svg>");
return sb.ToString();
}
}
10 changes: 10 additions & 0 deletions src/SharpEmf.Svg/GraphicsObject.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
9 changes: 9 additions & 0 deletions src/SharpEmf.Svg/GraphicsObjectType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace SharpEmf.Svg;

internal enum GraphicsObjectType
{
Unknown,
Brush,
Pen,
// TODO: add more stock object (font, color space, palette)
}
142 changes: 142 additions & 0 deletions src/SharpEmf.Svg/Helpers.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading