Add a transform that maps each channel to an index

It is not a very useful transform for now.

Change-Id: I759989fc340ed82534bb813c3896e9f88754c0b5
Reviewed-on: https://chromium-review.googlesource.com/c/codecs/libwebp2/+/6249119
Tested-by: WebM Builds <builds@webmproject.org>
Reviewed-by: Yannis Guyon <yguyon@google.com>
diff --git a/src/common/lossless/transforms.cc b/src/common/lossless/transforms.cc
index 88a11b8..107ba81 100644
--- a/src/common/lossless/transforms.cc
+++ b/src/common/lossless/transforms.cc
@@ -201,6 +201,27 @@
         // Only the color index in [0, num_colors-1] is stored in G.
         segments[2] = {0, static_cast<int32_t>(header.indexing_num_colors) - 1};
         break;
+      case TransformType::kNormalizeChannels: {
+        if (header.normalize_channels_has_palette) {
+          for (uint32_t c = 0; c < 4; ++c) {
+            if (header.normalize_channels_num_colors[c] > 0) {
+              segments[c] = {0, static_cast<int32_t>(
+                                    header.normalize_channels_num_colors[c]) -
+                                    1};
+            } else {
+              if (c != 0) return WP2_STATUS_INVALID_PARAMETER;
+              segments[c] = {0, 0};
+            }
+          }
+        } else {
+          for (uint32_t c = 0; c < 4; ++c) {
+            segments[c] =
+                segments[c] + Segment{-header.normalize_channels_min[c],
+                                      -header.normalize_channels_min[c]};
+          }
+        }
+        break;
+      }
       case TransformType::kNum:
         assert(false);
     }
diff --git a/src/common/vdebug.cc b/src/common/vdebug.cc
index b12de58..bfc3f0b 100644
--- a/src/common/vdebug.cc
+++ b/src/common/vdebug.cc
@@ -2782,8 +2782,8 @@
 }
 
 static const char* const kTransformNames[] = {
-    "Predictor", "Predictor with sub", "CrossColor",   "CrossColorGlobal",
-    "YCoCgR",    "SubstractGreen",     "ColorIndexing"};
+    "Predictor", "Predictor with sub", "CrossColor",    "CrossColorGlobal",
+    "YCoCgR",    "SubstractGreen",     "ColorIndexing", "NormalizeChannels"};
 STATIC_ASSERT_ARRAY_SIZE(kTransformNames, (uint32_t)TransformType::kNum);
 static const char* const kPredictorNames[] = {"Black",
                                               "180",
diff --git a/src/dec/lossless/losslessi_dec.cc b/src/dec/lossless/losslessi_dec.cc
index a426477..bd56df9 100644
--- a/src/dec/lossless/losslessi_dec.cc
+++ b/src/dec/lossless/losslessi_dec.cc
@@ -832,7 +832,7 @@
 }
 
 WP2Status Decoder::ReadTransform(uint32_t width, uint32_t height,
-                                 TransformType type,
+                                 TransformType type, int index,
                                  Transform* const transform) {
   transform->header_.type = type;
   transform->width_pic_ = width;
@@ -927,6 +927,35 @@
       transform->header_.indexing_num_colors = transform->data_.size() / 4;
       break;
     }
+    case TransformType::kNormalizeChannels: {
+      WP2::ANSDebugPrefix prefix(dec_, "normalize_channels");
+      std::array<int32_t, 4> minima_range, maxima_range;
+      std::array<TransformHeader, kPossibleTransformCombinationSize> headers;
+      for (int i = 0; i < index; ++i) {
+        headers[i] = transforms_[i].header_;
+      }
+      WP2_CHECK_STATUS(GetARGBRanges(headers, hdr_.symbols_info_.SampleFormat(),
+                                     minima_range, maxima_range, index));
+      for (int c = gparams_->has_alpha_ ? 0 : 1; c < 4; ++c) {
+        transform->header_.normalize_channels_min[c] =
+            dec_->ReadRange(minima_range[c], maxima_range[c], "min");
+      }
+      transform->header_.normalize_channels_has_palette =
+          dec_->ReadBool("has_palette");
+      if (transform->header_.normalize_channels_has_palette) {
+        for (int c = gparams_->has_alpha_ ? 0 : 1; c < 4; ++c) {
+          const uint32_t range = 1 + maxima_range[c] - minima_range[c];
+          const uint32_t mapping_size = dec_->ReadRange(1, range, "size");
+          transform->header_.normalize_channels_num_colors[c] = mapping_size;
+          WP2_CHECK_ALLOC_OK(
+              transform->normalize_channels_mapping_[c].resize(mapping_size));
+          WP2_CHECK_STATUS(
+              LoadMapping(dec_, mapping_size, range,
+                          transform->normalize_channels_mapping_[c].data()));
+        }
+      }
+      break;
+    }
     case TransformType::kSubtractGreen:
     case TransformType::kYCoCgR:
       break;
