26bool isBlank(uint8_t c) {
27 return c ==
' ' || c ==
'\t' || c ==
'\n' || c ==
'\r' || c ==
'\v' || c ==
'\f';
31void skipBlanks(Bytes data,
size_t& at) {
32 while (at < data.size()) {
33 if (isBlank(data[at])) {
35 }
else if (data[at] ==
'#') {
36 while (at < data.size() && data[at] !=
'\n') at++;
43std::optional<int> readNumber(Bytes data,
size_t& at) {
45 if (at >= data.size() || data[at] <
'0' || data[at] >
'9')
return std::nullopt;
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;
53 return static_cast<int>(value);
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"});
62 header.kind = data[1] -
'0';
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"});
70 header.width = *width;
71 header.height = *height;
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;
80 if (header.width <= 0 || header.height <= 0) {
81 return std::unexpected(Error{ErrorCode::InvalidInput,
"netpbm image has no area"});
83 if (header.maxval < 1 || header.maxval > 65535) {
84 return std::unexpected(Error{ErrorCode::InvalidInput,
"netpbm maxval out of range"});
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"});
92 if (at < data.size() && isBlank(data[at])) at++;
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);
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"};
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"};
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"});
132 out[sample] = data[at] ==
'1' ? 0 : 255;
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);
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);
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;
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);
172 std::vector<uint8_t> out(samples);
173 for (
size_t sample = 0; sample < samples; sample++) {
174 const auto at = header.offset + sample * width;
176 const int value = wide ? (data[at] << 8) | data[at + 1] : data[at];
177 out[sample] = scale(value, header.maxval);
194 if (header.kind != 3 && header.kind != 6) {
195 HAIO_FAIL(InvalidInput,
"P" + std::to_string(header.kind) +
" is greyscale, not rgb888");
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));
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)};
218 if (header.kind == 3 || header.kind == 6) {
219 HAIO_FAIL(InvalidInput,
"P" + std::to_string(header.kind) +
" is rgb888, not greyscale");
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));
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);
231 HAIO_TRY(levels, std::move(samples));
232 return Image<Color::GRAY8>{header.width, header.height, std::move(levels)};