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:
Powei Feng
2026-01-30 09:54:14 -08:00
committed by GitHub
parent 0131949aff
commit b40530ad3c
13 changed files with 1216 additions and 293 deletions

View File

@@ -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)

View File

@@ -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

View 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()

View 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.

View 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_ */

View 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_ */

View 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

View 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

View 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();
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;
};
}

View File

@@ -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