1 // Copyright (c) the JPEG XL Project Authors. All rights reserved.
3 // Use of this source code is governed by a BSD-style
4 // license that can be found in the LICENSE file.
6 #include "lib/extras/dec/pgx.h"
10 #include "lib/extras/size_constraints.h"
11 #include "lib/jxl/base/bits.h"
12 #include "lib/jxl/base/compiler_specific.h"
19 // NOTE: PGX is always grayscale
22 size_t bits_per_sample;
29 explicit Parser(const Span<const uint8_t> bytes)
30 : pos_(bytes.data()), end_(pos_ + bytes.size()) {}
32 // Sets "pos" to the first non-header byte/pixel on success.
33 Status ParseHeader(HeaderPGX* header, const uint8_t** pos) {
34 // codec.cc ensures we have at least two bytes => no range check here.
35 if (pos_[0] != 'P' || pos_[1] != 'G') return false;
37 return ParseHeaderPGX(header, pos);
40 // Exposed for testing
41 Status ParseUnsigned(size_t* number) {
42 if (pos_ == end_) return JXL_FAILURE("PGX: reached end before number");
43 if (!IsDigit(*pos_)) return JXL_FAILURE("PGX: expected unsigned number");
46 while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
48 *number += *pos_ - '0';
56 static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; }
57 static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; }
58 static bool IsWhitespace(const uint8_t c) {
59 return IsLineBreak(c) || c == '\t' || c == ' ';
63 if (pos_ == end_) return JXL_FAILURE("PGX: reached end before space");
64 const uint8_t c = *pos_;
65 if (c != ' ') return JXL_FAILURE("PGX: expected space");
70 Status SkipLineBreak() {
71 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).
76 } else if (*pos_ == '\r' && pos_ + 1 != end_ && *(pos_ + 1) == '\n') {
80 return JXL_FAILURE("PGX: expected line break");
83 Status SkipSingleWhitespace() {
84 if (pos_ == end_) return JXL_FAILURE("PGX: reached end before whitespace");
85 if (!IsWhitespace(*pos_)) return JXL_FAILURE("PGX: expected whitespace");
90 Status ParseHeaderPGX(HeaderPGX* header, const uint8_t** pos) {
91 JXL_RETURN_IF_ERROR(SkipSpace());
92 if (pos_ + 2 > end_) return JXL_FAILURE("PGX: header too small");
93 if (*pos_ == 'M' && *(pos_ + 1) == 'L') {
94 header->big_endian = true;
95 } else if (*pos_ == 'L' && *(pos_ + 1) == 'M') {
96 header->big_endian = false;
98 return JXL_FAILURE("PGX: invalid endianness");
101 JXL_RETURN_IF_ERROR(SkipSpace());
102 if (pos_ == end_) return JXL_FAILURE("PGX: header too small");
104 header->is_signed = false;
105 } else if (*pos_ == '-') {
106 header->is_signed = true;
108 return JXL_FAILURE("PGX: invalid signedness");
111 // Skip optional space
112 if (pos_ < end_ && *pos_ == ' ') pos_++;
113 JXL_RETURN_IF_ERROR(ParseUnsigned(&header->bits_per_sample));
114 JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
115 JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
116 JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
117 JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
119 JXL_RETURN_IF_ERROR(SkipLineBreak());
121 // TODO(jon): could do up to 24-bit by converting the values to
123 if (header->bits_per_sample > 16) {
124 return JXL_FAILURE("PGX: >16 bits not yet supported");
126 // TODO(lode): support signed integers. This may require changing the way
127 // external_image works.
128 if (header->is_signed) {
129 return JXL_FAILURE("PGX: signed not yet supported");
132 size_t numpixels = header->xsize * header->ysize;
133 size_t bytes_per_pixel = header->bits_per_sample <= 8 ? 1 : 2;
134 if (pos_ + numpixels * bytes_per_pixel > end_) {
135 return JXL_FAILURE("PGX: data too small");
143 const uint8_t* const end_;
148 Status DecodeImagePGX(const Span<const uint8_t> bytes,
149 const ColorHints& color_hints, PackedPixelFile* ppf,
150 const SizeConstraints* constraints) {
151 Parser parser(bytes);
152 HeaderPGX header = {};
154 if (!parser.ParseHeader(&header, &pos)) return false;
156 VerifyDimensions(constraints, header.xsize, header.ysize));
157 if (header.bits_per_sample == 0 || header.bits_per_sample > 32) {
158 return JXL_FAILURE("PGX: bits_per_sample invalid");
161 JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
162 /*is_gray=*/true, ppf));
163 ppf->info.xsize = header.xsize;
164 ppf->info.ysize = header.ysize;
165 // Original data is uint, so exponent_bits_per_sample = 0.
166 ppf->info.bits_per_sample = header.bits_per_sample;
167 ppf->info.exponent_bits_per_sample = 0;
168 ppf->info.uses_original_profile = true;
171 ppf->info.alpha_bits = 0;
172 ppf->info.alpha_exponent_bits = 0;
173 ppf->info.num_color_channels = 1; // Always grayscale
174 ppf->info.orientation = JXL_ORIENT_IDENTITY;
176 JxlDataType data_type;
177 if (header.bits_per_sample > 8) {
178 data_type = JXL_TYPE_UINT16;
180 data_type = JXL_TYPE_UINT8;
183 const JxlPixelFormat format{
185 /*data_type=*/data_type,
186 /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN,
190 // Allocates the frame buffer.
191 ppf->frames.emplace_back(header.xsize, header.ysize, format);
192 const auto& frame = ppf->frames.back();
193 size_t pgx_remaining_size = bytes.data() + bytes.size() - pos;
194 if (pgx_remaining_size < frame.color.pixels_size) {
195 return JXL_FAILURE("PGX file too small");
197 memcpy(frame.color.pixels(), pos, frame.color.pixels_size);
201 } // namespace extras