From 06c20d49e6b203377c0097dc81c1107993d38753 Mon Sep 17 00:00:00 2001 From: 5000user5000 Date: Thu, 13 Nov 2025 14:05:37 +0800 Subject: [PATCH 01/11] feat(core): implement JPEG decoder C++ core modules - Add BitStream for bit-level reading with byte stuffing support - Add IDCT with ZigZag ordering for 8x8 block transformation - Add Huffman decoder for DC/AC coefficient decoding - Add main JPEG decoder supporting Baseline DCT format --- src/cpp/bitstream.cpp | 118 ++++++++++++ src/cpp/bitstream.h | 74 ++++++++ src/cpp/decoder.cpp | 415 ++++++++++++++++++++++++++++++++++++++++++ src/cpp/decoder.h | 177 ++++++++++++++++++ src/cpp/huffman.cpp | 179 ++++++++++++++++++ src/cpp/huffman.h | 126 +++++++++++++ src/cpp/idct.cpp | 97 ++++++++++ src/cpp/idct.h | 87 +++++++++ 8 files changed, 1273 insertions(+) create mode 100644 src/cpp/bitstream.cpp create mode 100644 src/cpp/bitstream.h create mode 100644 src/cpp/decoder.cpp create mode 100644 src/cpp/decoder.h create mode 100644 src/cpp/huffman.cpp create mode 100644 src/cpp/huffman.h create mode 100644 src/cpp/idct.cpp create mode 100644 src/cpp/idct.h diff --git a/src/cpp/bitstream.cpp b/src/cpp/bitstream.cpp new file mode 100644 index 0000000..d3085e6 --- /dev/null +++ b/src/cpp/bitstream.cpp @@ -0,0 +1,118 @@ +#include "bitstream.h" +#include +#include + +namespace jpeg { + +BitStream::BitStream(const uint8_t* data, size_t size) + : data_(data), size_(size), byte_pos_(0), bit_pos_(0) { + if (!data || size == 0) { + throw std::invalid_argument("BitStream: invalid data or size"); + } +} + +uint16_t BitStream::readBits(int n_bits) { + if (n_bits <= 0 || n_bits > 16) { + throw std::invalid_argument("BitStream: n_bits must be between 1 and 16"); + } + + uint16_t result = 0; + + for (int i = 0; i < n_bits; ++i) { + // update byte and bit positions + if (bit_pos_ == 8) { + byte_pos_++; + bit_pos_ = 0; + } + + // 檢查是否超出範圍 + if (byte_pos_ >= size_) { + throw std::runtime_error("BitStream: unexpected end of data"); + } + + // 處理 byte stuffing: 如果當前 byte 是 0xFF + uint8_t current_byte = data_[byte_pos_]; + if (current_byte == 0xFF && bit_pos_ == 0) { + // 檢查下一個 byte + if (byte_pos_ + 1 < size_) { + uint8_t next_byte = data_[byte_pos_ + 1]; + if (next_byte == 0x00) { + // 這是 byte stuffing,跳過 0x00 + // 繼續使用 0xFF + } else if (next_byte >= 0xD0 && next_byte <= 0xD7) { + // RST marker,這裡簡單處理,繼續讀取 + // 更完整的實作應該處理 restart interval + } else { + // 其他 marker,可能是資料結束 + // 目前先簡單拋出異常 + throw std::runtime_error("BitStream: unexpected marker"); + } + } + } + + // 讀取一個 bit (MSB first) + int bit = (current_byte >> (7 - bit_pos_)) & 1; + result = (result << 1) | bit; + bit_pos_++; + } + + return result; +} + +uint16_t BitStream::peekBits(int n_bits) { + // 保存當前狀態 + size_t saved_byte_pos = byte_pos_; + int saved_bit_pos = bit_pos_; + + uint16_t result = readBits(n_bits); + + // 恢復狀態 + byte_pos_ = saved_byte_pos; + bit_pos_ = saved_bit_pos; + + return result; +} + +void BitStream::skipBits(int n_bits) { + readBits(n_bits); +} + +bool BitStream::hasMoreData() const { + return byte_pos_ < size_; +} + +size_t BitStream::getBitPosition() const { + return byte_pos_ * 8 + bit_pos_; +} + +void BitStream::reset(size_t byte_pos, int bit_pos) { + if (byte_pos >= size_) { + throw std::out_of_range("BitStream: byte_pos out of range"); + } + if (bit_pos < 0 || bit_pos >= 8) { + throw std::out_of_range("BitStream: bit_pos must be 0-7"); + } + byte_pos_ = byte_pos; + bit_pos_ = bit_pos; +} + +uint8_t BitStream::getNextByte() { + if (byte_pos_ >= size_) { + throw std::runtime_error("BitStream: unexpected end of data"); + } + + uint8_t byte = data_[byte_pos_++]; + + // 處理 byte stuffing + if (byte == 0xFF && byte_pos_ < size_) { + uint8_t next = data_[byte_pos_]; + if (next == 0x00) { + // Byte stuffing: 跳過 0x00 + byte_pos_++; + } + } + + return byte; +} + +} // namespace jpeg diff --git a/src/cpp/bitstream.h b/src/cpp/bitstream.h new file mode 100644 index 0000000..8b0b3ea --- /dev/null +++ b/src/cpp/bitstream.h @@ -0,0 +1,74 @@ +#ifndef BITSTREAM_H +#define BITSTREAM_H + +#include +#include +#include + +namespace jpeg { + +/** + * BitStream - 處理 JPEG bit 層級的資料讀取 + * + * JPEG 的 SOS (Start of Scan) 資料是以 bit 為單位編碼的, + * 而且包含 byte stuffing: 0xFF 後面必須跟 0x00,解碼時需跳過 0x00 + */ +class BitStream { +public: + BitStream(const uint8_t* data, size_t size); + + /** + * 讀取指定數量的 bits + * @param n_bits 要讀取的 bit 數量 (1-16) + * @return 讀取的數值 + */ + uint16_t readBits(int n_bits); + + /** + * 查看(peek)接下來的 bits 但不移動位置 + * @param n_bits 要查看的 bit 數量 + * @return 讀取的數值 + */ + uint16_t peekBits(int n_bits); + + /** + * 跳過指定數量的 bits + * @param n_bits 要跳過的 bit 數量 + */ + void skipBits(int n_bits); + + /** + * 檢查是否還有資料可讀 + * @return true 如果還有資料 + */ + bool hasMoreData() const; + + /** + * 取得當前位元位置 + * @return 當前的 bit 位置 + */ + size_t getBitPosition() const; + + /** + * 重設到指定位置 + * @param byte_pos 位元組位置 + * @param bit_pos 在該位元組內的 bit 位置 (0-7) + */ + void reset(size_t byte_pos = 0, int bit_pos = 0); + +private: + const uint8_t* data_; // 原始資料指標 + size_t size_; // 資料大小 + size_t byte_pos_; // 當前位元組位置 + int bit_pos_; // 當前 bit 位置 (0-7, 0 = MSB) + + /** + * 讀取下一個 byte,處理 byte stuffing + * @return 下一個有效的 byte + */ + uint8_t getNextByte(); +}; + +} // namespace jpeg + +#endif // BITSTREAM_H diff --git a/src/cpp/decoder.cpp b/src/cpp/decoder.cpp new file mode 100644 index 0000000..440b428 --- /dev/null +++ b/src/cpp/decoder.cpp @@ -0,0 +1,415 @@ +#include "decoder.h" +#include "idct.h" +#include +#include +#include +#include +#include + +namespace jpeg { + +JPEGDecoder::JPEGDecoder() + : width_(0), height_(0), num_components_(0), + restart_interval_(0), data_pos_(0) { + std::memset(qt_set_, 0, sizeof(qt_set_)); +} + +JPEGDecoder::~JPEGDecoder() { +} + +bool JPEGDecoder::decodeFile(const std::string& filename) { + + std::ifstream file(filename, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + return false; + } + + size_t size = file.tellg(); + file.seekg(0, std::ios::beg); + + jpeg_data_.resize(size); + file.read(reinterpret_cast(jpeg_data_.data()), size); + file.close(); + + data_pos_ = 0; + return parse(); +} + +bool JPEGDecoder::decodeMemory(const uint8_t* data, size_t size) { + jpeg_data_.assign(data, data + size); + data_pos_ = 0; + return parse(); +} + +uint8_t JPEGDecoder::readByte() { + if (data_pos_ >= jpeg_data_.size()) { + throw std::runtime_error("Unexpected end of JPEG data"); + } + return jpeg_data_[data_pos_++]; +} + +uint16_t JPEGDecoder::readWord() { + uint16_t high = readByte(); + uint16_t low = readByte(); + return (high << 8) | low; +} + +uint8_t JPEGDecoder::readMarker() { + uint8_t byte = readByte(); + if (byte != 0xFF) { + throw std::runtime_error("Expected marker"); + } + + // 跳過填充的 0xFF + byte = readByte(); + while (byte == 0xFF) { + byte = readByte(); + } + + return byte; +} + +void JPEGDecoder::skipSegment() { + uint16_t length = readWord(); + data_pos_ += length - 2; +} + +bool JPEGDecoder::parse() { + try { + // 檢查 SOI marker + uint8_t marker = readMarker(); + if (marker != MARKER_SOI) { + return false; + } + + // 解析各個 segments + while (true) { + marker = readMarker(); + + switch (marker) { + case MARKER_SOF0: + if (!processSOF0()) return false; + break; + + case MARKER_DQT: + if (!processDQT()) return false; + break; + + case MARKER_DHT: + if (!processDHT()) return false; + break; + + case MARKER_SOS: + if (!processSOS()) return false; + break; + + case MARKER_DRI: + if (!processDRI()) return false; + break; + + case MARKER_EOI: + return true; + + case MARKER_APP0: + default: + // 跳過不認識的 segment + skipSegment(); + break; + } + } + } catch (const std::exception& e) { + return false; + } + + return false; +} + +bool JPEGDecoder::processDQT() { + uint16_t length = readWord(); + size_t end_pos = data_pos_ + length - 2; + + while (data_pos_ < end_pos) { + uint8_t qt_info = readByte(); + int precision = (qt_info >> 4); // 0 = 8-bit, 1 = 16-bit + int qt_id = qt_info & 0x0F; + + if (qt_id >= 4) { + return false; + } + + // 讀取 64 個量化值 + for (int i = 0; i < 64; ++i) { + if (precision == 0) { + quantization_tables_[qt_id][i] = readByte(); + } else { + quantization_tables_[qt_id][i] = readWord(); + } + } + + qt_set_[qt_id] = true; + } + + return true; +} + +bool JPEGDecoder::processSOF0() { + uint16_t length = readWord(); + + uint8_t precision = readByte(); // 通常是 8 + height_ = readWord(); + width_ = readWord(); + num_components_ = readByte(); + + if (num_components_ != 1 && num_components_ != 3) { + return false; // 只支援 grayscale 或 YCbCr + } + + for (int i = 0; i < num_components_; ++i) { + components_[i].id = readByte(); + uint8_t sampling = readByte(); + components_[i].h_sample = sampling >> 4; + components_[i].v_sample = sampling & 0x0F; + components_[i].qt_id = readByte(); + } + + return true; +} + +bool JPEGDecoder::processDHT() { + uint16_t length = readWord(); + size_t end_pos = data_pos_ + length - 2; + + while (data_pos_ < end_pos) { + uint8_t ht_info = readByte(); + int table_class = (ht_info >> 4); // 0 = DC, 1 = AC + int table_id = ht_info & 0x0F; + + // 讀取 bits 陣列 + uint8_t bits[16]; + int total_codes = 0; + for (int i = 0; i < 16; ++i) { + bits[i] = readByte(); + total_codes += bits[i]; + } + + // 讀取符號值 + std::vector values(total_codes); + for (int i = 0; i < total_codes; ++i) { + values[i] = readByte(); + } + + // 建立霍夫曼表 + huffman_decoder_.setTable(table_id, table_class == 1, bits, values.data()); + } + + return true; +} + +bool JPEGDecoder::processDRI() { + uint16_t length = readWord(); + restart_interval_ = readWord(); + return true; +} + +bool JPEGDecoder::processSOS() { + uint16_t length = readWord(); + + int num_components = readByte(); + + for (int i = 0; i < num_components; ++i) { + int component_id = readByte(); + uint8_t table_ids = readByte(); + + // 找到對應的 component + for (int j = 0; j < num_components_; ++j) { + if (components_[j].id == component_id) { + components_[j].dc_table_id = table_ids >> 4; + components_[j].ac_table_id = table_ids & 0x0F; + break; + } + } + } + + // 跳過 3 個 bytes (Ss, Se, Ah/Al) + readByte(); // Start of spectral selection + readByte(); // End of spectral selection + readByte(); // Successive approximation + + // 剩下的資料是壓縮的圖像資料 + size_t scan_data_start = data_pos_; + + // 找到掃描資料的結尾(下一個 marker 或 EOI) + size_t scan_data_end = data_pos_; + while (scan_data_end < jpeg_data_.size()) { + if (jpeg_data_[scan_data_end] == 0xFF) { + uint8_t next = jpeg_data_[scan_data_end + 1]; + if (next != 0x00 && !(next >= 0xD0 && next <= 0xD7)) { + // 找到下一個 marker + break; + } + } + scan_data_end++; + } + + size_t scan_data_size = scan_data_end - scan_data_start; + + // 使用 BitStream 解碼 + BitStream bs(jpeg_data_.data() + scan_data_start, scan_data_size); + + // 計算 MCU 的數量 + int max_h_sample = 1, max_v_sample = 1; + for (int i = 0; i < num_components_; ++i) { + max_h_sample = std::max(max_h_sample, components_[i].h_sample); + max_v_sample = std::max(max_v_sample, components_[i].v_sample); + } + + int mcu_width = max_h_sample * 8; + int mcu_height = max_v_sample * 8; + int mcu_cols = (width_ + mcu_width - 1) / mcu_width; + int mcu_rows = (height_ + mcu_height - 1) / mcu_height; + + // 準備輸出緩衝區 + image_data_.resize(width_ * height_ * 3); + + // 儲存 Y, Cb, Cr 分量 + std::vector> y_data(mcu_rows * mcu_cols); + std::vector> cb_data(mcu_rows * mcu_cols); + std::vector> cr_data(mcu_rows * mcu_cols); + + // 解碼所有 MCU + int16_t prev_dc[3] = {0, 0, 0}; + + for (int mcu_row = 0; mcu_row < mcu_rows; ++mcu_row) { + for (int mcu_col = 0; mcu_col < mcu_cols; ++mcu_col) { + int mcu_index = mcu_row * mcu_cols + mcu_col; + + // 為每個分量解碼區塊 + for (int comp = 0; comp < num_components_; ++comp) { + int h_blocks = components_[comp].h_sample; + int v_blocks = components_[comp].v_sample; + + for (int v = 0; v < v_blocks; ++v) { + for (int h = 0; h < h_blocks; ++h) { + int16_t block[64]; + decodeBlock(bs, comp, &prev_dc[comp], block); + + // IDCT + uint8_t pixels[64]; + IDCT::transform8x8(block, pixels); + + // 儲存到對應的分量 + if (comp == 0) { // Y + y_data[mcu_index].insert(y_data[mcu_index].end(), pixels, pixels + 64); + } else if (comp == 1) { // Cb + cb_data[mcu_index].insert(cb_data[mcu_index].end(), pixels, pixels + 64); + } else { // Cr + cr_data[mcu_index].insert(cr_data[mcu_index].end(), pixels, pixels + 64); + } + } + } + } + } + } + + // Upsampling 和 YCbCr 轉 RGB + upsample(y_data, cb_data, cr_data); + + data_pos_ = scan_data_end; + return true; +} + +void JPEGDecoder::decodeBlock(BitStream& bs, int component_id, int16_t* prev_dc, int16_t block[64]) { + int16_t dc = huffman_decoder_.decodeDC(bs, components_[component_id].dc_table_id, *prev_dc); + *prev_dc = dc; + + int16_t ac[63]; + huffman_decoder_.decodeAC(bs, components_[component_id].ac_table_id, ac); + + // 組合成 64 個係數 (ZigZag 順序) + int16_t coeffs[64]; + coeffs[0] = dc; + std::memcpy(coeffs + 1, ac, 63 * sizeof(int16_t)); + + // 反 ZigZag + int16_t matrix[64]; + ZigZag::toMatrix(coeffs, matrix); + + // 反量化 + int qt_id = components_[component_id].qt_id; + for (int i = 0; i < 64; ++i) { + block[i] = matrix[i] * quantization_tables_[qt_id][i]; + } +} + +void JPEGDecoder::ycbcrToRgb(int y, int cb, int cr, uint8_t& r, uint8_t& g, uint8_t& b) { + int r_val = y + 1.402 * (cr - 128); + int g_val = y - 0.344136 * (cb - 128) - 0.714136 * (cr - 128); + int b_val = y + 1.772 * (cb - 128); + + r = (r_val < 0) ? 0 : (r_val > 255) ? 255 : r_val; + g = (g_val < 0) ? 0 : (g_val > 255) ? 255 : g_val; + b = (b_val < 0) ? 0 : (b_val > 255) ? 255 : b_val; +} + +void JPEGDecoder::upsample(const std::vector>& y_blocks, + const std::vector>& cb_blocks, + const std::vector>& cr_blocks) { + // 簡化版:假設 4:4:4 或 4:2:0 + // 這裡實作簡單的 nearest neighbor upsampling + + if (num_components_ == 1) { + // Grayscale + for (int row = 0; row < height_; ++row) { + for (int col = 0; col < width_; ++col) { + int mcu_col = col / 8; + int mcu_row = row / 8; + int block_x = col % 8; + int block_y = row % 8; + int mcu_index = mcu_row * ((width_ + 7) / 8) + mcu_col; + + if (mcu_index < static_cast(y_blocks.size()) && + !y_blocks[mcu_index].empty()) { + uint8_t y = y_blocks[mcu_index][block_y * 8 + block_x]; + int pixel_index = (row * width_ + col) * 3; + image_data_[pixel_index] = y; + image_data_[pixel_index + 1] = y; + image_data_[pixel_index + 2] = y; + } + } + } + } else { + // YCbCr 轉 RGB + for (int row = 0; row < height_; ++row) { + for (int col = 0; col < width_; ++col) { + int mcu_col = col / 8; + int mcu_row = row / 8; + int block_x = col % 8; + int block_y = row % 8; + int mcu_index = mcu_row * ((width_ + 7) / 8) + mcu_col; + + if (mcu_index < static_cast(y_blocks.size()) && + !y_blocks[mcu_index].empty()) { + uint8_t y = y_blocks[mcu_index][block_y * 8 + block_x]; + uint8_t cb = 128, cr = 128; + + if (!cb_blocks[mcu_index].empty()) { + cb = cb_blocks[mcu_index][block_y * 8 + block_x]; + } + if (!cr_blocks[mcu_index].empty()) { + cr = cr_blocks[mcu_index][block_y * 8 + block_x]; + } + + uint8_t r, g, b; + ycbcrToRgb(y, cb, cr, r, g, b); + + int pixel_index = (row * width_ + col) * 3; + image_data_[pixel_index] = r; + image_data_[pixel_index + 1] = g; + image_data_[pixel_index + 2] = b; + } + } + } + } +} + +} // namespace jpeg diff --git a/src/cpp/decoder.h b/src/cpp/decoder.h new file mode 100644 index 0000000..b3e1206 --- /dev/null +++ b/src/cpp/decoder.h @@ -0,0 +1,177 @@ +#ifndef DECODER_H +#define DECODER_H + +#include +#include +#include +#include +#include "huffman.h" +#include "bitstream.h" + +namespace jpeg { + +/** + * JPEGDecoder - JPEG 解碼器主類別 + * + * 負責整個 JPEG 解碼流程: + * 1. 解析文件結構 (markers) + * 2. 讀取量化表 (DQT) + * 3. 讀取圖像資訊 (SOF) + * 4. 讀取霍夫曼表 (DHT) + * 5. 解碼圖像資料 (SOS) + */ +class JPEGDecoder { +public: + JPEGDecoder(); + ~JPEGDecoder(); + + /** + * 從檔案解碼 JPEG 圖像 + * + * @param filename JPEG 檔案路徑 + * @return true 如果成功 + */ + bool decodeFile(const std::string& filename); + + /** + * 從記憶體解碼 JPEG 圖像 + * + * @param data JPEG 資料 + * @param size 資料大小 + * @return true 如果成功 + */ + bool decodeMemory(const uint8_t* data, size_t size); + + /** + * 取得解碼後的圖像資料(RGB 格式) + * + * @return RGB 資料指標(每個像素 3 bytes: R, G, B) + */ + const uint8_t* getImageData() const { return image_data_.data(); } + + int getWidth() const { return width_; } + + int getHeight() const { return height_; } + + int getChannels() const { return num_components_; } + +private: + // JPEG Markers + static constexpr uint8_t MARKER_SOI = 0xD8; // Start of Image + static constexpr uint8_t MARKER_EOI = 0xD9; // End of Image + static constexpr uint8_t MARKER_SOF0 = 0xC0; // Start of Frame (Baseline DCT) + static constexpr uint8_t MARKER_DHT = 0xC4; // Define Huffman Table + static constexpr uint8_t MARKER_DQT = 0xDB; // Define Quantization Table + static constexpr uint8_t MARKER_SOS = 0xDA; // Start of Scan + static constexpr uint8_t MARKER_DRI = 0xDD; // Define Restart Interval + static constexpr uint8_t MARKER_APP0 = 0xE0; // Application Segment 0 + + // 圖像資訊 + int width_; + int height_; + int num_components_; // 1 = grayscale, 3 = YCbCr + + // 顏色分量資訊 + struct Component { + int id; + int h_sample; // 水平取樣因子 + int v_sample; // 垂直取樣因子 + int qt_id; // 量化表 ID + int dc_table_id; // DC 霍夫曼表 ID + int ac_table_id; // AC 霍夫曼表 ID + }; + Component components_[3]; // 最多 3 個分量 (Y, Cb, Cr) + + // 量化表 + uint16_t quantization_tables_[4][64]; + bool qt_set_[4]; + + // 霍夫曼解碼器 + HuffmanDecoder huffman_decoder_; + + // Restart interval + int restart_interval_; + + // 解碼後的圖像資料 (RGB) + std::vector image_data_; + + // JPEG 原始資料 + std::vector jpeg_data_; + size_t data_pos_; + + /** + * 讀取一個 byte + */ + uint8_t readByte(); + + /** + * 讀取兩個 bytes (big-endian) + */ + uint16_t readWord(); + + /** + * 尋找下一個 marker + */ + uint8_t readMarker(); + + /** + * 解析 JPEG 檔案 + */ + bool parse(); + + /** + * 處理 DQT (Define Quantization Table) + */ + bool processDQT(); + + /** + * 處理 SOF0 (Start of Frame - Baseline DCT) + */ + bool processSOF0(); + + /** + * 處理 DHT (Define Huffman Table) + */ + bool processDHT(); + + /** + * 處理 SOS (Start of Scan) + */ + bool processSOS(); + + /** + * 處理 DRI (Define Restart Interval) + */ + bool processDRI(); + + /** + * 跳過一個 segment + */ + void skipSegment(); + + /** + * 解碼一個 MCU (Minimum Coded Unit) + */ + void decodeMCU(BitStream& bs, int mcu_row, int mcu_col); + + /** + * 解碼一個 8x8 區塊 + */ + void decodeBlock(BitStream& bs, int component_id, int16_t* prev_dc, int16_t block[64]); + + /** + * YCbCr 轉 RGB + */ + static void ycbcrToRgb(int y, int cb, int cr, uint8_t& r, uint8_t& g, uint8_t& b); + + /** + * Upsampling (色度子採樣重建) + */ + void upsample(const std::vector>& y_blocks, + const std::vector>& cb_blocks, + const std::vector>& cr_blocks); +}; + +} // namespace jpeg + +#endif // DECODER_H diff --git a/src/cpp/huffman.cpp b/src/cpp/huffman.cpp new file mode 100644 index 0000000..6bf05e0 --- /dev/null +++ b/src/cpp/huffman.cpp @@ -0,0 +1,179 @@ +#include "huffman.h" +#include +#include + +namespace jpeg { + +HuffmanTable::HuffmanTable() : built_(false) { + std::memset(min_code_, 0, sizeof(min_code_)); + std::memset(max_code_, 0, sizeof(max_code_)); + std::memset(val_offset_, 0, sizeof(val_offset_)); +} + +void HuffmanTable::build(const uint8_t bits[16], const uint8_t* values) { + // 計算總符號數 + int total_symbols = 0; + for (int i = 0; i < 16; ++i) { + total_symbols += bits[i]; + } + + // 複製符號值 + symbols_.clear(); + symbols_.reserve(total_symbols); + for (int i = 0; i < total_symbols; ++i) { + symbols_.push_back(values[i]); + } + + // 建立霍夫曼表(使用 JPEG 標準算法) + int k = 0; + uint32_t code = 0; + + for (int i = 0; i < 16; ++i) { + if (bits[i] != 0) { + val_offset_[i] = k; + min_code_[i] = code; + + for (int j = 0; j < bits[i]; ++j) { + code_to_symbol_[code] = symbols_[k]; + code++; + k++; + } + + max_code_[i] = code - 1; + } else { + min_code_[i] = 0xFFFFFFFF; + max_code_[i] = 0; + val_offset_[i] = 0; + } + + code <<= 1; + } + + built_ = true; +} + +uint8_t HuffmanTable::decode(BitStream& bs) const { + if (!built_) { + throw std::runtime_error("HuffmanTable: table not built"); + } + + uint32_t code = 0; + + // 逐位元讀取,嘗試匹配霍夫曼碼 + for (int i = 0; i < 16; ++i) { + code = (code << 1) | bs.readBits(1); + + if (code <= max_code_[i] && code >= min_code_[i]) { + // 找到匹配的碼 + int index = val_offset_[i] + (code - min_code_[i]); + if (index < static_cast(symbols_.size())) { + return symbols_[index]; + } + } + } + + throw std::runtime_error("HuffmanTable: invalid huffman code"); +} + +// HuffmanDecoder 實作 + +void HuffmanDecoder::setTable(int table_id, bool is_ac, const uint8_t bits[16], const uint8_t* values) { + if (table_id < 0 || table_id >= 4) { + throw std::out_of_range("HuffmanDecoder: table_id must be 0-3"); + } + + if (is_ac) { + ac_tables_[table_id].build(bits, values); + } else { + dc_tables_[table_id].build(bits, values); + } +} + +const HuffmanTable* HuffmanDecoder::getTable(int table_id, bool is_ac) const { + if (table_id < 0 || table_id >= 4) { + return nullptr; + } + + if (is_ac) { + return ac_tables_[table_id].isBuilt() ? &ac_tables_[table_id] : nullptr; + } else { + return dc_tables_[table_id].isBuilt() ? &dc_tables_[table_id] : nullptr; + } +} + +int16_t HuffmanDecoder::receiveExtend(BitStream& bs, int size) { + if (size == 0) { + return 0; + } + + // 讀取 size 個 bits + int value = bs.readBits(size); + + // 檢查是否為負數(最高位為 0) + int vt = 1 << (size - 1); + if (value < vt) { + // 負數:需要擴展 + vt = (-1 << size) + 1; + value = value + vt; + } + + return static_cast(value); +} + +int16_t HuffmanDecoder::decodeDC(BitStream& bs, int table_id, int16_t prev_dc) const { + const HuffmanTable* table = getTable(table_id, false); + if (!table) { + throw std::runtime_error("HuffmanDecoder: DC table not found"); + } + + // 解碼 DC 差分值的大小 + uint8_t size = table->decode(bs); + + // 讀取實際的差分值 + int16_t diff = receiveExtend(bs, size); + + // DC 使用差分編碼 + return prev_dc + diff; +} + +void HuffmanDecoder::decodeAC(BitStream& bs, int table_id, int16_t coeffs[63]) const { + const HuffmanTable* table = getTable(table_id, true); + if (!table) { + throw std::runtime_error("HuffmanDecoder: AC table not found"); + } + + // 初始化為 0 + std::memset(coeffs, 0, 63 * sizeof(int16_t)); + + int k = 0; + while (k < 63) { + // 解碼 (Run, Size) 對 + uint8_t rs = table->decode(bs); + + int run = rs >> 4; // 高 4 bits 是 run (前面有幾個 0) + int size = rs & 0x0F; // 低 4 bits 是 size (係數的 bit 長度) + + if (size == 0) { + if (run == 15) { + // ZRL (Zero Run Length): 16 個連續的 0 + k += 16; + } else { + // EOB (End of Block): 剩下的都是 0 + break; + } + } else { + // 跳過 run 個 0 + k += run; + + if (k >= 63) { + break; + } + + // 讀取 AC 係數 + coeffs[k] = receiveExtend(bs, size); + k++; + } + } +} + +} // namespace jpeg diff --git a/src/cpp/huffman.h b/src/cpp/huffman.h new file mode 100644 index 0000000..aef8209 --- /dev/null +++ b/src/cpp/huffman.h @@ -0,0 +1,126 @@ +#ifndef HUFFMAN_H +#define HUFFMAN_H + +#include +#include +#include +#include "bitstream.h" + +namespace jpeg { + +/** + * HuffmanTable - 霍夫曼表 + * + * 儲存 JPEG DHT (Define Huffman Table) segment 的資訊 + * 並提供解碼功能 + */ +class HuffmanTable { +public: + HuffmanTable(); + + /** + * 從 DHT segment 資料建立霍夫曼表 + * + * @param bits 16 個元素的陣列,bits[i] 表示長度為 i+1 的碼字數量 + * @param values 所有符號值的陣列 + */ + void build(const uint8_t bits[16], const uint8_t* values); + + /** + * 使用此霍夫曼表解碼一個符號 + * + * @param bs BitStream 物件 + * @return 解碼出的符號值 + */ + uint8_t decode(BitStream& bs) const; + + /** + * 檢查表是否已建立 + */ + bool isBuilt() const { return built_; } + +private: + bool built_; + + // 霍夫曼碼表:code -> symbol + // 為了效率,可以用不同的資料結構,這裡先用簡單的 map + std::map code_to_symbol_; + + // 每個碼長的最小碼值 + uint32_t min_code_[16]; + + // 每個碼長的最大碼值 + uint32_t max_code_[16]; + + // 每個碼長的符號起始索引 + int val_offset_[16]; + + // 符號值陣列 + std::vector symbols_; +}; + +/** + * HuffmanDecoder - 霍夫曼解碼器 + * + * 管理多個霍夫曼表(DC 和 AC,可能有多個) + */ +class HuffmanDecoder { +public: + /** + * 設定霍夫曼表 + * + * @param table_id 表格 ID (0-3) + * @param is_ac true 表示 AC 表,false 表示 DC 表 + * @param bits 16 個元素的陣列 + * @param values 符號值陣列 + */ + void setTable(int table_id, bool is_ac, const uint8_t bits[16], const uint8_t* values); + + /** + * 取得霍夫曼表 + * + * @param table_id 表格 ID + * @param is_ac true 表示 AC 表,false 表示 DC 表 + * @return 霍夫曼表的指標 + */ + const HuffmanTable* getTable(int table_id, bool is_ac) const; + + /** + * 解碼一個 MCU (Minimum Coded Unit) 的 DC 係數 + * + * @param bs BitStream 物件 + * @param table_id 霍夫曼表 ID + * @param prev_dc 前一個 DC 值(DC 使用差分編碼) + * @return 解碼出的 DC 係數 + */ + int16_t decodeDC(BitStream& bs, int table_id, int16_t prev_dc) const; + + /** + * 解碼一個 MCU 的 AC 係數 + * + * @param bs BitStream 物件 + * @param table_id 霍夫曼表 ID + * @param coeffs 輸出的 63 個 AC 係數(不包含 DC) + */ + void decodeAC(BitStream& bs, int table_id, int16_t coeffs[63]) const; + +private: + // DC 表(最多 4 個) + HuffmanTable dc_tables_[4]; + + // AC 表(最多 4 個) + HuffmanTable ac_tables_[4]; + + /** + * 從 BitStream 讀取一個有符號整數 + * + * @param bs BitStream 物件 + * @param size bit 數量 + * @return 解碼的整數值 + */ + static int16_t receiveExtend(BitStream& bs, int size); +}; + +} // namespace jpeg + +#endif // HUFFMAN_H diff --git a/src/cpp/idct.cpp b/src/cpp/idct.cpp new file mode 100644 index 0000000..ff6b90a --- /dev/null +++ b/src/cpp/idct.cpp @@ -0,0 +1,97 @@ +#include "idct.h" +#include +#include + +namespace jpeg { + +// ZigZag 掃描順序表 +const int ZigZag::ZIGZAG_TABLE[64] = { + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63 +}; + +void ZigZag::toMatrix(const int16_t zigzag[64], int16_t matrix[64]) { + for (int i = 0; i < 64; ++i) { + matrix[ZIGZAG_TABLE[i]] = zigzag[i]; + } +} + +void ZigZag::fromMatrix(const int16_t matrix[64], int16_t zigzag[64]) { + for (int i = 0; i < 64; ++i) { + zigzag[i] = matrix[ZIGZAG_TABLE[i]]; + } +} + +const int* ZigZag::getZigZagTable() { + return ZIGZAG_TABLE; +} + +// IDCT 實作 - Naive 版本使用標準公式 + +void IDCT::idct1D(const double input[8], double output[8]) { + for (int x = 0; x < 8; ++x) { + double sum = 0.0; + for (int u = 0; u < 8; ++u) { + double cu = (u == 0) ? 1.0 / std::sqrt(2.0) : 1.0; + sum += cu * input[u] * std::cos((2 * x + 1) * u * PI / 16.0); + } + output[x] = sum / 2.0; + } +} + +void IDCT::transform8x8Float(const int16_t input[64], uint8_t output[64]) { + double temp[64]; + double temp2[64]; + + // 先對每一行做 1D IDCT + for (int row = 0; row < 8; ++row) { + double row_input[8]; + double row_output[8]; + + for (int col = 0; col < 8; ++col) { + row_input[col] = static_cast(input[row * 8 + col]); + } + + idct1D(row_input, row_output); + + for (int col = 0; col < 8; ++col) { + temp[row * 8 + col] = row_output[col]; + } + } + + // 再對每一列做 1D IDCT + for (int col = 0; col < 8; ++col) { + double col_input[8]; + double col_output[8]; + + for (int row = 0; row < 8; ++row) { + col_input[row] = temp[row * 8 + col]; + } + + idct1D(col_input, col_output); + + for (int row = 0; row < 8; ++row) { + temp2[row * 8 + col] = col_output[row]; + } + } + + // Level shift (加 128) 並 clamp 到 0-255 + for (int i = 0; i < 64; ++i) { + int value = static_cast(std::round(temp2[i])) + 128; + output[i] = clamp(value); + } +} + +void IDCT::transform8x8(const int16_t input[64], uint8_t output[64]) { + // 目前使用 float 版本 + // 之後可以實作更快的整數版本或 SIMD 版本 + transform8x8Float(input, output); +} + +} // namespace jpeg diff --git a/src/cpp/idct.h b/src/cpp/idct.h new file mode 100644 index 0000000..cc68717 --- /dev/null +++ b/src/cpp/idct.h @@ -0,0 +1,87 @@ +#ifndef IDCT_H +#define IDCT_H + +#include + +namespace jpeg { + +/** + * IDCT - 反離散餘弦變換 (Inverse Discrete Cosine Transform) + * + * 將 8x8 的 DCT 係數矩陣轉換回 8x8 的像素值 + */ +class IDCT { +public: + /** + * 對 8x8 區塊執行 2D IDCT + * + * @param input 8x8 的 DCT 係數矩陣 (已反量化) + * @param output 8x8 的輸出像素矩陣 (0-255) + */ + static void transform8x8(const int16_t input[64], uint8_t output[64]); + + /** + * 對 8x8 區塊執行 2D IDCT (float 版本,更精確但較慢) + * + * @param input 8x8 的 DCT 係數矩陣 + * @param output 8x8 的輸出矩陣 (會自動進行 level shift 和 clamp) + */ + static void transform8x8Float(const int16_t input[64], uint8_t output[64]); + +private: + // IDCT 的縮放因子 + static constexpr double PI = 3.14159265358979323846; + + /** + * 1D IDCT + * @param input 8 個輸入係數 + * @param output 8 個輸出值 + */ + static void idct1D(const double input[8], double output[8]); + + /** + * 將值 clamp 到 0-255 範圍 + */ + static inline uint8_t clamp(int value) { + if (value < 0) return 0; + if (value > 255) return 255; + return static_cast(value); + } +}; + +/** + * ZigZag - 處理 ZigZag 掃描順序 + * + * JPEG 使用 ZigZag 順序來排列 DCT 係數, + * 需要將 1D 的 64 個係數重新排列成 8x8 矩陣 + */ +class ZigZag { +public: + /** + * 將 ZigZag 順序的 1D 陣列轉換為 8x8 矩陣 + * @param zigzag 64 個 ZigZag 順序的係數 + * @param matrix 8x8 矩陣輸出 + */ + static void toMatrix(const int16_t zigzag[64], int16_t matrix[64]); + + /** + * 將 8x8 矩陣轉換為 ZigZag 順序 + * @param matrix 8x8 矩陣 + * @param zigzag 64 個 ZigZag 順序的係數輸出 + */ + static void fromMatrix(const int16_t matrix[64], int16_t zigzag[64]); + + /** + * 取得 ZigZag 掃描表 + * @return 64 個位置的對應表 + */ + static const int* getZigZagTable(); + +private: + // ZigZag 掃描順序表 + static const int ZIGZAG_TABLE[64]; +}; + +} // namespace jpeg + +#endif // IDCT_H From fe70d4b9bd030bd25ab85f12dac851577bd7a1c0 Mon Sep 17 00:00:00 2001 From: 5000user5000 Date: Thu, 13 Nov 2025 14:06:02 +0800 Subject: [PATCH 02/11] pybind --- src/bindings/bindings.cpp | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/bindings/bindings.cpp diff --git a/src/bindings/bindings.cpp b/src/bindings/bindings.cpp new file mode 100644 index 0000000..3226c61 --- /dev/null +++ b/src/bindings/bindings.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include "../cpp/decoder.h" + +namespace py = pybind11; + +PYBIND11_MODULE(_fast_jpeg_decoder, m) { + m.doc() = "Fast JPEG Decoder - C++ implementation with Python bindings"; + + py::class_(m, "JPEGDecoder") + .def(py::init<>()) + .def("decode_file", &jpeg::JPEGDecoder::decodeFile, + py::arg("filename"), + "Decode a JPEG file from disk") + .def("decode_memory", [](jpeg::JPEGDecoder& self, py::bytes data) { + std::string str = data; + return self.decodeMemory( + reinterpret_cast(str.data()), + str.size() + ); + }, + py::arg("data"), + "Decode a JPEG from memory (bytes)") + .def("get_image_data", [](const jpeg::JPEGDecoder& self) { + int width = self.getWidth(); + int height = self.getHeight(); + const uint8_t* data = self.getImageData(); + + // 建立 numpy array + auto result = py::array_t({height, width, 3}); + auto buf = result.request(); + uint8_t* ptr = static_cast(buf.ptr); + + // 複製資料 + std::memcpy(ptr, data, width * height * 3); + + return result; + }, + "Get decoded image data as numpy array (H, W, 3)") + .def_property_readonly("width", &jpeg::JPEGDecoder::getWidth, + "Image width") + .def_property_readonly("height", &jpeg::JPEGDecoder::getHeight, + "Image height") + .def_property_readonly("channels", &jpeg::JPEGDecoder::getChannels, + "Number of color channels"); + + // 解碼檔案並返回 numpy array + m.def("decode", [](const std::string& filename) { + jpeg::JPEGDecoder decoder; + if (!decoder.decodeFile(filename)) { + throw std::runtime_error("Failed to decode JPEG file: " + filename); + } + + int width = decoder.getWidth(); + int height = decoder.getHeight(); + const uint8_t* data = decoder.getImageData(); + + auto result = py::array_t({height, width, 3}); + auto buf = result.request(); + uint8_t* ptr = static_cast(buf.ptr); + std::memcpy(ptr, data, width * height * 3); + + return result; + }, py::arg("filename"), "Decode a JPEG file and return as numpy array"); + + m.def("decode_bytes", [](py::bytes data) { + std::string str = data; + jpeg::JPEGDecoder decoder; + if (!decoder.decodeMemory( + reinterpret_cast(str.data()), + str.size())) { + throw std::runtime_error("Failed to decode JPEG from bytes"); + } + + int width = decoder.getWidth(); + int height = decoder.getHeight(); + const uint8_t* img_data = decoder.getImageData(); + + auto result = py::array_t({height, width, 3}); + auto buf = result.request(); + uint8_t* ptr = static_cast(buf.ptr); + std::memcpy(ptr, img_data, width * height * 3); + + return result; + }, py::arg("data"), "Decode a JPEG from bytes and return as numpy array"); +} From 80e6c834f73d227e555ac665dc0711425a39934d Mon Sep 17 00:00:00 2001 From: 5000user5000 Date: Thu, 13 Nov 2025 14:06:37 +0800 Subject: [PATCH 03/11] project setting --- setup.py | 47 ++++++++++++++ src/python/fast_jpeg_decoder/__init__.py | 82 ++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 setup.py create mode 100644 src/python/fast_jpeg_decoder/__init__.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e112a98 --- /dev/null +++ b/setup.py @@ -0,0 +1,47 @@ +""" +Setup script for Fast JPEG Decoder +""" + +from setuptools import setup, Extension +from setuptools.command.build_ext import build_ext +import sys +import os + +class get_pybind_include: + """Helper class to determine the pybind11 include path""" + def __str__(self): + import pybind11 + return pybind11.get_include() + +ext_modules = [ + Extension( + 'fast_jpeg_decoder._fast_jpeg_decoder', + sources=[ + 'src/cpp/bitstream.cpp', + 'src/cpp/idct.cpp', + 'src/cpp/huffman.cpp', + 'src/cpp/decoder.cpp', + 'src/bindings/bindings.cpp', + ], + include_dirs=[ + get_pybind_include(), + 'src/cpp', + ], + language='c++', + extra_compile_args=['-std=c++11', '-O3', '-Wall'], + ), +] + +setup( + name='fast_jpeg_decoder', + version='0.1.0', + author='Your Name', + description='A high-performance JPEG decoder implemented in C++', + long_description='', + ext_modules=ext_modules, + packages=['fast_jpeg_decoder'], + package_dir={'fast_jpeg_decoder': 'src/python/fast_jpeg_decoder'}, + install_requires=['numpy', 'pybind11>=2.6.0'], + python_requires='>=3.6', + zip_safe=False, +) diff --git a/src/python/fast_jpeg_decoder/__init__.py b/src/python/fast_jpeg_decoder/__init__.py new file mode 100644 index 0000000..b42bcd5 --- /dev/null +++ b/src/python/fast_jpeg_decoder/__init__.py @@ -0,0 +1,82 @@ +""" +Fast JPEG Decoder +================= + +A high-performance JPEG decoder implemented in C++ with Python bindings. + +Usage: + >>> import fast_jpeg_decoder as fjd + >>> image = fjd.decode('path/to/image.jpg') + >>> print(image.shape) # (height, width, 3) + + >>> # Or use the decoder class + >>> decoder = fjd.JPEGDecoder() + >>> decoder.decode_file('path/to/image.jpg') + >>> image = decoder.get_image_data() +""" + +try: + from ._fast_jpeg_decoder import ( + JPEGDecoder, + decode, + decode_bytes + ) +except ImportError as e: + raise ImportError( + "Failed to import C++ extension. " + "Please make sure the extension is built. " + "Run 'make' to build the extension." + ) from e + +__version__ = '0.1.0' +__all__ = ['JPEGDecoder', 'decode', 'decode_bytes'] + + +def load(filename): + """ + Load a JPEG image from file. + + Parameters + ---------- + filename : str + Path to the JPEG file + + Returns + ------- + numpy.ndarray + Image data as numpy array with shape (height, width, 3) + and dtype uint8. Channels are in RGB order. + + Examples + -------- + >>> import fast_jpeg_decoder as fjd + >>> image = fjd.load('photo.jpg') + >>> print(image.shape) + (480, 640, 3) + """ + return decode(filename) + + +def load_bytes(data): + """ + Load a JPEG image from bytes. + + Parameters + ---------- + data : bytes + JPEG image data as bytes + + Returns + ------- + numpy.ndarray + Image data as numpy array with shape (height, width, 3) + and dtype uint8. Channels are in RGB order. + + Examples + -------- + >>> import fast_jpeg_decoder as fjd + >>> with open('photo.jpg', 'rb') as f: + ... data = f.read() + >>> image = fjd.load_bytes(data) + """ + return decode_bytes(data) From 53221074642c234a43451a7ef5cd243cf8d907cc Mon Sep 17 00:00:00 2001 From: 5000user5000 Date: Thu, 13 Nov 2025 14:07:52 +0800 Subject: [PATCH 04/11] =?UTF-8?q?python=20=E6=B8=AC=E8=A9=A6=E5=92=8C?= =?UTF-8?q?=E7=AF=84=E4=BE=8B=E8=85=B3=E6=9C=AC=E4=BB=A5=E5=8F=8A=E7=B7=A8?= =?UTF-8?q?=E8=AD=AF=E8=85=B3=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 82 +++++++++++++++++++++ example.py | 69 ++++++++++++++++++ pytest.ini | 6 ++ tests/test_decoder.py | 166 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 323 insertions(+) create mode 100644 Makefile create mode 100755 example.py create mode 100644 pytest.ini create mode 100644 tests/test_decoder.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1b7c7ab --- /dev/null +++ b/Makefile @@ -0,0 +1,82 @@ +# Makefile for Fast JPEG Decoder + +# Compiler settings +CXX = g++ +CXXFLAGS = -std=c++11 -O3 -Wall -fPIC +INCLUDES = -Isrc/cpp + +# Python settings +PYTHON = python3 +PYTHON_CONFIG = python3-config +PYTHON_INCLUDES = $(shell $(PYTHON_CONFIG) --includes) +PYTHON_LDFLAGS = $(shell $(PYTHON_CONFIG) --ldflags) + +# pybind11 settings +PYBIND11_INCLUDES = $(shell $(PYTHON) -m pybind11 --includes) + +# Source files +CPP_SOURCES = src/cpp/bitstream.cpp \ + src/cpp/idct.cpp \ + src/cpp/huffman.cpp \ + src/cpp/decoder.cpp + +BINDING_SOURCES = src/bindings/bindings.cpp + +# Object files +CPP_OBJECTS = $(CPP_SOURCES:.cpp=.o) +BINDING_OBJECTS = $(BINDING_SOURCES:.cpp=.o) + +# Output +EXTENSION_SUFFIX = $(shell $(PYTHON_CONFIG) --extension-suffix) +TARGET = src/python/fast_jpeg_decoder/_fast_jpeg_decoder$(EXTENSION_SUFFIX) + +# Targets +.PHONY: all clean install test develop + +all: $(TARGET) + +# Build Python extension +$(TARGET): $(CPP_OBJECTS) $(BINDING_OBJECTS) + $(CXX) -shared -fPIC $(CXXFLAGS) $^ -o $@ $(PYTHON_LDFLAGS) + +# Build C++ objects +src/cpp/%.o: src/cpp/%.cpp + $(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ + +# Build binding objects +src/bindings/%.o: src/bindings/%.cpp + $(CXX) $(CXXFLAGS) $(INCLUDES) $(PYTHON_INCLUDES) $(PYBIND11_INCLUDES) -c $< -o $@ + +# Install package +install: all + $(PYTHON) setup.py install + +# Develop mode (editable install) +develop: all + $(PYTHON) -m pip install -e . + +# Run tests +test: develop + $(PYTHON) -m pytest tests/ -v + +# Clean build artifacts +clean: + rm -f $(CPP_OBJECTS) $(BINDING_OBJECTS) + rm -f $(TARGET) + rm -rf build/ dist/ *.egg-info + rm -rf src/python/fast_jpeg_decoder/*.so + rm -rf src/python/fast_jpeg_decoder/*.pyd + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete + +# Help +help: + @echo "Fast JPEG Decoder - Makefile" + @echo "" + @echo "Targets:" + @echo " all - Build the Python extension (default)" + @echo " install - Install the package" + @echo " develop - Install in development mode (editable)" + @echo " test - Run tests" + @echo " clean - Remove build artifacts" + @echo " help - Show this help message" diff --git a/example.py b/example.py new file mode 100755 index 0000000..2328640 --- /dev/null +++ b/example.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +範例:使用 Fast JPEG Decoder 解碼 JPEG 圖片 +""" + +import sys +import os +import numpy as np + +# Add src/python to path for development +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src', 'python')) + +try: + import fast_jpeg_decoder as fjd +except ImportError: + print("Error: fast_jpeg_decoder 模組未安裝") + print("請先執行: make develop") + sys.exit(1) + + +def main(): + """主函數""" + if len(sys.argv) < 2: + print("用法: python example.py ") + print("\n範例:") + print(" python example.py photo.jpg") + return + + filename = sys.argv[1] + + print(f"正在解碼: {filename}") + + try: + # 方法 1: 使用簡便函數 + image = fjd.load(filename) + print(f"✓ 解碼成功!") + print(f" 圖片尺寸: {image.shape[1]} x {image.shape[0]}") + print(f" 通道數: {image.shape[2]}") + print(f" 資料類型: {image.dtype}") + print(f" 數值範圍: [{image.min()}, {image.max()}]") + + # 方法 2: 使用 Decoder 類別 + print("\n使用 Decoder 類別:") + decoder = fjd.JPEGDecoder() + success = decoder.decode_file(filename) + + if success: + print(f"✓ 解碼成功!") + print(f" 寬度: {decoder.width}") + print(f" 高度: {decoder.height}") + print(f" 通道: {decoder.channels}") + + image2 = decoder.get_image_data() + + # 驗證兩種方法得到相同結果 + if np.array_equal(image, image2): + print("\n✓ 兩種方法結果一致") + else: + print("\n✗ 兩種方法結果不同") + + except Exception as e: + print(f"✗ 解碼失敗: {e}") + return 1 + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..9855d94 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = -v --tb=short diff --git a/tests/test_decoder.py b/tests/test_decoder.py new file mode 100644 index 0000000..1578884 --- /dev/null +++ b/tests/test_decoder.py @@ -0,0 +1,166 @@ +""" +Unit tests for Fast JPEG Decoder +""" + +import pytest +import numpy as np +import os +import sys + +# Add src to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src', 'python')) + +try: + import fast_jpeg_decoder as fjd +except ImportError: + pytest.skip("Extension not built yet", allow_module_level=True) + + +class TestJPEGDecoder: + """Test cases for JPEG decoder""" + + def test_decoder_creation(self): + """Test that decoder can be created""" + decoder = fjd.JPEGDecoder() + assert decoder is not None + + def test_load_function_exists(self): + """Test that load function exists""" + assert hasattr(fjd, 'load') + assert hasattr(fjd, 'decode') + + def test_decoder_methods(self): + """Test that decoder has required methods""" + decoder = fjd.JPEGDecoder() + assert hasattr(decoder, 'decode_file') + assert hasattr(decoder, 'decode_memory') + assert hasattr(decoder, 'get_image_data') + assert hasattr(decoder, 'width') + assert hasattr(decoder, 'height') + assert hasattr(decoder, 'channels') + + @pytest.mark.skipif( + not os.path.exists('tests/test_data'), + reason="Test data directory not found" + ) + def test_decode_sample_image(self): + """Test decoding a sample JPEG image""" + # This test requires a sample JPEG image in tests/test_data/ + test_image_path = 'tests/test_data/sample.jpg' + + if not os.path.exists(test_image_path): + pytest.skip("Sample image not found") + + # Test using load function + image = fjd.load(test_image_path) + + # Check that result is a numpy array + assert isinstance(image, np.ndarray) + + # Check shape (should be H x W x 3) + assert len(image.shape) == 3 + assert image.shape[2] == 3 + + # Check dtype + assert image.dtype == np.uint8 + + # Check that values are in valid range + assert np.all(image >= 0) + assert np.all(image <= 255) + + @pytest.mark.skipif( + not os.path.exists('tests/test_data'), + reason="Test data directory not found" + ) + def test_decoder_class(self): + """Test using decoder class directly""" + test_image_path = 'tests/test_data/sample.jpg' + + if not os.path.exists(test_image_path): + pytest.skip("Sample image not found") + + decoder = fjd.JPEGDecoder() + success = decoder.decode_file(test_image_path) + + assert success is True + assert decoder.width > 0 + assert decoder.height > 0 + assert decoder.channels in [1, 3] + + image = decoder.get_image_data() + assert isinstance(image, np.ndarray) + assert image.shape == (decoder.height, decoder.width, 3) + + def test_invalid_file(self): + """Test that invalid file raises appropriate error""" + with pytest.raises((RuntimeError, Exception)): + fjd.load('nonexistent_file.jpg') + + def test_decode_bytes(self): + """Test decoding from bytes""" + test_image_path = 'tests/test_data/sample.jpg' + + if not os.path.exists(test_image_path): + pytest.skip("Sample image not found") + + with open(test_image_path, 'rb') as f: + data = f.read() + + image = fjd.load_bytes(data) + assert isinstance(image, np.ndarray) + assert len(image.shape) == 3 + + +class TestImageProperties: + """Test image properties and correctness""" + + @pytest.mark.skipif( + not os.path.exists('tests/test_data'), + reason="Test data directory not found" + ) + def test_image_dimensions(self): + """Test that decoded image has correct dimensions""" + test_image_path = 'tests/test_data/sample.jpg' + + if not os.path.exists(test_image_path): + pytest.skip("Sample image not found") + + decoder = fjd.JPEGDecoder() + decoder.decode_file(test_image_path) + + # Width and height should be positive + assert decoder.width > 0 + assert decoder.height > 0 + + # Get image data + image = decoder.get_image_data() + + # Check that numpy array matches reported dimensions + assert image.shape[0] == decoder.height + assert image.shape[1] == decoder.width + + @pytest.mark.skipif( + not os.path.exists('tests/test_data'), + reason="Test data directory not found" + ) + def test_rgb_channels(self): + """Test that image has correct RGB channel order""" + test_image_path = 'tests/test_data/sample.jpg' + + if not os.path.exists(test_image_path): + pytest.skip("Sample image not found") + + image = fjd.load(test_image_path) + + # Should have 3 channels for RGB + assert image.shape[2] == 3 + + # Each channel should have valid values + for channel in range(3): + channel_data = image[:, :, channel] + assert channel_data.min() >= 0 + assert channel_data.max() <= 255 + + +if __name__ == '__main__': + pytest.main([__file__, '-v']) From 67c498f5c6dbc03c7007aac497dfa3c79bfff808 Mon Sep 17 00:00:00 2001 From: 5000user5000 Date: Thu, 13 Nov 2025 14:08:11 +0800 Subject: [PATCH 05/11] ignore build files --- .gitignore | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a93dc29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ + +# C++ build artifacts +*.o +*.so +*.a +*.dylib + +# Python build artifacts +__pycache__/ +*.pyc +*.pyo +*.pyd +*.egg-info/ +dist/ +build/ +*.egg + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Test images +*.jpg +*.jpeg +*.png +# !tests/test_data/*.jpg +# !tests/test_data/*.jpeg +# !tests/test_data/*.png + +# OS +.DS_Store +Thumbs.db + +# Documentation +info.md \ No newline at end of file From 8e329315e86a2e92c9706116fddaccb97a1015d3 Mon Sep 17 00:00:00 2001 From: 5000user5000 Date: Thu, 13 Nov 2025 14:08:27 +0800 Subject: [PATCH 06/11] doc --- README.md | 108 +++++++++++++++++++++++++++++++++++++++++++++++- tests/README.md | 36 ++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 tests/README.md diff --git a/README.md b/README.md index 5295477..277dd5b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,108 @@ -# Fast-Jpeg-Decoder +# Fast JPEG Decoder 視訊壓縮期末專案 + +高效能 JPEG 解碼器,核心計算使用 C++ 實現,透過 pybind11 提供 Python API。 + +## 特點 + +- **高效能**: 核心解碼邏輯使用 C++ 實現 +- **易用性**: 提供簡潔的 Python API +- **正確性**: 完整實現 JPEG Baseline DCT 解碼流程 +- **可擴展**: 模組化設計,便於後續優化(OpenMP, SIMD) + +## 安裝 + +### 依賴 + +- Python 3.6+ +- NumPy +- pybind11 +- C++ 編譯器(支援 C++11) + +### 從原始碼安裝 + +```bash +# 安裝依賴 +pip install numpy pybind11 + +# 編譯並安裝 +make develop +``` + +## 使用方法 + +### 基本用法 + +```python +import fast_jpeg_decoder as fjd + +# 從檔案載入 JPEG 圖片 +image = fjd.load('photo.jpg') +print(image.shape) # (height, width, 3) + +# 從 bytes 載入 +with open('photo.jpg', 'rb') as f: + data = f.read() +image = fjd.load_bytes(data) +``` + +### 使用 Decoder 類別 + +```python +import fast_jpeg_decoder as fjd + +decoder = fjd.JPEGDecoder() +decoder.decode_file('photo.jpg') + +print(f"Width: {decoder.width}") +print(f"Height: {decoder.height}") +print(f"Channels: {decoder.channels}") + +image = decoder.get_image_data() +``` + +## 測試 + +```bash +# 運行測試 +make test + +# 或直接使用 pytest +pytest tests/ -v +``` + +## 專案結構 + +``` +Fast-Jpeg-Decoder/ +├── src/ +│ ├── cpp/ # C++ 核心實現 +│ ├── bindings/ # pybind11 綁定 +│ └── python/ # Python 包裝 +├── tests/ # 單元測試 +├── benchmarks/ # 效能測試 +├── Makefile # 建構腳本 +└── setup.py # Python 安裝腳本 +``` + +## JPEG 解碼流程 + +1. 解析文件結構(Markers) +2. 霍夫曼解碼(Bitstream) +3. RLE 解碼 +4. 反 ZigZag 排序 +5. 反量化(Dequantization) +6. 反離散餘弦變換(IDCT) +7. 取樣重建(Upsampling) +8. 色彩空間轉換(YCbCr → RGB) + +## 開發計劃 + +- [x] Phase 1: 基礎實現(Naive 版本) +- [ ] Phase 2: CI/CD +- [ ] Phase 3: 效能優化(OpenMP, SIMD) +- [ ] Phase 4: 基準測試與比較 + +## License + +MIT License diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..c2fe0ad --- /dev/null +++ b/tests/README.md @@ -0,0 +1,36 @@ +# 測試資料 + +## 準備測試圖片 + +在運行測試之前,請在 `tests/test_data/` 目錄下放置一個測試用的 JPEG 圖片,命名為 `sample.jpg`。 + +### 使用 Python PIL 生成簡單測試圖片 + +```python +from PIL import Image +import numpy as np + +# 創建一個簡單的測試圖片 +width, height = 640, 480 +img = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8) +Image.fromarray(img).save('tests/test_data/sample.jpg', 'JPEG', quality=90) +``` + +### 或使用現有的 JPEG 圖片 + +```bash +cp /path/to/your/image.jpg tests/test_data/sample.jpg +``` + +## 運行測試 + +```bash +# 確保先編譯擴展 +make develop + +# 運行所有測試 +make test + +# 或使用 pytest +pytest tests/ -v +``` From c1476fe4792fa00390f6efd5c9a05fe0add8c723 Mon Sep 17 00:00:00 2001 From: 5000user5000 Date: Thu, 13 Nov 2025 14:18:06 +0800 Subject: [PATCH 07/11] create CI --- .github/workflows/ci.yml | 80 ++++++++++++++++++++++++++++++++++++++++ README.md | 3 ++ pyproject.toml | 27 ++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 pyproject.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..91ce952 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + push: + branches: [ main, develop, feature/* ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + name: Test with Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential g++ + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install numpy pybind11 pytest pillow + + - name: Build C++ extension + run: | + make clean + make all + + - name: Create test image + run: | + python -c "from PIL import Image; import numpy as np; img = np.random.randint(0, 256, (64, 64, 3), dtype=np.uint8); Image.fromarray(img).save('tests/test_data/sample.jpg', 'JPEG', quality=90)" + + - name: Run tests + run: | + python -m pytest tests/ -v --tb=short + + - name: Test import + run: | + python -c "import sys; sys.path.insert(0, 'src/python'); import fast_jpeg_decoder as fjd; print('Import successful'); print('Version:', fjd.__version__)" + + build-check: + name: Build Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install numpy pybind11 + + - name: Build extension + run: | + make clean + make all + + - name: Check build artifacts + run: | + ls -lh src/python/fast_jpeg_decoder/_fast_jpeg_decoder*.so diff --git a/README.md b/README.md index 277dd5b..d5d0ca6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # Fast JPEG Decoder + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + 視訊壓縮期末專案 高效能 JPEG 解碼器,核心計算使用 C++ 實現,透過 pybind11 提供 Python API。 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..655bbde --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools>=45", "wheel", "pybind11>=2.6.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "fast_jpeg_decoder" +version = "0.1.0" +description = "A high-performance JPEG decoder implemented in C++" +readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "numpy", + "pybind11>=2.6.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0", + "pillow", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" From a19d0e7df123d5b2a258b6896476a9da699141a3 Mon Sep 17 00:00:00 2001 From: 5000user5000 Date: Thu, 13 Nov 2025 14:20:06 +0800 Subject: [PATCH 08/11] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20py=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC=20(=E5=AF=A6=E9=9A=9B=E6=B8=AC=E8=A9=A6=20py=203.8~3.?= =?UTF-8?q?12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d5d0ca6..58cf2b4 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ### 依賴 -- Python 3.6+ +- Python 3.8+ - NumPy - pybind11 - C++ 編譯器(支援 C++11) @@ -102,7 +102,7 @@ Fast-Jpeg-Decoder/ ## 開發計劃 - [x] Phase 1: 基礎實現(Naive 版本) -- [ ] Phase 2: CI/CD +- [x] Phase 2: CI/CD - [ ] Phase 3: 效能優化(OpenMP, SIMD) - [ ] Phase 4: 基準測試與比較 From 10b1219edf0d215b77c07818dd0b0971e653b82a Mon Sep 17 00:00:00 2001 From: 5000user5000 Date: Thu, 13 Nov 2025 14:20:19 +0800 Subject: [PATCH 09/11] =?UTF-8?q?=E4=B8=8A=E5=82=B3=E6=B8=AC=E8=A9=A6?= =?UTF-8?q?=E8=B3=87=E6=96=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +++--- tests/test_data/sample.jpg | Bin 0 -> 1167 bytes 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 tests/test_data/sample.jpg diff --git a/.gitignore b/.gitignore index a93dc29..9762dcb 100644 --- a/.gitignore +++ b/.gitignore @@ -26,9 +26,9 @@ build/ *.jpg *.jpeg *.png -# !tests/test_data/*.jpg -# !tests/test_data/*.jpeg -# !tests/test_data/*.png +!tests/test_data/*.jpg +!tests/test_data/*.jpeg +!tests/test_data/*.png # OS .DS_Store diff --git a/tests/test_data/sample.jpg b/tests/test_data/sample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5cba5d15026c3c570c36d29180a91a4bebd91a59 GIT binary patch literal 1167 zcmex=``2_j6xdp@o1cgOJMMZh|#U;coyG6VInuyV4pa*FVB^NNrR z{vTivX!XqN1l2cOC(lau%ic3n%$}1|Xnp;}i+B-VC zCQY6)b=ve9GiNPYykzOJeA&aSFc^aar4&0 zM~|O8efIpt%U2&ieg5+G+xH(oe}VkP$iNKo7LbH^49#DHKz}i@urRZ*gZ#zFR1U<< zf-J0xhHOHPf$WKe!b(Ps93oB=7j8VrscandK{To8BA1wo$wSqTAg_UaMx4i*$nqK7 zV+eoUV&GwB1V$dSAcH-_pAVPTS{26jP0jpXv8Gn*@^aCenPKzGyZ*Ht3%m2F@MqpH zX{*rXcOUK6)w=xdW5g{Od%5U`AE$25pOgIQ?iaI?)&9GWmPLgw_tTd4+VT?0e7JGy zc6*C6MqmYgbHY=XM%CWKP+)%i>FyVGu`k2sTC6(`(wq!to(3`N-5zV_)i0g#XL~Jg zFW7A$xiz^^XU?@)-wt%;pZoID-Y?v|G^zyb%H^WHFy_0D@w`22cm6z)v<%^d9VZpnv_ekh6Gy{ecgk581 zwC(wG36MMIssY0(b{aT5fewYS*SCRPYH+&z!p%!-?cPI!`4|%WM@?$(`E!QAAeWzB z1#-hiuw*}yZ!XRNhWX1lkYm%mw*20M2va!w;YU!Yg90_@B`6|cjs=A;I8Y&Om25-u bjo+LwGqA_LfIL=X2lULdA3)FW|Gx Date: Fri, 21 Nov 2025 14:56:11 +0800 Subject: [PATCH 10/11] =?UTF-8?q?=E6=96=B0=E6=B8=AC=E8=A9=A6=E8=B3=87?= =?UTF-8?q?=E6=96=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_data/lena.jpg | Bin 0 -> 106738 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/test_data/lena.jpg diff --git a/tests/test_data/lena.jpg b/tests/test_data/lena.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7e83c443702a1525adc1ac0597ed9c95f4f8b862 GIT binary patch literal 106738 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

