Saving GDI+ Metafile as BMP cropped when screen scaling not 100% - c++

When saving a bitmap file from a GDI+ metafile object, the output is affected by the current display scale of the monitor.
For example, the following code sample generates these images:
with screen at 100% scaling (81x81 pixel bitmap generated).
with screen at 125% scaling (52x52 pixel bitmap generated).
#include <Windows.h>
// GDI+ libraries
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
void main()
{
// Startup GDI+
ULONG_PTR gdiplusToken;
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr);
{
// Create a metafile
const auto dc = GetDC(nullptr);
const auto cdc = CreateCompatibleDC(dc);
Gdiplus::Metafile metafile{ L"test.emf", cdc };
auto graphics = Gdiplus::Graphics::FromImage(&metafile);
// Draw some things
Gdiplus::SolidBrush yellowBrush{ Gdiplus::Color::Yellow };
graphics->FillRectangle(&yellowBrush, 0, 0, 70, 70);
Gdiplus::SolidBrush redBrush(Gdiplus::Color::Red);
graphics->FillRectangle(&redBrush, 10, 10, 50, 50);
delete graphics; // Must be deleted before saving (is this the correct way?)
// Save as BMP (MIME type: image/bmp)
CLSID bmpEncoderId{1434252288, 6660, 4563, {154, 115, 0, 0, 248, 30, 243, 46}};
auto status = metafile.Save(L"test.bmp", &bmpEncoderId);
// Cleanup
DeleteDC(cdc);
ReleaseDC(nullptr, dc);
}
// Shutdown GDI+
Gdiplus::GdiplusShutdown(gdiplusToken);
}
I've tried adding the following in several places (as suggested in other SO answers) but this doesn't seem to change the behviour.
// Set scaling
Gdiplus::MetafileHeader mfh{};
metafile.GetMetafileHeader(&mfh);
graphics->ScaleTransform(mfh.GetDpiX() / graphics->GetDpiX(),
mfh.GetDpiY() / graphics->GetDpiY());
Is it possible to export the full bitmap image independent of what scaling is set on the monitor?
SOLUTION: This was caused by not compiling as DPI-aware.

Related

bmp.Save returns FileNotFound

I wrote a program to save a BMP of a Greek word.
Here it is:
#include <Windows.h>
#include <gdiplus.h>
#include <iostream>
#pragma comment(lib,"gdiplus.lib")
using namespace Gdiplus;
using namespace std;
int main()
{
// Initialize GDI+.
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
{
// Create a bitmap.
Bitmap bmp(500, 500, PixelFormat24bppRGB);
Graphics graphics(&bmp);
// Set the font.
FontFamily fontFamily(L"Arial");
Font font(&fontFamily, 36, FontStyleRegular, UnitPixel);
graphics.SetTextRenderingHint(TextRenderingHintAntiAlias);
graphics.SetSmoothingMode(SmoothingModeHighQuality);
// Draw the text.
SolidBrush brush(Color(255, 0, 0, 0));
WCHAR text[] = L"ΔΙΔΑΣΚΑΛΙΑ";
PointF origin(100.0f, 100.0f);
graphics.DrawString(text, -1, &font, origin, &brush);
// Save the bitmap.
Status test = bmp.Save(L"greek_word.bmp", &ImageFormatBMP);
printf("%d", test);
}
// Clean up GDI+.
GdiplusShutdown(gdiplusToken);
return 0;
}
For some reason, it creates the BMP, but it has a file size of 0.
And, for some reason, bmp.Save returns a FileNotFound error.
Anyone know why I am getting this strange behavior?
Thanks.

How do I save GDI+ Graphics / HDC to a file?

