// Copyright (c) the JPEG XL Project Authors. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. #include "lib/extras/dec/pnm.h" #include #include #include #include #include #include "jxl/encode.h" #include "lib/extras/size_constraints.h" #include "lib/jxl/base/bits.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/status.h" namespace jxl { namespace extras { namespace { class Parser { public: explicit Parser(const Span bytes) : pos_(bytes.data()), end_(pos_ + bytes.size()) {} // Sets "pos" to the first non-header byte/pixel on success. Status ParseHeader(HeaderPNM* header, const uint8_t** pos) { // codec.cc ensures we have at least two bytes => no range check here. if (pos_[0] != 'P') return false; const uint8_t type = pos_[1]; pos_ += 2; switch (type) { case '4': return JXL_FAILURE("pbm not supported"); case '5': header->is_gray = true; return ParseHeaderPNM(header, pos); case '6': header->is_gray = false; return ParseHeaderPNM(header, pos); case '7': return ParseHeaderPAM(header, pos); case 'F': header->is_gray = false; return ParseHeaderPFM(header, pos); case 'f': header->is_gray = true; return ParseHeaderPFM(header, pos); default: return false; } } // Exposed for testing Status ParseUnsigned(size_t* number) { if (pos_ == end_) return JXL_FAILURE("PNM: reached end before number"); if (!IsDigit(*pos_)) return JXL_FAILURE("PNM: expected unsigned number"); *number = 0; while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { *number *= 10; *number += *pos_ - '0'; ++pos_; } return true; } Status ParseSigned(double* number) { if (pos_ == end_) return JXL_FAILURE("PNM: reached end before signed"); if (*pos_ != '-' && *pos_ != '+' && !IsDigit(*pos_)) { return JXL_FAILURE("PNM: expected signed number"); } // Skip sign const bool is_neg = *pos_ == '-'; if (is_neg || *pos_ == '+') { ++pos_; if (pos_ == end_) return JXL_FAILURE("PNM: reached end before digits"); } // Leading digits *number = 0.0; while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { *number *= 10; *number += *pos_ - '0'; ++pos_; } // Decimal places? if (pos_ < end_ && *pos_ == '.') { ++pos_; double place = 0.1; while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { *number += (*pos_ - '0') * place; place *= 0.1; ++pos_; } } if (is_neg) *number = -*number; return true; } private: static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; } static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; } static bool IsWhitespace(const uint8_t c) { return IsLineBreak(c) || c == '\t' || c == ' '; } Status SkipBlank() { if (pos_ == end_) return JXL_FAILURE("PNM: reached end before blank"); const uint8_t c = *pos_; if (c != ' ' && c != '\n') return JXL_FAILURE("PNM: expected blank"); ++pos_; return true; } Status SkipSingleWhitespace() { if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace"); if (!IsWhitespace(*pos_)) return JXL_FAILURE("PNM: expected whitespace"); ++pos_; return true; } Status SkipWhitespace() { if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace"); if (!IsWhitespace(*pos_) && *pos_ != '#') { return JXL_FAILURE("PNM: expected whitespace/comment"); } while (pos_ < end_ && IsWhitespace(*pos_)) { ++pos_; } // Comment(s) while (pos_ != end_ && *pos_ == '#') { while (pos_ != end_ && !IsLineBreak(*pos_)) { ++pos_; } // Newline(s) while (pos_ != end_ && IsLineBreak(*pos_)) pos_++; } while (pos_ < end_ && IsWhitespace(*pos_)) { ++pos_; } return true; } Status MatchString(const char* keyword, bool skipws = true) { const uint8_t* ppos = pos_; const uint8_t* kw = reinterpret_cast(keyword); while (*kw) { if (ppos >= end_) return JXL_FAILURE("PAM: unexpected end of input"); if (*kw != *ppos) return false; ppos++; kw++; } pos_ = ppos; if (skipws) { JXL_RETURN_IF_ERROR(SkipWhitespace()); } else { JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); } return true; } Status ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) { size_t depth = 3; size_t max_val = 255; JXL_RETURN_IF_ERROR(SkipWhitespace()); while (!MatchString("ENDHDR", /*skipws=*/false)) { if (MatchString("WIDTH")) { JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); JXL_RETURN_IF_ERROR(SkipWhitespace()); } else if (MatchString("HEIGHT")) { JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); JXL_RETURN_IF_ERROR(SkipWhitespace()); } else if (MatchString("DEPTH")) { JXL_RETURN_IF_ERROR(ParseUnsigned(&depth)); JXL_RETURN_IF_ERROR(SkipWhitespace()); } else if (MatchString("MAXVAL")) { JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val)); JXL_RETURN_IF_ERROR(SkipWhitespace()); } else if (MatchString("TUPLTYPE")) { if (MatchString("RGB_ALPHA")) { header->has_alpha = true; } else if (MatchString("RGB")) { } else if (MatchString("GRAYSCALE_ALPHA")) { header->has_alpha = true; header->is_gray = true; } else if (MatchString("GRAYSCALE")) { header->is_gray = true; } else if (MatchString("BLACKANDWHITE_ALPHA")) { header->has_alpha = true; header->is_gray = true; max_val = 1; } else if (MatchString("BLACKANDWHITE")) { header->is_gray = true; max_val = 1; } else if (MatchString("Alpha")) { header->ec_types.push_back(JXL_CHANNEL_ALPHA); } else if (MatchString("Depth")) { header->ec_types.push_back(JXL_CHANNEL_DEPTH); } else if (MatchString("SpotColor")) { header->ec_types.push_back(JXL_CHANNEL_SPOT_COLOR); } else if (MatchString("SelectionMask")) { header->ec_types.push_back(JXL_CHANNEL_SELECTION_MASK); } else if (MatchString("Black")) { header->ec_types.push_back(JXL_CHANNEL_BLACK); } else if (MatchString("CFA")) { header->ec_types.push_back(JXL_CHANNEL_CFA); } else if (MatchString("Thermal")) { header->ec_types.push_back(JXL_CHANNEL_THERMAL); } else { return JXL_FAILURE("PAM: unknown TUPLTYPE"); } } else { constexpr size_t kMaxHeaderLength = 20; char unknown_header[kMaxHeaderLength + 1]; size_t len = std::min(kMaxHeaderLength, end_ - pos_); strncpy(unknown_header, reinterpret_cast(pos_), len); unknown_header[len] = 0; return JXL_FAILURE("PAM: unknown header keyword: %s", unknown_header); } } size_t num_channels = header->is_gray ? 1 : 3; if (header->has_alpha) num_channels++; if (num_channels + header->ec_types.size() != depth) { return JXL_FAILURE("PAM: bad DEPTH"); } if (max_val == 0 || max_val >= 65536) { return JXL_FAILURE("PAM: bad MAXVAL"); } // e.g. When `max_val` is 1 , we want 1 bit: header->bits_per_sample = FloorLog2Nonzero(max_val) + 1; if ((1u << header->bits_per_sample) - 1 != max_val) return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)"); // PAM does not pack bits as in PBM. header->floating_point = false; header->big_endian = true; *pos = pos_; return true; } Status ParseHeaderPNM(HeaderPNM* header, const uint8_t** pos) { JXL_RETURN_IF_ERROR(SkipWhitespace()); JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); JXL_RETURN_IF_ERROR(SkipWhitespace()); JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); JXL_RETURN_IF_ERROR(SkipWhitespace()); size_t max_val; JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val)); if (max_val == 0 || max_val >= 65536) { return JXL_FAILURE("PNM: bad MaxVal"); } header->bits_per_sample = FloorLog2Nonzero(max_val) + 1; if ((1u << header->bits_per_sample) - 1 != max_val) return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)"); header->floating_point = false; header->big_endian = true; JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); *pos = pos_; return true; } Status ParseHeaderPFM(HeaderPNM* header, const uint8_t** pos) { JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); JXL_RETURN_IF_ERROR(SkipBlank()); JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); // The scale has no meaning as multiplier, only its sign is used to // indicate endianness. All software expects nominal range 0..1. double scale; JXL_RETURN_IF_ERROR(ParseSigned(&scale)); if (scale == 0.0) { return JXL_FAILURE("PFM: bad scale factor value."); } else if (std::abs(scale) != 1.0) { JXL_WARNING("PFM: Discarding non-unit scale factor"); } header->big_endian = scale > 0.0; header->bits_per_sample = 32; header->floating_point = true; JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); *pos = pos_; return true; } const uint8_t* pos_; const uint8_t* const end_; }; Span MakeSpan(const char* str) { return Bytes(reinterpret_cast(str), strlen(str)); } } // namespace struct PNMChunkedInputFrame { JxlChunkedFrameInputSource operator()() { return JxlChunkedFrameInputSource{ this, METHOD_TO_C_CALLBACK( &PNMChunkedInputFrame::GetColorChannelsPixelFormat), METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetColorChannelDataAt), METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetExtraChannelPixelFormat), METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetExtraChannelDataAt), METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::ReleaseCurrentData)}; } void /* NOLINT */ GetColorChannelsPixelFormat(JxlPixelFormat* pixel_format) { *pixel_format = format; } const void* GetColorChannelDataAt(size_t xpos, size_t ypos, size_t xsize, size_t ysize, size_t* row_offset) { const size_t bytes_per_channel = DivCeil(dec->header_.bits_per_sample, jxl::kBitsPerByte); const size_t num_channels = dec->header_.is_gray ? 1 : 3; const size_t bytes_per_pixel = num_channels * bytes_per_channel; *row_offset = dec->header_.xsize * bytes_per_pixel; const size_t offset = ypos * *row_offset + xpos * bytes_per_pixel; return dec->pnm_.data() + offset + dec->data_start_; } void GetExtraChannelPixelFormat(size_t ec_index, JxlPixelFormat* pixel_format) { (void)this; JXL_ABORT("Not implemented"); } const void* GetExtraChannelDataAt(size_t ec_index, size_t xpos, size_t ypos, size_t xsize, size_t ysize, size_t* row_offset) { (void)this; JXL_ABORT("Not implemented"); } void ReleaseCurrentData(const void* buffer) {} JxlPixelFormat format; const ChunkedPNMDecoder* dec; }; StatusOr ChunkedPNMDecoder::Init(const char* path) { ChunkedPNMDecoder dec; JXL_ASSIGN_OR_RETURN(dec.pnm_, MemoryMappedFile::Init(path)); size_t size = dec.pnm_.size(); if (size < 2) return JXL_FAILURE("Invalid ppm"); size_t hdr_buf = std::min(size, 10 * 1024); Span span(dec.pnm_.data(), hdr_buf); Parser parser(span); HeaderPNM& header = dec.header_; const uint8_t* pos = nullptr; if (!parser.ParseHeader(&header, &pos)) { return StatusCode::kGenericError; } dec.data_start_ = pos - span.data(); if (header.bits_per_sample == 0 || header.bits_per_sample > 16) { return JXL_FAILURE("Invalid bits_per_sample"); } if (header.has_alpha || !header.ec_types.empty() || header.floating_point) { return JXL_FAILURE("Only PGM and PPM inputs are supported"); } const size_t bytes_per_channel = DivCeil(dec.header_.bits_per_sample, jxl::kBitsPerByte); const size_t num_channels = dec.header_.is_gray ? 1 : 3; const size_t bytes_per_pixel = num_channels * bytes_per_channel; size_t row_size = dec.header_.xsize * bytes_per_pixel; if (size < header.ysize * row_size + dec.data_start_) { return JXL_FAILURE("PNM file too small"); } return dec; } jxl::Status ChunkedPNMDecoder::InitializePPF(const ColorHints& color_hints, PackedPixelFile* ppf) { // PPM specifies that in the raster, the sample values are "nonlinear" // (BP.709, with gamma number of 2.2). Deviate from the specification and // assume `sRGB` in our implementation. JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, header_.is_gray, ppf)); ppf->info.xsize = header_.xsize; ppf->info.ysize = header_.ysize; ppf->info.bits_per_sample = header_.bits_per_sample; ppf->info.exponent_bits_per_sample = 0; ppf->info.orientation = JXL_ORIENT_IDENTITY; ppf->info.alpha_bits = 0; ppf->info.alpha_exponent_bits = 0; ppf->info.num_color_channels = (header_.is_gray ? 1 : 3); ppf->info.num_extra_channels = 0; const JxlDataType data_type = header_.bits_per_sample > 8 ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8; const JxlPixelFormat format{ /*num_channels=*/ppf->info.num_color_channels, /*data_type=*/data_type, /*endianness=*/header_.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN, /*align=*/0, }; PNMChunkedInputFrame frame; frame.format = format; frame.dec = this; ppf->chunked_frames.emplace_back(header_.xsize, header_.ysize, frame); return true; } Status DecodeImagePNM(const Span bytes, const ColorHints& color_hints, PackedPixelFile* ppf, const SizeConstraints* constraints) { Parser parser(bytes); HeaderPNM header = {}; const uint8_t* pos = nullptr; if (!parser.ParseHeader(&header, &pos)) return false; JXL_RETURN_IF_ERROR( VerifyDimensions(constraints, header.xsize, header.ysize)); if (header.bits_per_sample == 0 || header.bits_per_sample > 32) { return JXL_FAILURE("PNM: bits_per_sample invalid"); } // PPM specifies that in the raster, the sample values are "nonlinear" // (BP.709, with gamma number of 2.2). Deviate from the specification and // assume `sRGB` in our implementation. JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, header.is_gray, ppf)); ppf->info.xsize = header.xsize; ppf->info.ysize = header.ysize; if (header.floating_point) { ppf->info.bits_per_sample = 32; ppf->info.exponent_bits_per_sample = 8; } else { ppf->info.bits_per_sample = header.bits_per_sample; ppf->info.exponent_bits_per_sample = 0; } ppf->info.orientation = JXL_ORIENT_IDENTITY; // No alpha in PNM and PFM ppf->info.alpha_bits = (header.has_alpha ? ppf->info.bits_per_sample : 0); ppf->info.alpha_exponent_bits = 0; ppf->info.num_color_channels = (header.is_gray ? 1 : 3); uint32_t num_alpha_channels = (header.has_alpha ? 1 : 0); uint32_t num_interleaved_channels = ppf->info.num_color_channels + num_alpha_channels; ppf->info.num_extra_channels = num_alpha_channels + header.ec_types.size(); for (auto type : header.ec_types) { PackedExtraChannel pec; pec.ec_info.bits_per_sample = ppf->info.bits_per_sample; pec.ec_info.type = type; ppf->extra_channels_info.emplace_back(std::move(pec)); } JxlDataType data_type; if (header.floating_point) { // There's no float16 pnm version. data_type = JXL_TYPE_FLOAT; } else { if (header.bits_per_sample > 8) { data_type = JXL_TYPE_UINT16; } else { data_type = JXL_TYPE_UINT8; } } const JxlPixelFormat format{ /*num_channels=*/num_interleaved_channels, /*data_type=*/data_type, /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN, /*align=*/0, }; const JxlPixelFormat ec_format{1, format.data_type, format.endianness, 0}; ppf->frames.clear(); { JXL_ASSIGN_OR_RETURN( PackedFrame frame, PackedFrame::Create(header.xsize, header.ysize, format)); ppf->frames.emplace_back(std::move(frame)); } auto* frame = &ppf->frames.back(); for (size_t i = 0; i < header.ec_types.size(); ++i) { JXL_ASSIGN_OR_RETURN( PackedImage ec, PackedImage::Create(header.xsize, header.ysize, ec_format)); frame->extra_channels.emplace_back(std::move(ec)); } size_t pnm_remaining_size = bytes.data() + bytes.size() - pos; if (pnm_remaining_size < frame->color.pixels_size) { return JXL_FAILURE("PNM file too small"); } uint8_t* out = reinterpret_cast(frame->color.pixels()); std::vector ec_out(header.ec_types.size()); for (size_t i = 0; i < ec_out.size(); ++i) { ec_out[i] = reinterpret_cast(frame->extra_channels[i].pixels()); } if (ec_out.empty()) { const bool flipped_y = header.bits_per_sample == 32; // PFMs are flipped for (size_t y = 0; y < header.ysize; ++y) { size_t y_in = flipped_y ? header.ysize - 1 - y : y; const uint8_t* row_in = &pos[y_in * frame->color.stride]; uint8_t* row_out = &out[y * frame->color.stride]; memcpy(row_out, row_in, frame->color.stride); } } else { size_t pwidth = PackedImage::BitsPerChannel(data_type) / 8; for (size_t y = 0; y < header.ysize; ++y) { for (size_t x = 0; x < header.xsize; ++x) { memcpy(out, pos, frame->color.pixel_stride()); out += frame->color.pixel_stride(); pos += frame->color.pixel_stride(); for (auto& p : ec_out) { memcpy(p, pos, pwidth); pos += pwidth; p += pwidth; } } } } if (ppf->info.exponent_bits_per_sample == 0) { ppf->input_bitdepth.type = JXL_BIT_DEPTH_FROM_CODESTREAM; } return true; } void TestCodecPNM() { size_t u = 77777; // Initialized to wrong value. double d = 77.77; // Failing to parse invalid strings results in a crash if `JXL_CRASH_ON_ERROR` // is defined and hence the tests fail. Therefore we only run these tests if // `JXL_CRASH_ON_ERROR` is not defined. #ifndef JXL_CRASH_ON_ERROR JXL_CHECK(false == Parser(MakeSpan("")).ParseUnsigned(&u)); JXL_CHECK(false == Parser(MakeSpan("+")).ParseUnsigned(&u)); JXL_CHECK(false == Parser(MakeSpan("-")).ParseUnsigned(&u)); JXL_CHECK(false == Parser(MakeSpan("A")).ParseUnsigned(&u)); JXL_CHECK(false == Parser(MakeSpan("")).ParseSigned(&d)); JXL_CHECK(false == Parser(MakeSpan("+")).ParseSigned(&d)); JXL_CHECK(false == Parser(MakeSpan("-")).ParseSigned(&d)); JXL_CHECK(false == Parser(MakeSpan("A")).ParseSigned(&d)); #endif JXL_CHECK(true == Parser(MakeSpan("1")).ParseUnsigned(&u)); JXL_CHECK(u == 1); JXL_CHECK(true == Parser(MakeSpan("32")).ParseUnsigned(&u)); JXL_CHECK(u == 32); JXL_CHECK(true == Parser(MakeSpan("1")).ParseSigned(&d)); JXL_CHECK(d == 1.0); JXL_CHECK(true == Parser(MakeSpan("+2")).ParseSigned(&d)); JXL_CHECK(d == 2.0); JXL_CHECK(true == Parser(MakeSpan("-3")).ParseSigned(&d)); JXL_CHECK(std::abs(d - -3.0) < 1E-15); JXL_CHECK(true == Parser(MakeSpan("3.141592")).ParseSigned(&d)); JXL_CHECK(std::abs(d - 3.141592) < 1E-15); JXL_CHECK(true == Parser(MakeSpan("-3.141592")).ParseSigned(&d)); JXL_CHECK(std::abs(d - -3.141592) < 1E-15); } } // namespace extras } // namespace jxl