All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.pdflayout.PageLayoutPDF Maven / Gradle / Ivy

There is a newer version: 7.3.5
Show newest version
/*
 * Copyright (C) 2014-2024 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.pdflayout;

import java.awt.color.ColorSpace;
import java.awt.color.ICC_Profile;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.util.Calendar;
import java.util.GregorianCalendar;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.WillClose;
import javax.annotation.concurrent.NotThreadSafe;

import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDMarkInfo;
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot;
import org.apache.pdfbox.pdmodel.graphics.color.PDOutputIntent;
import org.apache.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences;
import org.apache.xmpbox.XMPMetadata;
import org.apache.xmpbox.schema.AdobePDFSchema;
import org.apache.xmpbox.schema.DublinCoreSchema;
import org.apache.xmpbox.schema.PDFAIdentificationSchema;
import org.apache.xmpbox.schema.XMPBasicSchema;
import org.apache.xmpbox.type.BadFieldValueException;
import org.apache.xmpbox.xml.XmpSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.datetime.PDTConfig;
import com.helger.commons.datetime.PDTFactory;
import com.helger.commons.io.file.FileHelper;
import com.helger.commons.io.stream.NonBlockingByteArrayInputStream;
import com.helger.commons.io.stream.NonBlockingByteArrayOutputStream;
import com.helger.commons.io.stream.StreamHelper;
import com.helger.commons.state.EChange;
import com.helger.commons.string.StringHelper;
import com.helger.commons.vendor.VendorInfo;
import com.helger.pdflayout.base.IPLVisitable;
import com.helger.pdflayout.base.IPLVisitor;
import com.helger.pdflayout.base.PLPageSet;
import com.helger.pdflayout.base.PLPageSetPrepareResult;
import com.helger.pdflayout.render.PreparationContextGlobal;

/**
 * Main class for creating layouted PDFs. This class contains the meta data as
 * well as a list of {@link PLPageSet} objects that represent a set of pages
 * with a consistent layouting scheme.
 *
 * @author Philip Helger
 */
@NotThreadSafe
public class PageLayoutPDF implements IPLVisitable
{
  /**
   * By default certain parts of the created PDFs are compressed, to safe space.
   */
  public static final boolean DEFAULT_COMPRESS_PDF = true;
  /**
   * By default no PDF/A compliant PDF is created.
   */
  public static final boolean DEFAULT_CREATE_PDF_A = false;

  private static final Logger LOGGER = LoggerFactory.getLogger (PageLayoutPDF.class);

  private String m_sDocumentAuthor;
  private LocalDateTime m_aDocumentCreationDate;
  private String m_sDocumentCreator;
  private String m_sDocumentTitle;
  private String m_sDocumentKeywords;
  private String m_sDocumentSubject;
  private boolean m_bCompressPDF = DEFAULT_COMPRESS_PDF;
  private boolean m_bCreatePDF_A = DEFAULT_CREATE_PDF_A;
  private final ICommonsList  m_aPageSets = new CommonsArrayList <> ();
  private IPDDocumentCustomizer m_aDocumentCustomizer;

  /**
   * Constructor. Initializes Author, CreationDate and Creator from class
   * {@link VendorInfo}.
   */
  public PageLayoutPDF ()
  {
    m_sDocumentAuthor = VendorInfo.getVendorName () + " " + VendorInfo.getVendorURLWithoutProtocol ();
    m_aDocumentCreationDate = PDTFactory.getCurrentLocalDateTime ();
    m_sDocumentCreator = VendorInfo.getVendorName ();
  }

  /**
   * @return if PDF content should be compressed or not.
   */
  public final boolean isCompressPDF ()
  {
    return m_bCompressPDF;
  }

  /**
   * @param bCompressPDF
   *        true to enable creation of compressed PDFs,
   *        false to disable it.
   * @return this for chaining
   */
  @Nonnull
  public final PageLayoutPDF setCompressPDF (final boolean bCompressPDF)
  {
    m_bCompressPDF = bCompressPDF;
    return this;
  }

