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( + $""" + + + + """); + } +} \ No newline at end of file diff --git a/src/SharpEmf.Svg/RecordsProcessing/EmfDrawingRecordsHandlers.cs b/src/SharpEmf.Svg/RecordsProcessing/EmfDrawingRecordsHandlers.cs new file mode 100644 index 0000000..8c4ebe4 --- /dev/null +++ b/src/SharpEmf.Svg/RecordsProcessing/EmfDrawingRecordsHandlers.cs @@ -0,0 +1,77 @@ +using System.Text; +using SharpEmf.Records.Drawing; + +namespace SharpEmf.Svg.RecordsProcessing; + +internal static class EmfDrawingRecordsHandlers +{ + public static void HandlePolyPolygon16(StringBuilder svgSb, EmfState state, EmrPolyPolygon16 polyPolygon16) + { + var points = polyPolygon16.APoints; + var polygonPointCounts = polyPolygon16.PolygonPointCount; + + var scalingForMapMode = state.GetScalingForCurrentMapMode(); + var scaleMatrix = $"matrix({scalingForMapMode.X},0,0,{scalingForMapMode.Y},0,0)"; + + svgSb.Append($""); + } + + public static void HandlePolybezierTo16(StringBuilder svgSb, EmfState state, EmrPolyBezierTo16 polyBezierTo16) + { + if (!state.InPath) + { + return; + } + + var currentPointCounter = 0; + + foreach (var point in polyBezierTo16.APoints) + { + if (currentPointCounter % 3 == 0) + { + svgSb.Append("C "); + } + svgSb.Append($"{point.X} {point.Y} "); + + currentPointCounter++; + } + } + + public static void HandleExtTextOutW(StringBuilder svgSb, EmfState state, EmrExtTextOutW extTextOutW) + { + var scalingForMapMode = state.GetScalingForCurrentMapMode(); + var scaleMatrix = $"matrix({scalingForMapMode.X},0,0,{scalingForMapMode.Y},0,0)"; + + var text = extTextOutW.WEmrText.StringBuffer; + + var (x, y) = state.ScalePointForCurrentMapMode(extTextOutW.WEmrText.Reference.X, extTextOutW.WEmrText.Reference.Y); + + svgSb.AppendLine($"{text}"); + } +} \ No newline at end of file diff --git a/src/SharpEmf.Svg/RecordsProcessing/EmfObjectCreationRecordsHandlers.cs b/src/SharpEmf.Svg/RecordsProcessing/EmfObjectCreationRecordsHandlers.cs new file mode 100644 index 0000000..c03bb5b --- /dev/null +++ b/src/SharpEmf.Svg/RecordsProcessing/EmfObjectCreationRecordsHandlers.cs @@ -0,0 +1,22 @@ +using SharpEmf.Records.ObjectCreation; + +namespace SharpEmf.Svg.RecordsProcessing; + +internal static class EmfObjectCreationRecordsHandlers +{ + public static void HandleCreateBrushIndirect(EmfState state, EmrCreateBrushIndirect createBrushIndirect) + { + var index = createBrushIndirect.IHBrush; + + state.ObjectTable[index].LogBrush = createBrushIndirect.LogBrush; + state.ObjectTable[index].Type = GraphicsObjectType.Brush; + } + + public static void HandleExtCreatePen(EmfState state, EmrExtCreatePen extCreatePen) + { + var index = extCreatePen.IHPen; + + state.ObjectTable[index].LogPen = extCreatePen.Elp; + state.ObjectTable[index].Type = GraphicsObjectType.Pen; + } +} \ No newline at end of file diff --git a/src/SharpEmf.Svg/RecordsProcessing/EmfObjectManipulationRecordsHandlers.cs b/src/SharpEmf.Svg/RecordsProcessing/EmfObjectManipulationRecordsHandlers.cs new file mode 100644 index 0000000..bbfb798 --- /dev/null +++ b/src/SharpEmf.Svg/RecordsProcessing/EmfObjectManipulationRecordsHandlers.cs @@ -0,0 +1,86 @@ +using SharpEmf.Enums; +using SharpEmf.Objects; +using SharpEmf.Records.ObjectManipulation; + +namespace SharpEmf.Svg.RecordsProcessing; + +internal static class EmfObjectManipulationRecordsHandlers +{ + public static void HandleSelectObject(EmfState state, EmrSelectObject selectObject) + { + var index = selectObject.IHObject; + + // Selecting a stock object + if ((index & 0x80000000) != 0) + { + var stockObject = (StockObject)index; + + switch (stockObject) + { + case StockObject.WHITE_BRUSH: + state.CurrentPlaybackDeviceContext.SelectedBrush = new LogBrushEx( + brushStyle: BrushStyle.BS_SOLID, + color: new ColorRef(0xFF, 0xFF, 0xFF)); + break; + case StockObject.LTGRAY_BRUSH: + state.CurrentPlaybackDeviceContext.SelectedBrush = new LogBrushEx( + brushStyle: BrushStyle.BS_SOLID, + color: new ColorRef(0xC0, 0xC0, 0xC0)); + break; + case StockObject.GRAY_BRUSH: + state.CurrentPlaybackDeviceContext.SelectedBrush = new LogBrushEx( + brushStyle: BrushStyle.BS_SOLID, + color: new ColorRef(0x80, 0x80, 0x80)); + break; + case StockObject.DKGRAY_BRUSH: + state.CurrentPlaybackDeviceContext.SelectedBrush = new LogBrushEx( + brushStyle: BrushStyle.BS_SOLID, + color: new ColorRef(0x40, 0x40, 0x40)); + break; + case StockObject.NULL_BRUSH: + state.CurrentPlaybackDeviceContext.SelectedBrush = LogBrushEx.Null; + break; + case StockObject.WHITE_PEN: + state.CurrentPlaybackDeviceContext.SelectedPen = new LogPenEx( + penStyle: PenStyle.PS_COSMETIC | PenStyle.PS_SOLID, + color: new ColorRef(0xFF, 0xFF, 0xFF)); + break; + case StockObject.BLACK_PEN: + state.CurrentPlaybackDeviceContext.SelectedPen = new LogPenEx( + penStyle: PenStyle.PS_COSMETIC | PenStyle.PS_SOLID, + color: new ColorRef(0x00, 0x00, 0x00)); + break; + case StockObject.NULL_PEN: + state.CurrentPlaybackDeviceContext.SelectedPen = LogPenEx.Null; + break; + default: + Console.WriteLine($"Warning: Stock object {stockObject} is not supported"); + // TODO: handle other stock objects + break; + } + + return; + } + + var graphicsObject = state.ObjectTable[index]; + + if (graphicsObject.Type is GraphicsObjectType.Pen) + { + state.CurrentPlaybackDeviceContext.SelectedPen = graphicsObject.LogPen; + } + else if (graphicsObject.Type is GraphicsObjectType.Brush) + { + state.CurrentPlaybackDeviceContext.SelectedBrush = graphicsObject.LogBrush; + } + else if (graphicsObject.Type is GraphicsObjectType.Unknown) + { + Console.WriteLine("Warning: Unknown object type selected"); + } + } + + public static void HandleDeleteObject(EmfState state, EmrDeleteObject deleteObject) + { + var index = deleteObject.IHObject; + state.ObjectTable[index] = default; + } +} \ No newline at end of file diff --git a/src/SharpEmf.Svg/RecordsProcessing/EmfPathBracketRecordsHandlers.cs b/src/SharpEmf.Svg/RecordsProcessing/EmfPathBracketRecordsHandlers.cs new file mode 100644 index 0000000..d12a20e --- /dev/null +++ b/src/SharpEmf.Svg/RecordsProcessing/EmfPathBracketRecordsHandlers.cs @@ -0,0 +1,32 @@ +using System.Text; + +namespace SharpEmf.Svg.RecordsProcessing; + +internal static class EmfPathBracketRecordsHandlers +{ + public static void HandleBeginPath(StringBuilder svgSb, EmfState state) + { + state.InPath = true; + + var scalingForMapMode = state.GetScalingForCurrentMapMode(); + var scaleMatrix = $"matrix({scalingForMapMode.X},0,0,{scalingForMapMode.Y},0,0)"; + + svgSb.Append($""); + } + + public static void HandleCloseFigure(StringBuilder svgSb, EmfState state) + { + svgSb.Append("Z "); + } +} \ No newline at end of file diff --git a/src/SharpEmf.Svg/RecordsProcessing/EmfStateRecordsHandlers.cs b/src/SharpEmf.Svg/RecordsProcessing/EmfStateRecordsHandlers.cs new file mode 100644 index 0000000..bc8c725 --- /dev/null +++ b/src/SharpEmf.Svg/RecordsProcessing/EmfStateRecordsHandlers.cs @@ -0,0 +1,59 @@ +using System.Text; +using SharpEmf.Records.State; + +namespace SharpEmf.Svg.RecordsProcessing; + +internal static class EmfStateRecordsHandlers +{ + public static void HandleSetMapModeRecord(EmfState state, EmrSetMapMode setMapMode) + { + state.MapMode = setMapMode.MapMode; + } + + public static void HandleSetBkModeRecord(EmfState state, EmrSetBkMode setBkMode) + { + state.CurrentPlaybackDeviceContext.BkMode = setBkMode.BackgroundMode; + } + + public static void HandleSetWindowOrgExRecord(EmfState state, EmrSetWindowOrgEx setWindowOrgEx) + { + state.WindowOrigin = setWindowOrgEx.Origin; + } + + public static void HandleSetViewportOrgExRecord(EmfState state, EmrSetViewportOrgEx setViewportOrgEx) + { + state.ViewportOrigin = setViewportOrgEx.Origin; + } + + public static void HandleSetWindowExtExRecord(EmfState state, EmrSetWindowExtEx setWindowExtEx) + { + state.WindowExtent = setWindowExtEx.Extent; + } + + public static void HandleSetViewportExtExRecord(EmfState state, EmrSetViewportExtEx setViewportExtEx) + { + state.ViewportExtent = setViewportExtEx.Extent; + } + + public static void HandleSetPolyfillMode(EmfState state, EmrSetPolyfillMode setPolyfillMode) + { + state.CurrentPlaybackDeviceContext.PolyFillMode = setPolyfillMode.PolygonFillMode; + } + + public static void HandleMoveToEx(StringBuilder svgSb, EmfState state, EmrMoveToEx moveToEx) + { + if (state.InPath) + { + svgSb.Append($"M {moveToEx.Offset.X} {moveToEx.Offset.Y} "); + } + else + { + // TODO: set current position in state + } + } + + public static void HandleSetTextColor(EmfState state, EmrSetTextColor setTextColor) + { + state.CurrentPlaybackDeviceContext.TextColor = setTextColor.Color; + } +} \ No newline at end of file diff --git a/src/SharpEmf.Svg/SharpEmf.Svg.csproj b/src/SharpEmf.Svg/SharpEmf.Svg.csproj new file mode 100644 index 0000000..c320b52 --- /dev/null +++ b/src/SharpEmf.Svg/SharpEmf.Svg.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + diff --git a/src/SharpEmf.Svg/Utils.cs b/src/SharpEmf.Svg/Utils.cs new file mode 100644 index 0000000..b91c193 --- /dev/null +++ b/src/SharpEmf.Svg/Utils.cs @@ -0,0 +1,65 @@ +using System.Text; +using SharpEmf.Enums; + +namespace SharpEmf.Svg; + +internal static class Utils +{ + public static void AppendFill(StringBuilder svgSb, EmfState state) + { + var fillRuleStr = state.CurrentPlaybackDeviceContext.PolyFillMode switch + { + PolygonFillMode.WINDING => "fill-rule=\"nonzero\" ", + PolygonFillMode.ALTERNATE => "fill-rule=\"evenodd\" ", + _ => "" + }; + + switch (state.CurrentPlaybackDeviceContext.SelectedBrush.BrushStyle) + { + case BrushStyle.BS_NULL: + svgSb.Append("fill=\"none\" "); + break; + case BrushStyle.BS_SOLID: + svgSb.Append(fillRuleStr); + + svgSb.Append($"fill=\"#{state.CurrentPlaybackDeviceContext.SelectedBrush.Color.Red:X2}"); + svgSb.Append($"{state.CurrentPlaybackDeviceContext.SelectedBrush.Color.Green:X2}"); + svgSb.Append($"{state.CurrentPlaybackDeviceContext.SelectedBrush.Color.Blue:X2}\" "); + break; + case BrushStyle.BS_HATCHED: + case BrushStyle.BS_PATTERN: + case BrushStyle.BS_INDEXED: + case BrushStyle.BS_DIBPATTERN: + case BrushStyle.BS_DIBPATTERNPT: + case BrushStyle.BS_PATTERN8X8: + case BrushStyle.BS_DIBPATTERN8X8: + default: + svgSb.Append($"fill=\"#{state.CurrentPlaybackDeviceContext.SelectedBrush.Color.Red:X2}"); + svgSb.Append($"{state.CurrentPlaybackDeviceContext.SelectedBrush.Color.Green:X2}"); + svgSb.Append($"{state.CurrentPlaybackDeviceContext.SelectedBrush.Color.Blue:X2}\" "); + break; + } + } + + public static void AppendStroke(StringBuilder svgSb, EmfState state) + { + // TODO: use scaling from state + var selectedPen = state.CurrentPlaybackDeviceContext.SelectedPen; + if (selectedPen.PenStyle is PenStyle.PS_NULL) + { + svgSb.Append("stroke=\"none\" "); + } + + // switch (state.CurrentPlaybackDeviceContext.SelectedGraphicsObject.LogPen.PenStyle) + // { + // case PenStyle.PS_COSMETIC: + // sb.Append($"stroke-width=\"{stroke}\""); + // break; + // case PenStyle.PS_GEOMETRIC: + // sb.Append($"stroke-width=\"{stroke}\""); + // break; + // } + + // TODO: handle other pen styles + } +} \ No newline at end of file diff --git a/src/SharpEmf/Enums/EmfRecordType.cs b/src/SharpEmf/Enums/EmfRecordType.cs index c6ea5ff..3bf103e 100644 --- a/src/SharpEmf/Enums/EmfRecordType.cs +++ b/src/SharpEmf/Enums/EmfRecordType.cs @@ -95,6 +95,11 @@ public enum EmfRecordType : uint /// EMR_SETVIEWPORTORGEX = 0x0000000C, + /// + /// Defines the origin of the current brush + /// + EMR_SETBRUSHORGEX = 0x0000000D, + /// /// Defines the mapping mode, which defines the unit of measure used to transform page space units into device space units, /// and defines the orientation of the device's X and Y axes @@ -121,6 +126,11 @@ public enum EmfRecordType : uint /// EMR_SETPIXELV = 0x0000000F, + /// + /// Defines the current text foreground color + /// + EMR_SETTEXTCOLOR = 0x00000018, + /// /// Redefines the current clipping region by the specified offsets /// diff --git a/src/SharpEmf/Enums/PenStyle.cs b/src/SharpEmf/Enums/PenStyle.cs index efc98eb..6b588e3 100644 --- a/src/SharpEmf/Enums/PenStyle.cs +++ b/src/SharpEmf/Enums/PenStyle.cs @@ -7,6 +7,7 @@ namespace SharpEmf.Enums; /// A pen style is a combination of pen type, line style, line cap, and line join /// [PublicAPI] +[Flags] public enum PenStyle : uint { /// diff --git a/src/SharpEmf/Extensions/StreamExtensions.cs b/src/SharpEmf/Extensions/StreamExtensions.cs index 7aca345..cad631b 100644 --- a/src/SharpEmf/Extensions/StreamExtensions.cs +++ b/src/SharpEmf/Extensions/StreamExtensions.cs @@ -75,6 +75,8 @@ internal static byte[] ReadByteArray(this Stream stream, int length) var buffer = new byte[length]; stream.ReadExactly(buffer); + buffer.AsSpan().Reverse(); + return buffer; } diff --git a/src/SharpEmf/Objects/ColorRef.cs b/src/SharpEmf/Objects/ColorRef.cs index e226f97..387bed8 100644 --- a/src/SharpEmf/Objects/ColorRef.cs +++ b/src/SharpEmf/Objects/ColorRef.cs @@ -26,6 +26,14 @@ public readonly struct ColorRef // DO NOT remove this field, it is required for proper calculation of the struct size private readonly byte _reserved; + public ColorRef(byte red, byte green, byte blue) + { + Red = red; + Green = green; + Blue = blue; + _reserved = 0; + } + private ColorRef(byte red, byte green, byte blue, byte reserved) { Red = red; @@ -36,6 +44,7 @@ private ColorRef(byte red, byte green, byte blue, byte reserved) public static ColorRef Parse(Stream stream) { + // TODO: read this as Int32 and reinterpret as ColorRef via Unsafe.BitCast to reduce the number of stream reads var red = stream.ReadByte(); var green = stream.ReadByte(); var blue = stream.ReadByte(); diff --git a/src/SharpEmf/Objects/EmrText.cs b/src/SharpEmf/Objects/EmrText.cs index 2ac0852..d83fc80 100644 --- a/src/SharpEmf/Objects/EmrText.cs +++ b/src/SharpEmf/Objects/EmrText.cs @@ -97,7 +97,7 @@ private EmrText( DxBuffer = dxBuffer; } - public static EmrText Parse(Stream stream, EmfRecordType parentRecordType, int parentSizeWithoutTextBuffer) + public static EmrText Parse(Stream stream, EmfRecordType parentRecordType, long parentSizeWithoutTextBuffer) { var reference = PointL.Parse(stream); var chars = stream.ReadUInt32(); diff --git a/src/SharpEmf/Objects/LogBrushEx.cs b/src/SharpEmf/Objects/LogBrushEx.cs index f6134c2..85f1a86 100644 --- a/src/SharpEmf/Objects/LogBrushEx.cs +++ b/src/SharpEmf/Objects/LogBrushEx.cs @@ -37,6 +37,13 @@ public readonly struct LogBrushEx /// public HatchStyle HatchStyle { get; } + public LogBrushEx(BrushStyle brushStyle, ColorRef color) + { + BrushStyle = brushStyle; + Color = color; + HatchStyle = HatchStyle.HS_SOLIDCLR; + } + private LogBrushEx(BrushStyle brushStyle, ColorRef color, HatchStyle hatchStyle) { BrushStyle = brushStyle; @@ -44,6 +51,8 @@ private LogBrushEx(BrushStyle brushStyle, ColorRef color, HatchStyle hatchStyle) HatchStyle = hatchStyle; } + public static LogBrushEx Null { get; } = new(BrushStyle.BS_NULL, color: default); + public static LogBrushEx Parse(Stream stream) { var brushStyle = stream.ReadEnum(); diff --git a/src/SharpEmf/Objects/LogPenEx.cs b/src/SharpEmf/Objects/LogPenEx.cs index 5047ecf..48fa984 100644 --- a/src/SharpEmf/Objects/LogPenEx.cs +++ b/src/SharpEmf/Objects/LogPenEx.cs @@ -33,7 +33,7 @@ namespace SharpEmf.Objects; /// /// [PublicAPI] -public class LogPenEx +public readonly struct LogPenEx { /// /// Specifies the pen style @@ -89,6 +89,14 @@ public class LogPenEx /// public IReadOnlyList StyleEntry { get; } + public LogPenEx(PenStyle penStyle, ColorRef color) + { + PenStyle = penStyle; + ColorRef = color; + Width = 1; + BrushStyle = BrushStyle.BS_SOLID; + } + private LogPenEx(PenStyle penStyle, uint width, BrushStyle brushStyle, ColorRef colorRef, HatchStyle brushHatch, uint numStyleEntries, IReadOnlyList styleEntry) { PenStyle = penStyle; @@ -100,6 +108,8 @@ private LogPenEx(PenStyle penStyle, uint width, BrushStyle brushStyle, ColorRef StyleEntry = styleEntry; } + public static LogPenEx Null { get; } = new(PenStyle.PS_NULL, color: default); + public static LogPenEx Parse(Stream stream) { var penStyle = stream.ReadEnum(); diff --git a/src/SharpEmf/Records/Bitmap/EmrStretchDiBits.cs b/src/SharpEmf/Records/Bitmap/EmrStretchDiBits.cs index 49424aa..49fd1aa 100644 --- a/src/SharpEmf/Records/Bitmap/EmrStretchDiBits.cs +++ b/src/SharpEmf/Records/Bitmap/EmrStretchDiBits.cs @@ -3,6 +3,7 @@ using SharpEmf.Enums; using SharpEmf.Extensions; using SharpEmf.WmfTypes; +using SharpEmf.WmfTypes.Bitmap; namespace SharpEmf.Records.Bitmap; @@ -96,12 +97,12 @@ public record EmrStretchDiBits : EnhancedMetafileRecord /// /// The source bitmap header /// - public IReadOnlyList BmiSrc { get; } + public BitmapInfoHeader BmiHeader { get; } /// /// The source bitmap bits /// - public IReadOnlyList BitsSrc { get; } + public byte[] BitsSrc { get; } private EmrStretchDiBits( EmfRecordType recordType, @@ -121,8 +122,8 @@ private EmrStretchDiBits( TernaryRasterOperation bitBltRasterOperation, int cxDest, int cyDest, - IReadOnlyList bmiSrc, - IReadOnlyList bitsSrc) : base(recordType, size) + BitmapInfoHeader bmiHeader, + byte[] bitsSrc) : base(recordType, size) { Bounds = bounds; XDest = xDest; @@ -139,12 +140,17 @@ private EmrStretchDiBits( BitBltRasterOperation = bitBltRasterOperation; CXDest = cxDest; CYDest = cyDest; - BmiSrc = bmiSrc; + BmiHeader = bmiHeader; BitsSrc = bitsSrc; } public static EmrStretchDiBits Parse(Stream stream, EmfRecordType recordType, uint size) { + var positionBeforeParsing = + stream.Position - + // Base record fields + (Unsafe.SizeOf() + Unsafe.SizeOf()); + var bounds = RectL.Parse(stream); var xDest = stream.ReadInt32(); var yDest = stream.ReadInt32(); @@ -161,26 +167,8 @@ public static EmrStretchDiBits Parse(Stream stream, EmfRecordType recordType, ui var cxDest = stream.ReadInt32(); var cyDest = stream.ReadInt32(); - var selfSizeWithoutBuffers = - // Base record fields - Unsafe.SizeOf() + - Unsafe.SizeOf() + - // Self fields - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf(); + var positionAfterParsing = stream.Position; + var selfSizeWithoutBuffers = positionAfterParsing - positionBeforeParsing; long seekOffset = 0; if (offBmiSrc != 0) @@ -189,11 +177,11 @@ public static EmrStretchDiBits Parse(Stream stream, EmfRecordType recordType, ui stream.Seek(seekOffset, SeekOrigin.Current); } - var bmiSrc = stream.ReadByteArray((int)cbBmiSrc); + var bmiSrc = BitmapInfoHeader.Parse(stream); if (offBitsSrc != 0) { - seekOffset = offBitsSrc - (seekOffset + bmiSrc.Length + selfSizeWithoutBuffers); + seekOffset = offBitsSrc - (seekOffset + bmiSrc.Size + selfSizeWithoutBuffers); stream.Seek(seekOffset, SeekOrigin.Current); } diff --git a/src/SharpEmf/Records/Drawing/EmrExtTextOutA.cs b/src/SharpEmf/Records/Drawing/EmrExtTextOutA.cs index 6ec4800..2e64395 100644 --- a/src/SharpEmf/Records/Drawing/EmrExtTextOutA.cs +++ b/src/SharpEmf/Records/Drawing/EmrExtTextOutA.cs @@ -61,20 +61,18 @@ private EmrExtTextOutA( public static EmrExtTextOutA Parse(Stream stream, EmfRecordType recordType, uint size) { + var positionBeforeParsing = + stream.Position - + // Base record fields + (Unsafe.SizeOf() + Unsafe.SizeOf()); + var bounds = RectL.Parse(stream); var iGraphicsMode = stream.ReadEnum(); var exScale = stream.ReadFloat32(); var eyScale = stream.ReadFloat32(); - var selfSizeWithoutTextBuffer = - // Base record fields - Unsafe.SizeOf() + - Unsafe.SizeOf() + - // This record fields - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf(); + var positionAfterParsing = stream.Position; + var selfSizeWithoutTextBuffer = positionAfterParsing - positionBeforeParsing; var aEmrText = EmrText.Parse(stream, recordType, selfSizeWithoutTextBuffer); diff --git a/src/SharpEmf/Records/Drawing/EmrExtTextOutW.cs b/src/SharpEmf/Records/Drawing/EmrExtTextOutW.cs index d4f8958..f4f6414 100644 --- a/src/SharpEmf/Records/Drawing/EmrExtTextOutW.cs +++ b/src/SharpEmf/Records/Drawing/EmrExtTextOutW.cs @@ -61,20 +61,18 @@ private EmrExtTextOutW( public static EmrExtTextOutW Parse(Stream stream, EmfRecordType recordType, uint size) { + var positionBeforeParsing = + stream.Position - + // Base record fields + (Unsafe.SizeOf() + Unsafe.SizeOf()); + var bounds = RectL.Parse(stream); var iGraphicsMode = stream.ReadEnum(); var exScale = stream.ReadFloat32(); var eyScale = stream.ReadFloat32(); - var selfSizeWithoutTextBuffer = - // Base record fields - Unsafe.SizeOf() + - Unsafe.SizeOf() + - // This record fields - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf(); + var positionAfterParsing = stream.Position; + var selfSizeWithoutTextBuffer = positionAfterParsing - positionBeforeParsing; var wEmrText = EmrText.Parse(stream, recordType, selfSizeWithoutTextBuffer); diff --git a/src/SharpEmf/Records/Drawing/EmrPolyTextOutA.cs b/src/SharpEmf/Records/Drawing/EmrPolyTextOutA.cs index d8f9369..a9379f3 100644 --- a/src/SharpEmf/Records/Drawing/EmrPolyTextOutA.cs +++ b/src/SharpEmf/Records/Drawing/EmrPolyTextOutA.cs @@ -71,22 +71,19 @@ private EmrPolyTextOutA( public static EmrPolyTextOutA Parse(Stream stream, EmfRecordType recordType, uint size) { + var positionBeforeParsing = + stream.Position - + // Base record fields + (Unsafe.SizeOf() + Unsafe.SizeOf()); + var bounds = RectL.Parse(stream); var iGraphicsMode = stream.ReadEnum(); var exScale = stream.ReadFloat32(); var eyScale = stream.ReadFloat32(); var cStrings = stream.ReadUInt32(); - var selfSizeWithoutTextBuffers = - // Base record fields - Unsafe.SizeOf() + - Unsafe.SizeOf() + - // This record fields - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf(); + var positionAfterParsing = stream.Position; + var selfSizeWithoutTextBuffers = positionAfterParsing - positionBeforeParsing; var aEmrTexts = new List((int)cStrings); diff --git a/src/SharpEmf/Records/Drawing/EmrPolyTextOutW.cs b/src/SharpEmf/Records/Drawing/EmrPolyTextOutW.cs index 350fa02..0dbd04c 100644 --- a/src/SharpEmf/Records/Drawing/EmrPolyTextOutW.cs +++ b/src/SharpEmf/Records/Drawing/EmrPolyTextOutW.cs @@ -71,22 +71,19 @@ private EmrPolyTextOutW( public static EmrPolyTextOutW Parse(Stream stream, EmfRecordType recordType, uint size) { + var positionBeforeParsing = + stream.Position - + // Base record fields + (Unsafe.SizeOf() + Unsafe.SizeOf()); + var bounds = RectL.Parse(stream); var iGraphicsMode = stream.ReadEnum(); var exScale = stream.ReadFloat32(); var eyScale = stream.ReadFloat32(); var cStrings = stream.ReadUInt32(); - var selfSizeWithoutTextBuffers = - // Base record fields - Unsafe.SizeOf() + - Unsafe.SizeOf() + - // This record fields - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf() + - Unsafe.SizeOf(); + var positionAfterParsing = stream.Position; + var selfSizeWithoutTextBuffers = positionAfterParsing - positionBeforeParsing; var wEmrTexts = new List((int)cStrings); diff --git a/src/SharpEmf/Records/EnhancedMetafileRecord.cs b/src/SharpEmf/Records/EnhancedMetafileRecord.cs index 80c7019..329b814 100644 --- a/src/SharpEmf/Records/EnhancedMetafileRecord.cs +++ b/src/SharpEmf/Records/EnhancedMetafileRecord.cs @@ -55,10 +55,12 @@ public static EnhancedMetafileRecord Parse(Stream stream) EmfRecordType.EMR_SETWINDOWORGEX => EmrSetWindowOrgEx.Parse, EmfRecordType.EMR_SETVIEWPORTEXTEX => EmrSetViewportExtEx.Parse, EmfRecordType.EMR_SETVIEWPORTORGEX => EmrSetViewportOrgEx.Parse, + EmfRecordType.EMR_SETBRUSHORGEX => EmrSetBrushOrgEx.Parse, EmfRecordType.EMR_SETMAPMODE => EmrSetMapMode.Parse, EmfRecordType.EMR_SETBKMODE => EmrSetBkMode.Parse, EmfRecordType.EMR_SETPOLYFILLMODE => EmrSetPolyfillMode.Parse, EmfRecordType.EMR_SETPIXELV => EmrSetPixelV.Parse, + EmfRecordType.EMR_SETTEXTCOLOR => EmrSetTextColor.Parse, EmfRecordType.EMR_OFFSETCLIPRGN => EmrOffsetClipRgn.Parse, EmfRecordType.EMR_MOVETOEX => EmrMoveToEx.Parse, EmfRecordType.EMR_EXCLUDECLIPRECT => EmrExcludeClipRect.Parse, @@ -128,6 +130,8 @@ private static PlaceholderRecord SkipRecord(Stream stream, EmfRecordType recordT throw new MissingEmfRecordParserException(recordType); } + Console.WriteLine($"Skipped parsing EMF record of type: {recordType:X}"); + stream.Seek(size - 8, SeekOrigin.Current); return new PlaceholderRecord(recordType, size); } diff --git a/src/SharpEmf/Records/ObjectCreation/EmrExtCreatePen.cs b/src/SharpEmf/Records/ObjectCreation/EmrExtCreatePen.cs index b1b1b1d..5dca443 100644 --- a/src/SharpEmf/Records/ObjectCreation/EmrExtCreatePen.cs +++ b/src/SharpEmf/Records/ObjectCreation/EmrExtCreatePen.cs @@ -17,7 +17,7 @@ public record EmrExtCreatePen : EnhancedMetafileRecord, IEmfParsable /// This index MUST be saved so that this object can be reused or modified /// - public uint IhPen { get; } + public uint IHPen { get; } /// /// Specifies the offset from the start of this record to the DIB header if the record contains a DIB @@ -66,7 +66,7 @@ private EmrExtCreatePen( IReadOnlyList bmiSrc, IReadOnlyList bitsSrc) : base(recordType, size) { - IhPen = ihPen; + IHPen = ihPen; OffBmi = offBmi; CbBmi = cbBmi; OffBits = offBits; diff --git a/src/SharpEmf/Records/ObjectManipulation/EmrDeleteObject.cs b/src/SharpEmf/Records/ObjectManipulation/EmrDeleteObject.cs index 342edf0..26fc7a9 100644 --- a/src/SharpEmf/Records/ObjectManipulation/EmrDeleteObject.cs +++ b/src/SharpEmf/Records/ObjectManipulation/EmrDeleteObject.cs @@ -17,11 +17,11 @@ public record EmrDeleteObject : EnhancedMetafileRecord, IEmfParsable, which cannot be deleted /// - public uint IhObject { get; } + public uint IHObject { get; } private EmrDeleteObject(EmfRecordType Type, uint Size, uint ihObject) : base(Type, Size) { - IhObject = ihObject; + IHObject = ihObject; } public static EmrDeleteObject Parse(Stream stream, EmfRecordType recordType, uint size) diff --git a/src/SharpEmf/Records/State/EmrSetBrushOrgEx.cs b/src/SharpEmf/Records/State/EmrSetBrushOrgEx.cs new file mode 100644 index 0000000..1ee1d43 --- /dev/null +++ b/src/SharpEmf/Records/State/EmrSetBrushOrgEx.cs @@ -0,0 +1,24 @@ +using JetBrains.Annotations; +using SharpEmf.Enums; +using SharpEmf.Interfaces; +using SharpEmf.WmfTypes; + +namespace SharpEmf.Records.State; + +/// +[PublicAPI] +public record EmrSetBrushOrgEx : EnhancedMetafileRecord, IEmfParsable +{ + public PointL Origin { get; } + + private EmrSetBrushOrgEx(EmfRecordType Type, uint Size, PointL origin) : base(Type, Size) + { + Origin = origin; + } + + public static EmrSetBrushOrgEx Parse(Stream stream, EmfRecordType recordType, uint size) + { + var origin = PointL.Parse(stream); + return new EmrSetBrushOrgEx(recordType, size, origin); + } +} \ No newline at end of file diff --git a/src/SharpEmf/Records/State/EmrSetTextColor.cs b/src/SharpEmf/Records/State/EmrSetTextColor.cs new file mode 100644 index 0000000..432e688 --- /dev/null +++ b/src/SharpEmf/Records/State/EmrSetTextColor.cs @@ -0,0 +1,24 @@ +using JetBrains.Annotations; +using SharpEmf.Enums; +using SharpEmf.Interfaces; +using SharpEmf.Objects; + +namespace SharpEmf.Records.State; + +/// +[PublicAPI] +public record EmrSetTextColor : EnhancedMetafileRecord, IEmfParsable +{ + public ColorRef Color { get; } + + private EmrSetTextColor(EmfRecordType Type, uint Size, ColorRef color) : base(Type, Size) + { + Color = color; + } + + public static EmrSetTextColor Parse(Stream stream, EmfRecordType recordType, uint size) + { + var color = ColorRef.Parse(stream); + return new EmrSetTextColor(recordType, size, color); + } +} \ No newline at end of file diff --git a/src/SharpEmf/SharpEmf.csproj b/src/SharpEmf/SharpEmf.csproj index d57043d..2d9ab6c 100644 --- a/src/SharpEmf/SharpEmf.csproj +++ b/src/SharpEmf/SharpEmf.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/SharpEmf/WmfTypes/Bitmap/BitCount.cs b/src/SharpEmf/WmfTypes/Bitmap/BitCount.cs new file mode 100644 index 0000000..d558309 --- /dev/null +++ b/src/SharpEmf/WmfTypes/Bitmap/BitCount.cs @@ -0,0 +1,42 @@ +namespace SharpEmf.WmfTypes.Bitmap; + +/// +/// Specifies the number of bits that define each pixel and the maximum number of colors in a device-independent bitmap (DIB) +/// +public enum BitCount : ushort +{ + /// + /// The number of bits per pixel is undefined + /// + BI_BITCOUNT_0 = 0x0000, + + /// + /// The image is specified with two colors + /// + BI_BITCOUNT_1 = 0x0001, + + /// + /// The image is specified with a maximum of 16 colors. + /// + BI_BITCOUNT_2 = 0x0004, + + /// + /// The image is specified with a maximum of 256 colors + /// + BI_BITCOUNT_3 = 0x0008, + + /// + /// The image is specified with a maximum of 2^16 colors + /// + BI_BITCOUNT_4 = 0x0010, + + /// + /// The bitmap in the BitmapBuffer field of the DIB Object has a maximum of 2^24 colors + /// + BI_BITCOUNT_5 = 0x0018, + + /// + /// The bitmap in the BitmapBuffer field of the DIB Object has a maximum of 2^24 colors + /// + BI_BITCOUNT_6 = 0x0020 +} \ No newline at end of file diff --git a/src/SharpEmf/WmfTypes/Bitmap/BitmapInfoHeader.cs b/src/SharpEmf/WmfTypes/Bitmap/BitmapInfoHeader.cs new file mode 100644 index 0000000..dfe6993 --- /dev/null +++ b/src/SharpEmf/WmfTypes/Bitmap/BitmapInfoHeader.cs @@ -0,0 +1,150 @@ +using SharpEmf.Extensions; + +namespace SharpEmf.WmfTypes.Bitmap; + +/// +/// Contains information about the dimensions and color format of a device-independent bitmap (DIB) +/// +public class BitmapInfoHeader +{ + /// + /// Defines the size of this object, in bytes + /// + public uint Size { get; } + + /// + /// Defines the width of the DIB, in pixels. This value MUST be positive + /// + public int Width { get; } + + /// + /// Defines the height of the DIB, in pixels. This value MUST NOT be zero + /// + /// + /// If this value is positive, the bitmap is a bottom-up DIB and its origin is the lower-left corner. + /// This field SHOULD specify the height of the decompressed image file, if the Compression value specifies JPEG or PNG format + ///

+ /// If this value is negative, the image is a top-down DIB and its origin is the upper-left corner. + /// Top-down bitmaps do not support compression + /// + public int Height { get; } + + /// + /// Defines the number of planes for the target device. This value MUST be 0x0001 + /// + public ushort Planes { get; } + + /// + /// Defines the number of bits that define each pixel and the maximum number of colors in the DIB + /// + public BitCount BitCount { get; } + + /// + /// Defines the compression mode of the DIB + /// + public Compression Compression { get; } + + /// + /// Defines the size, in bytes, of the image + /// + /// + /// If the value is , this value SHOULD be zero and MUST be ignored + ///

+ /// If the ; value is or , this value MUST specify the size of the JPEG or PNG image buffer, respectively + ///
+ public uint SizeImage { get; } + + /// + /// Defines the horizontal resolution, in pixels-per-meter, of the target device for the DIB + /// + public int XPelsPerMeter { get; } + + /// + /// Defines the vertical resolution, in pixels-per-meter, of the target device for the DIB + /// + public int YPelsPerMeter { get; } + + /// + /// Specifies the number of indexes in the color table used by the DIB, as follows: + /// + /// + /// + /// + /// If this value is zero, the DIB uses the maximum number of colors that correspond to the value + /// + /// + /// + /// If this value is nonzero and the value is less than 16, this value specifies the number of colors used by the DIB + /// + /// + /// + /// If this value is nonzero and the value is 16 or greater, + /// this value specifies the size of the color table used to optimize performance of the system palette + /// + /// + /// + /// + public uint ColorUsed { get; } + + /// + /// Defines the number of color indexes that are required for displaying the DIB + /// + /// If this value is zero, all color indexes are required + /// + /// + public uint ColorImportant { get; } + + private BitmapInfoHeader( + uint size, + int width, + int height, + ushort planes, + BitCount bitCount, + Compression compression, + uint sizeImage, + int xPelsPerMeter, + int yPelsPerMeter, + uint colorUsed, + uint colorImportant) + { + Size = size; + Width = width; + Height = height; + Planes = planes; + BitCount = bitCount; + Compression = compression; + SizeImage = sizeImage; + XPelsPerMeter = xPelsPerMeter; + YPelsPerMeter = yPelsPerMeter; + ColorUsed = colorUsed; + ColorImportant = colorImportant; + } + + public static BitmapInfoHeader Parse(Stream stream) + { + var size = stream.ReadUInt32(); + var width = stream.ReadInt32(); + var height = stream.ReadInt32(); + var planes = stream.ReadUInt16(); + var bitCount = stream.ReadEnum(); + var compression = stream.ReadEnum(); + var sizeImage = stream.ReadUInt32(); + var xPelsPerMeter = stream.ReadInt32(); + var yPelsPerMeter = stream.ReadInt32(); + var colorUsed = stream.ReadUInt32(); + var colorImportant = stream.ReadUInt32(); + + return new BitmapInfoHeader( + size, + width, + height, + planes, + bitCount, + compression, + sizeImage, + xPelsPerMeter, + yPelsPerMeter, + colorUsed, + colorImportant); + } +} \ No newline at end of file diff --git a/src/SharpEmf/WmfTypes/Bitmap/Compression.cs b/src/SharpEmf/WmfTypes/Bitmap/Compression.cs new file mode 100644 index 0000000..42cca6d --- /dev/null +++ b/src/SharpEmf/WmfTypes/Bitmap/Compression.cs @@ -0,0 +1,17 @@ +namespace SharpEmf.WmfTypes.Bitmap; + +/// +/// Specifies the type of compression for a bitmap image +/// +public enum Compression : uint +{ + BI_RGB = 0x0000, + BI_RLE8 = 0x0001, + BI_RLE4 = 0x0002, + BI_BITFIELDS = 0x0003, + BI_JPEG = 0x0004, + BI_PNG = 0x0005, + BI_CMYK = 0x000B, + BI_CMYKRLE8 = 0x000C, + BI_CMYKRLE4 = 0x000D +} \ No newline at end of file diff --git a/src/SharpEmf/WmfTypes/PointL.cs b/src/SharpEmf/WmfTypes/PointL.cs index 12d1bce..05a3e65 100644 --- a/src/SharpEmf/WmfTypes/PointL.cs +++ b/src/SharpEmf/WmfTypes/PointL.cs @@ -1,4 +1,5 @@ -using JetBrains.Annotations; +using System.Diagnostics; +using JetBrains.Annotations; using SharpEmf.Extensions; namespace SharpEmf.WmfTypes; @@ -7,6 +8,7 @@ namespace SharpEmf.WmfTypes; /// Defines the coordinates of a point /// [PublicAPI] +[DebuggerDisplay("X: {X}, Y: {Y}")] public readonly struct PointL { /// @@ -25,6 +27,7 @@ private PointL(int x, int y) Y = y; } + // TODO: read this as Int64 and reinterpret as PointL via Unsafe.BitCast to reduce the number of stream reads public static PointL Parse(Stream stream) => new( x: stream.ReadInt32(), y: stream.ReadInt32()); diff --git a/src/SharpEmf/WmfTypes/PointS.cs b/src/SharpEmf/WmfTypes/PointS.cs index 0b23528..563f240 100644 --- a/src/SharpEmf/WmfTypes/PointS.cs +++ b/src/SharpEmf/WmfTypes/PointS.cs @@ -25,6 +25,7 @@ private PointS(short x, short y) Y = y; } + // TODO: read this as Int32 and reinterpret as PointS via Unsafe.BitCast to reduce the number of stream reads public static PointS Parse(Stream stream) => new( x: stream.ReadInt16(), y: stream.ReadInt16()); diff --git a/src/SharpEmf/WmfTypes/RectL.cs b/src/SharpEmf/WmfTypes/RectL.cs index 6718eb1..b3bf257 100644 --- a/src/SharpEmf/WmfTypes/RectL.cs +++ b/src/SharpEmf/WmfTypes/RectL.cs @@ -39,6 +39,7 @@ internal RectL(int left, int top, int right, int bottom) Bottom = bottom; } + // TODO: read this as Int128 and reinterpret as RectL via Unsafe.BitCast to reduce the number of stream reads public static RectL Parse(Stream stream) => new( left: stream.ReadInt32(), top: stream.ReadInt32(), diff --git a/src/SharpEmf/WmfTypes/SizeL.cs b/src/SharpEmf/WmfTypes/SizeL.cs index 5265923..b335ecb 100644 --- a/src/SharpEmf/WmfTypes/SizeL.cs +++ b/src/SharpEmf/WmfTypes/SizeL.cs @@ -25,6 +25,7 @@ private SizeL(uint cx, uint cy) Cy = cy; } + // TODO: read this as Int64 and reinterpret as SizeL via Unsafe.BitCast to reduce the number of stream reads public static SizeL Parse(Stream stream) => new( cx: stream.ReadUInt32(), cy: stream.ReadUInt32());