Search code examples
c++winapisvg.emf

EMF quality diminishes when window is shrinked, but is good when window dimensions are high


I am creating desktop application using C++ and pure WinApi. I need to display image that was given to me as SVG.

Since WinAPI supports only EMF files as vector format, I have used Inkscape to convert the file into EMF. My graphics design skills are at beginner level, but I have managed to convert SVG file into EMF successfully. However, the result is not looking as the original one, it is less "precise" so to say.

If I export the SVG as PNG and display it with GDI+, the result is the same as the original file. Unfortunately I need vector format.

To see exactly what I mean, download SVG, and EMF and PNG that I made here. Just click on Download:test.rar above 5 yellow stars ( see image below ).

enter image description here

Here are the instructions for creating minimal application that reproduces the problem:

1) Create default Win32 project in Visual Studio ( I use VS 2008, but this shouldn't be the problem );

2) Rewrite WM_PAINT like this:

case WM_PAINT:
    {
        hdc = BeginPaint(hWnd, &ps);
        // TODO: Add any drawing code here...

        RECT rcClient;
        GetClientRect( hWnd, &rcClient );

        FillRect( hdc, &rcClient, (HBRUSH)GetStockObject( LTGRAY_BRUSH) );

        // put metafile in the same place your app is
        HENHMETAFILE hemf = GetEnhMetaFile( L".\\test.emf" );
        ENHMETAHEADER emh; 
        GetEnhMetaFileHeader( hemf, sizeof(emh), &emh ); 

        // rescale metafile, and keep proportion
        UINT o_height = emh.rclFrame.bottom - emh.rclFrame.top, 
            o_width =  emh.rclFrame.right - emh.rclFrame.left;

        float scale = 0.5;

        scale = (float)( rcClient.right - rcClient.left ) / o_width;

        if( (float)( rcClient.bottom - rcClient.top ) / o_height  <  scale )
            scale = (float)( rcClient.bottom - rcClient.top ) / o_height;

        int marginX = ( rcClient.right - rcClient.left ) - (int)( o_width * scale );
        int marginY = ( rcClient.bottom - rcClient.top ) - (int)( o_height * scale );

        marginX /= 2;
        marginY /= 2;

        rcClient.left = rcClient.left + marginX;
        rcClient.right = rcClient.right - marginX;
        rcClient.top = rcClient.top + marginY;
        rcClient.bottom = rcClient.bottom - marginY;

        // Draw the picture.  
        PlayEnhMetaFile( hdc, hemf, &rcClient ); 

        // Release the metafile handle.  
        DeleteEnhMetaFile(hemf); 

        EndPaint(hWnd, &ps);
    }
    break;

3) Add following handlers for WM_SIZE and WM_ERASEBKGND just below WM_PAINT :

case WM_SIZE:
    InvalidateRect( hWnd, NULL, FALSE );
    return 0L;
case WM_ERASEBKGND:
    return 1L;

4) Resize the window to the smallest possible size, and then maximize it.

Notice that the bigger the window gets, the better the image quality is, but the smaller it gets the "less precise" the image gets. I tested this on Windows XP.

I am asking your help to get the same graphic quality of the EMF file as the original SVG.

Thank you for your time and efforts. Best regards.


