Coverage Report

Created: 2023-08-28 07:24

/src/libjxl/lib/extras/dec/pgx.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/pgx.h"
7
8
#include <string.h>
9
10
#include "lib/extras/size_constraints.h"
11
#include "lib/jxl/base/bits.h"
12
#include "lib/jxl/base/compiler_specific.h"
13
14
namespace jxl {
15
namespace extras {
16
namespace {
17
18
struct HeaderPGX {
19
  // NOTE: PGX is always grayscale
20
  size_t xsize;
21
  size_t ysize;
22
  size_t bits_per_sample;
23
  bool big_endian;
24
  bool is_signed;
25
};
26
27
class Parser {
28
 public:
29
  explicit Parser(const Span<const uint8_t> bytes)
30
0
      : pos_(bytes.data()), end_(pos_ + bytes.size()) {}
31
32
  // Sets "pos" to the first non-header byte/pixel on success.
33
0
  Status ParseHeader(HeaderPGX* header, const uint8_t** pos) {
34
    // codec.cc ensures we have at least two bytes => no range check here.
35
0
    if (pos_[0] != 'P' || pos_[1] != 'G') return false;
36
0
    pos_ += 2;
37
0
    return ParseHeaderPGX(header, pos);
38
0
  }
39
40
  // Exposed for testing
41
0
  Status ParseUnsigned(size_t* number) {
42
0
    if (pos_ == end_) return JXL_FAILURE("PGX: reached end before number");
43
0
    if (!IsDigit(*pos_)) return JXL_FAILURE("PGX: expected unsigned number");
44
45
0
    *number = 0;
46
0
    while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
47
0
      *number *= 10;
48
0
      *number += *pos_ - '0';
49
0
      ++pos_;
50
0
    }
51
52
0
    return true;
53
0
  }
54
55
 private:
56
0
  static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; }
57
0
  static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; }
58
0
  static bool IsWhitespace(const uint8_t c) {
59
0
    return IsLineBreak(c) || c == '\t' || c == ' ';
60
0
  }
61
62
0
  Status SkipSpace() {
63
0
    if (pos_ == end_) return JXL_FAILURE("PGX: reached end before space");
64
0
    const uint8_t c = *pos_;
65
0
    if (c != ' ') return JXL_FAILURE("PGX: expected space");
66
0
    ++pos_;
67
0
    return true;
68
0
  }
69
70
0
  Status SkipLineBreak() {
71
0
    if (pos_ == end_) return JXL_FAILURE("PGX: reached end before line break");
72
    // Line break can be either "\n" (0a) or "\r\n" (0d 0a).
73
0
    if (*pos_ == '\n') {
74
0
      pos_++;
75
0
      return true;
76
0
    } else if (*pos_ == '\r' && pos_ + 1 != end_ && *(pos_ + 1) == '\n') {
77
0
      pos_ += 2;
78
0
      return true;
79
0
    }
80
0
    return JXL_FAILURE("PGX: expected line break");
81
0
  }
82
83
0
  Status SkipSingleWhitespace() {
84
0
    if (pos_ == end_) return JXL_FAILURE("PGX: reached end before whitespace");
85
0
    if (!IsWhitespace(*pos_)) return JXL_FAILURE("PGX: expected whitespace");
86
0
    ++pos_;
87
0
    return true;
88
0
  }
89
90
0
  Status ParseHeaderPGX(HeaderPGX* header, const uint8_t** pos) {
91
0
    JXL_RETURN_IF_ERROR(SkipSpace());
92
0
    if (pos_ + 2 > end_) return JXL_FAILURE("PGX: header too small");
93
0
    if (*pos_ == 'M' && *(pos_ + 1) == 'L') {
94
0
      header->big_endian = true;
95
0
    } else if (*pos_ == 'L' && *(pos_ + 1) == 'M') {
96
0
      header->big_endian = false;
97
0
    } else {
98
0
      return JXL_FAILURE("PGX: invalid endianness");
99
0
    }
100
0
    pos_ += 2;
101
0
    JXL_RETURN_IF_ERROR(SkipSpace());
102
0
    if (pos_ == end_) return JXL_FAILURE("PGX: header too small");
103
0
    if (*pos_ == '+') {
104
0
      header->is_signed = false;
105
0
    } else if (*pos_ == '-') {
106
0
      header->is_signed = true;
107
0
    } else {
108
0
      return JXL_FAILURE("PGX: invalid signedness");
109
0
    }
110
0
    pos_++;
111
    // Skip optional space
112
0
    if (pos_ < end_ && *pos_ == ' ') pos_++;
113
0
    JXL_RETURN_IF_ERROR(ParseUnsigned(&header->bits_per_sample));
114
0
    JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
115
0
    JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
116
0
    JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
117
0
    JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
118
    // 0xa, or 0xd 0xa.
119
0
    JXL_RETURN_IF_ERROR(SkipLineBreak());
120
121
    // TODO(jon): could do up to 24-bit by converting the values to
122
    // JXL_TYPE_FLOAT.
123
0
    if (header->bits_per_sample > 16) {
124
0
      return JXL_FAILURE("PGX: >16 bits not yet supported");
125
0
    }
126
    // TODO(lode): support signed integers. This may require changing the way
127
    // external_image works.
128
0
    if (header->is_signed) {
129
0
      return JXL_FAILURE("PGX: signed not yet supported");
130
0
    }
131
132
0
    size_t numpixels = header->xsize * header->ysize;
133
0
    size_t bytes_per_pixel = header->bits_per_sample <= 8 ? 1 : 2;
134
0
    if (pos_ + numpixels * bytes_per_pixel > end_) {
135
0
      return JXL_FAILURE("PGX: data too small");
136
0
    }
137
138
0
    *pos = pos_;
139
0
    return true;
140
0
  }
141
142
  const uint8_t* pos_;
143
  const uint8_t* const end_;
144
};
145
146
}  // namespace
147
148
Status DecodeImagePGX(const Span<const uint8_t> bytes,
149
                      const ColorHints& color_hints, PackedPixelFile* ppf,
150
0
                      const SizeConstraints* constraints) {
151
0
  Parser parser(bytes);
152
0
  HeaderPGX header = {};
153
0
  const uint8_t* pos;
154
0
  if (!parser.ParseHeader(&header, &pos)) return false;
155
0
  JXL_RETURN_IF_ERROR(
156
0
      VerifyDimensions(constraints, header.xsize, header.ysize));
157
0
  if (header.bits_per_sample == 0 || header.bits_per_sample > 32) {
158
0
    return JXL_FAILURE("PGX: bits_per_sample invalid");
159
0
  }
160
161
0
  JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
162
0
                                      /*is_gray=*/true, ppf));
