chromium/chromeos/ash/services/recording/lzw_pixel_color_indices_writer.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/services/recording/lzw_pixel_color_indices_writer.h"

#include <cstdint>

#include "chromeos/ash/services/recording/gif_file_writer.h"

namespace recording {

namespace {

// The GIF specs specify a maximum of 12 bits per LZW compression code, which
// means that the maximum possible value for the codes is (2 ^ 12) - 1, which is
// equal to 4095, and the maximum number of codes is (2 ^ 12 = 4096).
constexpr LzwCode kMaxNumberOfLzwCodes = 1 << 12;

// The maximum number of bytes contained in a data sub block of the LZW encoded
// output.
constexpr uint8_t kMaxBytesPerDataSubBlock = 0xFF;

}  // namespace

LzwPixelColorIndicesWriter::LzwPixelColorIndicesWriter(
    GifFileWriter* gif_file_writer)
    : gif_file_writer_(gif_file_writer) {
  code_table_.reserve(kMaxNumberOfLzwCodes);
  byte_stream_buffer_.reserve(kMaxBytesPerDataSubBlock);
}

LzwPixelColorIndicesWriter::~LzwPixelColorIndicesWriter() = default;

void LzwPixelColorIndicesWriter::EncodeAndWrite(
    const ColorIndices& pixel_color_indices,
    uint8_t color_bit_depth) {
  if (pixel_color_indices.empty()) {
    return;
  }

  // Start the process with an empty table.
  code_table_.clear();

  // The `color_bit_depth` is the minimum number of bits needed to represent all
  // the indices in `pixel_color_indices`. For example, if we have 4 colors,
  // with indices 0, 1, 2, 3, The minimum number of bits needed to represent the
  // largest index (3) is 2 bits. This is the `color_bit_depth` and it's also
  // the value of the LZW minimum code size, and is required to be written to
  // the file as the first byte of the image data block.
  const LzwCode lzw_minimum_code_size = color_bit_depth;
  gif_file_writer_->WriteByte(lzw_minimum_code_size);

  // Remember, we're not writing color indices to the file, but rather LZW
  // compression codes. So we map each color index to an LZW code. So in the
  // above example with 4 colors, our LZW codes will be as follows:
  //
  // +-------------+----------+
  // | Color Index | LZW Code |
  // +-------------+----------+
  // |      0      |     0    |
  // |      1      |     1    |
  // |      2      |     2    |
  // |      3      |     3    |
  // +-------------+----------+
  //
  // We add 2 more control codes that act as directives to the decoder, a clear
  // code, and an End-of-Information (EoI) code. They're assigned the next
  // available codes 4, and 5 respectively.
  const LzwCode clear_code = 1 << color_bit_depth;
  const LzwCode eoi_code = clear_code + 1;

  // EoI code is currently the maximum LZW code used, and requires the minimum
  // number of bits `color_bit_depth + 1` to be represented in binary, which is
  // 3 bits in our above example.
  uint8_t current_code_bit_size = color_bit_depth + 1;

  // The next available unassigned LZW code is the value after the EoI code,
  // which is 6 in our above example.
  LzwCode next_available_code = eoi_code + 1;

  // First, output the `clear_code` with the `current_code_bit_size` to start
  // the compression process.
  AppendCodeToStreamBuffer(clear_code, current_code_bit_size);

  // We start at the very first pixel color index, and we use the value of that
  // index as the current LZW code (remember from the above table that color
  // indices map to LZW codes that have the same values).
  LzwCode current_code = pixel_color_indices[0];

  // Then we start iterating at the following index ...
  for (size_t i = 1; i < pixel_color_indices.size(); ++i) {
    // ... asking if the current sequence of indices represented by the
    // `current_code` when it gets appended with `next_color_index`, does it map
    // to an existing LZW code in the table?
    const ColorIndex next_color_index = pixel_color_indices[i];
    LzwCode& code_in_table =
        code_table_[current_code].next_index_to_code[next_color_index];
    if (code_in_table) {
      // If yes, then `code_in_table` is a valid one, which means we have seen
      // this pattern of indices before and mapped it to a code in the table.
      // So we use that code we just found and the current code to represent the
      // new pattern that results from appending `next_color_index` to the
      // pattern of indices we had before.
      current_code = code_in_table;
    } else {
      // If no, then this is the first time ever we see this new pattern, so we
      // output `current_code` since we're starting now a new pattern for a
      // sequence of indices that begin with `next_color_index`.
      AppendCodeToStreamBuffer(current_code, current_code_bit_size);

      // We also assign a new LZW code to that pattern we didn't see before.
      // Note that `code_in_table` is a reference, so changing its value changes
      // the entry in the table.
      code_in_table = next_available_code++;
      current_code = next_color_index;

      // If the code we just added can no longer be represented as a binary
      // value using the current minimum number of bits `current_code_bit_size`,
      // we need to increment it by 1.
      if (code_in_table >= (1 << current_code_bit_size)) {
        ++current_code_bit_size;
      }

      // If we are about to exceed the maximum 12 bits per LZW code defined by
      // the specs, we need to signal to the decoder that we're starting a new
      // compression table.
      if (next_available_code >= kMaxNumberOfLzwCodes) {
        AppendCodeToStreamBuffer(clear_code, current_code_bit_size);

        code_table_.clear();
        next_available_code = eoi_code + 1;
        current_code_bit_size = color_bit_depth + 1;
      }
    }
  }

  // At the end, we output the last code to the stream, and the control codes
  // `clear_code` and `eoi_code` to signal the end of the compression.
  AppendCodeToStreamBuffer(current_code, current_code_bit_size);
  AppendCodeToStreamBuffer(clear_code, current_code_bit_size);
  AppendCodeToStreamBuffer(eoi_code, current_code_bit_size);

  // We need to flush all the remaining data in `pending_byte_` and
  // `byte_stream_buffer_` to the file.
  if (next_bit_ != 0) {
    FlushPendingByteToStream();
  }

  if (!byte_stream_buffer_.empty()) {
    FlushStreamBufferToFile();
  }

  // Finally, we write the block terminator.
  gif_file_writer_->WriteByte(0x00);
}

// -----------------------------------------------------------------------------
// LzwPixelColorIndicesWriter::CodeTableEntry:

LzwPixelColorIndicesWriter::CodeTableEntry::CodeTableEntry() = default;
LzwPixelColorIndicesWriter::CodeTableEntry::CodeTableEntry(CodeTableEntry&&) =
    default;
LzwPixelColorIndicesWriter::CodeTableEntry&
LzwPixelColorIndicesWriter::CodeTableEntry::operator=(CodeTableEntry&&) =
    default;
LzwPixelColorIndicesWriter::CodeTableEntry::~CodeTableEntry() = default;

// -----------------------------------------------------------------------------
// LzwPixelColorIndicesWriter:

void LzwPixelColorIndicesWriter::AppendCodeToStreamBuffer(
    LzwCode code,
    uint8_t code_bit_size) {
  for (uint8_t i = 0; i < code_bit_size; ++i) {
    AppendBitToPendingByte(code & 0x01);
    code >>= 1;
  }
}

void LzwPixelColorIndicesWriter::AppendBitToPendingByte(uint8_t bit) {
  bit <<= next_bit_;
  pending_byte_ |= bit;
  ++next_bit_;

  if (next_bit_ > 7) {
    FlushPendingByteToStream();
  }
}

void LzwPixelColorIndicesWriter::FlushPendingByteToStream() {
  byte_stream_buffer_.push_back(pending_byte_);
  if (byte_stream_buffer_.size() == kMaxBytesPerDataSubBlock) {
    FlushStreamBufferToFile();
  }
  DCHECK_LE(byte_stream_buffer_.size(), kMaxBytesPerDataSubBlock);
  pending_byte_ = 0;
  next_bit_ = 0;
}

void LzwPixelColorIndicesWriter::FlushStreamBufferToFile() {
  DCHECK(!byte_stream_buffer_.empty());

  gif_file_writer_->WriteByte(byte_stream_buffer_.size());
  gif_file_writer_->WriteBuffer(byte_stream_buffer_);
  byte_stream_buffer_.clear();
}

}  // namespace recording