tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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