@@ -1013,23 +1042,21 @@
   dec_->AddDebugPrefix("transforms");
   const uint32_t index = dec_->ReadRValue(kPossibleEncodingRecipesNum, "index");
   encoding_algorithm_ = kPossibleEncodingRecipes[index].algorithm;
-  const TransformType* const transforms =
-      kPossibleEncodingRecipes[index].transforms;
-  uint32_t num_transforms = 0;
-  for (; num_transforms < kPossibleTransformCombinationSize; ++num_transforms) {
-    if (transforms[num_transforms] == TransformType::kNum) {
-      break;
-    }
-  }
 
-  // Deactivate all transforms in case the decoder is re-used.
-  for (Transform& t : transforms_) t.header_.type = TransformType::kNum;
-  for (uint32_t i = 0; i < num_transforms; ++i) {
-    TransformType type = transforms[i];
-    WP2_CHECK_STATUS(ReadTransform(tile_->rect.width, tile_->rect.height, type,
-                                   &transforms_[i]));
-    WP2_CHECK_REDUCED_STATUS(
-        RegisterTransformForVDebug(transforms_[i], config_.info));
+  uint32_t num_transforms = kPossibleTransformCombinationSize;
+  for (uint32_t i = 0; i < kPossibleTransformCombinationSize; ++i) {
+    const TransformType type = kPossibleEncodingRecipes[index].transforms[i];
+    Transform& transform = transforms_[i];
+    if (type == TransformType::kNum) {
+      num_transforms = std::min(num_transforms, i);
+      // Deactivate all transforms in case the decoder is re-used.
+      transform.header_.type = TransformType::kNum;
+    } else {
+      WP2_CHECK_STATUS(ReadTransform(tile_->rect.width, tile_->rect.height,
+                                     type, i, &transform));
+      WP2_CHECK_REDUCED_STATUS(
+          RegisterTransformForVDebug(transform, config_.info));
+    }
   }
 
   // Read headers for specific algorithms.
@@ -1103,7 +1130,8 @@
   }
 
   // Modify the main image ranges if we only use the palette.
-  if (num_transforms == 1 && transforms[0] == TransformType::kColorIndexing) {
+  if (num_transforms == 1 &&
+      transforms_[0].header_.type == TransformType::kColorIndexing) {
     hdr_.symbols_info_.InitAsLabelImage(tile_->rect.GetArea(), num_colors_);
   } else if (num_transforms != 0) {
     std::array<int32_t, 4> minima_range, maxima_range;
diff --git a/src/dec/lossless/losslessi_dec.h b/src/dec/lossless/losslessi_dec.h
index f7e8191..96911a8 100644
--- a/src/dec/lossless/losslessi_dec.h
+++ b/src/dec/lossless/losslessi_dec.h
@@ -44,12 +44,17 @@
 class Transform {
  public:
   TransformHeader header_;  // header.
-  uint32_t bits_ = 0;       // subsampling bits defining transform window.
   uint32_t width_pic_;      // initial picture width
   uint32_t height_pic_;     // initial picture height
-  uint32_t width_ = 0;      // transform width (if any)
-  uint32_t height_ = 0;     // transform height (if any)
-  WP2::Vector_s16 data_;    // transform data.
+
+  // Generic transform parameters.
+  uint32_t bits_ = 0;  // subsampling bits defining transform window.
+  uint32_t width_ = 0;
+  uint32_t height_ = 0;
+  WP2::Vector_s16 data_;
+
+  // Parameters when normalizing channels
+  std::array<WP2::Vector_u16, 4> normalize_channels_mapping_;
 };
 
 class Metadata {
@@ -120,7 +125,7 @@
                             WP2::Planef* bits_per_pixel);
   // Reads a transform of a certain size from the stream.
   WP2Status ReadTransform(uint32_t width, uint32_t height, TransformType type,
-                          Transform* transform);
+                          int index, Transform* transform);
 
   // Visual debug. 'symbol_type' can be a SymbolType for classical algorithm,
   // a Group4Mode for Group4 algorithm, or 0 for LZW algorithm.
diff --git a/src/dsp/lossless/decl_dsp.cc b/src/dsp/lossless/decl_dsp.cc
index 3ce6e0f..3e11f57 100644
--- a/src/dsp/lossless/decl_dsp.cc
+++ b/src/dsp/lossless/decl_dsp.cc
@@ -374,6 +374,28 @@
            width, dst);
 }
 
