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