Coverage Report

Created: 2026-09-28 07:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
1.21k
      : pos_(bytes.data()), end_(pos_ + bytes.size()) {}
39
40
  // Sets "pos" to the first non-header byte/pixel on success.
41
1.21k
  Status ParseHeader(HeaderPNM* header, const uint8_t** pos) {
42
    // codec.cc ensures we have at least two bytes => no range check here.
43
1.21k
    if (pos_[0] != 'P') return false;
44
1.21k
    const uint8_t type = pos_[1];
45
1.21k
    pos_ += 2;
46
47
1.21k
    switch (type) {
48
1
      case '1':
49
1
        return JXL_FAILURE("ascii pbm not supported");
50
51
1
      case '2':
52
1
        return JXL_FAILURE("ascii pgm not supported");
53
54
1
      case '3':
55
1
        return JXL_FAILURE("ascii ppm not supported");
56
57
1
      case '4':
58
1
        return JXL_FAILURE("pbm not supported");
59
60
127
      case '5':
61
127
        header->is_gray = true;
62
127
        return ParseHeaderPNM(header, pos);
63
64
126
      case '6':
65
126
        header->is_gray = false;
66
126
        return ParseHeaderPNM(header, pos);
67
68
471
      case '7':
69
471
        return ParseHeaderPAM(header, pos);
70
71
434
      case 'F':
72
434
        header->is_gray = false;
73
434
        return ParseHeaderPFM(header, pos);
74
75
57
      case 'f':
76
57
        header->is_gray = true;
77
57
        return ParseHeaderPFM(header, pos);
78
79
0
      default:
80
0
        return false;
81
1.21k
    }
82
1.21k
  }
83
84
  // Exposed for testing
85
2.62k
  Status ParseUnsigned(size_t* number) {
86
2.62k
    if (pos_ == end_) return JXL_FAILURE("PNM: reached end before number");
87
2.58k
    if (!IsDigit(*pos_)) return JXL_FAILURE("PNM: expected unsigned number");
88
89
2.54k
    *number = 0;
90
14.5k
    while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
91
12.1k
      const size_t digit = *pos_ - '0';
92
12.1k
      if (!SafeMul(*number, static_cast<size_t>(10), *number) ||
93
12.0k
          !SafeAdd(*number, digit, *number)) {
94
82
        return JXL_FAILURE("PNM: unsigned number too large");
95
82
      }
96
12.0k
      ++pos_;
97
12.0k
    }
98
99
2.46k
    return true;
100
2.54k
  }
101
102
344
  Status ParseSigned(double* number) {
103
344
    if (pos_ == end_) return JXL_FAILURE("PNM: reached end before signed");
104
105
340
    if (*pos_ != '-' && *pos_ != '+' && !IsDigit(*pos_)) {
106
8
      return JXL_FAILURE("PNM: expected signed number");
107
8
    }
108
109
    // Skip sign
110
332
    const bool is_neg = *pos_ == '-';
111
332
    if (is_neg || *pos_ == '+') {
112
7
      ++pos_;
113
7
      if (pos_ == end_) return JXL_FAILURE("PNM: reached end before digits");
114
7
    }
115
116
    // Leading digits
117
331
    *number = 0.0;
118
819
    while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
119
488
      *number *= 10;
120
488
      *number += *pos_ - '0';
121
488
      ++pos_;
122
488
    }
123
124
    // Decimal places?
125
331
    if (pos_ < end_ && *pos_ == '.') {
126
14
      ++pos_;
127
14
      double place = 0.1;
128
276
      while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
129
262
        *number += (*pos_ - '0') * place;
130
262
        place *= 0.1;
131
262
        ++pos_;
132
262
      }
133
14
    }
134
135
331
    if (is_neg) *number = -*number;
136
331
    return true;
137
332
  }
138
139
 private:
140
2.91k
  static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; }
141
50.9k
  static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; }
142
35.9k
  static bool IsWhitespace(const uint8_t c) {
143
35.9k
    return IsLineBreak(c) || c == '\t' || c == ' ';
144
35.9k
  }
145
146
438
  Status SkipBlank() {
147
438
    if (pos_ == end_) return JXL_FAILURE("PNM: reached end before blank");
148
415
    const uint8_t c = *pos_;
149
415
    if (c != ' ' && c != '\n') return JXL_FAILURE("PNM: expected blank");
150
396
    ++pos_;
151
396
    return true;
152
415
  }
153
154
1.48k
  Status SkipSingleWhitespace() {
155
1.48k
    if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace");
156
1.47k
    if (!IsWhitespace(*pos_)) return JXL_FAILURE("PNM: expected whitespace");
157
1.22k
    ++pos_;
158
1.22k
    return true;
159
1.47k
  }