+void NormalizeChannelsInverseTransform_C(const Transform* const transform,
+                                         uint32_t y_start, uint32_t y_end,
+                                         const int16_t* src, int16_t* dst) {
+  const uint32_t width = transform->width_pic_;
+  if (transform->header_.normalize_channels_has_palette) {
+    for (const int16_t* const src_end = src + 4 * (y_end - y_start) * width;
+         src < src_end; src += 4, dst += 4) {
+      for (int c = 0; c < 4; ++c) {
+        dst[c] = transform->header_.normalize_channels_min[c] +
+                 transform->normalize_channels_mapping_[c][src[c]];
+      }
+    }
+  } else {
+    for (const int16_t* const src_end = src + 4 * (y_end - y_start) * width;
+         src < src_end; src += 4, dst += 4) {
+      for (int c = 0; c < 4; ++c) {
+        dst[c] = transform->header_.normalize_channels_min[c] + src[c];
+      }
+    }
+  }
+}
+
 //------------------------------------------------------------------------------
 // SSE version
 
@@ -493,6 +515,10 @@
     case TransformType::kColorIndexing:
       ColorIndexInverseTransform_C(transform, row_start, row_end, in, out);
       break;
+    case TransformType::kNormalizeChannels:
+      NormalizeChannelsInverseTransform_C(transform, row_start, row_end, in,
+                                          out);
+      break;
     case TransformType::kNum:
       assert(false);
   }
diff --git a/src/enc/lossless/losslessi_enc.cc b/src/enc/lossless/losslessi_enc.cc
index 75081bc..93b37e6 100644
--- a/src/enc/lossless/losslessi_enc.cc
+++ b/src/enc/lossless/losslessi_enc.cc
@@ -790,6 +790,99 @@
   return status;
 }
 
