Recursive AI

Metafiles

All onedocument docs

How OneDocument draws embedded Windows metafiles. Namespace RecursiveAI.OneDocument.Rendering.Metafiles, package RecursiveAI.OneDocument.Rendering. The interpreter is OneImage's (RecursiveAI.OneImage.Metafile: it reads the file and plays it as a stream of device-independent drawing commands, see its own documentation); this page is the adapter from that stream to OneRender's display list. References are to [MS-EMF] and [MS-WMF].

What is drawn

InputResult
EMF (GDI records; EMF+ dual, GetDC and plus-only files)vector content, described below
WMF, placeable or not (including a WMF that wraps an enhanced metafile)converted to the same command stream by the interpreter, drawn the same way
damaged or not a metafilerender.metafile-damaged, TryPaint returns false and the caller falls back to the picture's cached bitmap or a placeholder

The interpreter turns all three into one stream of drawing commands, so the adapter has no format-specific code beyond the EMF+ commands (strings, glyphs, images, clear) and EMF+ brushes.

The hook

PictureHook.TryPaint(bytes, contentType, context, destination, crop) is the one call a picture painter makes. It decides by media type (image/x-emf, image/emf, image/x-wmf, image/wmf, or the extension) or by content (the EMF header signature, the placeable-WMF key, the standard WMF header), so a part with a wrong or missing content type still draws. It returns false without touching the list when it cannot draw. PictureHook.TryGetNaturalSize reads the frame from the header (hundredths of a millimetre, converted to points), for pictures laid out at natural size.

MetafilePaintContext carries what a page painter already has: the DisplayList (CSS pixels), a PictureImages table (display lists name images by handle; hand the same table to DisplayListPdfWriter or RasterOptions.Images), an id source, an optional font provider (default: the standard FontLibrary), a defect callback with the picture's part location, and whether to wrap the picture in a Figure tag with alternative text.

PictureCrop is a:srcRect (ECMA-376 Part 1 §20.1.8.55): fractions cut from each side, negative values extend the picture. The shown part spans destination, and everything the picture draws is clipped to it. Flips and rotation belong to the caller's own transform around the call.

Mapping

The command stream is a canvas with a transform and a clip, both set by commands. The adapter maps every point to page pixels itself (logical → picture device → page), so the display list holds only clips (the destination box, then the metafile's own clip) and short transforms around text and bitmaps.

CommandDisplay list
set-transform, save, restorethe adapter's own state; no items
clipa stack of PushClipPath items, re-pushed when the clip changes. Replace and reset clear it, intersect adds a shape, exclude adds a huge box with the shape cut out (even-odd). offset-clip moves the shapes
fill-path solidFillPath
fill-path hatchbackground fill, then 1 px lines on an 8 px grid anchored at the brush origin, clipped to the path. Horizontal and vertical lines sit on whole pixels
fill-path pattern/DIB brushthe bitmap as an image, tiled unscaled from the brush origin, clipped to the path
stroke-paththe pen is expanded to an outline (OneImage's stroker: width, caps, joins, miter limit, dashes) and filled, because the display list has no stroke item. A cosmetic pen is 1 CSS px wide whatever the scale; a geometric pen scales with the picture. Gaps of a dashed line take the background colour when the file says so
blitDrawImage under a transform that maps the bitmap onto the destination parallelogram (so PlgBlt rotation and mirrored StretchBlt work); constant alpha is a group; a transparent colour or a mask becomes pixel alpha; PatBlt with PATCOPY, BLACKNESS, WHITENESS is a fill
gradient-fill rectanglesFillPath with a linear gradient between the two vertex colours
EMF+ linear gradient brusha linear gradient with the file's stops (repeating for tiled wrap modes; the mirror of the flip modes is not drawn)
EMF+ path gradient brusha radial gradient from the centre colour to the boundary colour when the boundary is one colour; several boundary colours go to the bitmap path
EMF+ texture brushthe picture tiled (or drawn once for clamp) under the brush transform, clipped to the path
EMF+ hatch brushstyles 0 to 5 as the GDI hatches, anchored at the picture's corner; the other 48 styles go to the bitmap path
alphacolour alpha (ARGB brush and pen colours, gradient stops) passes through to the display list
draw-textDrawGlyphRun: the resolved font (OneFont face), glyph ids, per-glyph x from the record's advances (or the font's), cluster indexes and the text. Text stays text, so it is selectable and extractable in PDF. Escapement rotates the run in device space; alignment, character extra, justification, underline, strike-out, opaque and clip rectangles are applied
draw-string (EMF+)lines broken at spaces inside the layout rectangle, aligned as the string format says, at the font's line height, each line a DrawGlyphRun. Not clipped to the rectangle; vertical text goes to the bitmap path
draw-glyphs (EMF+)one run from the origin, or one glyph run per position; the command's extra transform multiplies the current one
draw-image (EMF+)DrawImage like a blit; opacity is a group; nearest or bilinear sampling follows the interpolation mode
clear (EMF+)an opaque colour fills the picture rectangle; a translucent clear after drawing goes to the bitmap path
rendering-hints, commentignored
diagnosticwarnings and errors become render.metafile-diagnostic defects with the record index (information-level ones are dropped)

Font choice: GDI face name, weight and italic go to the font provider like a document run's. A face that is not installed is reported once as render.metafile-font and the provider's stand-in is used (Arial → Liberation Sans, metric-compatible). A GDI cell height (positive lfHeight) is converted to an em size with the face's ascender and descender, as the interpreter does.

What the display list cannot express

Binary and ternary raster operations other than copy (XOR, AND, invert, SRCINVERT…), triangle gradients, pens made from hatch, pattern or gradient brushes, union or exclusive-or of clip regions, vertical text, EMF+ hatch styles above 5, path gradients with several boundary colours and a translucent clear after drawing have no display-list form. When a picture contains any of them the whole picture is drawn as one bitmap through OneImage (MetafileFormat at twice the destination size, at most MaxRasterSide = 4096 px), and render.metafile-rasterised names the reason. A half-vector picture with a wrong region looks worse than a clean bitmap, and these records are rare in Office pictures. Setting AllowRasterFallback = false draws the vector approximation instead: raster operations as plain copies, triangle gradients as the average colour, union/xor clips as replacements (render.metafile-approximated).

Not drawn (the interpreter also lists them): custom and compound pen caps, line-end anchors, colour matrices, text clipping to a string's layout rectangle. Right-to-left text order inside a draw-text run (the text is drawn in logical order), ETO_GLYPH_INDEX text beyond treating the indices as code points (as the interpreter does), vertical advances, and stroke placement refinements GDI applies to cosmetic pens (a half-pixel shift measured by OneImage; the vector output can differ from GDI by one pixel along edges).

Checked against Windows

testdata/metafile holds three sets written by Windows, each with the picture Windows drew at 256 × 256 (see the NOTICE.md in each folder): 25 EMF files recorded with GDI calls (the top folder), 24 EMF+ files in gdiplus/ (dual and plus-only, with GDI+'s own pictures) and 15 WMF files in wmf/ (placeable, with GDI's pictures, and non-placeable copies). MetafileTests.AgreementWithWindows draws each with the adapter, rasterises the display list with OneRender's image backend at 256 × 256 on white and counts pixels whose channels all differ by at most 48 (the same measure OneImage uses). The measure is information; the test only guards a floor (every case ≥ 60 %, GDI set mean ≥ 95 %).

CaseAgreement %Drawn asCaseAgreement %Drawn as
alphablend99.70vectorpolys97.63vector
arcs98.20vectorrect_basic98.25vector
beziers99.20vectorregions97.63bitmap (DSTINVERT)
bitblt_dc98.59bitmap (SRCINVERT)rop294.77bitmap (raster ops)
clip_path98.48vectorstate_stack98.16vector
clip_rect99.17bitmap (union/xor clip)stock_objects94.54vector
dib_pattern96.02vectorstretchdibits95.66bitmap (SRCINVERT)
geom_pens97.29bitmap (hatch pen)text_basic95.72vector
gradient98.96bitmap (triangles)text_styled96.06vector
hatch90.14vectorworld_xform98.10vector
lines_pens97.72vectormapmodes97.04vector
mmtext_raw97.18vectorpaths96.46vector
pixels_misc99.12bitmap (XOR)

Set means: GDI 97.42 over 25 cases, EMF+ 96.73 over 22, WMF 96.34 over 15. The GDI table above was measured with the first interpreter release and is within 0.3 points of the current one. The weakest: hatch (the diagonal lines are anti-aliased where GDI draws hard one-pixel lines, and the picture is scaled by 2/3 so their phase differs), stock_objects and rop2 (GDI draws wide-pen overlaps and stock pens its own way), text_basic/text_styled (Arial is drawn in Liberation Sans, which has the same advances but different outlines and hinting), and the bitmap-drawn cases, which carry the interpreter's own differences from GDI.

Not checked here, and needed before 1.0: Word, Excel and PowerPoint's own rendering of pictures with metafiles on the layout differential. The picture painter for inline and anchored pictures calls the hook when it lands; documents with embedded EMF pictures join the differential inputs then.