diff options
Diffstat (limited to 'third_party/jpeg-xl/lib/extras/dec/pnm.cc')
-rw-r--r-- | third_party/jpeg-xl/lib/extras/dec/pnm.cc | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/third_party/jpeg-xl/lib/extras/dec/pnm.cc b/third_party/jpeg-xl/lib/extras/dec/pnm.cc new file mode 100644 index 0000000000..c3c2247769 --- /dev/null +++ b/third_party/jpeg-xl/lib/extras/dec/pnm.cc @@ -0,0 +1,474 @@ +// 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 <stdlib.h> +#include <string.h> + +#include <cmath> + +#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 { + +struct HeaderPNM { + size_t xsize; + size_t ysize; + bool is_gray; // PGM + bool has_alpha; // PAM + size_t bits_per_sample; + bool floating_point; + bool big_endian; + std::vector<JxlExtraChannelType> ec_types; // PAM +}; + +class Parser { + public: + explicit Parser(const Span<const uint8_t> 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); + } + 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_; + while (*keyword) { + if (ppos >= end_) return JXL_FAILURE("PAM: unexpected end of input"); + if (*keyword != *ppos) return false; + ppos++; + keyword++; + } + 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<size_t>(kMaxHeaderLength, end_ - pos_); + strncpy(unknown_header, reinterpret_cast<const char*>(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<const uint8_t> MakeSpan(const char* str) { + return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str), + strlen(str)); +} + +} // namespace + +Status DecodeImagePNM(const Span<const uint8_t> 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 specify 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(); + ppf->frames.emplace_back(header.xsize, header.ysize, format); + auto* frame = &ppf->frames.back(); + for (size_t i = 0; i < header.ec_types.size(); ++i) { + frame->extra_channels.emplace_back(header.xsize, header.ysize, ec_format); + } + 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<uint8_t*>(frame->color.pixels()); + std::vector<uint8_t*> ec_out(header.ec_types.size()); + for (size_t i = 0; i < ec_out.size(); ++i) { + ec_out[i] = reinterpret_cast<uint8_t*>(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; + } + } + } + } + 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 |