160
161
8.42k
  Status SkipWhitespace() {
162
8.42k
    if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace");
163
8.39k
    if (!IsWhitespace(*pos_) && *pos_ != '#') {
164
278
      return JXL_FAILURE("PNM: expected whitespace/comment");
165
278
    }
166
167
17.3k
    while (pos_ < end_ && IsWhitespace(*pos_)) {
168
9.20k
      ++pos_;
169
9.20k
    }
170
171
    // Comment(s)
172
9.17k
    while (pos_ != end_ && *pos_ == '#') {
173
12.9k
      while (pos_ != end_ && !IsLineBreak(*pos_)) {
174
11.8k
        ++pos_;
175
11.8k
      }
176
      // Newline(s)
177
2.16k
      while (pos_ != end_ && IsLineBreak(*pos_)) pos_++;
178
1.05k
    }
179
180
8.82k
    while (pos_ < end_ && IsWhitespace(*pos_)) {
181
702
      ++pos_;
182
702
    }
183
8.12k
    return true;
184
8.39k
  }
185
186
40.0k
  Status MatchString(const char* keyword, bool skipws = true) {
187
40.0k
    const uint8_t* ppos = pos_;
188
40.0k
    const uint8_t* kw = reinterpret_cast<const uint8_t*>(keyword);
189
98.1k
    while (*kw) {
190
91.6k
      if (ppos >= end_) return JXL_FAILURE("PAM: unexpected end of input");
191
91.2k
      if (*kw != *ppos) return false;
192
58.1k
      ppos++;
193
58.1k
      kw++;
194
58.1k
    }
195
6.44k
    pos_ = ppos;
196
6.44k
    if (skipws) {
197
6.19k
      JXL_RETURN_IF_ERROR(SkipWhitespace());
198
6.19k
    } else {
199
255
      JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
200
255
    }
201
5.97k
    return true;
202
6.44k
  }
203
204
471
  Status ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) {
205
471
    size_t depth = 3;
206
471
    size_t max_val = 255;
207
471
    JXL_RETURN_IF_ERROR(SkipWhitespace());
208
3.89k
    while (!MatchString("ENDHDR", /*skipws=*/false)) {
209
3.84k
      if (MatchString("WIDTH")) {
210
261
        JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
211
259
        JXL_RETURN_IF_ERROR(SkipWhitespace());
212
3.58k
      } else if (MatchString("HEIGHT")) {
213
347
        JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
214
340
        JXL_RETURN_IF_ERROR(SkipWhitespace());
215
3.23k
      } else if (MatchString("DEPTH")) {
216
85
        JXL_RETURN_IF_ERROR(ParseUnsigned(&depth));
217
83
        JXL_RETURN_IF_ERROR(SkipWhitespace());
218
3.15k
      } else if (MatchString("MAXVAL")) {
219
440
        JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val));
220
436
        JXL_RETURN_IF_ERROR(SkipWhitespace());
221
2.71k
      } else if (MatchString("TUPLTYPE")) {
222
2.46k
        if (MatchString("RGB_ALPHA")) {
223
160
          header->has_alpha = true;
224
2.30k
        } else if (MatchString("RGB")) {
225
2.16k
        } else if (MatchString("GRAYSCALE_ALPHA")) {
226
70
          header->has_alpha = true;
227
70
          header->is_gray = true;
228
2.09k
        } else if (MatchString("GRAYSCALE")) {
229
174
          header->is_gray = true;
230
1.92k
        } else if (MatchString("BLACKANDWHITE_ALPHA")) {
231
479
          header->has_alpha = true;
232
479
          header->is_gray = true;
233
479
          max_val = 1;
234
1.44k
        } else if (MatchString("BLACKANDWHITE")) {
235
320
          header->is_gray = true;
236
320
          max_val = 1;
237
1.12k
        } else if (MatchString("Alpha")) {
238
0
          header->ec_types.push_back(JXL_CHANNEL_ALPHA);
239
1.12k
        } else if (MatchString("Depth")) {
240
0
          header->ec_types.push_back(JXL_CHANNEL_DEPTH);
241
1.12k
        } else if (MatchString("SpotColor")) {
242
0
          header->ec_types.push_back(JXL_CHANNEL_SPOT_COLOR);
243
1.12k
        } else if (MatchString("SelectionMask")) {
244
0
          header->ec_types.push_back(JXL_CHANNEL_SELECTION_MASK);
245
1.12k
        } else if (MatchString("Black")) {
246
0
          header->ec_types.push_back(JXL_CHANNEL_BLACK);
247
1.12k
        } else if (MatchString("CFA")) {
248
974
          header->ec_types.push_back(JXL_CHANNEL_CFA);
249
974
        } else if (MatchString("Thermal")) {
250
0
          header->ec_types.push_back(JXL_CHANNEL_THERMAL);
251
150
        } else if (MatchString("Unknown")) {
252
0
          header->ec_types.push_back(JXL_CHANNEL_UNKNOWN);
253
150
        } else if (MatchString("Optional")) {
254
0
          header->ec_types.push_back(JXL_CHANNEL_OPTIONAL);
255
150
        } else {
256
150
          return JXL_FAILURE("PAM: unknown TUPLTYPE");
257
150
        }
258
2.46k
      } else {
259
242
        constexpr size_t kMaxHeaderLength = 20;
260
242
        char unknown_header[kMaxHeaderLength + 1];
261
242
        size_t len = std::min<size_t>(kMaxHeaderLength, end_ - pos_);
262
242
        strncpy(unknown_header, reinterpret_cast<const char*>(pos_), len);
263
242
        unknown_header[len] = 0;
264
242
        return JXL_FAILURE("PAM: unknown header keyword: %s", unknown_header);
265
242
      }
266
3.84k
    }
267
54
    size_t num_channels = header->is_gray ? 1 : 3;
268
54
    if (header->has_alpha) num_channels++;
269
54
    if (num_channels + header->ec_types.size() != depth) {
270
0
      return JXL_FAILURE("PAM: bad DEPTH");
271
0
    }
272
54
    if (max_val == 0 || max_val >= 65536) {
273
49
      return JXL_FAILURE("PAM: bad MAXVAL");
274
49
    }
275
    // e.g. When `max_val` is 1 , we want 1 bit:
276
5
    header->bits_per_sample = FloorLog2Nonzero(max_val) + 1;
277
5
    if ((1u << header->bits_per_sample) - 1 != max_val)
278
1
      return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)");
279
    // PAM does not pack bits as in PBM.
280
281
4
    header->floating_point = false;
282
4
    header->big_endian = true;
283
4
    *pos = pos_;
284
4
    return true;
285
5
  }
286
287
253
  Status ParseHeaderPNM(HeaderPNM* header, const uint8_t** pos) {
288
253
    JXL_RETURN_IF_ERROR(SkipWhitespace());
289
249
    JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
290
291
205
    JXL_RETURN_IF_ERROR(SkipWhitespace());
292
195
    JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
293
294
185
    JXL_RETURN_IF_ERROR(SkipWhitespace());
295
179
    size_t max_val;
296
179
    JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val));
297
163
    if (max_val == 0 || max_val >= 65536) {
298
94
      return JXL_FAILURE("PNM: bad MaxVal");
299
94
    }
300
69
    header->bits_per_sample = FloorLog2Nonzero(max_val) + 1;
301
69
    if ((1u << header->bits_per_sample) - 1 != max_val)
302
11
      return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)");
303
58
    header->floating_point = false;
304
58
    header->big_endian = true;
305
306
58
    JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
307
308
52
    *pos = pos_;
309
52
    return true;
310
58
  }
311
312
491
  Status ParseHeaderPFM(HeaderPNM* header, const uint8_t** pos) {
313
491
    JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
314
477
    JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
315
316
438
    JXL_RETURN_IF_ERROR(SkipBlank());
317
396
    JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
318
319
357
    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
344
    double scale;
323
344
    JXL_RETURN_IF_ERROR(ParseSigned(&scale));
324
331
    if (scale == 0.0) {
325
6
      return JXL_FAILURE("PFM: bad scale factor value.");
326
325
    } else if (std::abs(scale) != 1.0) {
327
155
      JXL_WARNING("PFM: Discarding non-unit scale factor");
328
155
    }
329
325
    header->big_endian = scale > 0.0;
330
325
    header->bits_per_sample = 32;
331
325
    header->floating_point = true;
332
333
325
    JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
334
335
300
    *pos = pos_;
336
300
    return true;
337
325
  }
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
1.21k
                      const SizeConstraints* constraints) {
475
1.21k
  Parser parser(bytes);
476
1.21k
  HeaderPNM header = {};
477
1.21k
  const uint8_t* pos = nullptr;
478
1.21k
  if (!parser.ParseHeader(&header, &pos)) return false;
479
356
  JXL_RETURN_IF_ERROR(
480
356
      VerifyDimensions(constraints, header.xsize, header.ysize));
481
482
115
  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
115
  JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
490
115
                                      header.is_gray, ppf));
