pnm.cc (19021B)
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/encode.h> 9 10 #include <cmath> 11 #include <cstddef> 12 #include <cstdint> 13 #include <cstdlib> 14 #include <cstring> 15 16 #include "lib/extras/size_constraints.h" 17 #include "lib/jxl/base/bits.h" 18 #include "lib/jxl/base/c_callback_support.h" 19 #include "lib/jxl/base/span.h" 20 #include "lib/jxl/base/status.h" 21 22 namespace jxl { 23 namespace extras { 24 namespace { 25 26 class Parser { 27 public: 28 explicit Parser(const Span<const uint8_t> bytes) 29 : pos_(bytes.data()), end_(pos_ + bytes.size()) {} 30 31 // Sets "pos" to the first non-header byte/pixel on success. 32 Status ParseHeader(HeaderPNM* header, const uint8_t** pos) { 33 // codec.cc ensures we have at least two bytes => no range check here. 34 if (pos_[0] != 'P') return false; 35 const uint8_t type = pos_[1]; 36 pos_ += 2; 37 38 switch (type) { 39 case '4': 40 return JXL_FAILURE("pbm not supported"); 41 42 case '5': 43 header->is_gray = true; 44 return ParseHeaderPNM(header, pos); 45 46 case '6': 47 header->is_gray = false; 48 return ParseHeaderPNM(header, pos); 49 50 case '7': 51 return ParseHeaderPAM(header, pos); 52 53 case 'F': 54 header->is_gray = false; 55 return ParseHeaderPFM(header, pos); 56 57 case 'f': 58 header->is_gray = true; 59 return ParseHeaderPFM(header, pos); 60 61 default: 62 return false; 63 } 64 } 65 66 // Exposed for testing 67 Status ParseUnsigned(size_t* number) { 68 if (pos_ == end_) return JXL_FAILURE("PNM: reached end before number"); 69 if (!IsDigit(*pos_)) return JXL_FAILURE("PNM: expected unsigned number"); 70 71 *number = 0; 72 while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { 73 *number *= 10; 74 *number += *pos_ - '0'; 75 ++pos_; 76 } 77 78 return true; 79 } 80 81 Status ParseSigned(double* number) { 82 if (pos_ == end_) return JXL_FAILURE("PNM: reached end before signed"); 83 84 if (*pos_ != '-' && *pos_ != '+' && !IsDigit(*pos_)) { 85 return JXL_FAILURE("PNM: expected signed number"); 86 } 87 88 // Skip sign 89 const bool is_neg = *pos_ == '-'; 90 if (is_neg || *pos_ == '+') { 91 ++pos_; 92 if (pos_ == end_) return JXL_FAILURE("PNM: reached end before digits"); 93 } 94 95 // Leading digits 96 *number = 0.0; 97 while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { 98 *number *= 10; 99 *number += *pos_ - '0'; 100 ++pos_; 101 } 102 103 // Decimal places? 104 if (pos_ < end_ && *pos_ == '.') { 105 ++pos_; 106 double place = 0.1; 107 while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { 108 *number += (*pos_ - '0') * place; 109 place *= 0.1; 110 ++pos_; 111 } 112 } 113 114 if (is_neg) *number = -*number; 115 return true; 116 } 117 118 private: 119 static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; } 120 static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; } 121 static bool IsWhitespace(const uint8_t c) { 122 return IsLineBreak(c) || c == '\t' || c == ' '; 123 } 124 125 Status SkipBlank() { 126 if (pos_ == end_) return JXL_FAILURE("PNM: reached end before blank"); 127 const uint8_t c = *pos_; 128 if (c != ' ' && c != '\n') return JXL_FAILURE("PNM: expected blank"); 129 ++pos_; 130 return true; 131 } 132 133 Status SkipSingleWhitespace() { 134 if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace"); 135 if (!IsWhitespace(*pos_)) return JXL_FAILURE("PNM: expected whitespace"); 136 ++pos_; 137 return true; 138 } 139 140 Status SkipWhitespace() { 141 if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace"); 142 if (!IsWhitespace(*pos_) && *pos_ != '#') { 143 return JXL_FAILURE("PNM: expected whitespace/comment"); 144 } 145 146 while (pos_ < end_ && IsWhitespace(*pos_)) { 147 ++pos_; 148 } 149 150 // Comment(s) 151 while (pos_ != end_ && *pos_ == '#') { 152 while (pos_ != end_ && !IsLineBreak(*pos_)) { 153 ++pos_; 154 } 155 // Newline(s) 156 while (pos_ != end_ && IsLineBreak(*pos_)) pos_++; 157 } 158 159 while (pos_ < end_ && IsWhitespace(*pos_)) { 160 ++pos_; 161 } 162 return true; 163 } 164 165 Status MatchString(const char* keyword, bool skipws = true) { 166 const uint8_t* ppos = pos_; 167 const uint8_t* kw = reinterpret_cast<const uint8_t*>(keyword); 168 while (*kw) { 169 if (ppos >= end_) return JXL_FAILURE("PAM: unexpected end of input"); 170 if (*kw != *ppos) return false; 171 ppos++; 172 kw++; 173 } 174 pos_ = ppos; 175 if (skipws) { 176 JXL_RETURN_IF_ERROR(SkipWhitespace()); 177 } else { 178 JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); 179 } 180 return true; 181 } 182 183 Status ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) { 184 size_t depth = 3; 185 size_t max_val = 255; 186 JXL_RETURN_IF_ERROR(SkipWhitespace()); 187 while (!MatchString("ENDHDR", /*skipws=*/false)) { 188 if (MatchString("WIDTH")) { 189 JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); 190 JXL_RETURN_IF_ERROR(SkipWhitespace()); 191 } else if (MatchString("HEIGHT")) { 192 JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); 193 JXL_RETURN_IF_ERROR(SkipWhitespace()); 194 } else if (MatchString("DEPTH")) { 195 JXL_RETURN_IF_ERROR(ParseUnsigned(&depth)); 196 JXL_RETURN_IF_ERROR(SkipWhitespace()); 197 } else if (MatchString("MAXVAL")) { 198 JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val)); 199 JXL_RETURN_IF_ERROR(SkipWhitespace()); 200 } else if (MatchString("TUPLTYPE")) { 201 if (MatchString("RGB_ALPHA")) { 202 header->has_alpha = true; 203 } else if (MatchString("RGB")) { 204 } else if (MatchString("GRAYSCALE_ALPHA")) { 205 header->has_alpha = true; 206 header->is_gray = true; 207 } else if (MatchString("GRAYSCALE")) { 208 header->is_gray = true; 209 } else if (MatchString("BLACKANDWHITE_ALPHA")) { 210 header->has_alpha = true; 211 header->is_gray = true; 212 max_val = 1; 213 } else if (MatchString("BLACKANDWHITE")) { 214 header->is_gray = true; 215 max_val = 1; 216 } else if (MatchString("Alpha")) { 217 header->ec_types.push_back(JXL_CHANNEL_ALPHA); 218 } else if (MatchString("Depth")) { 219 header->ec_types.push_back(JXL_CHANNEL_DEPTH); 220 } else if (MatchString("SpotColor")) { 221 header->ec_types.push_back(JXL_CHANNEL_SPOT_COLOR); 222 } else if (MatchString("SelectionMask")) { 223 header->ec_types.push_back(JXL_CHANNEL_SELECTION_MASK); 224 } else if (MatchString("Black")) { 225 header->ec_types.push_back(JXL_CHANNEL_BLACK); 226 } else if (MatchString("CFA")) { 227 header->ec_types.push_back(JXL_CHANNEL_CFA); 228 } else if (MatchString("Thermal")) { 229 header->ec_types.push_back(JXL_CHANNEL_THERMAL); 230 } else if (MatchString("Unknown")) { 231 header->ec_types.push_back(JXL_CHANNEL_UNKNOWN); 232 } else if (MatchString("Optional")) { 233 header->ec_types.push_back(JXL_CHANNEL_OPTIONAL); 234 } else { 235 return JXL_FAILURE("PAM: unknown TUPLTYPE"); 236 } 237 } else { 238 constexpr size_t kMaxHeaderLength = 20; 239 char unknown_header[kMaxHeaderLength + 1]; 240 size_t len = std::min<size_t>(kMaxHeaderLength, end_ - pos_); 241 strncpy(unknown_header, reinterpret_cast<const char*>(pos_), len); 242 unknown_header[len] = 0; 243 return JXL_FAILURE("PAM: unknown header keyword: %s", unknown_header); 244 } 245 } 246 size_t num_channels = header->is_gray ? 1 : 3; 247 if (header->has_alpha) num_channels++; 248 if (num_channels + header->ec_types.size() != depth) { 249 return JXL_FAILURE("PAM: bad DEPTH"); 250 } 251 if (max_val == 0 || max_val >= 65536) { 252 return JXL_FAILURE("PAM: bad MAXVAL"); 253 } 254 // e.g. When `max_val` is 1 , we want 1 bit: 255 header->bits_per_sample = FloorLog2Nonzero(max_val) + 1; 256 if ((1u << header->bits_per_sample) - 1 != max_val) 257 return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)"); 258 // PAM does not pack bits as in PBM. 259 260 header->floating_point = false; 261 header->big_endian = true; 262 *pos = pos_; 263 return true; 264 } 265 266 Status ParseHeaderPNM(HeaderPNM* header, const uint8_t** pos) { 267 JXL_RETURN_IF_ERROR(SkipWhitespace()); 268 JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); 269 270 JXL_RETURN_IF_ERROR(SkipWhitespace()); 271 JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); 272 273 JXL_RETURN_IF_ERROR(SkipWhitespace()); 274 size_t max_val; 275 JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val)); 276 if (max_val == 0 || max_val >= 65536) { 277 return JXL_FAILURE("PNM: bad MaxVal"); 278 } 279 header->bits_per_sample = FloorLog2Nonzero(max_val) + 1; 280 if ((1u << header->bits_per_sample) - 1 != max_val) 281 return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)"); 282 header->floating_point = false; 283 header->big_endian = true; 284 285 JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); 286 287 *pos = pos_; 288 return true; 289 } 290 291 Status ParseHeaderPFM(HeaderPNM* header, const uint8_t** pos) { 292 JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); 293 JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); 294 295 JXL_RETURN_IF_ERROR(SkipBlank()); 296 JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); 297 298 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 double scale; 302 JXL_RETURN_IF_ERROR(ParseSigned(&scale)); 303 if (scale == 0.0) { 304 return JXL_FAILURE("PFM: bad scale factor value."); 305 } else if (std::abs(scale) != 1.0) { 306 JXL_WARNING("PFM: Discarding non-unit scale factor"); 307 } 308 header->big_endian = scale > 0.0; 309 header->bits_per_sample = 32; 310 header->floating_point = true; 311 312 JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); 313 314 *pos = pos_; 315 return true; 316 } 317 318 const uint8_t* pos_; 319 const uint8_t* const end_; 320 }; 321 322 } // namespace 323 324 struct PNMChunkedInputFrame { 325 JxlChunkedFrameInputSource operator()() { 326 return JxlChunkedFrameInputSource{ 327 this, 328 METHOD_TO_C_CALLBACK( 329 &PNMChunkedInputFrame::GetColorChannelsPixelFormat), 330 METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetColorChannelDataAt), 331 METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetExtraChannelPixelFormat), 332 METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetExtraChannelDataAt), 333 METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::ReleaseCurrentData)}; 334 } 335 336 void /* NOLINT */ GetColorChannelsPixelFormat(JxlPixelFormat* pixel_format) { 337 *pixel_format = format; 338 } 339 340 const void* GetColorChannelDataAt(size_t xpos, size_t ypos, size_t xsize, 341 size_t ysize, size_t* row_offset) { 342 const size_t bytes_per_channel = 343 DivCeil(dec->header_.bits_per_sample, jxl::kBitsPerByte); 344 const size_t num_channels = dec->header_.is_gray ? 1 : 3; 345 const size_t bytes_per_pixel = num_channels * bytes_per_channel; 346 *row_offset = dec->header_.xsize * bytes_per_pixel; 347 const size_t offset = ypos * *row_offset + xpos * bytes_per_pixel; 348 return dec->pnm_.data() + offset + dec->data_start_; 349 } 350 351 void GetExtraChannelPixelFormat(size_t ec_index, 352 JxlPixelFormat* pixel_format) { 353 (void)this; 354 *pixel_format = {}; 355 JXL_DEBUG_ABORT("Not implemented"); 356 } 357 358 const void* GetExtraChannelDataAt(size_t ec_index, size_t xpos, size_t ypos, 359 size_t xsize, size_t ysize, 360 size_t* row_offset) { 361 (void)this; 362 *row_offset = 0; 363 JXL_DEBUG_ABORT("Not implemented"); 364 return nullptr; 365 } 366 367 void ReleaseCurrentData(const void* buffer) {} 368 369 JxlPixelFormat format; 370 const ChunkedPNMDecoder* dec; 371 }; 372 373 StatusOr<ChunkedPNMDecoder> ChunkedPNMDecoder::Init(const char* path) { 374 ChunkedPNMDecoder dec; 375 JXL_ASSIGN_OR_RETURN(dec.pnm_, MemoryMappedFile::Init(path)); 376 size_t size = dec.pnm_.size(); 377 if (size < 2) return JXL_FAILURE("Invalid ppm"); 378 size_t hdr_buf = std::min<size_t>(size, 10 * 1024); 379 Span<const uint8_t> span(dec.pnm_.data(), hdr_buf); 380 Parser parser(span); 381 HeaderPNM& header = dec.header_; 382 const uint8_t* pos = nullptr; 383 if (!parser.ParseHeader(&header, &pos)) { 384 return StatusCode::kGenericError; 385 } 386 dec.data_start_ = pos - span.data(); 387 388 if (header.bits_per_sample == 0 || header.bits_per_sample > 16) { 389 return JXL_FAILURE("Invalid bits_per_sample"); 390 } 391 if (header.has_alpha || !header.ec_types.empty() || header.floating_point) { 392 return JXL_FAILURE("Only PGM and PPM inputs are supported"); 393 } 394 395 const size_t bytes_per_channel = 396 DivCeil(dec.header_.bits_per_sample, jxl::kBitsPerByte); 397 const size_t num_channels = dec.header_.is_gray ? 1 : 3; 398 const size_t bytes_per_pixel = num_channels * bytes_per_channel; 399 size_t row_size = dec.header_.xsize * bytes_per_pixel; 400 if (size < header.ysize * row_size + dec.data_start_) { 401 return JXL_FAILURE("PNM file too small"); 402 } 403 return dec; 404 } 405 406 jxl::Status ChunkedPNMDecoder::InitializePPF(const ColorHints& color_hints, 407 PackedPixelFile* ppf) { 408 // PPM specifies that in the raster, the sample values are "nonlinear" 409 // (BP.709, with gamma number of 2.2). Deviate from the specification and 410 // assume `sRGB` in our implementation. 411 JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, 412 header_.is_gray, ppf)); 413 414 ppf->info.xsize = header_.xsize; 415 ppf->info.ysize = header_.ysize; 416 ppf->info.bits_per_sample = header_.bits_per_sample; 417 ppf->info.exponent_bits_per_sample = 0; 418 ppf->info.orientation = JXL_ORIENT_IDENTITY; 419 ppf->info.alpha_bits = 0; 420 ppf->info.alpha_exponent_bits = 0; 421 ppf->info.num_color_channels = (header_.is_gray ? 1 : 3); 422 ppf->info.num_extra_channels = 0; 423 424 const JxlDataType data_type = 425 header_.bits_per_sample > 8 ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8; 426 const JxlPixelFormat format{ 427 /*num_channels=*/ppf->info.num_color_channels, 428 /*data_type=*/data_type, 429 /*endianness=*/header_.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN, 430 /*align=*/0, 431 }; 432 433 PNMChunkedInputFrame frame; 434 frame.format = format; 435 frame.dec = this; 436 ppf->chunked_frames.emplace_back(header_.xsize, header_.ysize, frame); 437 return true; 438 } 439 440 Status DecodeImagePNM(const Span<const uint8_t> bytes, 441 const ColorHints& color_hints, PackedPixelFile* ppf, 442 const SizeConstraints* constraints) { 443 Parser parser(bytes); 444 HeaderPNM header = {}; 445 const uint8_t* pos = nullptr; 446 if (!parser.ParseHeader(&header, &pos)) return false; 447 JXL_RETURN_IF_ERROR( 448 VerifyDimensions(constraints, header.xsize, header.ysize)); 449 450 if (header.bits_per_sample == 0 || header.bits_per_sample > 32) { 451 return JXL_FAILURE("PNM: bits_per_sample invalid"); 452 } 453 454 // PPM specifies that in the raster, the sample values are "nonlinear" 455 // (BP.709, with gamma number of 2.2). Deviate from the specification and 456 // assume `sRGB` in our implementation. 457 JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, 458 header.is_gray, ppf)); 459 460 ppf->info.xsize = header.xsize; 461 ppf->info.ysize = header.ysize; 462 if (header.floating_point) { 463 ppf->info.bits_per_sample = 32; 464 ppf->info.exponent_bits_per_sample = 8; 465 } else { 466 ppf->info.bits_per_sample = header.bits_per_sample; 467 ppf->info.exponent_bits_per_sample = 0; 468 } 469 470 ppf->info.orientation = JXL_ORIENT_IDENTITY; 471 472 // No alpha in PNM and PFM 473 ppf->info.alpha_bits = (header.has_alpha ? ppf->info.bits_per_sample : 0); 474 ppf->info.alpha_exponent_bits = 0; 475 ppf->info.num_color_channels = (header.is_gray ? 1 : 3); 476 uint32_t num_alpha_channels = (header.has_alpha ? 1 : 0); 477 uint32_t num_interleaved_channels = 478 ppf->info.num_color_channels + num_alpha_channels; 479 ppf->info.num_extra_channels = num_alpha_channels + header.ec_types.size(); 480 481 for (auto type : header.ec_types) { 482 PackedExtraChannel pec = {}; 483 pec.ec_info.bits_per_sample = ppf->info.bits_per_sample; 484 pec.ec_info.type = type; 485 ppf->extra_channels_info.emplace_back(std::move(pec)); 486 } 487 488 JxlDataType data_type; 489 if (header.floating_point) { 490 // There's no float16 pnm version. 491 data_type = JXL_TYPE_FLOAT; 492 } else { 493 if (header.bits_per_sample > 8) { 494 data_type = JXL_TYPE_UINT16; 495 } else { 496 data_type = JXL_TYPE_UINT8; 497 } 498 } 499 500 const JxlPixelFormat format{ 501 /*num_channels=*/num_interleaved_channels, 502 /*data_type=*/data_type, 503 /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN, 504 /*align=*/0, 505 }; 506 const JxlPixelFormat ec_format{1, format.data_type, format.endianness, 0}; 507 ppf->frames.clear(); 508 { 509 JXL_ASSIGN_OR_RETURN( 510 PackedFrame frame, 511 PackedFrame::Create(header.xsize, header.ysize, format)); 512 ppf->frames.emplace_back(std::move(frame)); 513 } 514 auto* frame = &ppf->frames.back(); 515 for (size_t i = 0; i < header.ec_types.size(); ++i) { 516 JXL_ASSIGN_OR_RETURN( 517 PackedImage ec, 518 PackedImage::Create(header.xsize, header.ysize, ec_format)); 519 frame->extra_channels.emplace_back(std::move(ec)); 520 } 521 size_t pnm_remaining_size = bytes.data() + bytes.size() - pos; 522 if (pnm_remaining_size < frame->color.pixels_size) { 523 return JXL_FAILURE("PNM file too small"); 524 } 525 526 uint8_t* out = reinterpret_cast<uint8_t*>(frame->color.pixels()); 527 std::vector<uint8_t*> ec_out(header.ec_types.size()); 528 for (size_t i = 0; i < ec_out.size(); ++i) { 529 ec_out[i] = reinterpret_cast<uint8_t*>(frame->extra_channels[i].pixels()); 530 } 531 if (ec_out.empty()) { 532 const bool flipped_y = header.bits_per_sample == 32; // PFMs are flipped 533 for (size_t y = 0; y < header.ysize; ++y) { 534 size_t y_in = flipped_y ? header.ysize - 1 - y : y; 535 const uint8_t* row_in = &pos[y_in * frame->color.stride]; 536 uint8_t* row_out = &out[y * frame->color.stride]; 537 memcpy(row_out, row_in, frame->color.stride); 538 } 539 } else { 540 JXL_RETURN_IF_ERROR(PackedImage::ValidateDataType(data_type)); 541 size_t pwidth = PackedImage::BitsPerChannel(data_type) / 8; 542 for (size_t y = 0; y < header.ysize; ++y) { 543 for (size_t x = 0; x < header.xsize; ++x) { 544 memcpy(out, pos, frame->color.pixel_stride()); 545 out += frame->color.pixel_stride(); 546 pos += frame->color.pixel_stride(); 547 for (auto& p : ec_out) { 548 memcpy(p, pos, pwidth); 549 pos += pwidth; 550 p += pwidth; 551 } 552 } 553 } 554 } 555 if (ppf->info.exponent_bits_per_sample == 0) { 556 ppf->input_bitdepth.type = JXL_BIT_DEPTH_FROM_CODESTREAM; 557 } 558 return true; 559 } 560 561 // Exposed for testing. 562 Status PnmParseSigned(Bytes str, double* v) { 563 return Parser(str).ParseSigned(v); 564 } 565 566 Status PnmParseUnsigned(Bytes str, size_t* v) { 567 return Parser(str).ParseUnsigned(v); 568 } 569 570 } // namespace extras 571 } // namespace jxl