最近在鼓捣自己的图片处理小工具时遇到了一个很简单的需求:对图片进行压缩从而适配不同的社交媒体,比如小红书(1280p),朋友圈(1080p)等,避免上传高清图片时被平台自动压缩从而降低图片的呈现效果。

原图(2505x3339),其实最原始的图超过了1亿像素,达到了70多M,这里无法上传

相信对于有一定图像处理经验的程序员而言,这个功能听上去太过于简单了,请出我们大名鼎鼎的OpenCV进行一波操作:

cv::imread()
cv::resize()
cv::imwrite()

不就解决了?当然再考虑一下缩放质量,那么插值时选择双三次插值cv::INTER_CUBIC应该就没问题了。


这样压缩后的图片如下图所示:

简单的opencv处理后的图片

和原图一对比我直接傻眼:这颗粒感是怎么回事?怎么照片颜色也不对了?再对比一下其他软件压缩后的质感和颜色,我意识到事情没这么简单。

一番研究后我发现了第一个问题:双三次插值以及其他一些更复杂的插值方法例如cv::INTER_LANCZOS4等,重在保留图片细节,所以压缩后会产生波纹和锯齿效应。相比之下,区域插值(cv::INTER_AREA)通过像素区域关系进行重采样,在缩小图像时效果是最好的,可有效避免波纹锯齿现象。


换成区域插值后果然颗粒感消失了:

opencv区域插值后的图片

但是颜色还是不对?于是又一波研究后我学到了一个新东西:ICC Profile,简而言之它是一个包含设备色彩特性、转换规则和元数据的二进制文件,用于确保颜色在不同设备之间的一致性。正常情况下ICC Profile会被写入到JPG图片中的APP2段(0xFFE2)中作为元数据之一,这样在不同设备上打开图片时就可以尽可能还原色彩。


而问题就出在这里:OpenCV直接无视jpg图片的所有元数据!我进而发现其生成的图片里的一些其他数据也被重置了,比如dpi从原图的240变成了96。这里的DPI信息是存储在APP1段中的两个Tag中的:XResolution(0x011A)与YResolution(0x011B)。更进一步来说,APP1段中包含了更多jpg的元数据,这些Tag统称为Exif。自然地,所有的Exif Tag也被OpenCV忽略了。


所以为了保证压缩后的图片效果,正确的工作流程应该是:读取jpg图片 - 读取原图的Exif(APP1)和ICC(APP2) - 用区域插值对图片数据进行resize - 写入resize后的图片数据 - 写入原图的Exif和ICC。这里值得注意,如果原图中的Exif包含了图片的尺寸信息ExifImageWidth(0xA002)和ExifImageHeight(0xA003),那么压缩后最好是对其进行正确修改。

那么问题就很明确了,我需要实现对于JPG文件进行读取和写入Exif以及ICC的函数。第一件事当然是看看有没有现成的开源库,一番搜索后我尝试了libjpeg, libexif, lcms等开源库,可能是由于我就偏喜欢在windows上用mingw而不是msvc,同时我用的conan2来进行包管理,上述的开源库各自都有问题,要么无法编译,要么运行时各种出错。

众所周知C++程序员最喜欢的就是造轮子,折腾了半天我决定(在AI的帮助下)自己实现这几个函数,本质上其实也不复杂,就是根据规则进行文件数据的读取和写入。

std::vector<uint8_t> getAPP1EXIFSegment(const char *filePathUnsafe)
{
    const auto filePath = hex::filesystem::stringToWString({filePathUnsafe});
    std::ifstream file(filePath.c_str(), std::ios::binary);
    if (!file.is_open())
    {
        LOG_ERROR("Could not open file " << filePathUnsafe);
        return {};
    }
    // check JEPG SOI
    uint16_t soi = readData<uint16_t>(file, true);
    if (soi != JPEG_MARKER)
    {
        LOG_ERROR("Invalid JPEG SOI: " << soi);
        return {};
    }
    // iterate through segments
    while (file)
    {
        uint16_t marker = readData<uint16_t>(file, true);
        uint16_t segmentLength = readData<uint16_t>(file, true);
        segmentLength -= 2;
        if (marker == APP1_MARKER)
        {
            // read the entire APP1 segment
            std::vector<uint8_t> exifData(segmentLength);
            file.read(reinterpret_cast<char *>(exifData.data()), segmentLength);
            return exifData;
        }
        // not APP1, skip
        file.seekg(segmentLength, std::ios::cur);
    }
    LOG_INFO("EXIF segment not found");
    return {};
}

