Add vanilla CALIC algorithm

Change-Id: I265de289292d8bd913b6ac04b92b9d94e6d83fc6
Reviewed-on: https://chromium-review.googlesource.com/c/codecs/libwebp2/+/6650922
Reviewed-by: Yannis Guyon <yguyon@google.com>
Tested-by: WebM Builds <builds@webmproject.org>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e8b749c..2fdb84a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -415,6 +415,8 @@
   ${CMAKE_CURRENT_SOURCE_DIR}/src/enc/trellis.cc
   ${CMAKE_CURRENT_SOURCE_DIR}/src/enc/writer_enc.cc
   # lossless
+  ${CMAKE_CURRENT_SOURCE_DIR}/src/common/lossless/calic.cc
+  ${CMAKE_CURRENT_SOURCE_DIR}/src/common/lossless/calic.h
   ${CMAKE_CURRENT_SOURCE_DIR}/src/common/lossless/color_cache.cc
   ${CMAKE_CURRENT_SOURCE_DIR}/src/common/lossless/color_cache.h
   ${CMAKE_CURRENT_SOURCE_DIR}/src/common/lossless/scp.cc
diff --git a/src/common/lossless/calic.cc b/src/common/lossless/calic.cc
new file mode 100644
index 0000000..aee3752
--- /dev/null
+++ b/src/common/lossless/calic.cc
@@ -0,0 +1,227 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -----------------------------------------------------------------------------
+//
+// Common CALIC code from "Context-Based, Adaptive, Lossless Image Coding"
+// IEEE TRANSACTIONS ON COMMUNICATIONS, VOL. 45, NO. 4, APRIL 1997
+// by Xiaolin Wu and Nasir Memon
+//
+// Author: Vincent Rabaud (vrabaud@google.com)
+
+#include "src/common/lossless/calic.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <cstdlib>
+
+#include "src/common/lossless/plane.h"
+#include "src/dsp/math.h"
+#include "src/utils/utils.h"
+#include "src/wp2/base.h"
+
+namespace WP2L {
+namespace calic {
+
+WP2Status CalicState::Init(uint32_t width, uint32_t height,
+                           Quantization quantization) {
+  WP2_CHECK_STATUS(WP2L::PlaneCodec::Init(width, height));
+  WP2_CHECK_ALLOC_OK(curr_diff_col_.resize(width));
+  WP2_CHECK_ALLOC_OK(curr_diff_row_.resize(width));
+  WP2_CHECK_ALLOC_OK(prev_diff_col_.resize(width));
+  WP2_CHECK_ALLOC_OK(prev_diff_row_.resize(width));
+
+  // Initialize the quantized delta table.
+  switch (quantization) {
+    case Quantization::kOriginal:
+      WP2_CHECK_ALLOC_OK(quantized_delta_.resize(141));
+      std::fill(&quantized_delta_[0], &quantized_delta_[5], 0);
+      std::fill(&quantized_delta_[5], &quantized_delta_[15], 1);
+      std::fill(&quantized_delta_[15], &quantized_delta_[25], 2);
+      std::fill(&quantized_delta_[25], &quantized_delta_[42], 3);
+      std::fill(&quantized_delta_[42], &quantized_delta_[60], 4);
+      std::fill(&quantized_delta_[60], &quantized_delta_[85], 5);
+      std::fill(&quantized_delta_[85], &quantized_delta_[140], 6);
+      quantized_delta_[140] = 7;
+      break;
+    case Quantization::kNum:
+      assert(false);
+      return WP2_STATUS_INVALID_PARAMETER;
+  }
+  if (quantized_delta_.back() + 1 != GetNumContexts(quantization)) {
+    return WP2_STATUS_INVALID_PARAMETER;
+  }
+
+  const int kSize = ((quantized_delta_.back() + 1) / 2) * (1 << 8);
+  WP2_CHECK_ALLOC_OK(mean_errors_.resize(kSize));
+  WP2_CHECK_ALLOC_OK(s_errors_.resize(kSize));
+  WP2_CHECK_ALLOC_OK(n_errors_.resize(kSize));
+  WP2_CHECK_STATUS(Reset());
+
+  return WP2_STATUS_OK;
+}
+
+void CalicState::SetMinMax(int16_t min_tpv, int16_t max_tpv) {
+  min_tpv_ = min_tpv;
+  max_tpv_ = max_tpv;
+  default_value_ = (min_tpv + max_tpv + 1) / 2;
+}
+
+uint8_t CalicState::GetNumContexts(Quantization quantization) {
+  switch (quantization) {
+    case Quantization::kOriginal:
+      return 8;
+      break;
+    case Quantization::kNum:
+    default:
+      assert(false);
+      return WP2_STATUS_INVALID_PARAMETER;
+  }
+}
+
+WP2Status CalicState::Reset() {
+  std::fill(mean_errors_.begin(), mean_errors_.end(), 0);
+  std::fill(s_errors_.begin(), s_errors_.end(), 0);
+  std::fill(n_errors_.begin(), n_errors_.end(), 0);
+  prev_pred_error_ = 0;
+  return WP2_STATUS_OK;
+}
+
+void CalicState::StartProcessingLine(uint32_t y, const int16_t* row) {
+  PlaneCodec::StartProcessingLine(y, row);
+
+  curr_diff_row_.swap(prev_diff_row_);
+  curr_diff_col_.swap(prev_diff_col_);
+}
+
+int16_t CalicState::PredictGAP(uint16_t dh, uint16_t dv, int16_t w, int16_t n,
+                               int16_t ne, int16_t nw) {
+  int16_t pred;
+  if (dv - dh > 80) {
+    // Sharp horizontal edge.
+    pred = w;
+  } else if (dv - dh < -80) {
+    // Sharp vertical edge.
+    pred = n;
+  } else {
+    int16_t p = (w + n) / 2 + (ne - nw) / 4;
+    if (dv - dh > 32) {
+      // Horizontal edge.
+      p = (p + w) / 2;
+    } else if (dv - dh > 8) {
+      // Weak horizontal edge.
+      p = (3 * p + w) / 4;
+    } else if (dv - dh < -32) {
+      // Vertical edge.
+      p = (p + n) / 2;
+    } else if (dv - dh < -8) {
+      // Weak vertical edge.
+      p = (3 * p + n) / 4;
+    }
+    pred = p;
+  }
+  return pred;
+}
+
+void CalicState::Predict(uint32_t x, int16_t& prediction, uint8_t& ctxt,
+                         bool& is_mean_error_negative) {
+  // Section 3: GAP, Gradient-Adjusted Prediction.
+  uint16_t dh = 0, dv = 0;
+  // Edges are not properly defined in the paper.
+  if (x > 0) {
+    dh = curr_diff_col_[x - 1];
+    dv = curr_diff_row_[x - 1];
+  } else {
+    dh = dv = 0;
+  }
+  if (y_ > 0) {
+    dh += prev_diff_col_[x];
+    dv += prev_diff_row_[x];
+    if (x + 1 < width_) {
+      dh += prev_diff_col_[x + 1];
+      dv += prev_diff_row_[x + 1];
+    }
+  }
+
+  // Edge conditions are not specified by the paper so we take the closest
+  // physical value, or the default one if none.
+  const int16_t w = x > 0 ? row_[x - 1] : y_ > 0 ? row_p_[0] : default_value_;
+  const int16_t n = y_ > 0 ? row_p_[x] : w;
+  const int16_t ne = x + 1 < width_ && y_ > 0 ? row_p_[x + 1] : n;
+  const int16_t nw = x > 0 && y_ > 0 ? row_p_[x - 1] : n;
+  pred_ = PredictGAP(dh, dv, w, n, ne, nw);
+  pred_ = std::clamp(pred_, min_tpv_, max_tpv_);
+
+  // Section 4: Coding Context Selection and Quantization.
+  static constexpr int kA = 1, kB = 1, kC = 2;
+  const size_t delta = kA * dh + kB * dv + kC * std::abs(prev_pred_error_);
+  const uint8_t quantized_delta = delta >= quantized_delta_.size()
+                                      ? quantized_delta_.back()
+                                      : quantized_delta_[delta];
+
+  // Section 5.A: Formation and Quantization of Contexts.
+  const int16_t nn = y_ > 1 ? row_pp_[x] : n;
+  const int16_t ww = x > 1 ? row_[x - 2] : w;
+  uint8_t b = 0;
+  b |= (n < pred_) << 0;
+  b |= (w < pred_) << 1;
+  b |= (nw < pred_) << 2;
+  b |= (ne < pred_) << 3;
+  b |= (nn < pred_) << 4;
+  b |= (ww < pred_) << 5;
+  b |= ((2 * n - nn) < pred_) << 6;
+  b |= ((2 * w - ww) < pred_) << 7;
+  compound_context_ = ((quantized_delta / 2) << 8) | b;
+
+  // Section 5.D: Error Feedback.
+  const auto mean_error = mean_errors_[compound_context_];
+  is_mean_error_negative = mean_error < 0;
+  const int16_t rounded_abs_residual =
+      WP2::RightShiftRound(std::abs(mean_error), kPrecisionBits);
+  prediction =
+      pred_ + (mean_error >= 0 ? rounded_abs_residual : -rounded_abs_residual);
+  prediction = std::clamp(prediction, min_tpv_, max_tpv_);
+
+  ctxt = quantized_delta;
+}
+
+void CalicState::Update(uint32_t x, int16_t tpv) {
+  if (x > 0) {
+    curr_diff_col_[x] = std::abs(tpv - row_[x - 1]);
+  } else {
+    curr_diff_col_[0] = 0;
+  }
+  if (y_ > 0) {
+    curr_diff_row_[x] = std::abs(row_[x] - row_p_[x]);
+  } else {
+    curr_diff_row_[x] = 0;
+  }
+  prev_pred_error_ = tpv - pred_;
+
+  // Section 5.C: Estimation of Error Magnitude Within a Context.
+  // Use the final prediction and not the original one (cf section 5.D).
+  s_errors_[compound_context_] += tpv - pred_;
+  ++n_errors_[compound_context_];
+  if (n_errors_[compound_context_] >= 128) {
+    s_errors_[compound_context_] /= 2;
+    n_errors_[compound_context_] = 64;
+  }
+
+  mean_errors_[compound_context_] = WP2::DivRound(
+      static_cast<int32_t>(s_errors_[compound_context_]) << kPrecisionBits,
+      static_cast<int32_t>(n_errors_[compound_context_]));
+}
+
+}  // namespace calic
+}  // namespace WP2L
diff --git a/src/common/lossless/calic.h b/src/common/lossless/calic.h
new file mode 100644
index 0000000..a436442
--- /dev/null
+++ b/src/common/lossless/calic.h
@@ -0,0 +1,89 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -----------------------------------------------------------------------------
+//
+// Implementation of the CALIC algorithm.
+//
+// Author: Vincent Rabaud (vrabaud@google.com)
+
+#ifndef WP2_COMMON_LOSSLESS_CALIC_H_
+#define WP2_COMMON_LOSSLESS_CALIC_H_
+
+#include <cstdint>
+
+#include "src/common/lossless/plane.h"
+#include "src/utils/vector.h"
+#include "src/wp2/base.h"
+
+namespace WP2L {
+namespace calic {
+
+// Class implementing the CALIC algorithm from Xiaolin Wu and Nasir Memon.
+class CalicState : public ::WP2L::PlaneCodec {
+ public:
+  // This is part of the format.
+  enum class Quantization { kOriginal, kNum };
+
+  WP2Status Init(uint32_t width, uint32_t height, Quantization quantization);
+  WP2Status Reset() override;
+  void StartProcessingLine(uint32_t y, const int16_t* row) override;
+
+  // Sets the minimum and maximum true pixel inclusive values in the image.
+  void SetMinMax(int16_t min_tpv, int16_t max_tpv);
+  // Get the number of contexts for the given quantization method.
+  static uint8_t GetNumContexts(Quantization quantization);
+  // Predicts the pixel value at position x, as well as the context `ctxt` and
+  // whether the mean error is negative.
+  void Predict(uint32_t x, int16_t& prediction, uint8_t& ctxt,
+               bool& is_mean_error_negative);
+  // Updates the internals after we know the true pixel value `tpv` at pixel x.
+  void Update(uint32_t x, int16_t tpv);
+
+ private:
+  using ::WP2L::PlaneCodec::Init;
+  // Number of bits used to perform operations with.
+  static inline constexpr int kPrecisionBits = 8;
+
+  // Predicts the pixel value using the GAP method. The horizontal and vertical
+  // gradients are given as well as the context of the 4 neighbors.
+  static int16_t PredictGAP(uint16_t dh, uint16_t dv, int16_t w, int16_t n,
+                            int16_t ne, int16_t nw);
+
+  // The minimum and maximum true pixel values in the image.
+  int16_t min_tpv_, max_tpv_;
+  // The default value to use when there is no information: the mean of the
+  // minimum and maximum true pixel values.
+  int16_t default_value_;
+
+  // The current prediction.
+  int16_t pred_;
+  // The previous error between the prediction and the true pixel value.
+  int16_t prev_pred_error_;
+  // Vectors containing the differences between the current and previous cols or
+  // rows, in the current or previous row.
+  WP2::Vector_u16 curr_diff_col_, curr_diff_row_, prev_diff_col_,
+      prev_diff_row_;
+  // Accumulator of the errors with precision kPrecisionBits.
+  WP2::Vector_s16 s_errors_;
+  WP2::Vector_u8 n_errors_;
+  // The mean error, with precision kPrecisionBits.
+  WP2::Vector_s32 mean_errors_;
+  WP2::Vector_u8 quantized_delta_;
+  uint16_t compound_context_;
+};
+
+}  // namespace calic
+}  // namespace WP2L
+
+#endif  // WP2_COMMON_LOSSLESS_CALIC_H_
diff --git a/src/common/lossless/plane.h b/src/common/lossless/plane.h
index b2851f2..c3aff20 100644
--- a/src/common/lossless/plane.h
+++ b/src/common/lossless/plane.h
@@ -66,7 +66,7 @@
 
 class PlaneCodec {
  public:
-  enum class Method { kScpClassical, kScpJxl, kNum };
+  enum class Method { kScpClassical, kScpJxl, kCalic, kNum };
 
   virtual ~PlaneCodec() = default;
 
diff --git a/src/dec/lossless/losslessi_dec.cc b/src/dec/lossless/losslessi_dec.cc
index 0e6c875..27e2216 100644
--- a/src/dec/lossless/losslessi_dec.cc
+++ b/src/dec/lossless/losslessi_dec.cc
@@ -632,7 +632,8 @@
     WP2_CHECK_STATUS(DecodeGroup4(width, last_row, bits_per_pixel, &src));
   } else if (encoding_algorithm_ == EncodingAlgorithm::kLZW) {
     WP2_CHECK_STATUS(DecodeLZW(width, last_row, bits_per_pixel, &src));
-  } else if (encoding_algorithm_ == EncodingAlgorithm::kSCP) {
+  } else if (encoding_algorithm_ == EncodingAlgorithm::kSCP ||
+             encoding_algorithm_ == EncodingAlgorithm::kCALIC) {
     WP2_CHECK_STATUS(
         DecodePlaneCodec(width, last_row, scp_global_, bits_per_pixel, &src));
   } else {
@@ -1143,7 +1144,8 @@
     }
     dec_->PopDebugPrefix();
     return WP2_STATUS_OK;
