[winpr,utils] improve winpr image API
* Add checks for bitmap read functions * Add more unit tests * Do not expose internals to WinPR
@ -22,9 +22,22 @@
|
||||
#include <errno.h>
|
||||
#include <winpr/crt.h>
|
||||
#include <winpr/user.h>
|
||||
#include <winpr/image.h>
|
||||
|
||||
#include "clipboard.h"
|
||||
|
||||
static const char* mime_bitmap[] = { "image/bmp", "image/x-bmp", "image/x-MS-bmp",
|
||||
"image/x-win-bitmap" };
|
||||
|
||||
#if defined(WINPR_UTILS_IMAGE_WEBP)
|
||||
static const char mime_webp[] = "image/webp";
|
||||
#endif
|
||||
#if defined(WINPR_UTILS_IMAGE_PNG)
|
||||
static const char mime_png[] = "image/png";
|
||||
#endif
|
||||
#if defined(WINPR_UTILS_IMAGE_JPEG)
|
||||
static const char mime_jpeg[] = "image/jpeg";
|
||||
#endif
|
||||
/**
|
||||
* Standard Clipboard Formats:
|
||||
* http://msdn.microsoft.com/en-us/library/windows/desktop/ff729168/
|
||||
@ -189,6 +202,19 @@ static void* clipboard_synthesize_utf8_string(wClipboard* clipboard, UINT32 form
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static BOOL is_format_bitmap(wClipboard* clipboard, UINT32 formatId)
|
||||
{
|
||||
for (size_t x = 0; x < ARRAYSIZE(mime_bitmap); x++)
|
||||
{
|
||||
const char* mime = mime_bitmap[x];
|
||||
const UINT32 altFormatId = ClipboardGetFormatId(clipboard, mime);
|
||||
if (altFormatId == formatId)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* "CF_DIB":
|
||||
*
|
||||
@ -206,9 +232,9 @@ static void* clipboard_synthesize_cf_dib(wClipboard* clipboard, UINT32 formatId,
|
||||
if (formatId == CF_DIBV5)
|
||||
{
|
||||
}
|
||||
else if (formatId == ClipboardGetFormatId(clipboard, "image/bmp"))
|
||||
else if (is_format_bitmap(clipboard, formatId))
|
||||
{
|
||||
const BITMAPFILEHEADER* pFileHeader = NULL;
|
||||
const BITMAPFILEHEADER* pFileHeader;
|
||||
|
||||
if (SrcSize < (sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)))
|
||||
return NULL;
|
||||
@ -245,13 +271,41 @@ static void* clipboard_synthesize_cf_dibv5(wClipboard* clipboard, UINT32 formatI
|
||||
if (formatId == CF_DIB)
|
||||
{
|
||||
}
|
||||
else if (formatId == ClipboardGetFormatId(clipboard, "image/bmp"))
|
||||
else if (is_format_bitmap(clipboard, formatId))
|
||||
{
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void* clipboard_prepend_bmp_header(const BITMAPINFOHEADER* pInfoHeader, const void* data,
|
||||
size_t size, UINT32* pSize)
|
||||
{
|
||||
WINPR_ASSERT(pInfoHeader);
|
||||
WINPR_ASSERT(pSize);
|
||||
|
||||
*pSize = 0;
|
||||
if ((pInfoHeader->biBitCount < 1) || (pInfoHeader->biBitCount > 32))
|
||||
return NULL;
|
||||
|
||||
const size_t DstSize = sizeof(BITMAPFILEHEADER) + size;
|
||||
BYTE* pDstData = (BYTE*)malloc(DstSize);
|
||||
|
||||
if (!pDstData)
|
||||
return NULL;
|
||||
|
||||
BITMAPFILEHEADER* pFileHeader = (BITMAPFILEHEADER*)pDstData;
|
||||
pFileHeader->bfType = 0x4D42;
|
||||
pFileHeader->bfSize = DstSize;
|
||||
pFileHeader->bfReserved1 = 0;
|
||||
pFileHeader->bfReserved2 = 0;
|
||||
pFileHeader->bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
|
||||
unsigned char* pDst = &pDstData[sizeof(BITMAPFILEHEADER)];
|
||||
CopyMemory(pDst, data, size);
|
||||
*pSize = DstSize;
|
||||
return pDstData;
|
||||
}
|
||||
|
||||
/**
|
||||
* "image/bmp":
|
||||
*
|
||||
@ -261,41 +315,15 @@ static void* clipboard_synthesize_cf_dibv5(wClipboard* clipboard, UINT32 formatI
|
||||
static void* clipboard_synthesize_image_bmp(wClipboard* clipboard, UINT32 formatId,
|
||||
const void* data, UINT32* pSize)
|
||||
{
|
||||
UINT32 SrcSize = 0;
|
||||
UINT32 DstSize = 0;
|
||||
BYTE* pDstData = NULL;
|
||||
SrcSize = *pSize;
|
||||
UINT32 SrcSize = *pSize;
|
||||
|
||||
if (formatId == CF_DIB)
|
||||
{
|
||||
BYTE* pDst = NULL;
|
||||
const BITMAPINFOHEADER* pInfoHeader = NULL;
|
||||
BITMAPFILEHEADER* pFileHeader = NULL;
|
||||
|
||||
if (SrcSize < sizeof(BITMAPINFOHEADER))
|
||||
return NULL;
|
||||
|
||||
pInfoHeader = (const BITMAPINFOHEADER*)data;
|
||||
|
||||
if ((pInfoHeader->biBitCount < 1) || (pInfoHeader->biBitCount > 32))
|
||||
return NULL;
|
||||
|
||||
DstSize = sizeof(BITMAPFILEHEADER) + SrcSize;
|
||||
pDstData = (BYTE*)malloc(DstSize);
|
||||
|
||||
if (!pDstData)
|
||||
return NULL;
|
||||
|
||||
pFileHeader = (BITMAPFILEHEADER*)pDstData;
|
||||
pFileHeader->bfType = 0x4D42;
|
||||
pFileHeader->bfSize = DstSize;
|
||||
pFileHeader->bfReserved1 = 0;
|
||||
pFileHeader->bfReserved2 = 0;
|
||||
pFileHeader->bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
|
||||
pDst = &pDstData[sizeof(BITMAPFILEHEADER)];
|
||||
CopyMemory(pDst, data, SrcSize);
|
||||
*pSize = DstSize;
|
||||
return pDstData;
|
||||
const BITMAPINFOHEADER* pInfoHeader = (const BITMAPINFOHEADER*)data;
|
||||
return clipboard_prepend_bmp_header(pInfoHeader, data, SrcSize, pSize);
|
||||
}
|
||||
else if (formatId == CF_DIBV5)
|
||||
{
|
||||
@ -304,6 +332,119 @@ static void* clipboard_synthesize_image_bmp(wClipboard* clipboard, UINT32 format
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void* clipboard_synthesize_image_bmp_to_format(wClipboard* clipboard, UINT32 formatId,
|
||||
UINT32 bmpFormat, const void* data,
|
||||
UINT32* pSize)
|
||||
{
|
||||
WINPR_ASSERT(clipboard);
|
||||
WINPR_ASSERT(data);
|
||||
WINPR_ASSERT(pSize);
|
||||
|
||||
size_t dsize = 0;
|
||||
void* result = NULL;
|
||||
|
||||
wImage* img = winpr_image_new();
|
||||
void* bmp = clipboard_synthesize_image_bmp(clipboard, formatId, data, pSize);
|
||||
const UINT32 SrcSize = *pSize;
|
||||
*pSize = 0;
|
||||
|
||||
if (!bmp || !img)
|
||||
goto fail;
|
||||
|
||||
if (winpr_image_read_buffer(img, bmp, SrcSize) <= 0)
|
||||
goto fail;
|
||||
|
||||
result = winpr_image_write_buffer(img, bmpFormat, &dsize);
|
||||
if (result)
|
||||
*pSize = dsize;
|
||||
|
||||
fail:
|
||||
free(bmp);
|
||||
winpr_image_free(img, TRUE);
|
||||
return result;
|
||||
}
|
||||
|
||||
#if defined(WINPR_UTILS_IMAGE_PNG)
|
||||
static void* clipboard_synthesize_image_bmp_to_png(wClipboard* clipboard, UINT32 formatId,
|
||||
const void* data, UINT32* pSize)
|
||||
{
|
||||
return clipboard_synthesize_image_bmp_to_format(clipboard, formatId, WINPR_IMAGE_PNG, data,
|
||||
pSize);
|
||||
}
|
||||
|
||||
static void* clipboard_synthesize_image_format_to_bmp(wClipboard* clipboard, UINT32 srcFormatId,
|
||||
const void* data, UINT32* pSize)
|
||||
{
|
||||
WINPR_ASSERT(clipboard);
|
||||
WINPR_ASSERT(data);
|
||||
WINPR_ASSERT(pSize);
|
||||
|
||||
void* dst = NULL;
|
||||
const UINT32 SrcSize = *pSize;
|
||||
size_t size = 0;
|
||||
wImage* image = winpr_image_new();
|
||||
if (!image)
|
||||
goto fail;
|
||||
|
||||
const int res = winpr_image_read_buffer(image, data, SrcSize);
|
||||
if (res <= 0)
|
||||
goto fail;
|
||||
|
||||
dst = winpr_image_write_buffer(image, WINPR_IMAGE_BITMAP, &size);
|
||||
if ((size < sizeof(WINPR_BITMAP_FILE_HEADER)) || (size > UINT32_MAX))
|
||||
{
|
||||
free(dst);
|
||||
dst = NULL;
|
||||
goto fail;
|
||||
}
|
||||
*pSize = (UINT32)size;
|
||||
|
||||
fail:
|
||||
winpr_image_free(image, TRUE);
|
||||
|
||||
if (dst)
|
||||
memmove(dst, &dst[sizeof(WINPR_BITMAP_FILE_HEADER)],
|
||||
size - sizeof(WINPR_BITMAP_FILE_HEADER));
|
||||
return dst;
|
||||
}
|
||||
|
||||
static void* clipboard_synthesize_image_png_to_bmp(wClipboard* clipboard, UINT32 formatId,
|
||||
const void* data, UINT32* pSize)
|
||||
{
|
||||
return clipboard_synthesize_image_format_to_bmp(clipboard, formatId, data, pSize);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(WINPR_UTILS_IMAGE_WEBP)
|
||||
static void* clipboard_synthesize_image_bmp_to_webp(wClipboard* clipboard, UINT32 formatId,
|
||||
const void* data, UINT32* pSize)
|
||||
{
|
||||
return clipboard_synthesize_image_bmp_to_format(clipboard, formatId, WINPR_IMAGE_WEBP, data,
|
||||
pSize);
|
||||
}
|
||||
|
||||
static void* clipboard_synthesize_image_webp_to_bmp(wClipboard* clipboard, UINT32 formatId,
|
||||
const void* data, UINT32* pSize)
|
||||
{
|
||||
return clipboard_synthesize_image_format_to_bmp(clipboard, formatId, data, pSize);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(WINPR_UTILS_IMAGE_JPEG)
|
||||
static void* clipboard_synthesize_image_bmp_to_jpeg(wClipboard* clipboard, UINT32 formatId,
|
||||
const void* data, UINT32* pSize)
|
||||
{
|
||||
return clipboard_synthesize_image_bmp_to_format(clipboard, formatId, WINPR_IMAGE_JPEG, data,
|
||||
pSize);
|
||||
}
|
||||
|
||||
static void* clipboard_synthesize_image_jpeg_to_bmp(wClipboard* clipboard, UINT32 formatId,
|
||||
const void* data, UINT32* pSize)
|
||||
{
|
||||
return clipboard_synthesize_image_format_to_bmp(clipboard, formatId, data, pSize);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* "HTML Format":
|
||||
*
|
||||
@ -476,127 +617,209 @@ static void* clipboard_synthesize_text_html(wClipboard* clipboard, UINT32 format
|
||||
|
||||
BOOL ClipboardInitSynthesizers(wClipboard* clipboard)
|
||||
{
|
||||
UINT32 formatId = 0;
|
||||
UINT32 altFormatId = 0;
|
||||
/**
|
||||
* CF_TEXT
|
||||
*/
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_TEXT, CF_OEMTEXT, clipboard_synthesize_cf_oemtext);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_TEXT, CF_UNICODETEXT,
|
||||
clipboard_synthesize_cf_unicodetext);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_TEXT, CF_LOCALE, clipboard_synthesize_cf_locale);
|
||||
altFormatId = ClipboardRegisterFormat(clipboard, mime_text_plain);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_TEXT, altFormatId, clipboard_synthesize_utf8_string);
|
||||
{
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_TEXT, CF_OEMTEXT,
|
||||
clipboard_synthesize_cf_oemtext);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_TEXT, CF_UNICODETEXT,
|
||||
clipboard_synthesize_cf_unicodetext);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_TEXT, CF_LOCALE, clipboard_synthesize_cf_locale);
|
||||
|
||||
UINT32 altFormatId = ClipboardRegisterFormat(clipboard, mime_text_plain);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_TEXT, altFormatId,
|
||||
clipboard_synthesize_utf8_string);
|
||||
}
|
||||
/**
|
||||
* CF_OEMTEXT
|
||||
*/
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_OEMTEXT, CF_TEXT, clipboard_synthesize_cf_text);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_OEMTEXT, CF_UNICODETEXT,
|
||||
clipboard_synthesize_cf_unicodetext);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_OEMTEXT, CF_LOCALE, clipboard_synthesize_cf_locale);
|
||||
altFormatId = ClipboardRegisterFormat(clipboard, mime_text_plain);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_OEMTEXT, altFormatId,
|
||||
clipboard_synthesize_utf8_string);
|
||||
{
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_OEMTEXT, CF_TEXT, clipboard_synthesize_cf_text);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_OEMTEXT, CF_UNICODETEXT,
|
||||
clipboard_synthesize_cf_unicodetext);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_OEMTEXT, CF_LOCALE,
|
||||
clipboard_synthesize_cf_locale);
|
||||
UINT32 altFormatId = ClipboardRegisterFormat(clipboard, mime_text_plain);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_OEMTEXT, altFormatId,
|
||||
clipboard_synthesize_utf8_string);
|
||||
}
|
||||
/**
|
||||
* CF_UNICODETEXT
|
||||
*/
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_UNICODETEXT, CF_TEXT, clipboard_synthesize_cf_text);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_UNICODETEXT, CF_OEMTEXT,
|
||||
clipboard_synthesize_cf_oemtext);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_UNICODETEXT, CF_LOCALE,
|
||||
clipboard_synthesize_cf_locale);
|
||||
altFormatId = ClipboardRegisterFormat(clipboard, mime_text_plain);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_UNICODETEXT, altFormatId,
|
||||
clipboard_synthesize_utf8_string);
|
||||
{
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_UNICODETEXT, CF_TEXT,
|
||||
clipboard_synthesize_cf_text);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_UNICODETEXT, CF_OEMTEXT,
|
||||
clipboard_synthesize_cf_oemtext);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_UNICODETEXT, CF_LOCALE,
|
||||
clipboard_synthesize_cf_locale);
|
||||
UINT32 altFormatId = ClipboardRegisterFormat(clipboard, mime_text_plain);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_UNICODETEXT, altFormatId,
|
||||
clipboard_synthesize_utf8_string);
|
||||
}
|
||||
/**
|
||||
* UTF8_STRING
|
||||
*/
|
||||
formatId = ClipboardRegisterFormat(clipboard, mime_text_plain);
|
||||
|
||||
if (formatId)
|
||||
{
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_TEXT, clipboard_synthesize_cf_text);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_OEMTEXT,
|
||||
clipboard_synthesize_cf_oemtext);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_UNICODETEXT,
|
||||
clipboard_synthesize_cf_unicodetext);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_LOCALE,
|
||||
clipboard_synthesize_cf_locale);
|
||||
}
|
||||
UINT32 formatId = ClipboardRegisterFormat(clipboard, mime_text_plain);
|
||||
|
||||
if (formatId)
|
||||
{
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_TEXT,
|
||||
clipboard_synthesize_cf_text);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_OEMTEXT,
|
||||
clipboard_synthesize_cf_oemtext);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_UNICODETEXT,
|
||||
clipboard_synthesize_cf_unicodetext);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_LOCALE,
|
||||
clipboard_synthesize_cf_locale);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* text/plain
|
||||
*/
|
||||
formatId = ClipboardRegisterFormat(clipboard, mime_text_plain);
|
||||
|
||||
if (formatId)
|
||||
{
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_TEXT, clipboard_synthesize_cf_text);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_OEMTEXT,
|
||||
clipboard_synthesize_cf_oemtext);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_UNICODETEXT,
|
||||
clipboard_synthesize_cf_unicodetext);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_LOCALE,
|
||||
clipboard_synthesize_cf_locale);
|
||||
}
|
||||
UINT32 formatId = ClipboardRegisterFormat(clipboard, mime_text_plain);
|
||||
|
||||
if (formatId)
|
||||
{
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_TEXT,
|
||||
clipboard_synthesize_cf_text);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_OEMTEXT,
|
||||
clipboard_synthesize_cf_oemtext);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_UNICODETEXT,
|
||||
clipboard_synthesize_cf_unicodetext);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_LOCALE,
|
||||
clipboard_synthesize_cf_locale);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* CF_DIB
|
||||
*/
|
||||
|
||||
if (formatId)
|
||||
{
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIB, CF_DIBV5, clipboard_synthesize_cf_dibv5);
|
||||
altFormatId = ClipboardRegisterFormat(clipboard, "image/bmp");
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIB, altFormatId,
|
||||
clipboard_synthesize_image_bmp);
|
||||
for (size_t x = 0; x < ARRAYSIZE(mime_bitmap); x++)
|
||||
{
|
||||
const char* mime = mime_bitmap[x];
|
||||
const UINT32 altFormatId = ClipboardRegisterFormat(clipboard, mime);
|
||||
if (altFormatId == 0)
|
||||
continue;
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIB, altFormatId,
|
||||
clipboard_synthesize_image_bmp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CF_DIBV5
|
||||
*/
|
||||
|
||||
if (formatId && 0)
|
||||
if (0)
|
||||
{
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIBV5, CF_DIB, clipboard_synthesize_cf_dib);
|
||||
altFormatId = ClipboardRegisterFormat(clipboard, "image/bmp");
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIBV5, altFormatId,
|
||||
clipboard_synthesize_image_bmp);
|
||||
|
||||
for (size_t x = 0; x < ARRAYSIZE(mime_bitmap); x++)
|
||||
{
|
||||
const char* mime = mime_bitmap[x];
|
||||
const UINT32 altFormatId = ClipboardRegisterFormat(clipboard, mime);
|
||||
if (altFormatId == 0)
|
||||
continue;
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIBV5, altFormatId,
|
||||
clipboard_synthesize_image_bmp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* image/bmp
|
||||
*/
|
||||
formatId = ClipboardRegisterFormat(clipboard, "image/bmp");
|
||||
|
||||
if (formatId)
|
||||
for (size_t x = 0; x < ARRAYSIZE(mime_bitmap); x++)
|
||||
{
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_DIB, clipboard_synthesize_cf_dib);
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, CF_DIBV5, clipboard_synthesize_cf_dibv5);
|
||||
const char* mime = mime_bitmap[x];
|
||||
const UINT32 altFormatId = ClipboardRegisterFormat(clipboard, mime);
|
||||
if (altFormatId == 0)
|
||||
continue;
|
||||
ClipboardRegisterSynthesizer(clipboard, altFormatId, CF_DIB, clipboard_synthesize_cf_dib);
|
||||
ClipboardRegisterSynthesizer(clipboard, altFormatId, CF_DIBV5,
|
||||
clipboard_synthesize_cf_dibv5);
|
||||
}
|
||||
|
||||
/**
|
||||
* image/png
|
||||
*/
|
||||
#if defined(WINPR_UTILS_IMAGE_PNG)
|
||||
{
|
||||
const UINT32 altFormatId = ClipboardRegisterFormat(clipboard, mime_png);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIB, altFormatId,
|
||||
clipboard_synthesize_image_bmp_to_png);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIBV5, altFormatId,
|
||||
clipboard_synthesize_image_bmp_to_png);
|
||||
ClipboardRegisterSynthesizer(clipboard, altFormatId, CF_DIB,
|
||||
clipboard_synthesize_image_png_to_bmp);
|
||||
ClipboardRegisterSynthesizer(clipboard, altFormatId, CF_DIBV5,
|
||||
clipboard_synthesize_image_png_to_bmp);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* image/webp
|
||||
*/
|
||||
#if defined(WINPR_UTILS_IMAGE_WEBP)
|
||||
{
|
||||
const UINT32 altFormatId = ClipboardRegisterFormat(clipboard, mime_webp);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIB, altFormatId,
|
||||
clipboard_synthesize_image_bmp_to_webp);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIBV5, altFormatId,
|
||||
clipboard_synthesize_image_webp_to_bmp);
|
||||
ClipboardRegisterSynthesizer(clipboard, altFormatId, CF_DIB,
|
||||
clipboard_synthesize_image_bmp_to_webp);
|
||||
ClipboardRegisterSynthesizer(clipboard, altFormatId, CF_DIBV5,
|
||||
clipboard_synthesize_image_webp_to_bmp);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* image/jpeg
|
||||
*/
|
||||
#if defined(WINPR_UTILS_IMAGE_JPEG)
|
||||
{
|
||||
const UINT32 altFormatId = ClipboardRegisterFormat(clipboard, mime_jpeg);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIB, altFormatId,
|
||||
clipboard_synthesize_image_bmp_to_jpeg);
|
||||
ClipboardRegisterSynthesizer(clipboard, CF_DIBV5, altFormatId,
|
||||
clipboard_synthesize_image_jpeg_to_bmp);
|
||||
ClipboardRegisterSynthesizer(clipboard, altFormatId, CF_DIB,
|
||||
clipboard_synthesize_image_bmp_to_jpeg);
|
||||
ClipboardRegisterSynthesizer(clipboard, altFormatId, CF_DIBV5,
|
||||
clipboard_synthesize_image_jpeg_to_bmp);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* HTML Format
|
||||
*/
|
||||
formatId = ClipboardRegisterFormat(clipboard, "HTML Format");
|
||||
|
||||
if (formatId)
|
||||
{
|
||||
altFormatId = ClipboardRegisterFormat(clipboard, "text/html");
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, altFormatId,
|
||||
clipboard_synthesize_text_html);
|
||||
UINT32 formatId = ClipboardRegisterFormat(clipboard, "HTML Format");
|
||||
|
||||
if (formatId)
|
||||
{
|
||||
const UINT32 altFormatId = ClipboardRegisterFormat(clipboard, "text/html");
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, altFormatId,
|
||||
clipboard_synthesize_text_html);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* text/html
|
||||
*/
|
||||
formatId = ClipboardRegisterFormat(clipboard, "text/html");
|
||||
|
||||
if (formatId)
|
||||
{
|
||||
altFormatId = ClipboardRegisterFormat(clipboard, "HTML Format");
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, altFormatId,
|
||||
clipboard_synthesize_html_format);
|
||||
UINT32 formatId = ClipboardRegisterFormat(clipboard, "text/html");
|
||||
|
||||
if (formatId)
|
||||
{
|
||||
const UINT32 altFormatId = ClipboardRegisterFormat(clipboard, "HTML Format");
|
||||
ClipboardRegisterSynthesizer(clipboard, formatId, altFormatId,
|
||||
clipboard_synthesize_html_format);
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
|
@ -29,8 +29,6 @@
|
||||
|
||||
#include <winpr/image.h>
|
||||
|
||||
#include "image.h"
|
||||
|
||||
#if defined(WINPR_UTILS_IMAGE_PNG)
|
||||
#include <png.h>
|
||||
#endif
|
||||
@ -54,6 +52,13 @@
|
||||
#include "../log.h"
|
||||
#define TAG WINPR_TAG("utils.image")
|
||||
|
||||
static SSIZE_T winpr_convert_from_jpeg(const char* comp_data, size_t comp_data_bytes, UINT32* width,
|
||||
UINT32* height, UINT32* bpp, char** ppdecomp_data);
|
||||
static SSIZE_T winpr_convert_from_png(const char* comp_data, size_t comp_data_bytes, UINT32* width,
|
||||
UINT32* height, UINT32* bpp, char** ppdecomp_data);
|
||||
static SSIZE_T winpr_convert_from_webp(const char* comp_data, size_t comp_data_bytes, UINT32* width,
|
||||
UINT32* height, UINT32* bpp, char** ppdecomp_data);
|
||||
|
||||
static BOOL writeBitmapFileHeader(wStream* s, const WINPR_BITMAP_FILE_HEADER* bf)
|
||||
{
|
||||
if (!Stream_EnsureRemainingCapacity(s, sizeof(WINPR_BITMAP_FILE_HEADER)))
|
||||
@ -79,7 +84,15 @@ static BOOL readBitmapFileHeader(wStream* s, WINPR_BITMAP_FILE_HEADER* bf)
|
||||
Stream_Read_UINT16(s, bf->bfReserved1);
|
||||
Stream_Read_UINT16(s, bf->bfReserved2);
|
||||
Stream_Read_UINT32(s, bf->bfOffBits);
|
||||
return TRUE;
|
||||
|
||||
if (bf->bfSize < sizeof(WINPR_BITMAP_FILE_HEADER))
|
||||
{
|
||||
WLog_ERR(TAG, "");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return Stream_CheckAndLogRequiredCapacity(TAG, s,
|
||||
bf->bfSize - sizeof(WINPR_BITMAP_FILE_HEADER));
|
||||
}
|
||||
|
||||
static BOOL writeBitmapInfoHeader(wStream* s, const WINPR_BITMAP_INFO_HEADER* bi)
|
||||
@ -101,11 +114,12 @@ static BOOL writeBitmapInfoHeader(wStream* s, const WINPR_BITMAP_INFO_HEADER* bi
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static BOOL readBitmapInfoHeader(wStream* s, WINPR_BITMAP_INFO_HEADER* bi)
|
||||
static BOOL readBitmapInfoHeader(wStream* s, WINPR_BITMAP_INFO_HEADER* bi, size_t* poffset)
|
||||
{
|
||||
if (!s || !bi || (!Stream_CheckAndLogRequiredLength(TAG, s, sizeof(WINPR_BITMAP_INFO_HEADER))))
|
||||
return FALSE;
|
||||
|
||||
const size_t start = Stream_GetPosition(s);
|
||||
Stream_Read_UINT32(s, bi->biSize);
|
||||
Stream_Read_INT32(s, bi->biWidth);
|
||||
Stream_Read_INT32(s, bi->biHeight);
|
||||
@ -117,7 +131,54 @@ static BOOL readBitmapInfoHeader(wStream* s, WINPR_BITMAP_INFO_HEADER* bi)
|
||||
Stream_Read_INT32(s, bi->biYPelsPerMeter);
|
||||
Stream_Read_UINT32(s, bi->biClrUsed);
|
||||
Stream_Read_UINT32(s, bi->biClrImportant);
|
||||
return TRUE;
|
||||
|
||||
if ((bi->biBitCount < 1) || (bi->biBitCount > 32))
|
||||
{
|
||||
WLog_WARN(TAG, "invalid biBitCount=%" PRIu32, bi->biBitCount);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader */
|
||||
size_t offset = 0;
|
||||
switch (bi->biCompression)
|
||||
{
|
||||
case BI_RGB:
|
||||
if (bi->biBitCount <= 8)
|
||||
{
|
||||
DWORD used = bi->biClrUsed;
|
||||
if (used == 0)
|
||||
used = (1 << bi->biBitCount) / 8;
|
||||
offset += sizeof(RGBQUAD) * used;
|
||||
}
|
||||
if (bi->biSizeImage == 0)
|
||||
{
|
||||
UINT32 stride = ((((bi->biWidth * bi->biBitCount) + 31) & ~31) >> 3);
|
||||
bi->biSizeImage = abs(bi->biHeight) * stride;
|
||||
}
|
||||
break;
|
||||
case BI_BITFIELDS:
|
||||
offset += sizeof(DWORD) * 3; // 3 DWORD color masks
|
||||
break;
|
||||
default:
|
||||
WLog_ERR(TAG, "unsupported biCompression %" PRIu32, bi->biCompression);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (bi->biSizeImage == 0)
|
||||
{
|
||||
WLog_ERR(TAG, "invalid biSizeImage %" PRIuz, bi->biSizeImage);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
const size_t pos = Stream_GetPosition(s) - start;
|
||||
if (bi->biSize < pos)
|
||||
{
|
||||
WLog_ERR(TAG, "invalid biSize %" PRIuz " < (actual) offset %" PRIuz, bi->biSize, pos);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
*poffset = offset;
|
||||
return Stream_SafeSeek(s, bi->biSize - pos);
|
||||
}
|
||||
|
||||
BYTE* winpr_bitmap_construct_header(size_t width, size_t height, size_t bpp)
|
||||
@ -140,19 +201,38 @@ BYTE* winpr_bitmap_construct_header(size_t width, size_t height, size_t bpp)
|
||||
bf.bfType[1] = 'M';
|
||||
bf.bfReserved1 = 0;
|
||||
bf.bfReserved2 = 0;
|
||||
bf.bfOffBits = (UINT32)sizeof(WINPR_BITMAP_FILE_HEADER) + sizeof(WINPR_BITMAP_INFO_HEADER);
|
||||
bi.biSize = (UINT32)sizeof(WINPR_BITMAP_INFO_HEADER);
|
||||
bf.bfOffBits = (UINT32)sizeof(WINPR_BITMAP_FILE_HEADER) + bi.biSize;
|
||||
bi.biSizeImage = (UINT32)imgSize;
|
||||
bf.bfSize = bf.bfOffBits + bi.biSizeImage;
|
||||
bi.biWidth = (INT32)width;
|
||||
bi.biHeight = -1 * (INT32)height;
|
||||
bi.biPlanes = 1;
|
||||
bi.biBitCount = (UINT16)bpp;
|
||||
bi.biCompression = 0;
|
||||
bi.biCompression = BI_RGB;
|
||||
bi.biXPelsPerMeter = (INT32)width;
|
||||
bi.biYPelsPerMeter = (INT32)height;
|
||||
bi.biClrUsed = 0;
|
||||
bi.biClrImportant = 0;
|
||||
bi.biSize = (UINT32)sizeof(WINPR_BITMAP_INFO_HEADER);
|
||||
|
||||
size_t offset = 0;
|
||||
switch (bi.biCompression)
|
||||
{
|
||||
case BI_RGB:
|
||||
if (bi.biBitCount <= 8)
|
||||
{
|
||||
DWORD used = bi.biClrUsed;
|
||||
if (used == 0)
|
||||
used = (1 << bi.biBitCount) / 8;
|
||||
offset += sizeof(RGBQUAD) * used;
|
||||
}
|
||||
break;
|
||||
case BI_BITFIELDS:
|
||||
offset += sizeof(DWORD) * 3; // 3 DWORD color masks
|
||||
break;
|
||||
default:
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!writeBitmapFileHeader(s, &bf))
|
||||
goto fail;
|
||||
@ -160,6 +240,10 @@ BYTE* winpr_bitmap_construct_header(size_t width, size_t height, size_t bpp)
|
||||
if (!writeBitmapInfoHeader(s, &bi))
|
||||
goto fail;
|
||||
|
||||
if (!Stream_EnsureRemainingCapacity(s, offset))
|
||||
goto fail;
|
||||
|
||||
Stream_Zero(s, offset);
|
||||
result = Stream_Buffer(s);
|
||||
fail:
|
||||
Stream_Free(s, result == 0);
|
||||
@ -218,7 +302,7 @@ int winpr_bitmap_write_ex(const char* filename, const BYTE* data, size_t stride,
|
||||
{
|
||||
FILE* fp = NULL;
|
||||
int ret = -1;
|
||||
const size_t bpp_stride = width * (bpp / 8);
|
||||
const size_t bpp_stride = ((((width * bpp) + 31) & ~31) >> 3);
|
||||
|
||||
if (stride == 0)
|
||||
stride = bpp_stride;
|
||||
@ -287,32 +371,44 @@ static int winpr_image_bitmap_read_buffer(wImage* image, const BYTE* buffer, siz
|
||||
int rc = -1;
|
||||
UINT32 index = 0;
|
||||
BOOL vFlip = 0;
|
||||
BYTE* pDstData = NULL;
|
||||
WINPR_BITMAP_FILE_HEADER bf;
|
||||
WINPR_BITMAP_INFO_HEADER bi;
|
||||
WINPR_BITMAP_FILE_HEADER bf = { 0 };
|
||||
WINPR_BITMAP_INFO_HEADER bi = { 0 };
|
||||
wStream sbuffer = { 0 };
|
||||
wStream* s = Stream_StaticConstInit(&sbuffer, buffer, size);
|
||||
|
||||
if (!s)
|
||||
return -1;
|
||||
|
||||
if (!readBitmapFileHeader(s, &bf) || !readBitmapInfoHeader(s, &bi))
|
||||
size_t bmpoffset = 0;
|
||||
if (!readBitmapFileHeader(s, &bf) || !readBitmapInfoHeader(s, &bi, &bmpoffset))
|
||||
goto fail;
|
||||
|
||||
if ((bf.bfType[0] != 'B') || (bf.bfType[1] != 'M'))
|
||||
{
|
||||
WLog_WARN(TAG, "Invalid bitmap header %c%c", bf.bfType[0], bf.bfType[1]);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
image->type = WINPR_IMAGE_BITMAP;
|
||||
|
||||
if (Stream_GetPosition(s) > bf.bfOffBits)
|
||||
goto fail;
|
||||
if (!Stream_SafeSeek(s, bf.bfOffBits - Stream_GetPosition(s)))
|
||||
const size_t pos = Stream_GetPosition(s);
|
||||
const size_t expect = bf.bfOffBits;
|
||||
|
||||
if (pos != expect)
|
||||
{
|
||||
WLog_WARN(TAG, "pos=%" PRIuz ", expected %" PRIuz ", offset=" PRIuz, pos, expect,
|
||||
bmpoffset);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (!Stream_CheckAndLogRequiredCapacity(TAG, s, bi.biSizeImage))
|
||||
goto fail;
|
||||
|
||||
if (bi.biWidth < 0)
|
||||
if (bi.biWidth <= 0)
|
||||
{
|
||||
WLog_WARN(TAG, "bi.biWidth=%" PRId32, bi.biWidth);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
image->width = (UINT32)bi.biWidth;
|
||||
|
||||
@ -327,9 +423,21 @@ static int winpr_image_bitmap_read_buffer(wImage* image, const BYTE* buffer, siz
|
||||
image->height = (UINT32)bi.biHeight;
|
||||
}
|
||||
|
||||
if (image->height <= 0)
|
||||
{
|
||||
WLog_WARN(TAG, "image->height=%" PRIu32, image->height);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
image->bitsPerPixel = bi.biBitCount;
|
||||
image->bytesPerPixel = (image->bitsPerPixel / 8);
|
||||
image->scanline = (bi.biSizeImage / image->height);
|
||||
image->scanline = ((((bi.biWidth * bi.biBitCount) + 31) & ~31) >> 3);
|
||||
const size_t bmpsize = 1ull * image->scanline * image->height;
|
||||
if (bmpsize != bi.biSizeImage)
|
||||
WLog_WARN(TAG, "bmpsize=%" PRIuz " != bi.biSizeImage=%" PRIu32, bmpsize, bi.biSizeImage);
|
||||
if (bi.biSizeImage < bmpsize)
|
||||
goto fail;
|
||||
|
||||
image->data = (BYTE*)malloc(bi.biSizeImage);
|
||||
|
||||
if (!image->data)
|
||||
@ -339,7 +447,7 @@ static int winpr_image_bitmap_read_buffer(wImage* image, const BYTE* buffer, siz
|
||||
Stream_Read(s, image->data, bi.biSizeImage);
|
||||
else
|
||||
{
|
||||
pDstData = &(image->data[(image->height - 1) * image->scanline]);
|
||||
BYTE* pDstData = &(image->data[(image->height - 1ull) * image->scanline]);
|
||||
|
||||
for (index = 0; index < image->height; index++)
|
||||
{
|
||||
@ -490,12 +598,12 @@ void* winpr_convert_to_jpeg(const void* data, size_t size, UINT32 width, UINT32
|
||||
unsigned long outsize = 0;
|
||||
struct jpeg_compress_struct cinfo = { 0 };
|
||||
|
||||
const size_t expect1 = stride * height;
|
||||
const size_t expect1 = 1ull * stride * height;
|
||||
const size_t bytes = (bpp + 7) / 8;
|
||||
const size_t expect2 = width * height * bytes;
|
||||
const size_t expect2 = 1ull * width * height * bytes;
|
||||
if (expect1 != expect2)
|
||||
return NULL;
|
||||
if (expect1 != size)
|
||||
if (expect1 > size)
|
||||
return NULL;
|
||||
|
||||
/* Set up the error handler. */
|
||||
|
@ -1,45 +0,0 @@
|
||||
/**
|
||||
* WinPR: Windows Portable Runtime
|
||||
* Image Utils
|
||||
*
|
||||
* Copyright 2024 Armin Novak <armin.novak@thincast.com>
|
||||
* Copyright 2024 Thincast Technologies GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef WINPR_UTILS_IMAGE_H
|
||||
#define WINPR_UTILS_IMAGE_H
|
||||
|
||||
#include <winpr/winpr.h>
|
||||
#include <winpr/wtypes.h>
|
||||
|
||||
WINPR_LOCAL void* winpr_convert_to_png(const void* data, size_t size, UINT32 width, UINT32 height,
|
||||
UINT32 stride, UINT32 bpp, UINT32* pSize);
|
||||
WINPR_LOCAL SSIZE_T winpr_convert_from_png(const char* comp_data, size_t comp_data_bytes,
|
||||
UINT32* width, UINT32* height, UINT32* bpp,
|
||||
char** ppdecomp_data);
|
||||
|
||||
WINPR_LOCAL void* winpr_convert_to_webp(const void* data, size_t size, UINT32 width, UINT32 height,
|
||||
UINT32 stride, UINT32 bpp, UINT32* pSize);
|
||||
WINPR_LOCAL SSIZE_T winpr_convert_from_webp(const char* comp_data, size_t comp_data_bytes,
|
||||
UINT32* width, UINT32* height, UINT32* bpp,
|
||||
char** ppdecomp_data);
|
||||
|
||||
WINPR_LOCAL void* winpr_convert_to_jpeg(const void* data, size_t size, UINT32 width, UINT32 height,
|
||||
UINT32 stride, UINT32 bpp, UINT32* pSize);
|
||||
WINPR_LOCAL SSIZE_T winpr_convert_from_jpeg(const char* comp_data, size_t comp_data_bytes,
|
||||
UINT32* width, UINT32* height, UINT32* bpp,
|
||||
char** ppdecomp_data);
|
||||
|
||||
#endif
|
@ -175,6 +175,43 @@ static BOOL test_read_write(void)
|
||||
return rc;
|
||||
}
|
||||
|
||||
static BOOL test_load_file(const char* name)
|
||||
{
|
||||
BOOL rc = FALSE;
|
||||
wImage* image = winpr_image_new();
|
||||
if (!image || !name)
|
||||
goto fail;
|
||||
|
||||
const int res = winpr_image_read(image, name);
|
||||
rc = (res > 0) ? TRUE : FALSE;
|
||||
|
||||
fail:
|
||||
winpr_image_free(image, TRUE);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static BOOL test_load(void)
|
||||
{
|
||||
const char* names[] = {
|
||||
"rgb.16a.bmp", "rgb.16a.nocolor.bmp", "rgb.16.bmp", "rgb.16.nocolor.bmp",
|
||||
"rgb.16x.bmp", "rgb.16x.nocolor.bmp", "rgb.24.bmp", "rgb.24.nocolor.bmp",
|
||||
"rgb.32.bmp", "rgb.32.nocolor.bmp", "rgb.32x.bmp", "rgb.32x.nocolor.bmp",
|
||||
"rgb.bmp"
|
||||
};
|
||||
|
||||
for (size_t x = 0; x < ARRAYSIZE(names); x++)
|
||||
{
|
||||
const char* name = names[x];
|
||||
char* fname = GetCombinedPath(TEST_SOURCE_PATH, name);
|
||||
const BOOL res = test_load_file(fname);
|
||||
free(fname);
|
||||
if (!res)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int TestImage(int argc, char* argv[])
|
||||
{
|
||||
int rc = 0;
|
||||
@ -183,10 +220,13 @@ int TestImage(int argc, char* argv[])
|
||||
WINPR_UNUSED(argv);
|
||||
|
||||
if (!test_equal())
|
||||
rc = -1;
|
||||
rc -= 1;
|
||||
|
||||
if (!test_read_write())
|
||||
rc = -1;
|
||||
rc -= 2;
|
||||
|
||||
if (!test_load())
|
||||
rc -= 4;
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
BIN
winpr/libwinpr/utils/test/rgb.16.bmp
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
winpr/libwinpr/utils/test/rgb.16.nocolor.bmp
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
winpr/libwinpr/utils/test/rgb.16a.bmp
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
winpr/libwinpr/utils/test/rgb.16a.nocolor.bmp
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
winpr/libwinpr/utils/test/rgb.16x.bmp
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
winpr/libwinpr/utils/test/rgb.16x.nocolor.bmp
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
winpr/libwinpr/utils/test/rgb.24.bmp
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
winpr/libwinpr/utils/test/rgb.24.nocolor.bmp
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
winpr/libwinpr/utils/test/rgb.32.nocolor.bmp
Normal file
After Width: | Height: | Size: 3.8 MiB |
BIN
winpr/libwinpr/utils/test/rgb.32x.bmp
Normal file
After Width: | Height: | Size: 3.8 MiB |
BIN
winpr/libwinpr/utils/test/rgb.32x.nocolor.bmp
Normal file
After Width: | Height: | Size: 3.8 MiB |