diff --git a/thirdparty/README.md b/thirdparty/README.md index dcd9ce2ca6..b8faf70151 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -1100,12 +1100,15 @@ Patches: ## tinyexr - Upstream: https://github.com/syoyo/tinyexr -- Version: 1.0.12 (735ff73ce5959cf005eb99ce517c9bcecab89dfb, 2025) +- Version: 1.0.13 (4946b5d92e13bcc8102ac2c8efd129596a90bf75, 2026) - License: BSD-3-Clause Files extracted from upstream source: +- `exr_reader.hh` +- `streamreader.hh` - `tinyexr.{cc,h}` +- `LICENSE` Patches: diff --git a/thirdparty/tinyexr/LICENSE b/thirdparty/tinyexr/LICENSE new file mode 100644 index 0000000000..d89add6035 --- /dev/null +++ b/thirdparty/tinyexr/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2014 - 2021, Syoyo Fujita and many contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/thirdparty/tinyexr/exr_reader.hh b/thirdparty/tinyexr/exr_reader.hh new file mode 100644 index 0000000000..8ea0d666fd --- /dev/null +++ b/thirdparty/tinyexr/exr_reader.hh @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025, Syoyo Fujita and many contributors. +// All rights reserved. +// +// EXR Reader: Reader class with error stack for safe memory reading + +#ifndef TINYEXR_EXR_READER_HH_ +#define TINYEXR_EXR_READER_HH_ + +#include +#include +#include "streamreader.hh" + +namespace tinyexr { + +// Reader class that wraps StreamReader and accumulates errors +class Reader { +public: + Reader(const uint8_t* data, size_t length, Endian endian = Endian::Little) + : stream_(data, length, endian), has_error_(false) {} + + // Check if any errors have occurred + bool has_error() const { return has_error_; } + + // Get all accumulated errors + const std::vector& errors() const { return errors_; } + + // Get the most recent error + std::string last_error() const { + return errors_.empty() ? "" : errors_.back(); + } + + // Get all errors as a single string + std::string all_errors() const { + std::string result; + for (size_t i = 0; i < errors_.size(); i++) { + if (i > 0) result += "\n"; + result += errors_[i]; + } + return result; + } + + // Clear error stack + void clear_errors() { + errors_.clear(); + has_error_ = false; + } + + // Read n bytes into destination buffer + bool read(size_t n, uint8_t* dst) { + if (!stream_.read(n, dst)) { + add_error("Failed to read " + std::to_string(n) + " bytes at position " + + std::to_string(stream_.tell())); + return false; + } + return true; + } + + // Read 1 byte + bool read1(uint8_t* dst) { + if (!stream_.read1(dst)) { + add_error("Failed to read 1 byte at position " + + std::to_string(stream_.tell())); + return false; + } + return true; + } + + // Read 2 bytes with endian swap + bool read2(uint16_t* dst) { + if (!stream_.read2(dst)) { + add_error("Failed to read 2 bytes at position " + + std::to_string(stream_.tell())); + return false; + } + return true; + } + + // Read 4 bytes with endian swap + bool read4(uint32_t* dst) { + if (!stream_.read4(dst)) { + add_error("Failed to read 4 bytes at position " + + std::to_string(stream_.tell())); + return false; + } + return true; + } + + // Read 8 bytes with endian swap + bool read8(uint64_t* dst) { + if (!stream_.read8(dst)) { + add_error("Failed to read 8 bytes at position " + + std::to_string(stream_.tell())); + return false; + } + return true; + } + + // Read a null-terminated string up to max_len bytes + // Returns false if no null terminator found within max_len + bool read_string(std::string* str, size_t max_len = 256) { + if (!str) { + add_error("Null pointer passed to read_string"); + return false; + } + + str->clear(); + size_t start_pos = stream_.tell(); + + for (size_t i = 0; i < max_len; i++) { + uint8_t c; + if (!stream_.read1(&c)) { + add_error("Failed to read string at position " + std::to_string(start_pos)); + return false; + } + if (c == '\0') { + return true; + } + str->push_back(static_cast(c)); + } + + add_error("String not null-terminated within " + std::to_string(max_len) + + " bytes at position " + std::to_string(start_pos)); + return false; + } + + // Seek to absolute position + bool seek(size_t pos) { + if (!stream_.seek(pos)) { + add_error("Failed to seek to position " + std::to_string(pos)); + return false; + } + return true; + } + + // Seek relative to current position + bool seek_relative(int64_t offset) { + size_t current = stream_.tell(); + int64_t new_pos = static_cast(current) + offset; + + if (new_pos < 0) { + add_error("Seek would move before start of stream"); + return false; + } + + return seek(static_cast(new_pos)); + } + + // Rewind to beginning + void rewind() { + stream_.rewind(); + } + + // Get current position + size_t tell() const { + return stream_.tell(); + } + + // Get remaining bytes + size_t remaining() const { + return stream_.remaining(); + } + + // Check if at end + bool eof() const { + return stream_.eof(); + } + + // Get total length + size_t length() const { + return stream_.length(); + } + + // Add a custom error message + void add_error(const std::string& msg) { + errors_.push_back(msg); + has_error_ = true; + } + + // Get direct access to underlying StreamReader (use with caution) + const StreamReader& stream() const { return stream_; } + +private: + StreamReader stream_; + std::vector errors_; + bool has_error_; +}; + +} // namespace tinyexr + +#endif // TINYEXR_EXR_READER_HH_ diff --git a/thirdparty/tinyexr/streamreader.hh b/thirdparty/tinyexr/streamreader.hh new file mode 100644 index 0000000000..3dbf278b37 --- /dev/null +++ b/thirdparty/tinyexr/streamreader.hh @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025, Syoyo Fujita and many contributors. +// All rights reserved. +// +// StreamReader: Simple header-only stream reader with endian support +// +// Part of TinyEXR V2 API (EXPERIMENTAL) + +#ifndef TINYEXR_STREAMREADER_HH_ +#define TINYEXR_STREAMREADER_HH_ + +#include +#include + +namespace tinyexr { + +enum class Endian { + Little, + Big, + Native +}; + +class StreamReader { +public: + // Constructor: takes memory address, length, and endianness + StreamReader(const uint8_t* data, size_t length, Endian endian = Endian::Little) + : data_(data), length_(length), pos_(0), endian_(endian) { + // Detect native endian + uint16_t test = 0x0001; + native_is_little_ = (*reinterpret_cast(&test) == 0x01); + + // Determine if we need to swap bytes + if (endian_ == Endian::Native) { + needs_swap_ = false; + } else { + bool data_is_little = (endian_ == Endian::Little); + needs_swap_ = (data_is_little != native_is_little_); + } + } + + // Read n bytes into destination buffer + // Returns false on out-of-bounds or error + bool read(size_t n, uint8_t* dst) { + if (!dst || n == 0) { + return false; + } + if (pos_ + n > length_) { + return false; // Out of bounds + } + std::memcpy(dst, data_ + pos_, n); + pos_ += n; + return true; + } + + // Read 1 byte (uint8_t) + bool read1(uint8_t* dst) { + return read(1, dst); + } + + // Read 2 bytes (uint16_t) with endian swap if needed + bool read2(uint16_t* dst) { + if (!dst) { + return false; + } + uint8_t buf[2]; + if (!read(2, buf)) { + return false; + } + + if (needs_swap_) { + *dst = static_cast(buf[1]) << 8 | + static_cast(buf[0]); + } else { + std::memcpy(dst, buf, 2); + } + return true; + } + + // Read 4 bytes (uint32_t) with endian swap if needed + bool read4(uint32_t* dst) { + if (!dst) { + return false; + } + uint8_t buf[4]; + if (!read(4, buf)) { + return false; + } + + if (needs_swap_) { + *dst = static_cast(buf[3]) << 24 | + static_cast(buf[2]) << 16 | + static_cast(buf[1]) << 8 | + static_cast(buf[0]); + } else { + std::memcpy(dst, buf, 4); + } + return true; + } + + // Read 8 bytes (uint64_t) with endian swap if needed + bool read8(uint64_t* dst) { + if (!dst) { + return false; + } + uint8_t buf[8]; + if (!read(8, buf)) { + return false; + } + + if (needs_swap_) { + *dst = static_cast(buf[7]) << 56 | + static_cast(buf[6]) << 48 | + static_cast(buf[5]) << 40 | + static_cast(buf[4]) << 32 | + static_cast(buf[3]) << 24 | + static_cast(buf[2]) << 16 | + static_cast(buf[1]) << 8 | + static_cast(buf[0]); + } else { + std::memcpy(dst, buf, 8); + } + return true; + } + + // Seek to absolute position + // Returns false if position is out of bounds + bool seek(size_t pos) { + if (pos > length_) { + return false; + } + pos_ = pos; + return true; + } + + // Rewind to beginning + void rewind() { + pos_ = 0; + } + + // Get current position + size_t tell() const { + return pos_; + } + + // Get remaining bytes + size_t remaining() const { + return length_ - pos_; + } + + // Check if at end + bool eof() const { + return pos_ >= length_; + } + + // Get total length + size_t length() const { + return length_; + } + +private: + const uint8_t* data_; + size_t length_; + size_t pos_; + Endian endian_; + bool native_is_little_; + bool needs_swap_; +}; + +} // namespace tinyexr + +#endif // TINYEXR_STREAMREADER_HH_ diff --git a/thirdparty/tinyexr/tinyexr.h b/thirdparty/tinyexr/tinyexr.h index 37ca5b47ad..dfd7164900 100644 --- a/thirdparty/tinyexr/tinyexr.h +++ b/thirdparty/tinyexr/tinyexr.h @@ -212,6 +212,11 @@ extern "C" { #define TINYEXR_COMPRESSIONTYPE_ZIPS (2) #define TINYEXR_COMPRESSIONTYPE_ZIP (3) #define TINYEXR_COMPRESSIONTYPE_PIZ (4) +#define TINYEXR_COMPRESSIONTYPE_PXR24 (5) +#define TINYEXR_COMPRESSIONTYPE_B44 (6) +#define TINYEXR_COMPRESSIONTYPE_B44A (7) +#define TINYEXR_COMPRESSIONTYPE_DWAA (8) // Not yet supported +#define TINYEXR_COMPRESSIONTYPE_DWAB (9) // Not yet supported #define TINYEXR_COMPRESSIONTYPE_ZFP (128) // TinyEXR extension #define TINYEXR_ZFP_COMPRESSIONTYPE_RATE (0) @@ -225,6 +230,12 @@ extern "C" { #define TINYEXR_TILE_ROUND_DOWN (0) #define TINYEXR_TILE_ROUND_UP (1) +// Spectral EXR types (based on JCGT 2021 paper and spectral-exr format) +// https://jcgt.org/published/0010/03/01/ +#define TINYEXR_SPECTRUM_REFLECTIVE (0) // T.{wavelength}nm channels +#define TINYEXR_SPECTRUM_EMISSIVE (1) // S0.{wavelength}nm channels +#define TINYEXR_SPECTRUM_POLARISED (2) // S0-S3.{wavelength}nm channels + typedef struct TEXRVersion { int version; // this must be 2 // tile format image; @@ -633,6 +644,61 @@ extern int LoadEXRFromMemory(float **out_rgba, int *width, int *height, const unsigned char *memory, size_t size, const char **err); +// Spectral EXR API (based on JCGT 2021 paper and spectral-exr format) +// https://jcgt.org/published/0010/03/01/ +// https://github.com/afichet/spectral-exr + +// Check if an EXR file contains spectral data (has spectralLayoutVersion attribute) +// Returns TINYEXR_SUCCESS if spectral, TINYEXR_ERROR_INVALID_DATA if not +extern int IsSpectralEXR(const char *filename); +extern int IsSpectralEXRFromMemory(const unsigned char *memory, size_t size); + +// Get spectrum type from EXR header (TINYEXR_SPECTRUM_REFLECTIVE/EMISSIVE/POLARISED) +// Returns spectrum type, or -1 if not a spectral EXR +extern int EXRGetSpectrumType(const EXRHeader *exr_header); + +// Format wavelength with European decimal convention (comma as separator) +// Output format: "550,000000" for 550.0nm +// buffer must be at least 32 bytes +extern void EXRFormatWavelength(char *buffer, size_t buffer_size, float wavelength_nm); + +// Create spectral channel name +// For emissive: "S{stokes}.{wavelength}nm" (e.g., "S0.550,000000nm") +// For reflective: "T.{wavelength}nm" (e.g., "T.550,000000nm") +// buffer must be at least 64 bytes +extern void EXRSpectralChannelName(char *buffer, size_t buffer_size, + float wavelength_nm, int stokes_component); +extern void EXRReflectiveChannelName(char *buffer, size_t buffer_size, + float wavelength_nm); + +// Parse wavelength from spectral channel name +// Returns wavelength in nm, or -1.0 if not a valid spectral channel name +extern float EXRParseSpectralChannelWavelength(const char *channel_name); + +// Get Stokes component from channel name (0-3, or -1 if not polarised/invalid) +extern int EXRGetStokesComponent(const char *channel_name); + +// Check if channel name is a spectral channel (S{n}.{wavelength}nm or T.{wavelength}nm) +extern int EXRIsSpectralChannel(const char *channel_name); + +// Get wavelengths from EXR header channels +// Returns number of unique wavelengths found +// wavelengths array must be pre-allocated, max_wavelengths is its size +extern int EXRGetWavelengths(const EXRHeader *exr_header, + float *wavelengths, int max_wavelengths); + +// Helper to add spectral attributes to an EXRHeader +// This sets spectralLayoutVersion and ROOT/units (or emissiveUnits for emissive) +// spectrum_type: TINYEXR_SPECTRUM_REFLECTIVE, EMISSIVE, or POLARISED +// units: unit string (e.g., "W.m^-2.sr^-1.nm^-1" for radiance) +extern int EXRSetSpectralAttributes(EXRHeader *exr_header, + int spectrum_type, + const char *units); + +// Get spectral units from EXR header +// Returns NULL if not found, otherwise pointer to units string (valid until header is freed) +extern const char* EXRGetSpectralUnits(const EXRHeader *exr_header); + #ifdef __cplusplus } #endif @@ -666,6 +732,7 @@ extern int LoadEXRFromMemory(float **out_rgba, int *width, int *height, #endif #include +#include #include #include #include @@ -678,6 +745,9 @@ extern int LoadEXRFromMemory(float **out_rgba, int *width, int *height, #include #include +// Include Reader class with error stack for safer memory reading +#include "exr_reader.hh" + // https://stackoverflow.com/questions/5047971/how-do-i-check-for-c11-support #if __cplusplus > 199711L || (defined(_MSC_VER) && _MSC_VER >= 1900) #define TINYEXR_HAS_CXX11 (1) @@ -3426,6 +3496,1112 @@ static bool DecompressPiz(unsigned char *outPtr, const unsigned char *inPtr, } #endif // TINYEXR_USE_PIZ +// ============================================================================ +// PXR24 decompression +// ============================================================================ + +// PXR24 stores 32-bit floats as 24-bit values (truncates 8 mantissa bits) +// HALF and UINT are stored without modification +static bool DecompressPxr24(unsigned char *outPtr, size_t outBufSize, + const unsigned char *inPtr, size_t inLen, + int data_width, int num_lines, + size_t num_channels, + const EXRChannelInfo *channels) { + // Calculate the PXR24 data size after zlib decompression + // PXR24 stores HALF as 2 bytes, UINT as 4 bytes, FLOAT as 3 bytes + // Data is stored with byte plane separation and delta encoding + size_t pxr24_size = 0; + for (size_t c = 0; c < num_channels; c++) { + int ch_width = data_width; // V1 doesn't handle subsampling in decompression + int ch_pixels = ch_width * num_lines; + + if (channels[c].pixel_type == TINYEXR_PIXELTYPE_UINT) { + pxr24_size += static_cast(ch_pixels) * 4; + } else if (channels[c].pixel_type == TINYEXR_PIXELTYPE_HALF) { + pxr24_size += static_cast(ch_pixels) * 2; + } else { // FLOAT + pxr24_size += static_cast(ch_pixels) * 3; + } + } + + // Allocate buffer for zlib-decompressed PXR24 data + std::vector pxr24_buf(pxr24_size); + size_t uncomp_size = pxr24_size; + + // PXR24 uses raw zlib compression + if (pxr24_size == inLen) { + // Uncompressed - copy directly + memcpy(pxr24_buf.data(), inPtr, inLen); + } else { +#if defined(TINYEXR_USE_MINIZ) && (TINYEXR_USE_MINIZ==1) + mz_ulong dest_len = static_cast(pxr24_size); + int ret = mz_uncompress(pxr24_buf.data(), &dest_len, inPtr, static_cast(inLen)); + if (ret != MZ_OK) { + return false; + } + uncomp_size = static_cast(dest_len); +#elif defined(TINYEXR_USE_STB_ZLIB) && (TINYEXR_USE_STB_ZLIB==1) + int outLen = stbi_zlib_decode_buffer(reinterpret_cast(pxr24_buf.data()), + static_cast(pxr24_size), reinterpret_cast(inPtr), static_cast(inLen)); + if (outLen < 0) { + return false; + } + uncomp_size = static_cast(outLen); +#elif defined(TINYEXR_USE_NANOZLIB) && (TINYEXR_USE_NANOZLIB==1) + uint64_t outLen = 0; + nanoz_status_t ret = nanoz_uncompress(inPtr, inLen, pxr24_size, pxr24_buf.data(), &outLen); + if (ret != NANOZ_SUCCESS) { + return false; + } + uncomp_size = static_cast(outLen); +#else + uLongf dest_len = static_cast(pxr24_size); + int ret = uncompress(pxr24_buf.data(), &dest_len, inPtr, static_cast(inLen)); + if (ret != Z_OK) { + return false; + } + uncomp_size = static_cast(dest_len); +#endif + } + + if (uncomp_size != pxr24_size) { + return false; + } + + // Convert PXR24 format to standard EXR format + // PXR24 uses: + // 1. Byte plane separation: bytes are stored by plane (all high bytes, then next-high, etc.) + // 2. Delta encoding: each pixel is stored as difference from previous pixel + const unsigned char* in_p = pxr24_buf.data(); + unsigned char* out_p = outPtr; + + for (int line = 0; line < num_lines; line++) { + for (size_t c = 0; c < num_channels; c++) { + int w = data_width; + + if (channels[c].pixel_type == TINYEXR_PIXELTYPE_UINT) { + // UINT: 4 byte planes with delta encoding + const unsigned char* ptr0 = in_p; + const unsigned char* ptr1 = in_p + w; + const unsigned char* ptr2 = in_p + w * 2; + const unsigned char* ptr3 = in_p + w * 3; + in_p += w * 4; + + unsigned int pixel = 0; + for (int x = 0; x < w; x++) { + unsigned int diff = (static_cast(ptr0[x]) << 24) | + (static_cast(ptr1[x]) << 16) | + (static_cast(ptr2[x]) << 8) | + (static_cast(ptr3[x])); + pixel += diff; + memcpy(out_p, &pixel, 4); + out_p += 4; + } + } else if (channels[c].pixel_type == TINYEXR_PIXELTYPE_HALF) { + // HALF: 2 byte planes with delta encoding + const unsigned char* ptr0 = in_p; + const unsigned char* ptr1 = in_p + w; + in_p += w * 2; + + unsigned int pixel = 0; + for (int x = 0; x < w; x++) { + unsigned int diff = (static_cast(ptr0[x]) << 8) | + (static_cast(ptr1[x])); + pixel += diff; + unsigned short h = static_cast(pixel); + memcpy(out_p, &h, 2); + out_p += 2; + } + } else { // FLOAT + // FLOAT: 3 byte planes with delta encoding, expand to 32-bit + const unsigned char* ptr0 = in_p; + const unsigned char* ptr1 = in_p + w; + const unsigned char* ptr2 = in_p + w * 2; + in_p += w * 3; + + unsigned int pixel = 0; + for (int x = 0; x < w; x++) { + // PXR24 stores 24-bit floats with delta encoding + // The diff is in the upper 24 bits + unsigned int diff = (static_cast(ptr0[x]) << 24) | + (static_cast(ptr1[x]) << 16) | + (static_cast(ptr2[x]) << 8); + pixel += diff; + memcpy(out_p, &pixel, 4); + out_p += 4; + } + } + } + } + + return true; +} + +// ============================================================================ +// B44/B44A decompression +// ============================================================================ + +// B44 compresses 4x4 blocks of HALF values to 14 bytes +// B44A can compress flat regions to 3 bytes + +// B44 lookup tables +// expTable: converts half-float value to exp(half/8) for p_linear channels +// logTable: converts half-float value to 8*log(half) for p_linear channels +// Note: These tables are primarily for luminance channels with p_linear attribute +static unsigned short g_b44_exp_table[65536]; +static unsigned short g_b44_log_table[65536]; +static bool g_b44_tables_initialized = false; + +// Half-float conversion helpers for B44 table initialization +static inline float B44HalfToFloat(unsigned short h) { + union { unsigned int i; float f; } u; + + int s = (h >> 15) & 0x1; + int e = (h >> 10) & 0x1f; + int m = h & 0x3ff; + + if (e == 0) { + if (m == 0) { + // Zero + u.i = s << 31; + return u.f; + } + // Denormal + float f = (float)m / 1024.0f; + f = f * (1.0f / 16384.0f); // 2^-14 + return s ? -f : f; + } else if (e == 31) { + // Inf or NaN + u.i = (s << 31) | 0x7f800000 | (m << 13); + return u.f; + } + + // Normal + u.i = (s << 31) | ((e + 112) << 23) | (m << 13); + return u.f; +} + +static inline unsigned short B44FloatToHalf(float f) { + union { unsigned int i; float f; } u; + u.f = f; + + int s = (u.i >> 31) & 0x1; + int e = (u.i >> 23) & 0xff; + int m = u.i & 0x7fffff; + + if (e == 0) { + return static_cast(s << 15); // Zero + } else if (e == 255) { + // Inf or NaN + return static_cast((s << 15) | 0x7c00 | (m >> 13)); + } else if (e < 113) { + // Too small - denormal or zero + if (e < 103) return static_cast(s << 15); + m = (m | 0x800000) >> (114 - e); + return static_cast((s << 15) | (m >> 13)); + } else if (e > 142) { + // Too large - infinity + return static_cast((s << 15) | 0x7c00); + } + + return static_cast((s << 15) | ((e - 112) << 10) | (m >> 13)); +} + +// Initialize B44 exp/log lookup tables (matches OpenEXR algorithm) +static void InitB44Tables() { + if (g_b44_tables_initialized) return; + + // Generate tables per OpenEXR's b44_table_init.c + for (int i = 0; i < 65536; i++) { + unsigned short x = static_cast(i); + + // expTable: convertFromLinear - exp(half / 8) + if ((x & 0x7c00) == 0x7c00) { + // infinity/nan -> 0 + g_b44_exp_table[i] = 0; + } else if (x >= 0x558c && x < 0x8000) { + // >= 8 * log(HALF_MAX) -> HALF_MAX + g_b44_exp_table[i] = 0x7bff; + } else { + float f = B44HalfToFloat(x); + f = static_cast(std::exp(static_cast(f) / 8.0)); + g_b44_exp_table[i] = B44FloatToHalf(f); + } + + // logTable: convertToLinear - 8 * log(half) + if ((x & 0x7c00) == 0x7c00) { + // infinity/nan -> 0 + g_b44_log_table[i] = 0; + } else if (x > 0x8000) { + // negative (excluding -0.0) -> 0 + g_b44_log_table[i] = 0; + } else { + float f = B44HalfToFloat(x); + if (f <= 0.0f) { + g_b44_log_table[i] = 0; + } else { + f = static_cast(8.0 * std::log(static_cast(f))); + g_b44_log_table[i] = B44FloatToHalf(f); + } + } + } + + g_b44_tables_initialized = true; +} + +// Convert half to linear-log space (for p_linear channels) +static inline unsigned short B44ConvertFromLinear(unsigned short h) { + return g_b44_exp_table[h]; +} + +// Convert linear-log back to half (for p_linear channels) +static inline unsigned short B44ConvertToLinear(unsigned short h) { + return g_b44_log_table[h]; +} + +// Unpack one 4x4 block from B44 compressed 14 bytes (matches OpenEXR unpack14) +static void UnpackB44Block(unsigned short dst[16], const unsigned char src[14]) { + // Extract t[0] (stored as ordered-magnitude value) + unsigned short s0 = (static_cast(src[0]) << 8) | src[1]; + + // Extract shift and compute bias + unsigned short shift = src[2] >> 2; + unsigned short bias = static_cast(0x20u << shift); + + // Reconstruct t values using running differences + // Pattern: s[0]->s[4]->s[8]->s[12], then s[0]->s[1], s[4]->s[5], etc. + + unsigned short s4 = static_cast( + static_cast(s0) + + static_cast(((static_cast(src[2]) << 4) | + (static_cast(src[3]) >> 4)) & 0x3fu) * (1u << shift) - bias); + + unsigned short s8 = static_cast( + static_cast(s4) + + static_cast(((static_cast(src[3]) << 2) | + (static_cast(src[4]) >> 6)) & 0x3fu) * (1u << shift) - bias); + + unsigned short s12 = static_cast( + static_cast(s8) + + static_cast(src[4] & 0x3fu) * (1u << shift) - bias); + + unsigned short s1 = static_cast( + static_cast(s0) + + static_cast(src[5] >> 2) * (1u << shift) - bias); + + unsigned short s5 = static_cast( + static_cast(s4) + + static_cast(((static_cast(src[5]) << 4) | + (static_cast(src[6]) >> 4)) & 0x3fu) * (1u << shift) - bias); + + unsigned short s9 = static_cast( + static_cast(s8) + + static_cast(((static_cast(src[6]) << 2) | + (static_cast(src[7]) >> 6)) & 0x3fu) * (1u << shift) - bias); + + unsigned short s13 = static_cast( + static_cast(s12) + + static_cast(src[7] & 0x3fu) * (1u << shift) - bias); + + unsigned short s2 = static_cast( + static_cast(s1) + + static_cast(src[8] >> 2) * (1u << shift) - bias); + + unsigned short s6 = static_cast( + static_cast(s5) + + static_cast(((static_cast(src[8]) << 4) | + (static_cast(src[9]) >> 4)) & 0x3fu) * (1u << shift) - bias); + + unsigned short s10 = static_cast( + static_cast(s9) + + static_cast(((static_cast(src[9]) << 2) | + (static_cast(src[10]) >> 6)) & 0x3fu) * (1u << shift) - bias); + + unsigned short s14 = static_cast( + static_cast(s13) + + static_cast(src[10] & 0x3fu) * (1u << shift) - bias); + + unsigned short s3 = static_cast( + static_cast(s2) + + static_cast(src[11] >> 2) * (1u << shift) - bias); + + unsigned short s7 = static_cast( + static_cast(s6) + + static_cast(((static_cast(src[11]) << 4) | + (static_cast(src[12]) >> 4)) & 0x3fu) * (1u << shift) - bias); + + unsigned short s11 = static_cast( + static_cast(s10) + + static_cast(((static_cast(src[12]) << 2) | + (static_cast(src[13]) >> 6)) & 0x3fu) * (1u << shift) - bias); + + unsigned short s15 = static_cast( + static_cast(s14) + + static_cast(src[13] & 0x3fu) * (1u << shift) - bias); + + // Store t values + dst[0] = s0; dst[1] = s1; dst[2] = s2; dst[3] = s3; + dst[4] = s4; dst[5] = s5; dst[6] = s6; dst[7] = s7; + dst[8] = s8; dst[9] = s9; dst[10] = s10; dst[11] = s11; + dst[12] = s12; dst[13] = s13; dst[14] = s14; dst[15] = s15; + + // Convert from ordered-magnitude to half-float + for (int i = 0; i < 16; i++) { + if (dst[i] & 0x8000) { + dst[i] &= 0x7fff; // Positive: clear sign bit + } else { + dst[i] = ~dst[i]; // Negative: invert all bits + } + } +} + +// Unpack a 3-byte flat block (all pixels same value) +static void UnpackB44FlatBlock(unsigned short dst[16], const unsigned char src[3]) { + unsigned short t = (static_cast(src[0]) << 8) | src[1]; + + // Convert from ordered-magnitude to half-float + unsigned short h; + if (t & 0x8000) { + h = t & 0x7fff; + } else { + h = ~t; + } + + for (int i = 0; i < 16; i++) { + dst[i] = h; + } +} + +static bool DecompressB44(unsigned char *outPtr, size_t outBufSize, + const unsigned char *inPtr, size_t inLen, + int data_width, int num_lines, + size_t num_channels, + const EXRChannelInfo *channels, + bool is_b44a) { + (void)is_b44a; // Flat block detection doesn't depend on B44/B44A for decoding + InitB44Tables(); + + // Validate that the output buffer is large enough for the decoded data. + // Use overflow-safe arithmetic: overflow in any multiplication or accumulation + // means the size cannot fit in memory, which is definitely > outBufSize. + { + size_t expected_out = 0; + for (size_t c = 0; c < num_channels; c++) { + int xs = channels[c].x_sampling > 0 ? channels[c].x_sampling : 1; + int ys = channels[c].y_sampling > 0 ? channels[c].y_sampling : 1; + size_t cw = static_cast((data_width + xs - 1) / xs); + size_t ch = static_cast((num_lines + ys - 1) / ys); + size_t bpp = (channels[c].pixel_type == TINYEXR_PIXELTYPE_HALF) ? 2u : 4u; + // Check cw * ch overflow + if (cw != 0 && ch > (SIZE_MAX / cw)) return false; + size_t pixels = cw * ch; + // Check pixels * bpp overflow + if (pixels > (SIZE_MAX / bpp)) return false; + size_t ch_total = pixels * bpp; + // Check accumulation overflow + if (expected_out > SIZE_MAX - ch_total) return false; + expected_out += ch_total; + } + if (expected_out > outBufSize) return false; + } + + const unsigned char* in_p = inPtr; + const unsigned char* in_end = inPtr + inLen; + + // First pass: decompress all channels into scratch buffers. + // For non-HALF channels, save the pointer and byte count so we can copy + // the raw data to the output buffer in the second pass. + std::vector> scratch_buffers(num_channels); + std::vector nonhalf_ptrs(num_channels, nullptr); + std::vector nonhalf_sizes(num_channels, 0); + + for (size_t c = 0; c < num_channels; c++) { + // Compute per-channel dimensions based on sampling + int x_sampling = channels[c].x_sampling > 0 ? channels[c].x_sampling : 1; + int y_sampling = channels[c].y_sampling > 0 ? channels[c].y_sampling : 1; + int ch_width = (data_width + x_sampling - 1) / x_sampling; + int ch_height = (num_lines + y_sampling - 1) / y_sampling; + + // B44 only works with HALF pixel types + if (channels[c].pixel_type != TINYEXR_PIXELTYPE_HALF) { + // For non-HALF channels, data is stored uncompressed; record position + size_t ch_bytes = static_cast(ch_width) * ch_height; + if (channels[c].pixel_type == TINYEXR_PIXELTYPE_UINT || + channels[c].pixel_type == TINYEXR_PIXELTYPE_FLOAT) { + ch_bytes *= 4; + } else { + ch_bytes *= 2; + } + if (in_p + ch_bytes > in_end) return false; + nonhalf_ptrs[c] = in_p; + nonhalf_sizes[c] = ch_bytes; + in_p += ch_bytes; + continue; + } + + // Calculate block dimensions (rounded up to multiple of 4) + int padded_width = ((ch_width + 3) / 4) * 4; + int padded_height = ((ch_height + 3) / 4) * 4; + int num_blocks_x = padded_width / 4; + int num_blocks_y = padded_height / 4; + + // Allocate scratch buffer for this channel + scratch_buffers[c].resize(static_cast(padded_width) * padded_height); + + // Process blocks + for (int by = 0; by < num_blocks_y; by++) { + for (int bx = 0; bx < num_blocks_x; bx++) { + unsigned short block[16]; + + if (in_p + 3 > in_end) return false; + + // Check for flat block (shift >= 13) + if (in_p[2] >= (13 << 2)) { + // 3-byte flat block + UnpackB44FlatBlock(block, in_p); + in_p += 3; + } else { + // Regular 14-byte block + if (in_p + 14 > in_end) return false; + UnpackB44Block(block, in_p); + in_p += 14; + } + + // Apply p_linear conversion (log table) if needed + if (channels[c].p_linear) { + for (int i = 0; i < 16; i++) { + block[i] = g_b44_log_table[block[i]]; + } + } + + // Store block in scratch buffer + for (int dy = 0; dy < 4; dy++) { + int y = by * 4 + dy; + for (int dx = 0; dx < 4; dx++) { + int x = bx * 4 + dx; + scratch_buffers[c][static_cast(y) * padded_width + x] = block[dy * 4 + dx]; + } + } + } + } + } + + // Second pass: copy from scratch buffers to output in per-channel format. + // Output format: all data for channel 0, then all data for channel 1, etc. + // ch_offset in DecodePixelData is accumulated per preceding channel sizes. + unsigned char* out_p = outPtr; + for (size_t c = 0; c < num_channels; c++) { + int x_sampling = channels[c].x_sampling > 0 ? channels[c].x_sampling : 1; + int y_sampling = channels[c].y_sampling > 0 ? channels[c].y_sampling : 1; + int ch_width = (data_width + x_sampling - 1) / x_sampling; + int ch_height = (num_lines + y_sampling - 1) / y_sampling; + int padded_width = ((ch_width + 3) / 4) * 4; + + if (channels[c].pixel_type == TINYEXR_PIXELTYPE_HALF) { + for (int y = 0; y < ch_height; y++) { + for (int x = 0; x < ch_width; x++) { + unsigned short val = scratch_buffers[c][static_cast(y) * padded_width + x]; + // Write as little-endian bytes so DecodePixelData's swap2 (LE->host) + // works correctly on both little- and big-endian platforms. + tinyexr::swap2(&val); + memcpy(out_p, &val, sizeof(val)); + out_p += sizeof(val); + } + } + } else if (channels[c].pixel_type == TINYEXR_PIXELTYPE_UINT || + channels[c].pixel_type == TINYEXR_PIXELTYPE_FLOAT) { + // Non-HALF data is stored uncompressed; copy from saved pointer + if (nonhalf_sizes[c] > 0) { + memcpy(out_p, nonhalf_ptrs[c], nonhalf_sizes[c]); + out_p += nonhalf_sizes[c]; + } + } + } + + return true; +} + +// ============================================================================ +// PXR24 compression (encoding) +// ============================================================================ + +// Convert float32 to float24 (PXR24 format) +static inline unsigned int float_to_float24(float f) { + union { float f; unsigned int i; } u; + u.f = f; + + unsigned int s = u.i & 0x80000000; + unsigned int e = u.i & 0x7f800000; + unsigned int m = u.i & 0x007fffff; + + if (e == 0x7f800000) { + if (m) { + // NaN - preserve sign and 15 leftmost mantissa bits + m >>= 8; + return (s >> 8) | (e >> 8) | m | (m == 0 ? 1 : 0); + } else { + // Infinity + return (s >> 8) | (e >> 8); + } + } + + // Finite - round mantissa to 15 bits + unsigned int i = ((e | m) + (m & 0x00000080)) >> 8; + + if (i >= 0x7f8000) { + // Overflow - truncate instead of round + i = (e | m) >> 8; + } + + return (s >> 8) | i; +} + +static bool CompressPxr24(std::vector& outBuf, + const unsigned char *inPtr, size_t inLen, + int data_width, int num_lines, + size_t num_channels, + const EXRChannelInfo *channels) { + // PXR24 stores HALF as 2 bytes, UINT as 4 bytes, FLOAT as 3 bytes (truncated) + // Data is stored with byte plane separation and delta encoding + size_t pxr24_size = 0; + for (size_t c = 0; c < num_channels; c++) { + int ch_pixels = data_width * num_lines; + + if (channels[c].pixel_type == TINYEXR_PIXELTYPE_UINT) { + pxr24_size += static_cast(ch_pixels) * 4; + } else if (channels[c].pixel_type == TINYEXR_PIXELTYPE_HALF) { + pxr24_size += static_cast(ch_pixels) * 2; + } else { // FLOAT + pxr24_size += static_cast(ch_pixels) * 3; + } + } + + // Create PXR24 format data + std::vector pxr24_buf(pxr24_size); + const unsigned char* in_p = inPtr; + unsigned char* out_p = pxr24_buf.data(); + + // PXR24 uses: + // 1. Byte plane separation: bytes are stored by plane (all high bytes, then next-high, etc.) + // 2. Delta encoding: each pixel is stored as difference from previous pixel + for (int line = 0; line < num_lines; line++) { + for (size_t c = 0; c < num_channels; c++) { + int w = data_width; + + if (channels[c].pixel_type == TINYEXR_PIXELTYPE_UINT) { + // UINT: 4 byte planes with delta encoding + unsigned char* ptr0 = out_p; + unsigned char* ptr1 = out_p + w; + unsigned char* ptr2 = out_p + w * 2; + unsigned char* ptr3 = out_p + w * 3; + out_p += w * 4; + + unsigned int prevPixel = 0; + for (int x = 0; x < w; x++) { + unsigned int pixel; + memcpy(&pixel, in_p, 4); + in_p += 4; + unsigned int diff = pixel - prevPixel; + prevPixel = pixel; + + ptr0[x] = static_cast(diff >> 24); + ptr1[x] = static_cast(diff >> 16); + ptr2[x] = static_cast(diff >> 8); + ptr3[x] = static_cast(diff); + } + } else if (channels[c].pixel_type == TINYEXR_PIXELTYPE_HALF) { + // HALF: 2 byte planes with delta encoding + unsigned char* ptr0 = out_p; + unsigned char* ptr1 = out_p + w; + out_p += w * 2; + + unsigned int prevPixel = 0; + for (int x = 0; x < w; x++) { + unsigned short h; + memcpy(&h, in_p, 2); + in_p += 2; + unsigned int pixel = h; + unsigned int diff = pixel - prevPixel; + prevPixel = pixel; + + ptr0[x] = static_cast(diff >> 8); + ptr1[x] = static_cast(diff); + } + } else { // FLOAT + // FLOAT: convert to 24-bit, then 3 byte planes with delta encoding + unsigned char* ptr0 = out_p; + unsigned char* ptr1 = out_p + w; + unsigned char* ptr2 = out_p + w * 2; + out_p += w * 3; + + unsigned int prevPixel = 0; + for (int x = 0; x < w; x++) { + float f; + memcpy(&f, in_p, 4); + in_p += 4; + unsigned int pixel24 = float_to_float24(f); + unsigned int diff = pixel24 - prevPixel; + prevPixel = pixel24; + + // Store as 24-bit diff (shifted to upper bits for proper reconstruction) + ptr0[x] = static_cast(diff >> 16); + ptr1[x] = static_cast(diff >> 8); + ptr2[x] = static_cast(diff); + } + } + } + } + + // Compress with zlib +#if defined(TINYEXR_USE_MINIZ) && (TINYEXR_USE_MINIZ==1) + mz_ulong dest_len = mz_compressBound(static_cast(pxr24_size)); + outBuf.resize(dest_len); + int ret = mz_compress(outBuf.data(), &dest_len, pxr24_buf.data(), static_cast(pxr24_size)); + if (ret != MZ_OK) { + return false; + } + outBuf.resize(static_cast(dest_len)); +#elif defined(TINYEXR_USE_STB_ZLIB) && (TINYEXR_USE_STB_ZLIB==1) + int outSize; + unsigned char* ret = stbi_zlib_compress(pxr24_buf.data(), static_cast(pxr24_size), &outSize, 8); + if (!ret) { + return false; + } + outBuf.assign(ret, ret + outSize); + free(ret); +#elif defined(TINYEXR_USE_NANOZLIB) && (TINYEXR_USE_NANOZLIB==1) + int outSize; + unsigned char* ret = nanoz_compress(pxr24_buf.data(), pxr24_size, &outSize, 8); + if (!ret) { + return false; + } + outBuf.assign(ret, ret + outSize); + free(ret); +#else + uLongf dest_len = compressBound(static_cast(pxr24_size)); + outBuf.resize(static_cast(dest_len)); + int ret = compress(outBuf.data(), &dest_len, pxr24_buf.data(), static_cast(pxr24_size)); + if (ret != Z_OK) { + return false; + } + outBuf.resize(static_cast(dest_len)); +#endif + + return true; +} + +// Overloaded version that takes ChannelInfo instead of EXRChannelInfo +// Note: Uses requested_pixel_type which is the file format, not the input type +static bool CompressPxr24(std::vector& outBuf, + const unsigned char *inPtr, size_t inLen, + int data_width, int num_lines, + size_t num_channels, + const std::vector& channels) { + // PXR24 stores HALF as 2 bytes, UINT as 4 bytes, FLOAT as 3 bytes (truncated) + // Data is stored with byte plane separation and delta encoding + // Use requested_pixel_type which is the actual format in the file + size_t pxr24_size = 0; + for (size_t c = 0; c < num_channels; c++) { + int ch_pixels = data_width * num_lines; + int file_type = channels[c].requested_pixel_type; + + if (file_type == TINYEXR_PIXELTYPE_UINT) { + pxr24_size += static_cast(ch_pixels) * 4; + } else if (file_type == TINYEXR_PIXELTYPE_HALF) { + pxr24_size += static_cast(ch_pixels) * 2; + } else { // FLOAT + pxr24_size += static_cast(ch_pixels) * 3; + } + } + + // Create PXR24 format data + std::vector pxr24_buf(pxr24_size); + const unsigned char* in_p = inPtr; + unsigned char* out_p = pxr24_buf.data(); + + // PXR24 uses: + // 1. Byte plane separation: bytes are stored by plane (all high bytes, then next-high, etc.) + // 2. Delta encoding: each pixel is stored as difference from previous pixel + for (int line = 0; line < num_lines; line++) { + for (size_t c = 0; c < num_channels; c++) { + int w = data_width; + int file_type = channels[c].requested_pixel_type; + + if (file_type == TINYEXR_PIXELTYPE_UINT) { + // UINT: 4 byte planes with delta encoding + unsigned char* ptr0 = out_p; + unsigned char* ptr1 = out_p + w; + unsigned char* ptr2 = out_p + w * 2; + unsigned char* ptr3 = out_p + w * 3; + out_p += w * 4; + + unsigned int prevPixel = 0; + for (int x = 0; x < w; x++) { + unsigned int pixel; + memcpy(&pixel, in_p, 4); + in_p += 4; + unsigned int diff = pixel - prevPixel; + prevPixel = pixel; + + ptr0[x] = static_cast(diff >> 24); + ptr1[x] = static_cast(diff >> 16); + ptr2[x] = static_cast(diff >> 8); + ptr3[x] = static_cast(diff); + } + } else if (file_type == TINYEXR_PIXELTYPE_HALF) { + // HALF: 2 byte planes with delta encoding + unsigned char* ptr0 = out_p; + unsigned char* ptr1 = out_p + w; + out_p += w * 2; + + unsigned int prevPixel = 0; + for (int x = 0; x < w; x++) { + unsigned short h; + memcpy(&h, in_p, 2); + in_p += 2; + unsigned int pixel = h; + unsigned int diff = pixel - prevPixel; + prevPixel = pixel; + + ptr0[x] = static_cast(diff >> 8); + ptr1[x] = static_cast(diff); + } + } else { // FLOAT + // FLOAT: convert to 24-bit, then 3 byte planes with delta encoding + unsigned char* ptr0 = out_p; + unsigned char* ptr1 = out_p + w; + unsigned char* ptr2 = out_p + w * 2; + out_p += w * 3; + + unsigned int prevPixel = 0; + for (int x = 0; x < w; x++) { + float f; + memcpy(&f, in_p, 4); + in_p += 4; + unsigned int pixel24 = float_to_float24(f); + unsigned int diff = pixel24 - prevPixel; + prevPixel = pixel24; + + // Store as 24-bit diff (shifted to upper bits for proper reconstruction) + ptr0[x] = static_cast(diff >> 16); + ptr1[x] = static_cast(diff >> 8); + ptr2[x] = static_cast(diff); + } + } + } + } + + // Compress with zlib +#if defined(TINYEXR_USE_MINIZ) && (TINYEXR_USE_MINIZ==1) + mz_ulong dest_len = mz_compressBound(static_cast(pxr24_size)); + outBuf.resize(dest_len); + int ret = mz_compress(outBuf.data(), &dest_len, pxr24_buf.data(), static_cast(pxr24_size)); + if (ret != MZ_OK) { + return false; + } + outBuf.resize(static_cast(dest_len)); +#elif defined(TINYEXR_USE_STB_ZLIB) && (TINYEXR_USE_STB_ZLIB==1) + int outSize; + unsigned char* ret = stbi_zlib_compress(pxr24_buf.data(), static_cast(pxr24_size), &outSize, 8); + if (!ret) { + return false; + } + outBuf.assign(ret, ret + outSize); + free(ret); +#elif defined(TINYEXR_USE_NANOZLIB) && (TINYEXR_USE_NANOZLIB==1) + int outSize; + unsigned char* ret = nanoz_compress(pxr24_buf.data(), pxr24_size, &outSize, 8); + if (!ret) { + return false; + } + outBuf.assign(ret, ret + outSize); + free(ret); +#else + uLongf dest_len = compressBound(static_cast(pxr24_size)); + outBuf.resize(static_cast(dest_len)); + int ret = compress(outBuf.data(), &dest_len, pxr24_buf.data(), static_cast(pxr24_size)); + if (ret != Z_OK) { + return false; + } + outBuf.resize(static_cast(dest_len)); +#endif + + return true; +} + +// ============================================================================ +// B44/B44A compression (encoding) +// ============================================================================ + +// Shift and round for B44 pack (matches OpenEXR's shiftAndRound) +static inline int B44ShiftAndRound(int x, int shift) { + // Compute y = x * pow(2, -shift), rounded to nearest integer + // In case of a tie, round to the even one + x <<= 1; + int a = (1 << shift) - 1; + shift += 1; + int b = (x >> shift) & 1; + return (x + a + b) >> shift; +} + +// Pack a 4x4 block of HALF values into 14 bytes (matches OpenEXR's pack()) +// Returns the number of bytes written (14 for normal, 3 for flat if flatfields=true) +static int PackB44Block(unsigned char* out, const unsigned short* block, bool flatfields, bool exactmax) { + int d[16]; + int r[15]; + int rMin, rMax; + unsigned short t[16]; + unsigned short tMax; + int shift = -1; + + const int bias = 0x20; + + // Convert half-float values to ordered-magnitude representation + // This ensures that if t[i] > t[j], then half[i] > half[j] as floats + for (int i = 0; i < 16; ++i) { + if ((block[i] & 0x7c00) == 0x7c00) { + t[i] = 0x8000; // NaN/Inf -> neutral value + } else if (block[i] & 0x8000) { + t[i] = ~block[i]; // Negative: invert all bits + } else { + t[i] = block[i] | 0x8000; // Positive: set sign bit + } + } + + // Find maximum t value + tMax = 0; + for (int i = 0; i < 16; ++i) { + if (tMax < t[i]) tMax = t[i]; + } + + // Compute running differences and find valid shift + do { + shift += 1; + + // Compute absolute differences from tMax, shifted and rounded + for (int i = 0; i < 16; ++i) { + d[i] = B44ShiftAndRound(tMax - t[i], shift); + } + + // Convert to running differences (specific pattern for B44) + r[0] = d[0] - d[4] + bias; + r[1] = d[4] - d[8] + bias; + r[2] = d[8] - d[12] + bias; + + r[3] = d[0] - d[1] + bias; + r[4] = d[4] - d[5] + bias; + r[5] = d[8] - d[9] + bias; + r[6] = d[12] - d[13] + bias; + + r[7] = d[1] - d[2] + bias; + r[8] = d[5] - d[6] + bias; + r[9] = d[9] - d[10] + bias; + r[10] = d[13] - d[14] + bias; + + r[11] = d[2] - d[3] + bias; + r[12] = d[6] - d[7] + bias; + r[13] = d[10] - d[11] + bias; + r[14] = d[14] - d[15] + bias; + + rMin = r[0]; + rMax = r[0]; + for (int i = 1; i < 15; ++i) { + if (rMin > r[i]) rMin = r[i]; + if (rMax < r[i]) rMax = r[i]; + } + } while (rMin < 0 || rMax > 0x3f); + + // Check for flat block (all pixels same value) + if (rMin == bias && rMax == bias && flatfields) { + // Encode as 3 bytes: t[0] and marker 0xfc + out[0] = static_cast(t[0] >> 8); + out[1] = static_cast(t[0]); + out[2] = 0xfc; // Flat block marker (shift >= 13) + return 3; + } + + if (exactmax) { + // Adjust t[0] so the max pixel is represented accurately + t[0] = tMax - static_cast(d[0] << shift); + } + + // Pack t[0], shift, and r[0]..r[14] into 14 bytes + out[0] = static_cast(t[0] >> 8); + out[1] = static_cast(t[0]); + out[2] = static_cast((shift << 2) | (r[0] >> 4)); + out[3] = static_cast((r[0] << 4) | (r[1] >> 2)); + out[4] = static_cast((r[1] << 6) | r[2]); + out[5] = static_cast((r[3] << 2) | (r[4] >> 4)); + out[6] = static_cast((r[4] << 4) | (r[5] >> 2)); + out[7] = static_cast((r[5] << 6) | r[6]); + out[8] = static_cast((r[7] << 2) | (r[8] >> 4)); + out[9] = static_cast((r[8] << 4) | (r[9] >> 2)); + out[10] = static_cast((r[9] << 6) | r[10]); + out[11] = static_cast((r[11] << 2) | (r[12] >> 4)); + out[12] = static_cast((r[12] << 4) | (r[13] >> 2)); + out[13] = static_cast((r[13] << 6) | r[14]); + + return 14; +} + +static bool CompressB44(std::vector& outBuf, + const unsigned char *inPtr, size_t inLen, + int data_width, int num_lines, + size_t num_channels, + const EXRChannelInfo *channels, + bool is_b44a) { + // Calculate number of 4x4 blocks + int num_blocks_x = (data_width + 3) / 4; + int num_blocks_y = (num_lines + 3) / 4; + + // Estimate output size (14 bytes per block per HALF channel) + size_t max_size = 0; + for (size_t c = 0; c < num_channels; c++) { + if (channels[c].pixel_type == TINYEXR_PIXELTYPE_HALF) { + max_size += static_cast(num_blocks_x) * num_blocks_y * 14; + } else if (channels[c].pixel_type == TINYEXR_PIXELTYPE_UINT || + channels[c].pixel_type == TINYEXR_PIXELTYPE_FLOAT) { + max_size += static_cast(data_width) * num_lines * 4; + } else { + max_size += static_cast(data_width) * num_lines * 2; + } + } + + outBuf.resize(max_size); + unsigned char* out_p = outBuf.data(); + + // Process each channel + size_t in_offset = 0; + for (size_t c = 0; c < num_channels; c++) { + if (channels[c].pixel_type != TINYEXR_PIXELTYPE_HALF) { + // Non-HALF channels are stored uncompressed + size_t ch_bytes = static_cast(data_width) * num_lines; + if (channels[c].pixel_type == TINYEXR_PIXELTYPE_UINT || + channels[c].pixel_type == TINYEXR_PIXELTYPE_FLOAT) { + ch_bytes *= 4; + } else { + ch_bytes *= 2; + } + memcpy(out_p, inPtr + in_offset, ch_bytes); + out_p += ch_bytes; + in_offset += ch_bytes; + continue; + } + + // Process HALF channel in 4x4 blocks + const unsigned short* ch_ptr = reinterpret_cast(inPtr + in_offset); + + for (int by = 0; by < num_blocks_y; by++) { + for (int bx = 0; bx < num_blocks_x; bx++) { + unsigned short block[16]; + + // Gather block pixels with edge replication for padding + for (int dy = 0; dy < 4; dy++) { + int y = by * 4 + dy; + int src_y = (y >= num_lines) ? (num_lines - 1) : y; + + for (int dx = 0; dx < 4; dx++) { + int x = bx * 4 + dx; + int src_x = (x >= data_width) ? (data_width - 1) : x; + + block[dy * 4 + dx] = ch_ptr[src_y * data_width + src_x]; + } + } + + // Pack block - PackB44Block handles flat block detection internally + // flatfields = is_b44a, exactmax = true (for better accuracy) + int bytes_written = PackB44Block(out_p, block, is_b44a, true); + out_p += bytes_written; + } + } + + in_offset += static_cast(data_width) * num_lines * 2; + } + + // Resize to actual size + outBuf.resize(static_cast(out_p - outBuf.data())); + + return true; +} + +// Overloaded version that takes ChannelInfo instead of EXRChannelInfo +// Note: Uses requested_pixel_type which is the file format, not the input type +static bool CompressB44(std::vector& outBuf, + const unsigned char *inPtr, size_t inLen, + int data_width, int num_lines, + size_t num_channels, + const std::vector& channels, + bool is_b44a) { + // Calculate number of 4x4 blocks + int num_blocks_x = (data_width + 3) / 4; + int num_blocks_y = (num_lines + 3) / 4; + + // Estimate output size - use requested_pixel_type (file format) + size_t max_size = 0; + for (size_t c = 0; c < num_channels; c++) { + int file_type = channels[c].requested_pixel_type; + if (file_type == TINYEXR_PIXELTYPE_HALF) { + max_size += static_cast(num_blocks_x) * num_blocks_y * 14; + } else if (file_type == TINYEXR_PIXELTYPE_UINT || + file_type == TINYEXR_PIXELTYPE_FLOAT) { + max_size += static_cast(data_width) * num_lines * 4; + } else { + max_size += static_cast(data_width) * num_lines * 2; + } + } + + outBuf.resize(max_size); + unsigned char* out_p = outBuf.data(); + + size_t in_offset = 0; + for (size_t c = 0; c < num_channels; c++) { + int file_type = channels[c].requested_pixel_type; + if (file_type != TINYEXR_PIXELTYPE_HALF) { + size_t ch_bytes = static_cast(data_width) * num_lines; + if (file_type == TINYEXR_PIXELTYPE_UINT || + file_type == TINYEXR_PIXELTYPE_FLOAT) { + ch_bytes *= 4; + } else { + ch_bytes *= 2; + } + memcpy(out_p, inPtr + in_offset, ch_bytes); + out_p += ch_bytes; + in_offset += ch_bytes; + continue; + } + + const unsigned short* ch_ptr = reinterpret_cast(inPtr + in_offset); + + for (int by = 0; by < num_blocks_y; by++) { + for (int bx = 0; bx < num_blocks_x; bx++) { + unsigned short block[16]; + + // Gather block pixels with edge replication for padding + for (int dy = 0; dy < 4; dy++) { + int y = by * 4 + dy; + int src_y = (y >= num_lines) ? (num_lines - 1) : y; + + for (int dx = 0; dx < 4; dx++) { + int x = bx * 4 + dx; + int src_x = (x >= data_width) ? (data_width - 1) : x; + + block[dy * 4 + dx] = ch_ptr[src_y * data_width + src_x]; + } + } + + // Pack block - PackB44Block handles flat block detection internally + int bytes_written = PackB44Block(out_p, block, is_b44a, true); + out_p += bytes_written; + } + } + + in_offset += static_cast(data_width) * num_lines * 2; + } + + outBuf.resize(static_cast(out_p - outBuf.data())); + return true; +} + #if TINYEXR_USE_ZFP struct ZFPCompressionParam { @@ -4159,6 +5335,257 @@ static bool DecodePixelData(/* out */ unsigned char **out_images, (void)num_channels; return false; #endif + } else if (compression_type == TINYEXR_COMPRESSIONTYPE_PXR24) { + // PXR24 compression: Use true PXR24 decompression + // PXR24 truncates FLOAT to 24-bits, HALF/UINT pass through unchanged + std::vector outBuf(static_cast(width) * + static_cast(num_lines) * + pixel_data_size); + + if (!tinyexr::DecompressPxr24( + reinterpret_cast(&outBuf.at(0)), outBuf.size(), + data_ptr, static_cast(data_len), + width, num_lines, static_cast(num_channels), channels)) { + return false; + } + + // Process decompressed data (same as ZIP path) + for (size_t c = 0; c < static_cast(num_channels); c++) { + if (channels[c].pixel_type == TINYEXR_PIXELTYPE_HALF) { + for (size_t v = 0; v < static_cast(num_lines); v++) { + const unsigned short *line_ptr = reinterpret_cast( + &outBuf.at(v * static_cast(pixel_data_size) * + static_cast(width) + + channel_offset_list[c] * static_cast(width))); + for (size_t u = 0; u < static_cast(width); u++) { + tinyexr::FP16 hf; + tinyexr::cpy2(&(hf.u), line_ptr + u); + tinyexr::swap2(reinterpret_cast(&hf.u)); + + if (requested_pixel_types[c] == TINYEXR_PIXELTYPE_HALF) { + unsigned short *image = + reinterpret_cast(out_images)[c]; + if (line_order == 0) { + image += (static_cast(line_no) + v) * + static_cast(x_stride) + + u; + } else { + image += (static_cast(height) - 1U - + (static_cast(line_no) + v)) * + static_cast(x_stride) + + u; + } + *image = hf.u; + } else { // HALF -> FLOAT + tinyexr::FP32 f32 = half_to_float(hf); + float *image = reinterpret_cast(out_images)[c]; + if (line_order == 0) { + image += (static_cast(line_no) + v) * + static_cast(x_stride) + + u; + } else { + image += (static_cast(height) - 1U - + (static_cast(line_no) + v)) * + static_cast(x_stride) + + u; + } + *image = f32.f; + } + } + } + } else if (channels[c].pixel_type == TINYEXR_PIXELTYPE_UINT) { + TINYEXR_CHECK_AND_RETURN_C(requested_pixel_types[c] == TINYEXR_PIXELTYPE_UINT, false); + + for (size_t v = 0; v < static_cast(num_lines); v++) { + const unsigned int *line_ptr = reinterpret_cast( + &outBuf.at(v * pixel_data_size * static_cast(width) + + channel_offset_list[c] * static_cast(width))); + for (size_t u = 0; u < static_cast(width); u++) { + unsigned int val; + tinyexr::cpy4(&val, line_ptr + u); + tinyexr::swap4(&val); + + unsigned int *image = + reinterpret_cast(out_images)[c]; + if (line_order == 0) { + image += (static_cast(line_no) + v) * + static_cast(x_stride) + + u; + } else { + image += (static_cast(height) - 1U - + (static_cast(line_no) + v)) * + static_cast(x_stride) + + u; + } + *image = val; + } + } + } else if (channels[c].pixel_type == TINYEXR_PIXELTYPE_FLOAT) { + TINYEXR_CHECK_AND_RETURN_C(requested_pixel_types[c] == TINYEXR_PIXELTYPE_FLOAT, false); + for (size_t v = 0; v < static_cast(num_lines); v++) { + const float *line_ptr = reinterpret_cast( + &outBuf.at(v * pixel_data_size * static_cast(width) + + channel_offset_list[c] * static_cast(width))); + for (size_t u = 0; u < static_cast(width); u++) { + float val; + tinyexr::cpy4(&val, line_ptr + u); + tinyexr::swap4(reinterpret_cast(&val)); + + float *image = reinterpret_cast(out_images)[c]; + if (line_order == 0) { + image += (static_cast(line_no) + v) * + static_cast(x_stride) + + u; + } else { + image += (static_cast(height) - 1U - + (static_cast(line_no) + v)) * + static_cast(x_stride) + + u; + } + *image = val; + } + } + } else { + return false; + } + } + } else if (compression_type == TINYEXR_COMPRESSIONTYPE_B44 || + compression_type == TINYEXR_COMPRESSIONTYPE_B44A) { + // B44/B44A compression: Use true B44 block decompression + // B44 is a lossy block compression for HALF data (4x4 blocks -> 14 bytes) + bool is_b44a = (compression_type == TINYEXR_COMPRESSIONTYPE_B44A); + + // Compute outBuf size matching DecompressB44's output layout: per-channel + // sequential data using subsampled dimensions for each channel. + size_t b44_out_size = 0; + for (size_t c = 0; c < static_cast(num_channels); c++) { + int xs = channels[c].x_sampling > 0 ? channels[c].x_sampling : 1; + int ys = channels[c].y_sampling > 0 ? channels[c].y_sampling : 1; + size_t cw = static_cast((width + xs - 1) / xs); + size_t ch = static_cast((num_lines + ys - 1) / ys); + size_t bpp = (channels[c].pixel_type == TINYEXR_PIXELTYPE_HALF) ? 2u : 4u; + b44_out_size += cw * ch * bpp; + } + + std::vector outBuf(b44_out_size); + + if (!tinyexr::DecompressB44( + reinterpret_cast(&outBuf.at(0)), outBuf.size(), + data_ptr, static_cast(data_len), + width, num_lines, static_cast(num_channels), channels, + is_b44a)) { + return false; + } + + // Process decompressed data - B44 returns data organized per channel, + // using subsampled dimensions (ch_width/ch_height based on x/y_sampling). + // Accumulate ch_offset based on actual subsampled sizes of preceding + // channels to handle mixed channel types and subsampling correctly. + size_t ch_offset = 0; + for (size_t c = 0; c < static_cast(num_channels); c++) { + int xs = channels[c].x_sampling > 0 ? channels[c].x_sampling : 1; + int ys = channels[c].y_sampling > 0 ? channels[c].y_sampling : 1; + size_t ch_width = static_cast((width + xs - 1) / xs); + size_t ch_height = static_cast((num_lines + ys - 1) / ys); + size_t ch_bytes = (channels[c].pixel_type == TINYEXR_PIXELTYPE_HALF) ? 2 : 4; + + if (channels[c].pixel_type == TINYEXR_PIXELTYPE_HALF) { + for (size_t v = 0; v < ch_height; v++) { + const unsigned short *line_ptr = reinterpret_cast( + &outBuf.at(ch_offset + v * ch_width * 2)); + for (size_t u = 0; u < ch_width; u++) { + tinyexr::FP16 hf; + tinyexr::cpy2(&(hf.u), line_ptr + u); + // B44 stream stores data in little-endian order (same as the + // encoder's buf); reverse the byte swap the encoder applied. + tinyexr::swap2(reinterpret_cast(&hf.u)); + + if (requested_pixel_types[c] == TINYEXR_PIXELTYPE_HALF) { + unsigned short *image = + reinterpret_cast(out_images)[c]; + if (line_order == 0) { + image += (static_cast(line_no) + v) * + static_cast(x_stride) + + u; + } else { + image += (static_cast(height) - 1U - + (static_cast(line_no) + v)) * + static_cast(x_stride) + + u; + } + *image = hf.u; + } else { // HALF -> FLOAT + tinyexr::FP32 f32 = half_to_float(hf); + float *image = reinterpret_cast(out_images)[c]; + if (line_order == 0) { + image += (static_cast(line_no) + v) * + static_cast(x_stride) + + u; + } else { + image += (static_cast(height) - 1U - + (static_cast(line_no) + v)) * + static_cast(x_stride) + + u; + } + *image = f32.f; + } + } + } + } else if (channels[c].pixel_type == TINYEXR_PIXELTYPE_UINT) { + TINYEXR_CHECK_AND_RETURN_C(requested_pixel_types[c] == TINYEXR_PIXELTYPE_UINT, false); + + for (size_t v = 0; v < ch_height; v++) { + const unsigned int *line_ptr = reinterpret_cast( + &outBuf.at(ch_offset + v * ch_width * 4)); + for (size_t u = 0; u < ch_width; u++) { + unsigned int val; + tinyexr::cpy4(&val, line_ptr + u); + tinyexr::swap4(&val); + + unsigned int *image = + reinterpret_cast(out_images)[c]; + if (line_order == 0) { + image += (static_cast(line_no) + v) * + static_cast(x_stride) + + u; + } else { + image += (static_cast(height) - 1U - + (static_cast(line_no) + v)) * + static_cast(x_stride) + + u; + } + *image = val; + } + } + } else if (channels[c].pixel_type == TINYEXR_PIXELTYPE_FLOAT) { + TINYEXR_CHECK_AND_RETURN_C(requested_pixel_types[c] == TINYEXR_PIXELTYPE_FLOAT, false); + for (size_t v = 0; v < ch_height; v++) { + const float *line_ptr = reinterpret_cast( + &outBuf.at(ch_offset + v * ch_width * 4)); + for (size_t u = 0; u < ch_width; u++) { + float val; + tinyexr::cpy4(&val, line_ptr + u); + tinyexr::swap4(reinterpret_cast(&val)); + + float *image = reinterpret_cast(out_images)[c]; + if (line_order == 0) { + image += (static_cast(line_no) + v) * + static_cast(x_stride) + + u; + } else { + image += (static_cast(height) - 1U - + (static_cast(line_no) + v)) * + static_cast(x_stride) + + u; + } + *image = val; + } + } + } else { + return false; + } + ch_offset += ch_width * ch_height * ch_bytes; + } } else if (compression_type == TINYEXR_COMPRESSIONTYPE_NONE) { for (size_t c = 0; c < num_channels; c++) { for (size_t v = 0; v < static_cast(num_lines); v++) { @@ -4584,6 +6011,13 @@ static int ParseEXRHeader(HeaderInfo *info, bool *empty_header, #endif } + // PXR24, B44, B44A compression types + if (data[0] == TINYEXR_COMPRESSIONTYPE_PXR24 || + data[0] == TINYEXR_COMPRESSIONTYPE_B44 || + data[0] == TINYEXR_COMPRESSIONTYPE_B44A) { + ok = true; + } + if (!ok) { if (err) { (*err) = "Unknown compression type."; @@ -5149,16 +6583,23 @@ static int DecodeChunk(EXRImage *exr_image, const EXRHeader *exr_header, num_scanline_blocks = 32; } else if (exr_header->compression_type == TINYEXR_COMPRESSIONTYPE_ZFP) { num_scanline_blocks = 16; + } else if (exr_header->compression_type == TINYEXR_COMPRESSIONTYPE_PXR24) { + num_scanline_blocks = 16; + } else if (exr_header->compression_type == TINYEXR_COMPRESSIONTYPE_B44 || + exr_header->compression_type == TINYEXR_COMPRESSIONTYPE_B44A) { + num_scanline_blocks = 32; + } #if TINYEXR_USE_ZFP + if (exr_header->compression_type == TINYEXR_COMPRESSIONTYPE_ZFP) { tinyexr::ZFPCompressionParam zfp_compression_param; if (!FindZFPCompressionParam(&zfp_compression_param, exr_header->custom_attributes, int(exr_header->num_custom_attributes), err)) { return TINYEXR_ERROR_INVALID_HEADER; } -#endif } +#endif if (exr_header->data_window.max_x < exr_header->data_window.min_x || exr_header->data_window.max_y < exr_header->data_window.min_y) { @@ -5988,6 +7429,11 @@ static int DecodeEXRImage(EXRImage *exr_image, const EXRHeader *exr_header, num_scanline_blocks = 32; } else if (exr_header->compression_type == TINYEXR_COMPRESSIONTYPE_ZFP) { num_scanline_blocks = 16; + } else if (exr_header->compression_type == TINYEXR_COMPRESSIONTYPE_PXR24) { + num_scanline_blocks = 16; + } else if (exr_header->compression_type == TINYEXR_COMPRESSIONTYPE_B44 || + exr_header->compression_type == TINYEXR_COMPRESSIONTYPE_B44A) { + num_scanline_blocks = 32; } if (exr_header->data_window.max_x < exr_header->data_window.min_x || @@ -6620,6 +8066,7 @@ int LoadEXRFromMemory(float **out_rgba, int *width, int *height, ret = ParseEXRHeaderFromMemory(&exr_header, &exr_version, memory, size, err); if (ret != TINYEXR_SUCCESS) { + FreeEXRHeader(&exr_header); return ret; } @@ -6633,6 +8080,8 @@ int LoadEXRFromMemory(float **out_rgba, int *width, int *height, InitEXRImage(&exr_image); ret = LoadEXRImageFromMemory(&exr_image, &exr_header, memory, size, err); if (ret != TINYEXR_SUCCESS) { + FreeEXRHeader(&exr_header); + FreeEXRImage(&exr_image); return ret; } @@ -7303,6 +8752,70 @@ static bool EncodePixelData(/* out */ std::vector& out_data, (void)compression_param; return false; #endif + } else if (compression_type == TINYEXR_COMPRESSIONTYPE_PXR24) { + // PXR24 compression: True PXR24 (truncates FLOAT to 24-bits + zlib) + std::vector block; + + if (!tinyexr::CompressPxr24(block, + reinterpret_cast(&buf.at(0)), + buf.size(), width, num_lines, + channels.size(), channels)) { + if (err) { + (*err) += "PXR24 compression failed.\n"; + } + return false; + } + + out_data.insert(out_data.end(), block.begin(), block.end()); + + } else if (compression_type == TINYEXR_COMPRESSIONTYPE_B44 || + compression_type == TINYEXR_COMPRESSIONTYPE_B44A) { + // B44/B44A compression: True B44 block compression for HALF data + bool is_b44a = (compression_type == TINYEXR_COMPRESSIONTYPE_B44A); + std::vector block; + + // CompressB44 expects per-channel sequential layout, but buf is + // scanline-interleaved: within each row, channels are stored contiguously + // (channel_offset_list[c] * width bytes into the row), and rows are + // stacked. Convert to per-channel sequential before compressing. + std::vector seq_buf(buf_size); + unsigned char *seq_p = seq_buf.data(); + for (size_t c = 0; c < channels.size(); c++) { + int file_type = channels[c].requested_pixel_type; + // HALF is 2 bytes; FLOAT and UINT are both 4 bytes + size_t ch_size = (file_type == TINYEXR_PIXELTYPE_HALF) ? 2 : 4; + for (int y = 0; y < num_lines; y++) { + const unsigned char *src = + &buf[y * pixel_data_size * width + channel_offset_list[c] * width]; + size_t row_bytes = static_cast(width) * ch_size; + memcpy(seq_p, src, row_bytes); +#if !TINYEXR_LITTLE_ENDIAN + // buf has already been byte-swapped to little-endian for file output. + // CompressB44 reads HALF values as host-endian unsigned shorts, so + // un-swap the bytes back to host-endian for correct B44 encoding. + if (file_type == TINYEXR_PIXELTYPE_HALF) { + unsigned short *p = reinterpret_cast(seq_p); + for (int x = 0; x < width; x++) { + tinyexr::swap2(p + x); + } + } +#endif + seq_p += row_bytes; + } + } + + if (!tinyexr::CompressB44(block, + reinterpret_cast(seq_buf.data()), + seq_buf.size(), width, num_lines, + channels.size(), channels, is_b44a)) { + if (err) { + (*err) += "B44 compression failed.\n"; + } + return false; + } + + out_data.insert(out_data.end(), block.begin(), block.end()); + } else { return false; } @@ -7446,6 +8959,11 @@ static int NumScanlines(int compression_type) { num_scanlines = 32; } else if (compression_type == TINYEXR_COMPRESSIONTYPE_ZFP) { num_scanlines = 16; + } else if (compression_type == TINYEXR_COMPRESSIONTYPE_PXR24) { + num_scanlines = 16; // PXR24 uses 16 scanlines per block (same as ZIP) + } else if (compression_type == TINYEXR_COMPRESSIONTYPE_B44 || + compression_type == TINYEXR_COMPRESSIONTYPE_B44A) { + num_scanlines = 32; // B44/B44A uses 32 scanlines per block } return num_scanlines; } @@ -8296,7 +9814,7 @@ int LoadDeepEXR(DeepImage *deep_image, const char *filename, const char **err) { if (attr_name.compare("compression") == 0) { compression_type = data[0]; - if (compression_type > TINYEXR_COMPRESSIONTYPE_PIZ) { + if (compression_type > TINYEXR_COMPRESSIONTYPE_B44A) { std::stringstream ss; ss << "Unsupported compression type : " << compression_type; tinyexr::SetErrorMessage(ss.str(), err); @@ -8305,6 +9823,9 @@ int LoadDeepEXR(DeepImage *deep_image, const char *filename, const char **err) { if (compression_type == TINYEXR_COMPRESSIONTYPE_ZIP) { num_scanline_blocks = 16; + } else if (compression_type == TINYEXR_COMPRESSIONTYPE_B44 || + compression_type == TINYEXR_COMPRESSIONTYPE_B44A) { + num_scanline_blocks = 32; } } else if (attr_name.compare("channels") == 0) { @@ -8665,16 +10186,19 @@ int FreeEXRImage(EXRImage *exr_image) { if (exr_image->next_level) { FreeEXRImage(exr_image->next_level); delete exr_image->next_level; + exr_image->next_level = NULL; } for (int i = 0; i < exr_image->num_channels; i++) { if (exr_image->images && exr_image->images[i]) { free(exr_image->images[i]); + exr_image->images[i] = NULL; } } if (exr_image->images) { free(exr_image->images); + exr_image->images = NULL; } if (exr_image->tiles) { @@ -8682,15 +10206,21 @@ int FreeEXRImage(EXRImage *exr_image) { for (int i = 0; i < exr_image->num_channels; i++) { if (exr_image->tiles[tid].images && exr_image->tiles[tid].images[i]) { free(exr_image->tiles[tid].images[i]); + exr_image->tiles[tid].images[i] = NULL; } } if (exr_image->tiles[tid].images) { free(exr_image->tiles[tid].images); + exr_image->tiles[tid].images = NULL; } } free(exr_image->tiles); + exr_image->tiles = NULL; } + exr_image->num_channels = 0; + exr_image->num_tiles = 0; + return TINYEXR_SUCCESS; } @@ -8844,6 +10374,71 @@ int ParseEXRMultipartHeaderFromFile(EXRHeader ***exr_headers, int *num_headers, exr_headers, num_headers, exr_version, file.data, file.size, err); } +// ======================================================================== +// Refactored loader functions using Reader class for safer memory access +// ======================================================================== + +namespace { // anonymous namespace for internal helpers + +// Parse EXR version header using Reader class +static int ParseEXRVersionWithReader(EXRVersion *version, tinyexr::Reader& reader) { + if (version == NULL) { + return TINYEXR_ERROR_INVALID_ARGUMENT; + } + + if (reader.length() < tinyexr::kEXRVersionSize) { + reader.add_error("Insufficient data size for EXR version header"); + return TINYEXR_ERROR_INVALID_DATA; + } + + // Check magic number: 0x76, 0x2f, 0x31, 0x01 + uint8_t header[4]; + if (!reader.read(4, header)) { + return TINYEXR_ERROR_INVALID_DATA; + } + + const uint8_t expected_header[] = {0x76, 0x2f, 0x31, 0x01}; + if (header[0] != expected_header[0] || header[1] != expected_header[1] || + header[2] != expected_header[2] || header[3] != expected_header[3]) { + reader.add_error("Invalid EXR magic number"); + return TINYEXR_ERROR_INVALID_MAGIC_NUMBER; + } + + // Parse version byte (must be 2) + uint8_t version_byte; + if (!reader.read1(&version_byte)) { + return TINYEXR_ERROR_INVALID_DATA; + } + + if (version_byte != 2) { + reader.add_error("Invalid EXR version"); + return TINYEXR_ERROR_INVALID_EXR_VERSION; + } + + version->version = 2; + + // Parse flags byte + uint8_t flags; + if (!reader.read1(&flags)) { + return TINYEXR_ERROR_INVALID_DATA; + } + + version->tiled = (flags & 0x2) ? true : false; // 9th bit + version->long_name = (flags & 0x4) ? true : false; // 10th bit + version->non_image = (flags & 0x8) ? true : false; // 11th bit (deep image) + version->multipart = (flags & 0x10) ? true : false; // 12th bit + + // Skip remaining 2 bytes to complete the 8-byte version header + uint8_t dummy[2]; + if (!reader.read(2, dummy)) { + return TINYEXR_ERROR_INVALID_DATA; + } + + return TINYEXR_SUCCESS; +} + +} // anonymous namespace + int ParseEXRVersionFromMemory(EXRVersion *version, const unsigned char *memory, size_t size) { if (version == NULL || memory == NULL) { @@ -8854,51 +10449,13 @@ int ParseEXRVersionFromMemory(EXRVersion *version, const unsigned char *memory, return TINYEXR_ERROR_INVALID_DATA; } - const unsigned char *marker = memory; + // Use Reader class for safer memory access + tinyexr::Reader reader(memory, size, tinyexr::Endian::Little); + int ret = ParseEXRVersionWithReader(version, reader); - // Header check. - { - const char header[] = {0x76, 0x2f, 0x31, 0x01}; - - if (memcmp(marker, header, 4) != 0) { - return TINYEXR_ERROR_INVALID_MAGIC_NUMBER; - } - marker += 4; - } - - version->tiled = false; - version->long_name = false; - version->non_image = false; - version->multipart = false; - - // Parse version header. - { - // must be 2 - if (marker[0] != 2) { - return TINYEXR_ERROR_INVALID_EXR_VERSION; - } - - if (version == NULL) { - return TINYEXR_SUCCESS; // May OK - } - - version->version = 2; - - if (marker[1] & 0x2) { // 9th bit - version->tiled = true; - } - if (marker[1] & 0x4) { // 10th bit - version->long_name = true; - } - if (marker[1] & 0x8) { // 11th bit - version->non_image = true; // (deep image) - } - if (marker[1] & 0x10) { // 12th bit - version->multipart = true; - } - } - - return TINYEXR_SUCCESS; + // Note: errors are accumulated in reader.errors() but not propagated + // to maintain compatibility with existing API + return ret; } int ParseEXRVersionFromFile(EXRVersion *version, const char *filename) { @@ -9391,6 +10948,394 @@ int SaveEXR(const float *data, int width, int height, int components, return ret; } +// ---------------------------------------------------------------- +// Spectral EXR API implementations +// ---------------------------------------------------------------- + +// Helper to format wavelength with comma as decimal separator (European convention) +void EXRFormatWavelength(char *buffer, size_t buffer_size, float wavelength_nm) { + if (!buffer || buffer_size < 16) return; + + // Format with 6 decimal places + int whole = static_cast(wavelength_nm); + int frac = static_cast((wavelength_nm - whole) * 1000000.0f + 0.5f); + +#ifdef _MSC_VER + sprintf_s(buffer, buffer_size, "%d,%06d", whole, frac); +#else + snprintf(buffer, buffer_size, "%d,%06d", whole, frac); +#endif +} + +// Create spectral channel name for emissive spectrum +void EXRSpectralChannelName(char *buffer, size_t buffer_size, + float wavelength_nm, int stokes_component) { + if (!buffer || buffer_size < 32) return; + + char wavelength_str[32]; + EXRFormatWavelength(wavelength_str, sizeof(wavelength_str), wavelength_nm); + +#ifdef _MSC_VER + sprintf_s(buffer, buffer_size, "S%d.%snm", stokes_component, wavelength_str); +#else + snprintf(buffer, buffer_size, "S%d.%snm", stokes_component, wavelength_str); +#endif +} + +// Create spectral channel name for reflective spectrum +void EXRReflectiveChannelName(char *buffer, size_t buffer_size, + float wavelength_nm) { + if (!buffer || buffer_size < 32) return; + + char wavelength_str[32]; + EXRFormatWavelength(wavelength_str, sizeof(wavelength_str), wavelength_nm); + +#ifdef _MSC_VER + sprintf_s(buffer, buffer_size, "T.%snm", wavelength_str); +#else + snprintf(buffer, buffer_size, "T.%snm", wavelength_str); +#endif +} + +// Parse wavelength from spectral channel name +float EXRParseSpectralChannelWavelength(const char *channel_name) { + if (!channel_name) return -1.0f; + + const char *p = channel_name; + + // Skip prefix: "S{n}." or "T." + if (*p == 'S' && p[1] >= '0' && p[1] <= '3' && p[2] == '.') { + p += 3; + } else if (*p == 'T' && p[1] == '.') { + p += 2; + } else { + return -1.0f; + } + + // Parse wavelength with comma as decimal separator + // Format: "550,000000nm" + char wavelength_str[64]; + size_t len = 0; + while (*p && *p != 'n' && len < sizeof(wavelength_str) - 1) { + wavelength_str[len++] = (*p == ',') ? '.' : *p; + p++; + } + wavelength_str[len] = '\0'; + + // Check for "nm" suffix + if (*p != 'n' || p[1] != 'm') { + return -1.0f; + } + + return static_cast(atof(wavelength_str)); +} + +// Get Stokes component from channel name +int EXRGetStokesComponent(const char *channel_name) { + if (!channel_name) return -1; + + if (channel_name[0] == 'S' && + channel_name[1] >= '0' && channel_name[1] <= '3' && + channel_name[2] == '.') { + return channel_name[1] - '0'; + } + + return -1; +} + +// Check if channel name is a spectral channel +int EXRIsSpectralChannel(const char *channel_name) { + if (!channel_name) return 0; + + // Check for "S{n}.{wavelength}nm" pattern + if (channel_name[0] == 'S' && + channel_name[1] >= '0' && channel_name[1] <= '3' && + channel_name[2] == '.') { + return EXRParseSpectralChannelWavelength(channel_name) > 0.0f ? 1 : 0; + } + + // Check for "T.{wavelength}nm" pattern + if (channel_name[0] == 'T' && channel_name[1] == '.') { + return EXRParseSpectralChannelWavelength(channel_name) > 0.0f ? 1 : 0; + } + + return 0; +} + +// Helper to find custom attribute by name +static const EXRAttribute* FindCustomAttribute(const EXRHeader *exr_header, + const char *name) { + if (!exr_header || !name) return NULL; + + for (int i = 0; i < exr_header->num_custom_attributes; i++) { + if (strcmp(exr_header->custom_attributes[i].name, name) == 0) { + return &exr_header->custom_attributes[i]; + } + } + return NULL; +} + +// Get spectrum type from EXR header +int EXRGetSpectrumType(const EXRHeader *exr_header) { + if (!exr_header) return -1; + + // Check for spectralLayoutVersion attribute + const EXRAttribute *layout_attr = FindCustomAttribute(exr_header, "spectralLayoutVersion"); + if (!layout_attr) return -1; + + // Check channel names to determine type + int has_stokes = 0; + int has_reflective = 0; + int has_emissive = 0; + + for (int i = 0; i < exr_header->num_channels; i++) { + const char *name = exr_header->channels[i].name; + if (name[0] == 'T' && name[1] == '.') { + has_reflective = 1; + } else if (name[0] == 'S' && name[1] >= '0' && name[1] <= '3' && name[2] == '.') { + has_emissive = 1; + if (name[1] != '0') { + has_stokes = 1; + } + } + } + + if (has_reflective) return TINYEXR_SPECTRUM_REFLECTIVE; + if (has_stokes) return TINYEXR_SPECTRUM_POLARISED; + if (has_emissive) return TINYEXR_SPECTRUM_EMISSIVE; + + return -1; +} + +// Get wavelengths from EXR header channels +int EXRGetWavelengths(const EXRHeader *exr_header, + float *wavelengths, int max_wavelengths) { + if (!exr_header || !wavelengths || max_wavelengths <= 0) return 0; + + int count = 0; + + for (int i = 0; i < exr_header->num_channels && count < max_wavelengths; i++) { + float wl = EXRParseSpectralChannelWavelength(exr_header->channels[i].name); + if (wl > 0.0f) { + // Check if wavelength already in list + int found = 0; + for (int j = 0; j < count; j++) { + if (std::fabs(static_cast(wavelengths[j] - wl)) < 0.01) { + found = 1; + break; + } + } + if (!found) { + wavelengths[count++] = wl; + } + } + } + + // Sort wavelengths + for (int i = 0; i < count - 1; i++) { + for (int j = i + 1; j < count; j++) { + if (wavelengths[i] > wavelengths[j]) { + float tmp = wavelengths[i]; + wavelengths[i] = wavelengths[j]; + wavelengths[j] = tmp; + } + } + } + + return count; +} + +// Get spectral units from EXR header +const char* EXRGetSpectralUnits(const EXRHeader *exr_header) { + if (!exr_header) return NULL; + + // Try ROOT/units first (spectral-exr format) + const EXRAttribute *attr = FindCustomAttribute(exr_header, "ROOT/units"); + if (attr && attr->value && attr->size > 0) { + return reinterpret_cast(attr->value); + } + + // Try emissiveUnits + attr = FindCustomAttribute(exr_header, "emissiveUnits"); + if (attr && attr->value && attr->size > 0) { + return reinterpret_cast(attr->value); + } + + return NULL; +} + +// Helper to add a string attribute +static int AddStringAttribute(EXRHeader *exr_header, const char *name, const char *value) { + if (!exr_header || !name || !value) return TINYEXR_ERROR_INVALID_ARGUMENT; + + int new_count = exr_header->num_custom_attributes + 1; + if (new_count > TINYEXR_MAX_CUSTOM_ATTRIBUTES) { + return TINYEXR_ERROR_DATA_TOO_LARGE; + } + + // Reallocate attributes array + EXRAttribute *new_attrs = static_cast( + realloc(exr_header->custom_attributes, + sizeof(EXRAttribute) * static_cast(new_count))); + if (!new_attrs) { + return TINYEXR_ERROR_INVALID_DATA; + } + + exr_header->custom_attributes = new_attrs; + EXRAttribute *attr = &exr_header->custom_attributes[exr_header->num_custom_attributes]; + + // Initialize the new attribute + memset(attr, 0, sizeof(EXRAttribute)); + +#ifdef _MSC_VER + strncpy_s(attr->name, sizeof(attr->name), name, 255); + strncpy_s(attr->type, sizeof(attr->type), "string", 255); +#else + strncpy(attr->name, name, 255); + attr->name[255] = '\0'; + strncpy(attr->type, "string", 255); + attr->type[255] = '\0'; +#endif + + size_t value_len = strlen(value) + 1; // Include null terminator + attr->value = static_cast(malloc(value_len)); + if (!attr->value) { + return TINYEXR_ERROR_INVALID_DATA; + } + memcpy(attr->value, value, value_len); + attr->size = static_cast(value_len); + + exr_header->num_custom_attributes = new_count; + + return TINYEXR_SUCCESS; +} + +// Helper to add an int attribute +static int AddIntAttribute(EXRHeader *exr_header, const char *name, int value) { + if (!exr_header || !name) return TINYEXR_ERROR_INVALID_ARGUMENT; + + int new_count = exr_header->num_custom_attributes + 1; + if (new_count > TINYEXR_MAX_CUSTOM_ATTRIBUTES) { + return TINYEXR_ERROR_DATA_TOO_LARGE; + } + + // Reallocate attributes array + EXRAttribute *new_attrs = static_cast( + realloc(exr_header->custom_attributes, + sizeof(EXRAttribute) * static_cast(new_count))); + if (!new_attrs) { + return TINYEXR_ERROR_INVALID_DATA; + } + + exr_header->custom_attributes = new_attrs; + EXRAttribute *attr = &exr_header->custom_attributes[exr_header->num_custom_attributes]; + + // Initialize the new attribute + memset(attr, 0, sizeof(EXRAttribute)); + +#ifdef _MSC_VER + strncpy_s(attr->name, sizeof(attr->name), name, 255); + strncpy_s(attr->type, sizeof(attr->type), "int", 255); +#else + strncpy(attr->name, name, 255); + attr->name[255] = '\0'; + strncpy(attr->type, "int", 255); + attr->type[255] = '\0'; +#endif + + attr->value = static_cast(malloc(sizeof(int))); + if (!attr->value) { + return TINYEXR_ERROR_INVALID_DATA; + } + memcpy(attr->value, &value, sizeof(int)); + attr->size = sizeof(int); + + exr_header->num_custom_attributes = new_count; + + return TINYEXR_SUCCESS; +} + +// Set spectral attributes on EXR header +int EXRSetSpectralAttributes(EXRHeader *exr_header, + int spectrum_type, + const char *units) { + if (!exr_header) return TINYEXR_ERROR_INVALID_ARGUMENT; + + int ret; + + // Add spectralLayoutVersion (always "1.0") + ret = AddStringAttribute(exr_header, "spectralLayoutVersion", "1.0"); + if (ret != TINYEXR_SUCCESS) return ret; + + // Add units attribute based on spectrum type + if (units && strlen(units) > 0) { + if (spectrum_type == TINYEXR_SPECTRUM_REFLECTIVE) { + ret = AddStringAttribute(exr_header, "ROOT/units", units); + } else { + ret = AddStringAttribute(exr_header, "emissiveUnits", units); + } + if (ret != TINYEXR_SUCCESS) return ret; + } + + // Add polarisation handedness for polarised images + if (spectrum_type == TINYEXR_SPECTRUM_POLARISED) { + ret = AddStringAttribute(exr_header, "polarisationHandedness", "left"); + if (ret != TINYEXR_SUCCESS) return ret; + } + + return TINYEXR_SUCCESS; +} + +// Check if file contains spectral data +int IsSpectralEXR(const char *filename) { + EXRVersion version; + int ret = ParseEXRVersionFromFile(&version, filename); + if (ret != TINYEXR_SUCCESS) return ret; + + const char *err = NULL; + EXRHeader header; + InitEXRHeader(&header); + + ret = ParseEXRHeaderFromFile(&header, &version, filename, &err); + if (ret != TINYEXR_SUCCESS) { + if (err) FreeEXRErrorMessage(err); + return ret; + } + + // Check for spectralLayoutVersion attribute + int is_spectral = (FindCustomAttribute(&header, "spectralLayoutVersion") != NULL); + + FreeEXRHeader(&header); + + return is_spectral ? TINYEXR_SUCCESS : TINYEXR_ERROR_INVALID_DATA; +} + +// Check if memory contains spectral EXR data +int IsSpectralEXRFromMemory(const unsigned char *memory, size_t size) { + if (!memory || size < 8) return TINYEXR_ERROR_INVALID_DATA; + + EXRVersion version; + int ret = ParseEXRVersionFromMemory(&version, memory, size); + if (ret != TINYEXR_SUCCESS) return ret; + + const char *err = NULL; + EXRHeader header; + InitEXRHeader(&header); + + ret = ParseEXRHeaderFromMemory(&header, &version, memory, size, &err); + if (ret != TINYEXR_SUCCESS) { + if (err) FreeEXRErrorMessage(err); + return ret; + } + + // Check for spectralLayoutVersion attribute + int is_spectral = (FindCustomAttribute(&header, "spectralLayoutVersion") != NULL); + + FreeEXRHeader(&header); + + return is_spectral ? TINYEXR_SUCCESS : TINYEXR_ERROR_INVALID_DATA; +} + #ifdef __clang__ // zero-as-null-pointer-constant #pragma clang diagnostic pop