公司动态

PDF 色彩保真工程实践【4】核心实现(上):从 TIFF 解码到 Flate 流构造

📅 2026/8/5 11:03:06
PDF 色彩保真工程实践【4】核心实现(上):从 TIFF 解码到 Flate 流构造
本文是系列文章的第 4 篇。工程已经能跑这一篇进入代码从 TIFF 中解码出未经色彩转换的 CMYK 采样同时提取 ICC Profile再将这些数据无损压缩并写入 PDF Flate Stream。TiffCmykReader::Read ↓ IccProfileParser::Parse ↓ ZlibUtil::Compress ↓ FoxitPdfObjectFactory::CreateFlateStream本项目完整源码https://github.com/AmyLin2013/pdf-cmyk-image本篇你将学到如何用 libtiff 的 scanline API 读取 CMYK而不触发任何 RGB 转换如何提取内嵌 ICC Profile并完成基础头部校验为什么我们自己用 zlib 压缩数据如何通过ReaderCallbackImportData把压缩字节写入 PDF 流。第一步用 scanline 读取 CMYK绝不走 RGBlibtiff 有个很方便的TIFFReadRGBAImage但它会把图像转成 RGBA——这正是我们要避开的。所以我们使用TIFFReadScanline逐行解码。它会还原 TIFF 的压缩和 Predictor但不会把 CMYK 转换成 RGB得到的是解码后的 CMYK 采样值。读取前先做严格校验确认它确实是 8 位、四通道的 CMYK颜色通道数需排除 extra samples// 关键校验TiffCmykReader.cpp 摘录示意constuint16_tcolor_samplesspp-extraSampleCount;// 排除额外通道boolisCmyk(photometricPHOTOMETRIC_SEPARATED)// 分色模式CMYK 印刷分色(inkSetINKSET_CMYK)// 墨色为 CMYK(color_samples4);// 恰好 4 个颜色通道if(!isCmyk)Fail(Not a standard CMYK TIFF.);if(bitsPerSample!8)Fail(Only 8-bit CMYK is supported.);if(TIFFIsTiled(tif))Fail(Tiled TIFF not supported.);// 仅支持 strip然后按 planar 配置逐行读取。TIFF 有两种像素排布CONTIG交错一行里 C M Y K C M Y K …若有多余通道则丢弃SEPARATE分平面C 平面、M 平面…分开存需要按平面重组若有多余平面同样只取前 4 个。两种都要正确处理最终得到一段紧凑的 CMYK 采样缓冲每像素 4 字节。 关键点整个读取过程没有发生色彩空间转换得到的是 TIFF 解码后的 CMYK 样本四个颜色分量的数值保持不变。第二步提取 ICC Profile 并检查关键头部字段CMYK TIFF 可以通过 TIFFTAG_ICCPROFILE 标签嵌入 ICC Profile。提取操作发生在第一步的读取流程中TiffCmykReader.cpp里通过TIFFGetField(tif, TIFFTAG_ICCPROFILE, ...)取得完整的 ICC 字节。本步骤不执行完整的 ICC 合规性验证只检查本工程后续构造 PDF 对象所需的几个关键头部字段// ICC 基本校验IccProfileParser.cpp 摘录示意if(raw.size()128)returnfalse;// 头部至少 128 字节if(memcmp(raw.data()36,acsp,4)!0)returnfalse;// ICC 签名if(memcmp(raw.data()16,CMYK,4)!0)isCmykfalse;// 数据色彩空间偏移 36 处必须是acspICC 文件魔数偏移 16 处的数据色彩空间为CMYK时isCmyk true对应 PDF 里的/N 4。偏移 0 处声明的 Profile 大小必须 ≥128 且不超过实际缓冲长度防止截断的 Profile 混入。若 TIFF 没有 ICC或 ICC 未通过本工程的基础头部校验程序会回退为 /DeviceCMYK。仍是 CMYK只是不带特性文件。第三步为什么自己用 zlib 压缩对于本项目这种需要无损保存原始 CMYK 采样的图像流FlateDecode 是合适的选择它使用 zlib/deflate 压缩解码后可以完整恢复原始字节。我们选择自己压缩而不是把原始数据交给 SDK 让它决定怎么存原因是——确定性我们明确知道流里存的就是“我们压缩后的字节”这里追求的“确定性”主要是数据路径和解码结果可验证而不是要求不同 zlib 版本生成完全相同的压缩字节序列。/Filter明确就是FlateDecode校验时用对应方式解压能拿回与源完全一致的原始数据做比对。压缩就是一层薄封装// zlib 压缩CmykImageEncoder.cpp 摘录示意uLongf boundcompressBound((uLong)input.size());output.resize(bound);compress2(output.data(),bound,input.data(),(uLong)input.size(),Z_BEST_COMPRESSION);output.resize(bound);实现中还检查了compress2的返回值非Z_OK即报错中止压缩失败会直接返回失败而不是把坏数据写入流——这也是“确定性”的一部分。第四步把压缩字节喂给 PDF 流以下 ImportData 行为基于 Foxit PDF SDK 11.1传入的数据已经按照指定 Filter 编码SDK 将其作为 Stream 的编码数据保存并在 Stream Dictionary 中设置对应的 /Filter。我们实现一个内存版的ReaderCallbackMemoryFileRead把压缩后的字节包起来声明 filter 为FlateDecode// 创建 Flate 流FoxitPdfObjectFactory.cpp 摘录示意std::vectoruint8_tcompressed;ZlibUtil::Compress(raw_data,compressed,err);// 先自己压PDFStream*streamPDFStream::Create(dict);// 用给定字典建流MemoryFileRead*readernewMemoryFileRead(std::move(compressed));stream-ImportData(reader,PDFStream::e_FlateDecode);// 存入 压缩字节filter Flateuint32 obj_numdoc_.AddIndirectObject(stream);// 生成间接对象这样流对象的原始字节就是压缩结果/Filter是FlateDecode将来读取时GetData(false)会返回解码后的原始数据——正好用于第 6 篇的逐字节比对。⚠️埋个伏笔这里new出来的MemoryFileRead由谁释放如果处理不当会在程序退出时崩溃。这个坑我们在第 6 篇专门讲——它是本项目最有价值的一个 bug。小结用 scanline 读 CMYK严格校验全程不转 RGB原样提取并校验 ICC Profile非法则回退 DeviceCMYK自己用 zlib 压缩以获得确定性通过ReaderCallbackImportData(FlateDecode)写入 PDF 流。下一篇预告第 5 篇《核心实现下拼装 Image XObject、页面资源与内容流》我们把这些流对象组装成完整图像挂到页面资源上并写出绘制指令。关于本系列完整源码https://github.com/AmyLin2013/pdf-cmyk-image 关于福昕 PDF SDK开发者站点 ·底层 PDF Object API 提供对流、字典、数组、引用的精确控制欢迎申请试用。 免费试用申请[https://developers.fuxinsoft.cn/free-trial/