How to Add 'Comments' to a JPEG File Using MFC - mfc

Within the property window of a JPEG image, there is a tab called 'Summary'. Within this tab, there is a field called 'Comments' I would like to write some MFC code which will add a given string to this field e.g "This is a photo".
Does some kind soul out there know how to do this?
Many thanks.

MFC doesn't provide this functionality, however you can use GDI+ for the task. The Image class is capable of reading and writing Exif metadata.
This is almost trivial, and explained under Reading and Writing Metadata. However, since the UserComment metadata tag allows for different character encodings, things get a little more involved. The following code1) implements a command line utility that allows to set (or replace if present) the UserComment field:
#include <windows.h>
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment(lib, "Gdiplus.lib")
int wmain( int argc, const wchar_t* argv[] ) {
// Input format: AddExifComment "<input filename>" "<comment>" "<output filename>"
if (argc != 4)
return -1;
// Initialize GDI+
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken{ 0 };
GdiplusStartup( &gdiplusToken, &gdiplusStartupInput, nullptr );
// Load image
Image* img = new Image(argv[1]);
// Construct Unicode comment
const auto& comment = MakeUnicodeComment(argv[2]);
// Assign the UserComment property
PropertyItem propertyItem;
propertyItem.id = PropertyTagExifUserComment;
propertyItem.length = comment.size();
propertyItem.type = PropertyTagTypeUndefined;
propertyItem.value = (void*)comment.data();
img->SetPropertyItem(&propertyItem);
// Save image
CLSID clsid;
GetEncoderClsid(L"image/jpeg", &clsid);
img->Save(argv[3], &clsid);
// Cleanup
delete img;
GdiplusShutdown(gdiplusToken);
return 0;
}
Constructing an appropriately formatted comment takes a bit of work. This is implemented in the following function:
#include <vector>
using std::vector;
#include <iterator>
using std::back_inserter;
vector<BYTE> MakeUnicodeComment(const wchar_t* text){
// Exif 2.2 header for Unicode (UCS-2): 'U', 'N', 'I', 'C', 'O', 'D', 'E', '\0'
static const char header[]{"UNICODE"};
static const size_t headerSize{ sizeof(header) / sizeof(header[0]) };
// UserComment field contains the 8-byte header followed by UTF-16LE encoded code units
vector<BYTE> buffer;
std::copy(header, header + headerSize, back_inserter(buffer));
// Append comment text (NUL terminator is not required)
auto current = text;
while (*current) {
buffer.push_back(*current & 0xFF);
buffer.push_back((*current >> 8) & 0xFF);
++current;
}
return buffer;
}
Retrieving the JPEG encoder from its MIME type is copied from Retrieving the Class Identifier for an Encoder for reference:
int GetEncoderClsid(const WCHAR* format, CLSID* pClsid) {
UINT num = 0; // number of image encoders
UINT size = 0; // size of the image encoder array in bytes
ImageCodecInfo* pImageCodecInfo = NULL;
GetImageEncodersSize(&num, &size);
if (size == 0)
return -1; // Failure
pImageCodecInfo = (ImageCodecInfo*)(malloc(size));
if (pImageCodecInfo == NULL)
return -1; // Failure
GetImageEncoders(num, size, pImageCodecInfo);
for (UINT j = 0; j < num; ++j) {
if (wcscmp(pImageCodecInfo[j].MimeType, format) == 0) {
*pClsid = pImageCodecInfo[j].Clsid;
free(pImageCodecInfo);
return j; // Success
}
}
free(pImageCodecInfo);
return -1; // Failure
}
1) Error handling elided for brevity.