bool isValidEXIF(const std::vector<uint8_t> &exifData)
{
    if (exifData.size() < 6)
    {
        return false;
    }
    return (std::memcmp(exifData.data(), "Exif\0\0", 6) == 0);
}

struct EXIFHeader
{
    bool bigEndian;
    uint32_t ifdOffset; // offset of IFD0 to TIFF header
};

EXIFHeader parseTIFFHeader(const std::vector<uint8_t> &exifData)
{
    EXIFHeader header{};
    // TIFF header starts from offset 6 and is at least 10 bytes long
    if (exifData.size() < 10 + 6)
    {
        LOG_ERROR("Invalid TIFF header");
        return header;
    }
    // check byte order
    if (std::memcmp(&exifData[6], "II", 2) == 0)
    {
        header.bigEndian = false;
    }
    else if (std::memcmp(&exifData[6], "MM", 2) == 0)
    {
        header.bigEndian = true;
    }
    else
    {
        LOG_ERROR("Invalid byte order in TIFF header");
        return header;
    }
    uint16_t magic = readData<uint16_t>(exifData, 8, header.bigEndian);
    if (magic != TIFF_MARKER)
    {
        LOG_ERROR("Invalid TIFF magic number: " << magic);
        return header;
    }
    // IFD0 offset
    header.ifdOffset = readData<uint32_t>(exifData, 10, header.bigEndian);
    return header;
}

struct EXIFTag
{
    uint16_t tagId;
    uint16_t dataType;
    uint32_t count;
    uint32_t valueOffset;
};

std::vector<EXIFTag> readIFD(const std::vector<uint8_t> &exifData, uint32_t offset, bool bigEndian)
{
    std::vector<EXIFTag> tags;
    if (exifData.size() < offset + 2)
    {
        LOG_ERROR("Invalid IFD offset");
        return tags;
    }
    uint16_t numEntries = readData<uint16_t>(exifData, offset, bigEndian);
    offset += 2;
    for (uint16_t i = 0; i < numEntries; i++)
    {
        if (exifData.size() < offset + 12)
        {
            break;
        }
        EXIFTag tag;
        tag.tagId = readData<uint16_t>(exifData, offset, bigEndian);
        tag.dataType = readData<uint16_t>(exifData, offset + 2, bigEndian);
        tag.count = readData<uint32_t>(exifData, offset + 4, bigEndian);
        tag.valueOffset = readData<uint32_t>(exifData, offset + 8, bigEndian);
        tags.push_back(tag);
        offset += 12;
    }
    return tags;
}

uint32_t getTypeSize(uint16_t dataType)
{
    switch (dataType)
    {
    case 1: // BYTE
    case 2: // ASCII
    case 7: // UNDEFINED
        return 1;
    case 3: // SHORT
        return 2;
    case 4: // LONG
    case 9: // SLONG
        return 4;
    case 5:  // RATIONAL
    case 10: // SRATIONAL
        return 8;
    default:
        return 0;
    }
}

