Coverage Report

Created: 2026-06-30 07:53

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
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