-  } else if (encoding_algorithm_ == EncodingAlgorithm::kSCP) {
+  } else if (encoding_algorithm_ == EncodingAlgorithm::kSCP ||
+             encoding_algorithm_ == EncodingAlgorithm::kCALIC) {
     for (int c = gparams_->has_alpha_ ? 0 : 1; c < 4; ++c) {
       scp_global_.max_tpv[c] =
           c == 0 ? WP2::kAlphaMax
diff --git a/src/dec/lossless/losslessi_dec.h b/src/dec/lossless/losslessi_dec.h
index 359f501..884f421 100644
--- a/src/dec/lossless/losslessi_dec.h
+++ b/src/dec/lossless/losslessi_dec.h
@@ -23,6 +23,7 @@
 #include <array>
 #include <cstdint>
 
+#include "src/common/lossless/calic.h"
 #include "src/common/lossless/color_cache.h"
 #include "src/common/lossless/plane.h"
 #include "src/common/lossless/scp.h"
@@ -86,6 +87,7 @@
   uint32_t maxerr_shift;
   int jxl_header_index;
   scp::JxlHeader jxl_header;
+  calic::CalicState::Quantization calic_quantization;
 };
 
 class Decoder {
diff --git a/src/dec/lossless/plane_dec.cc b/src/dec/lossless/plane_dec.cc
index 34c9a4a..b002cef 100644
--- a/src/dec/lossless/plane_dec.cc
+++ b/src/dec/lossless/plane_dec.cc
@@ -24,6 +24,7 @@
 #include <cstdio>
 #include <cstring>
 
+#include "src/common/lossless/calic.h"
 #include "src/common/lossless/plane.h"
 #include "src/common/lossless/scp.h"
 #include "src/common/symbols.h"
@@ -38,6 +39,69 @@
 
 namespace WP2L {
 namespace scp {
+////////////////////////////////////////////////////////////////////////////////
+
+class CalicDecState : public ::WP2L::calic::CalicState {
+ public:
+  int16_t CalcTPVFromPredictionAndDistance(int16_t p,
+                                           bool is_mean_error_negative,
+                                           int16_t d, int16_t min,
+                                           int16_t max) {
+    // Compute the maximum error for which the sign is not forced from the
+    // prediction. E.g., if the prediction is 10 and the value is in [0, 100],
+    // the signed error can only be in [-10, 10]. Beyond that, the error is
+    // positive in [0, 80].
+    const int16_t max_signed_error = std::min(max - p, p - min);
+    int16_t res;
+    if (d <= 2 * max_signed_error) {
+      res = d % 2 == 0 ? -d / 2 : (d + 1) / 2;
+      // End of section 5.D: Error Feedback.
+      if (is_mean_error_negative) res = -res;
+      p += res;
+    } else {
+      res = d - max_signed_error;
+      if (p + res > max) {
+        p -= res;
+      } else {
+        p += res;
+      }
+    }
+    return p;
+  }
+
+  WP2Status DecompressPlane(int plane_to_decompress, const ScpGlobal& global,
+                            WP2::SymbolReader& sr, ImagePlanes& img) {
+    const int16_t min_tpv = global.min_tpv[plane_to_decompress];
+    const int16_t max_tpv = global.max_tpv[plane_to_decompress];
+    SetMinMax(min_tpv, max_tpv);
+
+    for (size_t y = 0; y < img.height(); ++y) {
+      int16_t* row = img.PlaneRow(plane_to_decompress, y);
+      StartProcessingLine(y, row);
+
+      for (size_t x = 0; x < img.width(); ++x) {
+        // Predict.
+        uint8_t ctxt;
+        bool is_mean_error_negative;
+        int16_t prediction;
+        Predict(x, prediction, ctxt, is_mean_error_negative);
+
+        // Compute the true pixel value.
+        const int16_t residual =
+            sr.Read(/*sym=*/plane_to_decompress, ctxt, "sym");
+
+        row[x] = CalcTPVFromPredictionAndDistance(
+            prediction, is_mean_error_negative, residual, min_tpv, max_tpv);
+        WP2_CHECK_OK(row[x] >= min_tpv && row[x] <= max_tpv,
+                     WP2_STATUS_BITSTREAM_ERROR);
+
+        Update(x, row[x]);
+      }
+    }
+
+    return WP2_STATUS_OK;
+  }
+};
 
 ////////////////////////////////////////////////////////////////////////////////
 
@@ -194,6 +258,14 @@
       }
       num_contexts = scp::JXLState::GetNumContexts();
       break;
+    case PlaneCodec::Method::kCalic:
+      global->calic_quantization =
+          static_cast<calic::CalicState::Quantization>(dec_->ReadRValue(
+              static_cast<int>(calic::CalicState::Quantization::kNum),
+              "quantization"));
+      num_contexts =
+          calic::CalicState::GetNumContexts(global->calic_quantization);
+      break;
     case PlaneCodec::Method::kNum:
     default:
       assert(false);
@@ -251,6 +323,13 @@
                                     state, hdr_.sr_, src));
         break;
       }