bool getTagValueData(const std::vector<uint8_t> &exifData, uint32_t tiffHeaderStart, const EXIFTag &tag, bool bigEndian,
                     std::vector<uint8_t> &valueData)
{
    uint32_t typeSize = getTypeSize(tag.dataType);
    uint32_t totalSize = tag.count * typeSize;
    if (totalSize <= 4)
    {
        // data is stored inline in tag.valueOffset
        valueData.resize(totalSize);
        uint32_t inlineVal = tag.valueOffset;
        for (uint32_t i = 0; i < totalSize; i++)
        {
            if (bigEndian)
            {
                valueData[i] = (inlineVal >> (8 * (3 - i))) & 0xFF;
            }
            else
            {
                valueData[i] = (inlineVal >> (8 * i)) & 0xFF;
            }
        }
    }
    else
    {
        uint32_t valueOffset = tiffHeaderStart + tag.valueOffset;
        if (exifData.size() < valueOffset + totalSize)
        {
            return false;
        }
        valueData.assign(exifData.begin() + valueOffset, exifData.begin() + valueOffset + totalSize);
    }
    return true;
}

std::string getTagValueString(const std::vector<uint8_t> &exifData, uint32_t tiffHeaderStart, const EXIFTag &tag,
                              bool bigEndian)
{
    std::vector<uint8_t> valueData;
    if (!getTagValueData(exifData, tiffHeaderStart, tag, bigEndian, valueData))
        return "Error reading value";
    std::ostringstream oss;
    uint32_t typeSize = getTypeSize(tag.dataType);
    switch (tag.dataType)
    {
    case 1: // BYTE
    case 7: // UNDEFINED, output as hex
        for (size_t i = 0; i < valueData.size(); i++)
        {
            oss << std::hex << std::uppercase << static_cast<int>(valueData[i]);
            if (i < valueData.size() - 1)
                oss << " ";
        }
        break;
    case 2: // ASCII
    {
        std::string str(valueData.begin(), valueData.end());
        size_t pos = str.find('\0');
        if (pos != std::string::npos)
            str = str.substr(0, pos);
        oss << str;
        break;
    }
    case 3: // SHORT
    {
        if (tag.count == 1 && valueData.size() >= 2)
        {
            uint16_t val = (bigEndian) ? (valueData[0] << 8 | valueData[1]) : (valueData[1] << 8 | valueData[0]);
            oss << val;
        }
        else
        {
            for (size_t i = 0; i + 1 < valueData.size(); i += 2)
            {
                uint16_t val =
                    (bigEndian) ? (valueData[i] << 8 | valueData[i + 1]) : (valueData[i + 1] << 8 | valueData[i]);
                oss << val;
                if (i + 2 < valueData.size())
                    oss << ", ";
            }
        }
        break;
    }
    case 4: // LONG
    {
        if (tag.count == 1 && valueData.size() >= 4)
        {
            uint32_t val;
            if (bigEndian)
                val = (valueData[0] << 24) | (valueData[1] << 16) | (valueData[2] << 8) | valueData[3];
            else
                val = (valueData[3] << 24) | (valueData[2] << 16) | (valueData[1] << 8) | valueData[0];
            oss << val;
        }
        else
        {
            for (size_t i = 0; i + 3 < valueData.size(); i += 4)
            {
                uint32_t val;
                if (bigEndian)
                    val = (valueData[i] << 24) | (valueData[i + 1] << 16) | (valueData[i + 2] << 8) | valueData[i + 3];
                else
                    val = (valueData[i + 3] << 24) | (valueData[i + 2] << 16) | (valueData[i + 1] << 8) | valueData[i];
                oss << val;
                if (i + 4 < valueData.size())
                    oss << ", ";
            }
        }
        break;
    }
    case 5: // RATIONAL
    {
        for (size_t i = 0; i + 7 < valueData.size(); i += 8)
        {
            uint32_t num, den;
            if (bigEndian)
            {
                num = (valueData[i] << 24) | (valueData[i + 1] << 16) | (valueData[i + 2] << 8) | valueData[i + 3];
                den = (valueData[i + 4] << 24) | (valueData[i + 5] << 16) | (valueData[i + 6] << 8) | valueData[i + 7];
            }
            else
            {
                num = (valueData[i + 3] << 24) | (valueData[i + 2] << 16) | (valueData[i + 1] << 8) | valueData[i];
                den = (valueData[i + 7] << 24) | (valueData[i + 6] << 16) | (valueData[i + 5] << 8) | valueData[i + 4];
            }
            if (den != 0)
                oss << num << "/" << den << " (" << static_cast<double>(num) / den << ")";
            else
                oss << num << "/0";
            if (i + 8 < valueData.size())
                oss << ", ";
        }
        break;
    }
    case 9: // SLONG
    {
        if (tag.count == 1 && valueData.size() >= 4)
        {
            int32_t val;
            if (bigEndian)
                val = (valueData[0] << 24) | (valueData[1] << 16) | (valueData[2] << 8) | valueData[3];
            else
                val = (valueData[3] << 24) | (valueData[2] << 16) | (valueData[1] << 8) | valueData[0];
            oss << val;
        }
        else
        {
            for (size_t i = 0; i + 3 < valueData.size(); i += 4)
            {
                int32_t val;
                if (bigEndian)
                    val = (valueData[i] << 24) | (valueData[i + 1] << 16) | (valueData[i + 2] << 8) | valueData[i + 3];
                else
                    val = (valueData[i + 3] << 24) | (valueData[i + 2] << 16) | (valueData[i + 1] << 8) | valueData[i];
                oss << val;
                if (i + 4 < valueData.size())
                    oss << ", ";
            }
        }
        break;
    }
    case 10: // SRATIONAL
    {
        for (size_t i = 0; i + 7 < valueData.size(); i += 8)
        {
            int32_t num, den;
            if (bigEndian)
            {
                num = (valueData[i] << 24) | (valueData[i + 1] << 16) | (valueData[i + 2] << 8) | valueData[i + 3];
                den = (valueData[i + 4] << 24) | (valueData[i + 5] << 16) | (valueData[i + 6] << 8) | valueData[i + 7];
            }
            else
            {
                num = (valueData[i + 3] << 24) | (valueData[i + 2] << 16) | (valueData[i + 1] << 8) | valueData[i];
                den = (valueData[i + 7] << 24) | (valueData[i + 6] << 16) | (valueData[i + 5] << 8) | valueData[i + 4];
            }
            if (den != 0)
                oss << num << "/" << den << " (" << static_cast<double>(num) / den << ")";
            else
                oss << num << "/0";
            if (i + 8 < valueData.size())
                oss << ", ";
        }
        break;
    }
    default:
        oss << "Unsupported data type";
        break;
    }
    return oss.str();
}

