libjxl

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

frame_header.cc (19740B)


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