155 lines
6.1 KiB
C++
155 lines
6.1 KiB
C++
/**
|
||
* Minimal, dependency-free TIFF writer.
|
||
*
|
||
* Produces uncompressed, baseline TIFF 6.0 files (little-endian).
|
||
* Layout:
|
||
* [8-byte header] [pixel data] [0–1 pad byte] [IFD entry count (2 bytes)]
|
||
* [N × 12-byte IFD entries] [next-IFD offset = 0 (4 bytes)]
|
||
* [BitsPerSample data: 3 × SHORT]
|
||
*
|
||
* Placing pixel data before the IFD keeps the strip offset a fixed constant
|
||
* (always 8) and avoids a two-pass write. A padding byte is inserted when
|
||
* imageSize is odd so the IFD starts on a word boundary, as the TIFF spec
|
||
* requires.
|
||
*/
|
||
|
||
#include "tiff_writer.h"
|
||
#include "platform_utils.h"
|
||
|
||
#include <cstdint>
|
||
#include <cstring>
|
||
#include <fstream>
|
||
#include <iomanip>
|
||
#include <sstream>
|
||
#include <vector>
|
||
|
||
// ── TIFF helpers ──────────────────────────────────────────────────────────────
|
||
|
||
namespace {
|
||
|
||
constexpr uint16_t TIFF_SHORT = 3;
|
||
constexpr uint16_t TIFF_LONG = 4;
|
||
|
||
constexpr uint16_t TAG_IMAGE_WIDTH = 256;
|
||
constexpr uint16_t TAG_IMAGE_LENGTH = 257;
|
||
constexpr uint16_t TAG_BITS_PER_SAMPLE = 258;
|
||
constexpr uint16_t TAG_COMPRESSION = 259;
|
||
constexpr uint16_t TAG_PHOTOMETRIC = 262;
|
||
constexpr uint16_t TAG_STRIP_OFFSETS = 273;
|
||
constexpr uint16_t TAG_SAMPLES_PER_PIXEL = 277;
|
||
constexpr uint16_t TAG_ROWS_PER_STRIP = 278;
|
||
constexpr uint16_t TAG_STRIP_BYTE_COUNTS = 279;
|
||
constexpr uint16_t TAG_PLANAR_CONFIG = 284;
|
||
|
||
template <typename T>
|
||
void writeLE(std::vector<uint8_t>& buf, size_t offset, T value)
|
||
{
|
||
static_assert(std::is_integral<T>::value, "Integral required");
|
||
for (size_t i = 0; i < sizeof(T); ++i)
|
||
buf[offset + i] = static_cast<uint8_t>((value >> (8 * i)) & 0xFF);
|
||
}
|
||
|
||
template <typename T>
|
||
void appendLE(std::vector<uint8_t>& buf, T value)
|
||
{
|
||
const size_t pos = buf.size();
|
||
buf.resize(pos + sizeof(T));
|
||
writeLE(buf, pos, value);
|
||
}
|
||
|
||
void appendIFDEntry(std::vector<uint8_t>& buf,
|
||
uint16_t tag, uint16_t type,
|
||
uint32_t count, uint32_t valueOrOffset)
|
||
{
|
||
appendLE<uint16_t>(buf, tag);
|
||
appendLE<uint16_t>(buf, type);
|
||
appendLE<uint32_t>(buf, count);
|
||
appendLE<uint32_t>(buf, valueOrOffset);
|
||
}
|
||
|
||
} // namespace
|
||
|
||
// ── TiffWriter ────────────────────────────────────────────────────────────────
|
||
|
||
TiffWriter::TiffWriter(const std::string& outputDir)
|
||
: m_dir(outputDir)
|
||
{}
|
||
|
||
std::string TiffWriter::write(const FrameData& frame, uint64_t frameIndex)
|
||
{
|
||
if (frame.pixels.empty() || frame.width <= 0 || frame.height <= 0)
|
||
return "";
|
||
|
||
std::ostringstream nameStream;
|
||
nameStream << "frame_" << std::setw(8) << std::setfill('0') << frameIndex << ".tif";
|
||
const std::string filePath = m_dir + platform::pathSeparator() + nameStream.str();
|
||
|
||
// ── Offsets ───────────────────────────────────────────────────────────
|
||
|
||
const uint32_t W = static_cast<uint32_t>(frame.width);
|
||
const uint32_t H = static_cast<uint32_t>(frame.height);
|
||
const uint32_t imageSize = W * 3u * H;
|
||
|
||
// The TIFF spec requires IFDs to begin on a word (2-byte) boundary.
|
||
// Insert a 1-byte pad when imageSize is odd.
|
||
const uint32_t padBytes = imageSize % 2u;
|
||
|
||
constexpr uint32_t HEADER_SIZE = 8u;
|
||
constexpr uint16_t NUM_ENTRIES = 10u; // exactly the entries written below
|
||
constexpr uint32_t IFD_SIZE = 2u + NUM_ENTRIES * 12u + 4u;
|
||
|
||
const uint32_t pixelOffset = HEADER_SIZE;
|
||
const uint32_t ifdOffset = pixelOffset + imageSize + padBytes;
|
||
const uint32_t bpsOffset = ifdOffset + IFD_SIZE; // 3 × SHORT after IFD
|
||
|
||
// ── Assemble in memory ────────────────────────────────────────────────
|
||
|
||
std::vector<uint8_t> tiff;
|
||
tiff.reserve(bpsOffset + 6u);
|
||
|
||
// Header
|
||
appendLE<uint8_t> (tiff, 0x49); // 'I' – little-endian
|
||
appendLE<uint8_t> (tiff, 0x49); // 'I'
|
||
appendLE<uint16_t>(tiff, 42u); // TIFF magic number
|
||
appendLE<uint32_t>(tiff, ifdOffset); // offset of first IFD
|
||
|
||
// Pixel data
|
||
tiff.insert(tiff.end(), frame.pixels.begin(), frame.pixels.end());
|
||
|
||
// Alignment pad (0 or 1 byte)
|
||
if (padBytes)
|
||
appendLE<uint8_t>(tiff, 0u);
|
||
|
||
// IFD entry count (must match NUM_ENTRIES exactly)
|
||
appendLE<uint16_t>(tiff, NUM_ENTRIES);
|
||
|
||
// IFD entries — ascending tag order required by the spec
|
||
appendIFDEntry(tiff, TAG_IMAGE_WIDTH, TIFF_LONG, 1u, W);
|
||
appendIFDEntry(tiff, TAG_IMAGE_LENGTH, TIFF_LONG, 1u, H);
|
||
appendIFDEntry(tiff, TAG_BITS_PER_SAMPLE, TIFF_SHORT, 3u, bpsOffset); // → extra data
|
||
appendIFDEntry(tiff, TAG_COMPRESSION, TIFF_SHORT, 1u, 1u); // no compression
|
||
appendIFDEntry(tiff, TAG_PHOTOMETRIC, TIFF_SHORT, 1u, 2u); // RGB
|
||
appendIFDEntry(tiff, TAG_STRIP_OFFSETS, TIFF_LONG, 1u, pixelOffset);
|
||
appendIFDEntry(tiff, TAG_SAMPLES_PER_PIXEL, TIFF_SHORT, 1u, 3u);
|
||
appendIFDEntry(tiff, TAG_ROWS_PER_STRIP, TIFF_LONG, 1u, H); // single strip
|
||
appendIFDEntry(tiff, TAG_STRIP_BYTE_COUNTS, TIFF_LONG, 1u, imageSize);
|
||
appendIFDEntry(tiff, TAG_PLANAR_CONFIG, TIFF_SHORT, 1u, 1u); // chunky / interleaved
|
||
|
||
// Next-IFD offset = 0 (this is the only IFD)
|
||
appendLE<uint32_t>(tiff, 0u);
|
||
|
||
// Extra data: BitsPerSample values (8, 8, 8)
|
||
appendLE<uint16_t>(tiff, 8u); // R
|
||
appendLE<uint16_t>(tiff, 8u); // G
|
||
appendLE<uint16_t>(tiff, 8u); // B
|
||
|
||
// ── Write to disk ─────────────────────────────────────────────────────
|
||
|
||
std::ofstream out(filePath, std::ios::binary);
|
||
if (!out) return "";
|
||
out.write(reinterpret_cast<const char*>(tiff.data()),
|
||
static_cast<std::streamsize>(tiff.size()));
|
||
if (!out) return "";
|
||
return filePath;
|
||
}
|