[PATCH v8 0/4] MR11180: gdiplus: Properly handle raw WMF
This adds proper raw WMF support commonly used in \\wmetafile RTF documents It properly fixes the issue [this workaround solves](https://gitlab.winehq.org/mono/wpf/-/merge_requests/13) by adding the missing WMF support in gdiplus. Tested with the windows wdk installer -- v8: gdiplus/tests: Add tests for raw WMF load, playback and encoding gdiplus: Store the source HMETAFILE for proper lifetime management. https://gitlab.winehq.org/wine/wine/-/merge_requests/11180
From: Rose Hellsing <rose@pinkro.se> Add raw-WMF signatures to the WMF image codec so GdipLoadImageFromStream can detect raw \wmetafile data (e.g. embedded in RTF documents) in addition to the placeable WMF format. Signed-off-by: Rose Hellsing <rose@pinkro.se> --- dlls/gdiplus/image.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/dlls/gdiplus/image.c b/dlls/gdiplus/image.c index 17e1437eb5f..3ed7d9f83e8 100644 --- a/dlls/gdiplus/image.c +++ b/dlls/gdiplus/image.c @@ -5108,8 +5108,16 @@ static const WCHAR wmf_codecname[] = L"Built-in WMF"; static const WCHAR wmf_extension[] = L"*.WMF"; static const WCHAR wmf_mimetype[] = L"image/x-wmf"; static const WCHAR wmf_format[] = L"WMF"; -static const BYTE wmf_sig_pattern[] = { 0xd7, 0xcd }; -static const BYTE wmf_sig_mask[] = { 0xFF, 0xFF }; +static const BYTE wmf_sig_pattern[] = { + 0xd7, 0xcd, 0xc6, 0x9a, /* placeable WMF */ + 0x01, 0x00, 0x09, 0x00, /* raw memory metafile */ + 0x02, 0x00, 0x09, 0x00, /* raw disk metafile */ +}; +static const BYTE wmf_sig_mask[] = { + 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, +}; static const WCHAR png_codecname[] = L"Built-in PNG"; static const WCHAR png_extension[] = L"*.PNG"; @@ -5237,8 +5245,8 @@ static const struct image_codec codecs[NUM_CODECS] = { /* MimeType */ wmf_mimetype, /* Flags */ ImageCodecFlagsDecoder | ImageCodecFlagsSupportVector | ImageCodecFlagsBuiltin, /* Version */ 1, - /* SigCount */ 1, - /* SigSize */ 2, + /* SigCount */ 3, + /* SigSize */ 4, /* SigPattern */ wmf_sig_pattern, /* SigMask */ wmf_sig_mask, }, -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11180
From: Rose Hellsing <rose@pinkro.se> Rasterize metafiles to bitmaps inside the WIC-based encoders so GdipSaveImageToStream works for WMF/EMF inputs (e.g. encoding to PNG or JPEG), which is what upstream .NET WPF calls for WMF images embedded in RTF documents. Also preserve the decoder's metafile format choice in GdipLoadImageFromStream and report raw WMF streams as ImageFormatEMF, to match Windows' behavior. Signed-off-by: Rose Hellsing <rose@pinkro.se> --- dlls/gdiplus/image.c | 80 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/dlls/gdiplus/image.c b/dlls/gdiplus/image.c index 3ed7d9f83e8..4d537fd4f96 100644 --- a/dlls/gdiplus/image.c +++ b/dlls/gdiplus/image.c @@ -4348,7 +4348,14 @@ static GpStatus load_wmf(IStream *stream, GpMetafile **metafile) return GenericError; status = GdipCreateMetafileFromWmf(hmf, TRUE, is_placeable ? &pfh : NULL, metafile); - if (status != Ok) + if (status == Ok) + { + /* Windows reports raw WMF streams loaded through GdipLoadImageFromStream + * as EMF images. The decoder's format choice is preserved above. */ + if (!is_placeable) + (*metafile)->image.format = ImageFormatEMF; + } + else DeleteMetaFile(hmf); return status; } @@ -4609,7 +4616,10 @@ GpStatus WINGDIPAPI GdipLoadImageFromStream(IStream *stream, GpImage **image) /* take note of the original data format */ if (stat == Ok) { - memcpy(&(*image)->format, &codec->info.FormatID, sizeof(GUID)); + /* Metafile decoders may choose a more specific format (e.g. raw WMF + * streams are reported as EMF on Windows). Respect that choice. */ + if ((*image)->type != ImageTypeMetafile || IsEqualGUID(&(*image)->format, &GUID_NULL)) + memcpy(&(*image)->format, &codec->info.FormatID, sizeof(GUID)); return Ok; } @@ -4880,27 +4890,85 @@ static BOOL has_encoder_param_long(GDIPCONST EncoderParameters *params, GUID par return FALSE; } +static GpStatus rasterize_metafile(GpMetafile *metafile, GpBitmap **bitmap) +{ + GpStatus status; + GpBitmap *bmp; + GpGraphics *graphics; + REAL width, height; + INT pix_width, pix_height; + + width = metafile->bounds.Width; + height = metafile->bounds.Height; + + if (width <= 0.0f) width = 1.0f; + if (height <= 0.0f) height = 1.0f; + + /* Cap rasterization size to avoid excessive memory use. */ + if (width > 4096.0f || height > 4096.0f) + { + REAL scale = 4096.0f / (width > height ? width : height); + width *= scale; + height *= scale; + } + + pix_width = (INT)width; + if (pix_width <= 0) pix_width = 1; + pix_height = (INT)height; + if (pix_height <= 0) pix_height = 1; + + status = GdipCreateBitmapFromScan0(pix_width, pix_height, 0, PixelFormat32bppARGB, NULL, &bmp); + if (status != Ok) return status; + + status = GdipGetImageGraphicsContext((GpImage*)bmp, &graphics); + if (status == Ok) + { + /* Leave the background transparent (the bitmap is already zero- + * initialized to 0x00000000); this matches native gdiplus behavior. */ + status = GdipDrawImageRect(graphics, (GpImage*)metafile, 0.0f, 0.0f, + (REAL)pix_width, (REAL)pix_height); + GdipDeleteGraphics(graphics); + } + + if (status == Ok) + *bitmap = bmp; + else + GdipDisposeImage((GpImage*)bmp); + + return status; +} + static GpStatus encode_image_wic(GpImage *image, IStream *stream, REFGUID container, GDIPCONST EncoderParameters *params) { GpStatus status, terminate_status; + GpBitmap *rasterized = NULL; + GpImage *encode_image = image; - if (image->type != ImageTypeBitmap) + if (image->type == ImageTypeMetafile) + { + status = rasterize_metafile((GpMetafile*)image, &rasterized); + if (status != Ok) return status; + encode_image = (GpImage*)rasterized; + } + else if (image->type != ImageTypeBitmap) return GenericError; - status = initialize_encoder_wic(stream, container, image); + status = initialize_encoder_wic(stream, container, encode_image); if (status == Ok) - status = encode_frame_wic(image->encoder, image); + status = encode_frame_wic(encode_image->encoder, encode_image); if (!has_encoder_param_long(params, EncoderSaveFlag, EncoderValueMultiFrame)) { /* always try to terminate, but if something already failed earlier, keep the old status. */ - terminate_status = terminate_encoder_wic(image); + terminate_status = terminate_encoder_wic(encode_image); if (status == Ok) status = terminate_status; } + GdipDisposeImage((GpImage*)rasterized); + return status; } -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11180
From: Rose Hellsing <rose@pinkro.se> GdipCreateMetafileFromWmf converts the WMF to EMF for playback but the caller may still expect the original HMETAFILE to follow the documented ownership rules (kept on failure, released by gdiplus on success when delete=TRUE). Track the source handle on the GpMetafile and release it from METAFILE_Free, leaving the failure path to the caller per MSDN. --- dlls/gdiplus/gdiplus_private.h | 2 ++ dlls/gdiplus/metafile.c | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/dlls/gdiplus/gdiplus_private.h b/dlls/gdiplus/gdiplus_private.h index cc5f823214b..a1ed2cf7dbf 100644 --- a/dlls/gdiplus/gdiplus_private.h +++ b/dlls/gdiplus/gdiplus_private.h @@ -479,7 +479,9 @@ struct GpMetafile{ GpUnit unit; MetafileType metafile_type; HENHMETAFILE hemf; + HMETAFILE hwmf; int preserve_hemf; /* if true, hemf belongs to the app and should not be deleted */ + int preserve_hwmf; /* if true, hwmf belongs to the app and should not be deleted */ /* recording */ HDC record_dc; diff --git a/dlls/gdiplus/metafile.c b/dlls/gdiplus/metafile.c index 20adc83b95f..83484b9a00e 100644 --- a/dlls/gdiplus/metafile.c +++ b/dlls/gdiplus/metafile.c @@ -692,6 +692,8 @@ void METAFILE_Free(GpMetafile *metafile) DeleteEnhMetaFile(CloseEnhMetaFile(metafile->record_dc)); if (!metafile->preserve_hemf) DeleteEnhMetaFile(metafile->hemf); + if (!metafile->preserve_hwmf) + DeleteMetaFile(metafile->hwmf); if (metafile->record_graphics) { WARN("metafile closed while recording\n"); @@ -4315,6 +4317,9 @@ GpStatus WINGDIPAPI GdipCreateMetafileFromWmf(HMETAFILE hwmf, BOOL delete, if (retval == Ok) { + (*metafile)->hwmf = hwmf; + (*metafile)->preserve_hwmf = !delete; + if (placeable) { (*metafile)->image.xres = (REAL)placeable->Inch; @@ -4330,11 +4335,12 @@ GpStatus WINGDIPAPI GdipCreateMetafileFromWmf(HMETAFILE hwmf, BOOL delete, else (*metafile)->metafile_type = MetafileTypeWmf; (*metafile)->image.format = ImageFormatWMF; - - if (delete) DeleteMetaFile(hwmf); } else + { DeleteEnhMetaFile(hemf); + } + return retval; } -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11180
From: Rose Hellsing <rose@pinkro.se> Add a raw WMF test asset and tests that load it from a stream, draw it, and save it as PNG. --- dlls/gdiplus/tests/image.c | 376 +++++++++++++++++++++++++++++++++++++ 1 file changed, 376 insertions(+) diff --git a/dlls/gdiplus/tests/image.c b/dlls/gdiplus/tests/image.c index a1b81c8eda7..7548ed9e725 100644 --- a/dlls/gdiplus/tests/image.c +++ b/dlls/gdiplus/tests/image.c @@ -2028,6 +2028,25 @@ static const unsigned char wmfimage[180] = { 0x00,0x00,0xf0,0x01,0x00,0x00,0x04,0x00,0x00,0x00,0xf0,0x01,0x01,0x00,0x03,0x00, 0x00,0x00,0x00,0x00 }; +/* 16x16 raw WMF with no drawing records, only used to verify background color. */ +static const unsigned char empty_wmf[44] = { +0x01,0x00,0x09,0x00,0x00,0x03,0x16,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00, +0x00,0x00,0x05,0x00,0x00,0x00,0x0b,0x02,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00, +0x0c,0x02,0x10,0x00,0x10,0x00,0x03,0x00,0x00,0x00,0x00,0x00 +}; +/* 158-byte raw WMF (same content as wmfimage, but without the placeable header) */ +static const unsigned char raw_wmfimage[158] = { +0x01,0x00,0x09,0x00,0x00,0x03,0x4f,0x00,0x00,0x00,0x0f,0x00,0x08,0x00,0x00,0x00, +0x00,0x00,0x05,0x00,0x00,0x00,0x0b,0x02,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00, +0x0c,0x02,0x40,0x01,0x40,0x01,0x04,0x00,0x00,0x00,0x02,0x01,0x01,0x00,0x04,0x00, +0x00,0x00,0x04,0x01,0x0d,0x00,0x08,0x00,0x00,0x00,0xfa,0x02,0x05,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x2d,0x01,0x00,0x00,0x07,0x00, +0x00,0x00,0xfc,0x02,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x00,0x00, +0x2d,0x01,0x01,0x00,0x07,0x00,0x00,0x00,0xfc,0x02,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x04,0x00,0x00,0x00,0x2d,0x01,0x02,0x00,0x07,0x00,0x00,0x00,0x1b,0x04, +0x40,0x01,0x40,0x01,0x00,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0xf0,0x01,0x00,0x00, +0x04,0x00,0x00,0x00,0xf0,0x01,0x01,0x00,0x03,0x00,0x00,0x00,0x00,0x00 +}; static void test_getrawformat(void) { test_bufferrawformat((void*)pngimage, sizeof(pngimage), &ImageFormatPNG, __LINE__, FALSE); @@ -2183,6 +2202,358 @@ static void test_createfromwmf_noplaceable(void) GdipDisposeImage(img); } +static void test_loadwmf_noplaceable(void) +{ + LPSTREAM stream; + HGLOBAL hglob; + LPBYTE data; + HRESULT hres; + GpStatus stat; + GpImage *img; + GUID format; + + hglob = GlobalAlloc(0, sizeof(raw_wmfimage)); + data = GlobalLock(hglob); + memcpy(data, raw_wmfimage, sizeof(raw_wmfimage)); + GlobalUnlock(hglob); + data = NULL; + + hres = CreateStreamOnHGlobal(hglob, TRUE, &stream); + ok(hres == S_OK, "Failed to create a stream\n"); + if (hres != S_OK) return; + + stat = GdipLoadImageFromStream(stream, &img); + ok(stat == Ok, "GdipLoadImageFromStream failed, status %d\n", stat); + IStream_Release(stream); + if (stat != Ok) return; + + stat = GdipGetImageRawFormat(img, &format); + expect(Ok, stat); + if (stat == Ok) + { + /* Windows reports raw (non-placeable) WMF streams as EMF. */ + ok(IsEqualGUID(&format, &ImageFormatEMF), "Expected EMF format\n"); + } + + GdipDisposeImage(img); +} + +static void test_drawwmf(void) +{ + LPSTREAM stream; + HGLOBAL hglob; + LPBYTE data; + HRESULT hres; + GpStatus stat; + GpImage *img; + GpBitmap *bitmap; + GpGraphics *graphics; + + hglob = GlobalAlloc(0, sizeof(raw_wmfimage)); + data = GlobalLock(hglob); + memcpy(data, raw_wmfimage, sizeof(raw_wmfimage)); + GlobalUnlock(hglob); + data = NULL; + + hres = CreateStreamOnHGlobal(hglob, TRUE, &stream); + ok(hres == S_OK, "Failed to create a stream\n"); + if (hres != S_OK) return; + + stat = GdipLoadImageFromStream(stream, &img); + IStream_Release(stream); + ok(stat == Ok, "GdipLoadImageFromStream failed, status %d\n", stat); + if (stat != Ok) return; + + stat = GdipCreateBitmapFromScan0(64, 64, 0, PixelFormat32bppARGB, NULL, &bitmap); + expect(Ok, stat); + if (stat == Ok) + { + stat = GdipGetImageGraphicsContext((GpImage*)bitmap, &graphics); + expect(Ok, stat); + if (stat == Ok) + { + stat = GdipDrawImageRect(graphics, img, 0.0, 0.0, 64.0, 64.0); + expect(Ok, stat); + GdipDeleteGraphics(graphics); + } + GdipDisposeImage((GpImage*)bitmap); + } + + GdipDisposeImage(img); +} + +struct enumwmf_state +{ + GpMetafile *metafile; + unsigned int count; + unsigned int wmf_records; +}; + +static BOOL CALLBACK enumwmf_callback(EmfPlusRecordType record_type, unsigned int flags, + unsigned int dataSize, const unsigned char *pStr, void *userdata) +{ + struct enumwmf_state *state = (struct enumwmf_state*)userdata; + GpStatus stat; + + state->count++; + + if (record_type & GDIP_WMF_RECORD_BASE) + state->wmf_records++; + + stat = GdipPlayMetafileRecord(state->metafile, record_type, flags, dataSize, pStr); + ok(stat == Ok, "record %u: GdipPlayMetafileRecord failed with stat %d (type 0x%x)\n", + state->count, stat, record_type); + + return TRUE; +} + +static void test_enumwmf(void) +{ + LPSTREAM stream; + HGLOBAL hglob; + LPBYTE data; + HRESULT hres; + GpStatus stat; + GpImage *img; + GpBitmap *bitmap; + GpGraphics *graphics; + GpPointF dst_points[3] = {{0.0, 0.0}, {64.0, 0.0}, {0.0, 64.0}}; + GpRectF src_rect = {0.0, 0.0, 64.0, 64.0}; + struct enumwmf_state state = {0}; + + hglob = GlobalAlloc(0, sizeof(raw_wmfimage)); + data = GlobalLock(hglob); + memcpy(data, raw_wmfimage, sizeof(raw_wmfimage)); + GlobalUnlock(hglob); + + hres = CreateStreamOnHGlobal(hglob, TRUE, &stream); + ok(hres == S_OK, "Failed to create a stream\n"); + if (hres != S_OK) return; + + stat = GdipLoadImageFromStream(stream, &img); + IStream_Release(stream); + ok(stat == Ok, "GdipLoadImageFromStream failed, status %d\n", stat); + if (stat != Ok) return; + + stat = GdipCreateBitmapFromScan0(64, 64, 0, PixelFormat32bppARGB, NULL, &bitmap); + expect(Ok, stat); + if (stat == Ok) + { + stat = GdipGetImageGraphicsContext((GpImage*)bitmap, &graphics); + expect(Ok, stat); + if (stat == Ok) + { + state.metafile = (GpMetafile*)img; + + stat = GdipEnumerateMetafileSrcRectDestPoints(graphics, (GpMetafile*)img, + dst_points, 3, &src_rect, UnitPixel, enumwmf_callback, &state, NULL); + expect(Ok, stat); + + /* Although the EmfPlusRecordType enum reserves a range of values + * for WMF record types (the GDIP_WMF_RECORD_BASE bit), Windows + * internally converts WMF metafiles to EMF before enumeration and + * reports every record using plain EMF record types. */ + ok(state.count > 0, "Expected callback to be invoked at least once\n"); + ok(state.wmf_records == 0, + "Expected no WMF-typed records, got %u of %u\n", + state.wmf_records, state.count); + + GdipDeleteGraphics(graphics); + } + GdipDisposeImage((GpImage*)bitmap); + } + + GdipDisposeImage(img); +} + +static void test_savemetafile(void) +{ + static const CLSID CLSID_PngEncoder = + { 0x557cf406, 0x1a04, 0x11d3, { 0x9a, 0x73, 0x00, 0x00, 0xf8, 0x1e, 0xf3, 0x2e } }; + LPSTREAM stream, out_stream; + HGLOBAL hglob; + LPBYTE data; + HRESULT hres; + GpStatus stat; + GpImage *img; + STATSTG statstg; + + hglob = GlobalAlloc(0, sizeof(raw_wmfimage)); + data = GlobalLock(hglob); + memcpy(data, raw_wmfimage, sizeof(raw_wmfimage)); + GlobalUnlock(hglob); + data = NULL; + + hres = CreateStreamOnHGlobal(hglob, TRUE, &stream); + ok(hres == S_OK, "Failed to create a stream\n"); + if (hres != S_OK) return; + + stat = GdipLoadImageFromStream(stream, &img); + IStream_Release(stream); + ok(stat == Ok, "GdipLoadImageFromStream failed, status %d\n", stat); + if (stat != Ok) return; + + hres = CreateStreamOnHGlobal(NULL, TRUE, &out_stream); + ok(hres == S_OK, "Failed to create output stream\n"); + if (hres != S_OK) + { + GdipDisposeImage(img); + return; + } + + stat = GdipSaveImageToStream(img, out_stream, &CLSID_PngEncoder, NULL); + ok(stat == Ok, "GdipSaveImageToStream failed, status %d\n", stat); + + if (stat == Ok) + { + GpImage *loaded; + LARGE_INTEGER zero = {{0}}; + + memset(&statstg, 0, sizeof(statstg)); + hres = IStream_Stat(out_stream, &statstg, STATFLAG_NONAME); + ok(hres == S_OK, "IStream_Stat failed\n"); + ok(statstg.cbSize.QuadPart > 0, "saved PNG is empty\n"); + + /* Load the saved PNG back and inspect the result. */ + IStream_Seek(out_stream, zero, STREAM_SEEK_SET, NULL); + stat = GdipLoadImageFromStream(out_stream, &loaded); + ok(stat == Ok, "Failed to load saved PNG, status %d\n", stat); + + if (stat == Ok) + { + GUID format_guid; + PixelFormat pixel_format; + UINT width = 0, height = 0; + ARGB pixel = 0; + + stat = GdipGetImageRawFormat(loaded, &format_guid); + expect(Ok, stat); + ok(IsEqualGUID(&format_guid, &ImageFormatPNG), + "Expected PNG format for the saved image\n"); + + stat = GdipGetImagePixelFormat(loaded, &pixel_format); + expect(Ok, stat); + ok(pixel_format == PixelFormat32bppARGB, + "Expected PixelFormat32bppARGB for the saved PNG, got %#x\n", pixel_format); + + stat = GdipGetImageWidth(loaded, &width); + expect(Ok, stat); + stat = GdipGetImageHeight(loaded, &height); + expect(Ok, stat); + ok(width > 0 && height > 0, + "Saved PNG has invalid dimensions %ux%u\n", width, height); + + /* The raw_wmfimage WMF draws a single solid black rectangle covering + * its entire window, so every pixel of the rasterized image should be + * opaque black. This both verifies that the metafile actually played + * back during the save (a pure-white pixel would indicate the WMF + * was never drawn over the rasterizer's implicit white background) + * and that the rasterizer doesn't lose opacity along the way. */ + if (width > 0 && height > 0) + { + stat = GdipBitmapGetPixel((GpBitmap*)loaded, width / 2, height / 2, &pixel); + expect(Ok, stat); + ok(pixel == 0xff000000, + "Expected opaque black at center, got %.8lx\n", pixel); + } + + GdipDisposeImage(loaded); + } + } + + IStream_Release(out_stream); + GdipDisposeImage(img); +} + +static void test_savemetafile_background(void) +{ + static const CLSID CLSID_PngEncoder = + { 0x557cf406, 0x1a04, 0x11d3, { 0x9a, 0x73, 0x00, 0x00, 0xf8, 0x1e, 0xf3, 0x2e } }; + LPSTREAM stream, out_stream; + HGLOBAL hglob; + LPBYTE data; + HRESULT hres; + GpStatus stat; + GpImage *img, *loaded; + LARGE_INTEGER zero = {{0}}; + PixelFormat pixel_format; + UINT width = 0, height = 0, x, y; + ARGB pixel; + + hglob = GlobalAlloc(0, sizeof(empty_wmf)); + data = GlobalLock(hglob); + memcpy(data, empty_wmf, sizeof(empty_wmf)); + GlobalUnlock(hglob); + + hres = CreateStreamOnHGlobal(hglob, TRUE, &stream); + ok(hres == S_OK, "Failed to create a stream\n"); + if (hres != S_OK) return; + + stat = GdipLoadImageFromStream(stream, &img); + IStream_Release(stream); + ok(stat == Ok, "GdipLoadImageFromStream failed, status %d\n", stat); + if (stat != Ok) return; + + hres = CreateStreamOnHGlobal(NULL, TRUE, &out_stream); + ok(hres == S_OK, "Failed to create output stream\n"); + if (hres != S_OK) + { + GdipDisposeImage(img); + return; + } + + stat = GdipSaveImageToStream(img, out_stream, &CLSID_PngEncoder, NULL); + ok(stat == Ok, "GdipSaveImageToStream failed, status %d\n", stat); + if (stat != Ok) + { + IStream_Release(out_stream); + GdipDisposeImage(img); + return; + } + + /* Load the saved PNG back. */ + IStream_Seek(out_stream, zero, STREAM_SEEK_SET, NULL); + stat = GdipLoadImageFromStream(out_stream, &loaded); + ok(stat == Ok, "Failed to load saved PNG, status %d\n", stat); + + if (stat == Ok) + { + stat = GdipGetImagePixelFormat(loaded, &pixel_format); + expect(Ok, stat); + ok(pixel_format == PixelFormat32bppARGB, + "Expected PixelFormat32bppARGB for the saved PNG, got %#x\n", pixel_format); + + stat = GdipGetImageWidth(loaded, &width); + expect(Ok, stat); + stat = GdipGetImageHeight(loaded, &height); + expect(Ok, stat); + ok(width > 0 && height > 0, + "Saved PNG has invalid dimensions %ux%u\n", width, height); + + /* The empty WMF performs no drawing, so every pixel of the rasterized + * output should be the rasterizer's implicit background color, which + * native gdiplus leaves as the zero-initialized transparent black. */ + for (y = 0; y < height; y++) + { + for (x = 0; x < width; x++) + { + pixel = 0xdeadbeef; + stat = GdipBitmapGetPixel((GpBitmap*)loaded, x, y, &pixel); + expect(Ok, stat); + ok(pixel == 0x00000000, + "Expected transparent background at (%u,%u), got %.8lx\n", + x, y, pixel); + if (pixel != 0x00000000) goto done; + } + } +done: + GdipDisposeImage(loaded); + } + + IStream_Release(out_stream); + GdipDisposeImage(img); +} + static void test_resolution(void) { GpStatus stat; @@ -6893,6 +7264,11 @@ START_TEST(image) test_loadwmf(); test_createfromwmf(); test_createfromwmf_noplaceable(); + test_loadwmf_noplaceable(); + test_drawwmf(); + test_enumwmf(); + test_savemetafile(); + test_savemetafile_background(); test_resolution(); test_createhbitmap(); test_getthumbnail(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11180
On Fri Jun 19 15:54:06 2026 +0000, Esme Povirk wrote:
I think so, yeah. Okay I pushed a new set of patches that drops the WMF parsing and routes it all through the EMF handler
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11180#note_143665
This merge request was approved by Esme Povirk. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11180
participants (3)
-
Esme Povirk (@madewokherd) -
Rose Hellsing -
Rose Hellsing (@axtlos)