Skip to content

Commit

Permalink
New: Add support for EXIF data in non-animated PNGs when `ForceGDIPlu…
Browse files Browse the repository at this point in the history
…s` or `UseEmbeddedColorProfiles` is true

Merge PR #265 by https://github.com/qbnu
  • Loading branch information
sylikc committed Jun 21, 2024
2 parents 7e339ee + 04e62ea commit 5d67c64
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 28 deletions.
64 changes: 37 additions & 27 deletions src/JPEGView/ImageLoadThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
#include "BasicProcessing.h"
#include "dcraw_mod.h"
#include "TJPEGWrapper.h"
#ifndef WINXP
#include "PNGWrapper.h"
#ifndef WINXP
#include "JXLWrapper.h"
#include "HEIFWrapper.h"
#include "AVIFWrapper.h"
Expand Down Expand Up @@ -320,19 +320,14 @@ void CImageLoadThread::ProcessRequest(CRequestBase& request) {
DeleteCachedAvifDecoder();
ProcessReadWEBPRequest(&rq);
break;
#ifndef WINXP
case IF_PNG:
DeleteCachedGDIBitmap();
DeleteCachedWebpDecoder();
DeleteCachedJxlDecoder();
DeleteCachedAvifDecoder();
if (CSettingsProvider::This().ForceGDIPlus()) {
DeleteCachedPngDecoder();
ProcessReadGDIPlusRequest(&rq);
} else {
ProcessReadPNGRequest(&rq);
}
ProcessReadPNGRequest(&rq);
break;
#ifndef WINXP
case IF_JXL:
DeleteCachedGDIBitmap();
DeleteCachedWebpDecoder();
Expand Down Expand Up @@ -650,7 +645,6 @@ void CImageLoadThread::ProcessReadWEBPRequest(CRequest * request) {

#ifndef WINXP
void CImageLoadThread::ProcessReadPNGRequest(CRequest* request) {
bool bSuccess = false;
bool bUseCachedDecoder = false;
const wchar_t* sFileName;
sFileName = (const wchar_t*)request->FileName;
Expand All @@ -668,36 +662,42 @@ void CImageLoadThread::ProcessReadPNGRequest(CRequest* request) {
return;
}
}
char* pBuffer = NULL;
HGLOBAL hFileBuffer = NULL;
void* pBuffer = NULL;
try {
unsigned int nFileSize;
unsigned int nNumBytesRead;
if (!bUseCachedDecoder) {
// Don't read too huge files
nFileSize = ::GetFileSize(hFile, NULL);
if (nFileSize > MAX_PNG_FILE_SIZE) {
request->OutOfMemory = true;
::CloseHandle(hFile);
return ProcessReadGDIPlusRequest(request);
return;
}

pBuffer = new(std::nothrow) char[nFileSize];
hFileBuffer = ::GlobalAlloc(GMEM_MOVEABLE, nFileSize);
pBuffer = (hFileBuffer == NULL) ? NULL : ::GlobalLock(hFileBuffer);
if (pBuffer == NULL) {
if (hFileBuffer) ::GlobalFree(hFileBuffer);
request->OutOfMemory = true;
::CloseHandle(hFile);
return ProcessReadGDIPlusRequest(request);
return;
}
}
else {
} else {
nFileSize = 0; // to avoid compiler warnings, not used
}
if (bUseCachedDecoder || (::ReadFile(hFile, pBuffer, nFileSize, (LPDWORD)&nNumBytesRead, NULL) && nNumBytesRead == nFileSize)) {
int nWidth, nHeight, nBPP, nFrameCount, nFrameTimeMs;
bool bHasAnimation;
uint8* pPixelData = NULL;
void* pEXIFData;
void* pEXIFData = NULL;

#ifndef WINXP
// If UseEmbeddedColorProfiles is true and the image isn't animated, we should use GDI+ for better color management
if (bUseCachedDecoder || !CSettingsProvider::This().UseEmbeddedColorProfiles() || PngReader::IsAnimated(pBuffer, nFileSize))
bool bUseGDIPlus = CSettingsProvider::This().ForceGDIPlus() || CSettingsProvider::This().UseEmbeddedColorProfiles();
if (bUseCachedDecoder || !bUseGDIPlus || PngReader::IsAnimated(pBuffer, nFileSize))
pPixelData = (uint8*)PngReader::ReadImage(nWidth, nHeight, nBPP, bHasAnimation, nFrameCount, nFrameTimeMs, pEXIFData, request->OutOfMemory, pBuffer, nFileSize);
#endif

if (pPixelData != NULL) {
if (bHasAnimation)
Expand All @@ -708,25 +708,35 @@ void CImageLoadThread::ProcessReadPNGRequest(CRequest* request) {
*pImage32++ = Helpers::AlphaBlendBackground(*pImage32, CSettingsProvider::This().ColorTransparency());

request->Image = new CJPEGImage(nWidth, nHeight, pPixelData, pEXIFData, 4, 0, IF_PNG, bHasAnimation, request->FrameIndex, nFrameCount, nFrameTimeMs);
free(pEXIFData);
bSuccess = true;
}
else {
} else {
DeleteCachedPngDecoder();

IStream* pStream = NULL;
if (::CreateStreamOnHGlobal(hFileBuffer, FALSE, &pStream) == S_OK) {
Gdiplus::Bitmap* pBitmap = Gdiplus::Bitmap::FromStream(pStream, CSettingsProvider::This().UseEmbeddedColorProfiles());
bool isOutOfMemory, isAnimatedGIF;
pEXIFData = PngReader::GetEXIFBlock(pBuffer, nFileSize);
request->Image = ConvertGDIPlusBitmapToJPEGImage(pBitmap, 0, pEXIFData, 0, isOutOfMemory, isAnimatedGIF);
request->OutOfMemory = request->Image == NULL && isOutOfMemory;
pStream->Release();
delete pBitmap;
} else {
request->OutOfMemory = true;
}
}
free(pEXIFData);
}
}
catch (...) {
// delete request->Image;
// request->Image = NULL;
delete request->Image;
request->Image = NULL;
request->ExceptionError = true;
}
if (!bUseCachedDecoder) {
::CloseHandle(hFile);
delete[] pBuffer;
if (pBuffer) ::GlobalUnlock(hFileBuffer);
if (hFileBuffer) ::GlobalFree(hFileBuffer);
}
if (!bSuccess)
return ProcessReadGDIPlusRequest(request);
}
#endif

Expand Down
32 changes: 31 additions & 1 deletion src/JPEGView/PNGWrapper.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#include "stdafx.h"

#include "PNGWrapper.h"

#ifndef WINXP
#include "png.h"
#include "MaxImageDef.h"
#include <stdexcept>
#include <stdlib.h>

// Uncomment to build without APNG support
//#undef PNG_APNG_SUPPORTED
Expand Down Expand Up @@ -400,3 +401,32 @@ bool PngReader::IsAnimated(void* buffer, size_t sizebytes) {
}
return false;
}
#else
#define PNG_UINT_31_MAX (0x7fffffff)
#endif

void* PngReader::GetEXIFBlock(void* buffer, size_t sizebytes) {
size_t offset = 8; // skip PNG signature
while (offset + 7 < sizebytes) {
unsigned int chunksize = *(unsigned int*)((char*)buffer + offset);
// PNG chunk sizes are big-endian and must be converted to little-endian
chunksize = _byteswap_ulong(chunksize);

if (memcmp((char*)buffer + offset + 4, "eXIf", 4) == 0 && chunksize < 65528 && offset + chunksize + 7 < sizebytes) {
void* exif_chunk = malloc(chunksize + 10);
if (exif_chunk != NULL) {
memcpy(exif_chunk, "\xFF\xE1\0\0Exif\0\0", 10);
*((unsigned short*)exif_chunk + 1) = _byteswap_ushort(chunksize + 8);
memcpy((char*)exif_chunk + 10, (char*)buffer + offset + 8, chunksize);
}
return exif_chunk;
}

// Prevent infinite loop
if (chunksize > PNG_UINT_31_MAX) return NULL;

// 12 comes from 4 bytes for chunk size, 4 for chunk name, and 4 for CRC32
offset += chunksize + 12;
}
return NULL;
}
6 changes: 6 additions & 0 deletions src/JPEGView/PNGWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
class PngReader
{
public:
#ifndef WINXP
// Returns data in 4 byte BGRA
static void* ReadImage(int& width, // width of the image loaded.
int& height, // height of the image loaded.
Expand All @@ -20,11 +21,16 @@ class PngReader

// Returns true if PNG is animated, false otherwise
static bool IsAnimated(void* buffer, size_t sizebytes);
#endif
// Get EXIF Block
static void* GetEXIFBlock(void* buffer, size_t sizebytes);

#ifndef WINXP
private:
struct png_cache;
static png_cache cache;
static bool BeginReading(void* buffer, size_t sizebytes, bool& outOfMemory);
static void* ReadNextFrame(void** exif_chunk, unsigned int* exif_size);
static void DeleteCacheInternal(bool free_buffer);
#endif
};

0 comments on commit 5d67c64

Please sign in to comment.