Coverage Report

Created: 2023-08-28 07:24

/src/libjxl/lib/extras/dec/pnm.cc
Line
Count
Source (jump to first uncovered line)
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 <stdlib.h>
9
#include <string.h>
10
11
#include <cmath>
12
13
#include "lib/extras/size_constraints.h"
14
#include "lib/jxl/base/bits.h"
15
#include "lib/jxl/base/compiler_specific.h"
16
#include "lib/jxl/base/status.h"
17
18
namespace jxl {
19
namespace extras {
20
namespace {
21
22
struct HeaderPNM {
23
  size_t xsize;
24
  size_t ysize;
25
  bool is_gray;    // PGM
26
  bool has_alpha;  // PAM
27
  size_t bits_per_sample;
28
  bool floating_point;
29
  bool big_endian;
30
  std::vector<JxlExtraChannelType> ec_types;  // PAM
31
};
32
33
class Parser {
34
 public:
35
  explicit Parser(const Span<const uint8_t> bytes)
36
0
      : pos_(bytes.data()), end_(pos_ + bytes.size()) {}
37
38
  // Sets "pos" to the first non-header byte/pixel on success.
39
0
  Status ParseHeader(HeaderPNM* header, const uint8_t** pos) {
40
    // codec.cc ensures we have at least two bytes => no range check here.
41
0
    if (pos_[0] != 'P') return false;
42
0
    const uint8_t type = pos_[1];
43
0
    pos_ += 2;
44
45
0
    switch (type) {
46
0
      case '4':
47
0
        return JXL_FAILURE("pbm not supported");
48
49
0
      case '5':
50
0
        header->is_gray = true;
51
0
        return ParseHeaderPNM(header, pos);
52
53
0
      case '6':
54
0
        header->is_gray = false;
55
0
        return ParseHeaderPNM(header, pos);
56
57
0
      case '7':
58
0
        return ParseHeaderPAM(header, pos);
59
60
0
      case 'F':
61
0
        header->is_gray = false;
62
0
        return ParseHeaderPFM(header, pos);
63
64
0
      case 'f':
65
0
        header->is_gray = true;
66
0
        return ParseHeaderPFM(header, pos);
67
0
    }
68
0
    return false;
69
0
  }
70
71
  // Exposed for testing
72
0
  Status ParseUnsigned(size_t* number) {
73
0
    if (pos_ == end_) return JXL_FAILURE("PNM: reached end before number");
74
0
    if (!IsDigit(*pos_)) return JXL_FAILURE("PNM: expected unsigned number");
75
76
0
    *number = 0;
77
0
    while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
78
0
      *number *= 10;
79
0
      *number += *pos_ - '0';
80
0
      ++pos_;
81
0
    }
82
83
0
    return true;
84
0
  }
85
86
0
  Status ParseSigned(double* number) {
87
0
    if (pos_ == end_) return JXL_FAILURE("PNM: reached end before signed");
88
89
0
    if (*pos_ != '-' && *pos_ != '+' && !IsDigit(*pos_)) {
90
0
      return JXL_FAILURE("PNM: expected signed number");
91
0
    }
92
93
    // Skip sign
94
0
    const bool is_neg = *pos_ == '-';
95
0
    if (is_neg || *pos_ == '+') {
96
0
      ++pos_;
97
0
      if (pos_ == end_) return JXL_FAILURE("PNM: reached end before digits");
98
0
    }
99
100
    // Leading digits
101
0
    *number = 0.0;
102
0
    while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
103
0
      *number *= 10;
104
0
      *number += *pos_ - '0';
105
0
      ++pos_;
106
0
    }
107
108
    // Decimal places?
109
0
    if (pos_ < end_ && *pos_ == '.') {
110
0
      ++pos_;
111
0
      double place = 0.1;
112
0
      while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
113
0
        *number += (*pos_ - '0') * place;
114
0
        place *= 0.1;
115
0
        ++pos_;
116
0
      }
117
0
    }
118
119
0
    if (is_neg) *number = -*number;
120
0
    return true;
121
0
  }
122
123
 private:
124
0
  static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; }
125
0
  static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; }
126
0
  static bool IsWhitespace(const uint8_t c) {
127
0
    return IsLineBreak(c) || c == '\t' || c == ' ';
128
0
  }
129
130
0
  Status SkipBlank() {
131
0
    if (pos_ == end_) return JXL_FAILURE("PNM: reached end before blank");
132
0
    const uint8_t c = *pos_;
133
0
    if (c != ' ' && c != '\n') return JXL_FAILURE("PNM: expected blank");
134
0
    ++pos_;
135
0
    return true;
136
0
  }
137
138
0
  Status SkipSingleWhitespace() {
139
0
    if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace");
140
0
    if (!IsWhitespace(*pos_)) return JXL_FAILURE("PNM: expected whitespace");
141
0
    ++pos_;
142
0
    return true;
143
0
  }
144
145
0
  Status SkipWhitespace() {
146
0
    if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace");
147
0
    if (!IsWhitespace(*pos_) && *pos_ != '#') {
148
0
      return JXL_FAILURE("PNM: expected whitespace/comment");
149
0
    }
150
151
0
    while (pos_ < end_ && IsWhitespace(*pos_)) {
152
0
      ++pos_;
153
0
    }
154
155
    // Comment(s)
156
0
    while (pos_ != end_ && *pos_ == '#') {
157
0
      while (pos_ != end_ && !IsLineBreak(*pos_)) {
158
0
        ++pos_;
159
0
      }
160
      // Newline(s)
161
0
      while (pos_ != end_ && IsLineBreak(*pos_)) pos_++;
162
0
    }
163
164
0
    while (pos_ < end_ && IsWhitespace(*pos_)) {
165
0
      ++pos_;
166
0
    }
167
0
    return true;
168
0
  }
169
170
0
  Status MatchString(const char* keyword, bool skipws = true) {
171
0
    const uint8_t* ppos = pos_;
172
0
    while (*keyword) {
173
0
      if (ppos >= end_) return JXL_FAILURE("PAM: unexpected end of input");
174
0
      if (*keyword != *ppos) return false;
175
0
      ppos++;
176
0
      keyword++;
177
0
    }
178
0
    pos_ = ppos;
179
0
    if (skipws) {
180
0
      JXL_RETURN_IF_ERROR(SkipWhitespace());
181
0
    } else {
182
0
      JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
183
0
    }
184
0
    return true;
185
0
  }
186
187
0
  Status ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) {
188
0
    size_t depth = 3;
189
0
    size_t max_val = 255;
190
0
    JXL_RETURN_IF_ERROR(SkipWhitespace());
191
0
    while (!MatchString("ENDHDR", /*skipws=*/false)) {
192
0
      if (MatchString("WIDTH")) {
193
0
        JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
194
0
        JXL_RETURN_IF_ERROR(SkipWhitespace());
195
0
      } else if (MatchString("HEIGHT")) {
196
0
        JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
197
0
        JXL_RETURN_IF_ERROR(SkipWhitespace());
198
0
      } else if (MatchString("DEPTH")) {
199
0
        JXL_RETURN_IF_ERROR(ParseUnsigned(&depth));
200
0
        JXL_RETURN_IF_ERROR(SkipWhitespace());
201
0
      } else if (MatchString("MAXVAL")) {
202
0
        JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val));
203
0
        JXL_RETURN_IF_ERROR(SkipWhitespace());
204
0
      } else if (MatchString("TUPLTYPE")) {
205
0
        if (MatchString("RGB_ALPHA")) {
206
0
          header->has_alpha = true;
207
0
        } else if (MatchString("RGB")) {
208
0
        } else if (MatchString("GRAYSCALE_ALPHA")) {
209
0
          header->has_alpha = true;
210
0
          header->is_gray = true;
211
0
        } else if (MatchString("GRAYSCALE")) {
212
0
          header->is_gray = true;
213
0
        } else if (MatchString("BLACKANDWHITE_ALPHA")) {
214
0
          header->has_alpha = true;
215
0
          header->is_gray = true;
216
0
          max_val = 1;
217
0
        } else if (MatchString("BLACKANDWHITE")) {
218
0
          header->is_gray = true;
219
0
          max_val = 1;
220
0
        } else if (MatchString("Alpha")) {
221
0
          header->ec_types.push_back(JXL_CHANNEL_ALPHA);
222
0
        } else if (MatchString("Depth")) {
223
0
          header->ec_types.push_back(JXL_CHANNEL_DEPTH);
224
0
        } else if (MatchString("SpotColor")) {
225
0
          header->ec_types.push_back(JXL_CHANNEL_SPOT_COLOR);
226
0
        } else if (MatchString("SelectionMask")) {
227
0
          header->ec_types.push_back(JXL_CHANNEL_SELECTION_MASK);
228
0
        } else if (MatchString("Black")) {
229
0
          header->ec_types.push_back(JXL_CHANNEL_BLACK);
230
0
        } else if (MatchString("CFA")) {
231
0
          header->ec_types.push_back(JXL_CHANNEL_CFA);
232
0
        } else if (MatchString("Thermal")) {
233
0
          header->ec_types.push_back(JXL_CHANNEL_THERMAL);
234
0
        } else {
235
0
          return JXL_FAILURE("PAM: unknown TUPLTYPE");
236
0
        }
237
0
      } else {
238
0
        constexpr size_t kMaxHeaderLength = 20;
239
0
        char unknown_header[kMaxHeaderLength + 1];
240
0
        size_t len = std::min<size_t>(kMaxHeaderLength, end_ - pos_);
241
0
        strncpy(unknown_header, reinterpret_cast<const char*>(pos_), len);
242
0
        unknown_header[len] = 0;
243
0
        return JXL_FAILURE("PAM: unknown header keyword: %s", unknown_header);
244
0
      }
245
0
    }
246
0
    size_t num_channels = header->is_gray ? 1 : 3;
247
0
    if (header->has_alpha) num_channels++;
248
0
    if (num_channels + header->ec_types.size() != depth) {
249
0
      return JXL_FAILURE("PAM: bad DEPTH");
250
0
    }
251
0
    if (max_val == 0 || max_val >= 65536) {
252
0
      return JXL_FAILURE("PAM: bad MAXVAL");
253
0
    }
254
    // e.g. When `max_val` is 1 , we want 1 bit:
255
0
    header->bits_per_sample = FloorLog2Nonzero(max_val) + 1;
256
0
    if ((1u << header->bits_per_sample) - 1 != max_val)
257
0
      return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)");
258
    // PAM does not pack bits as in PBM.
259
260
0
    header->floating_point = false;
261
0
    header->big_endian = true;
262
0
    *pos = pos_;
263
0
    return true;
264
0
  }
265
266
0
  Status ParseHeaderPNM(HeaderPNM* header, const uint8_t** pos) {
267
0
    JXL_RETURN_IF_ERROR(SkipWhitespace());
268
0
    JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
269
270
0
    JXL_RETURN_IF_ERROR(SkipWhitespace());
271
0
    JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
272
273
0
    JXL_RETURN_IF_ERROR(SkipWhitespace());
274
0
    size_t max_val;
275
0
    JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val));
276
0
    if (max_val == 0 || max_val >= 65536) {
277
0
      return JXL_FAILURE("PNM: bad MaxVal");
278
0
    }
279
0
    header->bits_per_sample = FloorLog2Nonzero(max_val) + 1;
280
0
    if ((1u << header->bits_per_sample) - 1 != max_val)
281
0
      return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)");
282
0
    header->floating_point = false;
283
0
    header->big_endian = true;
284
285
0
    JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
286
287
0
    *pos = pos_;
288
0
    return true;
289
0
  }
290
291
0
  Status ParseHeaderPFM(HeaderPNM* header, const uint8_t** pos) {
292
0
    JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
293
0
    JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
294
295
0
    JXL_RETURN_IF_ERROR(SkipBlank());
296
0
    JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
297
298
0
    JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
299
    // The scale has no meaning as multiplier, only its sign is used to
300
    // indicate endianness. All software expects nominal range 0..1.
301
0
    double scale;
302
0
    JXL_RETURN_IF_ERROR(ParseSigned(&scale));
303
0
    if (scale == 0.0) {
304
0
      return JXL_FAILURE("PFM: bad scale factor value.");
305
0
    } else if (std::abs(scale) != 1.0) {
306
0
      JXL_WARNING("PFM: Discarding non-unit scale factor");
307
0
    }
308
0
    header->big_endian = scale > 0.0;
309
0
    header->bits_per_sample = 32;
310
0
    header->floating_point = true;
311
312
0
    JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
313
314
0
    *pos = pos_;
315
0
    return true;
316
0
  }