+      case PlaneCodec::Method::kCalic: {
+        scp::CalicDecState state;
+        WP2_CHECK_STATUS(state.Init(width, height, global.calic_quantization));
+        WP2_CHECK_STATUS(Decompress(global, width, height, gparams_->has_alpha_,
+                                    state, hdr_.sr_, src));
+        break;
+      }
       case PlaneCodec::Method::kNum:
       default:
         assert(false);
diff --git a/src/enc/lossless/analysisl_enc.cc b/src/enc/lossless/analysisl_enc.cc
index 3dc1db4..9e7e990 100644
--- a/src/enc/lossless/analysisl_enc.cc
+++ b/src/enc/lossless/analysisl_enc.cc
@@ -531,6 +531,48 @@
     return WP2_STATUS_OK;
   }
 
+  // TODO(vrabaud): Enable for all efforts once it gets better.
+  if (WP2Formatbpc(pic.format()) == 8 &&
+      !WP2IsPremultiplied(encoder.pic_.format()) &&
+      encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kCALIC) {
+    // Add a few more than the predefined ones.
+    std::array<std::array<uint32_t, 2>, kCommonCrossColorIndices.size() + 4>
+        y_uv_indices;
+    for (uint32_t i = 0; i < kCommonCrossColorIndices.size(); ++i) {
+      y_uv_indices[i] = {kCommonCrossColorIndices[i].first,
+                         kCommonCrossColorIndices[i].second};
+    }
+    // Taken from Table VI in "Multiplierless reversible colour transforms
+    // and their automatic selection for image data compression" by Tilo Strutz.
+    y_uv_indices[kCommonCrossColorIndices.size() + 0] = {4, 4};
+    y_uv_indices[kCommonCrossColorIndices.size() + 1] = {7, 7};
+    y_uv_indices[kCommonCrossColorIndices.size() + 2] = {9, 10};
+    y_uv_indices[kCommonCrossColorIndices.size() + 3] = {1, 10};
+
+    for (const auto& indices : y_uv_indices) {
+      WP2_CHECK_ALLOC_OK(configs->push_back(CrunchConfig()));
+      auto& config = configs->back();
+      WP2_CHECK_STATUS(FindEncodingRecipe(
+          WP2L::EncodingAlgorithm::kCALIC, &config,
+          TransformType::kCrossColorGlobal, TransformType::kNormalizeChannels));
+      config.transforms[0].cc_global_y_index = indices[0];
+      config.transforms[0].cc_global_uv_index = indices[1];
+      config.use_premultiplied = can_premultiply;
+    }
+    // Add with no channel decorrelation.
+    WP2_CHECK_ALLOC_OK(configs->push_back(CrunchConfig()));
+    auto& config = configs->back();
+    WP2_CHECK_STATUS(FindEncodingRecipe(WP2L::EncodingAlgorithm::kCALIC,
+                                        &config,
+                                        TransformType::kNormalizeChannels));
+    config.use_premultiplied = can_premultiply;
+
+    if (encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kCALIC) {
+      return WP2_STATUS_OK;
+    }
+  }
+
+  // TODO(vrabaud): Disable this once CALIC gets better.
   if (WP2Formatbpc(pic.format()) == 8 &&
       !WP2IsPremultiplied(encoder.pic_.format()) &&
       (encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kSCP ||
diff --git a/src/enc/lossless/losslessi_enc.cc b/src/enc/lossless/losslessi_enc.cc
index f129deb..b473c04 100644
--- a/src/enc/lossless/losslessi_enc.cc
+++ b/src/enc/lossless/losslessi_enc.cc
@@ -485,14 +485,22 @@
   }
 
   // Check if SCP is used.
+  std::array<int32_t, 4> minima_range, maxima_range;
   if (config.algorithm == EncodingAlgorithm::kSCP) {
-    std::array<int32_t, 4> minima_range, maxima_range;
     WP2_CHECK_STATUS(GetARGBRanges(config.transforms,
                                    symbols_info_init.SampleFormat(),
                                    minima_range, maxima_range));
-    WP2_CHECK_STATUS(PlaneCodecEncode(argb_buffer_, config_.effort,
-                                      minima_range, maxima_range, *enc_init,
-                                      encode_info));
+    WP2_CHECK_STATUS(ScpEncode(argb_buffer_, config_.effort, minima_range,
+                               maxima_range, *enc_init, encode_info));
+    WP2_CHECK_STATUS(progress.AdvanceBy(1.));
+    return WP2_STATUS_OK;
+  }
+  if (config.algorithm == EncodingAlgorithm::kCALIC) {
+    WP2_CHECK_STATUS(GetARGBRanges(config.transforms,
+                                   symbols_info_init.SampleFormat(),
+                                   minima_range, maxima_range));
+    WP2_CHECK_STATUS(CalicEncode(argb_buffer_, config_.effort, minima_range,
+                                 maxima_range, *enc_init, encode_info));
     WP2_CHECK_STATUS(progress.AdvanceBy(1.));
     return WP2_STATUS_OK;
   }
@@ -1135,6 +1143,9 @@
   if (config->algorithm == EncodingAlgorithm::kSCP) {
     return WP2_STATUS_OK;
   }
+  if (config->algorithm == EncodingAlgorithm::kCALIC) {
+    return WP2_STATUS_OK;
+  }
 
   if (has_alpha_ && image_is_premultiplied_) {
     // The lossless encoder may use unmultiplied samples for better compression,
diff --git a/src/enc/lossless/losslessi_enc.h b/src/enc/lossless/losslessi_enc.h
index abe9ef4..19047ff 100644
--- a/src/enc/lossless/losslessi_enc.h
+++ b/src/enc/lossless/losslessi_enc.h
@@ -314,10 +314,18 @@
 //------------------------------------------------------------------------------
 // Self-correcting predictor.
 
-WP2Status PlaneCodecEncode(const Buffer_s16& img, int effort,
-                           const std::array<int32_t, 4>& minima_range,
-                           const std::array<int32_t, 4>& maxima_range,
-                           WP2::ANSEnc& enc, EncodeInfo* encode_info);
+WP2Status ScpEncode(const Buffer_s16& img, int effort,
+                    const std::array<int32_t, 4>& minima_range,
+                    const std::array<int32_t, 4>& maxima_range,
+                    WP2::ANSEnc& enc, EncodeInfo* encode_info);
+
+//------------------------------------------------------------------------------
+// CALIC.
+
+WP2Status CalicEncode(const Buffer_s16& img, int effort,
+                      const std::array<int32_t, 4>& minima_range,
+                      const std::array<int32_t, 4>& maxima_range,
+                      WP2::ANSEnc& enc, EncodeInfo* encode_info);
 
 }  // namespace WP2L
 
diff --git a/src/enc/lossless/plane_enc.cc b/src/enc/lossless/plane_enc.cc
index 2cf5622..c890559 100644
--- a/src/enc/lossless/plane_enc.cc
+++ b/src/enc/lossless/plane_enc.cc
@@ -28,6 +28,7 @@
 #include <limits>
 #include <type_traits>
 
+#include "src/common/lossless/calic.h"
 #include "src/common/lossless/plane.h"
 #include "src/common/lossless/scp.h"
 #include "src/dsp/lossless/encl_dsp.h"
@@ -61,6 +62,76 @@
 
 ////////////////////////////////////////////////////////////////////////////////
 
+// CALIC based encoder.
+class CalicEncState : public ::WP2L::calic::CalicState {
+ public:
+  struct Config {
+    Quantization quantization = Quantization::kOriginal;
+  };
+
+  void SetConfig(const Config& config) { config_ = config; }
+
+  void EncodeHeader(WP2::ANSEncBase& enc) {
+    enc.PutRValue(static_cast<uint32_t>(config_.quantization),
+                  static_cast<uint32_t>(Quantization::kNum), "quantization");
+  }
+
+  // p = prediction, v = true pixel value, m = maximum true pixel value
+  int16_t CalcDistanceFromPredictionAndTPV(int16_t p,
+                                           bool is_mean_error_negative,
+                                           int16_t v, int16_t min,
+                                           int16_t max) {
+    const int16_t max_signed_error = std::min(max - p, p - min);
+    int16_t res = v - p;
+    int16_t d;
+    if (std::abs(res) <= max_signed_error) {
+      // End of section 5.D: Error Feedback.
+      if (is_mean_error_negative) res = -res;
+      d = res > 0 ? 2 * res - 1 : -2 * res;
+    } else {
+      d = std::abs(res) + max_signed_error;
+    }
+    return d;
+  }
+
+  WP2Status CompressPlane(const ImagePlanes& img, int plane_to_compress,
+                          int16_t min_tpv, int16_t max_tpv,
+                          Encoding* encoding) {
+    encoding->tokens.clear();
+    SetMinMax(min_tpv, max_tpv);
+
+    for (size_t y = 0; y < img.height(); ++y) {
+      const int16_t* row = img.PlaneRow(plane_to_compress, y);
+      StartProcessingLine(y, row);
+
+      for (size_t x = 0; x < img.width(); ++x) {
+        uint8_t ctxt;
+        bool is_mean_error_negative;
+        int16_t prediction;
+        Predict(x, prediction, ctxt, is_mean_error_negative);
+
+        WP2_CHECK_ALLOC_OK(encoding->tokens.push_back(Token(
+            ctxt,
+            CalcDistanceFromPredictionAndTPV(prediction, is_mean_error_negative,
+                                             row[x], min_tpv, max_tpv))));
+
+        Update(x, row[x]);
+      }
+    }
+
+    return WP2_STATUS_OK;
+  }
+
+  int GetNumContexts() const {
+    return calic::CalicState::GetNumContexts(config_.quantization);
+  }
+
+ private:
+  Config config_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
 // Encoder for the classical SCP.
 // SetConfig must be called after Init to be properly usable.
 class ClassicalEncState : public ClassicalState {
@@ -327,19 +398,19 @@
 
   return WP2_STATUS_OK;
 }
-}  // namespace scp
 
-WP2Status PlaneCodecEncode(const Buffer_s16& img, int effort,
-                           const std::array<int32_t, 4>& minima_range,
-                           const std::array<int32_t, 4>& maxima_range,
-                           WP2::ANSEnc& enc, EncodeInfo* encode_info) {
+static WP2Status PrepareData(const Buffer_s16& img,
+                             const std::array<int32_t, 4>& minima_range,
+                             const std::array<int32_t, 4>& maxima_range,
+                             std::array<int16_t, 4>& min_tpv,
+                             std::array<int16_t, 4>& max_tpv, WP2::ANSEnc& enc,
+                             WP2L::ImagePlanes& img_planes) {
   if (img.channel_bits != 8) return WP2_STATUS_INVALID_PARAMETER;
 
   const size_t width = img.width;
   const size_t height = img.height;
 
   // The code modifies the image for palette so must copy for now.
-  WP2L::ImagePlanes img_planes;
   WP2_CHECK_STATUS(img_planes.Create(width, height, img.has_alpha));
   for (int c = (img.has_alpha ? 0 : 1); c < 4; c++) {
     for (size_t y = 0; y < height; ++y) {
@@ -349,7 +420,6 @@
       }
     }
   }
-  std::array<int16_t, 4> min_tpv, max_tpv;
   FindExtrema(img.GetRow(0), width, height, img.has_alpha, min_tpv, max_tpv);
   enc.AddDebugPrefix("GlobalHeader");
   for (int c = img.has_alpha ? 0 : 1; c < 4; ++c) {
@@ -358,6 +428,23 @@
   }
   enc.PopDebugPrefix();
 
+  return WP2_STATUS_OK;
+}
+
+}  // namespace scp
+
+WP2Status ScpEncode(const Buffer_s16& img, int effort,
+                    const std::array<int32_t, 4>& minima_range,
+                    const std::array<int32_t, 4>& maxima_range,
+                    WP2::ANSEnc& enc, EncodeInfo* encode_info) {
+  const size_t width = img.width;
+  const size_t height = img.height;
+
+  WP2L::ImagePlanes img_planes;
+  std::array<int16_t, 4> min_tpv, max_tpv;
+  WP2_CHECK_STATUS(scp::PrepareData(img, minima_range, maxima_range, min_tpv,
+                                    max_tpv, enc, img_planes));
+
   float cost_best = std::numeric_limits<float>::max();
   WP2::ANSDictionaries dicts;
 
@@ -481,4 +568,26 @@
   return WP2_STATUS_OK;
 }
 
