imageio-lite: add simple tiff import/export (#9654)
- Add new library to do tiff import/export. This library is different from imageio in that it doesn't pull in additional 3p libraries. This reduces binary size and reduces complexity in maintaining the android build (which depends on libs/viewer). - The encode() code has been moved from libs/viewer to libs/imageio-lite - encode/decode only handles the simplest case of uncompressed rgba.
This commit is contained in:
@@ -904,6 +904,8 @@ add_subdirectory(${EXTERNAL}/getopt)
|
||||
add_subdirectory(${EXTERNAL}/perfetto/tnt)
|
||||
add_subdirectory(${EXTERNAL}/basisu/tnt)
|
||||
|
||||
# imageio-lite is needed for viewer
|
||||
add_subdirectory(${LIBRARIES}/imageio-lite)
|
||||
|
||||
# Note that this has to be placed after mikktspace in order for combine_static_libs to work.
|
||||
add_subdirectory(${LIBRARIES}/geometry)
|
||||
|
||||
@@ -22,6 +22,10 @@ add_library(image STATIC IMPORTED)
|
||||
set_target_properties(image PROPERTIES IMPORTED_LOCATION
|
||||
${FILAMENT_DIR}/lib/${ANDROID_ABI}/libimage.a)
|
||||
|
||||
add_library(imageio-lite STATIC IMPORTED)
|
||||
set_target_properties(imageio-lite PROPERTIES IMPORTED_LOCATION
|
||||
${FILAMENT_DIR}/lib/${ANDROID_ABI}/libimageio-lite.a)
|
||||
|
||||
add_library(ktxreader STATIC IMPORTED)
|
||||
set_target_properties(ktxreader PROPERTIES IMPORTED_LOCATION
|
||||
${FILAMENT_DIR}/lib/${ANDROID_ABI}/libktxreader.a)
|
||||
@@ -85,6 +89,7 @@ target_link_libraries(filament-utils-jni
|
||||
PRIVATE camutils
|
||||
PRIVATE iblprefilter
|
||||
PRIVATE image
|
||||
PRIVATE imageio-lite
|
||||
PRIVATE filament-jni
|
||||
PRIVATE ktxreader
|
||||
PRIVATE viewer
|
||||
|
||||
52
libs/imageio-lite/CMakeLists.txt
Normal file
52
libs/imageio-lite/CMakeLists.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
cmake_minimum_required(VERSION 3.19)
|
||||
project(imageio-lite)
|
||||
|
||||
set(TARGET imageio-lite)
|
||||
set(PUBLIC_HDR_DIR include)
|
||||
|
||||
# ==================================================================================================
|
||||
# Sources and headers
|
||||
# ==================================================================================================
|
||||
set(PUBLIC_HDRS
|
||||
include/imageio-lite/ImageDecoder.h
|
||||
include/imageio-lite/ImageEncoder.h
|
||||
)
|
||||
|
||||
set(SRCS
|
||||
src/ImageDecoder.cpp
|
||||
src/ImageEncoder.cpp
|
||||
)
|
||||
|
||||
# ==================================================================================================
|
||||
# Include and target definitions
|
||||
# ==================================================================================================
|
||||
include_directories(${PUBLIC_HDR_DIR})
|
||||
|
||||
add_library(${TARGET} STATIC ${PUBLIC_HDRS} ${SRCS})
|
||||
|
||||
target_include_directories(${TARGET} PUBLIC ${PUBLIC_HDR_DIR})
|
||||
set_target_properties(${TARGET} PROPERTIES FOLDER Libs)
|
||||
|
||||
target_link_libraries(${TARGET} PUBLIC image math utils)
|
||||
|
||||
# ==================================================================================================
|
||||
# Compiler flags
|
||||
# ==================================================================================================
|
||||
if (NOT MSVC)
|
||||
target_compile_options(${TARGET} PRIVATE -Wno-deprecated-register)
|
||||
endif()
|
||||
|
||||
# ==================================================================================================
|
||||
# Installation
|
||||
# ==================================================================================================
|
||||
install(TARGETS ${TARGET} ARCHIVE DESTINATION lib/${DIST_DIR})
|
||||
install(DIRECTORY ${PUBLIC_HDR_DIR}/imageio-lite DESTINATION include)
|
||||
|
||||
# ==================================================================================================
|
||||
# Tests
|
||||
# ==================================================================================================
|
||||
if (NOT ANDROID AND NOT WEBGL AND NOT IOS)
|
||||
add_executable(test_${TARGET} tests/test_imageio-lite.cpp)
|
||||
target_link_libraries(test_${TARGET} PRIVATE ${TARGET} image gtest)
|
||||
set_target_properties(test_${TARGET} PROPERTIES FOLDER Tests)
|
||||
endif()
|
||||
12
libs/imageio-lite/README.md
Normal file
12
libs/imageio-lite/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# imageio-lite
|
||||
|
||||
`imageio-lite` is a lightweight image input/output library for Filament.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
* **No Third-Party Dependencies:** Unlike `imageio`, this library must NOT depend on external libraries like `libpng`, `tinyexr`, etc. All image encoding/decoding logic must be implemented within this library or rely only on Filament's core libraries (`utils`, `math`, `image`).
|
||||
* **Lite:** It is intended for basic import/export tasks where dragging in full-blown image libraries is undesirable (e.g., to keep binary size small or build simple).
|
||||
|
||||
## Supported Formats
|
||||
|
||||
* **TIFF:** Read and Write support.
|
||||
54
libs/imageio-lite/include/imageio-lite/ImageDecoder.h
Normal file
54
libs/imageio-lite/include/imageio-lite/ImageDecoder.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef IMAGEIO_LITE_IMAGEDECODER_H_
|
||||
#define IMAGEIO_LITE_IMAGEDECODER_H_
|
||||
|
||||
#include <image/LinearImage.h>
|
||||
|
||||
#include <utils/CString.h>
|
||||
#include <utils/compiler.h>
|
||||
|
||||
#include <iosfwd>
|
||||
|
||||
namespace imageio_lite {
|
||||
|
||||
class UTILS_PUBLIC ImageDecoder {
|
||||
public:
|
||||
enum class ColorSpace { LINEAR, SRGB };
|
||||
|
||||
// Returns linear floating-point data, or a non-valid image if an error occurred.
|
||||
static image::LinearImage decode(std::istream& stream, utils::CString const& sourceName,
|
||||
ColorSpace sourceSpace = ColorSpace::SRGB);
|
||||
|
||||
class Decoder {
|
||||
public:
|
||||
virtual image::LinearImage decode() = 0;
|
||||
virtual ~Decoder() = default;
|
||||
ColorSpace getColorSpace() const noexcept { return mColorSpace; }
|
||||
void setColorSpace(ColorSpace colorSpace) noexcept { mColorSpace = colorSpace; }
|
||||
|
||||
private:
|
||||
ColorSpace mColorSpace = ColorSpace::SRGB;
|
||||
};
|
||||
|
||||
private:
|
||||
enum class Format { NONE, TIFF };
|
||||
};
|
||||
|
||||
} // namespace imageio_lite
|
||||
|
||||
#endif /* IMAGEIO_LITE_IMAGEDECODER_H_ */
|
||||
51
libs/imageio-lite/include/imageio-lite/ImageEncoder.h
Normal file
51
libs/imageio-lite/include/imageio-lite/ImageEncoder.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef IMAGEIO_LITE_IMAGEENCODER_H_
|
||||
#define IMAGEIO_LITE_IMAGEENCODER_H_
|
||||
|
||||
#include <image/LinearImage.h>
|
||||
|
||||
#include <utils/CString.h>
|
||||
#include <utils/compiler.h>
|
||||
|
||||
#include <iosfwd>
|
||||
|
||||
namespace imageio_lite {
|
||||
|
||||
class UTILS_PUBLIC ImageEncoder {
|
||||
public:
|
||||
enum class Format {
|
||||
TIFF, // 8-bit sRGB, 4 channels (RGBA)
|
||||
};
|
||||
|
||||
// Consumes linear floating-point data, returns false if unable to encode.
|
||||
static bool encode(std::ostream& stream, Format format, image::LinearImage const& image,
|
||||
utils::CString const& compression, utils::CString const& destName);
|
||||
|
||||
static Format chooseFormat(utils::CString const& name, bool forceLinear = false);
|
||||
static utils::CString chooseExtension(Format format);
|
||||
|
||||
class Encoder {
|
||||
public:
|
||||
virtual bool encode(const image::LinearImage& image) = 0;
|
||||
virtual ~Encoder() = default;
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace imageio_lite
|
||||
|
||||
#endif /* IMAGEIO_LITE_IMAGEENCODER_H_ */
|
||||
345
libs/imageio-lite/src/ImageDecoder.cpp
Normal file
345
libs/imageio-lite/src/ImageDecoder.cpp
Normal file
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <imageio-lite/ImageDecoder.h>
|
||||
|
||||
#include <image/ColorTransform.h>
|
||||
#include <image/ImageOps.h>
|
||||
|
||||
#include <math/vec3.h>
|
||||
#include <math/vec4.h>
|
||||
|
||||
#include <utils/Panic.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <istream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
// for ntohs
|
||||
#if defined(WIN32)
|
||||
#include <Winsock2.h>
|
||||
#include <utils/unwindows.h>
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
using namespace image;
|
||||
|
||||
namespace imageio_lite {
|
||||
|
||||
class TIFFDecoder : public ImageDecoder::Decoder {
|
||||
public:
|
||||
static TIFFDecoder* create(std::istream& stream);
|
||||
static bool checkSignature(char const* buf);
|
||||
|
||||
TIFFDecoder(const TIFFDecoder&) = delete;
|
||||
TIFFDecoder& operator=(const TIFFDecoder&) = delete;
|
||||
|
||||
private:
|
||||
explicit TIFFDecoder(std::istream& stream);
|
||||
~TIFFDecoder() override = default;
|
||||
|
||||
// ImageDecoder::Decoder interface
|
||||
LinearImage decode() override;
|
||||
|
||||
std::istream& mStream;
|
||||
std::streampos mStreamStartPos;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
image::LinearImage ImageDecoder::decode(std::istream& stream, utils::CString const& sourceName,
|
||||
ColorSpace sourceSpace) {
|
||||
|
||||
Format format = Format::NONE;
|
||||
|
||||
std::streampos pos = stream.tellg();
|
||||
char buf[16];
|
||||
stream.read(buf, sizeof(buf));
|
||||
|
||||
if (TIFFDecoder::checkSignature(buf)) {
|
||||
format = Format::TIFF;
|
||||
}
|
||||
|
||||
stream.seekg(pos);
|
||||
|
||||
std::unique_ptr<Decoder> decoder;
|
||||
switch (format) {
|
||||
case Format::NONE:
|
||||
return LinearImage();
|
||||
case Format::TIFF:
|
||||
decoder.reset(TIFFDecoder::create(stream));
|
||||
decoder->setColorSpace(sourceSpace);
|
||||
break;
|
||||
}
|
||||
|
||||
return decoder->decode();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
static inline float read32(std::istream& istream) {
|
||||
uint32_t data;
|
||||
istream.read(reinterpret_cast<char*>(&data), sizeof(uint32_t));
|
||||
data = ntohl(data);
|
||||
return *reinterpret_cast<float*>(&data);
|
||||
}
|
||||
|
||||
static inline float read16(std::istream& istream) {
|
||||
uint16_t data;
|
||||
istream.read(reinterpret_cast<char*>(&data), sizeof(uint16_t));
|
||||
return static_cast<float>(ntohs(data)) / std::numeric_limits<uint16_t>::max();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// TIFF Tag Definitions
|
||||
constexpr uint16_t ImageWidth = 256;
|
||||
constexpr uint16_t ImageLength = 257;
|
||||
constexpr uint16_t BitsPerSample = 258;
|
||||
constexpr uint16_t Compression = 259;
|
||||
constexpr uint16_t PhotometricInterpretation = 262;
|
||||
constexpr uint16_t StripOffsets = 273;
|
||||
constexpr uint16_t SamplesPerPixel = 277;
|
||||
constexpr uint16_t RowsPerStrip = 278;
|
||||
constexpr uint16_t StripByteCounts = 279;
|
||||
constexpr uint16_t PlanarConfiguration = 284;
|
||||
|
||||
|
||||
// TIFF Type Definitions
|
||||
constexpr uint16_t SHORT = 3;
|
||||
constexpr uint16_t LONG = 4;
|
||||
|
||||
constexpr uint32_t MAX_STRIPS = 1000000;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
TIFFDecoder* TIFFDecoder::create(std::istream& stream) { return new TIFFDecoder(stream); }
|
||||
|
||||
bool TIFFDecoder::checkSignature(char const* buf) {
|
||||
return memcmp(buf, "II\x2a\x00", 4) == 0 || memcmp(buf, "MM\x00\x2a", 4) == 0;
|
||||
}
|
||||
|
||||
TIFFDecoder::TIFFDecoder(std::istream& stream)
|
||||
: mStream(stream),
|
||||
mStreamStartPos(stream.tellg()) {}
|
||||
|
||||
LinearImage TIFFDecoder::decode() {
|
||||
char header[8];
|
||||
mStream.read(header, 8);
|
||||
FILAMENT_CHECK_PRECONDITION(mStream.gcount() == 8) << "Incomplete TIFF header";
|
||||
|
||||
bool const littleEndian = (header[0] == 0x49 && header[1] == 0x49);
|
||||
|
||||
auto const read16 = [&](std::istream& s) -> uint16_t {
|
||||
uint8_t b[2];
|
||||
s.read(reinterpret_cast<char*>(b), 2);
|
||||
if (littleEndian) {
|
||||
return uint16_t(b[0]) | (uint16_t(b[1]) << 8);
|
||||
}
|
||||
return uint16_t(b[1]) | (uint16_t(b[0]) << 8);
|
||||
};
|
||||
|
||||
auto const read32 = [&](std::istream& s) -> uint32_t {
|
||||
uint8_t b[4];
|
||||
s.read(reinterpret_cast<char*>(b), 4);
|
||||
if (littleEndian) {
|
||||
return uint32_t(b[0]) | (uint32_t(b[1]) << 8) | (uint32_t(b[2]) << 16) |
|
||||
(uint32_t(b[3]) << 24);
|
||||
}
|
||||
return uint32_t(b[3]) | (uint32_t(b[2]) << 8) | (uint32_t(b[1]) << 16) |
|
||||
(uint32_t(b[0]) << 24);
|
||||
};
|
||||
|
||||
auto const seek = [&](uint32_t offset) {
|
||||
mStream.seekg(mStreamStartPos + (std::streamoff) offset, std::ios::beg);
|
||||
};
|
||||
|
||||
// IFD offset is bytes 4-7
|
||||
uint32_t const ifdOffset =
|
||||
littleEndian ? (uint8_t) header[4] | ((uint8_t) header[5] << 8) |
|
||||
((uint8_t) header[6] << 16) | ((uint8_t) header[7] << 24)
|
||||
: (uint8_t) header[7] | ((uint8_t) header[6] << 8) |
|
||||
((uint8_t) header[5] << 16) | ((uint8_t) header[4] << 24);
|
||||
|
||||
seek(ifdOffset);
|
||||
|
||||
uint16_t const numEntries = read16(mStream);
|
||||
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
uint16_t compression = 1;
|
||||
uint16_t photometric = 0;
|
||||
uint16_t samplesPerPixel = 1;
|
||||
uint16_t bitsPerSample = 8;
|
||||
uint16_t planarConfiguration = 1;
|
||||
std::vector<uint32_t> stripOffsets;
|
||||
std::vector<uint32_t> stripByteCounts;
|
||||
|
||||
for (int i = 0; i < numEntries; ++i) {
|
||||
uint16_t const tag = read16(mStream);
|
||||
uint16_t const type = read16(mStream);
|
||||
uint32_t const count = read32(mStream);
|
||||
|
||||
// Value or offset field (4 bytes)
|
||||
uint8_t voBuf[4];
|
||||
mStream.read(reinterpret_cast<char*>(voBuf), 4);
|
||||
uint32_t const valueOrOffset =
|
||||
littleEndian ? (uint32_t) voBuf[0] | ((uint32_t) voBuf[1] << 8) |
|
||||
((uint32_t) voBuf[2] << 16) | ((uint32_t) voBuf[3] << 24)
|
||||
: (uint32_t) voBuf[3] | ((uint32_t) voBuf[2] << 8) |
|
||||
((uint32_t) voBuf[1] << 16) | ((uint32_t) voBuf[0] << 24);
|
||||
|
||||
// Save current position
|
||||
std::streampos const nextEntry = mStream.tellg();
|
||||
|
||||
// Correct SHORT decoding for Big Endian
|
||||
uint32_t entryValue = valueOrOffset;
|
||||
if (!littleEndian && type == SHORT && count == 1) {
|
||||
entryValue >>= 16;
|
||||
}
|
||||
|
||||
if (tag == ImageWidth) {
|
||||
width = entryValue;
|
||||
} else if (tag == ImageLength) {
|
||||
height = entryValue;
|
||||
} else if (tag == Compression) {
|
||||
compression = (uint16_t) entryValue;
|
||||
} else if (tag == PhotometricInterpretation) {
|
||||
photometric = (uint16_t) entryValue;
|
||||
} else if (tag == SamplesPerPixel) {
|
||||
samplesPerPixel = (uint16_t) entryValue;
|
||||
} else if (tag == PlanarConfiguration) {
|
||||
planarConfiguration = (uint16_t) entryValue;
|
||||
} else if (tag == BitsPerSample) {
|
||||
if (count == 1) {
|
||||
bitsPerSample = (uint16_t) entryValue;
|
||||
} else {
|
||||
seek(valueOrOffset);
|
||||
bitsPerSample = read16(mStream);
|
||||
}
|
||||
} else if (tag == StripOffsets) {
|
||||
FILAMENT_CHECK_PRECONDITION(count <= MAX_STRIPS) << "TIFF Decoder: Too many strips";
|
||||
stripOffsets.resize(count);
|
||||
if (count == 1) {
|
||||
stripOffsets[0] = valueOrOffset;
|
||||
} else {
|
||||
seek(valueOrOffset);
|
||||
for (uint32_t j = 0; j < count; ++j) {
|
||||
stripOffsets[j] = (type == SHORT) ? read16(mStream) : read32(mStream);
|
||||
}
|
||||
}
|
||||
} else if (tag == StripByteCounts) {
|
||||
FILAMENT_CHECK_PRECONDITION(count <= MAX_STRIPS) << "TIFF Decoder: Too many strips";
|
||||
stripByteCounts.resize(count);
|
||||
if (count == 1) {
|
||||
stripByteCounts[0] = valueOrOffset;
|
||||
} else {
|
||||
seek(valueOrOffset);
|
||||
for (uint32_t j = 0; j < count; ++j) {
|
||||
stripByteCounts[j] = (type == SHORT) ? read16(mStream) : read32(mStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mStream.seekg(nextEntry);
|
||||
}
|
||||
|
||||
FILAMENT_CHECK_PRECONDITION(compression == 1) << "TIFF Decoder: Unsupported compression";
|
||||
FILAMENT_CHECK_PRECONDITION(photometric == 2)
|
||||
<< "TIFF Decoder: Unsupported photometric interpretation (only RGB supported)";
|
||||
FILAMENT_CHECK_PRECONDITION(samplesPerPixel == 3 || samplesPerPixel == 4)
|
||||
<< "TIFF Decoder: Unsupported samples per pixel (only 3 or 4 supported)";
|
||||
FILAMENT_CHECK_PRECONDITION(bitsPerSample == 8)
|
||||
<< "TIFF Decoder: Unsupported bits per sample (only 8 supported)";
|
||||
FILAMENT_CHECK_PRECONDITION(planarConfiguration == 1)
|
||||
<< "TIFF Decoder: Unsupported planar configuration (only chunky supported)";
|
||||
|
||||
// Check for integer overflow
|
||||
FILAMENT_CHECK_PRECONDITION(width != 0 && height != 0) << "TIFF Decoder: Invalid dimensions";
|
||||
FILAMENT_CHECK_PRECONDITION(height <= std::numeric_limits<size_t>::max() / width)
|
||||
<< "TIFF Decoder: Image size overflow";
|
||||
size_t const pixelCount = (size_t) width * height;
|
||||
FILAMENT_CHECK_PRECONDITION(pixelCount <= std::numeric_limits<size_t>::max() / samplesPerPixel)
|
||||
<< "TIFF Decoder: Image size overflow";
|
||||
size_t const allocationSize = pixelCount * samplesPerPixel;
|
||||
|
||||
// Verify total strip bytes
|
||||
size_t totalStripBytes = 0;
|
||||
for (uint32_t const count: stripByteCounts) {
|
||||
FILAMENT_CHECK_PRECONDITION(std::numeric_limits<size_t>::max() - totalStripBytes >= count)
|
||||
<< "TIFF Decoder: Strip bytes overflow";
|
||||
totalStripBytes += count;
|
||||
}
|
||||
FILAMENT_CHECK_PRECONDITION(totalStripBytes <= allocationSize)
|
||||
<< "TIFF Decoder: Strip bytes exceed allocation";
|
||||
|
||||
// Assemble image
|
||||
auto imageData = std::make_unique<uint8_t[]>(allocationSize);
|
||||
uint8_t* dst = imageData.get();
|
||||
|
||||
FILAMENT_CHECK_PRECONDITION(stripOffsets.size() == stripByteCounts.size())
|
||||
<< "TIFF Decoder: Strip offsets and byte counts mismatch";
|
||||
|
||||
size_t totalBytesRead = 0;
|
||||
for (size_t i = 0; i < stripOffsets.size(); ++i) {
|
||||
seek(stripOffsets[i]);
|
||||
mStream.read(reinterpret_cast<char*>(dst), stripByteCounts[i]);
|
||||
dst += stripByteCounts[i];
|
||||
totalBytesRead += stripByteCounts[i];
|
||||
}
|
||||
|
||||
FILAMENT_CHECK_PRECONDITION(totalBytesRead >= allocationSize)
|
||||
<< "TIFF Decoder: Insufficient data read";
|
||||
|
||||
if (getColorSpace() == ImageDecoder::ColorSpace::SRGB) {
|
||||
if (samplesPerPixel == 4) {
|
||||
return toLinearWithAlpha<uint8_t>(
|
||||
width, height, width * 4, imageData.get(),
|
||||
[](uint8_t v) -> uint8_t { return v; },
|
||||
image::sRGBToLinear<filament::math::float4>);
|
||||
} else {
|
||||
return toLinear<uint8_t>(
|
||||
width, height, width * 3, imageData.get(),
|
||||
[](uint8_t v) -> uint8_t { return v; },
|
||||
image::sRGBToLinear<filament::math::float3>);
|
||||
}
|
||||
} else {
|
||||
if (samplesPerPixel == 4) {
|
||||
return toLinearWithAlpha<uint8_t>(
|
||||
width, height, width * 4, imageData.get(),
|
||||
[](uint8_t v) -> uint8_t { return v; },
|
||||
[](filament::math::float4 const& color) -> filament::math::float4 {
|
||||
return color;
|
||||
});
|
||||
} else {
|
||||
return toLinear<uint8_t>(
|
||||
width, height, width * 3, imageData.get(),
|
||||
[](uint8_t v) -> uint8_t { return v; },
|
||||
[](filament::math::float3 const& color) -> filament::math::float3 {
|
||||
return color;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace imageio_lite
|
||||
443
libs/imageio-lite/src/ImageEncoder.cpp
Normal file
443
libs/imageio-lite/src/ImageEncoder.cpp
Normal file
@@ -0,0 +1,443 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <imageio-lite/ImageEncoder.h>
|
||||
|
||||
#include <image/ColorTransform.h>
|
||||
|
||||
#include <math/vec3.h>
|
||||
#include <math/vec4.h>
|
||||
|
||||
#include <utils/Panic.h>
|
||||
#include <utils/compiler.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <ostream>
|
||||
#include <vector>
|
||||
|
||||
#if defined(WIN32)
|
||||
#include <Winsock2.h>
|
||||
#include <utils/unwindows.h>
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
using namespace filament::math;
|
||||
using namespace image;
|
||||
|
||||
namespace imageio_lite {
|
||||
|
||||
class TIFFEncoder : public ImageEncoder::Encoder {
|
||||
public:
|
||||
static TIFFEncoder* create(std::ostream& stream);
|
||||
|
||||
TIFFEncoder(TIFFEncoder const&) = delete;
|
||||
TIFFEncoder& operator=(TIFFEncoder const&) = delete;
|
||||
|
||||
private:
|
||||
explicit TIFFEncoder(std::ostream& stream);
|
||||
~TIFFEncoder() override = default;
|
||||
|
||||
// ImageEncoder::Encoder interface
|
||||
bool encode(LinearImage const& image) override;
|
||||
|
||||
std::ostream& mStream;
|
||||
std::streampos mStreamStartPos;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
bool ImageEncoder::encode(std::ostream& stream, Format format, image::LinearImage const& image,
|
||||
utils::CString const& compression, utils::CString const& destName) {
|
||||
std::unique_ptr<Encoder> encoder;
|
||||
switch (format) {
|
||||
case Format::TIFF:
|
||||
encoder.reset(TIFFEncoder::create(stream));
|
||||
break;
|
||||
}
|
||||
return encoder->encode(image);
|
||||
}
|
||||
|
||||
ImageEncoder::Format ImageEncoder::chooseFormat(utils::CString const& name, bool forceLinear) {
|
||||
utils::CString ext;
|
||||
std::string_view nameSv = name;
|
||||
size_t index = nameSv.rfind('.');
|
||||
if (index != std::string_view::npos && index != 0) {
|
||||
ext = utils::CString(nameSv.substr(index + 1));
|
||||
}
|
||||
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
||||
if (ext == "tif" || ext == "tiff") return Format::TIFF;
|
||||
|
||||
// Default to TIFF for this lite library if no other format matches?
|
||||
// Or should we return something indicating failure/default?
|
||||
// The original defaulted to PNG. Here we only support TIFF.
|
||||
return Format::TIFF;
|
||||
}
|
||||
|
||||
utils::CString ImageEncoder::chooseExtension(ImageEncoder::Format format) {
|
||||
switch (format) {
|
||||
case Format::TIFF:
|
||||
return ".tif";
|
||||
default:
|
||||
FILAMENT_CHECK_POSTCONDITION(false) << "Format not supported";
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// TIFF Header Structure
|
||||
struct TIFFHeader {
|
||||
uint16_t byteOrder;
|
||||
uint16_t magicNumber;
|
||||
uint32_t firstIFDOffset;
|
||||
};
|
||||
|
||||
static_assert(sizeof(TIFFHeader) == 8);
|
||||
|
||||
// Image File Directory (IFD) Entry Structure
|
||||
struct IFDEntry {
|
||||
uint16_t tag;
|
||||
uint16_t type;
|
||||
uint32_t count;
|
||||
uint32_t valueOffset;
|
||||
};
|
||||
|
||||
static_assert(sizeof(IFDEntry) == 12);
|
||||
|
||||
// TIFF Tag Definitions
|
||||
constexpr uint16_t ImageWidth = 256;
|
||||
constexpr uint16_t ImageLength = 257;
|
||||
constexpr uint16_t BitsPerSample = 258;
|
||||
constexpr uint16_t Compression = 259;
|
||||
constexpr uint16_t PhotometricInterpretation = 262;
|
||||
constexpr uint16_t StripOffsets = 273;
|
||||
constexpr uint16_t SamplesPerPixel = 277;
|
||||
constexpr uint16_t RowsPerStrip = 278;
|
||||
constexpr uint16_t StripByteCounts = 279;
|
||||
constexpr uint16_t XResolution = 282;
|
||||
constexpr uint16_t YResolution = 283;
|
||||
constexpr uint16_t ResolutionUnit = 296;
|
||||
constexpr uint16_t PlanarConfiguration = 284;
|
||||
|
||||
// TIFF Type Definitions
|
||||
constexpr uint16_t SHORT = 3;
|
||||
constexpr uint16_t LONG = 4;
|
||||
constexpr uint16_t RATIONAL = 5;
|
||||
|
||||
// TIFF resolution unit
|
||||
constexpr uint16_t Inch = 2;
|
||||
|
||||
constexpr uint16_t getType(uint16_t const tag) {
|
||||
switch (tag) {
|
||||
case ImageWidth:
|
||||
return LONG;
|
||||
case ImageLength:
|
||||
return LONG;
|
||||
case BitsPerSample:
|
||||
return SHORT;
|
||||
case Compression:
|
||||
return SHORT;
|
||||
case PhotometricInterpretation:
|
||||
return SHORT;
|
||||
case StripOffsets:
|
||||
return LONG;
|
||||
case SamplesPerPixel:
|
||||
return SHORT;
|
||||
case RowsPerStrip:
|
||||
return LONG;
|
||||
case StripByteCounts:
|
||||
return LONG;
|
||||
case PlanarConfiguration:
|
||||
return SHORT;
|
||||
case XResolution:
|
||||
return RATIONAL;
|
||||
case YResolution:
|
||||
return RATIONAL;
|
||||
case ResolutionUnit:
|
||||
return SHORT;
|
||||
default:
|
||||
return SHORT;
|
||||
}
|
||||
}
|
||||
|
||||
// Photometric Interpretation
|
||||
constexpr uint16_t RGBA = 2;
|
||||
|
||||
// Compression
|
||||
constexpr uint16_t NoCompression = 1;
|
||||
|
||||
// Planar Configuration
|
||||
constexpr uint16_t Chunky = 1;
|
||||
|
||||
// According to spec, 8K is the recommended max strip size.
|
||||
constexpr uint32_t MAX_STRIP_SIZE = 8192;
|
||||
|
||||
#define ROWS_PER_STRIP(width) (std::max(1u, MAX_STRIP_SIZE / (width * 4)))
|
||||
|
||||
void buildStrips(uint32_t width, uint32_t height, std::vector<uint32_t>& stripSizes,
|
||||
std::vector<uint32_t>& stripOffsets) {
|
||||
uint32_t const totalSize = width * height * 4;
|
||||
uint32_t const rowSize = width * 4;
|
||||
uint32_t const rowsPerStrip = ROWS_PER_STRIP(width);
|
||||
uint32_t const maxStripSize = rowSize * rowsPerStrip;
|
||||
|
||||
uint32_t size = totalSize;
|
||||
uint32_t offset = 0;
|
||||
while (size > 0) {
|
||||
uint32_t const stripSize = std::min(size, maxStripSize);
|
||||
size -= stripSize;
|
||||
|
||||
stripSizes.push_back(stripSize);
|
||||
stripOffsets.push_back(offset);
|
||||
offset += stripSize;
|
||||
}
|
||||
}
|
||||
|
||||
using AddIFDEntryFunc = std::function<void(uint16_t, uint32_t, uint32_t)>;
|
||||
|
||||
struct Offsets {
|
||||
uint32_t bitsPerSample;
|
||||
uint32_t xResolution;
|
||||
uint32_t yResolution;
|
||||
uint32_t stripByteCounts;
|
||||
uint32_t strips;
|
||||
};
|
||||
|
||||
void buildIFDEntries(uint32_t width, uint32_t height, Offsets offsets, uint32_t stripCount,
|
||||
AddIFDEntryFunc addIFD, uint32_t* size) {
|
||||
|
||||
auto addIFDEntry = [&](uint16_t tag, uint32_t count, uint32_t val) {
|
||||
addIFD(tag, count, val);
|
||||
if (size) {
|
||||
(*size) += sizeof(IFDEntry);
|
||||
}
|
||||
};
|
||||
|
||||
addIFDEntry(ImageWidth, 1, width);
|
||||
addIFDEntry(ImageLength, 1, height);
|
||||
addIFDEntry(Compression, 1, NoCompression);
|
||||
addIFDEntry(PhotometricInterpretation, 1, RGBA);
|
||||
addIFDEntry(SamplesPerPixel, 1, 4);
|
||||
addIFDEntry(RowsPerStrip, 1, ROWS_PER_STRIP(width));
|
||||
addIFDEntry(StripByteCounts, stripCount, offsets.stripByteCounts);
|
||||
addIFDEntry(PlanarConfiguration, 1, Chunky);
|
||||
addIFDEntry(BitsPerSample, 4, offsets.bitsPerSample);
|
||||
addIFDEntry(StripOffsets, stripCount, offsets.strips);
|
||||
addIFDEntry(XResolution, 1, offsets.xResolution);
|
||||
addIFDEntry(YResolution, 1, offsets.yResolution);
|
||||
addIFDEntry(ResolutionUnit, 1, Inch);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TIFFEncoder* TIFFEncoder::create(std::ostream& stream) {
|
||||
TIFFEncoder* encoder = new TIFFEncoder(stream);
|
||||
return encoder;
|
||||
}
|
||||
|
||||
TIFFEncoder::TIFFEncoder(std::ostream& stream)
|
||||
: mStream(stream),
|
||||
mStreamStartPos(stream.tellp()) {}
|
||||
|
||||
bool TIFFEncoder::encode(LinearImage const& image) {
|
||||
uint32_t const width = image.getWidth();
|
||||
uint32_t const height = image.getHeight();
|
||||
|
||||
// Convert to sRGB 8-bit RGBA
|
||||
size_t const srcChannels = image.getChannels();
|
||||
std::unique_ptr<uint8_t[]> rgbaData;
|
||||
if (srcChannels == 4) {
|
||||
rgbaData = fromLinearTosRGB<uint8_t, 4>(image);
|
||||
} else if (srcChannels == 3) {
|
||||
rgbaData = std::make_unique<uint8_t[]>(width * height * 4);
|
||||
auto rgb = fromLinearTosRGB<uint8_t, 3>(image);
|
||||
for (size_t i = 0, n = width * height; i < n; ++i) {
|
||||
rgbaData[i * 4 + 0] = rgb[i * 3 + 0];
|
||||
rgbaData[i * 4 + 1] = rgb[i * 3 + 1];
|
||||
rgbaData[i * 4 + 2] = rgb[i * 3 + 2];
|
||||
rgbaData[i * 4 + 3] = 255;
|
||||
}
|
||||
} else {
|
||||
rgbaData = std::make_unique<uint8_t[]>(width * height * 4);
|
||||
uint8_t* d = rgbaData.get();
|
||||
for (size_t y = 0; y < height; ++y) {
|
||||
float const* p = image.getPixelRef(0, y);
|
||||
for (size_t x = 0; x < width; ++x, p += srcChannels, d += 4) {
|
||||
if (srcChannels == 1) {
|
||||
uint8_t g = uint8_t(
|
||||
filament::math::saturate(image::linearTosRGB(p[0])) * 255.0f + 0.5f);
|
||||
d[0] = d[1] = d[2] = g;
|
||||
} else {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (i < srcChannels) {
|
||||
d[i] = uint8_t(
|
||||
filament::math::saturate(image::linearTosRGB(p[i])) * 255.0f +
|
||||
0.5f);
|
||||
} else {
|
||||
d[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
d[3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto write = [this](void const* data, size_t size) {
|
||||
mStream.write(reinterpret_cast<char const*>(data), size);
|
||||
};
|
||||
auto write16 = [this](uint16_t v) {
|
||||
uint8_t b[2] = { uint8_t(v & 0xFF), uint8_t(v >> 8) };
|
||||
mStream.write((char*) b, 2);
|
||||
};
|
||||
auto write32 = [this](uint32_t v) {
|
||||
uint8_t b[4] = { uint8_t(v & 0xFF), uint8_t((v >> 8) & 0xFF), uint8_t((v >> 16) & 0xFF),
|
||||
uint8_t(v >> 24) };
|
||||
mStream.write((char*) b, 4);
|
||||
};
|
||||
auto tell = [this]() { return (uint32_t) (mStream.tellp() - mStreamStartPos); };
|
||||
|
||||
// TIFF Header
|
||||
write16(0x4949); // Little-endian (II)
|
||||
write16(42);
|
||||
write32(8); // firstIFDOffset
|
||||
|
||||
auto noopIFD = [](uint16_t tag, uint32_t count, uint32_t val) {};
|
||||
|
||||
uint32_t ifdSize = 0;
|
||||
// We do a no-op to gather the size of the IFD entries
|
||||
buildIFDEntries(width, height, {}, 1, noopIFD, &ifdSize);
|
||||
|
||||
uint16_t const ifdCount = ifdSize / sizeof(IFDEntry);
|
||||
write16(ifdCount);
|
||||
|
||||
std::vector<uint32_t> stripByteCounts;
|
||||
std::vector<uint32_t> stripOffsets;
|
||||
|
||||
buildStrips(width, height, stripByteCounts, stripOffsets);
|
||||
|
||||
// Next IFD Offset (0 for none)
|
||||
uint32_t const nextIFDOffset = 0;
|
||||
|
||||
// At this point, we've written the header plus the number of IFD entries (a uint16_t).
|
||||
uint32_t offsetCursor = tell();
|
||||
|
||||
constexpr uint16_t bitsPerSample[4] = { 8, 8, 8, 8 };
|
||||
constexpr uint32_t xResolution[2] = { 1, 1 };
|
||||
constexpr uint32_t yResolution[2] = { 1, 1 };
|
||||
|
||||
Offsets const offsets = {
|
||||
.bitsPerSample = (offsetCursor += (ifdSize + sizeof(nextIFDOffset))),
|
||||
.xResolution = (offsetCursor += sizeof(bitsPerSample)),
|
||||
.yResolution = (offsetCursor += sizeof(xResolution)),
|
||||
.stripByteCounts = (offsetCursor += sizeof(yResolution)),
|
||||
.strips = (offsetCursor += sizeof(uint32_t) * stripByteCounts.size()),
|
||||
};
|
||||
|
||||
uint32_t const stripStart = offsets.strips + stripOffsets.size() * sizeof(uint32_t);
|
||||
std::for_each(stripOffsets.begin(), stripOffsets.end(),
|
||||
[stripStart](uint32_t& offset) { offset += stripStart; });
|
||||
|
||||
// Really build the IFD entries with the proper offsets and putting them into a vector.
|
||||
std::vector<IFDEntry> ifdEntries;
|
||||
auto addIFD = [&ifdEntries](uint16_t tag, uint32_t count, uint32_t val) {
|
||||
ifdEntries.push_back({});
|
||||
auto& entry = ifdEntries.back();
|
||||
entry.tag = tag;
|
||||
entry.type = getType(tag);
|
||||
entry.count = count;
|
||||
entry.valueOffset = val;
|
||||
};
|
||||
buildIFDEntries(width, height, offsets, (uint32_t) stripOffsets.size(), addIFD, nullptr);
|
||||
|
||||
// Fix for count=1: the ValueOffset should contain the value directly if it fits.
|
||||
if (stripOffsets.size() == 1) {
|
||||
for (auto& entry: ifdEntries) {
|
||||
if (entry.tag == StripOffsets) {
|
||||
entry.valueOffset = stripOffsets[0];
|
||||
}
|
||||
if (entry.tag == StripByteCounts) {
|
||||
entry.valueOffset = stripByteCounts[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IFD entries must be sorted by tag.
|
||||
std::sort(ifdEntries.begin(), ifdEntries.end(),
|
||||
[](auto const& a, auto const& b) { return a.tag < b.tag; });
|
||||
|
||||
// Begin writing IFD and all the other metadata arrays.
|
||||
for (const auto& entry: ifdEntries) {
|
||||
write16(entry.tag);
|
||||
write16(entry.type);
|
||||
write32(entry.count);
|
||||
write32(entry.valueOffset);
|
||||
}
|
||||
write32(nextIFDOffset);
|
||||
|
||||
FILAMENT_CHECK_POSTCONDITION(tell() == offsets.bitsPerSample)
|
||||
<< "TIFF Encode error: offset mismatch bitsPerSample";
|
||||
for (int i = 0; i < 4; i++) {
|
||||
write16(bitsPerSample[i]);
|
||||
}
|
||||
FILAMENT_CHECK_POSTCONDITION(tell() == offsets.xResolution)
|
||||
<< "TIFF Encode error: offset mismatch xResolution";
|
||||
for (int i = 0; i < 2; i++) {
|
||||
write32(xResolution[i]);
|
||||
}
|
||||
FILAMENT_CHECK_POSTCONDITION(tell() == offsets.yResolution)
|
||||
<< "TIFF Encode error: offset mismatch yResolution";
|
||||
for (int i = 0; i < 2; i++) {
|
||||
write32(yResolution[i]);
|
||||
}
|
||||
FILAMENT_CHECK_POSTCONDITION(tell() == offsets.stripByteCounts)
|
||||
<< "TIFF Encode error: offset mismatch stripByteCounts";
|
||||
for (uint32_t v: stripByteCounts) {
|
||||
write32(v);
|
||||
}
|
||||
FILAMENT_CHECK_POSTCONDITION(tell() == offsets.strips)
|
||||
<< "TIFF Encode error: offset mismatch strips";
|
||||
for (uint32_t v: stripOffsets) {
|
||||
write32(v);
|
||||
}
|
||||
|
||||
uint32_t const totalSize = width * height * 4;
|
||||
uint32_t const maxStripSize = ROWS_PER_STRIP(width) * width * 4;
|
||||
for (uint32_t i = 0, count = 0; i < totalSize;) {
|
||||
uint32_t const stripSize = std::min(totalSize - i, maxStripSize);
|
||||
|
||||
FILAMENT_CHECK_POSTCONDITION(tell() == stripOffsets[count++])
|
||||
<< "TIFF Encode error: offset mismatch stripOffsets";
|
||||
write(((uint8_t*) rgbaData.get()) + i, stripSize);
|
||||
|
||||
i += stripSize;
|
||||
};
|
||||
mStream.flush();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace imageio_lite
|
||||
242
libs/imageio-lite/tests/test_imageio-lite.cpp
Normal file
242
libs/imageio-lite/tests/test_imageio-lite.cpp
Normal file
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <imageio-lite/ImageDecoder.h>
|
||||
#include <imageio-lite/ImageEncoder.h>
|
||||
|
||||
#include <image/LinearImage.h>
|
||||
|
||||
#include <math/vec3.h>
|
||||
#include <math/vec4.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
using namespace image;
|
||||
using namespace imageio_lite;
|
||||
using namespace filament::math;
|
||||
|
||||
class ImageIOLiteTest : public testing::Test {};
|
||||
|
||||
TEST_F(ImageIOLiteTest, TIFFRoundTrip) {
|
||||
// Create a simple 4x4 image manually
|
||||
uint32_t w = 4;
|
||||
uint32_t h = 4;
|
||||
LinearImage src(w, h, 3);
|
||||
|
||||
// Fill with a pattern: R, G, B, White
|
||||
// Row 0: Red
|
||||
// Row 1: Green
|
||||
// Row 2: Blue
|
||||
// Row 3: White
|
||||
for (int y = 0; y < h; ++y) {
|
||||
for (int x = 0; x < w; ++x) {
|
||||
float* p = src.getPixelRef(x, y);
|
||||
if (y == 0) {
|
||||
p[0] = 1;
|
||||
p[1] = 0;
|
||||
p[2] = 0;
|
||||
} else if (y == 1) {
|
||||
p[0] = 0;
|
||||
p[1] = 1;
|
||||
p[2] = 0;
|
||||
} else if (y == 2) {
|
||||
p[0] = 0;
|
||||
p[1] = 0;
|
||||
p[2] = 1;
|
||||
} else {
|
||||
p[0] = 1;
|
||||
p[1] = 1;
|
||||
p[2] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::stringstream stream;
|
||||
|
||||
// Encode to TIFF
|
||||
bool success = ImageEncoder::encode(stream, ImageEncoder::Format::TIFF, src, "", "test.tif");
|
||||
ASSERT_TRUE(success);
|
||||
|
||||
// Decode from TIFF
|
||||
stream.seekg(0);
|
||||
LinearImage dst = ImageDecoder::decode(stream, "test.tif");
|
||||
|
||||
ASSERT_EQ(dst.getWidth(), src.getWidth());
|
||||
ASSERT_EQ(dst.getHeight(), src.getHeight());
|
||||
|
||||
// Since we go Linear -> sRGB (8-bit) -> Linear, there will be precision loss.
|
||||
// 1/255 is approx 0.004. So epsilon should be around that or slightly higher due to
|
||||
// conversions.
|
||||
float const epsilon = 0.01f;
|
||||
|
||||
for (int y = 0; y < h; ++y) {
|
||||
for (int x = 0; x < w; ++x) {
|
||||
// src is 3 channels
|
||||
float* s_ptr = src.getPixelRef(x, y);
|
||||
float3 s = { s_ptr[0], s_ptr[1], s_ptr[2] };
|
||||
|
||||
// dst is 4 channels (TIFF export forces RGBA)
|
||||
float* d_ptr = dst.getPixelRef(x, y);
|
||||
float4 d = { d_ptr[0], d_ptr[1], d_ptr[2], d_ptr[3] };
|
||||
|
||||
EXPECT_NEAR(s.r, d.r, epsilon);
|
||||
EXPECT_NEAR(s.g, d.g, epsilon);
|
||||
EXPECT_NEAR(s.b, d.b, epsilon);
|
||||
EXPECT_NEAR(1.0f, d.a, epsilon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ImageIOLiteTest, BigEndianTIFF) {
|
||||
std::vector<uint8_t> tiff = {
|
||||
0x4D,
|
||||
0x4D,
|
||||
0x00,
|
||||
0x2A,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x08, // Header (BE)
|
||||
};
|
||||
|
||||
auto write16 = [&](uint16_t v) {
|
||||
tiff.push_back(v >> 8);
|
||||
tiff.push_back(v & 0xFF);
|
||||
};
|
||||
auto write32 = [&](uint32_t v) {
|
||||
tiff.push_back(v >> 24);
|
||||
tiff.push_back((v >> 16) & 0xFF);
|
||||
tiff.push_back((v >> 8) & 0xFF);
|
||||
tiff.push_back(v & 0xFF);
|
||||
};
|
||||
|
||||
// IFD with mandatory tags
|
||||
write16(8); // Number of entries
|
||||
|
||||
// ImageWidth (256), LONG (4), 1, 1
|
||||
write16(256);
|
||||
write16(4);
|
||||
write32(1);
|
||||
write32(1);
|
||||
// ImageLength (257), LONG (4), 1, 1
|
||||
write16(257);
|
||||
write16(4);
|
||||
write32(1);
|
||||
write32(1);
|
||||
// BitsPerSample (258), SHORT (3), 1, 8 (Left justified: 0x00080000)
|
||||
write16(258);
|
||||
write16(3);
|
||||
write32(1);
|
||||
write32(0x00080000);
|
||||
// Compression (259), SHORT (3), 1, 1 (Left justified)
|
||||
write16(259);
|
||||
write16(3);
|
||||
write32(1);
|
||||
write32(0x00010000);
|
||||
// PhotometricInterpretation (262), SHORT (3), 1, 2 (RGB) (Left justified)
|
||||
write16(262);
|
||||
write16(3);
|
||||
write32(1);
|
||||
write32(0x00020000);
|
||||
// StripOffsets (273), LONG (4), 1, 100 (Offset to data)
|
||||
write16(273);
|
||||
write16(4);
|
||||
write32(1);
|
||||
write32(200);
|
||||
// SamplesPerPixel (277), SHORT (3), 1, 3 (RGB) (Left justified)
|
||||
write16(277);
|
||||
write16(3);
|
||||
write32(1);
|
||||
write32(0x00030000); // 3 channels
|
||||
// StripByteCounts (279), LONG (4), 1, 3 (1 pixel * 3 bytes)
|
||||
write16(279);
|
||||
write16(4);
|
||||
write32(1);
|
||||
write32(3);
|
||||
|
||||
write32(0); // Next IFD
|
||||
|
||||
// Fill data until offset 200
|
||||
while (tiff.size() < 200) tiff.push_back(0);
|
||||
// Pixel data (RGB)
|
||||
tiff.push_back(0xFF);
|
||||
tiff.push_back(0x00);
|
||||
tiff.push_back(0x00);
|
||||
|
||||
std::string s(tiff.begin(), tiff.end());
|
||||
std::stringstream stream(s);
|
||||
LinearImage dst = ImageDecoder::decode(stream, "be.tif");
|
||||
|
||||
// If BE decoding works, this returns a valid image.
|
||||
// If it fails (e.g. BitsPerSample read as 0), it returns invalid image.
|
||||
ASSERT_TRUE(dst.isValid());
|
||||
ASSERT_EQ(dst.getWidth(), 1);
|
||||
ASSERT_EQ(dst.getHeight(), 1);
|
||||
}
|
||||
|
||||
TEST_F(ImageIOLiteTest, MaliciousHugeStrips) {
|
||||
std::vector<uint8_t> tiff = {
|
||||
0x49,
|
||||
0x49,
|
||||
0x2A,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00, // Header (LE)
|
||||
};
|
||||
auto write16 = [&](uint16_t v) {
|
||||
tiff.push_back(v & 0xFF);
|
||||
tiff.push_back(v >> 8);
|
||||
};
|
||||
auto write32 = [&](uint32_t v) {
|
||||
tiff.push_back(v & 0xFF);
|
||||
tiff.push_back((v >> 8) & 0xFF);
|
||||
tiff.push_back((v >> 16) & 0xFF);
|
||||
tiff.push_back(v >> 24);
|
||||
};
|
||||
|
||||
write16(1); // 1 entry
|
||||
// StripOffsets (273), LONG (4), count=1000001, offset=0
|
||||
write16(273);
|
||||
write16(4);
|
||||
write32(1000001);
|
||||
write32(0);
|
||||
write32(0);
|
||||
|
||||
std::string s(tiff.begin(), tiff.end());
|
||||
|
||||
// We expect this to fail due to FILAMENT_CHECK_PRECONDITION.
|
||||
// In builds with exceptions, it throws. In builds without, it aborts.
|
||||
// EXPECT_DEATH expects an abort/crash. So we catch and abort if it throws.
|
||||
EXPECT_DEATH(
|
||||
{
|
||||
try {
|
||||
std::stringstream stream(s);
|
||||
ImageDecoder::decode(stream, "bad.tif");
|
||||
} catch (...) {
|
||||
std::abort();
|
||||
}
|
||||
},
|
||||
"");
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -23,8 +23,6 @@ set(SRCS
|
||||
src/Settings.cpp
|
||||
src/Settings_generated.cpp
|
||||
src/Settings_generated.h
|
||||
src/TIFFExport.cpp
|
||||
src/TIFFExport.h
|
||||
src/ViewerGui.cpp
|
||||
)
|
||||
|
||||
@@ -32,7 +30,7 @@ set(SRCS
|
||||
# Include and target definitions
|
||||
# ==================================================================================================
|
||||
add_library(${TARGET} STATIC ${PUBLIC_HDRS} ${SRCS})
|
||||
target_link_libraries(${TARGET} PUBLIC imgui filament gltfio_core filagui jsmn civetweb)
|
||||
target_link_libraries(${TARGET} PUBLIC imgui filament gltfio_core filagui jsmn civetweb imageio-lite)
|
||||
target_include_directories(${TARGET} PUBLIC ${PUBLIC_HDR_DIR})
|
||||
set_target_properties(${TARGET} PROPERTIES FOLDER Libs)
|
||||
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
#include <viewer/AutomationEngine.h>
|
||||
|
||||
#include "TIFFExport.h"
|
||||
#include <image/ColorTransform.h>
|
||||
#include <image/ImageOps.h>
|
||||
#include <image/LinearImage.h>
|
||||
#include <imageio-lite/ImageEncoder.h>
|
||||
|
||||
#include <filament/Camera.h>
|
||||
#include <filament/Engine.h>
|
||||
@@ -110,7 +113,11 @@ void AutomationEngine::exportScreenshot(View* view, Renderer* renderer, std::str
|
||||
if (extension == "ppm") {
|
||||
exportPPM(buffer, vp.width, vp.height, outstream);
|
||||
} else if (extension == "tif" || extension == "tiff") {
|
||||
exportTIFF(buffer, vp.width, vp.height, outstream);
|
||||
image::LinearImage image = image::toLinearWithAlpha<uint8_t>(
|
||||
vp.width, vp.height, vp.width * 4, (uint8_t*) buffer,
|
||||
[](uint8_t v) { return v; }, image::sRGBToLinear<filament::math::float4>);
|
||||
imageio_lite::ImageEncoder::encode(outstream,
|
||||
imageio_lite::ImageEncoder::Format::TIFF, image, "", "");
|
||||
} else {
|
||||
utils::slog.e << out.c_str() << " does not specify a supported file extension."
|
||||
<< utils::io::endl;
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "TIFFExport.h"
|
||||
|
||||
#include <utils/debug.h>
|
||||
#include <utils/Log.h>
|
||||
#include <utils/Panic.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
// TIFF Header Structure
|
||||
struct TIFFHeader {
|
||||
uint16_t byteOrder;
|
||||
uint16_t magicNumber;
|
||||
uint32_t firstIFDOffset;
|
||||
};
|
||||
|
||||
static_assert(sizeof(TIFFHeader) == 8);
|
||||
|
||||
// Image File Directory (IFD) Entry Structure
|
||||
struct IFDEntry {
|
||||
uint16_t tag;
|
||||
uint16_t type;
|
||||
uint32_t count;
|
||||
uint32_t valueOffset;
|
||||
};
|
||||
|
||||
static_assert(sizeof(IFDEntry) == 12);
|
||||
|
||||
// TIFF Tag Definitions
|
||||
constexpr uint16_t ImageWidth = 256;
|
||||
constexpr uint16_t ImageLength = 257;
|
||||
constexpr uint16_t BitsPerSample = 258;
|
||||
constexpr uint16_t Compression = 259;
|
||||
constexpr uint16_t PhotometricInterpretation = 262;
|
||||
constexpr uint16_t StripOffsets = 273;
|
||||
constexpr uint16_t SamplesPerPixel = 277;
|
||||
constexpr uint16_t RowsPerStrip = 278;
|
||||
constexpr uint16_t StripByteCounts = 279;
|
||||
constexpr uint16_t XResolution = 282;
|
||||
constexpr uint16_t YResolution = 283;
|
||||
constexpr uint16_t ResolutionUnit = 296;
|
||||
constexpr uint16_t PlanarConfiguration = 284;
|
||||
|
||||
// TIFF Type Definitions
|
||||
constexpr uint16_t SHORT = 3;
|
||||
constexpr uint16_t LONG = 4;
|
||||
constexpr uint16_t RATIONAL = 5;
|
||||
|
||||
// TIFF resolution unit
|
||||
constexpr uint16_t Inch = 2;
|
||||
|
||||
constexpr uint16_t getType(uint16_t const tag) {
|
||||
switch (tag) {
|
||||
case ImageWidth: return LONG;
|
||||
case ImageLength: return LONG;
|
||||
case BitsPerSample: return SHORT;
|
||||
case Compression: return SHORT;
|
||||
case PhotometricInterpretation: return SHORT;
|
||||
case StripOffsets: return LONG;
|
||||
case SamplesPerPixel: return SHORT;
|
||||
case RowsPerStrip: return LONG;
|
||||
case StripByteCounts: return LONG;
|
||||
case PlanarConfiguration: return SHORT;
|
||||
case XResolution: return RATIONAL;
|
||||
case YResolution: return RATIONAL;
|
||||
case ResolutionUnit: return SHORT;
|
||||
default:
|
||||
return SHORT;
|
||||
}
|
||||
}
|
||||
|
||||
// Photometric Interpretation
|
||||
constexpr uint16_t RGBA = 2;
|
||||
|
||||
// Compression
|
||||
constexpr uint16_t NoCompression = 1;
|
||||
|
||||
// Planar Configuration
|
||||
constexpr uint16_t Chunky = 1;
|
||||
|
||||
// According to spec, 8K is the recommended max strip size.
|
||||
constexpr uint32_t MAX_STRIP_SIZE = 8192;
|
||||
|
||||
#define ROWS_PER_STRIP(width) (MAX_STRIP_SIZE / (width * 4))
|
||||
|
||||
void buildStrips(uint32_t width, uint32_t height,
|
||||
std::vector<uint32_t>& stripSizes, std::vector<uint32_t>& stripOffsets) {
|
||||
uint32_t const totalSize = width * height * 4;
|
||||
uint32_t const rowSize = width * 4;
|
||||
uint32_t const rowsPerStrip = ROWS_PER_STRIP(width);
|
||||
uint32_t const maxStripSize = rowSize * rowsPerStrip;
|
||||
|
||||
uint32_t size = totalSize;
|
||||
uint32_t offset = 0;
|
||||
while (size > 0) {
|
||||
uint32_t const stripSize = std::min(size, maxStripSize);
|
||||
size -= stripSize;
|
||||
|
||||
stripSizes.push_back(stripSize);
|
||||
stripOffsets.push_back(offset);
|
||||
offset += stripSize;
|
||||
}
|
||||
}
|
||||
|
||||
using AddIFDEntryFunc = std::function<void(uint16_t, uint32_t, uint32_t)>;
|
||||
|
||||
struct Offsets {
|
||||
uint32_t bitsPerSample;
|
||||
uint32_t xResolution;
|
||||
uint32_t yResolution;
|
||||
uint32_t stripByteCounts;
|
||||
uint32_t strips;
|
||||
};
|
||||
|
||||
void buildIFDEntries(uint32_t width, uint32_t height, Offsets offsets, uint32_t stripCount,
|
||||
AddIFDEntryFunc addIFD, uint32_t* size) {
|
||||
|
||||
auto addIFDEntry = [&](uint16_t tag, uint32_t count, uint32_t val) {
|
||||
addIFD(tag, count, val);
|
||||
if (size) {
|
||||
(*size) += sizeof(IFDEntry);
|
||||
}
|
||||
};
|
||||
|
||||
addIFDEntry(ImageWidth, 1, width);
|
||||
addIFDEntry(ImageLength, 1, height);
|
||||
addIFDEntry(Compression, 1, NoCompression);
|
||||
addIFDEntry(PhotometricInterpretation, 1, RGBA);
|
||||
addIFDEntry(SamplesPerPixel, 1, 4);
|
||||
addIFDEntry(RowsPerStrip, 1, ROWS_PER_STRIP(width));
|
||||
addIFDEntry(StripByteCounts, stripCount, offsets.stripByteCounts);
|
||||
addIFDEntry(PlanarConfiguration, 1, Chunky);
|
||||
addIFDEntry(BitsPerSample, 4, offsets.bitsPerSample);
|
||||
addIFDEntry(StripOffsets, stripCount, offsets.strips);
|
||||
addIFDEntry(XResolution, 1, offsets.xResolution);
|
||||
addIFDEntry(YResolution, 1, offsets.yResolution);
|
||||
addIFDEntry(ResolutionUnit, 1, Inch);
|
||||
}
|
||||
|
||||
} // anonymous
|
||||
|
||||
void exportTIFF(void* rgbaData, uint32_t width, uint32_t height, std::ostream& file) {
|
||||
FILAMENT_CHECK_PRECONDITION(width * 4 < MAX_STRIP_SIZE)
|
||||
<< "output image's width is too large. width=" << width
|
||||
<< ", max-width=" << (MAX_STRIP_SIZE / 4);
|
||||
|
||||
uint32_t cursor = 0;
|
||||
auto write = [&file, &cursor](auto const& obj) {
|
||||
uint32_t const len = sizeof(obj);
|
||||
file.write(reinterpret_cast<char const*>(&obj), sizeof(obj));
|
||||
cursor += len;
|
||||
};
|
||||
|
||||
auto writeBytes = [&file, &cursor](uint8_t* bytes, uint32_t size) {
|
||||
file.write(reinterpret_cast<char const*>(bytes), size);
|
||||
cursor += size;
|
||||
};
|
||||
|
||||
// TIFF Header
|
||||
TIFFHeader header = {
|
||||
.byteOrder = 0x4949, // Little-endian
|
||||
.magicNumber = 42,
|
||||
.firstIFDOffset = sizeof(TIFFHeader),
|
||||
};
|
||||
write(header);
|
||||
|
||||
auto noopIFD = [](uint16_t tag, uint32_t count, uint32_t val) {};
|
||||
|
||||
uint32_t ifdSize = 0;
|
||||
// We do a no-op to gather the size of the IFD entries
|
||||
buildIFDEntries(width, height, {}, 1, noopIFD, &ifdSize);
|
||||
|
||||
uint16_t const ifdCount = ifdSize / sizeof(IFDEntry);
|
||||
write(ifdCount);
|
||||
|
||||
std::vector<uint32_t> stripByteCounts;
|
||||
std::vector<uint32_t> stripOffsets;
|
||||
|
||||
buildStrips(width, height, stripByteCounts, stripOffsets);
|
||||
|
||||
// Next IFD Offset (0 for none)
|
||||
uint32_t const nextIFDOffset = 0;
|
||||
|
||||
// At this point, we've written the header plus the number of IFD entries (a uint16_t).
|
||||
uint32_t offsetCursor = cursor;
|
||||
|
||||
constexpr uint16_t bitsPerSample[4] = { 8, 8, 8, 8 };
|
||||
constexpr uint32_t xResolution[2] = { 1, 1 };
|
||||
constexpr uint32_t yResolution[2] = { 1, 1 };
|
||||
|
||||
Offsets const offsets = {
|
||||
.bitsPerSample = (offsetCursor += (ifdSize + sizeof(nextIFDOffset))),
|
||||
.xResolution = (offsetCursor += sizeof(bitsPerSample)),
|
||||
.yResolution = (offsetCursor += sizeof(xResolution)),
|
||||
.stripByteCounts = (offsetCursor += sizeof(yResolution)),
|
||||
.strips = (offsetCursor += sizeof(uint32_t) * stripByteCounts.size()),
|
||||
};
|
||||
|
||||
uint32_t const stripStart = offsets.strips + stripOffsets.size() * sizeof(uint32_t);
|
||||
std::for_each(stripOffsets.begin(), stripOffsets.end(),
|
||||
[stripStart](uint32_t& offset) { offset += stripStart; });
|
||||
|
||||
// Really build the IFD entries with the proper offsets and putting them into a vector.
|
||||
std::vector<IFDEntry> ifdEntries;
|
||||
auto addIFD = [&ifdEntries](uint16_t tag, uint32_t count, uint32_t val) {
|
||||
ifdEntries.push_back({});
|
||||
auto& entry = ifdEntries.back();
|
||||
entry.tag = tag;
|
||||
entry.type = getType(tag);
|
||||
entry.count = count;
|
||||
entry.valueOffset = val;
|
||||
};
|
||||
buildIFDEntries(width, height, offsets, stripOffsets.size(), addIFD, nullptr);
|
||||
// IFD entries must be sorted by tag.
|
||||
std::sort(ifdEntries.begin(), ifdEntries.end(), [](auto const& a, auto const& b) {
|
||||
return a.tag < b.tag;
|
||||
});
|
||||
|
||||
// Begin writing IFD and all the other metadata arrays.
|
||||
std::for_each(ifdEntries.begin(), ifdEntries.end(), write);
|
||||
write(nextIFDOffset);
|
||||
|
||||
assert_invariant(cursor == offsets.bitsPerSample);
|
||||
write(bitsPerSample);
|
||||
assert_invariant(cursor == offsets.xResolution);
|
||||
write(xResolution);
|
||||
assert_invariant(cursor == offsets.yResolution);
|
||||
write(yResolution);
|
||||
assert_invariant(cursor == offsets.stripByteCounts);
|
||||
writeBytes((uint8_t*) stripByteCounts.data(), stripByteCounts.size() * sizeof(uint32_t));
|
||||
assert_invariant(cursor == offsets.strips);
|
||||
writeBytes((uint8_t*) stripOffsets.data(), stripOffsets.size() * sizeof(uint32_t));
|
||||
|
||||
uint32_t const totalSize = width * height * 4;
|
||||
uint32_t const maxStripSize = ROWS_PER_STRIP(width) * width * 4;
|
||||
for (uint32_t i = 0, count = 0; i < totalSize;) {
|
||||
uint32_t const stripSize = std::min(totalSize - i, maxStripSize);
|
||||
|
||||
assert_invariant(cursor == stripOffsets[count++]);
|
||||
writeBytes(((uint8_t*) rgbaData) + i, stripSize);
|
||||
|
||||
i += stripSize;
|
||||
};
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef VIEWER_TIFF_EXPORT_H
|
||||
#define VIEWER_TIFF_EXPORT_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
|
||||
void exportTIFF(void* buffer, uint32_t width, uint32_t height, std::ostream& outstream);
|
||||
|
||||
#endif // VIEWER_TIFF_EXPORT_H
|
||||
Reference in New Issue
Block a user