std::string getTagName(uint16_t tagId)
{
    auto it = EXIF_TAG_NAMES.find(tagId);
    if (it != EXIF_TAG_NAMES.end())
    {
        return it->second;
    }
    std::ostringstream oss;
    oss << "Unknown (0x" << std::hex << tagId << ")";
    return oss.str();
}

std::vector<uint8_t> getICCProfileBuffer(const char *filePath)
{
    const auto filePathSafe = hex::filesystem::stringToWString({filePath});
    std::ifstream file(filePathSafe.c_str(), std::ios::binary);
    if (!file.is_open())
    {
        LOG_ERROR("Could not open file " << filePath);
        return {};
    }

    const auto soi = readData<uint16_t>(file, true);
    if (soi != JPEG_MARKER)
    {
        LOG_ERROR("Invalid JPEG SOI: " << soi);
        return {};
    }

    std::map<uint8_t, std::vector<uint8_t>> iccSegments;
    uint8_t expectedSegmentCount = 0;
    bool foundICC = false;

    while (file)
    {
        uint16_t marker = readData<uint16_t>(file, true);
        if (marker == SOS_MARKER)
            break;

        uint16_t segmentLength = readData<uint16_t>(file, true);
        uint16_t dataLength = segmentLength - 2;

        // ICC Profile:
        //  - 12 bytes of ASCII string "ICC_PROFILE\0" (1 null byte included!)
        //  - 1 byte segment index starting from 1
        //  - 1 byte total segments
        //  - rest: ICC data of this segment
        if (marker == APP2_MARKER)
        {
            std::vector<uint8_t> segmentData(dataLength);
            file.read(reinterpret_cast<char *>(segmentData.data()), dataLength);

            size_t signatureLength = sizeof(ICC_SIGNATURE) - 1;
            if (dataLength >= signatureLength + 3 && memcmp(segmentData.data(), ICC_SIGNATURE, signatureLength) == 0 &&
                segmentData[signatureLength] == 0)
            {
                uint8_t seqNo = segmentData[signatureLength + 1];
                uint8_t segCount = segmentData[signatureLength + 2];

                if (expectedSegmentCount == 0)
                    expectedSegmentCount = segCount;
                else if (expectedSegmentCount != segCount)
                {
                    LOG_ERROR("Mismatch in ICC_PROFILE segment count");
                    return {};
                }

                std::vector<uint8_t> iccData(segmentData.begin() + signatureLength + 3, segmentData.end());
                iccSegments[seqNo] = iccData;
                foundICC = true;
            }
        }
        else
        {
            file.seekg(dataLength, std::ios::cur);
        }
    }

    if (!foundICC || iccSegments.size() != expectedSegmentCount)
    {
        LOG_ERROR("Incomplete or missing ICC_PROFILE segments");
        return {};
    }

    std::vector<uint8_t> iccBuffer;
    for (uint8_t i = 1; i <= expectedSegmentCount; i++)
    {
        if (iccSegments.find(i) == iccSegments.end())
        {
            LOG_ERROR("Missing ICC_PROFILE segment " << static_cast<int>(i));
            return {};
        }
        iccBuffer.insert(iccBuffer.end(), iccSegments[i].begin(), iccSegments[i].end());
    }

    return iccBuffer;
}