I am unable to save a GDI+ Graphics object, which is derived from a Device Context HDC, to a file.
What works: I am able to save a GDI+ Graphics derived from a Bitmap. Sample code (Win32):
Color color(255, 0, 0);
Pen pen(color, 2.0f);
CLSID pngClsid;
if(GetEncoderClsid(L"image/bmp", &pngClsid) < 0) // calls GetImageEncoders()
return;
// Graphics from Bitmap - works OK
Bitmap bitmap(300, 300, PixelFormat24bppRGB); // create Bitmap first
Graphics *graphics = new Graphics(&bitmap); // create Graphics second
graphics->Clear(Color(255, 255, 255, 255));
Status stat = graphics->DrawEllipse(&pen, 50, 50, 100, 100);
assert(stat == Ok);
stat = bitmap.Save(L"C:\\temp\\test1.bmp", &pngClsid, NULL);
assert(stat == Ok);
delete graphics;
Result:
What fails: If the Graphics object is derived from an HDC, I get a black rectangle. Regardless of whether I create the Bitmap at point [1], [2] or [3], I always get a black rectangle. Code:
CLSID pngClsid;
if(GetEncoderClsid(L"image/bmp", &pngClsid) < 0) // calls GetImageEncoders()
return;
// Graphics from HDC - fails
HDC hdc = GetDC(NULL);
Graphics *graphicsDC = new Graphics(hdc); // create Graphics first
graphicsDC->SetPageUnit(UnitPixel);
//Bitmap bitmapDC(300, 300, graphicsDC); // [1] create Bitmap second. Black rectangle if called here
graphicsDC->Clear(Color(255, 255, 255, 255));
//Bitmap bitmapDC(300, 300, graphicsDC); // [2] black rectangle if called here
HPEN penGDI = CreatePen(PS_SOLID, 3, RGB(0, 255, 0)); // old school GDI
HPEN oldPen = (HPEN)SelectObject(hdc, penGDI);
Ellipse(hdc, 50, 50, 150, 150);
DeleteObject(SelectObject(hdc, oldPen));
Bitmap bitmapDC(300, 300, graphicsDC); // [3] black rectangle if called here
Status stat = bitmapDC.Save(L"C:\\temp\\test2.bmp", &pngClsid, NULL);
assert(stat == Ok);
delete graphicsDC;
ReleaseDC(NULL, hdc);
Result:
Why I need this: I am converting code that contains thousands of calls to the GDI API. I want to start using GDI+ gradually without converting all GDI calls to GDI+ at once. I have mixed GDI/GDI+ successfully in other cases, for instance when creating gradients. The only difference is that in the other cases, I wasn't trying to save to a file.
I can't use CImage because it doesn't have groovy anti-aliasing.
So, how can I save a Graphic as image when starting with an HDC?

Creating, displaying, and then accessing bitmap/DIB data (w/o GetBitmapBits())

