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

PDF批注Flatten异常求助:旋转文档处理失效(iText 8.0.4/PDFBox)

解决方案:处理旋转PDF的批注扁平化问题

问题背景

需求是将PDF中所有批注转为嵌入图像,避免Mac Preview可移动批注。现有iText 8.0.4代码仅支持0度旋转文档,90/180/270度旋转文档处理后批注位置、方向异常。


方案1:修正iText 8代码

核心思路是根据页面旋转角度,转换批注坐标与外观方向,而非临时移除页面旋转。以下是兼容所有旋转角度的实现代码:

public static byte[] flattenAnnotations(byte[] data) throws Exception {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try (PdfDocument pdfDocument = new PdfDocument(new PdfReader(new ByteArrayInputStream(data)), new PdfWriter(baos))) {
        for (int i = 1; i <= pdfDocument.getNumberOfPages(); i++) {
            PdfPage page = pdfDocument.getPage(i);
            int rotation = page.getRotation();
            PdfCanvas canvas = new PdfCanvas(page);
            // 遍历批注副本,避免并发修改异常
            List<PdfAnnotation> annotations = new ArrayList<>(page.getAnnotations());

            for (PdfAnnotation annot : annotations) {
                if (annot instanceof PdfStampAnnotation) {
                    PdfDictionary appearanceDict = annot.getAppearanceObject(PdfName.N);
                    if (appearanceDict == null) continue;

                    PdfFormXObject appearanceXObj = new PdfFormXObject((PdfStream) appearanceDict.clone());
                    Rectangle annotRect = annot.getRectangle().toRectangle();
                    Rectangle pageSize = page.getPageSize();

                    // 根据页面旋转转换批注坐标
                    AffineTransform transform = getTransformForRotation(rotation, pageSize);
                    float[] transformedCoords = transform.transform(
                        new float[]{annotRect.getLeft(), annotRect.getBottom(), annotRect.getRight(), annotRect.getTop()},
                        0
                    );
                    Rectangle targetRect = new Rectangle(
                        transformedCoords[0], transformedCoords[1],
                        transformedCoords[2] - transformedCoords[0],
                        transformedCoords[3] - transformedCoords[1]
                    );

                    // 旋转批注外观以匹配页面方向
                    canvas.saveState();
                    if (rotation != 0) {
                        float centerX = targetRect.getLeft() + targetRect.getWidth() / 2;
                        float centerY = targetRect.getBottom() + targetRect.getHeight() / 2;
                        canvas.concatMatrix(AffineTransform.getRotateInstance(Math.toRadians(rotation), centerX, centerY));
                    }
                    canvas.addXObjectFittedIntoRectangle(appearanceXObj, targetRect);
                    canvas.restoreState();

                    page.removeAnnotation(annot);
                }
            }
        }

        // 扁平化表单域
        PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDocument, true);
        if (form != null) {
            form.flattenFields();
        }
    }
    return baos.toByteArray();
}

private static AffineTransform getTransformForRotation(int rotation, Rectangle pageSize) {
    AffineTransform transform = new AffineTransform();
    switch (rotation) {
        case 90:
            transform.translate(pageSize.getHeight(), 0);
            transform.rotate(Math.toRadians(90));
            break;
        case 180:
            transform.translate(pageSize.getWidth(), pageSize.getHeight());
            transform.rotate(Math.toRadians(180));
            break;
        case 270:
            transform.translate(0, pageSize.getWidth());
            transform.rotate(Math.toRadians(270));
            break;
        default:
            // 0度无需转换
            break;
    }
    return transform;
}

关键说明

  1. 不再跳过旋转页面,统一处理所有角度的页面
  2. 通过坐标转换,将批注原始矩形映射到旋转后的页面坐标系
  3. 以批注中心为原点旋转外观,保证显示方向与页面一致
  4. 绘制完成后移除原批注,避免可编辑状态

方案2:使用Apache PDFBox实现

若iText方案仍存在适配问题,可改用PDFBox实现,代码如下:

public static byte[] flattenAnnotationsWithPDFBox(byte[] data) throws IOException {
    try (PDDocument doc = PDDocument.load(data)) {
        for (PDPage page : doc.getPages()) {
            int rotation = page.getRotation();
            PDPageContentStream contentStream = new PDPageContentStream(
                doc, page, PDPageContentStream.AppendMode.APPEND, true, true
            );

            List<PDAnotation> annotations = new ArrayList<>(page.getAnnotations());
            for (PDAnotation annot : annotations) {
                if (annot instanceof PDStampAnnotation) {
                    PDAppearanceDictionary appearance = annot.getAppearance();
                    if (appearance == null) continue;

                    PDAppearanceEntry normalAppearance = appearance.getNormalAppearance();
                    if (normalAppearance == null) continue;

                    PDFormXObject xobject = normalAppearance.getAppearanceStream();
                    if (xobject == null) continue;

                    // 转换批注矩形到旋转后页面坐标
                    PDRectangle rect = annot.getRectangle();
                    Rectangle2D.Float annotRect = new Rectangle2D.Float(
                        rect.getLowerLeftX(), rect.getLowerLeftY(), rect.getWidth(), rect.getHeight()
                    );
                    Rectangle2D.Float transformedRect = transformRectForRotation(annotRect, page.getMediaBox(), rotation);

                    // 绘制并旋转批注外观
                    contentStream.saveGraphicsState();
                    if (rotation != 0) {
                        float centerX = transformedRect.x + transformedRect.width / 2;
                        float centerY = transformedRect.y + transformedRect.height / 2;
                        contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), centerX, centerY));
                    }
                    contentStream.drawForm(xobject, transformedRect.x, transformedRect.y, transformedRect.width, transformedRect.height);
                    contentStream.restoreGraphicsState();

                    page.getAnnotations().remove(annot);
                }
            }
            contentStream.close();
        }
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        doc.save(baos);
        return baos.toByteArray();
    }
}

private static Rectangle2D.Float transformRectForRotation(Rectangle2D.Float rect, PDRectangle pageSize, int rotation) {
    float pageWidth = pageSize.getWidth();
    float pageHeight = pageSize.getHeight();
    switch (rotation) {
        case 90:
            return new Rectangle2D.Float(rect.y, pageWidth - rect.x - rect.width, rect.height, rect.width);
        case 180:
            return new Rectangle2D.Float(pageWidth - rect.x - rect.width, pageHeight - rect.y - rect.height, rect.width, rect.height);
        case 270:
            return new Rectangle2D.Float(pageHeight - rect.y - rect.height, rect.x, rect.height, rect.width);
        default:
            return rect;
    }
}

关键说明

  1. 基于PDFBox的坐标系统,直接转换批注矩形位置
  2. 通过矩阵旋转实现外观与页面方向对齐
  3. 以追加模式写入内容流,保留原页面内容

内容的提问来源于stack exchange,提问作者Akhil NagaSai

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 03:24:53