Skip to content

Instantly share code, notes, and snippets.

@leidegre
Created August 6, 2023 07:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leidegre/8c0018e5330087aa0c340367bfa7b00e to your computer and use it in GitHub Desktop.
Save leidegre/8c0018e5330087aa0c340367bfa7b00e to your computer and use it in GitHub Desktop.
Tiff loader, very basic
#pragma once
namespace game {
enum ErrorCode {
// Prefix with GRR_
G_ERR_OK = 0,
G_ERR_ERROR,
G_ERR_WIN32,
G_ERR_FILE_NOT_FOUND,
G_ERR_QUIT,
G_ERR_DEVICE_CONTEXT,
G_ERR_IMAGE_DECODER,
G_ERR_UNKNOWN_IMAGE_FORMAT,
G_ERR_UNSUPPORTED_IMAGE_PARAMETERS,
G_ERR_OPEN_GL,
G_ERR_MEMORY,
G_ERR_TEXTURE_FROM_FILE,
G_ERR_CREATE_SHADER_PROGRAM,
G_ERR_COMPILE_SHADER_PROGRAM,
G_ERR_VALIDATE_PROGRAM_PIPELINE,
};
// Allow construction from ErrorCode (will make Error non-pod but does it matter?)
struct Error {
int code_;
int sub_; // Depends on error code
const char* message_;
bool ok() {
return code_ == G_ERR_OK;
}
bool HasError() {
return !(code_ == G_ERR_OK);
}
// File was not found
bool FileNotFound();
// Print error should be called to dump the error, just prior to program termination.
// It should not be used if the program is meant to handle the error and continue running.
void printError();
#if _WIN32
// Retrieves the calling thread's last-error code value.
// REQUIRES: _WIN32
static Error _GetLastError(const char* message = nullptr);
#endif
};
} // namespace game
#pragma once
#include <cstdint>
namespace game {
inline int Min(int x, int y) {
return x < y ? x : y;
}
inline int Min(int x, int y, int z) {
return Min(Min(x, y), z);
}
inline int Max(int x, int y) {
return x < y ? y : x;
}
inline int Max(int x, int y, int z) {
return Max(Max(x, y), z);
}
inline int64_t Min64(int64_t x, int64_t y) {
return x < y ? x : y;
}
inline uint64_t Max64(uint64_t x, uint64_t y) {
return x < y ? y : x;
}
} // namespace game
#pragma once
#include <cassert>
#include <cstdint>
#include <cstring>
#include <initializer_list>
namespace game {
// deprecated: use Slice<T> instead
struct ByteSlice {
uint8_t* data_;
// Number of readable bytes.
uint32_t len_;
// Number of writable bytes.
uint32_t cap_;
};
// deprecated: use Slice<T> instead
// UTF-8 encoded string. Maybe null-terminated.
struct Utf8Slice {
// Sequence of UTF-8 encoded bytes.
const char* str_;
// Length of UTF-8 encoded string in bytes excluding any null terminator.
uint32_t len_;
static Utf8Slice FromString(const char* str) {
return Utf8Slice{ str, str != nullptr ? (uint32_t)strlen(str) : 0 };
};
static Utf8Slice FromString(const uint8_t* str) {
return FromString((const char*)str);
};
// range based for
const char* begin() const {
return str_;
}
const char* end() const {
return str_ + len_;
}
};
// deprecated: use Slice<T> instead
// Mutable UTF-16 encoded string. Null-terminated.
struct Utf16Slice {
wchar_t* str_; // this should not be mutable
uint32_t len_;
uint32_t cap_;
template <uint32_t N> static Utf16Slice FromArray(wchar_t (&str)[N]) {
return Utf16Slice{ str, 0, N };
}
};
template <int32_t N, typename T> constexpr int32_t ArrayLength(T (&array)[N]) {
return N;
}
// A slice is a view of memory that may or may not be read only.
// If slice is passed as Slice<const T> the memory is read only.
template <typename T> struct Slice {
// https://go.dev/blog/slices-intro
T* ptr_;
int32_t len_;
int32_t cap_;
int Len() const {
return len_;
}
// Get the size of the used portion in bytes.
int ByteLen() const {
return len_ * (int)sizeof(T);
}
int Cap() const {
return cap_;
}
T& operator[](int index) const {
assert((0 <= index) & (index < len_));
return ptr_[index];
}
// ranged based for loop support
T* begin() const {
return ptr_;
}
T* end() const {
return ptr_ + len_;
}
};
// Append value to end of slice. Slice can be empty but must have allocated capacity.
template <typename T> Slice<T> Append(Slice<T> slice, T value) {
assert(slice.Len() + 1 <= slice.Cap());
slice.ptr_[slice.len_] = value;
return Slice<T>{ slice.ptr_, slice.len_ + 1, slice.cap_ };
};
namespace slice {
// Create a slice from an array
template <typename T, int Length> constexpr Slice<T> FromArray(T (&array)[Length]) {
return Slice<T>{ array, Length, Length };
};
// Create read only slice from initializer_list
// Note that this does not extend the lifetime of the initializer_list
// it is just a wrapper for the initializer_list interface; use responsibly
// This is not legal slice::FromInitializer({1, 2, 3})
template <typename T> constexpr Slice<const T> FromInitializer(std::initializer_list<T> initializer_list) {
int32_t length = (int32_t)initializer_list.size();
return { initializer_list.begin(), length, length };
};
} // namespace slice
} // namespace game
#include "tiff.hh"
#include "common-min-max.hh"
#include <cstdio>
namespace {
using namespace game;
struct Reader {
const uint8_t* p0_;
const uint8_t* p_;
const uint8_t* end_;
Reader() {
}
Reader(const ByteSlice data) : p0_(data.data_), p_(data.data_), end_(data.data_ + data.len_) {
}
void seek(uint32_t offset) {
p_ = p0_ + offset;
}
uint16_t readUint16() {
auto v = ((const uint16_t*)p_)[0];
p_ += 2;
return v;
}
uint32_t readUint32() {
auto v = ((const uint32_t*)p_)[0];
p_ += 4;
return v;
}
};
} // namespace
namespace game {
Error LoaderParseTiffImage(ByteSlice data, TiffImage* tiff) {
// this is a baseline TIFF reader
// it does not support compression
memset(tiff, 0, sizeof(TiffImage));
Reader r(data);
if (!(r.readUint16() == 0x4949)) {
return Error{G_ERR_IMAGE_DECODER, 0, "Cannot read TIFF header"};
}
if (!(r.readUint16() == 42)) {
return Error{G_ERR_IMAGE_DECODER, 1, "Cannot read TIFF header"};
}
// image file directory
uint32_t first_ifd_offset = r.readUint32();
r.seek(first_ifd_offset);
auto sizeOfType = [](uint16_t type) -> uint32_t {
switch (type) {
case 3:
return 2;
case 4:
return 4;
}
return 0;
};
int32_t ifd_entry_count = r.readUint16();
for (int32_t i = 0; i < ifd_entry_count; i++) {
auto tag = r.readUint16();
auto type = r.readUint16();
auto count = r.readUint32();
auto offset = r.readUint32();
// if the value is small (<= 4 bytes) the offset is the value
auto ptr_u8 = sizeOfType(type) * count <= 4 ? (r.p_ - 4) : (r.p0_ + offset);
auto ptr_u16 = (const uint16_t*)ptr_u8;
switch (tag) {
case 256: {
tiff->info_.width_ = offset;
break;
}
case 257: {
tiff->info_.length_ = offset;
break;
}
case 258: {
if (!(type == 3)) {
return Error{G_ERR_IMAGE_DECODER, 0, "Bits per sample must be of type long"};
}
tiff->info_.bits_per_sample_.type_ = type;
tiff->info_.bits_per_sample_.count_ = count;
tiff->info_.bits_per_sample_.ptr_.u8_ = ptr_u8;
break;
}
case 259: {
if (!(ptr_u16[0] == 1)) {
return Error{G_ERR_IMAGE_DECODER, 0, "Unsupported compression"};
}
tiff->info_.compression_ = ptr_u16[0];
break;
}
case 262: {
tiff->info_.photo_interp_ = ptr_u16[0];
break;
}
case 269: {
// DocumentName
break;
}
case 273: {
if (!(type == 3 || type == 4)) {
return Error{G_ERR_IMAGE_DECODER, 0, "Strip offset must be of type short or long"};
}
tiff->info_.strip_offsets_.type_ = type;
tiff->info_.strip_offsets_.count_ = count;
tiff->info_.strip_offsets_.ptr_.u8_ = ptr_u8;
break;
}
case 277: {
tiff->info_.samples_per_pixel_ = ptr_u16[0];
break;
}
case 278: {
tiff->info_.rows_per_strip_ = offset;
break;
}
case 279: {
if (!(type == 3 || type == 4)) {
return Error{G_ERR_IMAGE_DECODER, 0, "Strip byte counts must be of type short or long"};
}
tiff->info_.strip_byte_counts_.type_ = type;
tiff->info_.strip_byte_counts_.count_ = count;
tiff->info_.strip_byte_counts_.ptr_.u8_ = ptr_u8;
break;
}
case 282: {
tiff->info_.x_res_ = (float)offset; // ???
break;
}
case 283: {
tiff->info_.y_res_ = (float)offset; // ???
break;
}
case 284: {
if (!(*ptr_u16 == 1)) {
return Error{G_ERR_IMAGE_DECODER, 0, "Unsupported planar configuration"};
}
tiff->info_.planar_conf_ = ptr_u16[0];
break;
}
case 296: {
tiff->info_.res_unit_ = ptr_u16[0];
break;
}
case 338: {
tiff->info_.extra_samples_ = ptr_u16[0];
break;
}
case 339: {
if (!(type == 3)) {
return Error{G_ERR_IMAGE_DECODER, 0, "Sample format must be of type short"};
}
tiff->info_.sample_format_.type_ = type;
tiff->info_.sample_format_.count_ = count;
tiff->info_.sample_format_.ptr_.u8_ = ptr_u8;
break;
}
default: {
fprintf(stderr, "tiff: unkown ifd entry tag=%u type=%u count=%3u offset=%7u\n", tag, type, count, offset);
break;
}
}
}
auto strip_count = Max(1u, (tiff->info_.length_ / tiff->info_.rows_per_strip_));
if (!(tiff->info_.strip_offsets_.count_ == strip_count)) {
return Error{G_ERR_IMAGE_DECODER, 0, "Strip count does not match strip offsets"};
}
if (!(tiff->info_.strip_byte_counts_.count_ == strip_count)) {
return Error{G_ERR_IMAGE_DECODER, 0, "Strip count does not match strip byte counts"};
}
uint32_t bits_per_pixel = 0;
for (uint32_t i = 0; i < tiff->info_.bits_per_sample_.count_; i++) {
bits_per_pixel += tiff->info_.bits_per_sample_.Get(i);
}
tiff->bits_per_pixel_ = bits_per_pixel;
tiff->bytes_per_pixel_ = bits_per_pixel / 8;
return Error{};
}
} // namespace game
#include "common-error.hh"
#include "common-slice.hh"
#include <cstdint>
namespace game {
struct TiffArray {
union {
const uint8_t* u8_;
const uint16_t* u16_;
const uint32_t* u32_;
} ptr_;
uint32_t count_;
uint16_t type_;
uint32_t Get(uint32_t index) {
switch (type_) {
case 3:
return ptr_.u16_[index];
case 4:
return ptr_.u32_[index];
}
return 0;
}
};
struct TiffImage {
struct {
// The number of columns in the image, i.e., the number of pixels per
// scanline.
uint32_t width_;
// The number of rows (sometimes described as scanlines) in the image.
uint32_t length_;
TiffArray bits_per_sample_;
uint16_t compression_;
uint16_t photo_interp_;
// The offset to the first strip??? no this depends on the image size a
// larger image with more strips won't look like this...
TiffArray strip_offsets_;
uint16_t samples_per_pixel_;
// The number of rows in each strip (except possibly the last strip.)
uint32_t rows_per_strip_;
// The size of each strip in bytes, each strip has the same size.
TiffArray strip_byte_counts_;
float x_res_;
float y_res_;
uint16_t planar_conf_;
uint16_t res_unit_;
uint16_t extra_samples_;
TiffArray sample_format_;
} info_;
uint32_t bits_per_pixel_;
uint32_t bytes_per_pixel_;
};
struct TiffImageDataEnumerator {
const ByteSlice data_;
TiffImage* tiff_;
uint8_t* strip_;
uint8_t* strip_end_;
uint8_t* cur_;
uint32_t strip_index_;
TiffImageDataEnumerator(const ByteSlice data, TiffImage* tiff) : data_(data), tiff_(tiff) {
load(0);
}
// Load strip
void load(uint32_t strip_index) {
strip_ = data_.data_ + tiff_->info_.strip_offsets_.Get(strip_index);
strip_end_ =
data_.data_ + tiff_->info_.strip_offsets_.Get(strip_index) + tiff_->info_.strip_byte_counts_.Get(strip_index);
strip_index_ = strip_index;
}
uint32_t width() {
return tiff_->info_.width_;
}
uint32_t height() {
return tiff_->info_.length_;
}
bool next() {
if (strip_ < strip_end_) {
cur_ = strip_;
strip_ = strip_ + tiff_->bytes_per_pixel_;
return true;
}
if (strip_index_ + 1 < tiff_->info_.strip_offsets_.count_) {
load(strip_index_ + 1);
return next();
}
return false;
}
// requires: next()
uint8_t r8() const {
return cur_[0];
}
// requires: next()
uint8_t g8() const {
if (1 < tiff_->info_.bits_per_sample_.count_) {
return cur_[1];
}
return 0x00;
}
// requires: next()
uint8_t b8() const {
if (2 < tiff_->info_.bits_per_sample_.count_) {
return cur_[2];
}
return 0x00;
}
// requires: next()
uint8_t a8() const {
if (3 < tiff_->info_.bits_per_sample_.count_) {
return cur_[3];
}
return 0xff; // alpha (opaque)
}
// requires: next()
uint32_t argb() const {
return (uint32_t(a8()) << 24) | (uint32_t(r8()) << 16) | (uint32_t(g8()) << 8) | (uint32_t(b8()));
}
};
Error LoaderParseTiffImage(ByteSlice data, TiffImage* tiff);
} // namespace game
#include "tiff.hh"
#include "../test/test.h"
#include <cstdio>
using namespace game;
int main(int argc, char* argv[]) {
test_init(argc, argv);
TEST_CASE("TiffTest") {
uint8_t buffer[267] = {
0x49, 0x49, 0x2a, 0x00, 0x38, 0x00, 0x00, 0x00, 0xfe, 0x35, 0x21, 0xfb, 0x3b, 0x02, 0x35,
0x87, 0x57, 0x00, 0xff, 0x00, 0xd0, 0x00, 0x00, 0xa8, 0x1f, 0x3d, 0x7d, 0xff, 0x7d, 0x1f,
0x51, 0x2b, 0x4d, 0x26, 0x89, 0x00, 0xff, 0xff, 0xf2, 0xf2, 0xf2, 0x59, 0x59, 0x59, 0x00,
0x00, 0xff, 0x00, 0x00, 0x7f, 0x73, 0x73, 0x73, 0x99, 0x99, 0x99, 0x0e, 0x00, 0x00, 0x01,
0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x01,
0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x03, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x06, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0d, 0x01,
0x02, 0x00, 0x09, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x11, 0x01, 0x04, 0x00, 0x01,
0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x15, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00,
0x03, 0x00, 0x00, 0x00, 0x16, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
0x00, 0x17, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1a, 0x01,
0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, 0x1b, 0x01, 0x05, 0x00, 0x01,
0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, 0x1c, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x53, 0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2c, 0x01,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x01, 0x00, 0x01,
0x00, 0x01, 0x00, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x34, 0x78, 0x34, 0x00,
};
auto data = ByteSlice{buffer, 267, 267};
TiffImage tiff;
LoaderParseTiffImage(data, &tiff);
TiffImageDataEnumerator iter(data, &tiff);
ASSERT_EQUAL_UINT32(4, iter.width());
ASSERT_EQUAL_UINT32(4, iter.height());
fprintf(stderr, "\n");
for (uint32_t i = 0; i < iter.height(); i++) {
for (uint32_t j = 0; j < iter.width(); j++) {
if (!iter.next()) {
ASSERT_TRUE(false); // todo: fail
}
fprintf(
stderr,
"[%3u,%3u] %3u %3u %3u %3u 0x%08x\n",
j,
i,
iter.r8(),
iter.g8(),
iter.b8(),
iter.a8(),
iter.argb());
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment