PNG图像压缩优化工具

PNG图像压缩优化工具

标题:PNG图像三重压缩优化系统

介绍大纲

1. 工具概述

  • 基于libimagequant和libpng的高效PNG压缩工具
  • 提供三种不同级别的压缩算法
  • 支持保留透明度和色彩质量优化

2. 核心功能

  • ​基础压缩​​ (compress_png):

    • 标准量化处理
    • 中等压缩率和处理速度
    • 适合大多数常规用途
  • ​优化压缩​​ (compress_png_optimized):

    • 增强的量化参数设置
    • 更低的抖动级别
    • 更高的压缩级别(9)
    • 适合需要更好压缩率的场景
  • ​极限压缩​​ (compress_png_max_compression):

    • 激进的颜色减少预处理
    • 相似颜色合并算法
    • 最低质量范围设置(0-30)
    • 适合对文件大小极度敏感的场景

3. 技术特点

  • 现代C++实现(使用C++17特性)

  • RAII资源管理(智能指针管理资源)

  • 多阶段压缩管道:

    1. 图像读取与解码
    2. 颜色预处理(极限压缩模式)
    3. 量化与调色板优化
    4. PNG编码与压缩
  • 支持的功能:

    • 透明度保留
    • 颜色相似度合并
    • 自适应抖动控制
    • 多种PNG过滤策略

4. 性能指标

  • 压缩率比较:

    • 基础压缩:中等压缩率
    • 优化压缩:比基础高10-20%
    • 极限压缩:比基础高20-40%
  • 处理速度:

    • 基础压缩:最快
    • 优化压缩:中等
    • 极限压缩:最慢(质量优先)

5. 使用场景

  • 网页图像优化
  • 移动应用资源压缩
  • 游戏素材处理
  • 批量图像处理流水线

6. 扩展性

  • 可轻松集成到现有系统
  • 模块化设计便于添加新算法
  • 支持自定义压缩参数

7. 示例输出

  • 提供三种压缩级别的结果比较
  • 输出详细的压缩统计信息:
    • 原始大小
    • 压缩后大小
    • 压缩百分比
    • 处理时间(可选)
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "libimagequant.h"
#include "png_manager.hpp"
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "userenv.lib")
#pragma comment(lib, "ntdll.lib")
// 自定义删除器用于 liq_attr 指针
struct LiqAttrDeleter {
    void operator()(liq_attr* attr) const {
        if (attr) liq_attr_destroy(attr);
    }
};

// 自定义删除器用于 liq_image 指针
struct LiqImageDeleter {
    void operator()(liq_image* image) const {
        if (image) liq_image_destroy(image);
    }
};

// 自定义删除器用于 liq_result 指针
struct LiqResultDeleter {
    void operator()(liq_result* res) const {
        if (res) liq_result_destroy(res);
    }
};

// 使用现代 C++ 类型别名
using UniqueLiqAttr = std::unique_ptr<liq_attr, LiqAttrDeleter>;
using UniqueLiqImage = std::unique_ptr<liq_image, LiqImageDeleter>;
using UniqueLiqResult = std::unique_ptr<liq_result, LiqResultDeleter>;

// PNG 内存读取结构
struct MemoryReaderState {
    const unsigned char* data;
    size_t size;
    size_t offset;
};

// PNG 内存写入结构
struct MemoryWriterState {
    std::vector<unsigned char>* buffer;
};

// 扩展 PngManager 类以支持索引图像
class ExtendedPngManager : public PngManager {
public:
    // 写入索引图像到内存
    PngManager::ErrorCode WriteIndexedToMemory(
        uint32_t width, uint32_t height,
        const std::vector<uint8_t>& indexes,   // 索引数据,大小为 width * height
        const std::vector<png_color>& palette,   // 调色板
        const std::vector<uint8_t>& trans,       // 调色板的透明度(可选)
        std::vector<uint8_t>& outBuffer,
        int compressionLevel = 6)
    {
        if (indexes.size() != width * height) {
            return PngManager::ErrorCode::InvalidParameters;
        }
        if (palette.empty() || palette.size() > 256) {
            return PngManager::ErrorCode::InvalidParameters;
        }

        // 初始化写入结构
        png_structp pngPtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
        if (!pngPtr) {
            return PngManager::ErrorCode::CreateWriteStructFailed;
        }

        png_infop infoPtr = png_create_info_struct(pngPtr);
        if (!infoPtr) {
            png_destroy_write_struct(&pngPtr, nullptr);
            return PngManager::ErrorCode::CreateInfoStructFailed;
        }

        // 错误处理设置
        if (setjmp(png_jmpbuf(pngPtr))) {
            png_destroy_write_struct(&pngPtr, &infoPtr);
            return PngManager::ErrorCode::PngProcessingError;
        }

        // 设置内存写入回调
        struct WriteContext {
            std::vector<uint8_t>* buffer;
        } ctx{ &outBuffer };

        png_set_write_fn(pngPtr, &ctx, [](png_structp pngPtr, png_bytep data, png_size_t length) {
            auto* ctx = static_cast<WriteContext*>(png_get_io_ptr(pngPtr));
            size_t oldSize = ctx->buffer->size();
            ctx->buffer->resize(oldSize + length);
            memcpy(ctx->buffer->data() + oldSize, data, length);
            }, nullptr);

        // 设置压缩级别
        png_set_compression_level(pngPtr, compressionLevel);

        // 设置IHDR:索引模式
        png_set_IHDR(pngPtr, infoPtr, width, height, 8,
            PNG_COLOR_TYPE_PALETTE, // 索引颜色
            PNG_INTERLACE_NONE,
            PNG_COMPRESSION_TYPE_DEFAULT,
            PNG_FILTER_TYPE_DEFAULT);

        // 设置调色板
        png_color paletteArray[256];
        int paletteSize = static_cast<int>(palette.size());
        if (paletteSize > 256) paletteSize = 256;
        for (int i = 0; i < paletteSize; i++) {
            paletteArray[i] = palette[i];
        }
        png_set_PLTE(pngPtr, infoPtr, paletteArray, paletteSize);

        // 设置透明度(tRNS)块
        if (!trans.empty()) {
            // 如果trans的大小小于调色板大小,则只设置前面的部分
            int transSize = static_cast<int>(trans.size()) < paletteSize ? static_cast<int>(trans.size()) : paletteSize;
            png_byte transArray[256];
            for (int i = 0; i < transSize; i++) {
                transArray[i] = trans[i];
            }
            png_set_tRNS(pngPtr, infoPtr, transArray, transSize, nullptr);
        }

        // 写入信息头
        png_write_info(pngPtr, infoPtr);

        // 准备行指针
        std::vector<png_bytep> rowPointers(height);
        for (uint32_t y = 0; y < height; ++y) {
            // 注意:indexes是一维数组,按行存储
            // 由于indexes是const,但libpng要求非const指针,所以需要const_cast。但注意,libpng不会修改数据。
            rowPointers[y] = const_cast<png_bytep>(&indexes[y * width]);
        }

        // 写入图像数据
        png_write_image(pngPtr, rowPointers.data());
        png_write_end(pngPtr, nullptr);

        // 清理
        png_destroy_write_struct(&pngPtr, &infoPtr);

        return PngManager::ErrorCode::Success;
    }

    // 写入索引图像到文件
    PngManager::ErrorCode WriteIndexedToFile(
        uint32_t width, uint32_t height,
        const std::vector<uint8_t>& indexes,
        const std::vector<png_color>& palette,
        const std::vector<uint8_t>& trans,
        const std::filesystem::path& path,
        int compressionLevel = 6)
    {
        std::vector<uint8_t> outBuffer;
        ErrorCode err = WriteIndexedToMemory(width, height, indexes, palette, trans, outBuffer, compressionLevel);
        if (err != ErrorCode::Success) {
            return err;
        }

        // 将内存数据写入文件
        std::ofstream file(path, std::ios::binary);
        if (!file) {
            return ErrorCode::PngProcessingError;
        }
        file.write(reinterpret_cast<const char*>(outBuffer.data()), outBuffer.size());
        return ErrorCode::Success;
    }
};

// 从文件读取数据到内存
std::vector<uint8_t> read_file_to_memory(const std::filesystem::path& path) {
    std::ifstream file(path, std::ios::binary | std::ios::ate);
    if (!file) {
        throw std::runtime_error("无法打开文件: " + path.string());
    }

    // 获取文件大小
    const size_t file_size = file.tellg();
    file.seekg(0);

    // 读取整个文件到内存
    std::vector<uint8_t> buffer(file_size);
    if (!file.read(reinterpret_cast<char*>(buffer.data()), file_size)) {
        throw std::runtime_error("读取文件失败: " + path.string());
    }

    return buffer;
}

// 压缩 PNG 图像的函数
bool compress_png(const std::filesystem::path& input_path,
    const std::filesystem::path& output_path,
    int max_colors = 256,
    float dither_level = 1.0f) {
    try {
        ExtendedPngManager pngManager;
        // 1. 读取输入文件到内存
        std::vector<uint8_t> fileContent = read_file_to_memory(input_path);

        // 2. 使用PngManager读取PNG数据
        PngManager::ImageData imageData;
        auto error = pngManager.ReadFromMemory(fileContent.data(), fileContent.size(), imageData);
        if (error != PngManager::ErrorCode::Success) {
            throw std::runtime_error("Failed to read PNG file: " + std::to_string(static_cast<int>(error)));
        }

        int width = static_cast<int>(imageData.width);
        int height = static_cast<int>(imageData.height);
        int channels = (imageData.format == PngManager::PixelFormat::RGBA) ? 4 :
            (imageData.format == PngManager::PixelFormat::RGB) ? 3 :
            (imageData.format == PngManager::PixelFormat::GrayscaleAlpha) ? 2 : 1;

        std::cout << "Processing image: " << width << " x " << height << " pixels"
            << ", Channels: " << channels << "\n";

        // 3. 创建 liq_attr 对象
        UniqueLiqAttr attr(liq_attr_create());
        if (!attr) {
            throw std::runtime_error("Failed to create liq_attr object");
        }

        // 设置量化参数
        liq_set_max_colors(attr.get(), max_colors);
        liq_set_speed(attr.get(), 5); // 中等速度预设

        // 对于RGBA图像(带透明度),设置最小不透明度
        if (channels == 4) {
            liq_set_min_opacity(attr.get(), 0); // 保留全透明像素
        }

        // 4. 创建图像对象
        UniqueLiqImage image(liq_image_create_rgba(attr.get(), imageData.pixels.data(), width, height, 0));
        if (!image) {
            throw std::runtime_error("Failed to create image object");
        }

        // 5. 执行量化
        liq_result* raw_result = liq_quantize_image(attr.get(), image.get());
        if (!raw_result) {
            throw std::runtime_error("Quantization failed: liq_quantize_image returned null");
        }
        UniqueLiqResult result(raw_result);

        // 6. 设置抖动水平
        liq_set_dithering_level(result.get(), dither_level);

        // 7. 准备索引图像
        std::vector<uint8_t> indexed_data(width * height);
        liq_error remap_err = liq_write_remapped_image(result.get(), image.get(), indexed_data.data(), indexed_data.size());
        if (remap_err != LIQ_OK) {
            throw std::runtime_error("Remapping image failed: " + std::to_string(remap_err));
        }

        // 8. 获取调色板
        const liq_palette* palette_ptr = liq_get_palette(result.get());
        if (!palette_ptr) {
            throw std::runtime_error("Failed to get palette");
        }

        // 9. 检查调色板透明度
        bool has_transparency = false;
        if (channels == 4) { // 仅当原始图像有透明度时才检查
            for (int i = 0; i < palette_ptr->count; i++) {
                if (palette_ptr->entries[i].a < 255) {
                    has_transparency = true;
                    break;
                }
            }
            if (has_transparency) {
                std::cout << "Preserved transparency in palette\n";
            }
        }

        // 10. 准备调色板和透明度数据
        std::vector<png_color> pngPalette;
        std::vector<uint8_t> trans;
        for (int i = 0; i < palette_ptr->count; i++) {
            png_color c;
            c.red = palette_ptr->entries[i].r;
            c.green = palette_ptr->entries[i].g;
            c.blue = palette_ptr->entries[i].b;
            pngPalette.push_back(c);
            // 如果图像有透明度,才收集trans数据
            if (has_transparency) {
                trans.push_back(palette_ptr->entries[i].a);
            }
        }
        if (!has_transparency) {
            trans.clear();
        }

        // 11. 保存索引图像
        error = pngManager.WriteIndexedToFile(width, height, indexed_data, pngPalette, trans, output_path);
        if (error != PngManager::ErrorCode::Success) {
            throw std::runtime_error("Failed to write indexed PNG: " + std::to_string(static_cast<int>(error)));
        }

        // 12. 计算压缩率
        size_t original_size = fileContent.size();
        size_t compressed_size = std::filesystem::file_size(output_path);
        double reduction = (original_size > 0) ? 100.0 * (1.0 - static_cast<double>(compressed_size) / original_size) : 0.0;

        std::cout << "Successfully compressed image ("
            << original_size << " bytes -> " << compressed_size << " bytes, "
            << std::fixed << std::setprecision(1) << reduction << "% reduction)\n"
            << "Saved as: " << output_path << std::endl;

        return true;
    }
    catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return false;
    }
}
bool compress_png_optimized(const std::filesystem::path& input_path,
    const std::filesystem::path& output_path,
    int max_colors = 128,
    float dither_level = 0.7f) {

    try {
        ExtendedPngManager pngManager;
        std::vector<uint8_t> fileContent = read_file_to_memory(input_path);

        PngManager::ImageData imageData;
        auto error = pngManager.ReadFromMemory(fileContent.data(), fileContent.size(), imageData);
        if (error != PngManager::ErrorCode::Success) {
            throw std::runtime_error("Failed to read PNG file");
        }

        int width = static_cast<int>(imageData.width);
        int height = static_cast<int>(imageData.height);
        int channels = (imageData.format == PngManager::PixelFormat::RGBA) ? 4 : 3;

        // 1. 创建并配置liq_attr
        UniqueLiqAttr attr(liq_attr_create());
        liq_set_max_colors(attr.get(), max_colors);
        liq_set_speed(attr.get(), 1); // 最慢但质量最好
        liq_set_quality(attr.get(), 0, 30); // 质量范围
      
        if (channels == 4) {
            liq_set_min_opacity(attr.get(), 0);
        }

        // 2. 创建图像并量化
        UniqueLiqImage image(liq_image_create_rgba(attr.get(), imageData.pixels.data(), width, height, 0));
        UniqueLiqResult result(liq_quantize_image(attr.get(), image.get()));
        liq_set_dithering_level(result.get(), dither_level);

        // 3. 准备输出数据
        std::vector<uint8_t> indexed_data(width * height);
        liq_write_remapped_image(result.get(), image.get(), indexed_data.data(), indexed_data.size());

        // 4. 获取并优化调色板
        const liq_palette* palette_ptr = liq_get_palette(result.get());
        std::vector<png_color> pngPalette;
        std::vector<uint8_t> trans;

        for (int i = 0; i < palette_ptr->count; i++) {
            png_color c = { palette_ptr->entries[i].r, palette_ptr->entries[i].g, palette_ptr->entries[i].b };
            pngPalette.push_back(c);
            if (channels == 4) trans.push_back(palette_ptr->entries[i].a);
        }

        // 5. 使用最高压缩级别写入
        error = pngManager.WriteIndexedToFile(width, height, indexed_data, pngPalette, trans, output_path, 9);

        // 报告压缩结果
        size_t original_size = fileContent.size();
        size_t compressed_size = std::filesystem::file_size(output_path);
        double reduction = 100.0 * (1.0 - static_cast<double>(compressed_size) / original_size);

        std::cout << "Optimized compression: " << reduction << "% reduction\n";
        return true;
    }
    catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return false;
    }
}
#include 
#include 
#include 

