Home > Net >  How to convert CImage to Base64 string in MFC C
How to convert CImage to Base64 string in MFC C

Time:03-30

I'm porting some code from C# to C with MFC and one thing have stopped me. The original code generated an image and then encoded it as a base64 string to use for en embedded image when generating an HTML file.

The original code first converts it to a byte array

private byte[] AsBytes(System.Drawing.Image image)
{
      using (var ms = new System.IO.MemoryStream())
      {
           image.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
           return ms.ToArray();
      }
}

The conversion to Base64 is then a simple call, Convert.ToBase64String(pictureAsBytes) for MFC there is Base64Encode while not as nice it appears to do the job. The problem is going from CImage to CByteArray (or something else useful).

The code I have causes a lot of headache, but it looks like

AsBytes(CImage &image, CByteArray &bytes)
{
    int pitch = image.GetPitch();
    int size = abs(pitch) * image.GetHeight();
    const BYTE *src = (BYTE *)image.GetBits();

    if(pitch < 0)
    {
        src -= size;
    }
    
    BYTE *pBitmapData = new BYTE[size];
    memcpy(pBitmapData, src, size * sizeof(BYTE));

    for(int i = 0; i < size; i  )
    {
        bytes.Add(pBitmapData[i]);
    }
}

CodePudding user response:

After some fixes my AsBytes became

bool AsBytes(CImage &image, CByteArray &bytes)
{
    int size = abs(image.GetPitch()) * image.GetHeight();
    BYTE *result = new BYTE[size];

    IStream *pStream = NULL;
    HRESULT hr = CreateStreamOnHGlobal(0, TRUE, &pStream);
    if( SUCCEEDED(hr) )
    {
        hr = image.Save(pStream, Gdiplus::ImageFormatPNG);
        if( SUCCEEDED(hr) )
        {
            // Set to start
            LARGE_INTEGER offset;
            offset.HighPart = 0;
            offset.LowPart = 0;
            offset.QuadPart = 0;
            hr = pStream->Seek(offset, STREAM_SEEK_SET, 0);

            ULONG read;
            hr = pStream->Read(result, size, &read);
            if( SUCCEEDED(hr) )
            {
                bytes.SetSize(read * sizeof(BYTE));
                memcpy(bytes.GetData(), result, read * sizeof(BYTE));
            }

            pStream->Release();

            return true;
        }
    }
    return false;
}

It now uses stream to convert it.

CodePudding user response:

To serialize a CImage object into a stream of bytes you can use its CImage::Save overload that accepts an IStream interface, and pass it a memory stream. Since we don't know the resulting size ahead of time we have to make do with a stream that grows as required. SHCreateMemStream can be constructed with defaults for that purpose.

The following implementation serializes a CImage encoded as PNG data into a std::vector<uint8_t>:

#include <atlimage.h>
#include <comdef.h>
#include <Shlwapi.h>
#include <vector>

std::vector<uint8_t> as_bytes(CImage const& img)
{
    // Serialize image to memory stream
    CComPtr<IStream> stream {};
    stream.Attach(::SHCreateMemStream(nullptr, 0));
    _com_util::CheckError(img.Save(stream, Gdiplus::ImageFormatPNG));

    // Read memory stream into vector
    _com_util::CheckError(stream->Seek({}, STREAM_SEEK_SET, nullptr));
    std::vector<uint8_t> bytes {};

    uint8_t buffer[4096] = {};
    ULONG bytes_read { 0 };
    HRESULT hr { stream->Read(&buffer, sizeof(buffer), &bytes_read) };
    while (SUCCEEDED(hr) && bytes_read > 0)
    {
        bytes.insert(end(bytes), buffer, buffer   bytes_read);
        hr = stream->Read(&buffer, sizeof(buffer), &bytes_read);
    }
    // Let's not swallow any errors
    _com_util::CheckError(hr);

    return bytes;
}

The code is using C exceptions to report errors, making the function more natural to use. The implementation leaves some room for improvement, especially the (possibly many) re-allocations of the vector that's repeatedly appended to. A possible alternative would be to implement the IStream interface with a class that internally writes to a vector directly. This would allow for an implementation without copying data around.

For completeness, here's a base64-encoder that doesn't rely on ATL's implementation. It's using CryptBinaryToStringA instead:

#include <wincrypt.h>

#include <string>
#include <vector>

#pragma comment(lib, "Crypt32.lib")

std::string to_base64(std::vector<uint8_t> const& bytes)
{
    // Return empty string on empty input
    if (bytes.empty())
    {
        return {};
    }

    // Change as desired
    auto const flags { CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF };

    // Request required character count (including NUL character)
    DWORD chars_required {};
    if (!::CryptBinaryToStringA(bytes.data(), static_cast<DWORD>(bytes.size()), flags,
                                nullptr, &chars_required)
        || chars_required < 1)
    {
        throw std::runtime_error { "CryptBinaryToStringA() failed" };
    }

    // Create a sufficiently sized string and have the API write into it
    std::string base64(chars_required - 1, 0);
    if (!::CryptBinaryToStringA(bytes.data(), static_cast<DWORD>(bytes.size()), flags,
                                base64.data(), &chars_required))
    {
        throw std::runtime_error { "CryptBinaryToStringA() failed" };
    }

    return base64;
}

This requires C 17 to compile for the std::string::data() call to return a non-const pointer. Note that overwriting the trailing NUL terminator in the std::string with another NUL terminator is also well defined as of C <something>.

And with that you have a nice command line utility that base64-encodes images:

int wmain(int argc, wchar_t const* argv[])
{
    if (argc != 2)
    {
        return -1;
    }

    std::wstring const src { argv[1] };
    CImage src_img {};
    _com_util::CheckError(src_img.Load(src.c_str()));

    auto const bytes = as_bytes(src_img);
    auto const base64 = to_base64(bytes);

    printf("%s", base64.c_str());

    return 0;
}
  • Related