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

com.itextpdf.html2pdf.attach.impl.layout.HtmlDocumentRenderer Maven / Gradle / Ivy

Go to download

pdfHTML is an iText add-on that lets you to parse (X)HTML snippets and the associated CSS and converts them to PDF.

There is a newer version: 5.0.5
Show newest version
/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2021 iText Group NV
    Authors: Bruno Lowagie, Paulo Soares, et al.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License version 3
    as published by the Free Software Foundation with the addition of the
    following permission added to Section 15 as permitted in Section 7(a):
    FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
    ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
    OF THIRD PARTY RIGHTS

    This program is distributed in the hope that it will be useful, but
    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    or FITNESS FOR A PARTICULAR PURPOSE.
    See the GNU Affero General Public License for more details.
    You should have received a copy of the GNU Affero General Public License
    along with this program; if not, see http://www.gnu.org/licenses or write to
    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
    Boston, MA, 02110-1301 USA, or download the license from the following URL:
    http://itextpdf.com/terms-of-use/

    The interactive user interfaces in modified source and object code versions
    of this program must display Appropriate Legal Notices, as required under
    Section 5 of the GNU Affero General Public License.

    In accordance with Section 7(b) of the GNU Affero General Public License,
    a covered work must retain the producer line in every PDF that is created
    or manipulated using iText.

    You can be released from the requirements of the license by purchasing
    a commercial license. Buying such a license is mandatory as soon as you
    develop commercial activities involving the iText software without
    disclosing the source code of your own applications.
    These activities include: offering paid services to customers as an ASP,
    serving PDFs on the fly in a web application, shipping iText with a closed
    source product.

    For more information, please contact iText Software Corp. at this
    address: [email protected]
 */
package com.itextpdf.html2pdf.attach.impl.layout;

import com.itextpdf.html2pdf.attach.ProcessorContext;
import com.itextpdf.html2pdf.attach.impl.layout.HtmlBodyStylesApplierHandler.LowestAndHighest;
import com.itextpdf.html2pdf.attach.impl.layout.HtmlBodyStylesApplierHandler.PageStylesProperties;
import com.itextpdf.kernel.events.Event;
import com.itextpdf.kernel.events.IEventHandler;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.IPropertyContainer;
import com.itextpdf.layout.element.AreaBreak;
import com.itextpdf.layout.layout.LayoutArea;
import com.itextpdf.layout.layout.LayoutPosition;
import com.itextpdf.layout.layout.LayoutResult;
import com.itextpdf.layout.properties.FloatPropertyValue;
import com.itextpdf.layout.properties.Property;
import com.itextpdf.layout.renderer.DocumentRenderer;
import com.itextpdf.layout.renderer.IRenderer;
import com.itextpdf.layout.renderer.ParagraphRenderer;
import com.itextpdf.layout.renderer.TargetCounterHandler;
import com.itextpdf.styledxmlparser.css.ICssResolver;
import com.itextpdf.styledxmlparser.css.page.PageContextConstants;
import com.itextpdf.styledxmlparser.node.INode;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The DocumentRenderer class for HTML.
 */
public class HtmlDocumentRenderer extends DocumentRenderer {

    /**
     * The Constant TRIM_LAST_BLANK_PAGE.
     * In a future version, we might want to expose this value to the users,
     * or make it a public setting of the HTML renderer.
     */
    private static final boolean TRIM_LAST_BLANK_PAGE = true;

    /** The page context processor for the first page. */
    private PageContextProcessor firstPageProc;

    /** The page context processor for all left pages. */
    private PageContextProcessor leftPageProc;

    /** The page context processor for all right pages. */
    private PageContextProcessor rightPageProc;

    /**
     * Indicates if even pages are considered as left or right.
     * Important: this value might differ depending on page progression direction,
     * as well as because the first page break-before might change right page to left (in ltr cases),
     * but a blank page will not be added.
     */
    private boolean evenPagesAreLeft = true;

