Haio 0.2.X
Homebrew Asset IO/CDN
Loading...
Searching...
No Matches
decode.cpp
Go to the documentation of this file.
1#include <haio_codec.hpp>
2#include <haio_codecs.hpp>
3#include <haio_convert.hpp>
4
5#include <optional>
6#include <string>
7
8namespace {
9
10using Haio::Bytes;
11using Haio::Error;
12using Haio::ErrorCode;
13using Haio::Result;
14
15/** an image this wide would not fit anyway, and a header can claim it in six bytes */
16constexpr size_t maxPixels = size_t{1} << 28;
17
18struct Header {
19 int kind = 0; // the digit in P1 through P6
20 int width = 0;
21 int height = 0;
22 int maxval = 1;
23 size_t offset = 0; // first byte after the header
24};
25
26bool isBlank(uint8_t c) {
27 return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f';
28}
29
30/** comments may sit anywhere whitespace may, including between the width and height */
31void skipBlanks(Bytes data, size_t& at) {
32 while (at < data.size()) {
33 if (isBlank(data[at])) {
34 at++;
35 } else if (data[at] == '#') {
36 while (at < data.size() && data[at] != '\n') at++;
37 } else {
38 break;
39 }
40 }
41}
42
43std::optional<int> readNumber(Bytes data, size_t& at) {
44 skipBlanks(data, at);
45 if (at >= data.size() || data[at] < '0' || data[at] > '9') return std::nullopt;
46
47 long long value = 0;
48 while (at < data.size() && data[at] >= '0' && data[at] <= '9') {
49 value = value * 10 + (data[at] - '0');
50 if (value > 0x7FFFFFFF) return std::nullopt;
51 at++;
52 }
53 return static_cast<int>(value);
54}
55
56Result<Header> readHeader(Bytes data) {
57 if (data.size() < 2 || data[0] != 'P' || data[1] < '1' || data[1] > '6') {
58 return std::unexpected(Error{ErrorCode::InvalidInput, "not a netpbm header"});
59 }
60
61 Header header;
62 header.kind = data[1] - '0';
63 size_t at = 2;
64
65 const auto width = readNumber(data, at);
66 const auto height = readNumber(data, at);
67 if (!width || !height) {
68 return std::unexpected(Error{ErrorCode::InvalidInput, "netpbm header has no dimensions"});
69 }
70 header.width = *width;
71 header.height = *height;
72
73 // a bitmap has no maxval: every sample is one bit
74 if (header.kind != 1 && header.kind != 4) {
75 const auto maxval = readNumber(data, at);
76 if (!maxval) return std::unexpected(Error{ErrorCode::InvalidInput, "netpbm header has no maxval"});
77 header.maxval = *maxval;
78 }
79
80 if (header.width <= 0 || header.height <= 0) {
81 return std::unexpected(Error{ErrorCode::InvalidInput, "netpbm image has no area"});
82 }
83 if (header.maxval < 1 || header.maxval > 65535) {
84 return std::unexpected(Error{ErrorCode::InvalidInput, "netpbm maxval out of range"});
85 }
86 if (static_cast<size_t>(header.width) * static_cast<size_t>(header.height) > maxPixels) {
87 return std::unexpected(Error{ErrorCode::InvalidInput, "netpbm image is too large"});
88 }
89
90 // the binary payload starts one whitespace character after the header, and that
91 // character is part of the header rather than a separator that may repeat
92 if (at < data.size() && isBlank(data[at])) at++;
93 header.offset = at;
94 return header;
95}
96
97uint8_t scale(int value, int maxval) {
98 if (value <= 0) return 0;
99 if (value >= maxval) return 255;
100 return static_cast<uint8_t>((value * 255 + maxval / 2) / maxval);
101}
102
103/**
104 * an ascii sample is at least one digit, and all but the last need a separator, so a
105 * file shorter than this cannot hold what its header promises. checking before the
106 * allocation keeps a six byte header from asking for a gigabyte.
107 */
108std::optional<Error> checkAsciiRoom(Bytes data, const Header& header, size_t samples) {
109 const auto available = data.size() - header.offset;
110 if (samples > 0 && available < samples * 2 - 1) {
111 return Error{ErrorCode::InvalidInput, "netpbm ascii data is shorter than its header claims"};
112 }
113 return std::nullopt;
114}
115
116std::optional<Error> checkBinaryRoom(Bytes data, const Header& header, size_t bytes) {
117 if (data.size() - header.offset < bytes) {
118 return Error{ErrorCode::InvalidInput, "netpbm binary data is shorter than its header claims"};
119 }
120 return std::nullopt;
121}
122
123/** P1 writes one bit per token, where 1 means black, the opposite of a grey level */
124Result<std::vector<uint8_t>> readAsciiBits(Bytes data, const Header& header, size_t pixels) {
125 std::vector<uint8_t> out(pixels);
126 size_t at = header.offset;
127 for (size_t sample = 0; sample < pixels; sample++) {
128 skipBlanks(data, at);
129 if (at >= data.size() || (data[at] != '0' && data[at] != '1')) {
130 return std::unexpected(Error{ErrorCode::InvalidInput, "netpbm bitmap ran out of bits"});
131 }
132 out[sample] = data[at] == '1' ? 0 : 255;
133 at++;
134 }
135 return out;
136}
137
138Result<std::vector<uint8_t>> readAsciiSamples(Bytes data, const Header& header, size_t samples) {
139 std::vector<uint8_t> out(samples);
140 size_t at = header.offset;
141 for (size_t sample = 0; sample < samples; sample++) {
142 const auto value = readNumber(data, at);
143 if (!value) return std::unexpected(Error{ErrorCode::InvalidInput, "netpbm ascii data ran out of samples"});
144 out[sample] = scale(*value, header.maxval);
145 }
146 return out;
147}
148
149/** P4 packs bits most significant first and pads every row to a whole byte */
150Result<std::vector<uint8_t>> readBinaryBits(Bytes data, const Header& header) {
151 const auto stride = (static_cast<size_t>(header.width) + 7) / 8;
152 if (auto problem = checkBinaryRoom(data, header, stride * static_cast<size_t>(header.height))) {
153 return std::unexpected(*problem);
154 }
155
156 std::vector<uint8_t> out(static_cast<size_t>(header.width) * static_cast<size_t>(header.height));
157 for (size_t y = 0; y < static_cast<size_t>(header.height); y++) {
158 const auto row = header.offset + y * stride;
159 for (size_t x = 0; x < static_cast<size_t>(header.width); x++) {
160 const auto bit = (data[row + x / 8] >> (7 - (x % 8))) & 1;
161 out[y * static_cast<size_t>(header.width) + x] = bit ? 0 : 255;
162 }
163 }
164 return out;
165}
166
167Result<std::vector<uint8_t>> readBinarySamples(Bytes data, const Header& header, size_t samples) {
168 const bool wide = header.maxval > 255;
169 const auto width = wide ? size_t{2} : size_t{1};
170 if (auto problem = checkBinaryRoom(data, header, samples * width)) return std::unexpected(*problem);
171
172 std::vector<uint8_t> out(samples);
173 for (size_t sample = 0; sample < samples; sample++) {
174 const auto at = header.offset + sample * width;
175 // sixteen bit samples are big endian, whatever the machine is
176 const int value = wide ? (data[at] << 8) | data[at + 1] : data[at];
177 out[sample] = scale(value, header.maxval);
178 }
179 return out;
180}
181
182}
183
184namespace Haio::Codecs {
185
186/**
187 * @addtogroup decode
188 * @{
189 */
190/** the colour pipes: P3 and P6 carry three samples a pixel */
191template <>
193 HAIO_TRY(header, readHeader(blob.data));
194 if (header.kind != 3 && header.kind != 6) {
195 HAIO_FAIL(InvalidInput, "P" + std::to_string(header.kind) + " is greyscale, not rgb888");
196 }
197
198 const auto samples = static_cast<size_t>(header.width) * static_cast<size_t>(header.height) * 3;
199 if (header.kind == 3) HAIO_CHECK(checkAsciiRoom(blob.data, header, samples));
200
201 HAIO_TRY(pixels, header.kind == 3 ? readAsciiSamples(blob.data, header, samples)
202 : readBinarySamples(blob.data, header, samples));
203 return Image<Color::RGB888>{header.width, header.height, std::move(pixels)};
204}
205/** @} */
206
207/**
208 * @addtogroup decode
209 * @{
210 */
211/**
212 * the grey pipes: P2 and P5 carry one sample a pixel, P1 and P4 one bit. a bitmap
213 * calls 1 black, so the bit is inverted rather than scaled.
214 */
215template <>
217 HAIO_TRY(header, readHeader(blob.data));
218 if (header.kind == 3 || header.kind == 6) {
219 HAIO_FAIL(InvalidInput, "P" + std::to_string(header.kind) + " is rgb888, not greyscale");
220 }
221
222 const auto pixels = static_cast<size_t>(header.width) * static_cast<size_t>(header.height);
223 if (header.kind == 1 || header.kind == 2) HAIO_CHECK(checkAsciiRoom(blob.data, header, pixels));
224
226 header.kind == 1 ? readAsciiBits(blob.data, header, pixels)
227 : header.kind == 2 ? readAsciiSamples(blob.data, header, pixels)
228 : header.kind == 4 ? readBinaryBits(blob.data, header)
229 : readBinarySamples(blob.data, header, pixels);
230
231 HAIO_TRY(levels, std::move(samples));
232 return Image<Color::GRAY8>{header.width, header.height, std::move(levels)};
233}
234/** @} */
235
236}
Result< Image< Color::GRAY8 > > Decode< Format::PPM, Color::GRAY8 >(const Blob &blob)
Definition decode.cpp:216
Result< Image< Color::RGB888 > > Decode< Format::PPM, Color::RGB888 >(const Blob &blob)
Definition decode.cpp:192
#define HAIO_TRY(name, expr)
#define HAIO_FAIL(why, message)
#define HAIO_CHECK(expr)
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