framerip/tiff_writer.cpp

155 lines
6.1 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Minimal, dependency-free TIFF writer.
*
* Produces uncompressed, baseline TIFF 6.0 files (little-endian).
* Layout:
* [8-byte header] [pixel data] [01 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;
}