bool writeICCProfileToJPEG(const char *outputPathUnsafe, const std::vector<uint8_t> &iccProfile)
{
    if (iccProfile.empty())
    {
        LOG_ERROR("No ICC Profile data to write.");
        return false;
    }
    const auto outputPath = hex::filesystem::stringToWString({outputPathUnsafe});
    std::ifstream inputFile(outputPath.c_str(), std::ios::binary);
    if (!inputFile.is_open())
    {
        LOG_ERROR("Unable to open JPEG file for reading: " << outputPathUnsafe);
        return false;
    }

    std::vector<uint8_t> jpegData((std::istreambuf_iterator<char>(inputFile)), std::istreambuf_iterator<char>());
    inputFile.close();

    if (jpegData.size() < 2 || jpegData[0] != 0xFF || jpegData[1] != 0xD8)
    {
        LOG_ERROR("Invalid JPEG file, missing SOI marker.");
        return false;
    }

    const size_t maxSegmentSize = 65533 - 16; // limit APP2 maximum length
    uint8_t totalSegments = static_cast<uint8_t>((iccProfile.size() + maxSegmentSize - 1) / maxSegmentSize);

    std::vector<uint8_t> app2Data;
    app2Data.push_back(0xFF);
    app2Data.push_back(0xD8);

    size_t offset = 0;
    for (uint8_t i = 1; i <= totalSegments; i++)
    {
        size_t chunkSize = std::min(maxSegmentSize, iccProfile.size() - offset);

        // write APP2 marker
        app2Data.push_back(0xFF);
        app2Data.push_back(0xE2);

        // APP2 length = ICC Signature + null + segmentIndex + totalSegments + chunkSize + 2(APP2 marker)
        uint16_t segmentLength = static_cast<uint16_t>(chunkSize + 16);
        app2Data.push_back((segmentLength >> 8) & 0xFF);
        app2Data.push_back(segmentLength & 0xFF);

        // ICC Profile
        app2Data.insert(app2Data.end(), ICC_SIGNATURE, ICC_SIGNATURE + 12);

        // segment index total segments
        app2Data.push_back(i);
        app2Data.push_back(totalSegments);

        // ICC data
        app2Data.insert(app2Data.end(), iccProfile.begin() + offset, iccProfile.begin() + offset + chunkSize);
        offset += chunkSize;
    }

    // insert APP2 data
    app2Data.insert(app2Data.end(), jpegData.begin() + 2, jpegData.end());

    std::ofstream outputFile(outputPath.c_str(), std::ios::binary);
    if (!outputFile.is_open())
    {
        LOG_ERROR("Error: Unable to open output file for writing: " << outputPathUnsafe);
        return false;
    }
    outputFile.write(reinterpret_cast<const char *>(app2Data.data()), app2Data.size());
    outputFile.close();

    LOG_INFO("Successfully wrote ICC Profile to JPEG.");
    return true;
}

