本文是系列文章的第 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 字典按名字引用图像。我们要生成一个不与现有资源
冲突 的名字(如 ImCMYK1、ImCMYK2...):
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 页面上了。
⚠️ 对象生命周期提醒 :上一篇
new的MemoryFileRead回调,要一直有效到文档真正被销毁。谁来释放、什么时候释放,直接决定程序退出时会不会崩溃------下一篇揭晓。
顶层编排
以上步骤由 PdfCmykImageInserter 串起来:解析页面 → 造图像对象(ICCBased 或
DeviceCMYK)→ 注册资源 → 追加内容 → 返回结果。各模块职责单一,组合清晰。
小结
- ICC 流 + ICCBased 数组承载"CMYK + 特性文件";
- Image XObject 把色彩空间与 CMYK 数据绑定;
- 唯一资源名 + 间接引用挂接页面,兼容已有图像;
- 内容流用
q cm Do Q画出图像,兼容单流/数组两种/Contents。
下一篇预告
第 6 篇《怎么证明"真的无损"?------SHA-256 校验与三个真实踩坑》,我们用哈希证明无损,并复盘三个有价值的 bug(退出崩溃、依赖命名、构建传参)。
关于本系列
- 完整源码:https://github.com/AmyLin2013/pdf-cmyk-image
- 🧩 关于福昕 PDF SDK:开发者站点 ·
欢迎申请试用。 - 🆓 免费试用申请:https://developers.fuxinsoft.cn/free-trial/