    private PageMarginBoxesDrawingHandler marginBoxesHandler;
    private HtmlBodyStylesApplierHandler htmlBodyHandler;

    private Map pageStylesPropertiesMap = new HashMap<>();

    /**
     * The waiting element, an child element is kept waiting for the
     * next element to process the "keep with previous" property.
     */
    private IRenderer waitingElement;

    /**
     * Indicates if the first blank pages caused by a break-before-first
     * element should be trimmed.
     */
    private boolean shouldTrimFirstBlankPagesCausedByBreakBeforeFirstElement = true;

    /** Indicates if anything was added to the current area. */
    private boolean anythingAddedToCurrentArea = false;

    /** The estimated number of pages. */
    private int estimatedNumberOfPages;

    /**
     * Instantiates a new {@link HtmlDocumentRenderer} instance.
     *
     * @param document       an iText {@link Document} instance
     * @param immediateFlush the immediate flush indicator
     */
    public HtmlDocumentRenderer(Document document, boolean immediateFlush) {
        super(document, immediateFlush);
        htmlBodyHandler = new HtmlBodyStylesApplierHandler(this, pageStylesPropertiesMap);
        document.getPdfDocument().addEventHandler(PdfDocumentEvent.END_PAGE, htmlBodyHandler);
    }

    /**
     * Processes the page rules.
     *
     * @param rootNode    the root node
     * @param cssResolver the CSS resolver
     * @param context     the processor context
     */
    public void processPageRules(INode rootNode, ICssResolver cssResolver, ProcessorContext context) {
        PageContextProperties firstPageProps = PageContextProperties.resolve(rootNode, cssResolver, context.getCssContext(),
                PageContextConstants.FIRST, PageContextConstants.RIGHT);
        // TODO DEVSIX-4118 in documents with set to rtl on root document, first page is considered as left
        PageContextProperties leftPageProps = PageContextProperties.resolve(rootNode, cssResolver, context.getCssContext(),
                PageContextConstants.LEFT);
        PageContextProperties rightPageProps = PageContextProperties.resolve(rootNode, cssResolver, context.getCssContext(),
                PageContextConstants.RIGHT);

        PageSize defaultPageSize = document.getPdfDocument().getDefaultPageSize();
        float[] defaultPageMargins = {document.getTopMargin(), document.getRightMargin(), document.getBottomMargin(), document.getRightMargin()};
        firstPageProc = new PageContextProcessor(firstPageProps, context, defaultPageSize, defaultPageMargins);
        leftPageProc = new PageContextProcessor(leftPageProps, context, defaultPageSize, defaultPageMargins);
        rightPageProc = new PageContextProcessor(rightPageProps, context, defaultPageSize, defaultPageMargins);

        marginBoxesHandler = new HtmlDocumentRenderer.PageMarginBoxesDrawingHandler().setHtmlDocumentRenderer(this);
        document.getPdfDocument().addEventHandler(PdfDocumentEvent.END_PAGE, marginBoxesHandler);
    }

    /* (non-Javadoc)
     * @see com.itextpdf.layout.renderer.RootRenderer#addChild(com.itextpdf.layout.renderer.IRenderer)
     */
    @Override
    public void addChild(IRenderer renderer) {
        if (waitingElement != null) {
            if (Boolean.TRUE.equals(renderer.getProperty(Html2PdfProperty.KEEP_WITH_PREVIOUS))) {
                waitingElement.setProperty(Property.KEEP_WITH_NEXT, true);
            }
            IRenderer element = waitingElement;
            waitingElement = null;
            super.addChild(element);
            if (!isRunningElementsOnly(element)) {
                // After we have added any child, we should not trim first pages because of break before element, even if the added child had zero height
                shouldTrimFirstBlankPagesCausedByBreakBeforeFirstElement = false;
            }
        }
        waitingElement = renderer;

        FloatPropertyValue floatPropertyValue = renderer.getProperty(Property.FLOAT);
        Integer position = renderer.getProperty(Property.POSITION);
        if ((position != null && position == LayoutPosition.ABSOLUTE) || (floatPropertyValue != null && !floatPropertyValue.equals(FloatPropertyValue.NONE))) {
            waitingElement = null;
            super.addChild(renderer);
        }
    }

