Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JXL/HEIF/AVIF/WebP/PNG Exif data support #213

Merged
merged 9 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/JPEGView/AVIFWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ void* AvifReader::ReadImage(int& width,
int frame_index,
int& frame_count,
int& frame_time,
void*& exif_chunk,
bool& outOfMemory,
const void* buffer,
int sizebytes)
Expand All @@ -30,6 +31,7 @@ void* AvifReader::ReadImage(int& width,
width = height = 0;
nchannels = 4;
has_animation = false;
exif_chunk = NULL;

avifResult result;
int nthreads = 256; // sets maximum number of active threads allowed for libavif, default is 1
Expand Down Expand Up @@ -93,9 +95,22 @@ void* AvifReader::ReadImage(int& width,
if (cache.transform == NULL)
cache.transform = ICCProfileTransform::CreateTransform(icc.data, icc.size, ICCProfileTransform::FORMAT_BGRA);
ICCProfileTransform::DoTransform(cache.transform, cache.rgb.pixels, cache.rgb.pixels, width, height);

avifRWData exif = cache.decoder->image->exif;
if (exif.size > 8 && exif.size < 65528 && exif.data != NULL) {
exif_chunk = malloc(exif.size + 10);
if (exif_chunk != NULL) {
memcpy(exif_chunk, "\xFF\xE1\0\0Exif\0\0", 10);
*((unsigned short*)exif_chunk + 1) = _byteswap_ushort(exif.size + 8);
memcpy((uint8_t*)exif_chunk + 10, exif.data, exif.size);
}
}

void* pPixelData = cache.rgb.pixels;
if (!has_animation)
DeleteCache();
return cache.rgb.pixels;

return pPixelData;
sylikc marked this conversation as resolved.
Show resolved Hide resolved
}

void AvifReader::DeleteCache() {
Expand Down
1 change: 1 addition & 0 deletions src/JPEGView/AVIFWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class AvifReader
int frame_index, // index of frame
int& frame_count, // number of frames
int& frame_time, // frame duration in milliseconds
void*& exif_chunk, // Pointer to Exif data (must be freed by caller)
bool& outOfMemory, // set to true when no memory to read image
const void* buffer, // memory address containing jxl compressed data.
int sizebytes); // size of jxl compressed data
Expand Down
4 changes: 2 additions & 2 deletions src/JPEGView/EXIFDisplayCtl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ void CEXIFDisplayCtl::FillEXIFDataDisplay() {
CRawMetadata* pRawMetaData = CurrentImage()->GetRawMetadata();
if (pEXIFReader != NULL) {
sComment = pEXIFReader->GetUserComment();
if (sComment == NULL || sComment[0] == 0) {
if (sComment == NULL || sComment[0] == 0 || ((std::wstring) sComment).find_first_not_of(L" \t\n\r\f\v", 0) == std::wstring::npos) {
sComment = pEXIFReader->GetImageDescription();
}
if (pEXIFReader->GetAcquisitionTimePresent()) {
Expand Down Expand Up @@ -201,7 +201,7 @@ void CEXIFDisplayCtl::FillEXIFDataDisplay() {
}
}

if (sComment == NULL || sComment[0] == 0) {
if (sComment == NULL || sComment[0] == 0 || ((std::wstring)sComment).find_first_not_of(L" \t\n\r\f\v", 0) == std::wstring::npos) {
sComment = CurrentImage()->GetJPEGComment();
}
if (CSettingsProvider::This().ShowJPEGComments() && sComment != NULL && sComment[0] != 0) {
Expand Down
8 changes: 6 additions & 2 deletions src/JPEGView/EXIFReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ bool CEXIFReader::ParseDateString(SYSTEMTIME & date, const CString& str) {
return false;
}

CEXIFReader::CEXIFReader(void* pApp1Block)
CEXIFReader::CEXIFReader(void* pApp1Block, EImageFormat eImageFormat)
: m_exposureTime(0, 0) {

memset(&m_acqDate, 0, sizeof(SYSTEMTIME));
Expand Down Expand Up @@ -297,7 +297,11 @@ CEXIFReader::CEXIFReader(void* pApp1Block)
m_pLastIFD0 = pLastIFD0;

// image orientation
uint8* pTagOrientation = FindTag(pIFD0, pLastIFD0, 0x112, bLittleEndian);
uint8* pTagOrientation = NULL;
// orientation tags must be ignored for JXL, they are taken care of by the decoder
if (eImageFormat != IF_JXL) {
pTagOrientation = FindTag(pIFD0, pLastIFD0, 0x112, bLittleEndian);
}
if (pTagOrientation != NULL) {
m_nImageOrientation = ReadShortTag(pTagOrientation, bLittleEndian);
}
Expand Down
2 changes: 1 addition & 1 deletion src/JPEGView/EXIFReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class CEXIFReader {
// The pApp1Block must point to the APP1 block of the EXIF data, including the APP1 block marker
// The class does not take ownership of the memory (no copy made), thus the APP1 block must not be deleted
// while the EXIF reader class is deleted.
CEXIFReader(void* pApp1Block);
CEXIFReader(void* pApp1Block, EImageFormat eImageFormat);
~CEXIFReader(void);

// Parse date string in the EXIF date/time format
Expand Down
16 changes: 16 additions & 0 deletions src/JPEGView/HEIFWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ void * HeifReader::ReadImage(int &width,
int &height,
int &nchannels,
int &frame_count,
void* &exif_chunk,
bool &outOfMemory,
int frame_index,
const void *buffer,
Expand All @@ -18,6 +19,7 @@ void * HeifReader::ReadImage(int &width,
nchannels = 4;

unsigned char* pPixelData = NULL;
exif_chunk = NULL;

heif::Context context;
context.read_from_memory_without_copy(buffer, sizebytes);
Expand Down Expand Up @@ -69,5 +71,19 @@ void * HeifReader::ReadImage(int &width,
}
ICCProfileTransform::DeleteTransform(transform);

std::vector<heif_item_id> exif_blocks = handle.get_list_of_metadata_block_IDs("Exif");

if (!exif_blocks.empty()) {
std::vector<uint8_t> exif = handle.get_metadata(exif_blocks[0]);
if (exif.size() > 8 && exif.size() < 65538) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where's the magic 65538 number come from? I see it in other EXIF chunks of the code, where size+some number is 65538. It's an odd number, not a power of 2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These libraries all have their own ideas about where to start the Exif data from. JPEG Exif blocks are in the format FF E1 SS SS 45 78 69 66 00 00 [data]. The SS SS is a big-endian unsigned short representing the size of everything after FF E1.

libjxl gives 00 00 00 00 [data], libheif gives 00 00 00 00 45 78 69 66 00 00 [data], libavif, libwebp and libpng give [data], so they have different limits for size. If you want I can change it to 65536 + an offset and add notes explaining why.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, now it makes sense. Regarding the documentation, I'll just refer to this comment. This is good investigative information.

exif_chunk = malloc(exif.size());
if (exif_chunk != NULL) {
memcpy(exif_chunk, exif.data(), exif.size());
*((unsigned short*)exif_chunk) = _byteswap_ushort(0xFFE1);
*((unsigned short*)exif_chunk + 1) = _byteswap_ushort(exif.size() - 2);
}
}
}

return (void*)pPixelData;
}
1 change: 1 addition & 0 deletions src/JPEGView/HEIFWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class HeifReader
int &height, // height of the image loaded.
int &bpp, // BYTES (not bits) PER PIXEL.
int &frame_count, // number of top-level images
void* &exif_chunk, // Pointer to Exif data (must be freed by caller)
bool &outOfMemory, // set to true when no memory to read image
int frame_index, // index of requested frame
const void *buffer, // memory address containing heic compressed data.
Expand Down
30 changes: 20 additions & 10 deletions src/JPEGView/ImageLoadThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,8 @@ void CImageLoadThread::ProcessReadWEBPRequest(CRequest * request) {
int nFrameCount = 1;
int nFrameTimeMs = 0;
int nBPP;
uint8* pPixelData = (uint8*)WebpReaderWriter::ReadImage(nWidth, nHeight, nBPP, bHasAnimation, nFrameCount, nFrameTimeMs, request->OutOfMemory, pBuffer, nFileSize);
void* pEXIFData;
uint8* pPixelData = (uint8*)WebpReaderWriter::ReadImage(nWidth, nHeight, nBPP, bHasAnimation, nFrameCount, nFrameTimeMs, pEXIFData, request->OutOfMemory, pBuffer, nFileSize);
if (pPixelData && nBPP == 4) {
// Multiply alpha value into each AABBGGRR pixel
uint32* pImage32 = (uint32*)pPixelData;
Expand All @@ -648,7 +649,8 @@ void CImageLoadThread::ProcessReadWEBPRequest(CRequest * request) {
if (bHasAnimation) {
m_sLastWebpFileName = sFileName;
}
request->Image = new CJPEGImage(nWidth, nHeight, pPixelData, NULL, nBPP, 0, IF_WEBP, bHasAnimation, request->FrameIndex, nFrameCount, nFrameTimeMs);
request->Image = new CJPEGImage(nWidth, nHeight, pPixelData, pEXIFData, nBPP, 0, IF_WEBP, bHasAnimation, request->FrameIndex, nFrameCount, nFrameTimeMs);
free(pEXIFData);
}
else {
delete[] pPixelData;
Expand Down Expand Up @@ -710,10 +712,11 @@ void CImageLoadThread::ProcessReadPNGRequest(CRequest* request) {
int nWidth, nHeight, nBPP, nFrameCount, nFrameTimeMs;
bool bHasAnimation;
uint8* pPixelData = NULL;
void* pEXIFData;

// 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))
pPixelData = (uint8*)PngReader::ReadImage(nWidth, nHeight, nBPP, bHasAnimation, nFrameCount, nFrameTimeMs, request->OutOfMemory, pBuffer, nFileSize);
pPixelData = (uint8*)PngReader::ReadImage(nWidth, nHeight, nBPP, bHasAnimation, nFrameCount, nFrameTimeMs, pEXIFData, request->OutOfMemory, pBuffer, nFileSize);

if (pPixelData != NULL) {
if (bHasAnimation)
Expand All @@ -723,7 +726,8 @@ void CImageLoadThread::ProcessReadPNGRequest(CRequest* request) {
for (int i = 0; i < nWidth * nHeight; i++)
*pImage32++ = WebpAlphaBlendBackground(*pImage32, CSettingsProvider::This().ColorTransparency());

request->Image = new CJPEGImage(nWidth, nHeight, pPixelData, NULL, 4, 0, IF_PNG, bHasAnimation, request->FrameIndex, nFrameCount, nFrameTimeMs);
request->Image = new CJPEGImage(nWidth, nHeight, pPixelData, pEXIFData, 4, 0, IF_PNG, bHasAnimation, request->FrameIndex, nFrameCount, nFrameTimeMs);
free(pEXIFData);
bSuccess = true;
}
else {
Expand Down Expand Up @@ -787,7 +791,8 @@ void CImageLoadThread::ProcessReadJXLRequest(CRequest* request) {
if (bUseCachedDecoder || (::ReadFile(hFile, pBuffer, nFileSize, (LPDWORD)&nNumBytesRead, NULL) && nNumBytesRead == nFileSize)) {
int nWidth, nHeight, nBPP, nFrameCount, nFrameTimeMs;
bool bHasAnimation;
uint8* pPixelData = (uint8*)JxlReader::ReadImage(nWidth, nHeight, nBPP, bHasAnimation, nFrameCount, nFrameTimeMs, request->OutOfMemory, pBuffer, nFileSize);
void* pEXIFData;
uint8* pPixelData = (uint8*)JxlReader::ReadImage(nWidth, nHeight, nBPP, bHasAnimation, nFrameCount, nFrameTimeMs, pEXIFData, request->OutOfMemory, pBuffer, nFileSize);
if (pPixelData != NULL) {
if (bHasAnimation)
m_sLastJxlFileName = sFileName;
Expand All @@ -796,7 +801,8 @@ void CImageLoadThread::ProcessReadJXLRequest(CRequest* request) {
for (int i = 0; i < nWidth * nHeight; i++)
*pImage32++ = WebpAlphaBlendBackground(*pImage32, CSettingsProvider::This().ColorTransparency());

request->Image = new CJPEGImage(nWidth, nHeight, pPixelData, NULL, 4, 0, IF_JXL, bHasAnimation, request->FrameIndex, nFrameCount, nFrameTimeMs);
request->Image = new CJPEGImage(nWidth, nHeight, pPixelData, pEXIFData, 4, 0, IF_JXL, bHasAnimation, request->FrameIndex, nFrameCount, nFrameTimeMs);
free(pEXIFData);
} else {
DeleteCachedJxlDecoder();
}
Expand Down Expand Up @@ -858,8 +864,9 @@ void CImageLoadThread::ProcessReadAVIFRequest(CRequest* request) {
if (bUseCachedDecoder || (::ReadFile(hFile, pBuffer, nFileSize, (LPDWORD)&nNumBytesRead, NULL) && nNumBytesRead == nFileSize)) {
int nWidth, nHeight, nBPP, nFrameCount, nFrameTimeMs;
bool bHasAnimation;
void* pEXIFData;
uint8* pPixelData = (uint8*)AvifReader::ReadImage(nWidth, nHeight, nBPP, bHasAnimation, request->FrameIndex,
nFrameCount, nFrameTimeMs, request->OutOfMemory, pBuffer, nFileSize);
nFrameCount, nFrameTimeMs, pEXIFData, request->OutOfMemory, pBuffer, nFileSize);
if (pPixelData != NULL) {
if (bHasAnimation)
m_sLastAvifFileName = sFileName;
Expand All @@ -868,7 +875,8 @@ void CImageLoadThread::ProcessReadAVIFRequest(CRequest* request) {
for (int i = 0; i < nWidth * nHeight; i++)
*pImage32++ = WebpAlphaBlendBackground(*pImage32, CSettingsProvider::This().ColorTransparency());

request->Image = new CJPEGImage(nWidth, nHeight, pPixelData, NULL, 4, 0, IF_AVIF, bHasAnimation, request->FrameIndex, nFrameCount, nFrameTimeMs);
request->Image = new CJPEGImage(nWidth, nHeight, pPixelData, pEXIFData, 4, 0, IF_AVIF, bHasAnimation, request->FrameIndex, nFrameCount, nFrameTimeMs);
free(pEXIFData);
bSuccess = true;
} else {
DeleteCachedAvifDecoder();
Expand Down Expand Up @@ -920,14 +928,16 @@ void CImageLoadThread::ProcessReadHEIFRequest(CRequest* request) {
int nWidth, nHeight, nBPP, nFrameCount, nFrameTimeMs;
nFrameCount = 1;
nFrameTimeMs = 0;
uint8* pPixelData = (uint8*)HeifReader::ReadImage(nWidth, nHeight, nBPP, nFrameCount, request->OutOfMemory, request->FrameIndex, pBuffer, nFileSize);
void* pEXIFData;
uint8* pPixelData = (uint8*)HeifReader::ReadImage(nWidth, nHeight, nBPP, nFrameCount, pEXIFData, request->OutOfMemory, request->FrameIndex, pBuffer, nFileSize);
if (pPixelData != NULL) {
// Multiply alpha value into each AABBGGRR pixel
uint32* pImage32 = (uint32*)pPixelData;
for (int i = 0; i < nWidth * nHeight; i++)
*pImage32++ = WebpAlphaBlendBackground(*pImage32, CSettingsProvider::This().ColorTransparency());

request->Image = new CJPEGImage(nWidth, nHeight, pPixelData, NULL, nBPP, 0, IF_HEIF, false, request->FrameIndex, nFrameCount, nFrameTimeMs);
request->Image = new CJPEGImage(nWidth, nHeight, pPixelData, pEXIFData, nBPP, 0, IF_HEIF, false, request->FrameIndex, nFrameCount, nFrameTimeMs);
free(pEXIFData);
}
}
} catch(heif::Error he) {
Expand Down
2 changes: 1 addition & 1 deletion src/JPEGView/JPEGImage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ CJPEGImage::CJPEGImage(int nWidth, int nHeight, void* pPixels, void* pEXIFData,
m_nEXIFSize = pEXIF[2]*256 + pEXIF[3] + 2;
m_pEXIFData = new char[m_nEXIFSize];
memcpy(m_pEXIFData, pEXIFData, m_nEXIFSize);
m_pEXIFReader = new CEXIFReader(m_pEXIFData);
m_pEXIFReader = new CEXIFReader(m_pEXIFData, eImageFormat);
} else {
m_nEXIFSize = 0;
m_pEXIFData = NULL;
Expand Down
42 changes: 42 additions & 0 deletions src/JPEGView/JXLWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ struct JxlReader::jxl_cache {
int width;
int height;
void* transform;
std::vector<uint8_t> exif;
};

JxlReader::jxl_cache JxlReader::cache = { 0 };

// based on https://github.com/libjxl/libjxl/blob/main/examples/decode_oneshot.cc
// and https://github.com/libjxl/libjxl/blob/main/examples/decode_exif_metadata.cc
bool JxlReader::DecodeJpegXlOneShot(const uint8_t* jxl, size_t size, std::vector<uint8_t>* pixels, int& xsize,
int& ysize, bool& have_animation, int& frame_count, int& frame_time, std::vector<uint8_t>* icc_profile, bool& outOfMemory) {

Expand All @@ -33,11 +35,15 @@ bool JxlReader::DecodeJpegXlOneShot(const uint8_t* jxl, size_t size, std::vector
if (JXL_DEC_SUCCESS !=
JxlDecoderSubscribeEvents(cache.decoder.get(), JXL_DEC_BASIC_INFO |
JXL_DEC_COLOR_ENCODING |
JXL_DEC_BOX |
JXL_DEC_FRAME |
JXL_DEC_FULL_IMAGE)) {
return false;
}

if (JXL_DEC_SUCCESS != JxlDecoderSetDecompressBoxes(cache.decoder.get(), JXL_TRUE)) {
return false;
}

if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(cache.decoder.get(),
JxlResizableParallelRunner,
Expand All @@ -53,6 +59,8 @@ bool JxlReader::DecodeJpegXlOneShot(const uint8_t* jxl, size_t size, std::vector

JxlBasicInfo info;
JxlPixelFormat format = { 4, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0 };
const constexpr size_t kChunkSize = 65536;
size_t output_pos = 0;

bool loop_check = false;
for (;;) {
Expand Down Expand Up @@ -139,6 +147,26 @@ bool JxlReader::DecodeJpegXlOneShot(const uint8_t* jxl, size_t size, std::vector
JxlDecoderSubscribeEvents(cache.decoder.get(), JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE);
JxlDecoderSetInput(cache.decoder.get(), cache.data, cache.data_size);
JxlDecoderCloseInput(cache.decoder.get());
} else if (status == JXL_DEC_BOX) {
if (!cache.exif.empty()) {
size_t remaining = JxlDecoderReleaseBoxBuffer(cache.decoder.get());
cache.exif.resize(cache.exif.size() - remaining);
} else {
JxlBoxType type;
if (JXL_DEC_SUCCESS !=
JxlDecoderGetBoxType(cache.decoder.get(), type, true)) {
return false;
}
if (!memcmp(type, "Exif", 4)) {
cache.exif.resize(kChunkSize);
JxlDecoderSetBoxBuffer(cache.decoder.get(), cache.exif.data(), cache.exif.size());
}
}
} else if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) {
size_t remaining = JxlDecoderReleaseBoxBuffer(cache.decoder.get());
output_pos += kChunkSize - remaining;
cache.exif.resize(cache.exif.size() + kChunkSize);
JxlDecoderSetBoxBuffer(cache.decoder.get(), cache.exif.data() + output_pos, cache.exif.size() - output_pos);
} else {
return false;
}
Expand All @@ -151,6 +179,7 @@ void* JxlReader::ReadImage(int& width,
bool& has_animation,
int& frame_count,
int& frame_time,
void*& exif_chunk,
bool& outOfMemory,
const void* buffer,
int sizebytes)
Expand All @@ -160,6 +189,7 @@ void* JxlReader::ReadImage(int& width,
nchannels = 4;
has_animation = false;
unsigned char* pPixelData = NULL;
exif_chunk = NULL;


std::vector<uint8_t> pixels;
Expand All @@ -182,8 +212,20 @@ void* JxlReader::ReadImage(int& width,
for (uint32_t* i = (uint32_t*)pPixelData; (uint8_t*)i < pPixelData + size; i++)
*i = ((*i & 0x00FF0000) >> 16) | ((*i & 0x0000FF00)) | ((*i & 0x000000FF) << 16) | ((*i & 0xFF000000));
}

// Copy Exif data into the format understood by CEXIFReader
if (!cache.exif.empty() && cache.exif.size() > 8 && cache.exif.size() < 65532) {
exif_chunk = malloc(cache.exif.size() + 6);
if (exif_chunk != NULL) {
memcpy(exif_chunk, "\xFF\xE1\0\0Exif\0\0", 10);
*((unsigned short*)exif_chunk + 1) = _byteswap_ushort(cache.exif.size() + 4);
memcpy((uint8_t*)exif_chunk + 10, cache.exif.data() + 4, cache.exif.size() - 4);
}
}

if (!has_animation)
DeleteCache();

return pPixelData;
}

Expand Down
1 change: 1 addition & 0 deletions src/JPEGView/JXLWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class JxlReader
bool& has_animation, // if the image is animated
int& frame_count, // number of frames
int& frame_time, // frame duration in milliseconds
void*& exif, // Pointer to Exif data (must be freed by caller)
bool& outOfMemory, // set to true when no memory to read image
const void* buffer, // memory address containing jxl compressed data.
int sizebytes); // size of jxl compressed data
Expand Down
Loading