【问题标题】:How to retain page labels when concatenating an existing pdf with a pdf created from scratch?将现有 pdf 与从头创建的 pdf 连接时如何保留页面标签?
【发布时间】:2015-03-07 04:49:50
【问题描述】:

我有一个代码,它正在创建一个“封面”,然后将它与现有的 pdf 合并。合并后 pdf 标签丢失。如何保留现有 pdf 的 pdf 标签,然后将页面标签添加到从头创建的 pdf 页面(例如“封面页”)?我认为这本书的例子是关于检索和替换页面标签。在将现有 pdf 与从头创建的 pdf 连接时,我不知道如何应用它。我正在使用 itext 5.3.0。提前致谢。

编辑 根据 mkl 的评论

public ByteArrayOutputStream getConcatenatePDF()
{
    if (bitstream == null)
        return null;

    if (item == null)
    {
        item = getItem();
        if (item == null)
            return null;
    }

    ByteArrayOutputStream byteout = null;
    InputStream coverStream = null;

    try
    {
        // Get Cover Page
        coverStream = getCoverStream();
        if (coverStream == null) 
            return null;

        byteout = new ByteArrayOutputStream();
        int pageOffset = 0;
        ArrayList<HashMap<String, Object>> master = new ArrayList<HashMap<String, Object>>();

        Document document = null;
        PdfCopy    writer = null;
        PdfReader  reader = null;

        byte[] password = (ownerpass != null && !"".equals(ownerpass)) ? ownerpass.getBytes() : null;

        // Get infomation of the original pdf
        reader = new PdfReader(bitstream.retrieve(), password);

        boolean isPortfolio = reader.getCatalog().contains(PdfName.COLLECTION);
        char version = reader.getPdfVersion();
        int permissions = reader.getPermissions();

        // Get metadata
        HashMap<String, String> info = reader.getInfo();
        String title = (info.get("Title") == null || "".equals(info.get("Title")))
            ? getFieldValue("dc.title") : info.get("Title");
        String author = (info.get("Author") == null || "".equals(info.get("Author")))
            ? getFieldValue("dc.contributor.author") : info.get("Author");
        String subject = (info.get("Subject") == null || "".equals(info.get("Subject")))
            ? "" : info.get("Subject");
        String keywords = (info.get("Keywords") == null || "".equals(info.get("Keywords")))
            ? getFieldValue("dc.subject") : info.get("Keywords");

        reader.close();

        // Merge cover page and the original pdf
        InputStream[] is = new InputStream[2];
        is[0] = coverStream;
        is[1] = bitstream.retrieve();

        for (int i = 0; i < is.length; i++) 
        {
            // we create a reader for a certain document
            reader = new PdfReader(is[i], password);
            reader.consolidateNamedDestinations();

            if (i == 0) 
            {
                // step 1: creation of a document-object
                document = new Document(reader.getPageSizeWithRotation(1));

                // step 2: we create a writer that listens to the document
                writer = new PdfCopy(document, byteout);

                // Set metadata from the original pdf 
                // the position of these lines is important
                document.addTitle(title);
                document.addAuthor(author);
                document.addSubject(subject);
                document.addKeywords(keywords);

                if (pdfa)
                {
                    // Set thenecessary information for PDF/A-1B
                    // the position of these lines is important
                    writer.setPdfVersion(PdfWriter.VERSION_1_4);
                    writer.setPDFXConformance(PdfWriter.PDFA1B);
                    writer.createXmpMetadata();
                }
                else if (version == '5')
                    writer.setPdfVersion(PdfWriter.VERSION_1_5);
                else if (version == '6')
                    writer.setPdfVersion(PdfWriter.VERSION_1_6);
                else if (version == '7')
                    writer.setPdfVersion(PdfWriter.VERSION_1_7);
                else
                    ;  // no operation

                // Set security parameters
                if (!pdfa)
                {
                    if (password != null)
                    {
                        if (security && permissions != 0) 
                        {
                            writer.setEncryption(null, password, permissions, PdfWriter.STANDARD_ENCRYPTION_128);
                        } 
                        else
                        {
                            writer.setEncryption(null, password, PdfWriter.ALLOW_PRINTING | PdfWriter.ALLOW_COPY | PdfWriter.ALLOW_SCREENREADERS, PdfWriter.STANDARD_ENCRYPTION_128);
                        }
                    }
                }

                // step 3: we open the document
                document.open();

                // if this pdf is portfolio, does not add cover page
                if (isPortfolio)
                {
                    reader.close();
                    byte[] coverByte = getCoverByte();
                    if (coverByte == null || coverByte.length == 0) 
                        return null;
                    PdfCollection collection = new PdfCollection(PdfCollection.TILE);
                    writer.setCollection(collection);

                    PdfFileSpecification fs = PdfFileSpecification.fileEmbedded(writer, null, "cover.pdf", coverByte);
                    fs.addDescription("cover.pdf", false);
                    writer.addFileAttachment(fs);
                    continue;
                }
            }
            int n = reader.getNumberOfPages();
            // step 4: we add content
            PdfImportedPage page;
            PdfCopy.PageStamp stamp;
            for (int j = 0; j < n; )
            {
                ++j;
                page = writer.getImportedPage(reader, j);
                if (i == 1) {
                    stamp = writer.createPageStamp(page);
                    Rectangle mediabox = reader.getPageSize(j);
                    Rectangle crop = new Rectangle(mediabox);
                    writer.setCropBoxSize(crop);
                    // add overlay text
                    //<-- Code for adding overlay text -->
                    stamp.alterContents();
                }
                writer.addPage(page);
            }

            PRAcroForm form = reader.getAcroForm();
            if (form != null && !pdfa)
            {
                writer.copyAcroForm(reader);
            }
            // we retrieve the total number of pages
            List<HashMap<String, Object>> bookmarks = SimpleBookmark.getBookmark(reader);
            //if (bookmarks != null && !pdfa) 
            if (bookmarks != null) 
            {
                if (pageOffset != 0)
                {
                    SimpleBookmark.shiftPageNumbers(bookmarks, pageOffset, null);
                }
                master.addAll(bookmarks);
            }
            pageOffset += n;
        }
        if (!master.isEmpty())
        {
            writer.setOutlines(master);
        }

        if (isPortfolio)
        {
            reader = new PdfReader(bitstream.retrieve(), password);
            PdfDictionary catalog = reader.getCatalog();
            PdfDictionary documentnames = catalog.getAsDict(PdfName.NAMES);
            PdfDictionary embeddedfiles = documentnames.getAsDict(PdfName.EMBEDDEDFILES);
            PdfArray filespecs = embeddedfiles.getAsArray(PdfName.NAMES);
            PdfDictionary filespec;
            PdfDictionary refs;
            PRStream stream;
            PdfFileSpecification fs;
            String path;
            // copy embedded files
            for (int i = 0; i < filespecs.size(); ) 
            {
                filespecs.getAsString(i++);     // remove description
                filespec = filespecs.getAsDict(i++);
                refs = filespec.getAsDict(PdfName.EF);
                for (PdfName key : refs.getKeys()) 
                {
                    stream = (PRStream) PdfReader.getPdfObject(refs.getAsIndirectObject(key));
                    path = filespec.getAsString(key).toString();
                    fs = PdfFileSpecification.fileEmbedded(writer, null, path, PdfReader.getStreamBytes(stream));
                    fs.addDescription(path, false);
                    writer.addFileAttachment(fs);
                }
            }
        }

        if (pdfa)
        {
            InputStream iccFile = this.getClass().getClassLoader().getResourceAsStream(PROFILE);
            ICC_Profile icc = ICC_Profile.getInstance(iccFile);
            writer.setOutputIntents("Custom", "", "http://www.color.org", "sRGB IEC61966-2.1", icc);
            writer.setViewerPreferences(PdfWriter.PageModeUseOutlines);
        }
        // step 5: we close the document
        document.close();
    } 
    catch (Exception e) 
    {
        log.info(LogManager.getHeader(context, "cover_page: getConcatenatePDF", "bitstream_id="+bitstream.getID()+", error="+e.getMessage()));
        // e.printStackTrace();
        return null;
    }

    return byteout;
}