bool updateTagInIFD(std::vector<uint8_t> &exifData, uint32_t ifdAbsoluteOffset, uint16_t targetTag, uint32_t newValue,
                    bool bigEndian)
{
    if (exifData.size() < ifdAbsoluteOffset + 2)
        return false;
    uint16_t numEntries = readData<uint16_t>(exifData, ifdAbsoluteOffset, bigEndian);
    uint32_t entryOffset = ifdAbsoluteOffset + 2;
    for (uint16_t i = 0; i < numEntries; i++)
    {
        uint32_t currentEntryOffset = entryOffset + i * 12;
        if (exifData.size() < currentEntryOffset + 12)
        {
            break;
        }
        uint16_t tagId = readData<uint16_t>(exifData, currentEntryOffset, bigEndian);
        if (tagId == targetTag)
        {
            uint16_t dataType = readData<uint16_t>(exifData, currentEntryOffset + 2, bigEndian);
            uint32_t count = readData<uint32_t>(exifData, currentEntryOffset + 4, bigEndian);

            // currently only handle LONG(4) and count==1
            if (dataType == 4 && count == 1)
            {
                if (bigEndian)
                {
                    exifData[currentEntryOffset + 8] = (newValue >> 24) & 0xFF;
                    exifData[currentEntryOffset + 9] = (newValue >> 16) & 0xFF;
                    exifData[currentEntryOffset + 10] = (newValue >> 8) & 0xFF;
                    exifData[currentEntryOffset + 11] = newValue & 0xFF;
                }
                else
                {
                    exifData[currentEntryOffset + 8] = newValue & 0xFF;
                    exifData[currentEntryOffset + 9] = (newValue >> 8) & 0xFF;
                    exifData[currentEntryOffset + 10] = (newValue >> 16) & 0xFF;
                    exifData[currentEntryOffset + 11] = (newValue >> 24) & 0xFF;
                }
                return true;
            }
        }
    }
    return false;
}

// update ExifImageWidth (0xA002) and ExifImageHeight (0xA003)
bool updateExifDimensions(std::vector<uint8_t> &exifData, uint32_t newWidth, uint32_t newHeight)
{
    // check TIFF header
    if (exifData.size() < 16)
    {
        return false;
    }
    bool bigEndian;
    if (std::memcmp(&exifData[6], "II", 2) == 0)
    {
        bigEndian = false;
    }
    else if (std::memcmp(&exifData[6], "MM", 2) == 0)
    {
        bigEndian = true;
    }
    else
    {
        return false;
    }
    // check TIFF magic number
    uint16_t magic = readData<uint16_t>(exifData, 8, bigEndian);
    if (magic != TIFF_MARKER)
    {
        return false;
    }
    // IFD0 offset
    uint32_t ifd0Offset = readData<uint32_t>(exifData, 10, bigEndian);
    uint32_t ifd0AbsoluteOffset = 6 + ifd0Offset;

    // find ExifIFD in IFD0 (0x8769)
    uint16_t numEntries = readData<uint16_t>(exifData, ifd0AbsoluteOffset, bigEndian);
    uint32_t entryOffset = ifd0AbsoluteOffset + 2;
    uint32_t exifIFDOffset = 0;
    for (uint16_t i = 0; i < numEntries; i++)
    {
        uint32_t currentEntryOffset = entryOffset + i * 12;
        if (exifData.size() < currentEntryOffset + 12)
        {
            break;
        }
        uint16_t tagId = readData<uint16_t>(exifData, currentEntryOffset, bigEndian);
        if (tagId == 0x8769)
        { // ExifIFDPointer
            exifIFDOffset = readData<uint32_t>(exifData, currentEntryOffset + 8, bigEndian);
            break;
        }
    }
    if (exifIFDOffset == 0)
    {
        return false; // ExifIFDPointer not found
    }
    uint32_t exifIFDAbsoluteOffset = 6 + exifIFDOffset;
    bool updatedWidth = updateTagInIFD(exifData, exifIFDAbsoluteOffset, 0xA002, newWidth, bigEndian);
    bool updatedHeight = updateTagInIFD(exifData, exifIFDAbsoluteOffset, 0xA003, newHeight, bigEndian);
    return updatedWidth && updatedHeight;
}

