PDF 色彩保真工程实践【5】核心实现(下):拼装 Image XObject、页面资源与内容流

本文是系列文章的第 5 篇。上一篇我们把 ICC 与 CMYK 数据变成了 Flate 流。这一篇,

我们把这些流对象组装成一张完整的图像,挂到页面上,并写出让它显示的绘制指令。

本篇你将学到

  • 如何构造 ICC 流字典与 ICCBased 色彩空间数组;
  • 如何构造 Image XObject 并挂上 /ColorSpace
  • 如何把图像注册到页面 /Resources,并生成唯一资源名;
  • 如何追加 q cm /Im Do Q 内容流,兼容已有 /Contents

第一步:ICC 流 + ICCBased 色彩空间

先构造 ICC 流的字典(/N 4/Alternate /DeviceCMYK),再用上一篇的

CreateFlateStream 把 ICC 原始字节压缩写入,拿到它的间接对象号:

cpp 复制代码
// ICC 流 + ICCBased 数组(FoxitPdfObjectFactory.cpp 摘录示意)
PDFDictionary* icc_dict = PDFDictionary::Create();
icc_dict->SetAtInteger("N", 4);
icc_dict->SetAtName("Alternate", "DeviceCMYK");
uint32 icc_obj_num = 0;
CreateFlateStream(icc_dict, icc_raw, icc_obj_num, err);   // 写入 ICC 流

// ColorSpace 数组:[ /ICCBased <iccRef> ]
PDFArray* color_space = PDFArray::Create();
color_space->AddName("ICCBased");
color_space->AddElement(PDFObject::CreateReference(doc_, icc_obj_num));

这样 color_space 就是 [/ICCBased 10 0 R]------一个指向 ICC 流的引用数组。

第二步:构造 Image XObject

图像字典把上一步的色彩空间数组挂到 /ColorSpace,再写入 CMYK 采样数据:

cpp 复制代码
// Image XObject 字典(摘录示意)
PDFDictionary* img_dict = PDFDictionary::Create();
img_dict->SetAtName("Type", "XObject");
img_dict->SetAtName("Subtype", "Image");
img_dict->SetAtInteger("Width",  image.width);
img_dict->SetAtInteger("Height", image.height);
img_dict->SetAtInteger("BitsPerComponent", 8);
img_dict->SetAt("ColorSpace", color_space);              // 直接挂数组值

uint32 img_obj_num = 0;
CreateFlateStream(img_dict, image.cmykPixels, img_obj_num, err);  // 写入 CMYK 数据流

到这里,一张"色彩空间为 ICCBased CMYK、数据为原始 CMYK 采样"的图像就造好了。

若没有合法 ICC,则走另一条极简路径:img_dict->SetAtName("ColorSpace", "DeviceCMYK")

仍然是 CMYK,只是不带特性文件。

第三步:注册到页面资源,生成唯一名字

PDF 页面通过 /Resources /XObject 字典按名字引用图像。我们要生成一个不与现有资源
冲突
的名字(如 ImCMYK1ImCMYK2...):

cpp 复制代码
// 生成唯一资源名(PdfPageResourceManager.cpp 摘录示意)
std::string name;
for (int i = 1; ; ++i) {
  name = "ImCMYK" + std::to_string(i);
  if (!xobject_dict->HasKey(name.c_str())) break;        // 找到没被占用的名字
}
xobject_dict->SetAtReference(name.c_str(), image_indirect, doc_);  // 以间接引用挂接

这就是为什么向已有 PDF 反复插入时,资源名会自动递增------不会覆盖页面上原有的图像。

第四步:写出绘制指令(内容流)

有了资源名,还要在页面内容里"画"出来。PDF 用一小段操作符完成:

复制代码
q                          % 保存图形状态
w 0 0 h x y cm             % 变换矩阵:缩放到 w×h(显示宽高),平移到 (x, y)
/ImCMYK1 Do                % 绘制名为 ImCMYK1 的 XObject
Q                          % 恢复图形状态

对应代码是把这段文本作为一个新的内容流追加到页面。这里要兼容两种情况 :页面

/Contents 可能是单个流,也可能已经是流数组:

cpp 复制代码
// 追加内容流(PdfContentStreamManager.cpp 摘录示意)
PDFStream* content = PDFStream::Create(nullptr);
content->SetData((const uint8_t*)op.data(), op.size());
uint32 content_num = doc_.AddIndirectObject(content);

PDFObject* contents = page_dict->GetElement("Contents");
if (!contents) {
  page_dict->SetAtReference("Contents", content_indirect, doc_);   // 原来没有
} else if (contents 是数组) {
  array->AddReference(content_indirect, doc_);                     // 追加到数组
} else {
  // 原来是单个流:包成 [旧, 新] 数组
  PDFArray* arr = PDFArray::Create();
  arr->AddReference(old_contents, doc_);
  arr->AddReference(content_indirect, doc_);
  page_dict->SetAt("Contents", arr);
}

第五步:保存

最后一步是保存文档:

cpp 复制代码
doc.SaveAs(output_path, PDFDoc::e_SaveFlagNoOriginal);

至此,一张 CMYK 图像就被无损地插入并显示在 PDF 页面上了。

⚠️ 对象生命周期提醒 :上一篇 newMemoryFileRead 回调,要一直有效到文档

真正被销毁。谁来释放、什么时候释放,直接决定程序退出时会不会崩溃------下一篇揭晓。

顶层编排

以上步骤由 PdfCmykImageInserter 串起来:解析页面 → 造图像对象(ICCBased 或

DeviceCMYK)→ 注册资源 → 追加内容 → 返回结果。各模块职责单一,组合清晰。

小结

  • ICC 流 + ICCBased 数组承载"CMYK + 特性文件";
  • Image XObject 把色彩空间与 CMYK 数据绑定;
  • 唯一资源名 + 间接引用挂接页面,兼容已有图像;
  • 内容流用 q cm Do Q 画出图像,兼容单流/数组两种 /Contents

下一篇预告

第 6 篇《怎么证明"真的无损"?------SHA-256 校验与三个真实踩坑》,我们用哈希证明无损,并复盘三个有价值的 bug(退出崩溃、依赖命名、构建传参)。


关于本系列

相关推荐
Jazz_z1 小时前
如何用Python将彩色PDF一键转为黑白(灰度)
java·python·pdf
乱七八糟的屋子2 小时前
MTL4 (Matrix Template Library) 超详细实战教程|C++高性能稠密/稀疏矩阵计算
c++·矩阵·稀疏矩阵·mtl4·matrixtemplate·c++线性代数·高性能数值计算
依然鸣3 小时前
PTA团体程序设计天梯赛L2真题讲解L2-045-048
数据结构·c++·经验分享·学习·算法·pat考试·pat
有点。3 小时前
C++深度优先搜索(二)
开发语言·c++·深度优先
小小龙学IT4 小时前
Taskflow:用一张“任务图“玩转现代 C++ 并行编程开源项
c++·开源·github
wuminyu4 小时前
JUC包的AQS与JVM的ObjectMonitor机制对比分析
java·linux·c语言·jvm·c++
就不掉头发5 小时前
C++函数模板
java·开发语言·c++
王老师青少年编程6 小时前
2026年全国青少年信息素养大赛算法应用主题赛C++赛项【决赛】模拟卷(汇总)
c++·模拟卷·2026年·青少年信息素养大赛·算法应用主题赛·决赛
思麟呀7 小时前
C++(八):文件系统库+并行算法
开发语言·c++