Skip to content

Commit

Permalink
Merge pull request #259 from Tom94/apple-hdr-gainmap
Browse files Browse the repository at this point in the history
feat: support Apple HDR gain maps in HEIC/AVIF images
  • Loading branch information
Tom94 authored Feb 25, 2025
2 parents a9a7611 + c00775a commit 95121dd
Show file tree
Hide file tree
Showing 10 changed files with 636 additions and 143 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@
path = dependencies/Little-CMS
url = https://github.com/mm2/Little-CMS
shallow = true
[submodule "dependencies/libexif"]
path = dependencies/libexif
url = https://github.com/francois-random/libexif
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines")
endif()

set(TEV_LIBS clip OpenEXR::OpenEXR nanogui ${NANOGUI_EXTRA_LIBS})
set(TEV_LIBS clip exif_static OpenEXR::OpenEXR nanogui ${NANOGUI_EXTRA_LIBS})
if (MSVC)
set(TEV_LIBS ${TEV_LIBS} zlibstatic DirectXTex wsock32 ws2_32)
endif()
Expand All @@ -175,10 +175,12 @@ if (TEV_USE_LIBHEIF)
endif()

set(TEV_SOURCES
include/tev/imageio/AppleMakerNote.h src/imageio/AppleMakerNote.cpp
include/tev/imageio/ClipboardImageLoader.h src/imageio/ClipboardImageLoader.cpp
include/tev/imageio/EmptyImageLoader.h src/imageio/EmptyImageLoader.cpp
include/tev/imageio/ExrImageLoader.h src/imageio/ExrImageLoader.cpp
include/tev/imageio/ExrImageSaver.h src/imageio/ExrImageSaver.cpp
include/tev/imageio/GainMap.h src/imageio/GainMap.cpp
include/tev/imageio/ImageLoader.h src/imageio/ImageLoader.cpp
include/tev/imageio/ImageSaver.h src/imageio/ImageSaver.cpp
include/tev/imageio/PfmImageLoader.h src/imageio/PfmImageLoader.cpp
Expand Down Expand Up @@ -241,6 +243,7 @@ include_directories(
${ARGS_INCLUDE}
${CLIP_INCLUDE}
${DIRECTXTEX_INCLUDE}
${EXIF_INCLUDE}
${FMT_INCLUDE}
${GLFW_INCLUDE}
${NANOGUI_EXTRA_INCS}
Expand Down
13 changes: 6 additions & 7 deletions dependencies/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,7 @@ if (TEV_USE_LIBHEIF)
# Little-CMS/plugins/fast_float/src/*.c Little-CMS/plugins/fast_float/include/*.h
)

file(GLOB LCMS_HDRS
Little-CMS/include/*.h
# Little-CMS/plugins/fast_float/include/*.h
)

add_library(lcms2 STATIC ${LCMS_SRCS} ${LCMS_HDRS})
set_target_properties(lcms2 PROPERTIES PUBLIC_HEADER "${LCMS_HDRS}")
add_library(lcms2 STATIC ${LCMS_SRCS})

# We don't use the SSE2 components of CMS in tev; disable to simplify ARM compilation
target_compile_definitions(lcms2 PRIVATE -DCMS_DONT_USE_SSE2=1)
Expand All @@ -146,6 +140,9 @@ if (TEV_USE_LIBHEIF)
)
endif()

# Compile libexif for Exif metadata parsing
add_subdirectory(libexif EXCLUDE_FROM_ALL)

# Compile OpenEXR
set(IMATH_INSTALL OFF CACHE BOOL " " FORCE)
set(IMATH_INSTALL_PKG_CONFIG OFF CACHE BOOL " " FORCE)
Expand Down Expand Up @@ -180,6 +177,8 @@ set(ARGS_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/args PARENT_SCOPE)

set(CLIP_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/clip PARENT_SCOPE)

set(EXIF_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/libexif PARENT_SCOPE)

set(FMT_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/fmt/include PARENT_SCOPE)

if (NOT ${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
Expand Down
1 change: 1 addition & 0 deletions dependencies/libexif
Submodule libexif added at 38678a
2 changes: 2 additions & 0 deletions include/tev/Image.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ struct ImageData {
ImageData() = default;
ImageData(const ImageData&) = delete;
ImageData(ImageData&&) = default;
ImageData& operator=(const ImageData&) = delete;
ImageData& operator=(ImageData&&) = default;

std::vector<Channel> channels;
std::vector<std::string> layers;
Expand Down
137 changes: 137 additions & 0 deletions include/tev/imageio/AppleMakerNote.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* tev -- the EXR viewer
*
* Copyright (C) 2025 Thomas Müller <contact@tom94.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <tev/Common.h>

#include <cstdint>
#include <map>
#include <stdexcept>
#include <vector>

namespace tev {

bool isAppleMakernote(const uint8_t* data, size_t length);

template <typename T> T read(const uint8_t* data, bool reverseEndianness) {
if (reverseEndianness) {
T result;
for (size_t i = 0; i < sizeof(T); ++i) {
reinterpret_cast<uint8_t*>(&result)[i] = data[sizeof(T) - i - 1];
}

return result;
} else {
return *reinterpret_cast<const T*>(data);
}
}

struct AppleMakerNoteEntry {
enum class EFormat : uint16_t {
Byte = 1,
Ascii = 2,
Short = 3,
Long = 4,
Rational = 5,
Sbyte = 6,
Undefined = 7,
Sshort = 8,
Slong = 9,
Srational = 10,
Float = 11,
Double = 12,
};

uint16_t tag;
EFormat format;
uint32_t nComponents;
std::vector<uint8_t> data;

static size_t formatSize(EFormat format) {
switch (format) {
case EFormat::Byte:
case EFormat::Ascii:
case EFormat::Sbyte:
case EFormat::Undefined: return 1;
case EFormat::Short:
case EFormat::Sshort: return 2;
case EFormat::Long:
case EFormat::Slong:
case EFormat::Float: return 4;
case EFormat::Rational:
case EFormat::Srational:
case EFormat::Double: return 8;
default:
// The default size of 4 for unknown types is chosen to make parsing easier. Larger types would be stored at a remote
// location with the 4 bytes interpreted as an offset, which may be invalid depending on the indended behavior of the
// unknown type. Better play it safe and just read 4 bytes, leaving it to the user to know whether they represent an offset
// or a meaningful value by themselves.
return 4;
}

throw std::invalid_argument{std::string{"Unknown format: "} + std::to_string((uint32_t)format)};
}

size_t size() const { return nComponents * formatSize(format); }
};

class AppleMakerNote {
public:
AppleMakerNote(const uint8_t* data, size_t length);

template <typename T> T getFloat(uint16_t tag) const {
if (mTags.count(tag) == 0) {
throw std::invalid_argument{"Requested tag does not exist."};
}

const auto& entry = mTags.at(tag);
const uint8_t* data = entry.data.data();

switch (entry.format) {
case AppleMakerNoteEntry::EFormat::Byte: return static_cast<T>(*data);
case AppleMakerNoteEntry::EFormat::Short: return static_cast<T>(read<uint16_t>(data, mReverseEndianess));
case AppleMakerNoteEntry::EFormat::Long: return static_cast<T>(read<uint32_t>(data, mReverseEndianess));
case AppleMakerNoteEntry::EFormat::Rational: {
uint32_t numerator = read<uint32_t>(data, mReverseEndianess);
uint32_t denominator = read<uint32_t>(data + sizeof(uint32_t), mReverseEndianess);
return static_cast<T>(numerator) / static_cast<T>(denominator);
}
case AppleMakerNoteEntry::EFormat::Sbyte: return static_cast<T>(*reinterpret_cast<const int8_t*>(data));
case AppleMakerNoteEntry::EFormat::Sshort: return static_cast<T>(read<int16_t>(data, mReverseEndianess));
case AppleMakerNoteEntry::EFormat::Slong: return static_cast<T>(read<int32_t>(data, mReverseEndianess));
case AppleMakerNoteEntry::EFormat::Srational: {
int32_t numerator = read<int32_t>(data, mReverseEndianess);
int32_t denominator = read<int32_t>(data + sizeof(int32_t), mReverseEndianess);
return static_cast<T>(numerator) / static_cast<T>(denominator);
}
case AppleMakerNoteEntry::EFormat::Float: return static_cast<T>(*reinterpret_cast<const float*>(data));
case AppleMakerNoteEntry::EFormat::Double: return static_cast<T>(*reinterpret_cast<const double*>(data));
case AppleMakerNoteEntry::EFormat::Ascii:
case AppleMakerNoteEntry::EFormat::Undefined: throw std::invalid_argument{"Cannot convert this format to float."};
}

throw std::invalid_argument{"Unknown format."};
}

private:
std::map<uint8_t, AppleMakerNoteEntry> mTags;
bool mReverseEndianess = false;
};

} // namespace tev
33 changes: 33 additions & 0 deletions include/tev/imageio/GainMap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* tev -- the EXR viewer
*
* Copyright (C) 2025 Thomas Müller <contact@tom94.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <tev/Common.h>
#include <tev/Image.h>
#include <tev/Task.h>

class AppleMakerNote;

namespace tev {

Task<void> applyAppleGainMap(ImageData& image, const ImageData& gainMap, int priority, const AppleMakerNote& amn);

}


96 changes: 96 additions & 0 deletions src/imageio/AppleMakerNote.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* tev -- the EXR viewer
*
* Copyright (C) 2025 Thomas Müller <contact@tom94.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include <tev/Common.h>
#include <tev/imageio/AppleMakerNote.h>

using namespace std;

namespace tev {

static const uint8_t APPLE_SIGNATURE[] = {0x41, 0x70, 0x70, 0x6C, 0x65, 0x20, 0x69, 0x4F, 0x53, 0x00}; // "Apple iOS\0"
static const size_t SIG_LENGTH = sizeof(APPLE_SIGNATURE);

bool isAppleMakernote(const uint8_t* data, size_t length) {

if (length < SIG_LENGTH) {
return false;
}

return memcmp(data, APPLE_SIGNATURE, SIG_LENGTH) == 0;
}

// This whole function is one huge hack. It was pieced together by referencing the EXIF spec as well as the (non-functional) implementation
// over at libexif. https://github.com/libexif/libexif/blob/master/libexif/apple/exif-mnote-data-apple.c That, plus quite a bit of trial and
// error, finally got this to work. Who knows when Apple will break it. :)
AppleMakerNote::AppleMakerNote(const uint8_t* data, size_t length) {
mReverseEndianess = false;

size_t ofs = 0;
if ((data[ofs + 12] == 'M') && (data[ofs + 13] == 'M')) {
mReverseEndianess = std::endian::little == std::endian::native;
} else if ((data[ofs + 12] == 'I') && (data[ofs + 13] == 'I')) {
mReverseEndianess = std::endian::big == std::endian::native;
} else {
throw invalid_argument{"Failed to determine byte order."};
}

uint32_t tcount = read<uint16_t>(data + ofs + 14, mReverseEndianess);

if (length < ofs + 16 + tcount * 12 + 4) {
throw invalid_argument{"Too short"};
}

ofs += 16;

for (uint32_t i = 0; i < tcount; i++) {
if (ofs + 12 > length) {
throw invalid_argument{"Overflow"};
}

AppleMakerNoteEntry entry;
entry.tag = read<uint16_t>(data + ofs, mReverseEndianess);
entry.format = read<AppleMakerNoteEntry::EFormat>(data + ofs + 2, mReverseEndianess);
entry.nComponents = read<uint32_t>(data + ofs + 4, mReverseEndianess);

if (ofs + 4 + entry.size() > length) {
throw invalid_argument{"Elem overflow"};
}

size_t entryOffset;
if (entry.size() > 4) {
// Entry is stored somewhere else, pointed to by the following
entryOffset = read<uint32_t>(data + ofs + 8, mReverseEndianess); // -6?
} else {
entryOffset = ofs + 8;
}

entry.data = vector<uint8_t>(data + entryOffset, data + entryOffset + entry.size());

if (entryOffset + entry.size() > length) {
throw invalid_argument{"Offset overflow"};
}

ofs += 12;
mTags[entry.tag] = entry;

tlog::debug() << fmt::format("Tag: {} Format: {} Components: {}", entry.tag, (int)entry.format, entry.nComponents);
}
}

} // namespace tev
Loading

0 comments on commit 95121dd

Please sign in to comment.