I have inherited an old-school MFC Windows CE program, and am having to make some modifications to it. As part of this I have to create a monochrome image with text on it, and both display it on a screen as well as send each row of the image to a printer one at a time.
I originally used a bitmap, and had success using DrawText() and getting a test string (Hello World) to display on the screen (this code is in Figure 1). However, I hit a wall at the stage where I am looking to extract the wrap data from the bitmap. What I am trying to get is an array with 1s or 0s representing black or white. I had first thought I would use GetBitmapBits() but unfortunately the code I am working with is so old that function is not supported yet. I thought I could get around this issue by using GetBitmap() and then accessing the bmBits parameter. However this appears to always be null which was confirmed when I found the following link: Why does GetObject return an BITMAP with null bmBits?.
My next attempt was to follow the advice in the link and use CreateDIBSection() instead of CreateCompatibleBitmap(). This seems like the right path, and I should have access to the data I want, but unfortunately I cannot get the DIB to display (code is in Figure 2). I suspect I am doing something wrong in creating the header of the DIB, but I cannot figure out what my mistake is.
If anyone has suggestions for a way to access the data in the bitmap, or can see what I am doing wrong with the DIB, I would greatly appreciate the help!
*** FIGURE 1: Code to create and display a bitmap
void CRunPage::OnPaint()
{
CPaintDC dc(this); // property page device context for painting
CBitmap mBmp; // CBitmap object for displaying built-in bitmaps
CDC mDCMem; // CDC object to handle built-in bitmap
int iWidth, iHeight; // dimension to draw on the screen
int icurLabel, // current label index of open print file
iLabelNum; // number of labels in open print file
LPBITMAPINFOHEADER pBMIH; // bitmap header object for current label
LPBYTE pImage; // bitmap data for current label
CSize size; // size of label
int PreviewLeft,PreviewTop,PreviewWidth,PreviewHeight;
CRect Rect;
BITMAP bm;
LPVOID bmBits=NULL;
// Calculate the preview area
PreviewLeft=5;
PreviewTop=5;
GetDlgItem(IDC_RUN_NEXT)->GetWindowRect(&Rect);
ScreenToClient(&Rect);
PreviewWidth=Rect.left-PreviewLeft*2;
GetDlgItem(IDC_RUN_WRAPTEXT)->GetWindowRect(&Rect);
ScreenToClient(&Rect);
PreviewHeight=Rect.top-PreviewTop*2;
CRect textRect;
CString testText(_T("Hello World"));
CBitmap * pOldBitmap;
CBrush whiteBrush, *pOldBrush;
CPen blackPen, *pOldPen;
mDCMem.CreateCompatibleDC(&dc);
mBmp.CreateCompatibleBitmap(&dc, PreviewWidth+PreviewLeft*2, PreviewHeight+PreviewTop*2);
//mBmp.CreateCompatibleBitmap(&dc, PreviewWidth, PreviewHeight);
pOldBitmap = mDCMem.SelectObject(&mBmp);
blackPen.CreatePen(PS_SOLID, 2, RGB(0, 0, 0));
whiteBrush.CreateSolidBrush(RGB(255,255,255));
textRect.SetRect(0,0,PreviewWidth, PreviewHeight);
// this means behind the text will be a white box w/ a black boarder
pOldBrush = mDCMem.SelectObject(&whiteBrush);
pOldPen = mDCMem.SelectObject(&blackPen);
//these commands draw on the memory-only context (mDCMem)
mDCMem.Rectangle(&textRect);
mDCMem.DrawText((LPCTSTR)testText, 11, &textRect, DT_CENTER|DT_VCENTER);
mDCMem.SelectObject(pOldBrush);
mDCMem.SelectObject(pOldPen);
dc.StretchBlt(PreviewLeft,PreviewTop, PreviewWidth, PreviewHeight, & mDCMem, 0, 0, PreviewWidth, PreviewHeight, SRCCOPY);
mDCMem.SelectObject(pOldBitmap);
}
*** FIGURE 2: Trying to use a DIB instead of a bitmap
void CRunPage::OnPaint()
{
CPaintDC dc(this); // property page device context for painting
CBitmap mBmp; // CBitmap object for displaying built-in bitmaps
CDC mDCMem; // CDC object to handle built-in bitmap
int iWidth, iHeight; // dimension to draw on the screen
int icurLabel, // current label index of open print file
iLabelNum; // number of labels in open print file
LPBITMAPINFOHEADER pBMIH; // bitmap header object for current label
LPBYTE pImage; // bitmap data for current label
CSize size; // size of label
int PreviewLeft,PreviewTop,PreviewWidth,PreviewHeight;
CRect Rect;
BITMAP bm;
// Calculate the preview area
PreviewLeft=5;
PreviewTop=5;
GetDlgItem(IDC_RUN_NEXT)->GetWindowRect(&Rect);
ScreenToClient(&Rect);
PreviewWidth=Rect.left-PreviewLeft*2;
GetDlgItem(IDC_RUN_WRAPTEXT)->GetWindowRect(&Rect);
ScreenToClient(&Rect);
PreviewHeight=Rect.top-PreviewTop*2;
CRect textRect;
CString testText(_T("Hello World"));
CBitmap * pOldBitmap;
CBrush whiteBrush, *pOldBrush;
CPen blackPen, *pOldPen;
LPBYTE pFWandImageMem=NULL, pImageMem=NULL, pTemp=NULL;
int i=0,j=0, buffSize=0, numBytesPerRow=0, bitmapWidthPix,bitmapHeightPix;
char *numBytesPerRowString;
char temp;
void ** ppvBits;
BITMAPINFOHEADER bmif;
BITMAPINFO bmi;
HBITMAP myDIB, myOldDIB;
mDCMem.CreateCompatibleDC(&dc);
//this rect is the area in which I can draw (its x,y location is set by BitBlt or StretchBlt
//mBmp.CreateCompatibleBitmap(&dc, PreviewWidth+PreviewLeft*2, PreviewHeight+PreviewTop*2);
bmif.biSize = sizeof(BITMAPINFOHEADER);
bmif.biWidth = PreviewWidth+PreviewLeft*2;
bmif.biHeight = -(PreviewHeight+PreviewTop*2);//- means top down (I think? I tried both ways and neither worked)
bmif.biPlanes = 1;
bmif.biBitCount = 1;
bmif.biCompression = BI_RGB; // no compression
bmif.biSizeImage = 0; // Size (bytes) if image - this can be set to 0 for uncompressed images
bmif.biXPelsPerMeter = 0;
bmif.biYPelsPerMeter = 0;
bmif.biClrUsed =0;
bmif.biClrImportant = 0;
bmi.bmiColors[0].rgbBlue=0;
bmi.bmiColors[0].rgbGreen=0;
bmi.bmiColors[0].rgbRed=0;
bmi.bmiColors[0].rgbReserved=0;
bmi.bmiColors[1].rgbBlue=255;
bmi.bmiColors[1].rgbGreen=255;
bmi.bmiColors[1].rgbRed=255;
bmi.bmiColors[1].rgbReserved=0;
bmi.bmiHeader=bmif;
myDIB = CreateDIBSection(dc.GetSafeHdc(), &bmi, DIB_RGB_COLORS, ppvBits, NULL, 0);
myOldDIB = (HBITMAP)mDCMem.SelectObject(myDIB);//SelectObject(mDCMem, myDIB);
blackPen.CreatePen(PS_SOLID, 2, RGB(0, 0, 0));
whiteBrush.CreateSolidBrush(RGB(255,255,255));
textRect.SetRect(0,0,PreviewWidth, PreviewHeight);
// this means behind the text will be a white box w/ a black boarder
pOldBrush = mDCMem.SelectObject(&whiteBrush);
pOldPen = mDCMem.SelectObject(&blackPen);
//these commands draw on the memory-only context (mDCMem)
mDCMem.Rectangle(&textRect);
mDCMem.DrawText((LPCTSTR)testText, 11, &textRect, DT_CENTER|DT_VCENTER);
mDCMem.SelectObject(pOldBrush);
mDCMem.SelectObject(pOldPen);
dc.StretchBlt(PreviewLeft,PreviewTop, PreviewWidth, PreviewHeight, & mDCMem, 0, 0, PreviewWidth, PreviewHeight, SRCCOPY);
mDCMem.SelectObject(myOldDIB);
}
So I made two minor changes to the DIB code, and it is displaying the image correctly now.
First, I changed the way I passed in my pointer to the CreateDIBSection():
void ** ppvBits;
to
LPBYTE pBits;
And then I had to change how I passed that into CreateDIBSection. I also explicitly casted the return of CreateDIBSection() to an HBITMAP:
myDIB = CreateDIBSection(dc.GetSafeHdc(), &bmi, DIB_RGB_COLORS, (void**)&pBits, NULL, 0);
to
myDIB = (HBITMAP) CreateDIBSection(dc.GetSafeHdc(), &bmi, DIB_RGB_COLORS, ppvBits, NULL, 0);
I have not had a chance to see if I can access the image data, but I am past the initial issues now. Thanks to anyone who looked at this, and if people know how to do the first (device dependent bitmap) method I would be interested to know.