更新

根据 mkl 的回答,我将上面的代码修改为如下所示:

    public ByteArrayOutputStream getConcatenatePDF()
{
    if (bitstream == null)
        return null;

    if (item == null)
    {
        item = getItem();
        if (item == null)
            return null;
    }

    ByteArrayOutputStream byteout = null;
    try
    {
        // Get Cover Page
        InputStream coverStream = getCoverStream();
        if (coverStream == null) 
            return null;

        byteout = new ByteArrayOutputStream();

        InputStream documentStream = bitstream.retrieve();


        PdfReader coverPageReader = new PdfReader(coverStream);
        PdfReader reader = new PdfReader(documentStream);
        PdfStamper stamper = new PdfStamper(reader, byteout);

        PdfImportedPage page = stamper.getImportedPage(coverPageReader, 1);
        stamper.insertPage(1, coverPageReader.getPageSize(1));

        PdfContentByte content = stamper.getUnderContent(1);

        int n = reader.getNumberOfPages();
        for (int j = 2; j <= n; j++) {
           //code for overlay text
            ColumnText.showTextAligned(stamper.getOverContent(j), Element.ALIGN_CENTER, overlayText,
                    crop.getLeft(10), crop.getHeight() / 2 + crop.getBottom(), 90);
        }
        content.addTemplate(page, 0, 0);
        stamper.close();
    }
    catch (Exception e) 
    {
        log.info(LogManager.getHeader(context, "cover_page: getConcatenatePDF", "bitstream_id="+bitstream.getID()+", error="+e.getMessage()));
        e.printStackTrace();
        return null;
    }

    return byteout;
}