Solution

  • Solved it!

    The solution makes much, if not all of the solution I've submitted redundant. I've therefore decided to replace it with this one.

    There's a number of things to take into account and a number of concepts that are employed to get the desired result. These include (in no particular order)

    • The need to set a maskColour that closely matches the background, while also not being present in the final computed image. Pixels that straddle the border between transparent/opaque areas are blended values of the mask and the EMF's colour at that point.
    • The need to choose a scaling rate that's appropriate for the source image - in the case of this image and the code I've used, I chose 8. This means that we're drawing this particular EMF at about a megapixel, even though the destination is likely to be in the vicinity of about 85k pixels.
    • The need to manually set the alpha channel of the generated 32bit HBITMAP, since the GDI stretching/drawing functions disregard this channel, yet the AlphaBlend function requires them to be accurate.

    I also note that I've used old code to draw the background manually each time the screen is refreshed. A much better approach would be to create a patternBrush once which is then simply copied using the FillRect function. This is much faster than filling the rect with a solid colour and then drawing the lines over the top. I can't be bothered to re-write that part of the code, though I'll include a snippet for reference that I've used in other projects in the past.

    Here's a couple of shots of the result I get from the code below:

    enter image description here enter image description here enter image description here

    Here's the code I used to achieve it:

    #define WINVER 0x0500       // for alphablend stuff
    
    #include <windows.h>
    #include <commctrl.h>
    #include <stdio.h>
    #include <stdint.h>
    #include "resource.h"
    
    HINSTANCE hInst;
    
    HBITMAP mCreateDibSection(HDC hdc, int width, int height, int bitCount)
    {
        BITMAPINFO bi;
        ZeroMemory(&bi, sizeof(bi));
        bi.bmiHeader.biSize = sizeof(bi.bmiHeader);
        bi.bmiHeader.biWidth = width;
        bi.bmiHeader.biHeight = height;
        bi.bmiHeader.biPlanes = 1;
        bi.bmiHeader.biBitCount = bitCount;
        bi.bmiHeader.biCompression = BI_RGB;
        return CreateDIBSection(hdc, &bi, DIB_RGB_COLORS, 0,0,0);
    }
    
    void makePixelsTransparent(HBITMAP bmp, byte r, byte g, byte b)
    {
        BITMAP bm;
        GetObject(bmp, sizeof(bm), &bm);
        int x, y;
    
        for (y=0; y<bm.bmHeight; y++)
        {
            uint8_t *curRow = (uint8_t *)bm.bmBits;
            curRow += y * bm.bmWidthBytes;
            for (x=0; x<bm.bmWidth; x++)
            {
                if ((curRow[x*4 + 0] == b) && (curRow[x*4 + 1] == g) && (curRow[x*4 + 2] == r))
                {
                    curRow[x*4 + 0] = 0;      // blue
                    curRow[x*4 + 1] = 0;      // green
                    curRow[x*4 + 2] = 0;      // red
                    curRow[x*4 + 3] = 0;      // alpha
                }
                else
                    curRow[x*4 + 3] = 255;    // alpha
            }
        }
    }
    
    // Note: maskCol should be as close to the colour of the background as is practical
    //       this is because the pixels that border transparent/opaque areas will have
    //       their colours derived from a blending of the image colour and the maskColour
    //
    //       I.e - if drawing to a white background (255,255,255), you should NOT use a mask of magenta (255,0,255)
    //              this would result in a magenta-ish border
    HBITMAP HbitmapFromEmf(HENHMETAFILE hEmf, int width, int height, COLORREF maskCol)
    {
            ENHMETAHEADER emh;
            GetEnhMetaFileHeader(hEmf, sizeof(emh), &emh);
            int emfWidth, emfHeight;
            emfWidth = emh.rclFrame.right - emh.rclFrame.left;
            emfHeight = emh.rclFrame.bottom - emh.rclFrame.top;
    
            // these are arbitrary and selected to give a good mix of speed and accuracy
            // it may be worth considering passing this value in as a parameter to allow
            // fine-tuning
            emfWidth /= 8;
            emfHeight /= 8;
    
            // draw at 'native' size
            HBITMAP emfSrcBmp = mCreateDibSection(NULL, emfWidth, emfHeight, 32);
    
            HDC srcDC = CreateCompatibleDC(NULL);
            HBITMAP oldSrcBmp = (HBITMAP)SelectObject(srcDC, emfSrcBmp);
    
            RECT tmpEmfRect, emfRect;
            SetRect(&tmpEmfRect, 0,0,emfWidth,emfHeight);
    
            // fill background with mask colour
            HBRUSH bkgBrush = CreateSolidBrush(maskCol);
            FillRect(srcDC, &tmpEmfRect, bkgBrush);
            DeleteObject(bkgBrush);
    
            // draw emf
            PlayEnhMetaFile(srcDC, hEmf, &tmpEmfRect);
    
            HDC dstDC = CreateCompatibleDC(NULL);
            HBITMAP oldDstBmp;
            HBITMAP result;
            result = mCreateDibSection(NULL, width, height, 32);
            oldDstBmp = (HBITMAP)SelectObject(dstDC, result);
    
            SetStretchBltMode(dstDC, HALFTONE);
            StretchBlt(dstDC, 0,0,width,height, srcDC, 0,0, emfWidth,emfHeight, SRCCOPY);
    
            SelectObject(srcDC, oldSrcBmp);
            DeleteDC(srcDC);
            DeleteObject(emfSrcBmp);
    
            SelectObject(dstDC, oldDstBmp);
            DeleteDC(dstDC);
    
            makePixelsTransparent(result, GetRValue(maskCol),GetGValue(maskCol),GetBValue(maskCol));
    
            return result;
    }
    
    int rectWidth(RECT &r)
    {
        return r.right - r.left;
    }
    
    int rectHeight(RECT &r)
    {
        return r.bottom - r.top;
    }
    
    void onPaintEmf(HWND hwnd, HENHMETAFILE srcEmf)
    {
        PAINTSTRUCT ps;
        RECT mRect, drawRect;
        HDC hdc;
        double scaleWidth, scaleHeight, scale;
        int spareWidth, spareHeight;
        int emfWidth, emfHeight;
        ENHMETAHEADER emh;
    
        GetClientRect( hwnd, &mRect );
    
        hdc = BeginPaint(hwnd, &ps);
    
            // calculate the draw-size - retain aspect-ratio.
            GetEnhMetaFileHeader(srcEmf, sizeof(emh), &emh );
    
            emfWidth =  emh.rclFrame.right - emh.rclFrame.left;
            emfHeight = emh.rclFrame.bottom - emh.rclFrame.top;
    
            scaleWidth = (double)rectWidth(mRect) / emfWidth;
            scaleHeight = (double)rectHeight(mRect) / emfHeight;
            scale = min(scaleWidth, scaleHeight);
    
            int drawWidth, drawHeight;
            drawWidth = emfWidth * scale;
            drawHeight = emfHeight * scale;
    
            spareWidth = rectWidth(mRect) - drawWidth;
            spareHeight = rectHeight(mRect) - drawHeight;
    
            drawRect = mRect;
            InflateRect(&drawRect, -spareWidth/2, -spareHeight/2);
    
            // create a HBITMAP from the emf and draw it
            // **** note that the maskCol matches the background drawn by the below function ****
            HBITMAP srcImg = HbitmapFromEmf(srcEmf, drawWidth, drawHeight, RGB(230,230,230) );
    
            HDC memDC;
            HBITMAP old;
            memDC = CreateCompatibleDC(hdc);
            old = (HBITMAP)SelectObject(memDC, srcImg);
    
            byte alpha = 255;
            BLENDFUNCTION bf = {AC_SRC_OVER,0,alpha,AC_SRC_ALPHA};
            AlphaBlend(hdc, drawRect.left,drawRect.top, drawWidth,drawHeight,
                       memDC, 0,0,drawWidth,drawHeight, bf);
    
            SelectObject(memDC, old);
            DeleteDC(memDC);
            DeleteObject(srcImg);
    
        EndPaint(hwnd, &ps);
    }
    
    void drawHeader(HDC dst, RECT headerRect)
    {
        HBRUSH b1;
        int i,j;//,headerHeight = (headerRect.bottom - headerRect.top)+1;
    
            b1 = CreateSolidBrush(RGB(230,230,230));
            FillRect(dst, &headerRect,b1);
            DeleteObject(b1);
            HPEN oldPen, curPen;
            curPen = CreatePen(PS_SOLID, 1, RGB(216,216,216));
            oldPen = (HPEN)SelectObject(dst, curPen);
            for (j=headerRect.top;j<headerRect.bottom;j+=10)
            {
                MoveToEx(dst, headerRect.left, j, NULL);
                LineTo(dst, headerRect.right, j);
            }
    
            for (i=headerRect.left;i<headerRect.right;i+=10)
            {
                MoveToEx(dst, i, headerRect.top, NULL);
                LineTo(dst, i, headerRect.bottom);
            }
            SelectObject(dst, oldPen);
            DeleteObject(curPen);
            MoveToEx(dst, headerRect.left,headerRect.bottom,NULL);
            LineTo(dst, headerRect.right,headerRect.bottom);
    }
    
    BOOL CALLBACK DlgMain(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
        static HENHMETAFILE hemf;
    
        switch(uMsg)
        {
            case WM_INITDIALOG:
            {
                hemf = GetEnhMetaFile( "test.emf" );
            }
            return TRUE;
    
            case WM_PAINT:
                onPaintEmf(hwndDlg, hemf);
            return 0;
    
            case WM_ERASEBKGND:
            {
                RECT mRect;
                GetClientRect(hwndDlg, &mRect);
                drawHeader( (HDC)wParam, mRect);
            }
            return true;
    
            case WM_SIZE:
                InvalidateRect( hwndDlg, NULL, true );
                return 0L;
    
            case WM_CLOSE:
            {
                EndDialog(hwndDlg, 0);
            }
            return TRUE;
    
            case WM_COMMAND:
            {
                switch(LOWORD(wParam))
                {
                }
            }
            return TRUE;
        }
        return FALSE;
    }
    
    int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
    {
        hInst=hInstance;
        InitCommonControls();
        return DialogBox(hInst, MAKEINTRESOURCE(DLG_MAIN), NULL, (DLGPROC)DlgMain);
    }
    

    Finally, here's an example of creating a patternBrush for filling the background using the FillRect function. This approach is suitable for any tileable background.

    HBRUSH makeCheckerBrush(int squareSize, COLORREF col1, COLORREF col2)
    {
        HDC memDC, tmpDC = GetDC(NULL);
        HBRUSH result, br1, br2;
        HBITMAP old, bmp;
        RECT rc, r1, r2;
    
        br1 = CreateSolidBrush(col1);
        br2 = CreateSolidBrush(col2);
    
        memDC = CreateCompatibleDC(tmpDC);
        bmp = CreateCompatibleBitmap(tmpDC, 2*squareSize, 2*squareSize);
        old = (HBITMAP)SelectObject(memDC, bmp);
    
        SetRect(&rc, 0,0, squareSize*2, squareSize*2);
        FillRect(memDC, &rc, br1);
    
        // top right
        SetRect(&r1, squareSize, 0, 2*squareSize, squareSize);
        FillRect(memDC, &r1, br2);
    
        // bot left
        SetRect(&r2, 0, squareSize, squareSize, 2*squareSize);
        FillRect(memDC, &r2, br2);
    
        SelectObject(memDC, old);
        DeleteObject(br1);
        DeleteObject(br2);
        ReleaseDC(0, tmpDC);
        DeleteDC(memDC);
    
        result = CreatePatternBrush(bmp);
        DeleteObject(bmp);
        return result;
    }
    

    Example of result, created with:

    HBRUSH bkBrush = makeCheckerBrush(8, RGB(153,153,153), RGB(102,102,102)); enter image description here