5

Why won't lines etc be anti-aliased when doing this?

using (var myGraphics = Graphics.FromImage(bitmap))
{
myGraphics.CompositingQuality = CompositingQuality.HighQuality;
myGraphics.SmoothingMode = SmoothingMode.HighQuality;
myGraphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;

myGraphics.Clear(backgroundColor);

myGraphics.EnumerateMetafile(m_metafile, new Point(0, 0), m_metafileDelegate);
}

The delegate function looks like this:

private bool MetafileCallback(EmfPlusRecordType recordType, int flags, int dataSize, IntPtr data, PlayRecordCallback callbackData)
{
        byte[] dataArray = null;
        if (data != IntPtr.Zero)
        {
            // Copy the unmanaged record to a managed byte buffer 
            // that can be used by PlayRecord.
            dataArray = new byte[dataSize];
            Marshal.Copy(data, dataArray, 0, dataSize);
        }

        m_metafile.PlayRecord(recordType, flags, dataSize, dataArray);

        return true;
}

Do I need to override PlayRecord for a specific type to get anti-aliasing here?

The WMFs come from AutoCAD, if that's any help.

codekaizen
  • 26,990
  • 7
  • 84
  • 140
Macke
  • 24,812
  • 7
  • 82
  • 118
  • How is this related to WPF? `Graphics` is from `System.Drawing`, `Metafile` is from `System.Drawing.Image`, both namespaces of WinForms. – Clemens Jun 22 '16 at 13:26
  • I'm using it in a WPF-application, but yeah, probably not related. I'll update the tags. – Macke Jun 22 '16 at 13:42

1 Answers1

4

This is not possible in GDI+ using a WMF metafile, but it is with EMF Plus. You can convert to EMF Plus at the source, or on-the-fly with a poorly documented GDI+ method (see below).

GDI (not GDI+) renders the WMF file without using any of the compositing of the GDI+ Graphics object underlying it, it just is an enumeration of the direct GDI calls. See this question for more, but all answers say about the same thing.

If you can convert the file to EMF Plus, this will use the GDI+ methods to render the content, and use the GDI+ compositing including anti-aliasing. If you're already using WPF, you might also consider exporting to XPS which WPF can render antialiased.

If you cannot convert at the source, you can call a GDI+ method from C#, but it is not elegant. You need to have access to the native handles used by the System.Drawing classes:

[DllImport("gdiplus.dll", SetLastError = true, ExactSpelling = true, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
internal static extern int GdipConvertToEmfPlus(HandleRef graphics,
                                                HandleRef metafile,
                                                out Boolean conversionSuccess,
                                                EmfType emfType,
                                                [MarshalAsAttribute(UnmanagedType.LPWStr)]
                                                String description,
                                                out IntPtr convertedMetafile);

You would use this with code similar to the following:

using (var graphics = Graphics.FromImage(bmp))
using (var metafile = Metafile.FromFile(@"drawing.wmf"))
using (var imageAttr = new ImageAttributes())
{
    graphics.SmoothingMode = SmoothingMode.AntiAlias;
    graphics.CompositingQuality = CompositingQuality.HighQuality;
    graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;

    var metafileHandleField = typeof(Metafile).GetField("nativeImage", BindingFlags.Instance | BindingFlags.NonPublic);
    var imageAttributesHandleField = typeof(ImageAttributes).GetField("nativeImageAttributes", BindingFlags.Instance | BindingFlags.NonPublic);
    var graphicsHandleProperty = typeof(Graphics).GetProperty("NativeGraphics", BindingFlags.Instance | BindingFlags.NonPublic);
    var setNativeImage = typeof(Image).GetMethod("SetNativeImage", BindingFlags.Instance | BindingFlags.NonPublic);
    IntPtr mf = (IntPtr)metafileHandleField.GetValue(metafile);
    IntPtr ia = (IntPtr)imageAttributesHandleField.GetValue(imageAttr);
    IntPtr g = (IntPtr)graphicsHandleProperty.GetValue(graphics);

    Boolean isSuccess;
    IntPtr emfPlusHandle;
    var status = GdipConvertToEmfPlus(new HandleRef(graphics, g),
                                      new HandleRef(metafile, mf),
                                      out isSuccess,
                                      EmfType.EmfPlusOnly,
                                      "",
                                      out emfPlusHandle);
    if (status != 0)
    {
        throw new Exception("Can't convert");
    }

    using (var emfPlus = (Metafile)System.Runtime.Serialization.FormatterServices.GetSafeUninitializedObject(typeof(Metafile)))
    {
        setNativeImage.Invoke(emfPlus, new object[] { emfPlusHandle });

        // use EnumerateMetafile on emfPlus as per your example code or save it:
        emfPlus.Save(@"drawing.emf");
    }
}

Here's a working example for LinqPad. It converts a WMF file (drawing.wmf) to an EMF Plus metafile, and displays it in the results panel.

WMF file in Paint: WMF file with no anti-aliasing

Converted EMF+ file in Paint: EMF+ file with anti-aliasing


For the sake of completeness, the above GdipConvertToEmfPlus method is part of what is known as the "flat API" of GDI+. Its original purpose was to serve only the GDI+ C++ classes. The C++ API which uses this method is called Metafile.ConvertToEmfPlus.

Community
  • 1
  • 1
codekaizen
  • 26,990
  • 7
  • 84
  • 140
  • Thanks! I can't really do much about the source of the WMF (AutoCAD LT), so a bit of nativeness is fine in this case. I'll give it a try and get back if it works, which it looks like it should (given my limited WMF/EMF/GDI+ experience). (I figured the GDI functions would support some global "AntiAlias" flag and I've just missed which one.) – Macke Jun 27 '16 at 06:55
  • I'nm not getting there. (this, g) should be (graphics, g), right? Then I get isSuccess=true, but Metafile constructor fails with "generic error System.Runtime.InteropServices.ExternalException (0x80004005): A generic error occurred in GDI+." Also, imageAttr doesn't seem to be used at all? – Macke Jun 27 '16 at 14:23
  • You're right, the code isn't ready to use, and assumes you may have an ImageAttributes, which you may not. Do you have a WMF you can share to reproduce the issue? – codekaizen Jun 27 '16 at 17:13
  • Updated code to provide a working example. There were some problems using the Metafile constructor, so we need to call the `SetNativeHandle` directly in a notably crude, but effective, manner. – codekaizen Jun 27 '16 at 20:52
  • Thanks! Very much appreciated! SetNativeHandle did the trick. I really have to dig into how that works sometime. (Btw, imageAttribute / ia is not used in your code, and it works fine by removing all related calls.) (Also, thanks for opening me to the world of LinqPad ;-) – Macke Jun 28 '16 at 06:39
  • Ouch. I'm not getting the entire WMF, just 1/4 of it. See http://stackoverflow.com/questions/38071340/how-to-convert-wmf-to-emf-with-gdipconverttoemfplus-without-losing-three-quarte – Macke Jun 28 '16 at 08:49
  • When I run this, the result of emfPlus.Save(@"drawing.emf") is actually a PNG file, not an EMF+, so it doesn't scale well. – logicnet.dk Dec 14 '16 at 19:30
  • @logicnet.dk Unfortunately, you cannot save EMF files that way: "When you use the Save method to save a graphic image as a Windows Metafile Format (WMF) or Enhanced Metafile Format (EMF) file, the resulting file is saved as a Portable Network Graphics (PNG) file instead. This behavior occurs because the GDI+ component of the .NET Framework does not have an encoder that you can use to save files as .wmf or .emf files." - https://msdn.microsoft.com/en-us/library/vs/alm/system.drawing.imaging.metafile(v=vs.110).aspx – Sebastian Negraszus Apr 24 '17 at 14:48
  • Note that if your goal is to save the resulting EMF+ to file, simply use `GdipConvertToEmfPlusToFile` instead. You won't need `SetNativeImage` at all. – Ben Brian Apr 26 '18 at 20:58
  • Output file `drawing.emf` it is `PNG` – Andrei Krasutski Feb 11 '19 at 20:59