然后我将页面标签设置为封面。我省略了与我的问题无关的代码。

/**
 * 
 * @return InputStream the resulting output stream
 */
private InputStream getCoverStream()
{
    ByteArrayOutputStream byteout = getCover();
    return new ByteArrayInputStream(byteout.toByteArray());
}

/**
 * 
 * @return InputStream the resulting output stream
 */
private byte[] getCoverByte()
{
    ByteArrayOutputStream byteout = getCover();
    return byteout.toByteArray();
}

/**
 * 
 * @return InputStream the resulting output stream
 */
    private ByteArrayOutputStream getCover()


{
    ByteArrayOutputStream byteout;
    Document doc = null;
    try 
    {
        byteout = new ByteArrayOutputStream();   
        doc = new Document(PageSize.LETTER, 24, 24, 20, 40);
        PdfWriter pdfwriter = PdfWriter.getInstance(doc, byteout);
        PdfPageLabels labels = new PdfPageLabels();
        labels.addPageLabel(1, PdfPageLabels.EMPTY, "Cover page", 1);
        pdfwriter.setPageLabels(labels);

        pdfwriter.setPageEvent(new HeaderFooter());
        doc.open(); 
        //code omitted (contents of cover page)
        doc.close();
        return byteout; 
    } 
    catch (Exception e)
    {
        log.info(LogManager.getHeader(context, "cover_page", "bitstream_id="+bitstream.getID()+", error="+e.getMessage()));
        return null;
    }
}

修改后的代码保留了现有pdf的页面标签(见截图1)(documentStream),但生成的合并pdf(截图2和3)自插入封面以来,已关闭 1 页。根据 mkl 的建议,我应该在封面上使用页面标签,但导入页面的 pdf 标签似乎丢失了。我现在关心的是如何按照 mkl 的建议将页面标签设置为最终文档状态?我想我应该使用 PdfWriter 但我不知道在我修改后的代码中放在哪里。我是否正确假设在stamper.close() 部分之后,这是我文档的最终状态?再次提前感谢。

屏幕截图 1。 注意实际的页面 1 标记 封面

屏幕截图 2. 在插入生成的动态“封面”后合并 pdf。即使在我使用 labels.addPageLabel(1, PdfPageLabels.EMPTY, "Cover page", 1) 设置了插入页面的 pdf 标签后,页面标签“封面”现在也分配给了封面

屏幕截图 3。 请注意,页面标签 3 已分配给页面 2。

最终更新 感谢@mkl 下面的截图是我应用 mkl 答案的最新更新后的结果。页面标签现在已正确分配给页面。此外,使用 PdfStamper 代替 PdfCopy(在我的原始代码中使用)并没有破坏现有 pdf 的 PDF/A 合规性。

【问题讨论】:

  • 我有一个代码......将它与现有的 pdf 合并。 - 因为你没有显示该代码(许多人有不适当甚至不正确的合并例程),很难解释如何改进它。
  • @mkl,我已经发布了部分代码。谢谢你的评论。我希望有一个关于如何实现我想要的示例,但如果你能以某种方式改进我的代码,我将不胜感激。
  • 嗯,我看到您投入了大量工作来将一份文档中的所有信息保存在合并副本中。我建议您改为使用 PdfStamper 为该文档并将标题页作为第一页导入那里。这将自动保留所有原始文档级别的信息。我手头只有一部智能手机,因此无法详述。
  • 修改后的代码保留了现有 pdf (documentStream) 的页面标签,但合并后的 pdf 因插入封面页而偏离了 1 页。 - 究竟是什么你的意思是?以前称为“1”的页面是否显示“2”,但您希望它仍显示“1”?还是以前称为“1”的页面仍然显示“1”,但您希望它显示“2”,因为之前插入了一个页面?
  • 是的,只有 s PdfCopy 导入的页面会自动保留交互功能。您将不得不手动复制该链接注释。后来……

