ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

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 压缩数据;
  • 如何通过ReaderCallback+ImportData把压缩字节写入 PDF 流。

第一步:用 scanline 读取 CMYK(绝不走 RGB)

libtiff 有个很方便的TIFFReadRGBAImage,但它会把图像转成 RGBA——这正是我们要
避开的。所以我们使用TIFFReadScanline逐行解码。它会还原 TIFF 的压缩和 Predictor,但不会把 CMYK 转换成 RGB;得到的是解码后的 CMYK 采样值。

读取前先做严格校验,确认它确实是 8 位、四通道的 CMYK(颜色通道数需排除 extra samples):

// 关键校验(TiffCmykReader.cpp 摘录示意)constuint16_tcolor_samples=spp-extraSampleCount;// 排除额外通道boolisCmyk=(photometric==PHOTOMETRIC_SEPARATED)&&// 分色模式(CMYK 印刷分色)(inkSet==INKSET_CMYK)&&// 墨色为 CMYK(color_samples==4);// 恰好 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)isCmyk=false;// 数据色彩空间
  • 偏移 36 处必须是acsp(ICC 文件魔数);
  • 偏移 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 bound=compressBound((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::vector<uint8_t>compressed;ZlibUtil::Compress(raw_data,compressed,err);// 先自己压PDFStream*stream=PDFStream::Create(dict);// 用给定字典建流MemoryFileRead*reader=newMemoryFileRead(std::move(compressed));stream->ImportData(reader,PDFStream::e_FlateDecode);// 存入 = 压缩字节,filter = Flateuint32 obj_num=doc_.AddIndirectObject(stream);// 生成间接对象

这样,流对象的原始字节就是压缩结果,/FilterFlateDecode;将来读取时
GetData(false)会返回解码后的原始数据——正好用于第 6 篇的逐字节比对。

⚠️埋个伏笔:这里new出来的MemoryFileRead由谁释放?如果处理不当,会在
程序退出时崩溃。这个坑我们在第 6 篇专门讲——它是本项目最有价值的一个 bug。

小结

  • 用 scanline 读 CMYK,严格校验,全程不转 RGB;
  • 原样提取并校验 ICC Profile,非法则回退 DeviceCMYK;
  • 自己用 zlib 压缩以获得确定性;
  • 通过ReaderCallback+ImportData(FlateDecode)写入 PDF 流。

下一篇预告

第 5 篇《核心实现(下):拼装 Image XObject、页面资源与内容流》,我们把这些流对象
组装成完整图像,挂到页面资源上,并写出绘制指令。


关于本系列

  • 完整源码:https://github.com/AmyLin2013/pdf-cmyk-image
  • 🧩 关于福昕 PDF SDK:开发者站点 ·
    底层 PDF Object API 提供对流、字典、数组、引用的精确控制,欢迎申请试用。
  • 🆓 免费试用申请:[https://developers.fuxinsoft.cn/free-trial/
返回列表