5s#WCk^?i#VVYTcw-_*PnIO4X+14P=j{ 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 Date: Fri, 21 Nov 2025 15:12:45 +0800 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20BitStream=20b?= =?UTF-8?q?yte=20stuffing=20=E5=92=8C=20RST=20marker=20=E8=99=95=E7=90=86?= =?UTF-8?q?=E9=8C=AF=E8=AA=A4=20=20=20-=20=E4=BF=AE=E6=AD=A3=200xFF=200x00?= =?UTF-8?q?=20byte=20stuffing=20=E6=9C=AA=E8=B7=B3=E9=81=8E=200x00=20?= =?UTF-8?q?=E7=9A=84=E5=9A=B4=E9=87=8D=20bug=20=20=20-=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20RST=20marker=20(0xFF=200xD0-0xD7)=20=E6=9C=AA?= =?UTF-8?q?=E6=AD=A3=E7=A2=BA=E8=B7=B3=E9=81=8E=E7=9A=84=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=20=20=20-=20=E9=87=8D=E6=A7=8B=20BitStream=EF=BC=9A=E5=BC=95?= =?UTF-8?q?=E5=85=A5=2032-bit=20=E7=B7=A9=E8=A1=9D=E5=8D=80=E6=A9=9F?= =?UTF-8?q?=E5=88=B6=E6=8F=90=E5=8D=87=E6=95=88=E8=83=BD=20=20=20-=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20precision=20=E9=A9=97=E8=AD=89=E5=92=8C?= =?UTF-8?q?=E6=B8=85=E7=90=86=E6=9C=AA=E4=BD=BF=E7=94=A8=E8=AE=8A=E6=95=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cpp/bitstream.cpp | 145 +++++++++++++++++++++--------------------- src/cpp/bitstream.h | 9 +-- src/cpp/decoder.cpp | 16 +++-- 3 files changed, 89 insertions(+), 81 deletions(-) diff --git a/src/cpp/bitstream.cpp b/src/cpp/bitstream.cpp index d3085e6..d7de4ee 100644 --- a/src/cpp/bitstream.cpp +++ b/src/cpp/bitstream.cpp @@ -5,114 +5,117 @@ namespace jpeg { BitStream::BitStream(const uint8_t* data, size_t size) - : data_(data), size_(size), byte_pos_(0), bit_pos_(0) { + : data_(data), size_(size), byte_pos_(0), bit_buffer_(0), bits_in_buffer_(0) { if (!data || size == 0) { throw std::invalid_argument("BitStream: invalid data or size"); } } -uint16_t BitStream::readBits(int n_bits) { - if (n_bits <= 0 || n_bits > 16) { - throw std::invalid_argument("BitStream: n_bits must be between 1 and 16"); - } - - uint16_t result = 0; - - for (int i = 0; i < n_bits; ++i) { - // update byte and bit positions - if (bit_pos_ == 8) { - byte_pos_++; - bit_pos_ = 0; - } - - // 檢查是否超出範圍 - if (byte_pos_ >= size_) { - throw std::runtime_error("BitStream: unexpected end of data"); - } - - // 處理 byte stuffing: 如果當前 byte 是 0xFF - uint8_t current_byte = data_[byte_pos_]; - if (current_byte == 0xFF && bit_pos_ == 0) { - // 檢查下一個 byte - if (byte_pos_ + 1 < size_) { - uint8_t next_byte = data_[byte_pos_ + 1]; +void BitStream::fillBuffer() { + while (bits_in_buffer_ <= 24 && byte_pos_ < size_) { + uint8_t byte = data_[byte_pos_++]; + + // 處理 byte stuffing: 如果是 0xFF,檢查下一個 byte + if (byte == 0xFF) { + if (byte_pos_ < size_) { + uint8_t next_byte = data_[byte_pos_]; if (next_byte == 0x00) { - // 這是 byte stuffing,跳過 0x00 - // 繼續使用 0xFF + // 是 byte stuffing (FF 00),跳過 00 + byte_pos_++; } else if (next_byte >= 0xD0 && next_byte <= 0xD7) { - // RST marker,這裡簡單處理,繼續讀取 - // 更完整的實作應該處理 restart interval + // 是 RST marker (FF Dx),跳過整個 marker + byte_pos_++; + continue; // 不要將 0xFF 放入緩衝區,繼續讀取下一個 byte } else { - // 其他 marker,可能是資料結束 - // 目前先簡單拋出異常 - throw std::runtime_error("BitStream: unexpected marker"); + // 其他 marker,可能是錯誤或資料結束 + // 根據 JPEG 規範,這裡應該是資料結束,但我們保守地放入 0xFF } } } - - // 讀取一個 bit (MSB first) - int bit = (current_byte >> (7 - bit_pos_)) & 1; - result = (result << 1) | bit; - bit_pos_++; + + bit_buffer_ = (bit_buffer_ << 8) | byte; + bits_in_buffer_ += 8; } - - return result; } -uint16_t BitStream::peekBits(int n_bits) { - // 保存當前狀態 - size_t saved_byte_pos = byte_pos_; - int saved_bit_pos = bit_pos_; +uint16_t BitStream::readBits(int n_bits) { + if (n_bits <= 0 || n_bits > 16) { + throw std::invalid_argument("BitStream: n_bits must be between 1 and 16"); + } + + if (bits_in_buffer_ < n_bits) { + fillBuffer(); + } + + if (bits_in_buffer_ < n_bits) { + throw std::runtime_error("BitStream: unexpected end of data"); + } - uint16_t result = readBits(n_bits); + uint16_t result = bit_buffer_ >> (bits_in_buffer_ - n_bits); - // 恢復狀態 - byte_pos_ = saved_byte_pos; - bit_pos_ = saved_bit_pos; + bits_in_buffer_ -= n_bits; + bit_buffer_ &= (1U << bits_in_buffer_) - 1; return result; } +uint16_t BitStream::peekBits(int n_bits) { + if (n_bits <= 0 || n_bits > 16) { + throw std::invalid_argument("BitStream: n_bits must be between 1 and 16"); + } + + if (bits_in_buffer_ < n_bits) { + fillBuffer(); + } + + if (bits_in_buffer_ < n_bits) { + throw std::runtime_error("BitStream: unexpected end of data"); + } + + return bit_buffer_ >> (bits_in_buffer_ - n_bits); +} + void BitStream::skipBits(int n_bits) { - readBits(n_bits); + if (bits_in_buffer_ >= n_bits) { + bits_in_buffer_ -= n_bits; + bit_buffer_ &= (1U << bits_in_buffer_) - 1; + } else { + n_bits -= bits_in_buffer_; + bits_in_buffer_ = 0; + bit_buffer_ = 0; + + // 這裡可以優化,但為了簡單起見,先這樣 + size_t bytes_to_skip = n_bits / 8; + byte_pos_ += bytes_to_skip; + n_bits %= 8; + + if (n_bits > 0) { + readBits(n_bits); + } + } } bool BitStream::hasMoreData() const { - return byte_pos_ < size_; + return bits_in_buffer_ > 0 || byte_pos_ < size_; } size_t BitStream::getBitPosition() const { - return byte_pos_ * 8 + bit_pos_; + return byte_pos_ * 8 - bits_in_buffer_; } void BitStream::reset(size_t byte_pos, int bit_pos) { - if (byte_pos >= size_) { + if (byte_pos > size_) { // allow reset to end throw std::out_of_range("BitStream: byte_pos out of range"); } if (bit_pos < 0 || bit_pos >= 8) { throw std::out_of_range("BitStream: bit_pos must be 0-7"); } byte_pos_ = byte_pos; - bit_pos_ = bit_pos; -} - -uint8_t BitStream::getNextByte() { - if (byte_pos_ >= size_) { - throw std::runtime_error("BitStream: unexpected end of data"); - } - - uint8_t byte = data_[byte_pos_++]; - - // 處理 byte stuffing - if (byte == 0xFF && byte_pos_ < size_) { - uint8_t next = data_[byte_pos_]; - if (next == 0x00) { - // Byte stuffing: 跳過 0x00 - byte_pos_++; - } + bits_in_buffer_ = 0; + bit_buffer_ = 0; + if (bit_pos > 0) { + readBits(bit_pos); // 讀取並丟棄,以對齊 bit 位置 } - - return byte; } } // namespace jpeg diff --git a/src/cpp/bitstream.h b/src/cpp/bitstream.h index 8b0b3ea..e6458cd 100644 --- a/src/cpp/bitstream.h +++ b/src/cpp/bitstream.h @@ -60,13 +60,14 @@ class BitStream { const uint8_t* data_; // 原始資料指標 size_t size_; // 資料大小 size_t byte_pos_; // 當前位元組位置 - int bit_pos_; // 當前 bit 位置 (0-7, 0 = MSB) + + uint32_t bit_buffer_; // 位元緩衝區 + int bits_in_buffer_; // 緩衝區中剩餘的 bit 數量 /** - * 讀取下一個 byte,處理 byte stuffing - * @return 下一個有效的 byte + * 填充緩衝區,處理 byte stuffing */ - uint8_t getNextByte(); + void fillBuffer(); }; } // namespace jpeg diff --git a/src/cpp/decoder.cpp b/src/cpp/decoder.cpp index 440b428..3d27949 100644 --- a/src/cpp/decoder.cpp +++ b/src/cpp/decoder.cpp @@ -153,9 +153,13 @@ bool JPEGDecoder::processDQT() { } bool JPEGDecoder::processSOF0() { - uint16_t length = readWord(); - - uint8_t precision = readByte(); // 通常是 8 + readWord(); // length + + uint8_t precision = readByte(); // precision (通常是 8) + if (precision != 8) { + throw std::runtime_error("Only 8-bit precision is supported"); + } + height_ = readWord(); width_ = readWord(); num_components_ = readByte(); @@ -206,13 +210,13 @@ bool JPEGDecoder::processDHT() { } bool JPEGDecoder::processDRI() { - uint16_t length = readWord(); + readWord(); // length restart_interval_ = readWord(); return true; } bool JPEGDecoder::processSOS() { - uint16_t length = readWord(); + readWord(); // length int num_components = readByte(); @@ -412,4 +416,4 @@ void JPEGDecoder::upsample(const std::vector>& y_blocks, } } -} // namespace jpeg +} // namespace jpeg \ No newline at end of file