| // 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 |