libjxl

FORK: libjxl patches used on blog
git clone https://git.neptards.moe/blog/libjxl.git
Log | Files | Refs | Submodules | README | LICENSE

pnm.cc (19663B)


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