    /* (non-Javadoc)
     * @see com.itextpdf.layout.renderer.DocumentRenderer#close()
     */
    @Override
    public void close() {
        processWaitingElement();
        super.close();
        trimLastPageIfNecessary();
        document.getPdfDocument().removeEventHandler(PdfDocumentEvent.END_PAGE, marginBoxesHandler);
        document.getPdfDocument().removeEventHandler(PdfDocumentEvent.END_PAGE, htmlBodyHandler);
        for (int i = 1; i <= document.getPdfDocument().getNumberOfPages(); ++i) {
            PdfPage page = document.getPdfDocument().getPage(i);
            if (!page.isFlushed()) {
                marginBoxesHandler.processPage(document.getPdfDocument(), i);
                htmlBodyHandler.processPage(page, i);
            }
        }
    }

    /**
     * Removes event handlers that were added to pdf document when this {@link HtmlDocumentRenderer} was created.
     */
    void removeEventHandlers() {
        document.getPdfDocument().removeEventHandler(PdfDocumentEvent.END_PAGE, htmlBodyHandler);
        // This handler is added in processPageRules method.
        document.getPdfDocument().removeEventHandler(PdfDocumentEvent.END_PAGE, marginBoxesHandler);
    }

    /* (non-Javadoc)
     * @see com.itextpdf.layout.renderer.DocumentRenderer#getNextRenderer()
     */
    @Override
    public IRenderer getNextRenderer() {
        // Process waiting element to get the correct number of pages
        processWaitingElement();
        HtmlDocumentRenderer relayoutRenderer = new HtmlDocumentRenderer(document, immediateFlush);
        PageSize defaultPageSize = document.getPdfDocument().getDefaultPageSize();
        float[] defaultPageMargins = {document.getTopMargin(), document.getRightMargin(), document.getBottomMargin(), document.getRightMargin()};
        relayoutRenderer.firstPageProc = firstPageProc.reset(defaultPageSize, defaultPageMargins);
        relayoutRenderer.leftPageProc = leftPageProc.reset(defaultPageSize, defaultPageMargins);
        relayoutRenderer.rightPageProc = rightPageProc.reset(defaultPageSize, defaultPageMargins);
        relayoutRenderer.estimatedNumberOfPages = currentArea == null ? estimatedNumberOfPages :
                currentArea.getPageNumber() - simulateTrimLastPage();
        relayoutRenderer.marginBoxesHandler = marginBoxesHandler.setHtmlDocumentRenderer(relayoutRenderer);
        relayoutRenderer.targetCounterHandler = new TargetCounterHandler(targetCounterHandler);
        return relayoutRenderer;
    }

    @Override
    public void flush(){
        processWaitingElement();
        super.flush();
    }

    /**
     * Layouts waiting element.
     */
    public void processWaitingElement(){
        if (waitingElement != null) {
            IRenderer r = this.waitingElement;
            waitingElement = null;
            super.addChild(r);
        }
    }