Displaying multiple bitmaps in MFC

I'm trying to display two bitmaps of the same image in the view at different location as shown below, but it's showing only the first one. If I comment out the first one then the other one is displayed.
void CChildView::OnPaint()
{
// Load the bitmap
CBitmap BmpLady;
// Load the bitmap from the resource
BmpLady.LoadBitmap(IDB_MB);
CPaintDC dc(this); // device context for painting
// Create a memory device compatible with the above CPaintDC variable
CDC MemDCLady;
MemDCLady.CreateCompatibleDC(&dc);
// Select the new bitmap
CBitmap *BmpPrevious = MemDCLady.SelectObject(&BmpLady);
// Copy the bits from the memory DC into the current dc
dc.BitBlt(20, 10, 436, 364, &MemDCLady, 0, 0, SRCCOPY);
// Restore the old bitmap
dc.SelectObject(BmpPrevious);
// Draw another bitmap for same image.
CPaintDC dc1(this);
CDC MemDCLady1;
MemDCLady1.CreateCompatibleDC(&dc1);
CBitmap *BmpPrevious1 = MemDCLady1.SelectObject(&BmpLady);
dc1.BitBlt(200, 100, 436, 364, &MemDCLady1, 0, 0, SRCCOPY);
dc1.SelectObject(BmpPrevious1);
}
How to display both the images simultaneously? Please help. Thanks in advance.
P.S: I'm fairly new to MFC.
There is no need to CreateCompatibleDC again for the second bitmap. With the following changes I'm able to display both the bitmaps simultaneously
void CChildView::OnPaint()
{
CBitmap BmpLady;
// Load the bitmap from the resource
BmpLady.LoadBitmap(IDB_MB);
CPaintDC dc(this);
CDC MemDCLady;
// Create a memory device compatible with the above CPaintDC variable
MemDCLady.CreateCompatibleDC(&dc);
// Select the new bitmap
//CBitmap *BmpPrevious = MemDCLady.SelectObject(&BmpLady);
MemDCLady.SelectObject(&BmpLady);
// Copy the bits from the memory DC into the current dc
dc.BitBlt(20, 10, 436, 364, &MemDCLady, 0, 0, SRCCOPY);
// MemDCLady.SelectObject(&BmpLady);
// Copy the bits from the memory DC into the current dc
dc.BitBlt(200, 100, 436, 364, &MemDCLady, 0, 0, SRCCOPY);
}

Windows 7 and ScreenShot.cpp GDI+ PNG problemo