  /**
   * @return if PDF/A conformant PDF should be created or not.
   * @since 6.0.3
   */
  public final boolean isCreatePDF_A ()
  {
    return m_bCreatePDF_A;
  }

  /**
   * @param bCreatePDF_A
   *        true to enable creation of PDF/A, false to
   *        disable it.
   * @return this for chaining
   * @since 6.0.3
   */
  @Nonnull
  public final PageLayoutPDF setCreatePDF_A (final boolean bCreatePDF_A)
  {
    m_bCreatePDF_A = bCreatePDF_A;
    return this;
  }

  @Nullable
  public final String getDocumentAuthor ()
  {
    return m_sDocumentAuthor;
  }

  @Nonnull
  public final PageLayoutPDF setDocumentAuthor (@Nullable final String sDocumentAuthor)
  {
    m_sDocumentAuthor = sDocumentAuthor;
    return this;
  }

  @Nullable
  public final LocalDateTime getDocumentCreationDateTime ()
  {
    return m_aDocumentCreationDate;
  }

  @Nonnull
  public final PageLayoutPDF setDocumentCreationDateTime (@Nullable final LocalDateTime aDocumentCreationDate)
  {
    m_aDocumentCreationDate = aDocumentCreationDate;
    return this;
  }

  @Nullable
  public final String getDocumentCreator ()
  {
    return m_sDocumentCreator;
  }

  @Nonnull
  public final PageLayoutPDF setDocumentCreator (@Nullable final String sDocumentCreator)
  {
    m_sDocumentCreator = sDocumentCreator;
    return this;
  }

  @Nullable
  public final String getDocumentTitle ()
  {
    return m_sDocumentTitle;
  }

  @Nonnull
  public final PageLayoutPDF setDocumentTitle (@Nullable final String sDocumentTitle)
  {
    m_sDocumentTitle = sDocumentTitle;
    return this;
  }

  @Nullable
  public final String getDocumentKeywords ()
  {
    return m_sDocumentKeywords;
  }

  @Nonnull
  public final PageLayoutPDF setDocumentKeywords (@Nullable final String sDocumentKeywords)
  {
    m_sDocumentKeywords = sDocumentKeywords;
    return this;
  }

  @Nullable
  public final String getDocumentSubject ()
  {
    return m_sDocumentSubject;
  }

  @Nonnull
  public final PageLayoutPDF setDocumentSubject (@Nullable final String sDocumentSubject)
  {
    m_sDocumentSubject = sDocumentSubject;
    return this;
  }

  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList  getAllPageSets ()
  {
    return m_aPageSets.getClone ();
  }

  /**
   * Add a new page set
   *
   * @param aPageSet
   *        The page set to be added. May not be null.
   * @return this for chaining
   */
  @Nonnull
  public PageLayoutPDF addPageSet (@Nonnull final PLPageSet aPageSet)
  {
    ValueEnforcer.notNull (aPageSet, "PageSet");
    m_aPageSets.add (aPageSet);
    return this;
  }

  @Nonnull
  public EChange removePageSet (@Nullable final PLPageSet aPageSet)
  {
    return m_aPageSets.removeObject (aPageSet);
  }

  @Nullable
  public final IPDDocumentCustomizer getDocumentCustomizer ()
  {
    return m_aDocumentCustomizer;
  }

  @Nonnull
  public final PageLayoutPDF setDocumentCustomizer (@Nullable final IPDDocumentCustomizer aDocumentCustomizer)
  {
    m_aDocumentCustomizer = aDocumentCustomizer;
    return this;
  }

  @Nonnull
  public EChange visit (@Nonnull final IPLVisitor aVisitor) throws IOException
  {
    EChange ret = EChange.UNCHANGED;
    for (final PLPageSet aPageSet : m_aPageSets)
      ret = ret.or (aPageSet.visit (aVisitor));
    return ret;
  }