标签: pdf itext


【解决方案1】:

添加封面

通常使用PdfCopy 合并 PDF 是正确的选择,它从复制的页面创建一个新文档,尽可能多地复制页面级信息,而不是首选任何单个文档。

不过,您的情况有些特殊:您有一个文档,您喜欢其结构和内容,并希望通过添加一个页面(即标题页)对其进行小幅更改。来自主文档的所有所有信息包括文档级信息(例如元数据、嵌入文件等)仍应出现在结果中。

在这种用例中,使用PdfStamper 更合适,您可以将更改“标记”到现有 PDF 上。

你可能想从this这样的东西开始:

try (   InputStream documentStream = getClass().getResourceAsStream("template.pdf");
        InputStream titleStream = getClass().getResourceAsStream("title.pdf");
        OutputStream outputStream = new FileOutputStream(new File(RESULT_FOLDER, "test-with-title-page.pdf"))    )
{
    PdfReader titleReader = new PdfReader(titleStream);
    PdfReader reader = new PdfReader(documentStream);
    PdfStamper stamper = new PdfStamper(reader, outputStream);

    PdfImportedPage page = stamper.getImportedPage(titleReader, 1);
    stamper.insertPage(1, titleReader.getPageSize(1));
    PdfContentByte content = stamper.getUnderContent(1);
    content.addTemplate(page, 0, 0);

    stamper.close();
}

PS:关于cmets中的问题:

在我上面的代码中,我应该有一个覆盖文本(在 stamp.alterContents() 部分之前),但出于测试目的我省略了这部分代码。你能告诉我如何实现它吗?

您是指叠加水印之类的东西吗? PdfStamper 允许您访问可以在其上绘制任何内容的每个页面的“over content”:

PdfContentByte overContent = stamper.getOverContent(pageNumber);

保留页面标签

我的另一个问题是关于页面偏移,因为我插入了封面,页码偏离了 1 页。我该如何解决?

不幸的是,iText 的PdfStamper不会自动更新被操作 PDF 的页面标签定义。实际上这并不奇怪,因为不清楚插入的页面是如何标记的。 @Bruno 至少,iText 可以更改从插入页码开始 开始的页面标签部分。

但是,使用 iText 的低级 API 可以修复原始标签位置并为插入的页面添加标签。这可以与 iText in Action PageLabelExample 示例类似地实现,更准确地说是它的 manipulatePageLabel 部分;只需在stamper.close() 之前添加this

    PdfDictionary root = reader.getCatalog();
    PdfDictionary labels = root.getAsDict(PdfName.PAGELABELS);
    if (labels != null)
    {
        PdfArray newNums = new PdfArray();

        newNums.add(new PdfNumber(0));
        PdfDictionary coverDict = new PdfDictionary();
        coverDict.put(PdfName.P, new PdfString("Cover Page"));
        newNums.add(coverDict);

        PdfArray nums = labels.getAsArray(PdfName.NUMS);
        if (nums != null)
        {
            for (int i = 0; i < nums.size() - 1; )
            {
                int n = nums.getAsNumber(i++).intValue();
                newNums.add(new PdfNumber(n+1));
                newNums.add(nums.getPdfObject(i++));
            }
        }

        labels.put(PdfName.NUMS, newNums);
        stamper.markUsed(labels);
    }

对于带有这些标签的文档:

它会生成一个带有这些标签的文档:

保持链接

我刚刚发现插入的页面“封面页”丢失了链接注释。我想知道是否有解决方法,因为根据本书,使用 PdfStamper 时插入页面的交互功能会丢失。

确实,在 iText PDF 生成类中,只有 Pdf*Copy* 保留了注释等交互功能。不幸的是,人们必须决定是否愿意

  • 创建一个真正的新 PDF (PdfWriter),除了可嵌入的内容之外,没有来自其他 PDF 的任何信息;
  • 处理单个现有 PDF ('PdfStamper'),保留该 PDF 中的所有信息,但除了可嵌入的内容外,没有其他 PDF 中的任何信息;
  • 合并任意数量的现有 PDF (PdfCopy),保留所有这些 PDF 中的大部分页面级信息,但没有任何文档级信息。

