Haio 0.2.X
Homebrew Asset IO/CDN
Loading...
Searching...
No Matches
header.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <haio_codec.hpp>
4
5#include <optional>
6#include <string_view>
7
8/**
9 * the eighteen bytes every tga starts with, read once for the three files that need
10 * them. it is a header beside the sources rather than a third .cpp because nothing
11 * outside this directory has any use for a targa header, and the build scans .cpp
12 * files for capabilities: a helper that lived in one of them would be shared by
13 * accident or copied on purpose.
14 */
16
17inline constexpr size_t headerBytes = 18;
18
19/**
20 * an image this large would not fit anyway, and a header claims it in four bytes.
21 *
22 * the same number the netpbm decoder uses, for the same reason: the dimensions are
23 * read before there is any picture to measure them against, and a run length payload
24 * is not bounded by the size of the file the way a plain one is.
25 */
26inline constexpr size_t maxPixels = size_t{1} << 28;
27
28/**
29 * what a tga 2.0 writer leaves at the end of the file: two offsets nobody here uses
30 * and a signature that is the only thing about the format anybody can recognise
31 * outright. haio writes one so its own files are named by that rather than by the
32 * guesswork below.
33 */
34inline constexpr std::string_view footerSignature = "TRUEVISION-XFILE.";
35inline constexpr size_t footerBytes = 26;
36
37struct Header {
38 uint8_t idLength = 0;
39 uint8_t mapType = 0;
40 uint8_t imageType = 0;
41 uint16_t mapFirst = 0;
42 uint16_t mapCount = 0;
43 uint8_t mapBits = 0;
44 uint16_t width = 0;
45 uint16_t height = 0;
46 uint8_t depth = 0;
47 uint8_t descriptor = 0;
48
49 size_t mapOffset = 0; /**< the colour map, when there is one */
50 size_t dataOffset = 0; /**< the pixels, whatever came before them */
51
52 /** mapped, truecolor or grey, with the run length flavours folded onto their own */
53 int kind() const { return imageType > 8 ? imageType - 8 : imageType; }
54 bool rle() const { return imageType > 8; }
55
56 /** one stored unit: a colour for a truecolor picture, an index for a mapped one */
57 size_t unit() const { return (static_cast<size_t>(depth) + 7) / 8; }
58 size_t pixels() const { return static_cast<size_t>(width) * static_cast<size_t>(height); }
59
60 /**
61 * how many of the sixteen bits are alpha, which for this format is a claim rather
62 * than a layout: the bit is in the same place either way, and this says whether
63 * anybody meant anything by it.
64 */
65 int attributeBits() const { return descriptor & 0x0f; }
66
67 /** the origin bits, which say where the first stored pixel belongs on screen */
68 bool flipX() const { return (descriptor & 0x10) != 0; }
69 bool flipY() const { return (descriptor & 0x20) == 0; }
70};
71
72/** the signature sits eight bytes into the footer, after the two offsets */
73inline bool hasFooter(Bytes data) {
74 if (data.size() < footerBytes) return false;
75 const auto at = data.size() - footerBytes + 8;
76 for (size_t i = 0; i < footerSignature.size(); i++) {
77 if (data[at + i] != static_cast<uint8_t>(footerSignature[i])) return false;
78 }
79 return true;
80}
81
82/**
83 * the header, made to prove itself.
84 *
85 * every other container haio reads begins with bytes that mean nothing else. a tga
86 * begins with its own dimensions, so there is nothing to compare against and the only
87 * question that can be asked is whether the eighteen bytes are consistent: a type it
88 * names, a depth that type can have, a colour map exactly when one is called for, and
89 * a file long enough to hold what all of that promises. anything less strict would
90 * make tga the answer for every unrecognised file.
91 */
93 if (data.size() < headerBytes) {
94 return std::unexpected(Error{ErrorCode::InvalidInput, "this file is too short to be a tga"});
95 }
96
97 const auto u16 = [data](size_t at) {
98 return static_cast<uint16_t>(data[at] | (data[at + 1] << 8));
99 };
100
101 Header header;
102 header.idLength = data[0];
103 header.mapType = data[1];
104 header.imageType = data[2];
105 header.mapFirst = u16(3);
106 header.mapCount = u16(5);
107 header.mapBits = data[7];
108 // 8 to 11 are where the picture sits on a screen, which is not part of the picture
109 header.width = u16(12);
110 header.height = u16(14);
111 header.depth = data[16];
112 header.descriptor = data[17];
113
114 const auto fail = [](const char* why) {
115 return std::unexpected(Error{ErrorCode::InvalidInput, why});
116 };
117
118 if (header.mapType > 1) return fail("this tga names a colour map type nobody defined");
119 if (header.kind() < 1 || header.kind() > 3) return fail("this tga names an image type nobody defined");
120 // the top two descriptor bits were interleaving in the first version and have to
121 // be zero in this one, which makes them a cheap way to tell noise from a header
122 if ((header.descriptor & 0xc0) != 0) return fail("this tga sets descriptor bits that 2.0 reserved");
123 if (header.width == 0 || header.height == 0) return fail("this tga has no area");
124
125 if (header.kind() == 1) {
126 if (header.mapType != 1 || header.mapCount == 0) return fail("this tga indexes a colour map it does not carry");
127 if (header.mapBits != 15 && header.mapBits != 16 && header.mapBits != 24 && header.mapBits != 32) {
128 return fail("this tga names a colour map entry size nobody defined");
129 }
130 if (header.depth != 8 && header.depth != 16) return fail("a mapped tga indexes one or two bytes at a time");
131 } else if (header.mapType == 0 && (header.mapFirst != 0 || header.mapCount != 0 || header.mapBits != 0)) {
132 return fail("this tga describes a colour map and then says it has none");
133 }
134
135 if (header.kind() == 2 && header.depth != 15 && header.depth != 16 && header.depth != 24 && header.depth != 32) {
136 return fail("this tga names a truecolor depth nobody defined");
137 }
138 if (header.kind() == 3 && header.depth != 8) return fail("a grey tga is one byte a pixel");
139
140 header.mapOffset = headerBytes + static_cast<size_t>(header.idLength);
141 header.dataOffset = header.mapOffset;
142 if (header.mapType == 1) {
143 header.dataOffset += static_cast<size_t>(header.mapCount) * ((static_cast<size_t>(header.mapBits) + 7) / 8);
144 }
145
146 if (header.pixels() > maxPixels) return fail("this tga is larger than haio will decode");
147
148 if (header.dataOffset >= data.size()) return fail("this tga header promises pixels the file does not reach");
149
150 const auto available = data.size() - header.dataOffset;
151 if (!header.rle()) {
152 if (available < header.pixels() * header.unit()) {
153 return fail("this tga holds fewer pixels than its header claims");
154 }
155 } else if (header.pixels() / 128 > available / (1 + header.unit())) {
156 /**
157 * a compressed picture is as long as it turns out to be, so it cannot be
158 * measured the way a plain one can. what can be said is the most it could
159 * possibly be: a packet is a count byte and at least one unit, and it covers
160 * at most 128 pixels. a file that does not reach even that is either truncated
161 * or claiming a size it means to be expanded into.
162 */
163 return fail("this tga claims more picture than its runs could possibly hold");
164 }
165
166 return header;
167}
168
169/**
170 * which of haio's colours these bytes already are.
171 *
172 * a tga stores what haio calls bgr888, bgra8888 and the two fives-and-ones outright,
173 * so the answer is a layout rather than a conversion: that is the whole reason those
174 * colours exist, and why decoding one is a copy and not a loop.
175 */
176inline std::optional<Color> colorOf(const Header& header) {
177 switch (header.kind()) {
178 case 1: return Color::PALETTE;
179 case 3: return Color::GRAY8;
180 case 2:
181 if (header.depth == 24) return Color::BGR888;
182 if (header.depth == 32) return Color::BGRA8888;
183 // the same two bytes either way: the attribute bits are what decides
184 // whether the top one is an alpha or nothing at all
185 return header.attributeBits() != 0 ? Color::RGBA5551 : Color::RGB555;
186 default: break;
187 }
188 return std::nullopt;
189}
190
191}
constexpr size_t footerBytes
Definition header.hpp:35
std::optional< Color > colorOf(const Header &header)
Definition header.hpp:176
constexpr std::string_view footerSignature
Definition header.hpp:34
bool hasFooter(Bytes data)
Definition header.hpp:73
constexpr size_t headerBytes
Definition header.hpp:17
Result< Header > readHeader(Bytes data)
Definition header.hpp:92
constexpr size_t maxPixels
Definition header.hpp:26
std::expected< T, Error > Result
std::span< const uint8_t > Bytes
size_t pixels() const
Definition header.hpp:58
int attributeBits() const
Definition header.hpp:65
size_t unit() const
Definition header.hpp:57