+WP2Status Encoder::ApplyNormalizeChannels(
+    const std::array<int32_t, 4>& minima_range,
+    const std::array<int32_t, 4>& maxima_range, TransformHeader& header,
+    WP2::ANSEnc& enc) {
+  const WP2::ANSDebugPrefix prefix(&enc, "normalize_channels");
+  std::array<int16_t, 4> min, max;
+  const uint32_t width = argb_buffer_.width;
+  const uint32_t height = argb_buffer_.height;
+  const int first_channel = has_alpha_ ? 0 : 1;
+  for (int c = first_channel; c < 4; c++) {
+    const int16_t* const row = argb_buffer_.GetRow(0);
+    min[c] = max[c] = row[c];
+  }
+  // Figure out the min and max values.
+  for (uint32_t y = 0; y < height; ++y) {
+    const int16_t* const row = argb_buffer_.GetRow(y);
+    for (uint32_t x = 0; x < width; ++x) {
+      for (int c = first_channel; c < 4; c++) {
+        if (row[4 * x + c] < min[c]) {
+          min[c] = row[4 * x + c];
+        } else if (row[4 * x + c] > max[c]) {
+          max[c] = row[4 * x + c];
+        }
+      }
+    }
+  }
+  // Store the minimum.
+  for (int c = first_channel; c < 4; ++c) {
+    header.normalize_channels_min[c] = min[c];
+    enc.PutRange(min[c], minima_range[c], maxima_range[c], "min");
+    max[c] -= min[c];
+  }
+
+  // TODO(vrabaud) This is a basic heuristic for now.
+  constexpr uint32_t kMinSizeForPalette = 256;
+  header.normalize_channels_has_palette = width * height >= kMinSizeForPalette;
+  if (!enc.PutBool(header.normalize_channels_has_palette, "has_palette")) {
+    // Remove the min value to only have positive values.
+    for (uint32_t y = 0; y < height; ++y) {
+      int16_t* const row = argb_buffer_.GetRow(y);
+      for (uint32_t x = 0; x < width; ++x) {
+        for (int c = first_channel; c < 4; c++) row[x + c] -= min[c];
+      }
+    }
+    return WP2_STATUS_OK;
+  }
+  // Compute whether a value is present.
+  std::array<WP2::Vector_u8, 4> present;
+  for (int c = first_channel; c < 4; ++c) {
+    WP2_CHECK_ALLOC_OK(present[c].resize(max[c] + 1));
+    std::fill(present[c].begin(), present[c].end(), 0);
+    for (uint32_t y = 0; y < height; ++y) {
+      int16_t* const row = argb_buffer_.GetRow(y);
+      for (uint32_t x = 0; x < width; ++x) {
+        row[4 * x + c] -= min[c];
+        present[c][row[4 * x + c]] = 1;
+      }
+    }
+  }
+  // Convert to palette.
+  WP2::Vector_u16 palette;
+  WP2::Vector_u16 mappings;
+  for (int c = first_channel; c < 4; ++c) {
+    WP2_CHECK_ALLOC_OK(palette.resize(max[c] + 1));
+    WP2_CHECK_ALLOC_OK(mappings.resize(max[c] + 1));
+    uint32_t mapping_size = 0;
+    for (uint32_t i = 0; i < present[c].size(); ++i) {
+      if (present[c][i]) {
+        palette[i] = mapping_size;
+        mappings[mapping_size] = i;
+        ++mapping_size;
+      } else {
+        palette[i] = std::numeric_limits<uint16_t>::max();
+      }
+    }
+    // Store the mapping.
+    const uint32_t range = 1 + maxima_range[c] - minima_range[c];
+    enc.PutRange(mapping_size, 1, range, "size");
+    header.normalize_channels_num_colors[c] = mapping_size;
+    WP2::VectorNoCtor<WP2::OptimizeArrayStorageStat> stats;
+    WP2_CHECK_ALLOC_OK(stats.resize(mapping_size));
+    StoreMapping(mappings.data(), mapping_size, range, stats.data(), &enc);
+    // Apply the mapping.
+    for (uint32_t y = 0; y < height; ++y) {
+      int16_t* const row = argb_buffer_.GetRow(y);
+      for (uint32_t x = c; x < 4 * width; x += 4) {
+        row[x] = palette[row[x]];
+      }
+    }
+  }
+  return WP2_STATUS_OK;
+}
+
 // -----------------------------------------------------------------------------
 
 // Allocates the memory for argb (W x H) buffer, 2 rows of context for
@@ -974,6 +1067,16 @@
             transform.cc_global_second_transform,
             transform.cc_global_third_transform, enc));
         break;
+      case TransformType::kNormalizeChannels: {
+        std::array<int32_t, 4> minima_range, maxima_range;
+        WP2_CHECK_STATUS(
+            GetARGBRanges(config->transforms, symbols_info_.SampleFormat(),
+                          minima_range, maxima_range, /*num_transforms=*/i));
+        WP2_CHECK_STATUS(ApplyNormalizeChannels(minima_range, maxima_range,
+                                                transform, *enc));
+        WP2_CHECK_STATUS(progress_local.AdvanceBy(1.));
+        break;
+      }
       case TransformType::kColorIndexing:
       case TransformType::kNum:
         assert(false);
diff --git a/src/enc/lossless/losslessi_enc.h b/src/enc/lossless/losslessi_enc.h
index 202fa72..46f59af 100644
--- a/src/enc/lossless/losslessi_enc.h
+++ b/src/enc/lossless/losslessi_enc.h
@@ -139,6 +139,9 @@
                                   TransformHeader::CCTransform third_transform,
                                   WP2::ANSEnc* enc);
   WP2Status ApplyColorIndexing(const CrunchConfig& config);
