Created
August 6, 2023 07:03
-
-
Save leidegre/8c0018e5330087aa0c340367bfa7b00e to your computer and use it in GitHub Desktop.
Tiff loader, very basic
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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