    /* (non-Javadoc)
     * @see com.itextpdf.layout.renderer.DocumentRenderer#updateCurrentArea(com.itextpdf.layout.layout.LayoutResult)
     */
    @Override
    protected LayoutArea updateCurrentArea(LayoutResult overflowResult) {
        AreaBreak areaBreak = overflowResult != null ? overflowResult.getAreaBreak() : null;
        if (areaBreak instanceof HtmlPageBreak) {
            HtmlPageBreakType htmlPageBreakType = ((HtmlPageBreak) areaBreak).getBreakType();
            if (shouldTrimFirstBlankPagesCausedByBreakBeforeFirstElement && currentArea != null &&
                    overflowResult.getStatus() == LayoutResult.NOTHING && currentArea.isEmptyArea() && currentArea.getPageNumber() == 1) {
                // Remove blank page that was added just to have area for elements to layout on.
                // Now we will add a page with dimensions and all the stuff that is requested by page-break-before
                document.getPdfDocument().removePage(1);
                overflowResult = null;
                currentArea = null;
                shouldTrimFirstBlankPagesCausedByBreakBeforeFirstElement = false;

                if (HtmlPageBreakType.LEFT.equals(htmlPageBreakType) && !isPageLeft(1) || HtmlPageBreakType.RIGHT.equals(htmlPageBreakType) && !isPageRight(1)) {
                    evenPagesAreLeft = !evenPagesAreLeft; // hack to change the "evenness" of the first page without adding an unnecessary blank page
                }
            }

            anythingAddedToCurrentArea = anythingAddedToCurrentArea || overflowResult != null && overflowResult.getStatus() == LayoutResult.PARTIAL;

            if (HtmlPageBreakType.ALWAYS.equals(htmlPageBreakType)) {
                LayoutArea nextArea = currentArea;
                if (anythingAddedToCurrentArea || currentArea == null) {
                    nextArea = super.updateCurrentArea(overflowResult);
                }
                anythingAddedToCurrentArea = false;
                return nextArea;
            } else if (HtmlPageBreakType.LEFT.equals(htmlPageBreakType)) {
                LayoutArea nextArea = currentArea;
                if (anythingAddedToCurrentArea || currentArea == null || !isPageLeft(currentArea.getPageNumber())) {
                    do {
                        nextArea = super.updateCurrentArea(overflowResult);
                    } while (!isPageLeft(currentArea.getPageNumber()));
                }
                anythingAddedToCurrentArea = false;
                return nextArea;
            } else if (HtmlPageBreakType.RIGHT.equals(htmlPageBreakType)) {
                LayoutArea nextArea = currentArea;
                if (anythingAddedToCurrentArea || currentArea == null || !isPageRight(currentArea.getPageNumber())) {
                    do {
                        nextArea = super.updateCurrentArea(overflowResult);
                    } while (!isPageRight(currentArea.getPageNumber()));
                }
                anythingAddedToCurrentArea = false;
                return nextArea;
            }
        }
        anythingAddedToCurrentArea = false;

        return super.updateCurrentArea(overflowResult);
    }

    /* (non-Javadoc)
     * @see com.itextpdf.layout.renderer.RootRenderer#shrinkCurrentAreaAndProcessRenderer(com.itextpdf.layout.renderer.IRenderer, java.util.List, com.itextpdf.layout.layout.LayoutResult)
     */
    @Override
    protected void shrinkCurrentAreaAndProcessRenderer(IRenderer renderer, List resultRenderers, LayoutResult result) {
        if (renderer != null) {
            anythingAddedToCurrentArea = true;
        }
        super.shrinkCurrentAreaAndProcessRenderer(renderer, resultRenderers, result);
    }

    @Override
    protected void flushSingleRenderer(IRenderer resultRenderer) {
        if (!isElementOnNonStaticLayout(resultRenderer)) {
            LayoutArea area = resultRenderer.getOccupiedArea();
            updateLowestAndHighestPoints(area.getBBox(), area.getPageNumber());
        }
        super.flushSingleRenderer(resultRenderer);
    }