+WP2Status CalicEncode(const Buffer_s16& img, int effort,
+                      const std::array<int32_t, 4>& minima_range,
+                      const std::array<int32_t, 4>& maxima_range,
+                      WP2::ANSEnc& enc, EncodeInfo* encode_info) {
+  WP2L::ImagePlanes img_planes;
+  std::array<int16_t, 4> min_tpv, max_tpv;
+  WP2_CHECK_STATUS(scp::PrepareData(img, minima_range, maxima_range, min_tpv,
+                                    max_tpv, enc, img_planes));
+  WP2::ANSDictionaries dicts;
+
+  // Encode using CALIC.
+  scp::CalicEncState state;
+  scp::CalicEncState::Config calic_config_best;
+  calic_config_best.quantization = calic::CalicState::Quantization::kOriginal;
+  state.SetConfig(calic_config_best);
+  WP2_CHECK_STATUS(
+      state.Init(img.width, img.height, calic_config_best.quantization));
+  WP2_CHECK_STATUS(Compress(img_planes, min_tpv, max_tpv, effort, enc, state,
+                            dicts, encode_info));
+  return WP2_STATUS_OK;
+}
+
 }  // namespace WP2L
diff --git a/src/wp2/format_constants.h b/src/wp2/format_constants.h
index e082ac1..ad78a83 100644
--- a/src/wp2/format_constants.h
+++ b/src/wp2/format_constants.h
@@ -370,6 +370,7 @@
   kLZW,
   // Self-correcting predictor.
   kSCP,
