You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用PdfStamper放大复制PDF页面时注释异常问题求解

解决方案

问题根源

  • 复制页面丢失注释:PdfImportedPage仅会导入页面的内容流数据,而PDF注释是独立存储在页面对象的/Annots数组中,不会随内容流自动迁移
  • 放大后注释位置偏移:修改页面尺寸和内容流坐标变换规则后,注释的坐标仍是基于原始页面坐标系定义的,没有同步调整导致错位

具体实现代码

带注释复制页面

不要用Image.getInstance包装PdfImportedPage的实现,改用内容流复制+注释迁移的方案:

// 插入和原第1页尺寸一致的空白页作为第2页
stamper.insertPage(2, reader.getPageSize(1));
// 复制原第1页的内容流到新页面
PdfImportedPage originPage1 = stamper.getImportedPage(reader, 1);
stamper.getOverContent(2).addTemplate(originPage1, 0, 0);
// 迁移原第1页的所有注释到新页面
PdfDictionary originPageDict = reader.getPageN(1);
PdfArray originAnnots = originPageDict.getAsArray(PdfName.ANNOTS);
if (originAnnots != null && !originAnnots.isEmpty()) {
    PdfDictionary newPageDict = stamper.getReader().getPageN(2);
    PdfArray newAnnots = new PdfArray();
    for (int i = 0; i < originAnnots.size(); i++) {
        // 深度复制注释对象,避免修改原文档数据
        PdfDictionary newAnnot = new PdfDictionary(originAnnots.getAsDict(i));
        newAnnots.add(newAnnot);
    }
    newPageDict.put(PdfName.ANNOTS, newAnnots);
}

页面放大同步调整注释坐标

在原有放大逻辑的基础上,新增注释坐标修正逻辑:

for (int i = 1; i <= n; i++) {
    PdfDictionary page = reader.getPageN(i);
    PdfArray media = page.getAsArray(PdfName.CROPBOX);
    if (media == null) {
        media = page.getAsArray(PdfName.MEDIABOX);
    }
    float originalW = media.getAsNumber(2).floatValue();
    float originalH = media.getAsNumber(3).floatValue();
    // 调整页面尺寸
    PdfArray crop = new PdfArray();
    crop.add(new PdfNumber(0));
    crop.add(new PdfNumber(0));
    float newW = originalW * 2;
    float newH = originalH;
    crop.add(new PdfNumber(newW));
    crop.add(new PdfNumber(newH));
    page.put(PdfName.MEDIABOX, crop);
    page.put(PdfName.CROPBOX, crop);
    // 调整内容流坐标
    stamper.getUnderContent(i).setLiteral(String.format("\nq 1 0 0 1 %.1f 0 cm\nq\n", originalW / 2));
    stamper.getOverContent(i).setLiteral("\nQ\nQ\n");

    // 同步调整所有注释的坐标
    PdfArray annots = page.getAsArray(PdfName.ANNOTS);
    if (annots != null && !annots.isEmpty()) {
        for (int j = 0; j < annots.size(); j++) {
            PdfDictionary annot = annots.getAsDict(j);
            PdfArray rect = annot.getAsArray(PdfName.RECT);
            if (rect != null) {
                // 和内容流平移逻辑保持一致,所有x坐标右移originalW/2
                float x1 = rect.getAsNumber(0).floatValue() + originalW / 2;
                float y1 = rect.getAsNumber(1).floatValue();
                float x2 = rect.getAsNumber(2).floatValue() + originalW / 2;
                float y2 = rect.getAsNumber(3).floatValue();
                rect.set(0, new PdfNumber(x1));
                rect.set(1, new PdfNumber(y1));
                rect.set(2, new PdfNumber(x2));
                rect.set(3, new PdfNumber(y2));
                // 移除原注释外观,让阅读器自动重新生成适配新坐标的外观,避免手动修改外观流的复杂度
                annot.remove(PdfName.AP);
            }
        }
    }
}

注意事项

  • 以上代码适配iText 5版本,若使用iText 7,替换为PdfDocument对应API即可
  • 若存在签名注释、3D注释等特殊类型注释,需额外调整对应属性的坐标参数
  • 若需要保留注释原始外观,需手动修改注释外观流的坐标变换矩阵,和页面内容流的变换规则保持一致

内容的提问来源于stack exchange,提问作者Jianfei Xiong

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.02 17:36:04