was using XP without issue for a long time. switched to 7 and trying to capture screenshots with my previously functioning code no longer works. simple concept and relatively generic code...just find the window that i call and save it as a .png. any ideas what might make this bad boy run again? can't debug with my current setup, but it makes it all the way and spits out the error message after bmp->save(...) ...couldn't save image file. edit: also a file does get created/saved, but it is blank and not written to. perhaps the bitmap encoding or GDI is screwed up?
bool CScreenShot::Snap(CString wintitle, CString file, CString& ermsg)
{
ermsg = ""; // no error message
// create screen shot bitmap
EnumWinProcStruct prm = {0, (LPSTR)(LPCTSTR)wintitle, 0};
// Find the descriptor of the window with the caption wintitle
EnumDesktopWindows(0, EnumWindowsProc, (LPARAM)&prm);
if(!prm.hwnd)
{
ermsg.Format("couldn't find window \"%s\"", wintitle);
return false;
}
// Make the window the topmost window
SetWindowPos(prm.hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
Sleep(300);
// Get device context for the top-level window and client rect
HDC hDC = GetDC(prm.hwnd);
RECT rc;
GetClientRect(prm.hwnd, &rc);
HDC memDC = CreateCompatibleDC(hDC);
// Set the size and color depth for the screen shot image
BITMAPINFO bmpInfo;
memset(&bmpInfo, 0, sizeof(bmpInfo));
bmpInfo.bmiHeader.biSize = sizeof(bmpInfo.bmiHeader);
bmpInfo.bmiHeader.biWidth = rc.right - rc.left;
bmpInfo.bmiHeader.biHeight = rc.bottom - rc.top;
bmpInfo.bmiHeader.biPlanes = 1;
bmpInfo.bmiHeader.biBitCount = 24;
bmpInfo.bmiHeader.biCompression = BI_RGB;
bmpInfo.bmiHeader.biSizeImage = bmpInfo.bmiHeader.biWidth * bmpInfo.bmiHeader.biHeight * 3;
// Create memory buffer and perform a bit-block transfer of the color data from the window to the memory
LPVOID addr;
HBITMAP memBM = CreateDIBSection(memDC, &bmpInfo, DIB_RGB_COLORS, &addr, 0, 0);
HGDIOBJ stdBM = SelectObject(memDC, memBM);
BOOL OK = BitBlt(memDC, 0, 0, bmpInfo.bmiHeader.biWidth, bmpInfo.bmiHeader.biHeight, hDC, 0, 0, SRCCOPY);
ReleaseDC(prm.hwnd, hDC);
SetWindowPos(prm.hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
// Initialize GDI+.
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
if(GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL) != Ok)
{
ermsg.Format("couldn't start GDI+");
return false;
}
// Create a Bitmap object for work with images defined by pixel data from the GDI HBitmap and the GDI HPalette.
Bitmap* bmp = ::new Bitmap(memBM, DIB_RGB_COLORS);
SelectObject(memDC, stdBM);
DeleteObject(memBM);
DeleteDC(memDC);
// Find the encoder for "image/png" mime type
CLSID encoderClsid;
EncoderParameters encoderParameters;
GetEncoderClsid(L"image/png", &encoderClsid);
encoderParameters.Count = 0;
// Convert file name to Unicode (wide-char) string.
WCHAR fn[_MAX_PATH];
MultiByteToWideChar(CP_THREAD_ACP, MB_PRECOMPOSED, file, file.GetLength() + 1, fn, _MAX_PATH);
// Save the screen shot into the specified file using image encoder with the mime style "image/png"
if(bmp->Save(fn, &encoderClsid, &encoderParameters) != Ok)
{
ermsg.Format("couldn't save image file \"%s\"", file);
return false;
}
::delete bmp;
GdiplusShutdown(gdiplusToken);
return true;
}
The error message implies that you're trying to save the file to a folder that you don't have permission to write to. Many folders such as Program Files are now protected. Since you didn't include the path in your sample code I'm unable to determine if this is the actual problem.
Edit: Another possibility is that the Bitmap is improperly constructed which causes the Save to fail. The second parameter to the constructor is supposed to be a handle to a palette, I think DIB_RGB_COLORS would be invalid here and you should use NULL. Also there are a couple of caveats noted in the Microsoft documentation and perhaps the different OS versions react differently when you break the rules:
You are responsible for deleting the GDI bitmap and the GDI palette. However, you should not delete the GDI bitmap or the GDI palette until after the GDI+ Bitmap::Bitmap object is deleted or goes out of scope.
Do not pass to the GDI+ Bitmap::Bitmap constructor a GDI bitmap or a GDI palette that is currently (or was previously) selected into a device context.
win7 won't accept encoderParameters.Count == 0 for some reason. Set that == 1 and you should be all set.
you probably could also just remove that parameter from Save() (overloaded)