OK,Finally I manage to solove the problem!There is the code:
OK,Finally I manage to solove the problem!There is the code:
#include "stdafx.h"
#include <windows.h>
#include <gdiplus.h>
#include <stdio.h>
using namespace Gdiplus;
#pragma comment(lib, "gdiplus.lib")
int GetEncoderClsid(const WCHAR* format, CLSID* pClsid)
{
UINT num= 0;
UINT size= 0;
ImageCodecInfo* pImageCodecInfo= NULL;
GetImageEncodersSize(&num, &size);
if(size== 0)
{
return -1;
}
pImageCodecInfo= (ImageCodecInfo*)(malloc(size));
if(pImageCodecInfo== NULL)
{
return -1;
}
GetImageEncoders(num, size, pImageCodecInfo);
for(UINT j=0; j< num; ++j)
{
if(wcscmp(pImageCodecInfo[j].MimeType, format)== 0)
{
*pClsid= pImageCodecInfo[j].Clsid;
free(pImageCodecInfo);
return j;
}
}
free(pImageCodecInfo);
return -1;
}
// load bitmap from memory,sync way
Bitmap* LoadBitmapFromMemory(const void* memory, DWORD size)
{
Bitmap* bmp = NULL;
IStream* stream = NULL;
if (CreateStreamOnHGlobal(NULL, TRUE, &stream) == S_OK)
{
ULARGE_INTEGER uli;
uli.QuadPart = size;
stream->SetSize(uli);
if (stream->Write(memory, size, NULL) == S_OK)
bmp = new Bitmap(stream);
stream->Release();
}
return bmp;
}
// load bitmap from file,sync way
Bitmap* LoadBitmapFromFile(const TCHAR* file_name)
{
Bitmap* bmp = NULL;
HANDLE file_handle = CreateFile(file_name, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (file_handle != INVALID_HANDLE_VALUE)
{
DWORD temp = 0;
DWORD file_size = GetFileSize(file_handle, &temp);
if (file_size && !temp) // the file must be less than 4G
{
unsigned char* buffer = new unsigned char[file_size];
if (ReadFile(file_handle, buffer, file_size, &temp, NULL))
bmp = LoadBitmapFromMemory(buffer, temp);
delete [] buffer;
}
CloseHandle(file_handle);
}
return bmp;
}
int _tmain(int argc, _TCHAR* argv[])
{
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
Status stat;
CLSID clsid;
char propertyValue[] = "Fake Photograph";
Bitmap* bitmap = LoadBitmapFromFile(L"E:/sandbox/stone.jpg");
PropertyItem* propertyItem = new PropertyItem;
// Get the CLSID of the JPEG encoder.
GetEncoderClsid(L"image/jpeg", &clsid);
propertyItem->id = PropertyTagCopyright;
propertyItem->length = 16; // string length including NULL terminator
propertyItem->type = PropertyTagTypeASCII;
propertyItem->value = propertyValue;
bitmap->SetPropertyItem(propertyItem);
stat = bitmap->Save(L"E:/sandbox/stone.jpg", &clsid, NULL);
if(stat == Ok)
printf("FakePhoto2.jpg saved successfully.\n");
delete propertyItem;
delete bitmap;
GdiplusShutdown(gdiplusToken);
return 0;
return 0;
}

Related

How to convert CImage to Base64 string in MFC C++

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]);
}
}
After some fixes my AsBytes became
bool AsBytes(CImage &image, CByteArray &bytes)
{
IStream *pStream = NULL;
HRESULT hr = CreateStreamOnHGlobal(0, TRUE, &pStream);
if( SUCCEEDED(hr) )
{
hr = image.Save(pStream, Gdiplus::ImageFormatPNG);
if( SUCCEEDED(hr) )
{
// Get size
ULARGE_INTEGER liSize;
IStream_Size(pStream, &liSize);
// Assume no huge files
int size = liSize.QuadPart;
BYTE *result = new BYTE[size];
// 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));
}
delete [] result;
pStream->Release();
return true;
}
}
pStream->Release();
return false;
}
It now uses stream to convert it.
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));
// Find size in bytes
ULARGE_INTEGER size {};
_com_util::CheckError(stream->Seek({}, STREAM_SEEK_CUR, &size));
if (size.HighPart != 0)
{
throw std::runtime_error("Images larger than 4GiB not supported");
}
// Read memory stream into vector
std::vector<uint8_t> bytes(size.QuadPart);
_com_util::CheckError(stream->Seek({}, STREAM_SEEK_SET, nullptr));
ULONG bytes_read { 0 };
_com_util::CheckError(stream->Read(bytes.data(),
static_cast<ULONG>(bytes.size()),
&bytes_read));
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, in particular the additional allocation of bytes plus the copy. 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;
}

Extract all icons of a file

First of all, I want to access all icons (16x16...256x256 and larger) in a ".exe" file.
As a result of my research, I found such code:
#ifndef __ICON_LIST_H__
#define __ICON_LIST_H__
#include <windows.h>
#include <vector>
class IconFile: public std::vector<HICON>{
public:
IconFile(){};
IconFile(std::string i_filename){
addIconsFromFile(i_filename);
};
int addIconsFromFile(std::string i_fileName){
int iCount=0;
HANDLE file = CreateFile( i_fileName.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, 0, NULL);
if(file!=INVALID_HANDLE_VALUE){
int size = GetFileSize(file,NULL);
DWORD actRead;
BYTE* buffer = new BYTE[size];
ReadFile(file, buffer, size, &actRead, NULL);
CloseHandle(file);
int ind = -1;
for(int p = 0; p< size-4; ++p){
if(buffer[p]==40 && buffer[p+1]==0 && buffer[p+2]==0 && buffer[p+3]==0){
HICON icon = CreateIconFromResourceEx(&buffer[p], size-p, true, 0x00030000,0,0,0);
if(icon){
++iCount;
this->push_back(icon);
}
}
}
delete[] buffer;
}
return iCount;
};
};
#endif //__ICON_LIST_H__
This code works fine but doesn't show 256x256 or larger icon, Code output:
std::vector(0x8ef2013, 0x64ce1867, 0x24681219)
Currently, this is the icon list of the file:
How can I get the 256x256 icon?
Use LoadLibraryEx to load the file, EnumResourceNames to enumerate icons, and CreateIconFromResourceEx to lead each icon.
Note that driving a class from std::vector and other C++ Standard Library containers is not recommended.
The example below uses LR_SHARED, you might want to change that.
#include <windows.h>
#include <vector>
BOOL CALLBACK EnumIcons(HMODULE hmodule, LPCTSTR type, LPTSTR lpszName,
LONG_PTR ptr)
{
if (!ptr)
return FALSE;
auto pvec = (std::vector<HICON>*)ptr;
auto hRes = FindResource(hmodule, lpszName, type);
if (!hRes)
return TRUE;
auto size = SizeofResource(hmodule, hRes);
auto hg = LoadResource(hmodule, hRes);
if (!hg)
return TRUE;
auto bytes = (BYTE*)LockResource(hg);
auto hicon = CreateIconFromResourceEx(bytes, size, TRUE, 0x00030000,
0, 0, LR_SHARED);
if (hicon)
pvec->push_back(hicon);
return TRUE;
}
int main()
{
std::vector<HICON> vec;
const char* modulepath = "file.exe";
HMODULE hmodule = LoadLibraryEx(modulepath, NULL,
LOAD_LIBRARY_AS_IMAGE_RESOURCE);
if (!hmodule)
return 0;
EnumResourceNames(hmodule, RT_ICON,(ENUMRESNAMEPROC)EnumIcons,(LONG_PTR)&vec);
for (auto e : vec)
{
ICONINFOEX ii = { sizeof(ii) };
if (!GetIconInfoEx(e, &ii) || !ii.hbmColor)
continue;
BITMAP bm;
GetObject(ii.hbmColor, sizeof(bm), &bm);
printf("%d %d %d\n", bm.bmWidth, bm.bmHeight, bm.bmBitsPixel);
}
//free icons...
return 0;
}

