Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: SVG converter #88

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
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; }

Check warning on line 13 in src/SharpEmf.Svg/EmfState.cs

View workflow job for this annotation

GitHub Actions / macos

Non-nullable property 'ObjectTable' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 13 in src/SharpEmf.Svg/EmfState.cs

View workflow job for this annotation

GitHub Actions / macos

Non-nullable property 'ObjectTable' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 13 in src/SharpEmf.Svg/EmfState.cs

View workflow job for this annotation

GitHub Actions / linux

Non-nullable property 'ObjectTable' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 13 in src/SharpEmf.Svg/EmfState.cs

View workflow job for this annotation

GitHub Actions / linux

Non-nullable property 'ObjectTable' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 13 in src/SharpEmf.Svg/EmfState.cs

View workflow job for this annotation

GitHub Actions / windows

Non-nullable property 'ObjectTable' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 13 in src/SharpEmf.Svg/EmfState.cs

View workflow job for this annotation

GitHub Actions / windows

Non-nullable property 'ObjectTable' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
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;

Check warning on line 11 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / macos

The variable 'windowOrgX' is assigned but its value is never used

Check warning on line 11 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / macos

The variable 'windowOrgX' is assigned but its value is never used

Check warning on line 11 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / linux

The variable 'windowOrgX' is assigned but its value is never used

Check warning on line 11 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / linux

The variable 'windowOrgX' is assigned but its value is never used

Check warning on line 11 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / windows

The variable 'windowOrgX' is assigned but its value is never used

Check warning on line 11 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / windows

The variable 'windowOrgX' is assigned but its value is never used
double windowOrgY = 0.0;

Check warning on line 12 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / macos

The variable 'windowOrgY' is assigned but its value is never used

Check warning on line 12 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / macos

The variable 'windowOrgY' is assigned but its value is never used

Check warning on line 12 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / linux

The variable 'windowOrgY' is assigned but its value is never used

Check warning on line 12 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / linux

The variable 'windowOrgY' is assigned but its value is never used

Check warning on line 12 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / windows

The variable 'windowOrgY' is assigned but its value is never used

Check warning on line 12 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / windows

The variable 'windowOrgY' is assigned but its value is never used
double viewPortOrgX = 0.0;

Check warning on line 13 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / macos

The variable 'viewPortOrgX' is assigned but its value is never used

Check warning on line 13 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / linux

The variable 'viewPortOrgX' is assigned but its value is never used

Check warning on line 13 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / windows

The variable 'viewPortOrgX' is assigned but its value is never used
double viewPortOrgY = 0.0;

Check warning on line 14 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / macos

The variable 'viewPortOrgY' is assigned but its value is never used

Check warning on line 14 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / linux

The variable 'viewPortOrgY' is assigned but its value is never used

Check warning on line 14 in src/SharpEmf.Svg/Helpers.cs

View workflow job for this annotation

GitHub Actions / windows

The variable 'viewPortOrgY' is assigned but its value is never used
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
Loading