+  WP2Status ApplyNormalizeChannels(const std::array<int32_t, 4>& minima_range,
+                                   const std::array<int32_t, 4>& maxima_range,
+                                   TransformHeader& header, WP2::ANSEnc& enc);
   // Copy the original pic_ when need.
   WP2Status MakeInputImageCopy(const CrunchConfig& config);
 
diff --git a/src/wp2/format_constants.h b/src/wp2/format_constants.h
index 02d5882..4b08c90 100644
--- a/src/wp2/format_constants.h
+++ b/src/wp2/format_constants.h
@@ -372,6 +372,7 @@
   kYCoCgR,  // reversible YCoCg (unused)
   kSubtractGreen,
   kColorIndexing,
+  kNormalizeChannels,  // maps the channels to a range of [0, num_colors-1]
   kNum
 };
 // Possible image transform combinations that can be applied.
@@ -411,6 +412,9 @@
      {TransformType::kColorIndexing, TransformType::kPredictor,
       TransformType::kNum}},
     {EncodingAlgorithm::kWebP,
+     {TransformType::kNormalizeChannels, TransformType::kPredictor,
+      TransformType::kNum}},
+    {EncodingAlgorithm::kWebP,
      {TransformType::kPredictor, TransformType::kCrossColor,
       TransformType::kNum}},
     // Triplet.
diff --git a/tests/lossless/test_transforms.cc b/tests/lossless/test_transforms.cc
index a7553ef..cd208f5 100644
--- a/tests/lossless/test_transforms.cc
+++ b/tests/lossless/test_transforms.cc
@@ -127,4 +127,48 @@
             WP2_STATUS_INVALID_PARAMETER);
 }
 
+// Tests the cross color global transform.
+TEST(PerChannelIndexingTest, Pattern) {
+  constexpr char kSource1_64x48[] = "source1_64x48.png";
+  ArgbBuffer full_original(WP2_ARGB_32);
+  ASSERT_WP2_OK(ReadImage(testutil::GetTestDataPath(kSource1_64x48).c_str(),
+                          &full_original));
+  for (int test_case = 0; test_case < 2; ++test_case) {
+    ArgbBuffer original(WP2_ARGB_32);
+    if (test_case == 0) {
+      WP2_ASSERT_STATUS(original.SetView(full_original));
+    } else {
+      // Trigger the non-palette code.
+      WP2_ASSERT_STATUS(
+          original.SetView(full_original, WP2::Rectangle(0, 0, 10, 10)));
+    }
+
+    // Define the encoder config for lossless, including kCrossColorGlobal.
+    EncoderConfig encoder_config;
+    encoder_config.alpha_quality = 100;
+    encoder_config.quality = 100;
+    encoder_config.keep_unmultiplied = true;
+    encoder_config.lossless_config =
+        std::make_shared<WP2::EncoderConfig::LosslessCrunchConfig>();
+    WP2L::CrunchConfig &config = *encoder_config.lossless_config;
+    config.algorithm = WP2L::EncodingAlgorithm::kWebP;
+    config.transform_bits = config.histo_bits = 5;
+    config.lz77s_types_to_try[0] = WP2L::kLZ77Standard;
+    config.lz77s_types_to_try_size = 1;
+    config.transforms[0].type = WP2L::TransformType::kNormalizeChannels;
+    config.transforms[1].type = WP2L::TransformType::kPredictor;
+    config.transforms[2].type = WP2L::TransformType::kNum;
+
+    MemoryWriter memory_writer;
+    WP2_ASSERT_STATUS(Encode(original, &memory_writer, encoder_config));
+
+    DecoderConfig decoder_config;
+    ArgbBuffer decompressed(WP2_ARGB_32);
+    WP2_ASSERT_STATUS(Decode(memory_writer.mem_, memory_writer.size_,
+                             &decompressed, decoder_config));
+
+    EXPECT_TRUE(testutil::Compare(original, decompressed,
+                                  /*file_name=*/"cross_color_global"));
+  }
+}
 }  // namespace WP2