How to create a bitmap/image from Scan0?

I'm having trouble porting the following C# code to C++:
protected override void OnPaint(CefBrowser browser, CefPaintElementType type, CefRectangle[] dirtyRects
, System.IntPtr buffer, int width, int height)
{
if (isPainting == true)
return;
isPainting = true;
// Save the provided buffer (a bitmap image) as a PNG.
using (System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(width, height, width * 4, System.Drawing.Imaging.PixelFormat.Format32bppRgb, buffer))
{
bitmap.Save(#"LastOnPaint.png", System.Drawing.Imaging.ImageFormat.Png);
} // End Using bitmap
}
What it does:
Create an image from a WebSite/SVG as rendered by the latest version of Chromium embedded and save it as a file.
So this is the corresponding render-handler in C++:
void RenderHandler::OnPaint(
CefRefPtr<CefBrowser> browser,
CefRenderHandler::PaintElementType type,
const CefRenderHandler::RectList& dirtyRects,
const void* buffer, int width, int height
) {
// size_t len = sizeof(buffer) / sizeof(void*);
// printf("buffer length: %zu\n", len); // 1...
// Array size is probably: width*height * 4;
}
So I was looking into what C# does in the bitmap-constructor, which is the following:
public Bitmap(int width, int height, int stride, PixelFormat format, IntPtr scan0)
{
IntPtr bitmap = IntPtr.Zero;
int status = Gdip.GdipCreateBitmapFromScan0(width, height, stride, unchecked((int)format), new HandleRef(null, scan0), out bitmap);
Gdip.CheckStatus(status);
SetNativeImage(bitmap);
}
internal void SetNativeImage(IntPtr handle) {
if (handle == IntPtr.Zero)
throw new ArgumentException(SR.GetString(SR.NativeHandle0), "handle");
nativeImage = handle;
}
Which traces to
internal const string Gdiplus = "gdiplus.dll";
[DllImport(ExternDll.Gdiplus, SetLastError=true, ExactSpelling=true, CharSet=System.Runtime.InteropServices.CharSet.Unicode)] // 3 = Unicode
[ResourceExposure(ResourceScope.Machine)]
internal static extern int GdipCreateBitmapFromScan0(int width, int height, int stride, int format, HandleRef scan0, out IntPtr bitmap);
So I thought I could just call GdipCreateBitmapFromScan0 in gdibitmapflat and be almost finished
GpStatus WINGDIPAPI GdipCreateBitmapFromScan0(INT width
, INT height, INT stride, PixelFormat format
, BYTE* scan0, GpBitmap** bitmap)
So I gathered the necessary header-files for GDI, which was a horrible experience
#ifndef __BITMAPHELPER_H__
#define __BITMAPHELPER_H__
// #define WIN32_LEAN_AND_MEAN
#pragma warning(disable:4458)
#include <Windows.h>
#include <ObjIdl.h>
#include <minmax.h>
#include <gdiplus.h>
#include <wingdi.h>
#include <gdiplusbitmap.h>
#include <gdiplusflat.h>
using namespace Gdiplus;
#pragma comment (lib,"gdiplus.lib")
#pragma warning(default:4458)
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cstdint>
#include <cstdbool>
#include <algorithm>
#include <memory>
And thought this would about do it
#include "BitmapHelper.h"
static void Test()
{
GpBitmap *bitmap = NULL;
GdipCreateBitmapFromScan0(100, 100, 0, PixelFormat32bppARGB, NULL, &bitmap); // create a bitmap object with specified width/height/color
// GpGraphics *graph;
// Image * syntaxTest = NULL;
//syntaxTest->FromFile(TEXT("d:\\abc.jpg"), true); // create an image object
// Bitmap::FromBITMAPINFO
// GpImage *image = NULL;
// Gdiplus::Image()
Bitmap *bmp = NULL;
// GdipLoadImageFromFile(TEXT("d:\\abc.jpg"), &image); // create an image object
// GdipGetImageGraphicsContext(bitmap, &graph); // create a graphic object via bitmap object
// GdipDrawImageI(graph, image, 100, 100); // draw image to this graphic object, it can be done
}
However, it turns out the compiler doesn't know GdipCreateBitmapFromScan0, although it's definitely inside #include <gdiplusflat.h>...
How to create a bitmap/image from Scan0 ?
Note:
While I am at it, I don't want to resort to C++.NET, and ideally not to the WinAPI either; because i'd like it to work on Linux too. And not to a monstrous dependency like SDL either.
So far, it looks like my possible alternatives are using this code:
https://codereview.stackexchange.com/questions/196084/read-and-write-bmp-file-in-c
which means I have to create the bitmap header myselfs.
Or I could use some code from ImageIO.
I can't quite belive that creating a simple bitmap on even a single operating-system is that hard...
Is there really no better (and portable) way to create a simple bitmap from a trivial array of pixel colors ?
And why does the compiler not find GdipCreateBitmapFromScan0 ?
If I had used LoadLibrary and GetProcAddress to invoke it instead of f*ing windows header files, I'd be about finished by now...
And why does #include <gdiplus.h> not include its own dependencies ?
Your looking at the internals of .NET have led you toward using a function that's not part of the documented, public interface of GDI+. It looks to me like that's the real cause of most of your problems.
What I think you probably want to do is start by creating a GdiPlus::Bitmap object from your pixels. It has a constructor that looks like it'll directly accept your data.
Once you've created the Bitmap object, you call its Save member function. Bitmap is publicly derived from Image, so you're basically dealing with the normal Image::Save to generate a PNG.
If you want to eliminate the dependency on Windows code, you might consider using (for one obvious possibility) libpng instead. This gives you quite a lot more control over the process, at the expense of being quite a bit more work to use (depending on what you want to do, probably on the order of a half dozen to a dozen lines of code rather than one or two).
So, after having done this in both GDI+ and raw C, I can safely say that it's actually faster, and not to mention considerably less problematic and less google-intensive just doing the image-handling without GDI/GDI+. Whoever implemented GDI+ has a major brain damage.
Since I haven't yet handled transparency properly, and not yet incorporated lodepng, I've added GDI+ as an optional extra option, for the time being.
// A program to read, write, and crop BMP image files.
#include "Bmp.h"
// Make a copy of a string on the heap.
// - Postcondition: the caller is responsible to free
// the memory for the string.
char *_string_duplicate(const char *string)
{
char *copy = (char*)malloc(sizeof(*copy) * (strlen(string) + 1));
if (copy == NULL)
{
// return "Not enough memory for error message";
const char* error_message = "Not enough memory for error message";
size_t len = strlen(error_message);
char* error = (char*)malloc(len * sizeof(char) + 1);
strcpy(error, error_message);
return error;
}
strcpy(copy, string);
return copy;
}
// Check condition and set error message.
bool _check(bool condition, char **error, const char *error_message)
{
bool is_valid = true;
if (!condition)
{
is_valid = false;
if (*error == NULL) // to avoid memory leaks
{
*error = _string_duplicate(error_message);
}
}
return is_valid;
}
// Write an image to an already open file.
// - Postcondition: it is the caller's responsibility to free the memory
// for the error message.
// - Return: true if and only if the operation succeeded.
bool write_bmp(FILE *fp, BMPImage *image, char **error)
{
// Write header
rewind(fp);
size_t num_read = fwrite(&image->header, sizeof(image->header), 1, fp);
if (!_check(num_read == 1, error, "Cannot write image"))
{
return false;
}
// Write image data
num_read = fwrite(image->data, image->header.image_size_bytes, 1, fp);
if (!_check(num_read == 1, error, "Cannot write image"))
{
return false;
}
return true;
}
// Free all memory referred to by the given BMPImage.
void free_bmp(BMPImage *image)
{
free(image->data);
free(image);
}
// Open file. In case of error, print message and exit.
FILE *_open_file(const char *filename, const char *mode)
{
FILE *fp = fopen(filename, mode);
if (fp == NULL)
{
fprintf(stderr, "Could not open file %s\n", filename);
exit(EXIT_FAILURE);
}
return fp;
}
// Close file and release memory.void _clean_up(FILE *fp, BMPImage *image, char **error)
void _clean_up(FILE *fp, BMPImage *image, char **error)
{
if (fp != NULL)
{
fclose(fp);
}
free_bmp(image);
free(*error);
}
// Print error message and clean up resources.
void _handle_error(char **error, FILE *fp, BMPImage *image)
{
fprintf(stderr, "ERROR: %s\n", *error);
_clean_up(fp, image, error);
exit(EXIT_FAILURE);
}
void write_image(const char *filename, BMPImage *image, char **error)
{
FILE *output_ptr = _open_file(filename, "wb");
if (!write_bmp(output_ptr, image, error))
{
_handle_error(error, output_ptr, image);
}
fflush(output_ptr);
fclose(output_ptr);
_clean_up(output_ptr, image, error);
}
// Return the size of an image row in bytes.
// - Precondition: the header must have the width of the image in pixels.
uint32_t computeImageSize(BMPHeader *bmp_header)
{
uint32_t bytes_per_pixel = bmp_header->bits_per_pixel / BITS_PER_BYTE;
uint32_t bytes_per_row_without_padding = bmp_header->width_px * bytes_per_pixel;
uint32_t padding = (4 - (bmp_header->width_px * bytes_per_pixel) % 4) % 4;
uint32_t row_size_bytes = bytes_per_row_without_padding + padding;
return row_size_bytes * bmp_header->height_px;
}
#ifdef USE_GDI
#pragma warning(disable:4189)
int GetEncoderClsid(const WCHAR* format, CLSID* pClsid)
{
UINT num = 0; // number of image encoders
UINT size = 0; // size of the image encoder array in bytes
Gdiplus::ImageCodecInfo* pImageCodecInfo = NULL;
Gdiplus::GetImageEncodersSize(&num, &size);
if (size == 0)
return -1; // Failure
pImageCodecInfo = (Gdiplus::ImageCodecInfo*)(malloc(size));
if (pImageCodecInfo == NULL)
return -1; // Failure
Gdiplus::GetImageEncoders(num, size, pImageCodecInfo);
for (UINT j = 0; j < num; ++j)
{
if (wcscmp(pImageCodecInfo[j].MimeType, format) == 0)
{
*pClsid = pImageCodecInfo[j].Clsid;
free(pImageCodecInfo);
return j; // Success
} // if (wcscmp(pImageCodecInfo[j].MimeType, format) == 0)
} // Next j
free(pImageCodecInfo);
return -1; // Failure
}
// https://github.com/lvandeve/lodepng
static bool notInitialized = true;
void WriteBitmapToFile(const char *filename, int width, int height, const void* buffer)
{
// HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
if (notInitialized)
{
// https://learn.microsoft.com/en-us/windows/desktop/api/gdiplusinit/nf-gdiplusinit-gdiplusstartup
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
Gdiplus::Status isOk = Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
if (isOk != Gdiplus::Status::Ok)
{
printf("Failed on GdiplusStartup\n");
}
notInitialized = false;
// defer
// GdiplusShutdown(gdiplusToken);
} // End if (notInitialized)
// https://learn.microsoft.com/en-us/windows/desktop/gdiplus/-gdiplus-constant-image-pixel-format-constants
Gdiplus::Bitmap* myBitmap = new Gdiplus::Bitmap(width, height, width*4, PixelFormat32bppARGB, (BYTE*)buffer);
// myBitmap->RotateFlip(Gdiplus::Rotate180FlipY);
CLSID pngClsid;
// int result = GetEncoderClsid(L"image/tiff", &tiffClsid);
int result = GetEncoderClsid(L"image/png", &pngClsid);
printf("End GetEncoderClsid:\n");
if (result == -1)
printf("Error: GetEncoderClsid\n");
// throw std::runtime_error("Bitmap::Save");
// if (Ok != myBitmap->Save(L"D\foobartest.png", &pngClsid)) printf("Error: Bitmap::Save");
// WTF ? I guess a standard C/C++-stream would have been too simple ?
IStream* oStream = nullptr;
if (CreateStreamOnHGlobal(NULL, TRUE, (LPSTREAM*)&oStream) != S_OK)
printf("Error on creating an empty IStream\n");
Gdiplus::EncoderParameters encoderParameters;
encoderParameters.Count = 1;
encoderParameters.Parameter[0].Guid = Gdiplus::EncoderQuality;
encoderParameters.Parameter[0].Type = Gdiplus::EncoderParameterValueTypeLong;
encoderParameters.Parameter[0].NumberOfValues = 1;
ULONG quality = 100;
encoderParameters.Parameter[0].Value = &quality;
// https://learn.microsoft.com/en-us/windows/desktop/api/gdiplusheaders/nf-gdiplusheaders-image-save(inistream_inconstclsid_inconstencoderparameters)
if (Gdiplus::Status::Ok != myBitmap->Save(oStream, &pngClsid, &encoderParameters))
printf("Error: Bitmap::Save\n");
// throw std::runtime_error("Bitmap::Save");
ULARGE_INTEGER ulnSize;
LARGE_INTEGER lnOffset;
lnOffset.QuadPart = 0;
oStream->Seek(lnOffset, STREAM_SEEK_END, &ulnSize);
oStream->Seek(lnOffset, STREAM_SEEK_SET, NULL);
uint8_t *pBuff = new uint8_t[(unsigned int)ulnSize.QuadPart];
ULONG ulBytesRead;
oStream->Read(pBuff, (ULONG)ulnSize.QuadPart, &ulBytesRead);
FILE *output_ptr = _open_file(filename, "wb");
fwrite((void*)pBuff, sizeof(uint8_t), (unsigned int)ulnSize.QuadPart, output_ptr);
fflush(output_ptr);
fclose(output_ptr);
oStream->Release();
delete pBuff;
delete myBitmap;
// https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp
// std::string rotated_string = base64_encode((const unsigned char*)pBuff, ulnSize.QuadPart);
}
#pragma warning(default:4189)
#else
// TODO: PNG-Encoder
// https://github.com/lvandeve/lodepng
// https://lodev.org/lodepng/
BMPImage * CreateBitmapFromScan0(int32_t w, int32_t h, uint8_t* scan0)
{
BMPImage *new_image = (BMPImage *)malloc(sizeof(*new_image));
BMPHeader *header = (BMPHeader *)malloc(sizeof(*header));
new_image->header = *header;
new_image->header.type = MAGIC_VALUE;
new_image->header.bits_per_pixel = BITS_PER_PIXEL;
new_image->header.width_px = w;
new_image->header.height_px = h;
new_image->header.image_size_bytes = computeImageSize(&new_image->header);
new_image->header.size = BMP_HEADER_SIZE + new_image->header.image_size_bytes;
new_image->header.dib_header_size = DIB_HEADER_SIZE;
new_image->header.offset = (uint32_t) sizeof(BMPHeader);
new_image->header.num_planes = 1;
new_image->header.compression = 0;
new_image->header.reserved1 = 0;
new_image->header.reserved2 = 0;
new_image->header.num_colors = 0;
new_image->header.important_colors = 0;
new_image->header.x_resolution_ppm = 3780; // image->header.x_resolution_ppm;
new_image->header.y_resolution_ppm = 3780; // image->header.y_resolution_ppm;
new_image->data = (uint8_t*)malloc(sizeof(*new_image->data) * new_image->header.image_size_bytes);
memcpy(new_image->data, scan0, new_image->header.image_size_bytes);
return new_image;
}
void WriteBitmapToFile(const char *filename, int width, int height, const void* buffer)
{
BMPImage * image = CreateBitmapFromScan0((int32_t)width, (int32_t)height, (uint8_t*)buffer);
char *error = NULL;
write_image(filename, image, &error);
}
#endif
Header:
#ifndef BITMAPLION_BITMAPINFORMATION_H
#define BITMAPLION_BITMAPINFORMATION_H
#ifdef __cplusplus
// #include <iostream>
// #include <fstream>
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <cstring>
#else
#include <stdio.h>
#include <stdlib.h> // for malloc
#include <stdint.h>
#include <stdbool.h>
#include <string.h> // for strlen, strcopy
#endif
#ifdef __linux__
//linux specific code goes here
#elif _WIN32
// windows specific code goes here
#pragma warning(disable:4458)
#include <Windows.h>
#include <ObjIdl.h>
#include <minmax.h>
#include <gdiplus.h>
// #include <gdiplusheaders.h>
// #include <wingdi.h>
// #include <gdiplusbitmap.h>
// #include <gdiplusflat.h>
// #include <Gdipluspixelformats.h>
#pragma comment (lib,"gdiplus.lib")
// using namespace Gdiplus;
#pragma warning(default:4458)
#else
#endif
#define BMP_HEADER_SIZE 54
#define DIB_HEADER_SIZE 40
// Correct values for the header
#define MAGIC_VALUE 0x4D42
#define NUM_PLANE 1
#define COMPRESSION 0
#define NUM_COLORS 0
#define IMPORTANT_COLORS 0
#define BITS_PER_BYTE 8
// #define BITS_PER_PIXEL 24
#define BITS_PER_PIXEL 32
#ifdef _MSC_VER
#pragma pack(push) // save the original data alignment
#pragma pack(1) // Set data alignment to 1 byte boundary
#endif
typedef struct
#ifndef _MSC_VER
__attribute__((packed))
#endif
{
uint16_t type; // Magic identifier: 0x4d42
uint32_t size; // File size in bytes
uint16_t reserved1; // Not used
uint16_t reserved2; // Not used
uint32_t offset; // Offset to image data in bytes from beginning of file
uint32_t dib_header_size; // DIB Header size in bytes
int32_t width_px; // Width of the image
int32_t height_px; // Height of image
uint16_t num_planes; // Number of color planes
uint16_t bits_per_pixel; // Bits per pixel
uint32_t compression; // Compression type
uint32_t image_size_bytes; // Image size in bytes
int32_t x_resolution_ppm; // Pixels per meter
int32_t y_resolution_ppm; // Pixels per meter
uint32_t num_colors; // Number of colors
uint32_t important_colors; // Important colors
} BMPHeader;
#ifdef _MSC_VER
#pragma pack(pop) // restore the previous pack setting
#endif
typedef struct {
BMPHeader header;
// unsigned char* data;
// It is more informative and will force a necessary compiler error
// on a rare machine with 16-bit char.
uint8_t* data;
} BMPImage;
// #define USE_GDI true
#ifndef USE_GDI
BMPImage * CreateBitmapFromScan0(int32_t w, int32_t h, uint8_t* scan0);
#endif
void WriteBitmapToFile(const char *filename, int width, int height, const void* buffer);
#endif //BITMAPLION_BITMAPINFORMATION_H

GetDIBits sets image buffer to all 0's (all black)

Trying to take a screenshot of a window as a bitmap. The code below is creating a properly sized bitmap, but every pixel is black. In other words, GetDIBits is setting imageBuffer to all 0's.
The posted code saves a bitmap for every notepad open and visible on the screen. None of the asserts fail.
The BITMAPFILEHEADER and actual writing to a file is omitted, because the final for loop with asserts shows GetDIBits set imageBuffer to all 0's, so there's no need to examine code after that point.
(In the executable's properties, under Configuration Properties->General, I have Character Set to "Not Set" to avoid unicode necessities.)
#include "stdafx.h"
#include <vector>
#include <sstream>
#include <Windows.h>
#include <iostream>
#include <assert.h>
using namespace std;
BOOL CALLBACK getNotepadWindowsCallback(HWND window, LPARAM notepadWindowsLparam) {
if (NULL != GetParent(window)) {
return true;
}
if (false == IsWindowVisible(window)) {
return true;
}
char text[1024] = { 0 };
GetWindowText(window, text, sizeof(text));
if (NULL == strstr(text, " - Notepad")) {
return true;
}
reinterpret_cast<vector<HWND>*>(notepadWindowsLparam)->push_back(window);
return true;
}
vector<HWND> getNotepadWindows() {
vector<HWND> notepadWindows;
EnumWindows(getNotepadWindowsCallback, reinterpret_cast<LPARAM>(&notepadWindows));
return notepadWindows;
}
int _tmain(int argc, _TCHAR* argv[]) {
for (HWND notepadWindow : getNotepadWindows()) {
HDC notepadWindowDeviceContext = GetDC(notepadWindow);
assert(NULL != notepadWindowDeviceContext);
HDC memoryDeviceContext = CreateCompatibleDC(notepadWindowDeviceContext);
assert(NULL != memoryDeviceContext);
RECT notepadWindowRectangle;
if (0 == GetClientRect(notepadWindow, &notepadWindowRectangle)) {
assert(true == false);
}
SIZE notepadWindowSize;
notepadWindowSize.cx = notepadWindowRectangle.right - notepadWindowRectangle.left + 1;
notepadWindowSize.cy = notepadWindowRectangle.bottom - notepadWindowRectangle.top + 1;
HBITMAP memoryBitmap = CreateCompatibleBitmap(notepadWindowDeviceContext, notepadWindowSize.cx, notepadWindowSize.cy);
assert(NULL != memoryBitmap);
HBITMAP defaultBitmap = static_cast<HBITMAP>(SelectObject(memoryDeviceContext, memoryBitmap));
assert(NULL != defaultBitmap);
assert(TRUE == BitBlt(memoryDeviceContext, 0, 0, notepadWindowSize.cx, notepadWindowSize.cy, notepadWindowDeviceContext, notepadWindowRectangle.left, notepadWindowRectangle.right, SRCCOPY));
BITMAPINFO bitmapinfo = { 0 };
bitmapinfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bitmapinfo.bmiHeader.biWidth = notepadWindowSize.cx;
bitmapinfo.bmiHeader.biHeight = notepadWindowSize.cy;
bitmapinfo.bmiHeader.biPlanes = 1;
bitmapinfo.bmiHeader.biBitCount = 4 * 8;
bitmapinfo.bmiHeader.biCompression = BI_RGB;
//bitmapinfo.bmiHeader.biSizeImage, per MSDN, may be set to zero for BI_RGB bitmaps
int imageBufferSize = notepadWindowSize.cx*notepadWindowSize.cy;
int* imageBuffer = new(int[imageBufferSize]);
// doing a memset here to initialize imageBuffer to 0's makes no change - leaving it out makes clear GetDIBits is setting imageBuffer to 0's, because it goes in with random garbage
assert(NULL != SelectObject(memoryDeviceContext, defaultBitmap)); // this must happen before GetDIBits, per MSDN, so memoryBitmap is not selected into a device context
int returnValue = GetDIBits(memoryDeviceContext, memoryBitmap, 0, notepadWindowSize.cy, static_cast<LPVOID>(imageBuffer), &bitmapinfo, DIB_RGB_COLORS);
assert(0 != returnValue);
cout << "returnValue is " << returnValue << endl; // shows proper number of lines is written
for (int i = 0; i < imageBufferSize; ++i) {
assert(0 == imageBuffer[i]);
}
DeleteDC(memoryDeviceContext);
ReleaseDC(NULL, notepadWindowDeviceContext);
}
}
Oops. Helps to give BitBlt notepadWindowRectangle.top instead of notepadWindowRectangle.right for its y1 parameter. Fixing that makes it work.

AccessViolationException when using C++ DLL from C#

I have a C++ DLL for use from C#. I have a function which takes a string passed to it, and I have those set on the C++ function parameters as const char * like so:
int __stdcall extract_all_frames(const char* szDestination, float scaleFactor)
The main body of this function is copied directly from a working FFmpeg example function so I'm almost certain the problem isn't there. I feel like the problem is in this modification I made to it:
//Open file
char szFilename[32];
sprintf_s(szFilename, sizeof(szFilename), "frame%d.ppm\0", iFrame);
// JOIN szFILENAME AND szDESTINATION
std::string buffer(szDestination, sizeof(szDestination));
buffer.append("\\");
buffer.append(szDestination);
Which is supposed to be a concatenated path and directory. I then pass buffer.c_str() into fopen_s(), which takes const char * not std::string. Whenever calling this function from C#, I get the following exception:
A first chance exception of type 'System.AccessViolationException' occurred in XRF FFmpeg Invoke Test.exe
Additional information: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
This is the complete code:
#include "stdafx.h"
#pragma comment (lib, "avcodec.lib")
#pragma comment (lib, "avformat.lib")
#pragma comment (lib, "avutil.lib")
#pragma comment (lib, "swscale.lib")
extern "C"
{
#include <libavcodec\avcodec.h>
#include <libavformat\avformat.h>
#include <libavutil\avutil.h>
#include <libswscale\swscale.h>
}
#include <string>
#include "Xrf.FFmpeg.hpp"
void save_frame(AVFrame* pFrame, int iFrame, const char* szDestination)
{
//Open file
char szFilename[32];
sprintf_s(szFilename, sizeof(szFilename), "frame%d.ppm\0", iFrame);
// JOIN szFILENAME AND szDESTINATION
std::string buffer(szDestination, sizeof(szDestination));
buffer.append("\\");
buffer.append(szDestination);
FILE* pFile;
errno_t openError = fopen_s(&pFile, buffer.c_str(), "wb");
if (pFile == NULL)
{
return;
}
//Write header
int width = pFrame->width;
int height = pFrame->height;
fprintf(pFile, "P6\n%d %d\n255\n", width, height);
//Write pixel data
for (int y = 0; y < height; y++)
{
fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);
}
// Close file
fclose(pFile);
}
int __stdcall extract_all_frames(const char* szPath, const char* szDestination, float scaleFactor)
{
// Check if scaleFactor is valid
if ((scaleFactor != 0.f) &&
(scaleFactor > 3.f))
{
fprintf(stderr, "Xrf: Scale factor '%f' out of bounds!\nMust be greater than 0, and less then or equal to 3.0.\n", scaleFactor);
return -1;
}
// Register all formats and codecs
av_register_all();
AVFormatContext* pFormatCtx;
if (avformat_open_input(&pFormatCtx, szPath, nullptr, nullptr) != 0)
{
fprintf(stderr, "libavformat: Couldn't open file '%s'!\n", szPath);
return -1;
}
// Retrieve stream information
if (avformat_find_stream_info(pFormatCtx, nullptr) < 0)
{
fprintf(stderr, "libavformat: Unable to find stream information!\n");
return -1;
}
// Dump information about file onto standard error
av_dump_format(pFormatCtx, 0, szPath, 0);
// Find the first video stream
size_t i;
int videoStream = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++)
{
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoStream = i;
break;
}
}
if (videoStream == -1)
{
fprintf(stderr, "libavformat: No video stream found!\n");
return -1;
}
// Get a pointer to the codec context for the video stream
AVCodecContext* pCodecCtx = pFormatCtx->streams[videoStream]->codec;
// Scale the frame
int scaleHeight = static_cast<int>(floor(pCodecCtx->height * scaleFactor));
int scaleWidth = static_cast<int>(floor(pCodecCtx->width * scaleFactor));
//Check if frame sizes are valid (not 0, because that's dumb)
if (scaleHeight == 0 || scaleWidth == 0)
{
fprintf(stderr, "Xrf: Scale factor caused a zero value in either width or height!\n");
return -1;
}
// Find the decoder for the video stream
AVCodec* pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if (pCodec == NULL)
{
fprintf(stderr, "libavcodec: Unsupported codec!\n");
return -1; // Codec not found
}
// Open codec
AVDictionary* optionsDict = nullptr;
if (avcodec_open2(pCodecCtx, pCodec, &optionsDict) < 0)
{
fprintf(stderr, "libavcodec: Couldn't open codec '%s'!\n", pCodec->long_name);
return -1;
}
// Allocate video frame
AVFrame* pFrame = av_frame_alloc();
// Allocate an AVFrame structure
AVFrame* pFrameRGB = av_frame_alloc();
if (pFrameRGB == NULL)
{
fprintf(stderr, "libavformat: Unable to allocate a YUV->RGB resampling AVFrame!\n");
return -1;
}
// Determine required buffer size and allocate buffer
int numBytes = avpicture_get_size(PIX_FMT_RGB24, scaleWidth, scaleHeight);
uint8_t* buffer = static_cast <uint8_t *> (av_malloc(numBytes * sizeof(uint8_t)));
struct SwsContext* sws_ctx = sws_getContext(pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt,
scaleWidth,
scaleHeight,
PIX_FMT_RGB24,
SWS_BILINEAR,
nullptr, nullptr, nullptr);
// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill(reinterpret_cast <AVPicture *> (pFrameRGB),
buffer,
PIX_FMT_RGB24,
scaleWidth,
scaleHeight);
// Read frames and save first five frames to disk
AVPacket packet;
int frameFinished;
while (av_read_frame(pFormatCtx, &packet) >= 0)
{
// Is this a packet from the video stream?
if (packet.stream_index == videoStream)
{
// Decode video frame
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
// Did we get a video frame?
if (frameFinished)
{
// Convert the image from its native format to RGB
sws_scale(sws_ctx,
static_cast <uint8_t const * const *> (pFrame->data),
pFrame->linesize,
0,
pCodecCtx->height,
pFrameRGB->data,
pFrameRGB->linesize);
// Save the frame to disk
if (++i <= 5)
{
save_frame(pFrameRGB, i, szDestination);
}
}
}
// Free the packet that was allocated by av_read_frame
av_free_packet(&packet);
}
av_free(buffer); // Free the RGB image
av_free(pFrameRGB);
av_free(pFrame); // Free the YUV frame
avcodec_close(pCodecCtx); // Close the codec
avformat_close_input(&pFormatCtx); // Close the video file
return 0;
}
I don't know if the error is in my modification (most likely, I'm extremely new to C++), or the other code, as the exception only throws on the invocation line in C#, not the actual C++ line causing the problem.
This is wrong:
std::string buffer(szDestination, sizeof(szDestination));
szDestination is a pointer, thus sizeof(szDestination) will return the pointer size, in bytes, not the number of characters.
If szDestination is a null terminated string, use strlen or similar function to determine the number of characters. If it isn't null terminated, then you need to pass the number of bytes to copy as a parameter.
The better thing to do is when your DLL function is called:
int __stdcall extract_all_frames(const char* szPath, const char* szDestination, float scaleFactor)
take those pointers and immediately assign them to std::string. Then drop all usage of char* or const char* from there. There is no need for your helper functions to deal with "dumb" character pointers.
Example:
int __stdcall extract_all_frames(const char* szPath, const char* szDestination, float scaleFactor)
{
std::string sPath = szPath;
std::string sDestination = sDestination;
// From here, use sPath and sDestination
//...
}
// redefinition of save_frame
//...
void save_frame(AVFrame* pFrame, int iFrame, const std::string& szDestination)
{
//Open file
std::string buffer = "frame" + to_string(iFrame) + ".ppm\0";
buffer.append("\\");
buffer.append(szDestination);
//...
}