/src/libjxl/lib/extras/dec/pnm.cc
Line | Count | Source |
1 | | // Copyright (c) the JPEG XL Project Authors. All rights reserved. |
2 | | // |
3 | | // Use of this source code is governed by a BSD-style |
4 | | // license that can be found in the LICENSE file. |
5 | | |
6 | | #include "lib/extras/dec/pnm.h" |
7 | | |
8 | | #include <jxl/codestream_header.h> |
9 | | #include <jxl/encode.h> |
10 | | #include <jxl/types.h> |
11 | | |
12 | | #include <algorithm> |
13 | | #include <cmath> |
14 | | #include <cstddef> |
15 | | #include <cstdint> |
16 | | #include <cstdlib> |
17 | | #include <cstring> |
18 | | #include <utility> |
19 | | #include <vector> |
20 | | |
21 | | #include "lib/extras/dec/color_hints.h" |
22 | | #include "lib/extras/mmap.h" |
23 | | #include "lib/extras/packed_image.h" |
24 | | #include "lib/extras/size_constraints.h" |
25 | | #include "lib/jxl/base/bits.h" |
26 | | #include "lib/jxl/base/c_callback_support.h" |
27 | | #include "lib/jxl/base/common.h" |
28 | | #include "lib/jxl/base/span.h" |
29 | | #include "lib/jxl/base/status.h" |
30 | | |
31 | | namespace jxl { |
32 | | namespace extras { |
33 | | namespace { |
34 | | |
35 | | class Parser { |
36 | | public: |
37 | | explicit Parser(const Span<const uint8_t> bytes) |
38 | 0 | : pos_(bytes.data()), end_(pos_ + bytes.size()) {} |
39 | | |
40 | | // Sets "pos" to the first non-header byte/pixel on success. |
41 | 0 | Status ParseHeader(HeaderPNM* header, const uint8_t** pos) { |
42 | | // codec.cc ensures we have at least two bytes => no range check here. |
43 | 0 | if (pos_[0] != 'P') return false; |
44 | 0 | const uint8_t type = pos_[1]; |
45 | 0 | pos_ += 2; |
46 | |
|
47 | 0 | switch (type) { |
48 | 0 | case '1': |
49 | 0 | return JXL_FAILURE("ascii pbm not supported"); |
50 | | |
51 | 0 | case '2': |
52 | 0 | return JXL_FAILURE("ascii pgm not supported"); |
53 | | |
54 | 0 | case '3': |
55 | 0 | return JXL_FAILURE("ascii ppm not supported"); |
56 | | |
57 | 0 | case '4': |
58 | 0 | return JXL_FAILURE("pbm not supported"); |
59 | | |
60 | 0 | case '5': |
61 | 0 | header->is_gray = true; |
62 | 0 | return ParseHeaderPNM(header, pos); |
63 | | |
64 | 0 | case '6': |
65 | 0 | header->is_gray = false; |
66 | 0 | return ParseHeaderPNM(header, pos); |
67 | | |
68 | 0 | case '7': |
69 | 0 | return ParseHeaderPAM(header, pos); |
70 | | |
71 | 0 | case 'F': |
72 | 0 | header->is_gray = false; |
73 | 0 | return ParseHeaderPFM(header, pos); |
74 | | |
75 | 0 | case 'f': |
76 | 0 | header->is_gray = true; |
77 | 0 | return ParseHeaderPFM(header, pos); |
78 | | |
79 | 0 | default: |
80 | 0 | return false; |
81 | 0 | } |
82 | 0 | } |
83 | | |
84 | | // Exposed for testing |
85 | 0 | Status ParseUnsigned(size_t* number) { |
86 | 0 | if (pos_ == end_) return JXL_FAILURE("PNM: reached end before number"); |
87 | 0 | if (!IsDigit(*pos_)) return JXL_FAILURE("PNM: expected unsigned number"); |
88 | | |
89 | 0 | *number = 0; |
90 | 0 | while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { |
91 | 0 | const size_t digit = *pos_ - '0'; |
92 | 0 | if (!SafeMul(*number, static_cast<size_t>(10), *number) || |
93 | 0 | !SafeAdd(*number, digit, *number)) { |
94 | 0 | return JXL_FAILURE("PNM: unsigned number too large"); |
95 | 0 | } |
96 | 0 | ++pos_; |
97 | 0 | } |
98 | | |
99 | 0 | return true; |
100 | 0 | } |
101 | | |
102 | 0 | Status ParseSigned(double* number) { |
103 | 0 | if (pos_ == end_) return JXL_FAILURE("PNM: reached end before signed"); |
104 | | |
105 | 0 | if (*pos_ != '-' && *pos_ != '+' && !IsDigit(*pos_)) { |
106 | 0 | return JXL_FAILURE("PNM: expected signed number"); |
107 | 0 | } |
108 | | |
109 | | // Skip sign |
110 | 0 | const bool is_neg = *pos_ == '-'; |
111 | 0 | if (is_neg || *pos_ == '+') { |
112 | 0 | ++pos_; |
113 | 0 | if (pos_ == end_) return JXL_FAILURE("PNM: reached end before digits"); |
114 | 0 | } |
115 | | |
116 | | // Leading digits |
117 | 0 | *number = 0.0; |
118 | 0 | while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { |
119 | 0 | *number *= 10; |
120 | 0 | *number += *pos_ - '0'; |
121 | 0 | ++pos_; |
122 | 0 | } |
123 | | |
124 | | // Decimal places? |
125 | 0 | if (pos_ < end_ && *pos_ == '.') { |
126 | 0 | ++pos_; |
127 | 0 | double place = 0.1; |
128 | 0 | while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { |
129 | 0 | *number += (*pos_ - '0') * place; |
130 | 0 | place *= 0.1; |
131 | 0 | ++pos_; |
132 | 0 | } |
133 | 0 | } |
134 | |
|
135 | 0 | if (is_neg) *number = -*number; |
136 | 0 | return true; |
137 | 0 | } |
138 | | |
139 | | private: |
140 | 0 | static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; } |
141 | 0 | static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; } |
142 | 0 | static bool IsWhitespace(const uint8_t c) { |
143 | 0 | return IsLineBreak(c) || c == '\t' || c == ' '; |
144 | 0 | } |
145 | | |
146 | 0 | Status SkipBlank() { |
147 | 0 | if (pos_ == end_) return JXL_FAILURE("PNM: reached end before blank"); |
148 | 0 | const uint8_t c = *pos_; |
149 | 0 | if (c != ' ' && c != '\n') return JXL_FAILURE("PNM: expected blank"); |
150 | 0 | ++pos_; |
151 | 0 | return true; |
152 | 0 | } |
153 | | |
154 | 0 | Status SkipSingleWhitespace() { |
155 | 0 | if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace"); |
156 | 0 | if (!IsWhitespace(*pos_)) return JXL_FAILURE("PNM: expected whitespace"); |
157 | 0 | ++pos_; |
158 | 0 | return true; |
159 | 0 | } |
160 | | |
161 | 0 | Status SkipWhitespace() { |
162 | 0 | if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace"); |
163 | 0 | if (!IsWhitespace(*pos_) && *pos_ != '#') { |
164 | 0 | return JXL_FAILURE("PNM: expected whitespace/comment"); |
165 | 0 | } |
166 | | |
167 | 0 | while (pos_ < end_ && IsWhitespace(*pos_)) { |
168 | 0 | ++pos_; |
169 | 0 | } |
170 | | |
171 | | // Comment(s) |
172 | 0 | while (pos_ != end_ && *pos_ == '#') { |
173 | 0 | while (pos_ != end_ && !IsLineBreak(*pos_)) { |
174 | 0 | ++pos_; |
175 | 0 | } |
176 | | // Newline(s) |
177 | 0 | while (pos_ != end_ && IsLineBreak(*pos_)) pos_++; |
178 | 0 | } |
179 | |
|
180 | 0 | while (pos_ < end_ && IsWhitespace(*pos_)) { |
181 | 0 | ++pos_; |
182 | 0 | } |
183 | 0 | return true; |
184 | 0 | } |
185 | | |
186 | 0 | Status MatchString(const char* keyword, bool skipws = true) { |
187 | 0 | const uint8_t* ppos = pos_; |
188 | 0 | const uint8_t* kw = reinterpret_cast<const uint8_t*>(keyword); |
189 | 0 | while (*kw) { |
190 | 0 | if (ppos >= end_) return JXL_FAILURE("PAM: unexpected end of input"); |
191 | 0 | if (*kw != *ppos) return false; |
192 | 0 | ppos++; |
193 | 0 | kw++; |
194 | 0 | } |
195 | 0 | pos_ = ppos; |
196 | 0 | if (skipws) { |
197 | 0 | JXL_RETURN_IF_ERROR(SkipWhitespace()); |
198 | 0 | } else { |
199 | 0 | JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); |
200 | 0 | } |
201 | 0 | return true; |
202 | 0 | } |
203 | | |
204 | 0 | Status ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) { |
205 | 0 | size_t depth = 3; |
206 | 0 | size_t max_val = 255; |
207 | 0 | JXL_RETURN_IF_ERROR(SkipWhitespace()); |
208 | 0 | while (!MatchString("ENDHDR", /*skipws=*/false)) { |
209 | 0 | if (MatchString("WIDTH")) { |
210 | 0 | JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); |
211 | 0 | JXL_RETURN_IF_ERROR(SkipWhitespace()); |
212 | 0 | } else if (MatchString("HEIGHT")) { |
213 | 0 | JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); |
214 | 0 | JXL_RETURN_IF_ERROR(SkipWhitespace()); |
215 | 0 | } else if (MatchString("DEPTH")) { |
216 | 0 | JXL_RETURN_IF_ERROR(ParseUnsigned(&depth)); |
217 | 0 | JXL_RETURN_IF_ERROR(SkipWhitespace()); |
218 | 0 | } else if (MatchString("MAXVAL")) { |
219 | 0 | JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val)); |
220 | 0 | JXL_RETURN_IF_ERROR(SkipWhitespace()); |
221 | 0 | } else if (MatchString("TUPLTYPE")) { |
222 | 0 | if (MatchString("RGB_ALPHA")) { |
223 | 0 | header->has_alpha = true; |
224 | 0 | } else if (MatchString("RGB")) { |
225 | 0 | } else if (MatchString("GRAYSCALE_ALPHA")) { |
226 | 0 | header->has_alpha = true; |
227 | 0 | header->is_gray = true; |
228 | 0 | } else if (MatchString("GRAYSCALE")) { |
229 | 0 | header->is_gray = true; |
230 | 0 | } else if (MatchString("BLACKANDWHITE_ALPHA")) { |
231 | 0 | header->has_alpha = true; |
232 | 0 | header->is_gray = true; |
233 | 0 | max_val = 1; |
234 | 0 | } else if (MatchString("BLACKANDWHITE")) { |
235 | 0 | header->is_gray = true; |
236 | 0 | max_val = 1; |
237 | 0 | } else if (MatchString("Alpha")) { |
238 | 0 | header->ec_types.push_back(JXL_CHANNEL_ALPHA); |
239 | 0 | } else if (MatchString("Depth")) { |
240 | 0 | header->ec_types.push_back(JXL_CHANNEL_DEPTH); |
241 | 0 | } else if (MatchString("SpotColor")) { |
242 | 0 | header->ec_types.push_back(JXL_CHANNEL_SPOT_COLOR); |
243 | 0 | } else if (MatchString("SelectionMask")) { |
244 | 0 | header->ec_types.push_back(JXL_CHANNEL_SELECTION_MASK); |
245 | 0 | } else if (MatchString("Black")) { |
246 | 0 | header->ec_types.push_back(JXL_CHANNEL_BLACK); |
247 | 0 | } else if (MatchString("CFA")) { |
248 | 0 | header->ec_types.push_back(JXL_CHANNEL_CFA); |
249 | 0 | } else if (MatchString("Thermal")) { |
250 | 0 | header->ec_types.push_back(JXL_CHANNEL_THERMAL); |
251 | 0 | } else if (MatchString("Unknown")) { |
252 | 0 | header->ec_types.push_back(JXL_CHANNEL_UNKNOWN); |
253 | 0 | } else if (MatchString("Optional")) { |
254 | 0 | header->ec_types.push_back(JXL_CHANNEL_OPTIONAL); |
255 | 0 | } else { |
256 | 0 | return JXL_FAILURE("PAM: unknown TUPLTYPE"); |
257 | 0 | } |
258 | 0 | } else { |
259 | 0 | constexpr size_t kMaxHeaderLength = 20; |
260 | 0 | char unknown_header[kMaxHeaderLength + 1]; |
261 | 0 | size_t len = std::min<size_t>(kMaxHeaderLength, end_ - pos_); |
262 | 0 | strncpy(unknown_header, reinterpret_cast<const char*>(pos_), len); |
263 | 0 | unknown_header[len] = 0; |
264 | 0 | return JXL_FAILURE("PAM: unknown header keyword: %s", unknown_header); |
265 | 0 | } |
266 | 0 | } |
267 | 0 | size_t num_channels = header->is_gray ? 1 : 3; |
268 | 0 | if (header->has_alpha) num_channels++; |
269 | 0 | if (num_channels + header->ec_types.size() != depth) { |
270 | 0 | return JXL_FAILURE("PAM: bad DEPTH"); |
271 | 0 | } |
272 | 0 | if (max_val == 0 || max_val >= 65536) { |
273 | 0 | return JXL_FAILURE("PAM: bad MAXVAL"); |
274 | 0 | } |
275 | | // e.g. When `max_val` is 1 , we want 1 bit: |
276 | 0 | header->bits_per_sample = FloorLog2Nonzero(max_val) + 1; |
277 | 0 | if ((1u << header->bits_per_sample) - 1 != max_val) |
278 | 0 | return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)"); |
279 | | // PAM does not pack bits as in PBM. |
280 | | |
281 | 0 | header->floating_point = false; |
282 | 0 | header->big_endian = true; |
283 | 0 | *pos = pos_; |
284 | 0 | return true; |
285 | 0 | } |
286 | | |
287 | 0 | Status ParseHeaderPNM(HeaderPNM* header, const uint8_t** pos) { |
288 | 0 | JXL_RETURN_IF_ERROR(SkipWhitespace()); |
289 | 0 | JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); |
290 | | |
291 | 0 | JXL_RETURN_IF_ERROR(SkipWhitespace()); |
292 | 0 | JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); |
293 | | |
294 | 0 | JXL_RETURN_IF_ERROR(SkipWhitespace()); |
295 | 0 | size_t max_val; |
296 | 0 | JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val)); |
297 | 0 | if (max_val == 0 || max_val >= 65536) { |
298 | 0 | return JXL_FAILURE("PNM: bad MaxVal"); |
299 | 0 | } |
300 | 0 | header->bits_per_sample = FloorLog2Nonzero(max_val) + 1; |
301 | 0 | if ((1u << header->bits_per_sample) - 1 != max_val) |
302 | 0 | return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)"); |
303 | 0 | header->floating_point = false; |
304 | 0 | header->big_endian = true; |
305 | |
|
306 | 0 | JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); |
307 | | |
308 | 0 | *pos = pos_; |
309 | 0 | return true; |
310 | 0 | } |
311 | | |
312 | 0 | Status ParseHeaderPFM(HeaderPNM* header, const uint8_t** pos) { |
313 | 0 | JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); |
314 | 0 | JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); |
315 | | |
316 | 0 | JXL_RETURN_IF_ERROR(SkipBlank()); |
317 | 0 | JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); |
318 | | |
319 | 0 | JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); |
320 | | // The scale has no meaning as multiplier, only its sign is used to |
321 | | // indicate endianness. All software expects nominal range 0..1. |
322 | 0 | double scale; |
323 | 0 | JXL_RETURN_IF_ERROR(ParseSigned(&scale)); |
324 | 0 | if (scale == 0.0) { |
325 | 0 | return JXL_FAILURE("PFM: bad scale factor value."); |
326 | 0 | } else if (std::abs(scale) != 1.0) { |
327 | 0 | JXL_WARNING("PFM: Discarding non-unit scale factor"); |
328 | 0 | } |
329 | 0 | header->big_endian = scale > 0.0; |
330 | 0 | header->bits_per_sample = 32; |
331 | 0 | header->floating_point = true; |
332 | |
|
333 | 0 | JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); |
334 | | |
335 | 0 | *pos = pos_; |
336 | 0 | return true; |
337 | 0 | } |
338 | | |
339 | | const uint8_t* pos_; |
340 | | const uint8_t* const end_; |
341 | | }; |
342 | | |
343 | | } // namespace |
344 | | |
345 | | struct PNMChunkedInputFrame { |
346 | 0 | JxlChunkedFrameInputSource operator()() { |
347 | 0 | return JxlChunkedFrameInputSource{ |
348 | 0 | this, |
349 | 0 | METHOD_TO_C_CALLBACK( |
350 | 0 | &PNMChunkedInputFrame::GetColorChannelsPixelFormat), |
351 | 0 | METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetColorChannelDataAt), |
352 | 0 | METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetExtraChannelPixelFormat), |
353 | 0 | METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetExtraChannelDataAt), |
354 | 0 | METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::ReleaseCurrentData)}; |
355 | 0 | } |
356 | | |
357 | 0 | void /* NOLINT */ GetColorChannelsPixelFormat(JxlPixelFormat* pixel_format) { |
358 | 0 | *pixel_format = format; |
359 | 0 | } |
360 | | |
361 | | const void* GetColorChannelDataAt(size_t xpos, size_t ypos, size_t xsize, |
362 | 0 | size_t ysize, size_t* row_offset) { |
363 | 0 | const size_t bytes_per_channel = |
364 | 0 | DivCeil(dec->header_.bits_per_sample, jxl::kBitsPerByte); |
365 | 0 | const size_t num_channels = dec->header_.is_gray ? 1 : 3; |
366 | 0 | const size_t bytes_per_pixel = num_channels * bytes_per_channel; |
367 | 0 | *row_offset = dec->header_.xsize * bytes_per_pixel; |
368 | 0 | const size_t offset = ypos * *row_offset + xpos * bytes_per_pixel; |
369 | 0 | return dec->pnm_.data() + offset + dec->data_start_; |
370 | 0 | } |
371 | | |
372 | | void GetExtraChannelPixelFormat(size_t ec_index, |
373 | 0 | JxlPixelFormat* pixel_format) { |
374 | 0 | (void)this; |
375 | 0 | *pixel_format = {}; |
376 | 0 | JXL_DEBUG_ABORT("Not implemented"); |
377 | 0 | } |
378 | | |
379 | | const void* GetExtraChannelDataAt(size_t ec_index, size_t xpos, size_t ypos, |
380 | | size_t xsize, size_t ysize, |
381 | 0 | size_t* row_offset) { |
382 | 0 | (void)this; |
383 | 0 | *row_offset = 0; |
384 | 0 | JXL_DEBUG_ABORT("Not implemented"); |
385 | 0 | return nullptr; |
386 | 0 | } |
387 | | |
388 | 0 | void ReleaseCurrentData(const void* buffer) {} |
389 | | |
390 | | JxlPixelFormat format; |
391 | | const ChunkedPNMDecoder* dec; |
392 | | }; |
393 | | |
394 | | StatusOr<ChunkedPNMDecoder> ChunkedPNMDecoder::Init( |
395 | 0 | const char* path, const SizeConstraints* constraints) { |
396 | 0 | ChunkedPNMDecoder dec; |
397 | 0 | JXL_ASSIGN_OR_RETURN(dec.pnm_, MemoryMappedFile::Init(path)); |
398 | 0 | size_t size = dec.pnm_.size(); |
399 | 0 | if (size < 2) return JXL_FAILURE("Invalid ppm"); |
400 | 0 | size_t hdr_buf = std::min<size_t>(size, 10 * 1024); |
401 | 0 | Span<const uint8_t> span(dec.pnm_.data(), hdr_buf); |
402 | 0 | Parser parser(span); |
403 | 0 | HeaderPNM& header = dec.header_; |
404 | 0 | const uint8_t* pos = nullptr; |
405 | 0 | if (!parser.ParseHeader(&header, &pos)) { |
406 | 0 | return StatusCode::kGenericError; |
407 | 0 | } |
408 | 0 | dec.data_start_ = pos - span.data(); |
409 | |
|
410 | 0 | if (header.bits_per_sample == 0 || header.bits_per_sample > 16) { |
411 | 0 | return JXL_FAILURE("Invalid bits_per_sample"); |
412 | 0 | } |
413 | 0 | if (header.has_alpha || !header.ec_types.empty() || header.floating_point) { |
414 | 0 | return JXL_FAILURE("Only PGM and PPM inputs are supported"); |
415 | 0 | } |
416 | 0 | JXL_RETURN_IF_ERROR( |
417 | 0 | VerifyDimensions(constraints, header.xsize, header.ysize)); |
418 | | |
419 | 0 | const size_t bytes_per_channel = |
420 | 0 | DivCeil(dec.header_.bits_per_sample, jxl::kBitsPerByte); |
421 | 0 | const size_t num_channels = dec.header_.is_gray ? 1 : 3; |
422 | 0 | const size_t bytes_per_pixel = num_channels * bytes_per_channel; |
423 | 0 | size_t row_size; |
424 | 0 | if (!SafeMul(dec.header_.xsize, bytes_per_pixel, row_size)) { |
425 | 0 | return JXL_FAILURE("PNM image dimensions are too large"); |
426 | 0 | } |
427 | 0 | size_t required_size; |
428 | 0 | if (!SafeMul(header.ysize, row_size, required_size) || |
429 | 0 | !SafeAdd(required_size, dec.data_start_, required_size)) { |
430 | 0 | return JXL_FAILURE("PNM image dimensions are too large"); |
431 | 0 | } |
432 | 0 | if (size < required_size) { |
433 | 0 | return JXL_FAILURE("PNM file too small"); |
434 | 0 | } |
435 | 0 | return dec; |
436 | 0 | } |
437 | | |
438 | | jxl::Status ChunkedPNMDecoder::InitializePPF(const ColorHints& color_hints, |
439 | 0 | PackedPixelFile* ppf) { |
440 | | // PPM specifies that in the raster, the sample values are "nonlinear" |
441 | | // (BP.709, with gamma number of 2.2). Deviate from the specification and |
442 | | // assume `sRGB` in our implementation. |
443 | 0 | JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, |
444 | 0 | header_.is_gray, ppf)); |
445 | | |
446 | 0 | ppf->info.xsize = header_.xsize; |
447 | 0 | ppf->info.ysize = header_.ysize; |
448 | 0 | ppf->info.bits_per_sample = header_.bits_per_sample; |
449 | 0 | ppf->info.exponent_bits_per_sample = 0; |
450 | 0 | ppf->info.orientation = JXL_ORIENT_IDENTITY; |
451 | 0 | ppf->info.alpha_bits = 0; |
452 | 0 | ppf->info.alpha_exponent_bits = 0; |
453 | 0 | ppf->info.num_color_channels = (header_.is_gray ? 1 : 3); |
454 | 0 | ppf->info.num_extra_channels = 0; |
455 | |
|
456 | 0 | const JxlDataType data_type = |
457 | 0 | header_.bits_per_sample > 8 ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8; |
458 | 0 | const JxlPixelFormat format{ |
459 | 0 | /*num_channels=*/ppf->info.num_color_channels, |
460 | 0 | /*data_type=*/data_type, |
461 | 0 | /*endianness=*/header_.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN, |
462 | 0 | /*align=*/0, |
463 | 0 | }; |
464 | |
|
465 | 0 | PNMChunkedInputFrame frame; |
466 | 0 | frame.format = format; |
467 | 0 | frame.dec = this; |
468 | 0 | ppf->chunked_frames.emplace_back(header_.xsize, header_.ysize, frame); |
469 | 0 | return true; |
470 | 0 | } |
471 | | |
472 | | Status DecodeImagePNM(const Span<const uint8_t> bytes, |
473 | | const ColorHints& color_hints, PackedPixelFile* ppf, |
474 | 0 | const SizeConstraints* constraints) { |
475 | 0 | Parser parser(bytes); |
476 | 0 | HeaderPNM header = {}; |
477 | 0 | const uint8_t* pos = nullptr; |
478 | 0 | if (!parser.ParseHeader(&header, &pos)) return false; |
479 | 0 | JXL_RETURN_IF_ERROR( |
480 | 0 | VerifyDimensions(constraints, header.xsize, header.ysize)); |
481 | | |
482 | 0 | if (header.bits_per_sample == 0 || header.bits_per_sample > 32) { |
483 | 0 | return JXL_FAILURE("PNM: bits_per_sample invalid"); |
484 | 0 | } |
485 | | |
486 | | // PPM specifies that in the raster, the sample values are "nonlinear" |
487 | | // (BP.709, with gamma number of 2.2). Deviate from the specification and |
488 | | // assume `sRGB` in our implementation. |
489 | 0 | JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, |
490 | 0 | header.is_gray, ppf)); |
491 | | |
492 | 0 | ppf->info.xsize = header.xsize; |
493 | 0 | ppf->info.ysize = header.ysize; |
494 | 0 | if (header.floating_point) { |
495 | 0 | ppf->info.bits_per_sample = 32; |
496 | 0 | ppf->info.exponent_bits_per_sample = 8; |
497 | 0 | } else { |
498 | 0 | ppf->info.bits_per_sample = header.bits_per_sample; |
499 | 0 | ppf->info.exponent_bits_per_sample = 0; |
500 | 0 | } |
501 | |
|
502 | 0 | ppf->info.orientation = JXL_ORIENT_IDENTITY; |
503 | | |
504 | | // No alpha in PNM and PFM |
505 | 0 | ppf->info.alpha_bits = (header.has_alpha ? ppf->info.bits_per_sample : 0); |
506 | 0 | ppf->info.alpha_exponent_bits = 0; |
507 | 0 | ppf->info.num_color_channels = (header.is_gray ? 1 : 3); |
508 | 0 | uint32_t num_alpha_channels = (header.has_alpha ? 1 : 0); |
509 | 0 | uint32_t num_interleaved_channels = |
510 | 0 | ppf->info.num_color_channels + num_alpha_channels; |
511 | 0 | ppf->info.num_extra_channels = num_alpha_channels + header.ec_types.size(); |
512 | |
|
513 | 0 | for (auto type : header.ec_types) { |
514 | 0 | PackedExtraChannel pec = {}; |
515 | 0 | pec.ec_info.bits_per_sample = ppf->info.bits_per_sample; |
516 | 0 | pec.ec_info.type = type; |
517 | 0 | ppf->extra_channels_info.emplace_back(std::move(pec)); |
518 | 0 | } |
519 | |
|
520 | 0 | JxlDataType data_type; |
521 | 0 | if (header.floating_point) { |
522 | | // There's no float16 pnm version. |
523 | 0 | data_type = JXL_TYPE_FLOAT; |
524 | 0 | } else { |
525 | 0 | if (header.bits_per_sample > 8) { |
526 | 0 | data_type = JXL_TYPE_UINT16; |
527 | 0 | } else { |
528 | 0 | data_type = JXL_TYPE_UINT8; |
529 | 0 | } |
530 | 0 | } |
531 | | |
532 | | // No align - pixels are tightly packed. |
533 | 0 | constexpr size_t kAlign = 0; |
534 | 0 | size_t twidth = PackedImage::BitsPerChannel(data_type) / 8; |
535 | 0 | const JxlPixelFormat format{ |
536 | 0 | /*num_channels=*/num_interleaved_channels, |
537 | 0 | /*data_type=*/data_type, |
538 | 0 | /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN, |
539 | 0 | kAlign, |
540 | 0 | }; |
541 | | // EC format is same as color, but 1-channel. |
542 | 0 | JxlPixelFormat ec_format = format; |
543 | 0 | ec_format.num_channels = 1; |
544 | | // Compute required pixel-data size with overflow checks. Without these, |
545 | | // a crafted header (large xsize/ysize) wraps the multiplication and lets |
546 | | // the size check below pass while the actual memcpy below reads OOB. |
547 | 0 | size_t total_channels = num_interleaved_channels + header.ec_types.size(); |
548 | 0 | size_t required_pnm_size; |
549 | 0 | if (!SafeMul(header.xsize, total_channels, required_pnm_size) || |
550 | 0 | !SafeMul(required_pnm_size, twidth, required_pnm_size) || |
551 | 0 | !SafeMul(required_pnm_size, header.ysize, required_pnm_size)) { |
552 | 0 | return JXL_FAILURE("PNM image dimensions are too large"); |
553 | 0 | } |
554 | 0 | size_t pnm_remaining_size = bytes.data() + bytes.size() - pos; |
555 | 0 | if (pnm_remaining_size < required_pnm_size) { |
556 | 0 | return JXL_FAILURE("PNM file too small"); |
557 | 0 | } |
558 | | |
559 | 0 | ppf->frames.clear(); |
560 | 0 | { |
561 | 0 | JXL_ASSIGN_OR_RETURN( |
562 | 0 | PackedFrame frame, |
563 | 0 | PackedFrame::Create(header.xsize, header.ysize, format)); |
564 | 0 | ppf->frames.emplace_back(std::move(frame)); |
565 | 0 | } |
566 | 0 | auto* frame = &ppf->frames.back(); |
567 | 0 | uint8_t* out = reinterpret_cast<uint8_t*>(frame->color.pixels()); |
568 | 0 | std::vector<uint8_t*> ec_out; |
569 | 0 | for (size_t i = 0; i < header.ec_types.size(); ++i) { |
570 | 0 | JXL_ASSIGN_OR_RETURN( |
571 | 0 | PackedImage ec, |
572 | 0 | PackedImage::Create(header.xsize, header.ysize, ec_format)); |
573 | 0 | frame->extra_channels.emplace_back(std::move(ec)); |
574 | 0 | ec_out.emplace_back( |
575 | 0 | reinterpret_cast<uint8_t*>(frame->extra_channels.back().pixels())); |
576 | 0 | JXL_DASSERT(frame->extra_channels.back().stride == header.xsize * twidth); |
577 | 0 | } |
578 | 0 | JXL_DASSERT(frame->color.stride == |
579 | 0 | header.xsize * num_interleaved_channels * twidth); |
580 | 0 | if (ec_out.empty()) { |
581 | 0 | const bool flipped_y = (header.bits_per_sample == 32); // PFMs are flipped |
582 | 0 | if (!flipped_y) { |
583 | | // When there are no EC and input is not flipped we can copy the whole |
584 | | // image at once. |
585 | 0 | memcpy(out, pos, header.ysize * frame->color.stride); |
586 | 0 | } else { |
587 | | // Otherwise copy row-by-row. |
588 | 0 | for (size_t y = 0; y < header.ysize; ++y) { |
589 | 0 | size_t y_out = header.ysize - 1 - y; |
590 | 0 | const uint8_t* row_in = pos + y * frame->color.stride; |
591 | 0 | uint8_t* row_out = out + y_out * frame->color.stride; |
592 | 0 | memcpy(row_out, row_in, frame->color.stride); |
593 | 0 | } |
594 | 0 | } |
595 | 0 | } else { |
596 | | // In case there are EC, we have to deinterleave data pixel-wise. |
597 | 0 | JXL_RETURN_IF_ERROR(PackedImage::ValidateDataType(data_type)); |
598 | 0 | size_t color_stride = twidth * num_interleaved_channels; |
599 | 0 | for (size_t y = 0; y < header.ysize; ++y) { |
600 | 0 | for (size_t x = 0; x < header.xsize; ++x) { |
601 | 0 | memcpy(out, pos, frame->color.pixel_stride()); |
602 | 0 | out += color_stride; |
603 | 0 | pos += color_stride; |
604 | 0 | for (auto& p : ec_out) { |
605 | 0 | memcpy(p, pos, twidth); |
606 | 0 | pos += twidth; |
607 | 0 | p += twidth; |
608 | 0 | } |
609 | 0 | } |
610 | 0 | } |
611 | 0 | } |
612 | 0 | if (ppf->info.exponent_bits_per_sample == 0) { |
613 | 0 | ppf->input_bitdepth.type = JXL_BIT_DEPTH_FROM_CODESTREAM; |
614 | 0 | } |
615 | 0 | return true; |
616 | 0 | } |
617 | | |
618 | | // Exposed for testing. |
619 | 0 | Status PnmParseSigned(Bytes str, double* v) { |
620 | 0 | return Parser(str).ParseSigned(v); |
621 | 0 | } |
622 | | |
623 | 0 | Status PnmParseUnsigned(Bytes str, size_t* v) { |
624 | 0 | return Parser(str).ParseUnsigned(v); |
625 | 0 | } |
626 | | |
627 | | } // namespace extras |
628 | | } // namespace jxl |