在您的情况下,我认为新的封面只有静态内容,没有动态功能,因此假设 PdfStamper 是最好的。如果您只需要处理链接,您可以考虑手动复制链接,例如使用这个辅助方法

/**
 * <p>
 * A primitive attempt at copying links from page <code>sourcePage</code>
 * of <code>PdfReader reader</code> to page <code>targetPage</code> of
 * <code>PdfStamper stamper</code>.
 * </p>
 * <p>
 * This method is meant only for the use case at hand, i.e. copying a link
 * to an external URI without expecting any advanced features.
 * </p>
 */
void copyLinks(PdfStamper stamper, int targetPage, PdfReader reader, int sourcePage)
{
    PdfDictionary sourcePageDict = reader.getPageNRelease(sourcePage);
    PdfArray annotations = sourcePageDict.getAsArray(PdfName.ANNOTS);
    if (annotations != null && annotations.size() > 0)
    {
        for (PdfObject annotationObject : annotations)
        {
            annotationObject = PdfReader.getPdfObject(annotationObject);
            if (!annotationObject.isDictionary())
                continue;
            PdfDictionary annotation = (PdfDictionary) annotationObject;
            if (!PdfName.LINK.equals(annotation.getAsName(PdfName.SUBTYPE)))
                continue;

            PdfArray rectArray = annotation.getAsArray(PdfName.RECT);
            if (rectArray == null || rectArray.size() < 4)
                continue;
            Rectangle rectangle = PdfReader.getNormalizedRectangle(rectArray);

            PdfName hightLight = annotation.getAsName(PdfName.H);
            if (hightLight == null)
                hightLight = PdfAnnotation.HIGHLIGHT_INVERT;

            PdfDictionary actionDict = annotation.getAsDict(PdfName.A);
            if (actionDict == null || !PdfName.URI.equals(actionDict.getAsName(PdfName.S)))
                continue;
            PdfString urlPdfString = actionDict.getAsString(PdfName.URI);
            if (urlPdfString == null)
                continue;
            PdfAction action = new PdfAction(urlPdfString.toString());

            PdfAnnotation link = PdfAnnotation.createLink(stamper.getWriter(), rectangle, hightLight, action);
            stamper.addAnnotation(link, targetPage);
        }
    }
}

您可以在插入原始页面后立即调用:

        PdfImportedPage page = stamper.getImportedPage(titleReader, 1);
        stamper.insertPage(1, titleReader.getPageSize(1));
        PdfContentByte content = stamper.getUnderContent(1);
        content.addTemplate(page, 0, 0);
        copyLinks(stamper, 1, titleReader, 1);

注意,这个方法真的很简单。它仅考虑具有 URI 操作的链接,并使用与原始页面相同的位置、目标和突出显示设置在目标页面上创建链接。如果原始版本使用了更精细的特征(例如,如果它带来了自己的外观流,甚至仅使用边框样式属性)并且您想保留这些特征,则必须改进方法以将这些特征的条目也复制到新注释。

【讨论】:

  • 谢谢,使用您的方法确实改进并缩短了我的代码!;-) 我有 2 个问题。在我上面的代码中,我应该有一个覆盖文本(stamp.alterContents() 部分之前),但出于测试目的我省略了该部分代码。你能给我一个想法如何实现吗?我的另一个问题是关于页面偏移,因为我插入了封面,页码偏离了 1 页。我该如何解决?再次感谢。
  • 我已经设法解决了覆盖问题。我的叠加 btw 应该插入到现有 pdf 的每一页中。我使用 for 循环和stamper.getOverContent() 做到了这一点。我似乎无法解决页面偏移量。我按照您的建议使用页面标签。所以我在生成的“封面”中添加了页面标签,但是我分配的页面标签丢失了。我在书中读到“正在导入的页面上存在的交互功能丢失了。”这包括页面标签吗?提前致谢。
  • 我似乎无法解决页面偏移量。 - 我会考虑将页面标签设置为最终文档状态。
  • 如果您能指导我如何在最终文档状态下设置页面标签,我将非常感谢您。我将根据您的回答发布我提出的代码。非常感谢...
  • 请先解释一下合并后的 pdf 是如何减少 1 页,参见。我对你原来的问题的新评论。如果您能提供示例文档(主要、封面、组合),那就更清楚了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-04
  • 1970-01-01
  • 2020-08-27
  • 1970-01-01
  • 2016-01-29
  • 1970-01-01
相关资源
最近更新 更多