Java tutorial
/** * Copyright (C) 2014-2015 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.element; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.annotation.Nonnegative; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.helger.commons.ValueEnforcer; import com.helger.commons.annotations.Nonempty; import com.helger.commons.annotations.ReturnsMutableCopy; import com.helger.commons.annotations.ReturnsMutableObject; import com.helger.commons.collections.CollectionHelper; import com.helger.commons.string.StringHelper; import com.helger.commons.string.ToStringGenerator; import com.helger.pdflayout.PLDebug; import com.helger.pdflayout.render.ERenderingElementType; import com.helger.pdflayout.render.IRenderingContextCustomizer; import com.helger.pdflayout.render.PDPageContentStreamWithCache; import com.helger.pdflayout.render.PageSetupContext; import com.helger.pdflayout.render.PreparationContext; import com.helger.pdflayout.render.RenderPageIndex; import com.helger.pdflayout.render.RenderingContext; import com.helger.pdflayout.spec.BorderSpec; import com.helger.pdflayout.spec.BorderStyleSpec; import com.helger.pdflayout.spec.EVertAlignment; import com.helger.pdflayout.spec.SizeSpec; /** * Represents a single page layout as element * * @author Philip Helger */ public class PLPageSet extends AbstractPLBaseElement<PLPageSet> { public static final class PageSetPrepareResult { private float m_fHeaderHeight = Float.NaN; private final List<PLElementWithSize> m_aContentHeight = new ArrayList<PLElementWithSize>(); private float m_fFooterHeight = Float.NaN; private final List<List<PLElementWithSize>> m_aPerPageElements = new ArrayList<List<PLElementWithSize>>(); PageSetPrepareResult() { } /** * Set the page header height. * * @param fHeaderHeight * Height without padding or margin. */ void setHeaderHeight(final float fHeaderHeight) { m_fHeaderHeight = fHeaderHeight; } /** * @return Page header height without padding or margin. */ public float getHeaderHeight() { return m_fHeaderHeight; } /** * @param aElement * The element to be added. May not be <code>null</code>. The element * height must be without padding or margin. */ void addElement(@Nonnull final PLElementWithSize aElement) { if (aElement == null) throw new NullPointerException("element"); m_aContentHeight.add(aElement); } /** * @return A list of all elements. Never <code>null</code>. The height of * the contained elements is without padding or margin. */ @Nonnull @ReturnsMutableCopy List<PLElementWithSize> getAllElements() { return CollectionHelper.newList(m_aContentHeight); } /** * Set the page footer height. * * @param fFooterHeight * Height without padding or margin. */ void setFooterHeight(final float fFooterHeight) { m_fFooterHeight = fFooterHeight; } /** * @return Page footer height without padding or margin. */ public float getFooterHeight() { return m_fFooterHeight; } /** * Add a list of elements for a single page. * * @param aCurPageElements * The list to use. May neither be <code>null</code> nor empty. */ void addPerPageElements(@Nonnull @Nonempty final List<PLElementWithSize> aCurPageElements) { ValueEnforcer.notEmptyNoNullValue(aCurPageElements, "CurPageElements"); m_aPerPageElements.add(aCurPageElements); } @Nonnegative public int getPageCount() { return m_aPerPageElements.size(); } @Nonnegative public int getPageNumber() { return getPageCount() + 1; } @Nonnull @ReturnsMutableObject(reason = "speed") List<List<PLElementWithSize>> directGetPerPageElements() { return m_aPerPageElements; } } private static final Logger s_aLogger = LoggerFactory.getLogger(PLPageSet.class); private final SizeSpec m_aPageSize; private AbstractPLElement<?> m_aPageHeader; private final List<AbstractPLElement<?>> m_aElements = new ArrayList<AbstractPLElement<?>>(); private AbstractPLElement<?> m_aPageFooter; private IRenderingContextCustomizer m_aRCCustomizer; public PLPageSet(@Nonnull final PDRectangle aPageRect) { this(SizeSpec.create(aPageRect)); } public PLPageSet(@Nonnegative final float fWidth, @Nonnegative final float fHeight) { this(new SizeSpec(fWidth, fHeight)); } public PLPageSet(@Nonnull final SizeSpec aPageSize) { m_aPageSize = ValueEnforcer.notNull(aPageSize, "PageSize"); } @Nonnull public SizeSpec getPageSize() { return m_aPageSize; } public float getPageWidth() { return m_aPageSize.getWidth(); } public float getPageHeight() { return m_aPageSize.getHeight(); } @Nullable public IRenderingContextCustomizer getRenderingContextCustomizer() { return m_aRCCustomizer; } @Nonnull public PLPageSet setRenderingContextCustomizer(@Nullable final IRenderingContextCustomizer aRCCustomizer) { m_aRCCustomizer = aRCCustomizer; return this; } /** * @return The usable page width without the x-paddings and x-margins */ @Nonnegative public float getAvailableWidth() { return m_aPageSize.getWidth() - getMarginPlusPaddingXSum(); } /** * @return The usable page height without the y-paddings and y-margins */ @Nonnegative public float getAvailableHeight() { return m_aPageSize.getHeight() - getMarginPlusPaddingYSum(); } /** * @return The global page header. May be <code>null</code>. */ @Nullable public AbstractPLElement<?> getPageHeader() { return m_aPageHeader; } /** * Set the global page header * * @param aPageHeader * The global page header. May be <code>null</code>. * @return this */ @Nonnull public PLPageSet setPageHeader(@Nullable final AbstractPLElement<?> aPageHeader) { m_aPageHeader = aPageHeader; return this; } @Nonnull public List<? extends AbstractPLElement<?>> getAllElements() { return CollectionHelper.newList(m_aElements); } @Nonnull public PLPageSet addElement(@Nonnull final AbstractPLElement<?> aElement) { if (aElement == null) throw new NullPointerException("element"); m_aElements.add(aElement); return this; } /** * @return The global page footer. May be <code>null</code>. */ @Nullable public AbstractPLElement<?> getPageFooter() { return m_aPageFooter; } /** * Set the global page footer * * @param aPageFooter * The global page footer. May be <code>null</code>. * @return this */ @Nonnull public PLPageSet setPageFooter(@Nullable final AbstractPLElement<?> aPageFooter) { m_aPageFooter = aPageFooter; return this; } /** * @return The y-top of the page */ public float getYTop() { return m_aPageSize.getHeight() - getMarginTop() - getPaddingTop(); } @Nonnull public PageSetPrepareResult prepareAllPages() throws IOException { // The result element final PageSetPrepareResult ret = new PageSetPrepareResult(); // Prepare page header if (m_aPageHeader != null) { // Page header does not care about page padding final PreparationContext aRPC = new PreparationContext( m_aPageSize.getWidth() - getMarginXSum() - m_aPageHeader.getMarginPlusPaddingXSum(), getMarginTop() - m_aPageHeader.getMarginPlusPaddingYSum()); final SizeSpec aElementSize = m_aPageHeader.prepare(aRPC); ret.setHeaderHeight(aElementSize.getHeight()); if (aElementSize.getHeight() > getMarginTop()) { // If the height of the header exceeds the available top-margin, modify // the margin so that the header fits! setMarginTop(aElementSize.getHeight() + m_aPageHeader.getMarginPlusPaddingYSum()); } } // Prepare footer if (m_aPageFooter != null) { // Page footer does not care about page padding final PreparationContext aRPC = new PreparationContext( m_aPageSize.getWidth() - getMarginXSum() - m_aPageFooter.getMarginPlusPaddingXSum(), getMarginBottom() - m_aPageFooter.getMarginPlusPaddingYSum()); final SizeSpec aElementSize = m_aPageFooter.prepare(aRPC); ret.setFooterHeight(aElementSize.getHeight()); if (aElementSize.getHeight() > getMarginBottom()) { // If the height of the footer exceeds the available bottom-margin, // modify the margin so that the footer fits! setMarginBottom(aElementSize.getHeight() + m_aPageFooter.getMarginPlusPaddingYSum()); } } if (getMarginYSum() > m_aPageSize.getHeight()) throw new IllegalStateException("Header and footer together (" + getMarginYSum() + ") take more height than available on the page (" + m_aPageSize.getHeight() + ")!"); // Prepare content elements // Must be done after header and footer, because the margins may got // adopted! for (final AbstractPLElement<?> aElement : m_aElements) { final float fAvailableWidth = getAvailableWidth() - aElement.getMarginPlusPaddingXSum(); final float fAvailableHeight = getAvailableHeight() - aElement.getMarginPlusPaddingYSum(); final PreparationContext aRPC = new PreparationContext(fAvailableWidth, fAvailableHeight); final SizeSpec aElementSize = aElement.prepare(aRPC); ret.addElement(new PLElementWithSize(aElement, aElementSize)); } // Split into pieces that fit onto a page final float fYTop = getYTop(); final float fYLeast = getMarginBottom() + getPaddingBottom(); { List<PLElementWithSize> aCurPageElements = new ArrayList<PLElementWithSize>(); if (PLDebug.isDebugPrepare()) PLDebug.debugSplit(this, "Start preparing elements"); // Start at the top float fCurY = fYTop; // Create a copy of the list, so that we can safely modify it final List<PLElementWithSize> aElementsWithSize = ret.getAllElements(); while (!aElementsWithSize.isEmpty()) { // Use the first element PLElementWithSize aElementWithSize = aElementsWithSize.remove(0); final AbstractPLElement<?> aElement = aElementWithSize.getElement(); boolean bIsPagebreakDesired = aElement instanceof PLPageBreak; if (bIsPagebreakDesired && aCurPageElements.isEmpty() && !((PLPageBreak) aElement).isForcePageBreak()) { // a new page was just started and no forced break is present, so no // page break is necessary bIsPagebreakDesired = false; } final float fElementWidth = aElementWithSize.getWidth(); final float fElementHeightFull = aElementWithSize.getHeightFull(); final float fAvailableHeight = fCurY - fYLeast; if (fCurY - fElementHeightFull < fYLeast || bIsPagebreakDesired) { // Element does not fit on page - try to split final boolean bIsSplittable = aElement.isSplittable(); if (bIsSplittable) { if (PLDebug.isDebugSplit()) PLDebug.debugSplit(this, "Trying to split " + aElement.getDebugID() + " into pieces for available width " + fElementWidth + " and height " + fAvailableHeight); // split elements final PLSplitResult aSplitResult = aElement.getAsSplittable().splitElements(fElementWidth, fAvailableHeight - aElement.getMarginPlusPaddingYSum()); if (aSplitResult != null) { // Re-add them to the list and try again (they may be splitted // recursively) aElementsWithSize.add(0, aSplitResult.getFirstElement()); aElementsWithSize.add(1, aSplitResult.getSecondElement()); if (PLDebug.isDebugSplit()) PLDebug.debugSplit(this, "Split " + aElement.getDebugID() + " into pieces: " + aSplitResult.getFirstElement().getElement().getDebugID() + " (" + aSplitResult.getFirstElement().getWidth() + "+" + aSplitResult.getFirstElement().getElement().getMarginPlusPaddingXSum() + " & " + aSplitResult.getFirstElement().getHeight() + "+" + aSplitResult.getFirstElement().getElement().getMarginPlusPaddingYSum() + ") and " + aSplitResult.getSecondElement().getElement().getDebugID() + " (" + aSplitResult.getSecondElement().getWidth() + "+" + aSplitResult.getSecondElement().getElement().getMarginPlusPaddingXSum() + " & " + aSplitResult.getSecondElement().getHeight() + "+" + aSplitResult.getSecondElement().getElement().getMarginPlusPaddingYSum() + ")"); continue; } if (PLDebug.isDebugSplit()) PLDebug.debugSplit(this, "The single element " + aElement.getDebugID() + " does not fit onto a single page (" + fAvailableHeight + ") even though it is splittable!"); } // Next page if (aCurPageElements.isEmpty()) { if (!bIsPagebreakDesired) { // one element too large for a page s_aLogger.warn("The single element " + aElement.getDebugID() + " does not fit onto a single page" + (bIsSplittable ? " even though it is splittable!" : " and is not splittable!")); } } else { // We found elements fitting onto a page (at least one) if (s_aLogger.isDebugEnabled()) s_aLogger.debug("Adding " + aCurPageElements.size() + " elements to page " + ret.getPageNumber()); if (PLDebug.isDebugPrepare()) { final List<String> aLastPageContent = new ArrayList<String>(); for (final PLElementWithSize aCurElement : aCurPageElements) aLastPageContent.add(aCurElement.getElement().getDebugID()); PLDebug.debugPrepare(this, "Finished page with: " + StringHelper.getImploded(aLastPageContent)); } ret.addPerPageElements(aCurPageElements); aCurPageElements = new ArrayList<PLElementWithSize>(); // Start new page fCurY = fYTop; // Re-add element and continue from start, so that splitting happens aElementsWithSize.add(0, aElementWithSize); // Continue with next element continue; } } // Handle vertical alignment of top-level elements if (aElement instanceof IPLHasVerticalAlignment<?>) { final EVertAlignment eVertAlignment = ((IPLHasVerticalAlignment<?>) aElement).getVertAlign(); float fPaddingTop; switch (eVertAlignment) { case TOP: fPaddingTop = 0f; break; case MIDDLE: fPaddingTop = (fAvailableHeight - fElementHeightFull) / 2; break; case BOTTOM: fPaddingTop = fAvailableHeight - fElementHeightFull; break; default: throw new IllegalStateException("Unsupported vertical alignment: " + eVertAlignment); } if (fPaddingTop != 0f) { final SizeSpec aOldSize = aElement.getPreparedSize(); aElement.markAsNotPrepared(); aElement.setPaddingTop(aElement.getPaddingTop() + fPaddingTop); final SizeSpec aNewSize = new SizeSpec(aOldSize.getWidth(), aOldSize.getHeight() + fPaddingTop); aElement.markAsPrepared(aNewSize); aElementWithSize = new PLElementWithSize(aElement, aNewSize); } } // Add element to current page (may also be a page break) aCurPageElements.add(aElementWithSize); fCurY -= fElementHeightFull; } // Add elements to last page if (!aCurPageElements.isEmpty()) { if (s_aLogger.isDebugEnabled()) s_aLogger.debug("Finally adding " + aCurPageElements.size() + " elements to page " + ret.getPageNumber()); ret.addPerPageElements(aCurPageElements); if (PLDebug.isDebugPrepare()) { final List<String> aLastPageContent = new ArrayList<String>(); for (final PLElementWithSize aCurElement : aCurPageElements) aLastPageContent.add(aCurElement.getElement().getDebugID()); PLDebug.debugPrepare(this, "Finished last page with: " + StringHelper.getImploded(aLastPageContent)); } } } return ret; } /** * Render all pages of this layout to the specified PDDocument * * @param aPrepareResult * The preparation result. May not be <code>null</code>. * @param aDoc * The PDDocument. May not be <code>null</code>. * @param bDebug * <code>true</code> for debug output * @param nPageSetIndex * Page set index. Always ≥ 0. * @param nTotalPageStartIndex * Total page index. Always ≥ 0. * @param nTotalPageCount * Total page count. Always ≥ 0. * @throws IOException * In case of render errors */ public void renderAllPages(@Nonnull final PageSetPrepareResult aPrepareResult, @Nonnull final PDDocument aDoc, final boolean bDebug, @Nonnegative final int nPageSetIndex, @Nonnegative final int nTotalPageStartIndex, @Nonnegative final int nTotalPageCount) throws IOException { // Start at the left final float fXLeft = getMarginLeft() + getPaddingLeft(); final float fYTop = getYTop(); final boolean bCompressPDF = !bDebug; int nPageIndex = 0; final int nPageCount = aPrepareResult.getPageCount(); for (final List<PLElementWithSize> aPerPage : aPrepareResult.directGetPerPageElements()) { if (PLDebug.isDebugRender()) PLDebug.debugRender(this, "Start rendering page index " + nPageIndex + " (" + (nTotalPageStartIndex + nPageIndex) + ") with page size " + getPageWidth() + " & " + getPageHeight() + " and available size " + getAvailableWidth() + " & " + getAvailableHeight()); final RenderPageIndex aPageIndex = new RenderPageIndex(nPageSetIndex, nPageIndex, nPageCount, nTotalPageStartIndex + nPageIndex, nTotalPageCount); // Layout in memory final PDPage aPage = new PDPage(m_aPageSize.getAsRectangle()); aDoc.addPage(aPage); { final PageSetupContext aCtx = new PageSetupContext(aDoc, aPage); if (m_aPageHeader != null) m_aPageHeader.doPageSetup(aCtx); for (final PLElementWithSize aElement : aPerPage) aElement.getElement().doPageSetup(aCtx); if (m_aPageFooter != null) m_aPageFooter.doPageSetup(aCtx); } final PDPageContentStreamWithCache aContentStream = new PDPageContentStreamWithCache(aDoc, aPage, false, bCompressPDF); try { // Page rect before content - debug: red { final float fLeft = getMarginLeft(); final float fTop = m_aPageSize.getHeight() - getMarginTop(); final float fWidth = m_aPageSize.getWidth() - getMarginXSum(); final float fHeight = m_aPageSize.getHeight() - getMarginYSum(); // Fill before border if (getFillColor() != null) { aContentStream.setNonStrokingColor(getFillColor()); aContentStream.fillRect(fLeft, fTop - fHeight, fWidth, fHeight); } BorderSpec aRealBorder = getBorder(); if (shouldApplyDebugBorder(aRealBorder, bDebug)) aRealBorder = new BorderSpec(new BorderStyleSpec(PLDebug.BORDER_COLOR_PAGESET)); if (aRealBorder.hasAnyBorder()) renderBorder(aContentStream, fLeft, fTop, fWidth, fHeight, aRealBorder); } // Start with the page rect if (m_aPageHeader != null) { // Page header does not care about page padding // header top-left final RenderingContext aRC = new RenderingContext(ERenderingElementType.PAGE_HEADER, aContentStream, bDebug, getMarginLeft() + m_aPageHeader.getMarginLeft(), m_aPageSize.getHeight() - m_aPageHeader.getMarginTop(), m_aPageSize.getWidth() - getMarginXSum() - m_aPageHeader.getMarginXSum(), aPrepareResult.getHeaderHeight() + m_aPageHeader.getPaddingYSum()); aPageIndex.setPlaceholdersInRenderingContext(aRC); if (m_aRCCustomizer != null) m_aRCCustomizer.customizeRenderingContext(aRC); m_aPageHeader.perform(aRC); } float fCurY = fYTop; for (final PLElementWithSize aElementWithHeight : aPerPage) { final AbstractPLElement<?> aElement = aElementWithHeight.getElement(); // Get element height final float fThisHeight = aElementWithHeight.getHeight(); final float fThisHeightWithPadding = fThisHeight + aElement.getPaddingYSum(); final RenderingContext aRC = new RenderingContext(ERenderingElementType.CONTENT_ELEMENT, aContentStream, bDebug, fXLeft + aElement.getMarginLeft(), fCurY - aElement.getMarginTop(), getAvailableWidth() - aElement.getMarginXSum(), fThisHeightWithPadding); aPageIndex.setPlaceholdersInRenderingContext(aRC); if (m_aRCCustomizer != null) m_aRCCustomizer.customizeRenderingContext(aRC); aElement.perform(aRC); fCurY -= fThisHeightWithPadding + aElement.getMarginYSum(); } if (m_aPageFooter != null) { // Page footer does not care about page padding // footer top-left final float fStartLeft = getMarginLeft() + m_aPageFooter.getMarginLeft(); final float fStartTop = getMarginBottom() - m_aPageFooter.getMarginTop(); final float fWidth = m_aPageSize.getWidth() - getMarginXSum() - m_aPageFooter.getMarginXSum(); final float fHeight = aPrepareResult.getFooterHeight() + m_aPageFooter.getPaddingYSum(); final RenderingContext aRC = new RenderingContext(ERenderingElementType.PAGE_FOOTER, aContentStream, bDebug, fStartLeft, fStartTop, fWidth, fHeight); aPageIndex.setPlaceholdersInRenderingContext(aRC); if (m_aRCCustomizer != null) m_aRCCustomizer.customizeRenderingContext(aRC); m_aPageFooter.perform(aRC); } } finally { aContentStream.close(); } ++nPageIndex; } if (PLDebug.isDebugRender()) PLDebug.debugRender(this, "Finished rendering"); } @Override public String toString() { return ToStringGenerator.getDerived(super.toString()).append("pageSize", m_aPageSize) .appendIfNotNull("pageHeader", m_aPageHeader).append("elements", m_aElements) .appendIfNotNull("pageFooter", m_aPageFooter).appendIfNotNull("RCCustomizer", m_aRCCustomizer) .toString(); } }