    /* (non-Javadoc)
     * @see com.itextpdf.layout.renderer.DocumentRenderer#addNewPage(com.itextpdf.kernel.geom.PageSize)
     */
    @Override
    protected PageSize addNewPage(PageSize customPageSize) {
        PdfPage addedPage;

        int pageNumber = document.getPdfDocument().getNumberOfPages() + 1;
        PageContextProcessor nextProcessor = getPageProcessor(pageNumber);
        if (customPageSize != null) {
            addedPage = document.getPdfDocument().addNewPage(customPageSize);
        } else {
            addedPage = document.getPdfDocument().addNewPage(nextProcessor.getPageSize());
        }

        nextProcessor.processNewPage(addedPage);

        float[] margins = nextProcessor.computeLayoutMargins();
        BodyHtmlStylesContainer[] styles = new BodyHtmlStylesContainer[] {
                ((IPropertyContainer) document).getProperty(Html2PdfProperty.HTML_STYLING),
                ((IPropertyContainer) document).getProperty(Html2PdfProperty.BODY_STYLING)};
        pageStylesPropertiesMap.put(pageNumber, new PageStylesProperties(styles));
        updateDefaultMargins(styles, margins);

        setProperty(Property.MARGIN_TOP, margins[0]);
        setProperty(Property.MARGIN_RIGHT, margins[1]);
        setProperty(Property.MARGIN_BOTTOM, margins[2]);
        setProperty(Property.MARGIN_LEFT, margins[3]);

        return new PageSize(addedPage.getTrimBox());
    }

    boolean shouldAttemptTrimLastPage() {
        return TRIM_LAST_BLANK_PAGE && document.getPdfDocument().getNumberOfPages() > 1;
    }

    void trimLastPageIfNecessary() {
        if (shouldAttemptTrimLastPage()) {
            PdfDocument pdfDocument = document.getPdfDocument();
            PdfPage lastPage = pdfDocument.getLastPage();
            if (lastPage.getContentStreamCount() == 1 && lastPage.getContentStream(0).getOutputStream().getCurrentPos() <= 0) {
                // Remove last empty page
                pdfDocument.removePage(pdfDocument.getNumberOfPages());
            }
        }
    }

    /**
     * Returns the number of pages that will be trimmed on {@link #close()}
     * @return 0 if no pages will be trimmed, or positive number of trimmed pages in case any are trimmed
     */
    int simulateTrimLastPage() {
        if (shouldAttemptTrimLastPage()) {
            int lastPageNumber = document.getPdfDocument().getNumberOfPages();
            // At the moment we only check if some element was positioned on this page
            // However, there might theoretically be an inconsistency with the method that
            // actually does the trimming because that method checks the canvas output only.
            // We might want to simulate drawing on canvas here in the future, or possibly
            // consider invisible elements in the method that actually does the trimming
            boolean willAnyContentBeDrawnOnLastPage = false;
            for (IRenderer renderer : childRenderers) {
                if (renderer.getOccupiedArea().getPageNumber() == lastPageNumber) {
                    willAnyContentBeDrawnOnLastPage = true;
                }
            }
            for (IRenderer renderer : positionedRenderers) {
                if (renderer.getOccupiedArea().getPageNumber() == lastPageNumber) {
                    willAnyContentBeDrawnOnLastPage = true;
                }
            }
            return willAnyContentBeDrawnOnLastPage ? 0 : 1;
        } else {
            return 0;
        }
    }

    /**
     * Gets a page processor for the page.
     *
     * @param pageNum the number of the page for which the {@link PageContextProcessor} shall be obtained
     * @return a page processor
     */
    PageContextProcessor getPageProcessor(int pageNum) {
        // If first page, but break-before: left for ltr is present, we should use left page instead of first
        if (pageNum == 1 && evenPagesAreLeft) {
            return firstPageProc;
        } else if (isPageLeft(pageNum)) {
            return leftPageProc;
        } else {
            return rightPageProc;
        }
    }

    /**
     * Gets the estimated number of pages.
     *
     * @return the estimated number of pages
     */
    int getEstimatedNumberOfPages() {
        return estimatedNumberOfPages;
    }