  /**
   * Explicitly prepare all available page sets. That means that the page sets
   * cannot be modified again, but the content sizes can be determined.
   *
   * @since 7.3.1
   */
  public void prepareAllPageSets ()
  {
    // Dummy document
    try (final PDDocument aDoc = new PDDocument ())
    {
      // Global context
      final PreparationContextGlobal aGlobalPrepareCtx = new PreparationContextGlobal (aDoc);
      // Through all page sets
      for (final PLPageSet aPageSet : m_aPageSets)
      {
        aPageSet.prepareAllPages (aGlobalPrepareCtx);
      }
    }
    catch (final IOException ex)
    {
      LOGGER.error ("Failed to prepare page sets", ex);
    }
  }

  /**
   * Render this layout to an OutputStream.
   *
   * @param aOS
   *        The output stream to write to. May not be null. Is
   *        closed automatically.
   * @return this for chaining
   * @throws PDFCreationException
   *         In case of an error
   */
  @Nonnull
  public PageLayoutPDF renderTo (@Nonnull @WillClose final OutputStream aOS) throws PDFCreationException
  {
    ValueEnforcer.notNull (aOS, "OutputStream");

    try (final NonBlockingByteArrayOutputStream aTmpOS = new NonBlockingByteArrayOutputStream ())
    {
      // create a new document
      // Use a buffered OS - approx 30% faster!
      try (final PDDocument aDoc = new PDDocument ();
           final OutputStream aBufferedOS = StreamHelper.getBuffered (m_bCreatePDF_A ? aTmpOS : aOS))
      {
        // Small consistency check to avoid creating empty, invalid PDFs
        int nTotalElements = 0;
        for (final PLPageSet aPageSet : m_aPageSets)
          nTotalElements += aPageSet.getElementCount ();
        if (nTotalElements == 0)
          throw new PDFCreationException ("All page sets are empty!");

        // Set document properties
        {
          final PDDocumentInformation aProperties = new PDDocumentInformation ();
          if (StringHelper.hasText (m_sDocumentAuthor))
            aProperties.setAuthor (m_sDocumentAuthor);
          if (m_aDocumentCreationDate != null)
            aProperties.setCreationDate (GregorianCalendar.from (m_aDocumentCreationDate.atZone (PDTConfig.getDefaultZoneId ())));
          if (StringHelper.hasText (m_sDocumentCreator))
            aProperties.setCreator (m_sDocumentCreator);
          if (StringHelper.hasText (m_sDocumentTitle))
            aProperties.setTitle (m_sDocumentTitle);
          if (StringHelper.hasText (m_sDocumentKeywords))
            aProperties.setKeywords (m_sDocumentKeywords);
          if (StringHelper.hasText (m_sDocumentSubject))
            aProperties.setSubject (m_sDocumentSubject);
          aProperties.setProducer (PLConfig.PROJECT_NAME +
                                   " " +
                                   PLConfig.PROJECT_VERSION +
                                   " - " +
                                   PLConfig.PROJECT_URL);

          // add the created properties
          aDoc.setDocumentInformation (aProperties);
        }

        // Prepare all page sets
        final PreparationContextGlobal aGlobalPrepareCtx = new PreparationContextGlobal (aDoc);
        final PLPageSetPrepareResult [] aPRs = new PLPageSetPrepareResult [m_aPageSets.size ()];
        int nPageSetIndex = 0;
        int nTotalPageCount = 0;
        for (final PLPageSet aPageSet : m_aPageSets)
        {
          final PLPageSetPrepareResult aPR;

          // Handle pre prepared page sets
          if (aPageSet.isPrepared ())
            aPR = aPageSet.internalGetPrepareResult ();
          else
            aPR = aPageSet.prepareAllPages (aGlobalPrepareCtx);
          aPRs[nPageSetIndex] = aPR;
          nTotalPageCount += aPR.getPageCount ();
          nPageSetIndex++;
        }

        // Start applying all page sets - real rendering
        nPageSetIndex = 0;
        final int nPageSetCount = m_aPageSets.size ();
        int nTotalPageIndex = 0;
        for (final PLPageSet aPageSet : m_aPageSets)
        {
          final PLPageSetPrepareResult aPR = aPRs[nPageSetIndex];
          aPageSet.renderAllPages (aPR,
                                   aDoc,
                                   m_bCompressPDF,
                                   nPageSetIndex,
                                   nPageSetCount,
                                   nTotalPageIndex,
                                   nTotalPageCount);
          // Inc afterwards
          nTotalPageIndex += aPR.getPageCount ();
          nPageSetIndex++;
        }

        // Customize the whole document (optional)
        if (m_aDocumentCustomizer != null)
          m_aDocumentCustomizer.customizeDocument (aDoc);

        // save document to output stream
        aDoc.save (aBufferedOS);

        if (LOGGER.isDebugEnabled ())
          LOGGER.debug ("PDF successfully created");
      }
      catch (final IOException ex)
      {
        throw new PDFCreationException ("IO Error", ex);
      }
      catch (final Exception ex)
      {
        throw new PDFCreationException ("Internal error", ex);
      }

      if (m_bCreatePDF_A)
      {
        if (LOGGER.isDebugEnabled ())
          LOGGER.debug ("Start adding PDF/A information");

        // Add metadata (needed by PDF/A)
        try (final PDDocument aDoc = Loader.loadPDF (aTmpOS.getBufferOrCopy ());
             final OutputStream aBufferedOS = StreamHelper.getBuffered (aOS))
        {

          final Calendar aCreationDate = m_aDocumentCreationDate == null ? PDTFactory.createCalendar ()
                                                                         : GregorianCalendar.from (m_aDocumentCreationDate.atZone (PDTConfig.getDefaultZoneId ()));
          final String sProducer = PLConfig.PROJECT_NAME + " " + PLConfig.PROJECT_VERSION;

          final XMPMetadata aXmpMetadata = XMPMetadata.createXMPMetadata ();
          final AdobePDFSchema pdfSchema = aXmpMetadata.createAndAddAdobePDFSchema ();
          pdfSchema.setProducer (sProducer);

          final XMPBasicSchema aXmpBasicSchema = aXmpMetadata.createAndAddXMPBasicSchema ();
          aXmpBasicSchema.setCreatorTool (sProducer);
          aXmpBasicSchema.setCreateDate (aCreationDate);
          aXmpBasicSchema.setModifyDate (aCreationDate);

          final PDDocumentCatalog aDocCatalogue = aDoc.getDocumentCatalog ();

          final PDMarkInfo aMarkInfo = new PDMarkInfo ();
          final PDStructureTreeRoot aTreeRoot = new PDStructureTreeRoot ();
          aDocCatalogue.setMarkInfo (aMarkInfo);
          aDocCatalogue.setStructureTreeRoot (aTreeRoot);
          aDocCatalogue.getMarkInfo ().setMarked (true);

          final PDDocumentInformation aDocInfo = aDoc.getDocumentInformation ();
          aDocInfo.setCreationDate (aCreationDate);
          aDocInfo.setModificationDate (aCreationDate);
          if (StringHelper.hasText (m_sDocumentAuthor))
            aDocInfo.setAuthor (m_sDocumentAuthor);
          aDocInfo.setProducer (sProducer);
          if (StringHelper.hasText (m_sDocumentCreator))
            aDocInfo.setCreator (m_sDocumentCreator);
          if (StringHelper.hasText (m_sDocumentTitle))
            aDocInfo.setTitle (m_sDocumentTitle);
          if (StringHelper.hasText (m_sDocumentSubject))
            aDocInfo.setSubject (m_sDocumentSubject);

          try
          {
            final DublinCoreSchema aDCSchema = aXmpMetadata.createAndAddDublinCoreSchema ();
            if (StringHelper.hasText (m_sDocumentTitle))
              aDCSchema.setTitle (m_sDocumentTitle);
            if (StringHelper.hasText (m_sDocumentCreator))
              aDCSchema.addCreator (m_sDocumentCreator);
            if (StringHelper.hasText (m_sDocumentKeywords))
              aDCSchema.addDescription ("", m_sDocumentKeywords);
            if (StringHelper.hasText (m_sDocumentSubject))
              aDCSchema.addSubject (m_sDocumentSubject);
            aDCSchema.addDate (aCreationDate);

            final PDFAIdentificationSchema aIdentificationSchema = aXmpMetadata.createAndAddPDFAIdentificationSchema ();
            aIdentificationSchema.setPart (Integer.valueOf (3));
            aIdentificationSchema.setConformance ("A");

            try (final NonBlockingByteArrayOutputStream aXmpOS = new NonBlockingByteArrayOutputStream ())
            {
              final XmpSerializer aSerializer = new XmpSerializer ();
              aSerializer.serialize (aXmpMetadata, aXmpOS, true);

              final PDMetadata aMetadata = new PDMetadata (aDoc);
              aMetadata.importXMPMetadata (aXmpOS.toByteArray ());
              aDocCatalogue.setMetadata (aMetadata);
            }
          }
          catch (final BadFieldValueException ex)
          {
            throw new IllegalArgumentException ("Failed to set PDF Metadata", ex);
          }

          // Set color profile (needed by PDF/A)
          final ICC_Profile aRgbProfile = ICC_Profile.getInstance (ColorSpace.CS_sRGB);
          final byte [] aRgbBytes = aRgbProfile.getData ();

          try (final NonBlockingByteArrayInputStream aColorProfile = new NonBlockingByteArrayInputStream (aRgbBytes))
          {
            final PDOutputIntent aIntent = new PDOutputIntent (aDoc, aColorProfile);
            aIntent.setInfo ("sRGB IEC61966-2.1");
            aIntent.setOutputCondition ("sRGB IEC61966-2.1");
            aIntent.setOutputConditionIdentifier ("sRGB IEC61966-2.1");
            aIntent.setRegistryName ("http://www.color.org");

            aDocCatalogue.addOutputIntent (aIntent);
            aDocCatalogue.setLanguage ("de-DE");
          }

          for (final PDPage aPage : aDoc.getPages ())
          {
            final PDViewerPreferences aViewerPrefs = new PDViewerPreferences (aPage.getCOSObject ());
            aViewerPrefs.setDisplayDocTitle (true);
            aDocCatalogue.setViewerPreferences (aViewerPrefs);
          }

          // save document to final output stream
          aDoc.save (aBufferedOS);

          if (LOGGER.isDebugEnabled ())
            LOGGER.debug ("PDF with PDF/A successfully created");
        }
        catch (final IOException ex)
        {
          throw new PDFCreationException ("IO Error", ex);
        }
        catch (final Exception ex)
        {
          throw new PDFCreationException ("Internal error", ex);
        }
      }
    } // close aTmpOS

    return this;
  }