491
492
115
  ppf->info.xsize = header.xsize;
493
115
  ppf->info.ysize = header.ysize;
494
115
  if (header.floating_point) {
495
74
    ppf->info.bits_per_sample = 32;
496
74
    ppf->info.exponent_bits_per_sample = 8;
497
74
  } else {
498
41
    ppf->info.bits_per_sample = header.bits_per_sample;
499
41
    ppf->info.exponent_bits_per_sample = 0;
500
41
  }
501
502
115
  ppf->info.orientation = JXL_ORIENT_IDENTITY;
503
504
  // No alpha in PNM and PFM
505
115
  ppf->info.alpha_bits = (header.has_alpha ? ppf->info.bits_per_sample : 0);
506
115
  ppf->info.alpha_exponent_bits = 0;
507
115
  ppf->info.num_color_channels = (header.is_gray ? 1 : 3);
508
115
  uint32_t num_alpha_channels = (header.has_alpha ? 1 : 0);
509
115
  uint32_t num_interleaved_channels =
510
115
      ppf->info.num_color_channels + num_alpha_channels;
511
115
  ppf->info.num_extra_channels = num_alpha_channels + header.ec_types.size();
512
513
115
  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
115
  JxlDataType data_type;
521
115
  if (header.floating_point) {
522
    // There's no float16 pnm version.
523
74
    data_type = JXL_TYPE_FLOAT;
524
74
  } else {
525
41
    if (header.bits_per_sample > 8) {
526
4
      data_type = JXL_TYPE_UINT16;
527
37
    } else {
528
37
      data_type = JXL_TYPE_UINT8;
529
37
    }
530
41
  }
531
532
  // No align - pixels are tightly packed.
533
115
  constexpr size_t kAlign = 0;
534
115
  size_t twidth = PackedImage::BitsPerChannel(data_type) / 8;
535
115
  const JxlPixelFormat format{
536
115
      /*num_channels=*/num_interleaved_channels,
537
115
      /*data_type=*/data_type,
538
115
      /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN,
539
115
      kAlign,
540
115
  };
541
  // EC format is same as color, but 1-channel.
542
115
  JxlPixelFormat ec_format = format;
543
115
  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
115
  size_t total_channels = num_interleaved_channels + header.ec_types.size();
548
115
  size_t required_pnm_size;
549
115
  if (!SafeMul(header.xsize, total_channels, required_pnm_size) ||
550
115
      !SafeMul(required_pnm_size, twidth, required_pnm_size) ||
551
115
      !SafeMul(required_pnm_size, header.ysize, required_pnm_size)) {
552
0
    return JXL_FAILURE("PNM image dimensions are too large");
553
0
  }
554
115
  size_t pnm_remaining_size = bytes.data() + bytes.size() - pos;
555
115
  if (pnm_remaining_size < required_pnm_size) {
556
37
    return JXL_FAILURE("PNM file too small");
557
37
  }
558
559
78
  ppf->frames.clear();
560
78
  {
561
78
    JXL_ASSIGN_OR_RETURN(
562
78
        PackedFrame frame,
563
78
        PackedFrame::Create(header.xsize, header.ysize, format));
564
78
    ppf->frames.emplace_back(std::move(frame));
565
78
  }
566
0
  auto* frame = &ppf->frames.back();
567
78
  uint8_t* out = reinterpret_cast<uint8_t*>(frame->color.pixels());
568
78
  std::vector<uint8_t*> ec_out;
569
78
  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
78
  JXL_DASSERT(frame->color.stride ==
579
78
              header.xsize * num_interleaved_channels * twidth);
580
78
  if (ec_out.empty()) {
581
78
    const bool flipped_y = (header.bits_per_sample == 32);  // PFMs are flipped
582
78
    if (!flipped_y) {
583
    // When there are no EC and input is not flipped we can copy the whole
584
    // image at once.
585
37
      memcpy(out, pos, header.ysize * frame->color.stride);
586
41
    } else {
587
      // Otherwise copy row-by-row.
588
649
      for (size_t y = 0; y < header.ysize; ++y) {
589
608
        size_t y_out = header.ysize - 1 - y;
590
608
        const uint8_t* row_in = pos + y * frame->color.stride;
591
608
        uint8_t* row_out = out + y_out * frame->color.stride;
592
608
        memcpy(row_out, row_in, frame->color.stride);
593
608
      }
594
41
    }
595
78
  } 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
78
  if (ppf->info.exponent_bits_per_sample == 0) {
613
37
    ppf->input_bitdepth.type = JXL_BIT_DEPTH_FROM_CODESTREAM;
614
37
  }
615
78
  return true;
616
78
}
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