[PATCH v7 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 -- v7: gdiplus/tests: Add tests for raw WMF load, playback and encoding gdiplus: Enumerate and play WMF records natively 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> Use EnumMetaFile/PlayMetaFileRecord for WMF metafiles instead of relying solely on the SetWinMetaFileBits EMF conversion. --- dlls/gdiplus/gdiplus_private.h | 2 + dlls/gdiplus/metafile.c | 101 +++++++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 6 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..adc15604538 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"); @@ -2800,6 +2802,36 @@ GpStatus WINGDIPAPI GdipPlayMetafileRecord(GDIPCONST GpMetafile *metafile, return OutOfMemory; } } + else if (recordType & GDIP_WMF_RECORD_BASE) + { + /* WMF record */ + if (real_metafile->playback_dc) + { + METARECORD *record; + DWORD record_size = dataSize + 6; + + /* WMF record size is expressed in 16-bit words. */ + if (record_size & 1) + record_size++; + + record = calloc(1, record_size); + if (record) + { + record->rdSize = record_size / 2; + record->rdFunction = recordType & 0xffff; + if (dataSize) + memcpy(record->rdParm, data, dataSize); + + if (!PlayMetaFileRecord(real_metafile->playback_dc, real_metafile->handle_table, + record, real_metafile->handle_count)) + ERR("PlayMetaFileRecord failed\n"); + + free(record); + } + else + return OutOfMemory; + } + } else { EmfPlusRecordHeader *header = (EmfPlusRecordHeader*)(data)-1; @@ -3826,6 +3858,33 @@ static int CALLBACK enum_metafile_proc(HDC hDC, HANDLETABLE *lpHTable, const ENH pStr, data->callback_data); } +static int CALLBACK enum_metafile_wmf_proc(HDC hDC, HANDLETABLE *lpHTable, METARECORD *lpMFR, + int nObj, LPARAM lpData) +{ + struct enum_metafile_data *data = (struct enum_metafile_data*)lpData; + const BYTE *pStr; + UINT data_size; + + data->metafile->handle_table = lpHTable; + data->metafile->handle_count = nObj; + + /* WMF record size is in 16-bit words and includes the 6-byte header + * (DWORD rdSize + WORD rdFunction). */ + if (lpMFR->rdSize * 2 > 6) + { + pStr = (const BYTE*)lpMFR->rdParm; + data_size = lpMFR->rdSize * 2 - 6; + } + else + { + pStr = NULL; + data_size = 0; + } + + return data->callback(GDIP_WMF_RECORD_TO_EMFPLUS(lpMFR->rdFunction), 0, + data_size, pStr, data->callback_data); +} + GpStatus WINGDIPAPI GdipEnumerateMetafileSrcRectDestPoints(GpGraphics *graphics, GDIPCONST GpMetafile *metafile, GDIPCONST GpPointF *destPoints, INT count, GDIPCONST GpRectF *srcRect, Unit srcUnit, EnumerateMetafileProc callback, @@ -3845,7 +3904,7 @@ GpStatus WINGDIPAPI GdipEnumerateMetafileSrcRectDestPoints(GpGraphics *graphics, if (!graphics || !metafile || !destPoints || count != 3 || !srcRect) return InvalidParameter; - if (!metafile->hemf) + if (!metafile->hemf && !metafile->hwmf) return InvalidParameter; if (metafile->playback_graphics) @@ -3918,7 +3977,11 @@ GpStatus WINGDIPAPI GdipEnumerateMetafileSrcRectDestPoints(GpGraphics *graphics, { real_metafile->page_unit = UnitDisplay; real_metafile->page_scale = 1.0; - stat = METAFILE_PlaybackUpdateWorldTransform(real_metafile); + if (metafile->hwmf && (metafile->metafile_type == MetafileTypeWmf || + metafile->metafile_type == MetafileTypeWmfPlaceable)) + stat = GdipResetWorldTransform(graphics); + else + stat = METAFILE_PlaybackUpdateWorldTransform(real_metafile); } if (stat == Ok) @@ -3937,8 +4000,30 @@ GpStatus WINGDIPAPI GdipEnumerateMetafileSrcRectDestPoints(GpGraphics *graphics, } if (stat == Ok) - EnumEnhMetaFile(real_metafile->playback_dc, metafile->hemf, enum_metafile_proc, - &data, &dst_bounds); + { + if (metafile->hwmf && (metafile->metafile_type == MetafileTypeWmf || + metafile->metafile_type == MetafileTypeWmfPlaceable)) + { + /* For native WMF playback, use gdi32's mapping mode to map + * the metafile's logical window directly to the destination + * rectangle. */ + SetMapMode(real_metafile->playback_dc, MM_ANISOTROPIC); + SetWindowOrgEx(real_metafile->playback_dc, srcRect->X, srcRect->Y, NULL); + SetWindowExtEx(real_metafile->playback_dc, srcRect->Width, srcRect->Height, NULL); + SetViewportOrgEx(real_metafile->playback_dc, dst_bounds.left, dst_bounds.top, NULL); + SetViewportExtEx(real_metafile->playback_dc, + dst_bounds.right - dst_bounds.left, + dst_bounds.bottom - dst_bounds.top, NULL); + + EnumMetaFile(real_metafile->playback_dc, metafile->hwmf, + enum_metafile_wmf_proc, (LPARAM)&data); + } + else + { + EnumEnhMetaFile(real_metafile->playback_dc, metafile->hemf, enum_metafile_proc, + &data, &dst_bounds); + } + } METAFILE_PlaybackReleaseDC(real_metafile); @@ -4315,6 +4400,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 +4418,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 | 379 +++++++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) diff --git a/dlls/gdiplus/tests/image.c b/dlls/gdiplus/tests/image.c index a1b81c8eda7..09d40cf5784 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,361 @@ 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); + + /* The metafile playback must invoke the user callback at least once + * for each record encountered. Despite the WmfRecordType* entries in + * the EmfPlusRecordType enum, Windows seems to internally convert WMF + * metafiles to EMF before enumeration and reports every record using + * plain EMF record types. Wine currently uses native WMF playback, + * so it reports records with the WMF bit set instead. */ + ok(state.count > 0, "Expected callback to be invoked at least once\n"); + todo_wine + 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 +7267,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:42:20 2026 +0000, Rose Hellsing wrote:
Wines EMF handling appears to be lossy so I made this a `todo_wine` for now since fixing that seems out of scope for this MR Oh, this is the possibility I was worried about.
Currently, Wine is converting to EMF in all cases, so the enumeration behavior matches native (other than Wine's WMF->EMF conversion being broken it sounds like?). So changing it to the WMF records risks causing a regression as an application might depend on that. I hadn't realized before reviewing this that Wine's gdiplus has a working (if flawed) route for drawing WMF images, and after that I didn't re-evaluate what you needed for this bug, and so I sent you on an unnecessary side-quest. Sorry about that. It sounds like all you *really* need is the format detection and encoding fix. Unless you know another case where native does use the WMF records, we should probably not introduce the separate WMF enumeration path so we don't risk a regression, and keep depending on the EMF conversion as we did previously. Storing the WMF handle is a good change though (as that lets us properly manage the lifetime), even though we don't actually use it for anything else. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11180#note_143651
On Fri Jun 19 15:42:20 2026 +0000, Esme Povirk wrote:
Oh, this is the possibility I was worried about. Currently, Wine is converting to EMF in all cases, so the enumeration behavior matches native (other than Wine's WMF->EMF conversion being broken it sounds like?). So changing it to the WMF records risks causing a regression as an application might depend on that. I hadn't realized before reviewing this that Wine's gdiplus has a working (if flawed) route for drawing WMF images, and after that I didn't re-evaluate what you needed for this bug, and so I sent you on an unnecessary side-quest. Sorry about that. It sounds like all you *really* need is the format detection and encoding fix. Unless you know another case where native does use the WMF records, we should probably not introduce the separate WMF enumeration path so we don't risk a regression, and keep depending on the EMF conversion as we did previously. Storing the WMF handle is a good change though (as that lets us properly manage the lifetime), even though we don't actually use it for anything else. Hm right that makes sense. So it'd be best to drop all the WMF record stuff and just keep the signature and WMF handle patches?
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11180#note_143654
On Fri Jun 19 15:53:10 2026 +0000, Rose Hellsing wrote:
Hm right that makes sense. So it'd be best to drop all the WMF record stuff and just keep the signature and WMF handle patches? I think so, yeah.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11180#note_143655
participants (3)
-
Esme Povirk (@madewokherd) -
Rose Hellsing -
Rose Hellsing (@axtlos)