  /**
   * Render this layout to an OutputStream.
   *
   * @param aCustomizer
   *        The customizer to be invoked before the document is written to the
   *        stream. May be null.
   * @param aOS
   *        The output stream to write to. May not be null. Is
   *        closed automatically.
   * @return this for chaining
   * @throws PDFCreationException
   *         In case of an error
   * @deprecated Since 5.1.0; Call
   *             {@link #setDocumentCustomizer(IPDDocumentCustomizer)} and than
   *             {@link #renderTo(OutputStream)}
   */
  @Nonnull
  @Deprecated
  public final PageLayoutPDF renderTo (@Nullable final IPDDocumentCustomizer aCustomizer,
                                       @Nonnull @WillClose final OutputStream aOS) throws PDFCreationException
  {
    setDocumentCustomizer (aCustomizer);
    return renderTo (aOS);
  }

  /**
   * Render this layout to a {@link File}.
   *
   * @param aFile
   *        The output stream to write to. May not be null. Is
   *        closed automatically.
   * @return this for chaining
   * @throws PDFCreationException
   *         In case of an error
   * @throws IllegalArgumentException
   *         In case the file cannot be opened for writing
   * @since 5.1.0
   */
  @Nonnull
  public PageLayoutPDF renderTo (@Nonnull final File aFile) throws PDFCreationException
  {
    final OutputStream aOS = FileHelper.getOutputStream (aFile);
    if (aOS == null)
      throw new IllegalArgumentException ("Failed to open file '" + aFile.getAbsolutePath () + "' for writing");
    return renderTo (aOS);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy