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

使用iTextPDF生成的PDF页脚无法被屏幕阅读器读取的求助

问题描述

我用iTextPDF生成了带页脚的PDF,遇到了无障碍访问问题:开启setTagged()后,JAWS和Adobe Reader都无法读取页脚;但关闭setTagged()会导致图片、表格等其他无障碍问题。我尝试过设置各种标签和角色都没用,同时看到有文章说页眉页脚不该加入文档结构以避免影响阅读流程,但我的需求是页脚必须能被屏幕阅读器读取。想知道有没有解决办法,或者是否有标准明确规定页眉页脚不能被屏幕阅读器读取。


主PDF生成代码

import com.itextpdf.io.font.FontConstants;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfViewerPreferences;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.tagging.StandardRoles;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.*;
import com.itextpdf.layout.property.TextAlignment;

import java.io.File;
import java.io.IOException;

public class PDFGenerator {

    public static final String DEST = "C:\\Users\\skesha01\\temp\\output.pdf";

    public static void main(String[] args) throws IOException {
        File file = new File(DEST);
        file.getParentFile().mkdirs();
        new PDFGenerator().createPdf(DEST);
    }

    public void createPdf(String dest) throws IOException {
        PdfDocument pdfDoc = new PdfDocument(new PdfWriter(dest));
        pdfDoc.setTagged();
        pdfDoc.getCatalog().setViewerPreferences(new PdfViewerPreferences().setDisplayDocTitle(true));
        Document doc = new Document(pdfDoc, PageSize.A4);
        pdfDoc.addEventHandler(PdfDocumentEvent.END_PAGE, new FooterEventHandler(doc));

        addContent(doc);
        doc.close();
    }

    public void addContent(Document doc) throws IOException {
        doc.add(new Paragraph("Sample PDF with footer, page numbers, and table fields")
                .setFont(PdfFontFactory.createFont(FontConstants.HELVETICA))
                .setFontSize(12)
                .setTextAlignment(TextAlignment.CENTER));
  
        Table table = new Table(3).useAllAvailableWidth();
        Cell headerCell1 = new Cell().add(new Paragraph("Header 1"));
        Cell headerCell2 = new Cell().add(new Paragraph("Header 2"));
        Cell headerCell3 = new Cell().add(new Paragraph("Header 3"));
        table.addHeaderCell(headerCell1).getAccessibilityProperties().setRole(StandardRoles.TH);
        table.addHeaderCell(headerCell2).getAccessibilityProperties().setRole(StandardRoles.TH);
        table.addHeaderCell(headerCell3).getAccessibilityProperties().setRole(StandardRoles.TH);
        for (int i = 0; i < 1; i++) {
            table.addCell("Row " + (i + 1) + ", Column 1").getAccessibilityProperties().setRole(StandardRoles.TD);
            table.addCell("Row " + (i + 1) + ", Column 2").getAccessibilityProperties().setRole(StandardRoles.TD);
            table.addCell("Row " + (i + 1) + ", Column 3").getAccessibilityProperties().setRole(StandardRoles.TD);
        }
        table.getAccessibilityProperties().setRole(StandardRoles.TABLE);
        table.getAccessibilityProperties().setAlternateDescription("I am reading this table alternate text");
        doc.add(table);
    }
}

原页脚事件处理器代码

import com.itextpdf.io.font.FontConstants;
import com.itextpdf.kernel.events.Event;
import com.itextpdf.kernel.events.IEventHandler;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.tagging.StandardRoles;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Text;

import java.io.IOException;

public class FooterEventHandler implements IEventHandler {
    protected PdfFont font;

    private Document document;


    public FooterEventHandler(Document document) {
        try {
            font = PdfFontFactory.createFont(FontConstants.HELVETICA);
            this.document = document;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void handleEvent(Event event) {
        PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
        PdfDocument pdfDoc = docEvent.getDocument();
        PdfCanvas canvas = new PdfCanvas(pdfDoc.getLastPage());

        canvas.setStrokeColorRgb(0, 0, 0).setLineWidth(0.5f);
        canvas.moveTo(36, 20).lineTo(pdfDoc.getDefaultPageSize().getRight() - 36, 20).stroke();

        addFooter(canvas, pdfDoc, docEvent.getPage());
    }
    private void addFooter(PdfCanvas canvas, PdfDocument pdfDoc, PdfPage page) {

        canvas.setStrokeColorRgb(0, 0, 0).setLineWidth(0.5f);
        canvas.moveTo(36, 20).lineTo(pdfDoc.getDefaultPageSize().getRight() - 36, 20).stroke();
        Text footerText = new Text("Footer Text - Page " + pdfDoc.getPageNumber(page));
        footerText.getAccessibilityProperties().setRole(StandardRoles.ARTIFACT);
        canvas.beginText().beginMarkedContent(PdfName.Artifact)
                .setFontAndSize(font, 10)
                .moveText((pdfDoc.getDefaultPageSize().getLeft() + pdfDoc.getDefaultPageSize().getRight()) / 2, 10)
                .showText(footerText.getText())
                .endText();
    }
}

解决方案

问题根源

  1. 原代码将页脚标记为Artifact(人工制品),这会告诉屏幕阅读器忽略这段内容;
  2. 直接使用PdfCanvas绘制文本,没有将其纳入PDF的结构化内容树,开启setTagged()后,页脚不会被识别为可访问内容;
  3. 关于标准:PDF/UA(无障碍PDF国际标准)并未禁止页眉页脚被读取,只是建议避免重复内容干扰正常阅读流程——若页脚包含重要信息(如页码),完全可以纳入结构化内容,只需处理得当。

修改后的页脚事件处理器代码

import com.itextpdf.io.font.FontConstants;
import com.itextpdf.kernel.events.Event;
import com.itextpdf.kernel.events.IEventHandler;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.tagging.StandardRoles;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.property.TextAlignment;
import com.itextpdf.layout.property.VerticalAlignment;

import java.io.IOException;

public class FooterEventHandler implements IEventHandler {
    protected PdfFont font;
    private Document document;

    public FooterEventHandler(Document document) {
        try {
            font = PdfFontFactory.createFont(FontConstants.HELVETICA);
            this.document = document;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void handleEvent(Event event) {
        PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
        PdfDocument pdfDoc = docEvent.getDocument();
        PdfPage page = docEvent.getPage();

        // 绘制页脚分隔线(用Paragraph边框保持结构化)
        Rectangle pageSize = page.getPageSize();
        document.showTextAligned(new Paragraph("")
                        .setBorderBottom(0.5f)
                        .setWidth(pageSize.getWidth() - 72),
                pageSize.getLeft() + 36, 25, pdfDoc.getPageNumber(page),
                TextAlignment.CENTER, VerticalAlignment.BOTTOM, 0);

        // 添加结构化页脚文本
        Paragraph footer = new Paragraph("Footer Text - Page " + pdfDoc.getPageNumber(page))
                .setFont(font)
                .setFontSize(10);
        // 设置无障碍角色为脚注类,帮助屏幕阅读器识别
        footer.getAccessibilityProperties().setRole(StandardRoles.FNOTE);

        // 对齐到页面底部居中
        document.showTextAligned(footer,
                pageSize.getWidth() / 2, 10, pdfDoc.getPageNumber(page),
                TextAlignment.CENTER, VerticalAlignment.BOTTOM, 0);
    }
}

关键修改点

  1. 移除Artifact标记,不再将页脚设为可忽略内容;
  2. 使用document.showTextAligned()添加Paragraph元素,自动将页脚纳入PDF结构化内容树;
  3. 为页脚设置StandardRoles.FNOTE角色,明确内容类型;
  4. 用Paragraph边框替代直接绘制线条,保持内容结构化一致性。

验证效果

修改后,开启setTagged()生成的PDF,JAWS和Adobe Reader可正常读取页脚,同时图片、表格的无障碍属性也能正常保留。


内容的提问来源于stack exchange,提问作者karthik b

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 15:35:02