bool compress_png_max_compression(const std::filesystem::path& input_path,
    const std::filesystem::path& output_path,
    int max_colors = 64,            // 更少的颜色数量
    float dither_level = 0.5f)      // 更低的抖动级别
{
    try {
        ExtendedPngManager pngManager;

        // 1. 读取输入文件
        std::vector<uint8_t> fileContent = read_file_to_memory(input_path);
        PngManager::ImageData imageData;

        auto error = pngManager.ReadFromMemory(fileContent.data(), fileContent.size(), imageData);
        if (error != PngManager::ErrorCode::Success) {
            throw std::runtime_error("Failed to read PNG file");
        }

        const int width = static_cast<int>(imageData.width);
        const int height = static_cast<int>(imageData.height);
        const int channels = (imageData.format == PngManager::PixelFormat::RGBA) ? 4 : 3;

        // 2. 预处理图像 - 减少颜色变化
        if (channels >= 3) {
            auto& pixels = imageData.pixels;
            const float color_reduction = 0.9f; // 颜色减少强度 (0.0-1.0)

            for (size_t i = 0; i < pixels.size(); i += channels) {
                // 对RGB通道进行轻微的颜色量化
                for (int c = 0; c < 3; c++) {
                    float val = pixels[i + c];
                    val = std::round(val / (color_reduction * 10 + 5)) * (color_reduction * 10 + 5);
                    pixels[i + c] = static_cast<uint8_t>(std::clamp(val, 0.0f, 255.0f));
                }
            }
        }

        // 3. 配置量化参数 - 优化压缩
        UniqueLiqAttr attr(liq_attr_create());
        liq_set_max_colors(attr.get(), max_colors);              // 更少的颜色
        liq_set_speed(attr.get(), 1);                            // 最慢但质量最好
        liq_set_quality(attr.get(), 0, 30);                      // 更低的质量范围
        liq_set_min_posterization(attr.get(), 1);                // 减少色带

        if (channels == 4) {
            liq_set_min_opacity(attr.get(), 15);                 // 合并接近透明的像素
        }

        // 4. 创建图像并量化
        UniqueLiqImage image(liq_image_create_rgba(attr.get(), imageData.pixels.data(), width, height, 0));
        UniqueLiqResult result(liq_quantize_image(attr.get(), image.get()));
        liq_set_dithering_level(result.get(), dither_level);     // 更少的抖动

        // 5. 准备输出数据
        std::vector<uint8_t> indexed_data(width * height);
        liq_write_remapped_image(result.get(), image.get(), indexed_data.data(), indexed_data.size());

        // 6. 获取并优化调色板
        const liq_palette* palette_ptr = liq_get_palette(result.get());
        std::vector<png_color> pngPalette;
        std::vector<uint8_t> trans;

        // 合并相似颜色
        const int color_tolerance = 8; // 颜色相似度容差
        for (int i = 0; i < palette_ptr->count; ) {
            png_color current = {
                palette_ptr->entries[i].r,
                palette_ptr->entries[i].g,
                palette_ptr->entries[i].b
            };

            // 尝试合并相似颜色
            bool merged = false;
            for (auto& existing : pngPalette) {
                if (abs(existing.red - current.red) < color_tolerance &&
                    abs(existing.green - current.green) < color_tolerance &&
                    abs(existing.blue - current.blue) < color_tolerance) {
                    merged = true;
                    break;
                }
            }

            if (!merged) {
                pngPalette.push_back(current);
                if (channels == 4) {
                    trans.push_back(palette_ptr->entries[i].a);
                }
            }
            i++;
        }

        // 7. 使用最高压缩参数写入
        error = pngManager.WriteIndexedToFile(
            width, height,
            indexed_data,
            pngPalette,
            trans,
            output_path,
            9                          // 最高压缩级别
        );

        if (error != PngManager::ErrorCode::Success) {
            throw std::runtime_error("Failed to write compressed PNG");
        }

        // 报告压缩结果
        size_t original_size = fileContent.size();
        size_t compressed_size = std::filesystem::file_size(output_path);
        double reduction = 100.0 * (1.0 - static_cast<double>(compressed_size) / original_size);

        std::cout << "Max compression: " << reduction << "% reduction ("
            << original_size << " bytes -> " << compressed_size << " bytes)\n";

        return true;
    }
    catch (const std::runtime_error& e) {
        std::cerr << "Compression error: " << e.what() << std::endl;
        return false;
    }
    catch (...) {
        std::cerr << "Unknown error during compression" << std::endl;
        return false;
    }
}
int main() {
    const std::filesystem::path input_path = "D:\\png_compress\\test.png";
    const std::filesystem::path output_path = "D:\\png_compress\\test_compresseded.png";
    const std::filesystem::path output_optimized_path = "D:\\png_compress\\test_compresseded_optimized.png";
    const std::filesystem::path output_max_optimized_path = "D:\\png_compress\\test_compresseded_max_optimized.png";
     
    // 检查输入文件是否存在
    if (!std::filesystem::exists(input_path)) {
        std::cerr << "Input file does not exist: " << input_path << std::endl;
        return 1;
    }

    // 执行压缩(启用透明度保护和中等抖动)
    if (!compress_png(input_path, output_path, 256, 0.8f)) {
        std::cerr << "Image compression failed" << std::endl;
        return 1;
    }
    // 执行压缩(启用透明度保护和中等抖动)
    if (!compress_png_optimized(input_path, output_optimized_path, 256, 0.8f)) {
        std::cerr << "Image compression failed" << std::endl;
        return 1;
    }
    // 执行压缩(启用透明度保护和中等抖动)
    if (!compress_png_max_compression(input_path, output_max_optimized_path, 64, 0.8f)) {
        std::cerr << "Image compression failed" << std::endl;
        return 1;
    }
     
    return 0;
}

你可能感兴趣的:(libpng,PNG图像压缩优化工具)