Table of Contents

Windows metafiles

Egami.Metafiles plays Windows metafiles, the vector pictures Office and Windows programs exchange: WMF (Windows Metafile), EMF (Enhanced Metafile) and EMF+ (GDI+ records inside an EMF), and their gzip forms WMZ and EMZ. It draws them through Egami.Drawing's canvas, so a chart pasted from Excel stays vector on a PDF page or in SVG output and rasterizes the same on any OS. It only reads and plays: it does not write metafiles. It is fully managed.

Loading

Metafile.Load reads a file, a stream or bytes in memory; gzip is detected by its first bytes. Metafile.Kind says what it holds (a MetafileKind: Wmf, Emf, EmfPlusDual or EmfPlusOnly), and the header gives the picture's size:

  • Size and Frame are in points: an EMF's frame rectangle (0.01 mm), a placeable WMF's box and units per inch, or, for a WMF without a placeable header, its window extent at 96 DPI (with a Metafile.NoPlaceableHeader warning).
  • Dpi is the resolution the records were made at, and Description an EMF's application and picture names.

Loading is lenient: a record that runs past the end of the file ends the walk with a warning, and what came before it plays. Warnings lists what the load found; MetafileLoadOptions.Context sets the limits (the file size and what EMZ and WMZ inflate to) and the diagnostics handler.

// Untrusted files: the context's limits bound the file size and what EMZ and WMZ inflate to; a record that runs past
// the end is skipped with a warning, and what came before it plays.
MetafileLoadOptions options = new()
{
    Context = EgamiContext.Default with { Limits = SafetyLimits.Default with { MaxTotalBytes = 16 << 20 } },
};
Metafile picture = Metafile.Load(new MemoryStream(bytes), options);
if (picture.Kind is MetafileKind.EmfPlusDual or MetafileKind.EmfPlusOnly)
{
    Console.WriteLine("Drawn by its EMF+ records, as GDI+ draws it.");
}

A picture taken out of a document is often already in memory; Load takes the bytes as they are.

// A picture taken out of a document, already in memory: no stream needed.
Metafile picture = Metafile.Load(bytes);
using Raster<Rgba8> page = Raster.Create<Rgba8>(200, 150);
using RasterCanvas<Rgba8> canvas = page.CreateCanvas();
picture.Draw(canvas, new RectF(0, 0, 200, 150));

Drawing

Metafile.Draw plays the picture on any canvas: in points from the canvas's origin, or with its frame stretched to a box, as Word stretches a picture to its frame. Metafile.Rasterize draws into a new raster at a DPI (96 by default) or a width and height, over a background:

// WMF, EMF and EMF+ alike, and the gzip forms WMZ and EMZ.
Metafile chart = Metafile.Load(path);
Console.WriteLine($"{chart.Kind}, {chart.Size.X} by {chart.Size.Y} points");

// At twice the screen's resolution, over white; text uses the installed fonts.
using Raster<Rgba8> image = chart.Rasterize<Rgba8>(new MetafileRenderOptions
{
    Dpi = 192,
    Background = new RgbaF32(1, 1, 1, 1),
});
image.Save(output);

Egami.Metafiles plays the records as Windows does (GDI for WMF and EMF, GDI+ for EMF+), in managed code on any OS.

Metafile picture = Metafile.Load(path);

// The frame is stretched to the box, as Word stretches a picture to its frame. The same calls draw on a PDF page.
using Raster<Rgba8> page = Raster.Create<Rgba8>(400, 300);
using RasterCanvas<Rgba8> canvas = page.CreateCanvas();
canvas.Clear(new RgbaF32(1, 1, 1, 1));
picture.Draw(canvas, new RectF(50, 50, 300, 200));

MetafileRenderOptions carries the rest:

  • Antialias is on by default; off, GDI records draw aliased as GDI itself draws them, which is what to compare with Windows. EMF+ records follow the file's own smoothing either way.
  • Playback (a MetafilePlayback) picks which records of a dual file play: Auto, the EMF+ records as GDI+ plays them, or Gdi, the GDI records only, as programs without GDI+ draw the file.
  • Fonts and FontSubstitutions choose the faces for text; TextAsPaths draws text as outlines.