    private void updateDefaultMargins(BodyHtmlStylesContainer[] styles, float[] defaultMargins) {
        for (int i = 0; i < 2; i++) {
            if (styles[i] != null) {
                for (int j = 0; j < 4; j++) {
                    defaultMargins[j] += styles[i].getTotalWidth()[j];
                }
            }
        }
    }

    private boolean isElementOnNonStaticLayout(IRenderer resultRenderer) {
        boolean nonStaticLayout = false;
        if (resultRenderer.hasProperty(Property.POSITION)) {
            int positionProperty = (int) resultRenderer.getProperty(Property.POSITION);
            nonStaticLayout = positionProperty == LayoutPosition.ABSOLUTE || positionProperty == LayoutPosition.FIXED;
        }
        if (!nonStaticLayout && resultRenderer.hasProperty(Property.FLOAT)) {
            FloatPropertyValue floatProperty = resultRenderer.getProperty(Property.FLOAT);
            nonStaticLayout = floatProperty == FloatPropertyValue.LEFT || floatProperty == FloatPropertyValue.RIGHT;
        }
        return nonStaticLayout;
    }

    private void updateLowestAndHighestPoints(Rectangle rectangle, int page) {
        if (!pageStylesPropertiesMap.containsKey(page)) {
            return;
        }
        LowestAndHighest currentPagePoints = pageStylesPropertiesMap.get(page).lowestAndHighest;
        if (currentPagePoints == null) {
            pageStylesPropertiesMap.get(page).lowestAndHighest
                    = new LowestAndHighest(rectangle.getY(), rectangle.getY() + rectangle.getHeight());
        } else {
            float newLowestPoint = rectangle.getY();
            float newHighestPoint = rectangle.getY() + rectangle.getHeight();
            currentPagePoints.lowest = Math.min(newLowestPoint, currentPagePoints.lowest);
            currentPagePoints.highest = Math.max(newHighestPoint, currentPagePoints.highest);
        }
    }

    private static boolean isRunningElementsOnly(IRenderer waitingElement) {
        boolean res;
        if (res = waitingElement instanceof ParagraphRenderer && !waitingElement.getChildRenderers().isEmpty()) {
            List childRenderers = waitingElement.getChildRenderers();
            int i = 0;
            while (res && i < childRenderers.size()) {
                res = childRenderers.get(i++) instanceof RunningElement.RunningElementRenderer;
            }
        }
        return res;
    }

    /**
     * Checks if the current page is a left page.
     *
     * @return true, if is current page left
     */
    private boolean isPageLeft(int pageNum) {
        // TODO DEVSIX-4118 rtl
        boolean pageIsEven = pageNum % 2 == 0;
        return evenPagesAreLeft == pageIsEven;
    }

    /**
     * Checks if the current page is a right page.
     *
     * @return true, if is current page right
     */
    private boolean isPageRight(int pageNum) {
        return !isPageLeft(pageNum);
    }

    private static class PageMarginBoxesDrawingHandler implements IEventHandler {
        private HtmlDocumentRenderer htmlDocumentRenderer;

        PageMarginBoxesDrawingHandler setHtmlDocumentRenderer(HtmlDocumentRenderer htmlDocumentRenderer) {
            this.htmlDocumentRenderer = htmlDocumentRenderer;
            return this;
        }

        @Override
        public void handleEvent(Event event) {
            if (event instanceof PdfDocumentEvent) {
                PdfPage page = ((PdfDocumentEvent) event).getPage();
                PdfDocument pdfDoc = ((PdfDocumentEvent) event).getDocument();
                int pageNumber = pdfDoc.getPageNumber(page);
                processPage(pdfDoc, pageNumber);
            }
        }

        void processPage(PdfDocument pdfDoc, int pageNumber) {
            PageContextProcessor pageProcessor = htmlDocumentRenderer.getPageProcessor(pageNumber);
            pageProcessor.processPageEnd(pageNumber, pdfDoc, htmlDocumentRenderer);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy