Skip to content

Limitations ​

Output is compared with images rendered by Windows. The exact per-fixture bounds are in src/gdi-parity.fixture.test.ts. Known differences:

Those bounds include nonzero pixel and channel tolerances. A passing test is not always pixel-exact output. See the current parity priorities for the zero-tolerance review, text coverage gaps and reference-environment issues.

Color adjustment and image effects ​

  • The HALFTONE stretch mode matches the existing uniform-axis controls for 32-bit output, but independent captures expose a second enlargement branch whose selection and filtering remain unresolved. Source content and spatial arrangement both affect selection; 240 fixed-geometry controls and 50 histogram-preserving controls rule out geometry-only and palette-only selectors. The captured replicated branch first smooths exact two-by-two checkers, then takes the nearest source pixel. A reduction averages each pixel's footprint and then sharpens. All seven plain uniform-axis fixtures (2x, 3x, 0.5x, 0.65x and 1.37x) match exactly. Mixed-axis stretching follows the native reduce, per-axis sharpen, then enlarge sequence; all 96 native size/pattern combinations match byte for byte (src/halftone-mixed.fixture.test.ts). The reduction keeps its weights through the sharpening: each footprint is split into 8192 shares, and a source pixel takes the difference of the cumulative shares floor(j * 8192 / footprint) at its edges (exact for the native impulse and random-data captures of over 100 size pairs, and for sources up to 300 pixels), and an enlargement other than exactly 2x area-resamples the sharpened samples to destination size and smooths that row with a destination-space FIR that spans one source pixel either way: its taps sample one box-plus-triangle-like kernel (about a quarter box, the rest a triangle with a bent, quadratic-looking tail) at k / ratio source pixels and are normalised to 1. The native weights are whole 13-bit shares: the cumulative weight of a destination pixel up to source pixel j is rounded up, a pixel's weight is the difference of two cumulative shares, and a row sums to exactly 8192 (impulse- and random-data-fitted integer weight rows of about 410 size pairs, ratios 1.05x to 8x, have this structure, and a mirrored enlarged axis reverses the finished rows instead of the source). The kernel is an interpolation table (steps of 1/96 source pixel) solved for the one curve that reproduces all those measured rows; its generating formula is not known, so it is a fit: sizes that were held out of the fit get about 95% of their weight rows exact and the rest are one share off between two neighbouring source pixels (random 8-24 pixel images with an enlarged and a halved-or-more reduced axis, 436 of them against native: 0.02% of the bytes differ and 94% of the images are exact, where the previous float kernel differed in 1.7% of the bytes and matched 6%). A pair of rows (columns) squeezed into one while the other axis grows is despeckled first (a pixel of a one-pixel 2 x 2 checker block becomes the rounded mean of its pair) and, when the other axis at least doubles, keeps only its last row (column), which is then enlarged like any other single-row source; this closes every single-row destination in the captures. Native ramp/checker fixtures retain colour and edge differences there (bounds in the parity tests). The horizontal ramp enlargement is within one level everywhere.
  • EMR_SETCOLORADJUSTMENT affects HALFTONE StretchBlt and StretchDIBits calls, as in GDI. The stages, their order and constants were measured on native 32x32x32 colour cubes: per-channel gamma, reference black/white, contrast (exp(0.0148885 c)), brightness (0.95625 b), log and negative curves match every input level exactly. Illuminants 1-5, 7 and 8 use the native colour cubes; colorfulness and tint scale and turn CIE u'v' chroma exactly (1 + c/100, 0.6 degrees per tint unit) and compress out-of-gamut colours affinely (after flooring the turned u' at 0, which the strongly rotated blues need), with a remaining +-1 rounding difference on about 0.05-0.4% of the channels (the BT.709 matrix is the 7-decimal one derived from the primaries and D65 white; a colour adjustment that combines an illuminant with colorfulness or tint feeds the illuminant's unclamped linear output into the chroma stage, which is within a level on most channels but still up to 70 levels off for a few strongly blue colours). Illuminant, colorfulness and tint go through Windows' 32-level palette with its 66x65 ordered dither, reproduced from native captures including the pattern origin, so the illuminant controls and the combined-adjustment fixtures are pixel-exact apart from that chroma rounding (at most 3 levels) and, for mixed-axis stretches of non-2x ratios, the geometry bounds below. Native captures cover mirrored enlargement, brush origins and rotated/skewed blits; rotated adjustment follows the first HALFTONE blit in the playback session. Direct-DIB colour-band transitions remain open (see the parity priorities).
  • EMF+ image effects (SerializableObject) are applied to the image's pixels before it is drawn. MS-EMFPLUS does not specify the algorithms, so they were measured against the effected bitmaps GDI+ itself writes into its recordings (src/emf-plus-image-effects.fixture.test.ts). Color matrix, lookup table, brightness/contrast and color balance match the native fixtures exactly. Sharpen strength matches all integer amounts 0–100 independently of radius. Blur: every filter product is kept to 1/256 of a level, the centre weight is applied as two half-weight products that are each rounded on their own, and each sum rounds half up. Below radius 20 GDI+ filters each row horizontally, then vertically filters the leading ceil(height / width) rows, and the converter follows that rule; the native ramp and colour-impulse dimension sweeps are exact. At radius 20 and above, native reduction factors are measured across 957 quarter radii, and the native algorithm is reproduced axis by axis: every row is reduced horizontally (floor of the block mean, a partial last block averaged over its own pixels), filtered at the reduced radius, and enlarged back to full width (linear, truncating, continuing the end line beyond the outermost samples; a single reduced sample leaves one trailing pixel unfiltered); every column of those rows then goes through the same reduce, filter and enlarge. A kernel whose half-width, rounded up to odd, reaches the reduced size (partial block included) clamps at the ends, a narrower one reflects. The kernel weights are float32 values formed in arithmetic that truncates toward zero (the offset accumulates as q_i = q_(i-1) + 1.4 / radius, w_i = exp(-q_i^2), the sum is w_0 + 2 w_1 + 2 w_2 + ... in order, and each weight is w_i * (256 / sum)), which decides every product that lands within a few ten-thousandths of a rounding tie. With them, blur is exact: no pixel differs from GDI+ across random noise images at random radii from 1 to 255 and sizes from 8 to 512 pixels, nor in the ramp, impulse, 957-radius and two-pixel-wide captures, and so is sharpen. Expanded blur reduces a cropped source rectangle from its own corner and enlarges only that rectangle's own reduced samples (the transparent padding samples shape the filtering alone), with samples beyond the buffer transparent rather than reflected; a lone reduced sample is continued toward a transparent neighbour from its block's middle, or from just past the last pixel of a shorter block. The 504 expanded draws and a further 150 narrow and edge-reaching draws are exact. Color curves use complete native lookups for all eight adjustments and every legal intensity, matching all 256 levels. Levels also handles inverted and equal thresholds and the full midtone range; its tables are formed in float32 arithmetic that truncates toward zero and match a 258,560-value sweep and 2.6 million further native values exactly. Hue/saturation/lightness is exact: GDI+ works in all-integer HSL (lightness (max+min)>>1, saturation floored to 0-255, a 255-step hue, 16.16 fixed-point controls) and the converter reproduces it for the whole 16.7 million-colour RGB cube at 20 settings and the 1,390,080-pair sweep. Tint is exact: GDI+ works in integer luma-preserving arithmetic (luma weights 54/183/19 over 256, the tint colour from the 256-index hue palette with a maximum of 254, the amount as a signed weight round(2.55 |amount|) that scales the largest channel by w / 256) and the converter reproduces it for the whole 16.7 million-colour RGB cube at every amount and 20 hues, and the 253,440-pair sweep. Red-eye correction follows a model measured from GDI+'s outputs alone (a clean-room, black-box fit; see the header of src/emf-plus-image-effects.ts): per area, redness R - max(G, B) is pulled toward grey in 60 six-degree sectors around a redness-weighted centre (weights redness over G + 0.225 B, carried 0.81 of the previous area's centre into the next area, and reset to the area middle when the centre falls far outside it). Each sector's mean redness sets the correction through a quadratic falloff whose strength steps with the area's 30th/70th percentile luminance spread; output is R -= round(11/16 a), G, B += round(5/16 a). This cuts the share of mismatching pixels on the native fixtures from 3.7/6.9/46.2% (original rule) and 0.71/1.49/3.51% (previous model) to 0.057/0.472/2.77% (left/both/whole), and the pupil and skin scenes generated for fitting agree more closely than before. Not reproduced: part of the left pupil of the whole fixture (1 to 3 levels off), the exact fallback centre of the chart image, near-black reds whose weights are quantised in GDI+, the contribution of non-red dark pixels to the centre in mixed areas, small residuals in second areas, and order-sensitive pure-red native outputs. The sector darkness falloff is symmetric around its mean while darkness stays below twice that mean; 103 nonzero-darkness controls, including 31 held-out cases, are exact.
  • EmfPlusDual recordings add a processed bitmap fallback after the effect draw. EmfPlusOnly recordings carry the effect and original source without that fallback. Both types have been regenerated and tested: 74 cases each, plus an explicit Dual blur case. Only recordings test the effect algorithms directly; red-eye retains measured residuals. Deferred PNG and SVG draws preserve their effects and cropped source mapping.
  • An effect is applied to the draw's source rectangle only, reading one more column and row, as GDI+ does. A blur with expandEdge blurs transparency in at the edges but, as in GDI+, draws nothing beyond the source rectangle. MS-EMFPLUS requires pixel source units; malformed non-pixel records do not apply effects.
  • GDI+ records a color curve effect under the lookup table's identifier, and its own playback then draws the image without the effect. The converter does the same.
  • An effect is skipped when the image cannot be decoded to pixels. PNG, BMP, JPEG, GIF and TIFF have bundled decoders. Animated GIF and multipage TIFF initially draw their first frame/page, as GDI+ does. Native references verify offset GIF frames, background colours, transparency, local palettes, LZW-compressed and multipage TIFF, plus odd-width bilevel uncompressed, PackBits and CCITT Group 3/4 TIFF. Group 3 also handles reversed bit order and multiple strips. Deflate strips (both compression tags and horizontal prediction) and uncompressed/Deflate tiles also match native references exactly. JPEG-compressed TIFF strips and tiles (RGB and YCbCr) and standalone JPEG (greyscale, RGB, 4:4:4, 4:2:2, 4:2:0, progressive) match the native references exactly: the bundled decoder reproduces libjpeg's accurate integer IDCT, triangle chroma upsampling and fixed-point colour conversion. CMYK, arithmetic-coded and 12-bit JPEG use a less exact fallback. A canvas backend remains preferred when available. Other encoded formats may need a browser or @napi-rs/canvas.

Pen transforms ​

EMF+ pen transforms (uniform, non-uniform and skewed) match GDI+ exactly on the 17 pen-transform fixtures. That includes dashes, which GDI+ lays out along the path in world space at the untransformed pen width. Native fixtures verify dashes under uniform and non-uniform scales, including antialiasing. GDI+ rejects singular pen transforms; the converter ignores such malformed transform data. Square, round, diamond and arrow anchor caps use their native shapes; the new centered-cap fixtures match every pixel with and without antialiasing.

Clockwise inside-frame RoundRects now round vertical inset widths down and curve controls up, with horizontal half-FIX shifts applied in both arc directions. Independent native controls verify 87/216 clockwise paths and all 72 even-FIX controls exactly; remaining side/corner coordinates are at most one FIX away. Scaled EMF and WMF shape residuals remain 80 and seven pixels.

Text ​

Grayscale GDI+ glyph origins now snap independently to the nearest quarter pixel in x and y, verified on native sweeps at 1/64-pixel intervals. ClearType snaps horizontal placement to the nearest sixth pixel and preserves that fraction through scan conversion, while vertical placement snaps to whole pixels. Diagonal/base glyph hinting still differs.

  • The ANSI, small-text and multi-string record fixtures retain glyph-edge differences under 0.1% (src/emf-text-records.fixture.test.ts). PolyTextOut C1 handling now follows native playback; focused Courier controls are exact and Arial retains three edge pixels. EMF+ masks retain decorations and use native grayscale hinting and 16 shades; 150 grayscale and 34 ClearType baseline-controlled glyph captures, plus every captured contrast mapping, are exact. Broader ClearType, antialiasing and TextContrast fixtures still differ, including diagonal hinting and fractional positioning. Current fonts reproduce the three textx-plus antialias/ClearType references exactly, so those residuals are renderer differences; other old captures lack complete provenance.
  • ANSI text is decoded with the host's TextDecoder for common Windows code pages. Johab and OEM CP437 use bundled Windows mappings; other unsupported encodings fall back to Windows-1252.
  • Vertical ETO_PDY advances are supported with or without the fonts option.
  • Without fonts, SVG text measurements are estimates. Supply the matching fonts for exact justification.

Wide pens and paths ​

  • All 2,136 paths in the native WidenPath sweep (2–64 px pens, every cap and join) fill identically. The flattened-pen perpendicular follows a closed rule instead of a fitted table: the interpolated point of a pen edge, rounded to the half-pixel grid after a half-unit bias, with a further half unit when the edge's extent in that channel is odd. It matches native WidenPath for every direction tested at integer, fractional and wider-than-100 px widths (about 390,000 whole-pixel directions at widths from 1 to 206 px, none differing), including vertical segments, exact ties and segments parallel to a pen edge.
  • Wide pens on curves (Beziers, Arcs, Chords, Pies, AngleArcs and paths with curves) follow the pen at each flattened segment instead of treating the curve as a polyline: a pure Bezier matches native WidenPath exactly under every cap and join in 80 captured cases (previously none). Arcs, chords and pies stay within a few pixels: 148 of 240 captured outlines differ, in 1,437 pixels in all (previously 259 of 320 in 14,199), mostly the square caps of arcs (the extension of a square cap on an arc differs from the end tangent's by a few 1/16 pixels) and the line-to-curve corners of chords and pies. Flat-capped round joins match 5,998 of 5,999 native random three-point paths (previously 86 differed), and fractional pen widths 6.5–44 px match 2,997 of 3,000 random paths (previously 286 differed).
  • Dashed wide curves follow WidenPath exactly for round and flat caps; square caps leave small differences at the path ends and on arcs (curve-dash.json: 78 of 100 square-capped samples, 524 pixels in all), and the whole-pixel segment measure is only known for one logical unit per device pixel without rotation (other scales measure the segments exactly).
  • EMR_WIDENPATH ellipse outlines now match native vertices for every cap/join combination in the 2–32 px sweep, including the duplicated inner triangles. The stroked-outline fixture matches exactly; mixed cap/join polyline and curve cases retain small residuals.
  • EMF+ 1-pixel antialiased lines can differ by one antialiasing sample at their ends, and some closed widened outlines by one sample along an edge.
  • PS_INSIDEFRAME pens in EMF playback now pull a Rectangle, RoundRect, Ellipse, Arc, Chord or Pie box in by the pen width along each axis, as native GetPath shows (the 16-width emf-insideframe-* sheets of every shape are pixel-exact at the identity scale; before, an EMF inside-frame pen was stroked centred on the outline, 16% to 35% of the pixels off). Under a fractional world scale 80 of 170,962 pixels remain (0.047%): the Arc, Chord and Pie paths on the half-FIX edges an odd pen width leaves, and a few RoundRect side points. Solid wide pens under axis-aligned unequal scales now use an elliptical nib, including its square-cap metric and miter limits: all 216 controls across eight native image sheets are exact, and all 3,600 native widened outlines and 16,128 independent direction/corner controls are exact. Elliptical pens resolve parallel-edge ties using the logical 45-degree boundary. Dashed pens, rotated/sheared unequal scales and the GM_COMPATIBLE path retain the geometric-mean approximation; transformed curves need independent coverage. EMF+ Inset-aligned and compound-array (CompoundArray) pens are GDI+ features: the converter widens them with its GDI+ widener (exact on the probed gpx-pen-styles pens) and with Canvas strokes only under gdiAntialias: true; native captures of other closed-figure inset or compound pens have not been made.

GM_COMPATIBLE recordings ​

EMF files do not record the graphics mode. Windows plays back RoundRect, Arc, Chord, Pie and null-pen Ellipse records differently from how a GM_COMPATIBLE application drew them on screen. The converter follows Windows playback.

EMF+ ​

  • Rotated HighQualityBicubic DrawImage differs beyond eight channel levels on 0.013% of pixels in the current fixture (previously 0.14%). The unrotated Bicubic PNG fixture now matches every RGB pixel, and 144 independent arithmetic captures are exact including alpha. Near-integer unit-scale copy eligibility is confirmed over a half-open phase interval; four positive-boundary controls remain open among 384 phase controls. Another 936 independent copy captures improve from 866 to 930 exact. High-quality draws scaled in only one axis now filter both axes; 480 arithmetic/crop/mirror/alpha controls tighten without per-case regressions. High-quality kernels and crop/mirror geometry retain larger zero-tolerance sampling differences. SVG defaults to renderer scaling; imageResampling: 'exact' uses device-resolution GDI+ resampling.
  • Path gradients now preserve independent horizontal/vertical focus scales, including zero and fully focused axes. All seven rectangular and elliptical controls and seven triangular controls are within one channel level in PNG and embedded SVG image payloads. Curve boundaries match all 1,556 vertices in the native random/ellipse Flatten probe. Folded focus strips use odd coverage and can extend outside the outer polygon. Both collapsed triangle controls are within one channel level. Forty of 252 independent contour/order controls retain 57 pixels beyond one level; channel rounding and wrap sampling remain open. Exact native linear Blend lookup tables do not establish exact path-gradient sampling.

WMF ​

  • PS_INSIDEFRAME rectangles, ellipses, rounded rectangles, chords, pies and arcs match the native 0.5–10 px half-pixel sweeps exactly. wmf-shapes-scaled (0.96 device scale) keeps 0.008% of its pixels (7, from 24): the inside-frame row (the Chord and Pie edges and corners and one Ellipse and one RoundRect edge pixel; the Arc, Chord and Pie paths on the half-FIX edges of an odd pen width are not modelled). RoundRects under cosmetic, wide and null pens match native at the identity and 0.96 scales for every corner size from 6 to 30 units. Arc, Chord and Pie paths follow Windows' angle and trigonometry model (an approximate arc tangent in 32 slope intervals per octant, a 128-gon for the sine and cosine, and tangent lines on the polygon points): of 900 native arcs, 898 paths match exactly under each arc direction and the other two differ by a single 1/16-pixel unit in one point.
  • Mirrored LAYOUT_RTL cosmetic and wide line fixtures match Windows exactly.
  • Metric map modes default to a 96 dpi reference device. Set wmfReferenceDpi to a number or { x, y } when the original physical-device resolution is known. Metafiles do not always record that information.

Additional native controls confirm preservation of SPVFS/SFVFS signed vector words (64 newly exact controls). General diagonal projection and movement rounding remains unresolved. Path gradients with uniform surrounds now interpolate unequal-alpha colors through premultiplied channels; 109 of 120 independent controls are within one level, with 16 remaining boundary/coverage pixels beyond one level. Varying-surround interpolation remains open. The fractional HALFTONE kernel and vertical-first intermediate rounding match the measured central RGB crops in test-only helpers; unresolved native branch selection still prevents general runtime dispatch. Expanded native captures now cover 92 extents reproducibly; nine old text extents still need valid independent captures.

Released under the Apache-2.0 License.