tor-browser

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

frame_header.cc (19829B)


      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/jxl/frame_header.h"
      7 
      8 #if JXL_DEBUG_V_LEVEL >= 1
      9 #include <sstream>
     10 #include <string>
     11 #endif
     12 
     13 #include "lib/jxl/base/printf_macros.h"
     14 #include "lib/jxl/base/status.h"
     15 #include "lib/jxl/common.h"  // kMaxNumPasses
     16 #include "lib/jxl/fields.h"
     17 #include "lib/jxl/pack_signed.h"
     18 
     19 namespace jxl {
     20 
     21 constexpr uint8_t YCbCrChromaSubsampling::kHShift[] = {0, 1, 1, 0};
     22 constexpr uint8_t YCbCrChromaSubsampling::kVShift[] = {0, 1, 0, 1};
     23 
     24 static Status VisitBlendMode(Visitor* JXL_RESTRICT visitor,
     25                             BlendMode default_value, BlendMode* blend_mode) {
     26  uint32_t encoded = static_cast<uint32_t>(*blend_mode);
     27 
     28  JXL_QUIET_RETURN_IF_ERROR(visitor->U32(
     29      Val(static_cast<uint32_t>(BlendMode::kReplace)),
     30      Val(static_cast<uint32_t>(BlendMode::kAdd)),
     31      Val(static_cast<uint32_t>(BlendMode::kBlend)), BitsOffset(2, 3),
     32      static_cast<uint32_t>(default_value), &encoded));
     33  if (encoded > static_cast<uint32_t>(BlendMode::kMul)) {
     34    return JXL_FAILURE("Invalid blend_mode");
     35  }
     36  *blend_mode = static_cast<BlendMode>(encoded);
     37  return true;
     38 }
     39 
     40 static Status VisitFrameType(Visitor* JXL_RESTRICT visitor,
     41                             FrameType default_value, FrameType* frame_type) {
     42  uint32_t encoded = static_cast<uint32_t>(*frame_type);
     43 
     44  JXL_QUIET_RETURN_IF_ERROR(
     45      visitor->U32(Val(static_cast<uint32_t>(FrameType::kRegularFrame)),
     46                   Val(static_cast<uint32_t>(FrameType::kDCFrame)),
     47                   Val(static_cast<uint32_t>(FrameType::kReferenceOnly)),
     48                   Val(static_cast<uint32_t>(FrameType::kSkipProgressive)),
     49                   static_cast<uint32_t>(default_value), &encoded));
     50  *frame_type = static_cast<FrameType>(encoded);
     51  return true;
     52 }
     53 
     54 BlendingInfo::BlendingInfo() { Bundle::Init(this); }
     55 
     56 Status BlendingInfo::VisitFields(Visitor* JXL_RESTRICT visitor) {
     57  JXL_QUIET_RETURN_IF_ERROR(
     58      VisitBlendMode(visitor, BlendMode::kReplace, &mode));
     59  if (visitor->Conditional(nonserialized_num_extra_channels > 0 &&
     60                           (mode == BlendMode::kBlend ||
     61                            mode == BlendMode::kAlphaWeightedAdd))) {
     62    // Up to 11 alpha channels for blending.
     63    JXL_QUIET_RETURN_IF_ERROR(visitor->U32(
     64        Val(0), Val(1), Val(2), BitsOffset(3, 3), 0, &alpha_channel));
     65    if (visitor->IsReading() &&
     66        alpha_channel >= nonserialized_num_extra_channels) {
     67      return JXL_FAILURE("Invalid alpha channel for blending");
     68    }
     69  }
     70  if (visitor->Conditional((nonserialized_num_extra_channels > 0 &&
     71                            (mode == BlendMode::kBlend ||
     72                             mode == BlendMode::kAlphaWeightedAdd)) ||
     73                           mode == BlendMode::kMul)) {
     74    JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &clamp));
     75  }
     76  // 'old' frame for blending. Only necessary if this is not a full frame, or
     77  // blending is not kReplace.
     78  if (visitor->Conditional(mode != BlendMode::kReplace ||
     79                           nonserialized_is_partial_frame)) {
     80    JXL_QUIET_RETURN_IF_ERROR(
     81        visitor->U32(Val(0), Val(1), Val(2), Val(3), 0, &source));
     82  }
     83  return true;
     84 }
     85 
     86 #if JXL_DEBUG_V_LEVEL >= 1
     87 std::string BlendingInfo::DebugString() const {
     88  std::ostringstream os;
     89  os << (mode == BlendMode::kReplace            ? "Replace"
     90         : mode == BlendMode::kAdd              ? "Add"
     91         : mode == BlendMode::kBlend            ? "Blend"
     92         : mode == BlendMode::kAlphaWeightedAdd ? "AlphaWeightedAdd"
     93                                                : "Mul");
     94  if (nonserialized_num_extra_channels > 0 &&
     95      (mode == BlendMode::kBlend || mode == BlendMode::kAlphaWeightedAdd)) {
     96    os << ",alpha=" << alpha_channel << ",clamp=" << clamp;
     97  } else if (mode == BlendMode::kMul) {
     98    os << ",clamp=" << clamp;
     99  }
    100  if (mode != BlendMode::kReplace || nonserialized_is_partial_frame) {
    101    os << ",source=" << source;
    102  }
    103  return os.str();
    104 }
    105 #endif
    106 
    107 AnimationFrame::AnimationFrame(const CodecMetadata* metadata)
    108    : nonserialized_metadata(metadata) {
    109  Bundle::Init(this);
    110 }
    111 Status AnimationFrame::VisitFields(Visitor* JXL_RESTRICT visitor) {
    112  if (visitor->Conditional(nonserialized_metadata != nullptr &&
    113                           nonserialized_metadata->m.have_animation)) {
    114    JXL_QUIET_RETURN_IF_ERROR(
    115        visitor->U32(Val(0), Val(1), Bits(8), Bits(32), 0, &duration));
    116  }
    117 
    118  if (visitor->Conditional(
    119          nonserialized_metadata != nullptr &&
    120          nonserialized_metadata->m.animation.have_timecodes)) {
    121    JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(32, 0, &timecode));
    122  }
    123  return true;
    124 }
    125 
    126 YCbCrChromaSubsampling::YCbCrChromaSubsampling() { Bundle::Init(this); }
    127 Passes::Passes() { Bundle::Init(this); }
    128 Status Passes::VisitFields(Visitor* JXL_RESTRICT visitor) {
    129  JXL_QUIET_RETURN_IF_ERROR(
    130      visitor->U32(Val(1), Val(2), Val(3), BitsOffset(3, 4), 1, &num_passes));
    131  JXL_ENSURE(num_passes <= kMaxNumPasses);  // Cannot happen when reading
    132 
    133  if (visitor->Conditional(num_passes != 1)) {
    134    JXL_QUIET_RETURN_IF_ERROR(visitor->U32(
    135        Val(0), Val(1), Val(2), BitsOffset(1, 3), 0, &num_downsample));
    136    JXL_ENSURE(num_downsample <= 4);  // 1,2,4,8
    137    if (num_downsample > num_passes) {
    138      return JXL_FAILURE("num_downsample %u > num_passes %u", num_downsample,
    139                         num_passes);
    140    }
    141 
    142    for (uint32_t i = 0; i < num_passes - 1; i++) {
    143      JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(2, 0, &shift[i]));
    144    }
    145    shift[num_passes - 1] = 0;
    146 
    147    for (uint32_t i = 0; i < num_downsample; ++i) {
    148      JXL_QUIET_RETURN_IF_ERROR(
    149          visitor->U32(Val(1), Val(2), Val(4), Val(8), 1, &downsample[i]));
    150      if (i > 0 && downsample[i] >= downsample[i - 1]) {
    151        return JXL_FAILURE("downsample sequence should be decreasing");
    152      }
    153    }
    154    for (uint32_t i = 0; i < num_downsample; ++i) {
    155      JXL_QUIET_RETURN_IF_ERROR(
    156          visitor->U32(Val(0), Val(1), Val(2), Bits(3), 0, &last_pass[i]));
    157      if (i > 0 && last_pass[i] <= last_pass[i - 1]) {
    158        return JXL_FAILURE("last_pass sequence should be increasing");
    159      }
    160      if (last_pass[i] >= num_passes) {
    161        return JXL_FAILURE("last_pass %u >= num_passes %u", last_pass[i],
    162                           num_passes);
    163      }
    164    }
    165  }
    166 
    167  return true;
    168 }
    169 
    170 #if JXL_DEBUG_V_LEVEL >= 1
    171 std::string Passes::DebugString() const {
    172  std::ostringstream os;
    173  os << "p=" << num_passes;
    174  if (num_downsample) {
    175    os << ",ds=";
    176    for (uint32_t i = 0; i < num_downsample; ++i) {
    177      os << last_pass[i] << ":" << downsample[i];
    178      if (i + 1 < num_downsample) os << ";";
    179    }
    180  }
    181  bool have_shifts = false;
    182  for (uint32_t i = 0; i < num_passes; ++i) {
    183    if (shift[i]) have_shifts = true;
    184  }
    185  if (have_shifts) {
    186    os << ",shifts=";
    187    for (uint32_t i = 0; i < num_passes; ++i) {
    188      os << shift[i];
    189      if (i + 1 < num_passes) os << ";";
    190    }
    191  }
    192  return os.str();
    193 }
    194 #endif
    195 
    196 FrameHeader::FrameHeader(const CodecMetadata* metadata)
    197    : animation_frame(metadata), nonserialized_metadata(metadata) {
    198  Bundle::Init(this);
    199 }
    200 
    201 Status ReadFrameHeader(BitReader* JXL_RESTRICT reader,
    202                       FrameHeader* JXL_RESTRICT frame) {
    203  return Bundle::Read(reader, frame);
    204 }
    205 
    206 Status FrameHeader::VisitFields(Visitor* JXL_RESTRICT visitor) {
    207  if (visitor->AllDefault(*this, &all_default)) {
    208    // Overwrite all serialized fields, but not any nonserialized_*.
    209    visitor->SetDefault(this);
    210    return true;
    211  }
    212 
    213  JXL_QUIET_RETURN_IF_ERROR(
    214      VisitFrameType(visitor, FrameType::kRegularFrame, &frame_type));
    215  if (visitor->IsReading() && nonserialized_is_preview &&
    216      frame_type != kRegularFrame) {
    217    return JXL_FAILURE("Only regular frame could be a preview");
    218  }
    219 
    220  // FrameEncoding.
    221  bool is_modular = (encoding == FrameEncoding::kModular);
    222  JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &is_modular));
    223  encoding = (is_modular ? FrameEncoding::kModular : FrameEncoding::kVarDCT);
    224 
    225  // Flags
    226  JXL_QUIET_RETURN_IF_ERROR(visitor->U64(0, &flags));
    227 
    228  // Color transform
    229  bool xyb_encoded = nonserialized_metadata == nullptr ||
    230                     nonserialized_metadata->m.xyb_encoded;
    231 
    232  if (xyb_encoded) {
    233    color_transform = ColorTransform::kXYB;
    234  } else {
    235    // Alternate if kYCbCr.
    236    bool alternate = color_transform == ColorTransform::kYCbCr;
    237    JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &alternate));
    238    color_transform =
    239        (alternate ? ColorTransform::kYCbCr : ColorTransform::kNone);
    240  }
    241 
    242  // Chroma subsampling for YCbCr, if no DC frame is used.
    243  if (visitor->Conditional(color_transform == ColorTransform::kYCbCr &&
    244                           ((flags & kUseDcFrame) == 0))) {
    245    JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&chroma_subsampling));
    246  }
    247 
    248  size_t num_extra_channels =
    249      nonserialized_metadata != nullptr
    250          ? nonserialized_metadata->m.extra_channel_info.size()
    251          : 0;
    252 
    253  // Upsampling
    254  if (visitor->Conditional((flags & kUseDcFrame) == 0)) {
    255    JXL_QUIET_RETURN_IF_ERROR(
    256        visitor->U32(Val(1), Val(2), Val(4), Val(8), 1, &upsampling));
    257    if (nonserialized_metadata != nullptr &&
    258        visitor->Conditional(num_extra_channels != 0)) {
    259      const std::vector<ExtraChannelInfo>& extra_channels =
    260          nonserialized_metadata->m.extra_channel_info;
    261      extra_channel_upsampling.resize(extra_channels.size(), 1);
    262      for (size_t i = 0; i < extra_channels.size(); ++i) {
    263        uint32_t dim_shift =
    264            nonserialized_metadata->m.extra_channel_info[i].dim_shift;
    265        uint32_t& ec_upsampling = extra_channel_upsampling[i];
    266        ec_upsampling >>= dim_shift;
    267        JXL_QUIET_RETURN_IF_ERROR(
    268            visitor->U32(Val(1), Val(2), Val(4), Val(8), 1, &ec_upsampling));
    269        ec_upsampling <<= dim_shift;
    270        if (ec_upsampling < upsampling) {
    271          return JXL_FAILURE(
    272              "EC upsampling (%u) < color upsampling (%u), which is invalid.",
    273              ec_upsampling, upsampling);
    274        }
    275        if (ec_upsampling > 8) {
    276          return JXL_FAILURE("EC upsampling too large (%u)", ec_upsampling);
    277        }
    278      }
    279    } else {
    280      extra_channel_upsampling.clear();
    281    }
    282  }
    283 
    284  // Modular- or VarDCT-specific data.
    285  if (visitor->Conditional(encoding == FrameEncoding::kModular)) {
    286    JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(2, 1, &group_size_shift));
    287  }
    288  if (visitor->Conditional(encoding == FrameEncoding::kVarDCT &&
    289                           color_transform == ColorTransform::kXYB)) {
    290    JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(3, 3, &x_qm_scale));
    291    JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(3, 2, &b_qm_scale));
    292  } else {
    293    x_qm_scale = b_qm_scale = 2;  // noop
    294  }
    295 
    296  // Not useful for kPatchSource
    297  if (visitor->Conditional(frame_type != FrameType::kReferenceOnly)) {
    298    JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&passes));
    299  }
    300 
    301  if (visitor->Conditional(frame_type == FrameType::kDCFrame)) {
    302    // Up to 4 pyramid levels - for up to 16384x downsampling.
    303    JXL_QUIET_RETURN_IF_ERROR(
    304        visitor->U32(Val(1), Val(2), Val(3), Val(4), 1, &dc_level));
    305  }
    306  if (frame_type != FrameType::kDCFrame) {
    307    dc_level = 0;
    308  }
    309 
    310  bool is_partial_frame = false;
    311  if (visitor->Conditional(frame_type != FrameType::kDCFrame)) {
    312    JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &custom_size_or_origin));
    313    if (visitor->Conditional(custom_size_or_origin)) {
    314      const U32Enc enc(Bits(8), BitsOffset(11, 256), BitsOffset(14, 2304),
    315                       BitsOffset(30, 18688));
    316      // Frame offset, only if kRegularFrame or kSkipProgressive.
    317      if (visitor->Conditional(frame_type == FrameType::kRegularFrame ||
    318                               frame_type == FrameType::kSkipProgressive)) {
    319        uint32_t ux0 = PackSigned(frame_origin.x0);
    320        uint32_t uy0 = PackSigned(frame_origin.y0);
    321        JXL_QUIET_RETURN_IF_ERROR(visitor->U32(enc, 0, &ux0));
    322        JXL_QUIET_RETURN_IF_ERROR(visitor->U32(enc, 0, &uy0));
    323        frame_origin.x0 = UnpackSigned(ux0);
    324        frame_origin.y0 = UnpackSigned(uy0);
    325      }
    326      // Frame size
    327      JXL_QUIET_RETURN_IF_ERROR(visitor->U32(enc, 0, &frame_size.xsize));
    328      JXL_QUIET_RETURN_IF_ERROR(visitor->U32(enc, 0, &frame_size.ysize));
    329      if (custom_size_or_origin &&
    330          (frame_size.xsize == 0 || frame_size.ysize == 0)) {
    331        return JXL_FAILURE(
    332            "Invalid crop dimensions for frame: zero width or height");
    333      }
    334      int32_t image_xsize = default_xsize();
    335      int32_t image_ysize = default_ysize();
    336      if (frame_type == FrameType::kRegularFrame ||
    337          frame_type == FrameType::kSkipProgressive) {
    338        is_partial_frame |= frame_origin.x0 > 0;
    339        is_partial_frame |= frame_origin.y0 > 0;
    340        is_partial_frame |= (static_cast<int32_t>(frame_size.xsize) +
    341                             frame_origin.x0) < image_xsize;
    342        is_partial_frame |= (static_cast<int32_t>(frame_size.ysize) +
    343                             frame_origin.y0) < image_ysize;
    344      }
    345    }
    346  }
    347 
    348  // Blending info, animation info and whether this is the last frame or not.
    349  if (visitor->Conditional(frame_type == FrameType::kRegularFrame ||
    350                           frame_type == FrameType::kSkipProgressive)) {
    351    blending_info.nonserialized_num_extra_channels = num_extra_channels;
    352    blending_info.nonserialized_is_partial_frame = is_partial_frame;
    353    JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&blending_info));
    354    bool replace_all = (blending_info.mode == BlendMode::kReplace);
    355    extra_channel_blending_info.resize(num_extra_channels);
    356    for (size_t i = 0; i < num_extra_channels; i++) {
    357      auto& ec_blending_info = extra_channel_blending_info[i];
    358      ec_blending_info.nonserialized_is_partial_frame = is_partial_frame;
    359      ec_blending_info.nonserialized_num_extra_channels = num_extra_channels;
    360      JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&ec_blending_info));
    361      replace_all &= (ec_blending_info.mode == BlendMode::kReplace);
    362    }
    363    if (visitor->IsReading() && nonserialized_is_preview) {
    364      if (!replace_all || custom_size_or_origin) {
    365        return JXL_FAILURE("Preview is not compatible with blending");
    366      }
    367    }
    368    if (visitor->Conditional(nonserialized_metadata != nullptr &&
    369                             nonserialized_metadata->m.have_animation)) {
    370      animation_frame.nonserialized_metadata = nonserialized_metadata;
    371      JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&animation_frame));
    372    }
    373    JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(true, &is_last));
    374  } else {
    375    is_last = false;
    376  }
    377 
    378  // ID of that can be used to refer to this frame. 0 for a non-zero-duration
    379  // frame means that it will not be referenced. Not necessary for the last
    380  // frame.
    381  if (visitor->Conditional(frame_type != kDCFrame && !is_last)) {
    382    JXL_QUIET_RETURN_IF_ERROR(
    383        visitor->U32(Val(0), Val(1), Val(2), Val(3), 0, &save_as_reference));
    384  }
    385 
    386  // If this frame is not blended on another frame post-color-transform, it may
    387  // be stored for being referenced either before or after the color transform.
    388  // If it is blended post-color-transform, it must be blended after. It must
    389  // also be blended after if this is a kRegular frame that does not cover the
    390  // full frame, as samples outside the partial region are from a
    391  // post-color-transform frame.
    392  if (frame_type != FrameType::kDCFrame) {
    393    if (visitor->Conditional(CanBeReferenced() &&
    394                             blending_info.mode == BlendMode::kReplace &&
    395                             !is_partial_frame &&
    396                             (frame_type == FrameType::kRegularFrame ||
    397                              frame_type == FrameType::kSkipProgressive))) {
    398      JXL_QUIET_RETURN_IF_ERROR(
    399          visitor->Bool(false, &save_before_color_transform));
    400    } else if (visitor->Conditional(frame_type == FrameType::kReferenceOnly)) {
    401      JXL_QUIET_RETURN_IF_ERROR(
    402          visitor->Bool(true, &save_before_color_transform));
    403      size_t xsize = custom_size_or_origin ? frame_size.xsize
    404                                           : nonserialized_metadata->xsize();
    405      size_t ysize = custom_size_or_origin ? frame_size.ysize
    406                                           : nonserialized_metadata->ysize();
    407      if (!save_before_color_transform &&
    408          (xsize < nonserialized_metadata->xsize() ||
    409           ysize < nonserialized_metadata->ysize() || frame_origin.x0 != 0 ||
    410           frame_origin.y0 != 0)) {
    411        return JXL_FAILURE(
    412            "non-patch reference frame with invalid crop: %" PRIuS "x%" PRIuS
    413            "%+d%+d",
    414            xsize, ysize, static_cast<int>(frame_origin.x0),
    415            static_cast<int>(frame_origin.y0));
    416      }
    417    }
    418  } else {
    419    save_before_color_transform = true;
    420  }
    421 
    422  JXL_QUIET_RETURN_IF_ERROR(VisitNameString(visitor, &name));
    423 
    424  loop_filter.nonserialized_is_modular = is_modular;
    425  JXL_RETURN_IF_ERROR(visitor->VisitNested(&loop_filter));
    426 
    427  JXL_QUIET_RETURN_IF_ERROR(visitor->BeginExtensions(&extensions));
    428  // Extensions: in chronological order of being added to the format.
    429  return visitor->EndExtensions();
    430 }
    431 
    432 #if JXL_DEBUG_V_LEVEL >= 1
    433 std::string FrameHeader::DebugString() const {
    434  std::ostringstream os;
    435  os << (encoding == FrameEncoding::kVarDCT ? "VarDCT" : "Modular");
    436  os << ",";
    437  os << (frame_type == FrameType::kRegularFrame    ? "Regular"
    438         : frame_type == FrameType::kDCFrame       ? "DC"
    439         : frame_type == FrameType::kReferenceOnly ? "Reference"
    440                                                   : "SkipProgressive");
    441  if (frame_type == FrameType::kDCFrame) {
    442    os << "(lv" << dc_level << ")";
    443  }
    444 
    445  if (flags) {
    446    os << ",";
    447    uint32_t remaining = flags;
    448 
    449 #define TEST_FLAG(name)           \
    450  if (flags & Flags::k##name) {   \
    451    remaining &= ~Flags::k##name; \
    452    os << #name;                  \
    453    if (remaining) os << "|";     \
    454  }
    455    TEST_FLAG(Noise);
    456    TEST_FLAG(Patches);
    457    TEST_FLAG(Splines);
    458    TEST_FLAG(UseDcFrame);
    459    TEST_FLAG(SkipAdaptiveDCSmoothing);
    460 #undef TEST_FLAG
    461  }
    462 
    463  os << ",";
    464  os << (color_transform == ColorTransform::kXYB     ? "XYB"
    465         : color_transform == ColorTransform::kYCbCr ? "YCbCr"
    466                                                     : "None");
    467 
    468  if (encoding == FrameEncoding::kModular) {
    469    os << ",shift=" << group_size_shift;
    470  } else if (color_transform == ColorTransform::kXYB) {
    471    os << ",qm=" << x_qm_scale << ";" << b_qm_scale;
    472  }
    473  if (frame_type != FrameType::kReferenceOnly) {
    474    os << "," << passes.DebugString();
    475  }
    476  if (custom_size_or_origin) {
    477    os << ",xs=" << frame_size.xsize;
    478    os << ",ys=" << frame_size.ysize;
    479    if (frame_type == FrameType::kRegularFrame ||
    480        frame_type == FrameType::kSkipProgressive) {
    481      os << ",x0=" << frame_origin.x0;
    482      os << ",y0=" << frame_origin.y0;
    483    }
    484  }
    485  if (upsampling > 1) os << ",up=" << upsampling;
    486  if (loop_filter.gab) os << ",Gaborish";
    487  if (loop_filter.epf_iters > 0) os << ",epf=" << loop_filter.epf_iters;
    488  if (animation_frame.duration > 0) os << ",dur=" << animation_frame.duration;
    489  if (frame_type == FrameType::kRegularFrame ||
    490      frame_type == FrameType::kSkipProgressive) {
    491    os << ",";
    492    os << blending_info.DebugString();
    493    for (size_t i = 0; i < extra_channel_blending_info.size(); ++i) {
    494      os << (i == 0 ? "[" : ";");
    495      os << extra_channel_blending_info[i].DebugString();
    496      if (i + 1 == extra_channel_blending_info.size()) os << "]";
    497    }
    498  }
    499  if (save_as_reference > 0) os << ",ref=" << save_as_reference;
    500  os << "," << (save_before_color_transform ? "before" : "after") << "_ct";
    501  if (is_last) os << ",last";
    502  return os.str();
    503 }
    504 #endif
    505 
    506 }  // namespace jxl