// A dual EMF+ file plays its EMF+ records by default, as Office and GDI+ do; Gdi plays only the GDI records, as
// programs without GDI+ draw it.
Metafile picture = Metafile.Load(path);
using Raster<Rgba8> gdi = picture.Rasterize<Rgba8>(new MetafileRenderOptions { Playback = MetafilePlayback.Gdi });

Text and fonts

Text records name a face, and each glyph's place comes from the record itself, so a line keeps its length in another face. A face the fonts lack is replaced by a metric-compatible open font where one exists (Arial by Liberation Sans or Arimo, Times New Roman by Liberation Serif or Tinos, Courier New by Liberation Mono or Cousine, Calibri by Carlito, Cambria by Caladea), then by your FontSubstitutions, then by the generic family; each substitution is a Metafile.FontSubstituted warning. Symbol and Wingdings text maps to Unicode in a substitute. Text given as glyph indices (Word writes these for complex scripts) is drawn only in the face it was made for, since another face would show the wrong letters.

// Faces the machine lacks: the built-in table maps Calibri to Carlito, Arial to Liberation Sans and so on; add your own.
Metafile picture = Metafile.Load(path);
using Raster<Rgba8> image = picture.Rasterize<Rgba8>(new MetafileRenderOptions
{
    FontSubstitutions = new Dictionary<string, string> { ["Segoe UI"] = "Noto Sans" },
    Antialias = false, // GDI's own aliased look, for comparing with Windows
});

Each glyph still goes where the file put it, so lines keep their length in a substitute face.

The open fonts are found only when they are installed, and many servers have none. The Egami.Fonts.Substitutes package bundles Liberation, Carlito and Caladea, so put its faces in the Fonts you pass:

// The same pictures on a machine without Arial or Calibri: the bundled substitutes first, then the installed fonts.
var fonts = new FontCollection();
foreach (FontFamily family in SubstituteFonts.Collection)
{
    foreach (Typeface face in family.Faces)
    {
        fonts.Add(face);
    }
}

fonts.AddSystemFonts();
using Raster<Rgba8> image = Metafile.Load(path).Rasterize<Rgba8>(new MetafileRenderOptions { Fonts = fonts });

Raster.Load

Registered with EgamiContext.WithFormat, MetafileFormat lets Raster.Load draw a metafile at its size at 96 DPI onto a transparent Rgba8 raster, and Raster.Probe give that size from the header without drawing. MetafileDecodeOptions.Dpi sets another resolution.

// Register the format once; Raster.Load then draws metafiles at their own size at 96 DPI.
EgamiContext context = EgamiContext.Default.WithFormat(MetafileFormat.Info);
using Raster raster = Raster.Load(path, context);
RasterInfo info = Raster.Probe(path, context); // the size from the header, nothing drawn

// MetafileDecodeOptions sets the resolution; Raster.Load then draws at that DPI.
using Raster sharp = Raster.Load(path, context, new MetafileDecodeOptions { Dpi = 300 });

What plays, and how close it is

The GDI records of WMF and EMF play as GDI plays them: map modes, the world transform, pens and brushes (hatches and pattern brushes sized from the reference device, so they scale with the picture), paths, clipping regions, bitmaps through Egami's BMP reader, and text. Drawn aliased, the synthetic test files match Windows GDI within a mean of one level and 99% of pixels within 16 levels, family by family; text is measured with a looser bound, since GDI hints glyphs to whole pixels. Raster operations Egami draws with the nearest blend: AND as multiply, OR as lighten, XOR as difference, and the common transparent-bitmap idioms as one masked image; others draw as a copy with a Metafile.RasterOperation warning.

EMF+ records play as GDI+ plays them: the world and page transforms, containers, clips with all six combine modes, smoothing, pixel offset and compositing; solid, hatch, texture, linear and path gradient brushes (path gradients as a Gouraud mesh); pens with dashes, compound lines, inset alignment and anchor and arrow caps; cardinal curves; images, with JPEG and PNG handed to the canvas unchanged so a PDF page embeds them as they are, and metafile images played as vectors up to eight levels deep; GDI+ 1.1 image effects; DrawString laid out in its rectangle and DrawDriverString. In a dual file the GDI records play only where GDI+ hands drawing to GDI (EmfPlusGetDC).

Flood fills, OpenGL records, escapes and PostScript carried in comments are skipped with a Metafile.SkippedRecord warning. The diagnostics page lists every Metafile.* id.