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