+  kCALIC,
   kNum,
   kDefault = kNum
 };
@@ -405,6 +406,12 @@
      {TransformType::kNum, TransformType::kNum, TransformType::kNum}},
     {EncodingAlgorithm::kLZW,
      {TransformType::kNum, TransformType::kNum, TransformType::kNum}},
+    {EncodingAlgorithm::kCALIC,
+     {TransformType::kCrossColorGlobal, TransformType::kNormalizeChannels,
+      TransformType::kNum}},
+    {EncodingAlgorithm::kCALIC,
+     {TransformType::kNormalizeChannels, TransformType::kNum,
+      TransformType::kNum}},
     {EncodingAlgorithm::kSCP,
      {TransformType::kNormalizeChannels, TransformType::kNum,
       TransformType::kNum}},
diff --git a/tests/lossless/test_plane_codecs.cc b/tests/lossless/test_plane_codecs.cc
index dc4fc69..b751a1d 100644
--- a/tests/lossless/test_plane_codecs.cc
+++ b/tests/lossless/test_plane_codecs.cc
@@ -104,7 +104,8 @@
                                      std::array<int, 2>{555, 577},
                                      std::array<int, 2>{577, 555},
                                      std::array<int, 2>{5, 800}),
-                     testing::Values(WP2L::EncodingAlgorithm::kSCP)));
+                     testing::Values(WP2L::EncodingAlgorithm::kSCP,
+                                     WP2L::EncodingAlgorithm::kCALIC)));
 
 class LosslessTestFile : public testing::TestWithParam<
                              std::tuple<std::string, WP2L::EncodingAlgorithm>> {
@@ -126,7 +127,8 @@
     LosslessTestInstantiation, LosslessTestFile,
     testing::Combine(testing::Values("background.png", "logo.png",
                                      "source1_64x48.png"),
-                     testing::Values(WP2L::EncodingAlgorithm::kSCP)));
+                     testing::Values(WP2L::EncodingAlgorithm::kSCP,
+                                     WP2L::EncodingAlgorithm::kCALIC)));
 
 ////////////////////////////////////////////////////////////////////////////////