317
318
  const uint8_t* pos_;
319
  const uint8_t* const end_;
320
};
321
322
0
Span<const uint8_t> MakeSpan(const char* str) {
323
0
  return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str),
324
0
                             strlen(str));
325
0
}
326
327
}  // namespace
328
329
Status DecodeImagePNM(const Span<const uint8_t> bytes,
330
                      const ColorHints& color_hints, PackedPixelFile* ppf,
331
0
                      const SizeConstraints* constraints) {
332
0
  Parser parser(bytes);
333
0
  HeaderPNM header = {};
334
0
  const uint8_t* pos = nullptr;
335
0
  if (!parser.ParseHeader(&header, &pos)) return false;
336
0
  JXL_RETURN_IF_ERROR(
337
0
      VerifyDimensions(constraints, header.xsize, header.ysize));
338
339
0
  if (header.bits_per_sample == 0 || header.bits_per_sample > 32) {
340
0
    return JXL_FAILURE("PNM: bits_per_sample invalid");
341
0
  }
342
343
  // PPM specify that in the raster, the sample values are "nonlinear" (BP.709,
344
  // with gamma number of 2.2). Deviate from the specification and assume
345
  // `sRGB` in our implementation.
346
0
  JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
347
0
                                      header.is_gray, ppf));
348
349
0
  ppf->info.xsize = header.xsize;
350
0
  ppf->info.ysize = header.ysize;
351
0
  if (header.floating_point) {
352
0
    ppf->info.bits_per_sample = 32;
353
0
    ppf->info.exponent_bits_per_sample = 8;
354
0
  } else {
355
0
    ppf->info.bits_per_sample = header.bits_per_sample;
356
0
    ppf->info.exponent_bits_per_sample = 0;
357
0
  }
358
359
0
  ppf->info.orientation = JXL_ORIENT_IDENTITY;
360
361
  // No alpha in PNM and PFM
362
0
  ppf->info.alpha_bits = (header.has_alpha ? ppf->info.bits_per_sample : 0);
363
0
  ppf->info.alpha_exponent_bits = 0;
364
0
  ppf->info.num_color_channels = (header.is_gray ? 1 : 3);
365
0
  uint32_t num_alpha_channels = (header.has_alpha ? 1 : 0);
366
0
  uint32_t num_interleaved_channels =
367
0
      ppf->info.num_color_channels + num_alpha_channels;
368
0
  ppf->info.num_extra_channels = num_alpha_channels + header.ec_types.size();
369
370
0
  for (auto type : header.ec_types) {
371
0
    PackedExtraChannel pec;
372
0
    pec.ec_info.bits_per_sample = ppf->info.bits_per_sample;
373
0
    pec.ec_info.type = type;
374
0
    ppf->extra_channels_info.emplace_back(std::move(pec));
375
0
  }
376
377
0
  JxlDataType data_type;
378
0
  if (header.floating_point) {
379
    // There's no float16 pnm version.
380
0
    data_type = JXL_TYPE_FLOAT;
381
0
  } else {
382
0
    if (header.bits_per_sample > 8) {
383
0
      data_type = JXL_TYPE_UINT16;
384
0
    } else {
385
0
      data_type = JXL_TYPE_UINT8;
386
0
    }
387
0
  }
388
389
0
  const JxlPixelFormat format{
390
0
      /*num_channels=*/num_interleaved_channels,
391
0
      /*data_type=*/data_type,
392
0
      /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN,
393
0
      /*align=*/0,
394
0
  };
395
0
  const JxlPixelFormat ec_format{1, format.data_type, format.endianness, 0};
396
0
  ppf->frames.clear();
397
0
  ppf->frames.emplace_back(header.xsize, header.ysize, format);
398
0
  auto* frame = &ppf->frames.back();
399
0
  for (size_t i = 0; i < header.ec_types.size(); ++i) {
400
0
    frame->extra_channels.emplace_back(header.xsize, header.ysize, ec_format);
401
0
  }
402
0
  size_t pnm_remaining_size = bytes.data() + bytes.size() - pos;
403
0
  if (pnm_remaining_size < frame->color.pixels_size) {
404
0
    return JXL_FAILURE("PNM file too small");
405
0
  }