bool resizeImage(const char *inputPath, const char *outputPathUnsafe, int newWidth, int newHeight)
{
    // read exif from input image
    std::vector<uint8_t> exifSegment = getAPP1EXIFSegment(inputPath);
    if (!exifSegment.empty())
    {
        // update ExifImageWidth and ExifImageHeight
        if (!updateExifDimensions(exifSegment, newWidth, newHeight))
        {
            LOG_WARNING("Failed to update EXIF dimensions. Original values will be preserved.");
        }
    }
    else
    {
        LOG_INFO("No EXIF data found in original image.");
    }

    // resize with openCV
    cv::Mat src = cv::imread(inputPath, cv::IMREAD_COLOR);
    if (src.empty())
    {
        LOG_ERROR("Error: Unable to read image: " << inputPath);
        return false;
    }
    cv::Mat resized;
    cv::resize(src, resized, cv::Size(newWidth, newHeight), 0, 0, cv::INTER_AREA);

    // encode resized image as jpg
    std::vector<uint8_t> jpegBuffer;
    if (!cv::imencode(".jpg", resized, jpegBuffer))
    {
        LOG_ERROR("Failed to encode resized image to JPEG.");
        return false;
    }
    if (jpegBuffer.size() < 2 || jpegBuffer[0] != 0xFF || jpegBuffer[1] != 0xD8)
    {
        LOG_ERROR("Encoded JPEG data does not start with valid SOI marker.");
        return false;
    }

    // construct new JPEG data
    // SOI
    std::vector<uint8_t> newJpegData;
    newJpegData.push_back(jpegBuffer[0]);
    newJpegData.push_back(jpegBuffer[1]);

    // APP1 segment (0xFFE1)
    if (!exifSegment.empty())
    {
        newJpegData.push_back(0xFF);
        newJpegData.push_back(0xE1);
        uint16_t lengthField = static_cast<uint16_t>(exifSegment.size() + 2);
        newJpegData.push_back((lengthField >> 8) & 0xFF);
        newJpegData.push_back(lengthField & 0xFF);
        newJpegData.insert(newJpegData.end(), exifSegment.begin(), exifSegment.end());
    }
    // append rest data of original jpg
    newJpegData.insert(newJpegData.end(), jpegBuffer.begin() + 2, jpegBuffer.end());

    // write output file
    const auto outputPath = hex::filesystem::stringToWString({outputPathUnsafe});
    std::ofstream outFile(outputPath.c_str(), std::ios::binary);
    if (!outFile.is_open())
    {
        LOG_ERROR("Could not open output file: " << outputPathUnsafe);
        return false;
    }
    outFile.write(reinterpret_cast<const char *>(newJpegData.data()), newJpegData.size());
    outFile.close();
    LOG_INFO("Image resized and written to output file with updated EXIF dimensions.");

    // write ICC profile
    std::vector<uint8_t> iccProfile = getICCProfileBuffer(inputPath);
    if (!iccProfile.empty())
    {
        writeICCProfileToJPEG(outputPathUnsafe, iccProfile);
    }

    return true;
}

最终调用resizeImage函数对图片进行压缩后,我终于获得了想要的效果:

最终效果图(1280p),质感和颜色都正确!


值得一提的是,商业软件例如小红书在压缩的时候可能还会进行更多处理比如锐化之类的,从而让最后压缩的图片效果更好。这里只是我个人的一番简单探索,相信实际的压缩算法会更复杂。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