如何使用iText以编程方式将页眉和页脚添加到现有的基于表单的PDF中?

时间:2016-01-22 01:56:28

标签: itext

我需要使用iText以编程方式将页眉和页脚添加到现有的基于表单的PDF中。现有的PDF来自用户,它不包含页眉和页脚的空间。因此,解决方案是通过将现有PDF的内容与页眉和页脚连接来创建新的PDF。但是,此方法仅适用于不包含任何形式的PDF。对于包含AcroForm或XFA表单的交互式PDF,它失败如下:(1)AcroForm在新PDF中变得扁平化。 (2)XFA表单根本不导入 - 新PDF显示“请稍候...如果此消息最终未被文档的正确内容替换,则您的PDF查看器可能无法显示此类文档。 ..“。

这是我的代码:

import java.awt.Color;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfWriter;
import com.lowagie.text.Font;
import com.lowagie.text.pdf.PdfGState;
import com.lowagie.text.pdf.PdfStamper;
import com.lowagie.text.FontFactory;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.Phrase;
import com.lowagie.text.pdf.ColumnText;
import com.lowagie.text.pdf.PdfImportedPage;

public class PdfFormCopyTest {
    private static final String ACRO_FORM_PDF = "AcroForm.pdf";
    private static final String XFA_FORM_PDF = "XfaForm.pdf";
    private static final String NO_FORM_PDF = "NoForm.pdf";
    private static final String ACRO_FORM_PDF_NEW = "AcroForm-new.pdf";
    private static final String XFA_FORM_PDF_NEW = "XfaForm-new.pdf";
    private static final String NO_FORM_PDF_NEW = "NoForm-new.pdf";
    private static final float MARGIN_LEFT = 36.0f;
    private static final float MARGIN_RIGHT = 36.0f;
    private static final float MARGIN_BOTTOM = 56.0f;
    private static final float MARGIN_TOP = 36.0f;
    private static final float FONT_SIZE = 10.0f;
    private static final float MIN_LINE_HEIGHT = FONT_SIZE * 1.5f;

    /**
     * @param args
     */
    public static void main(String[] args) {
        try {
            createPdfFromAcroFormBasedPdf();
            createPdfFromXfaFormBasedPdf();
            createPdfFromFormlessPdf();
        }
        catch (Exception error) {
            System.out.println(error.getMessage());
        }
    }

    private static void createPdfFromAcroFormBasedPdf() throws IOException, DocumentException {
        System.out.println("Creating new PDF from an existing PDF containing AcroForm.....");

        PdfReader reader = new PdfReader(ACRO_FORM_PDF);        
        createNewPdfWithHeaderFooter(reader, ACRO_FORM_PDF_NEW);

        System.out.println("Success");
    }

    private static void createPdfFromXfaFormBasedPdf() throws IOException, DocumentException {
        System.out.println("Creating new PDF from an existing PDF containing XfaForm......");

        PdfReader reader = new PdfReader(XFA_FORM_PDF);
        createNewPdfWithHeaderFooter(reader, XFA_FORM_PDF_NEW);

        System.out.println("Success");
    }

    private static void createPdfFromFormlessPdf() throws IOException, DocumentException {
        System.out.println("Creating new PDF from an existing PDF containing no form......");

        PdfReader reader = new PdfReader(NO_FORM_PDF);
        createNewPdfWithHeaderFooter(reader, NO_FORM_PDF_NEW);

        System.out.println("Success");
    }

    /**
     * Creates a new PDF which contains header and footer from the specified input PdfReader object
     * and saves the result as the specified output file.
     * @param reader A PdfReader for the existing PDF.
     * @param outputFileName Name of the PDF file which contains header and footer.
     * @throws IOException
     * @throws DocumentException
     */
    private static void createNewPdfWithHeaderFooter(PdfReader reader, String outputFileName)
            throws IOException, DocumentException {
        String footer = getFooter();
        String header = getHeader();
        List<Float> footerHeights = computeHeights(footer, reader, Font.NORMAL);
        List<Float> headerHeights = computeHeights(header, reader, Font.BOLD);

        InputStream resizedPdfStream = createPdfWithHeaderFooterSpace(reader, footerHeights, headerHeights);

        PdfStamper stamper = null;
        try {
            FileOutputStream fos = new FileOutputStream(outputFileName);
            PdfReader newReader = new PdfReader(resizedPdfStream);
            stamper = new PdfStamper(newReader, fos);

            int numberOfPages = stamper.getReader().getNumberOfPages();
            for (int pageNumber = 1; pageNumber <= numberOfPages; pageNumber++) {
                Rectangle rect = stamper.getReader().getPageSize(pageNumber);
                PdfContentByte pageContent = stamper.getOverContent(pageNumber);
                pageContent.saveState();
                pageContent.setGState(new PdfGState());
                renderHeaderFooter(rect, pageContent, header, footer);
                pageContent.restoreState();
            }
        }
        finally {
            if (stamper != null) {
                stamper.close();
            }
        }
    }

    /**
     * Computes the height of the specified content for each page
     * in the specified PdfReader with the specified font weight.
     * @param content The string content for which the height of each page is computed.
     * @param reader A PdfReader containing the existing PDF.
     * @param fontWeight The font weight.
     * @return A list of float representing the height of each page.
     * @throws IOException
     * @throws DocumentException
     */
    private static List<Float> computeHeights(String content, PdfReader reader, int fontWeight)
            throws IOException, DocumentException {
        List<Float> contentHeights = new ArrayList<Float>();

        int numberOfPages = reader.getNumberOfPages();
        for (int pageNumber = 1; pageNumber <= numberOfPages; pageNumber++) {
            Rectangle pageSize = reader.getPageSize(pageNumber);
            float height = computeWrappedTextHeight(content, pageSize.getWidth(), fontWeight);
            contentHeights.add(pageNumber - 1, height);
        }

        return contentHeights;
    }

    /**
     * Creates a new PDF with place holder for header and footer from the specified parameters.
     * @param reader The PdfReader storing the contents of the PDF to be created.
     * @param footerHeights The footer height for each page.
     * @param headerHeights The header height for each page.
     * @return An InputStream representing the new PDF.
     * @throws IOException
     * @throws DocumentException
     */
    private static InputStream createPdfWithHeaderFooterSpace(PdfReader reader,
            List<Float> footerHeights, List<Float> headerHeights) throws IOException, DocumentException {
        ByteArrayOutputStream baos = null;
        Document newDocument = null;

        try {
            baos = new ByteArrayOutputStream();
            newDocument = new Document();
            PdfWriter newPdfWriter = PdfWriter.getInstance(newDocument, baos);
            PdfContentByte newPdfCanvas = null;

            int numberOfPages = reader.getNumberOfPages();
            for (int pageNumber = 1; pageNumber <= numberOfPages; pageNumber++) {
                Rectangle oldPageSize = reader.getPageSize(pageNumber);
                float oldPageWidth = oldPageSize.getWidth();
                float oldPageHeight = oldPageSize.getHeight();
                float footerHeight = footerHeights.get(pageNumber - 1);
                float headerHeight = headerHeights.get(pageNumber - 1);
                float newPageHeight = calculateNewPageHeight(oldPageHeight, headerHeight, footerHeight);
                float newPageWidth = calculateNewPageWidth(oldPageWidth);
                Rectangle newPageSize  = new Rectangle(0, 0, newPageWidth, newPageHeight);
                newDocument.setPageSize(newPageSize);

                if (!newDocument.isOpen()) {
                    newDocument.open();
                    newPdfCanvas = newPdfWriter.getDirectContent();
                }

                float xFactor = 1.0f;
                float yFactor = 1.0f;
                float xOffset = MARGIN_LEFT;
                float yOffset = MARGIN_BOTTOM + footerHeight;

                PdfImportedPage importedPage = newPdfWriter.getImportedPage(reader, pageNumber);
                newPdfCanvas.addTemplate(importedPage, xFactor, 0, 0, yFactor, xOffset, yOffset);
                newDocument.newPage();
            }
        }
        finally {
            if (newDocument != null && newDocument.isOpen()) {
                newDocument.close();
            }
        }

        return new ByteArrayInputStream(baos.toByteArray());
    }

    /**
     * Computes the height of the specified string content which must
     * wrap at the specified maximum line width with the specified font weight.
     * @param content The string content for which the height is computed.
     * @param maxLineWidth The maximum line width at which the content must wrap.
     * @param fontWeight The font weight.
     * @return The height of the specified content which wraps at
     * the specified maximum line width with the specified font weight.
     */
    private static float computeWrappedTextHeight(String content, float maxLineWidth, int fontWeight) {
        float totalHeight = 0.0f;

        Font font = FontFactory.getFont(BaseFont.HELVETICA, FONT_SIZE);
        font.setStyle(fontWeight);
        BaseFont baseFont = font.getCalculatedBaseFont(true);

        String lineText = "";
        int currentWordStart = -1;
        float lineHeight;

        for (int charIndex = 0; charIndex < content.length(); charIndex++) {
            String currentChar = content.substring(charIndex, charIndex + 1);
            lineText = lineText + currentChar;
            boolean isCurrentCharWordSeparator = isWordSeperator(currentChar);
            float lineWidth = computeLineWidth(lineText, baseFont);
            if (charIndex == 0 || (!isCurrentCharWordSeparator && currentWordStart < 0)) {
                currentWordStart = charIndex;
            }

            if (lineWidth > maxLineWidth || currentChar.equals("\n")) {
                // Start a new line.
                if (isCurrentCharWordSeparator) {
                    // The current character is a word separator - break the line at the current character.
                    lineHeight = computeLineHeight(lineText, baseFont);

                    // Reset line text.
                    if (currentChar.equals("\n")) {
                        lineText = "";
                    }
                    else {
                        lineText = currentChar;
                    }
                }
                else {
                    // The current character is in the middle of a word - break the line at the previous word separator.
                    int lineEnd = lineText.length() - (charIndex - currentWordStart) - 1;
                    if (lineEnd > 0) {
                        String currentWordExcludedLineText = lineText.substring(0, lineEnd);
                        lineHeight = computeLineHeight(currentWordExcludedLineText, baseFont);

                        charIndex = currentWordStart; // New line starts at the beginning of the current word.
                        lineText = "";
                    }
                    else {
                        lineHeight = computeLineHeight(lineText, baseFont);
                        lineText =  currentChar;
                    }
                }

                totalHeight = totalHeight + lineHeight;
            }

            // If it is at a new word break, reset the current word starting index so that
            // the next iteration can set it at the beginning of the next word.
            if (charIndex > 0 && isCurrentCharWordSeparator && currentWordStart >= 0) {
                currentWordStart = -1;
            }
        }

        lineHeight = computeLineHeight(lineText, baseFont);
        totalHeight = totalHeight + lineHeight;

        return totalHeight;
    }

    /**
     * Determines if the specified string is a word separator.
     * @param c The string to test.
     * @return true if the specified string is a word separator; false othewise. 
     */
    private static boolean isWordSeperator(String c) {
        return (c.equals("\n") || c.equals("\t") || c.equals(" "));
    }

    /**
     * Computes the line width of the specified line text with the specified base font.
     * @param lineText The line text.
     * @param baseFont A BaseFont object representing the base font of the line.
     * @return A float representing the width of the line.
     */
    private static float computeLineWidth(String lineText, BaseFont baseFont) {
        return baseFont.getWidthPoint(lineText, FONT_SIZE);
    }

    /**
     * Computes the line height with the specified parameters.
     * @param lineText The line text.
     * @param baseFont A BaseFont object representing the base font of the line.
     * @return A float value representing the height of the line.
     */
    private static float computeLineHeight(String lineText, BaseFont baseFont) {
        float lineHeight = baseFont.getAscentPoint(lineText, FONT_SIZE) - baseFont.getDescentPoint(lineText, FONT_SIZE);

        if (lineHeight < MIN_LINE_HEIGHT) {
            lineHeight = MIN_LINE_HEIGHT;
        }

        return lineHeight;
    }

    /**
     * Renders the header and footer to the specified Rectangle with the specified page content, header and footer.
     * @param rect A Rectangle to render the header and footer.
     * @param pageContent A PdfContentByte representing the content of the page.
     * @param header The page header.
     * @param footer The page footer.
     * @throws DocumentException
     * @throws IOException
     */
    private static void renderHeaderFooter(Rectangle rect, PdfContentByte pageContent, String header, String footer)
            throws DocumentException, IOException {
        float margin = 36.0f;
        int sides = 2;

        float footerHeight = (float)Math.ceil(computeWrappedTextHeight(footer, rect.getWidth() - margin * sides, Font.NORMAL));
        float headerHeight = (float)Math.ceil(computeWrappedTextHeight(footer, rect.getWidth() - margin * sides, Font.BOLD));
        if (headerHeight < MIN_LINE_HEIGHT) {
            headerHeight = MIN_LINE_HEIGHT;
        }

        // Render header.
        Font headerFont = getDefaultFont();
        headerFont.setStyle(Font.BOLD);
        Phrase headerPhrase = new Phrase(header, headerFont);
        ColumnText headerRenderer = new ColumnText(pageContent);
        headerRenderer.setSimpleColumn(headerPhrase, margin, rect.getHeight() - headerHeight - margin + 4,
            rect.getWidth() - margin, rect.getHeight() - margin + 4, MIN_LINE_HEIGHT, Element.ALIGN_RIGHT);
        headerRenderer.go();    

        // Render footer.
        Phrase footerPhrase = new Phrase(footer, getDefaultFont());
        ColumnText footerRender = new ColumnText(pageContent);
        footerRender.setSimpleColumn(footerPhrase, margin, margin, rect.getWidth() - margin, footerHeight + margin, MIN_LINE_HEIGHT, Element.ALIGN_CENTER);
        footerRender.go();
    }

    /**
     * Calculates the height of the new page with the specified parameters.
     * @param oldPageHeight The height of the old page.
     * @param headerHeight The height of header.
     * @param footerHeight The height of footer.
     * @return The height of the new page.
     */
    private static float calculateNewPageHeight(float oldPageHeight, float headerHeight, float footerHeight) {
        return oldPageHeight + MARGIN_TOP + headerHeight + footerHeight + MARGIN_BOTTOM;
    }

    /**
     * Calculates the width of the new page with the specified width of old page.
     * @param oldPageWidth The width of the old page.
     * @return The width of the new page.
     */
    private static float calculateNewPageWidth(float oldPageWidth) {
        return oldPageWidth + MARGIN_LEFT + MARGIN_RIGHT;
    }

    private static String getHeader() {
        return "This is dynamically added header.";
    }

    private static String getFooter() {
        StringBuilder footerBuilder = new StringBuilder();

        footerBuilder.append("This is the dynamically added footer.");
        footerBuilder.append("\n\n");
        footerBuilder.append("Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. ");
        footerBuilder.append("Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. ");
        footerBuilder.append("Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. ");
        footerBuilder.append("Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. ");
        footerBuilder.append("Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. ");
        footerBuilder.append("Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. Footer paragraph 1 content. ");
        footerBuilder.append("\n\n");
        footerBuilder.append("Footer paragraph 2 content. Footer paragraph 2 content. Footer paragraph 2 content. Footer paragraph 2 content. ");
        footerBuilder.append("Footer paragraph 2 content. Footer paragraph 2 content. Footer paragraph 2 content. Footer paragraph 2 content. ");
        footerBuilder.append("Footer paragraph 2 content. Footer paragraph 2 content. Footer paragraph 2 content. Footer paragraph 2 content. ");
        footerBuilder.append("Footer paragraph 2 content. Footer paragraph 2 content. Footer paragraph 2 content. Footer paragraph 2 content. ");
        footerBuilder.append("\n\n");
        footerBuilder.append("Footer paragraph 3 content. Footer paragraph 3 content. Footer paragraph 3 content. Footer paragraph 3 content.");

        return footerBuilder.toString();
    }

    private static Font getDefaultFont() {
        return FontFactory.getFont(BaseFont.HELVETICA, FONT_SIZE, Color.BLACK);
    }
}

1 个答案:

答案 0 :(得分:2)

  

我需要使用iText以编程方式将页眉和页脚添加到现有的基于表单的PDF中。现有的PDF来自用户,它不包含页眉和页脚的空间。因此,解决方案是通过将现有PDF的内容与页眉和页脚连接来创建新的PDF。

不,这不是一个好的解决方案。您最好使用PdfStamper,更改现有页面的大小,并在新页面区域中添加页眉和页脚。特别是当你现在已经使用PdfStamper作为最后一步时。

<{3}}中的@Mark Storer展示了如何操纵 MediaBox 的底部。同样,您也可以更改其顶部。正如Mark在他的回答中所说,你可能还需要改变 CropBox

  

但是,此方法仅适用于不包含任何形式的PDF。对于包含AcroForm或XFA表单的交互式PDF,它失败如下:(1)AcroForm在新PDF中变得扁平化。

使用您的代码,AcroForm表单元素不应该展平(即它们的外观不应该添加到静态PDF内容中),但它们应该丢失。但是,有时候,边界线或表格字段边界的其他指示实际上已经是静态内容的一部分。这可能是你的情况。

原因是您的代码使用PdfWriter.getImportedPage,这种方法只接受页面内容流,但没有像AcroForm这样的交互式功能表单字段小部件注释。

  

(2)XFA表单根本不导入 - 新的PDF显示&#34;请稍候...如果此消息最终未被文档的正确内容替换,则您的PDF查看器可能不是能够显示这种类型的文件...&#34;。

XFA表单是其自己的文档类型,仅使用PDF文件作为传输介质。您的PdfWriter.getImportedPage甚至没有在文档中看到XFA数据,只复制了XFA PDF文档在没有XFA支持的情况下在PDF查看器上显示的页面。

如果是XFA表单,PDF页面对象通常不会显示最终显示的内容。而是PDF传输XFA XML。因此,对任何现有PDF页面的所有更改都是看不见的。您必须提取该XFA XML,对其进行操作并再次存储。

iText对XFA的支持有限,而您似乎使用的古代版本根本没有。