406
407
0
  uint8_t* out = reinterpret_cast<uint8_t*>(frame->color.pixels());
408
0
  std::vector<uint8_t*> ec_out(header.ec_types.size());
409
0
  for (size_t i = 0; i < ec_out.size(); ++i) {
410
0
    ec_out[i] = reinterpret_cast<uint8_t*>(frame->extra_channels[i].pixels());
411
0
  }
412
0
  if (ec_out.empty()) {
413
0
    const bool flipped_y = header.bits_per_sample == 32;  // PFMs are flipped
414
0
    for (size_t y = 0; y < header.ysize; ++y) {
415
0
      size_t y_in = flipped_y ? header.ysize - 1 - y : y;
416
0
      const uint8_t* row_in = &pos[y_in * frame->color.stride];
417
0
      uint8_t* row_out = &out[y * frame->color.stride];
418
0
      memcpy(row_out, row_in, frame->color.stride);
419
0
    }
420
0
  } else {
421
0
    size_t pwidth = PackedImage::BitsPerChannel(data_type) / 8;
422
0
    for (size_t y = 0; y < header.ysize; ++y) {
423
0
      for (size_t x = 0; x < header.xsize; ++x) {
424
0
        memcpy(out, pos, frame->color.pixel_stride());
425
0
        out += frame->color.pixel_stride();
426
0
        pos += frame->color.pixel_stride();
427
0
        for (auto& p : ec_out) {
428
0
          memcpy(p, pos, pwidth);
429
0
          pos += pwidth;
430
0
          p += pwidth;
431
0
        }
432
0
      }
433
0
    }
434
0
  }
435
0
  return true;
436
0
}
437
438
0
void TestCodecPNM() {
439
0
  size_t u = 77777;  // Initialized to wrong value.
440
0
  double d = 77.77;
441
// Failing to parse invalid strings results in a crash if `JXL_CRASH_ON_ERROR`
442
// is defined and hence the tests fail. Therefore we only run these tests if
443
// `JXL_CRASH_ON_ERROR` is not defined.
444
0
#ifndef JXL_CRASH_ON_ERROR
445
0
  JXL_CHECK(false == Parser(MakeSpan("")).ParseUnsigned(&u));
446
0
  JXL_CHECK(false == Parser(MakeSpan("+")).ParseUnsigned(&u));
447
0
  JXL_CHECK(false == Parser(MakeSpan("-")).ParseUnsigned(&u));
448
0
  JXL_CHECK(false == Parser(MakeSpan("A")).ParseUnsigned(&u));
449
450
0
  JXL_CHECK(false == Parser(MakeSpan("")).ParseSigned(&d));
451
0
  JXL_CHECK(false == Parser(MakeSpan("+")).ParseSigned(&d));
452
0
  JXL_CHECK(false == Parser(MakeSpan("-")).ParseSigned(&d));
453
0
  JXL_CHECK(false == Parser(MakeSpan("A")).ParseSigned(&d));
454
0
#endif
455
0
  JXL_CHECK(true == Parser(MakeSpan("1")).ParseUnsigned(&u));
456
0
  JXL_CHECK(u == 1);
457
458
0
  JXL_CHECK(true == Parser(MakeSpan("32")).ParseUnsigned(&u));
459
0
  JXL_CHECK(u == 32);
460
461
0
  JXL_CHECK(true == Parser(MakeSpan("1")).ParseSigned(&d));
462
0
  JXL_CHECK(d == 1.0);
463
0
  JXL_CHECK(true == Parser(MakeSpan("+2")).ParseSigned(&d));
464
0
  JXL_CHECK(d == 2.0);
465
0
  JXL_CHECK(true == Parser(MakeSpan("-3")).ParseSigned(&d));
466
0
  JXL_CHECK(std::abs(d - -3.0) < 1E-15);
467
0
  JXL_CHECK(true == Parser(MakeSpan("3.141592")).ParseSigned(&d));
468
0
  JXL_CHECK(std::abs(d - 3.141592) < 1E-15);
469
0
  JXL_CHECK(true == Parser(MakeSpan("-3.141592")).ParseSigned(&d));
470
0
  JXL_CHECK(std::abs(d - -3.141592) < 1E-15);
471
0
}
472
473
}  // namespace extras
474
}  // namespace jxl