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;
}