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资源管理(智能指针管理资源)
-
多阶段压缩管道:
- 图像读取与解码
- 颜色预处理(极限压缩模式)
- 量化与调色板优化
- PNG编码与压缩
-
支持的功能:
- 透明度保留
- 颜色相似度合并
- 自适应抖动控制
- 多种PNG过滤策略
4. 性能指标
-
压缩率比较:
- 基础压缩:中等压缩率
- 优化压缩:比基础高10-20%
- 极限压缩:比基础高20-40%
-
处理速度:
- 基础压缩:最快
- 优化压缩:中等
- 极限压缩:最慢(质量优先)
5. 使用场景
- 网页图像优化
- 移动应用资源压缩
- 游戏素材处理
- 批量图像处理流水线
6. 扩展性
- 可轻松集成到现有系统
- 模块化设计便于添加新算法
- 支持自定义压缩参数
7. 示例输出
- 提供三种压缩级别的结果比较
- 输出详细的压缩统计信息:
- 原始大小
- 压缩后大小
- 压缩百分比
- 处理时间(可选)
#include <iostream>
#include <fstream>
#include <vector>
#include <memory>
#include <filesystem>
#include <cstring>
#include <cmath>
#include <png.h>
#include <string>
#include <iomanip>
#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 * heightconst 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_attrUniqueLiqAttr 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 <algorithm>
#include <vector>
#include <cmath>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;
}