163
0
  ppf->info.xsize = header.xsize;
164
0
  ppf->info.ysize = header.ysize;
165
  // Original data is uint, so exponent_bits_per_sample = 0.
166
0
  ppf->info.bits_per_sample = header.bits_per_sample;
167
0
  ppf->info.exponent_bits_per_sample = 0;
168
0
  ppf->info.uses_original_profile = true;
169
170
  // No alpha in PGX
171
0
  ppf->info.alpha_bits = 0;
172
0
  ppf->info.alpha_exponent_bits = 0;
173
0
  ppf->info.num_color_channels = 1;  // Always grayscale
174
0
  ppf->info.orientation = JXL_ORIENT_IDENTITY;
175
176
0
  JxlDataType data_type;
177
0
  if (header.bits_per_sample > 8) {
178
0
    data_type = JXL_TYPE_UINT16;
179
0
  } else {
180
0
    data_type = JXL_TYPE_UINT8;
181
0
  }
182
183
0
  const JxlPixelFormat format{
184
0
      /*num_channels=*/1,
185
0
      /*data_type=*/data_type,
186
0
      /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN,
187
0
      /*align=*/0,
188
0
  };
189
0
  ppf->frames.clear();
190
  // Allocates the frame buffer.
191
0
  ppf->frames.emplace_back(header.xsize, header.ysize, format);
192
0
  const auto& frame = ppf->frames.back();
193
0
  size_t pgx_remaining_size = bytes.data() + bytes.size() - pos;
194
0
  if (pgx_remaining_size < frame.color.pixels_size) {
195
0
    return JXL_FAILURE("PGX file too small");
196
0
  }
197
0
  memcpy(frame.color.pixels(), pos, frame.color.pixels_size);
198
0
  return true;
199
0
}
200
201
}  // namespace extras
202
}  // namespace jxl