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

docet.engine.PDFDocumentHandler Maven / Gradle / Ivy

There is a newer version: 1.20.0
Show newest version
/*
 * Licensed to Diennea S.r.l. under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. Diennea S.r.l. licenses this file
 * to you 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 docet.engine;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Shape;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import org.jsoup.Jsoup;
import org.jsoup.helper.W3CDom;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.parser.Parser;
import org.jsoup.parser.Tag;
import org.jsoup.select.Elements;
import org.xhtmlrenderer.context.StyleReference;
import org.xhtmlrenderer.css.style.CalculatedStyle;
import org.xhtmlrenderer.extend.NamespaceHandler;
import org.xhtmlrenderer.extend.UserInterface;
import org.xhtmlrenderer.layout.BoxBuilder;
import org.xhtmlrenderer.layout.Layer;
import org.xhtmlrenderer.layout.LayoutContext;
import org.xhtmlrenderer.layout.SharedContext;
import org.xhtmlrenderer.pdf.ITextFontContext;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextOutputDevice;
import org.xhtmlrenderer.pdf.ITextReplacedElementFactory;
import org.xhtmlrenderer.pdf.ITextTextRenderer;
import org.xhtmlrenderer.pdf.ITextUserAgent;
import org.xhtmlrenderer.render.BlockBox;
import org.xhtmlrenderer.render.PageBox;
import org.xhtmlrenderer.render.RenderingContext;
import org.xhtmlrenderer.render.ViewportBox;
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
import org.xhtmlrenderer.util.Configuration;

import com.lowagie.text.DocumentException;
import com.lowagie.text.pdf.PdfDestination;
import com.lowagie.text.pdf.PdfOutline;
import com.lowagie.text.pdf.PdfWriter;

import docet.DocetDocumentPlaceholder;
import docet.DocetDocumentResourcesAccessor;
import docet.DocetLanguage;
import docet.SimpleDocetDocumentAccessor;
import docet.error.DocetDocumentParsingException;
import docet.model.DocetDocument;

/**
 * Generates a PDF from a {@link DocetDocument} using xhtmlrenderer
 *
 * @author diego.salvi
 */
public class PDFDocumentHandler {

    private static final Logger LOGGER = Logger.getLogger(PDFDocumentHandler.class.getName());

    private static final DocetDocumentResourcesAccessor DEFAULT_ACCESSOR = new SimpleDocetDocumentAccessor();

    private static final boolean DEFAULT_DEBUG = false;
    private static final boolean DEFAULT_COVER = true;
    private static final boolean DEFAULT_TOC = true;
    private static final boolean DEFAULT_BOOKMARKS = true;

    private static final String DEFAULT_TITLE = "";

    public static final DocetLanguage DEFAULT_LANGUAGE = DocetLanguage.EN;

    /* These two defaults combine to produce an effective resolution of 96 px to the inch */
    public static final float DEFAULT_DOTS_PER_POINT = 20f * 4f / 3f;
    public static final int DEFAULT_DOTS_PER_PIXEL = 20;

    public static final String DEFAULT_BASE_URL = "";

    public static final String DEFAULT_CSS = "docet/pdf/docetpdf.css";
    public static final String DEFAULT_COVER_IMAGE = "";
    public static final String DEFAULT_FOOTER_COVER = "Powered by https://docetproject.org/";

    private static final W3CDom W3CDOM = new W3CDom();

    public static final Parser HTML_PARSER = Parser.htmlParser();

    /** Patterns for replace placeholders */
    private static final Map REPLACEMENT_PATTERNS;
    static {
        REPLACEMENT_PATTERNS = new EnumMap<>(HTMLPlaceholder.class);

        for(HTMLPlaceholder placeholder : HTMLPlaceholder.values()) {
            REPLACEMENT_PATTERNS.put(placeholder, Pattern.compile("${" + placeholder + "}", Pattern.LITERAL));
        }
    }

    private final boolean renderCover;
    private final boolean renderToc;
    private final boolean renderBookmarks;

    private final float dotsPerPoint;
    private final int dotsPerPixel;

    private final String baseURL;
    private final NamespaceHandler namespaceHandler;

    private String css;

    private final String title;

    private final DocetDocumentResourcesAccessor accessor;
    private final DocetLanguage language;

    /* Declared as LinkedHashMap, we need the order preserving feature! */
    private final LinkedHashMap documents = new LinkedHashMap<>();

    private final SharedContext sharedContext;
    private final ITextOutputDevice outputDevice;

    private final Document cover;
    private final Element pageHead;
    private final Element pageHeadBody;

    private final Map placeholders;

    public static interface Builder {

        public Builder debug(boolean debug);

        public Builder title(String title);

        public Builder cover(boolean cover);

        public Builder toc(boolean toc);

        public Builder bookmarks(boolean bookmarks);

        public Builder placeholders(DocetDocumentResourcesAccessor accessor);

        public Builder language(DocetLanguage language);

        public Builder dotsPerPoint(float dotsPerPoint);

        public Builder dotsPerPixel(int dotsPerPixel);

        public Builder baseURL(String url);

        public Builder namespaceHandler(NamespaceHandler namespaceHandler);

        public PDFDocumentHandler create() throws DocetDocumentParsingException;

    }

    public static final Builder builder() {
        return new BuilderImpl();
    }

    private PDFDocumentHandler(
            boolean debug,
            boolean renderCover,
            boolean renderToc,
            boolean renderBookmarks,
            float dotsPerPoint,
            int dotsPerPixel,
            String baseURL,
            NamespaceHandler namespaceHandler,
            String title,
            DocetDocumentResourcesAccessor accessor,
            DocetLanguage language)
                    throws DocetDocumentParsingException {

        this.renderCover = renderCover;
        this.renderToc = renderToc;
        this.renderBookmarks = renderBookmarks;

        this.dotsPerPoint = dotsPerPoint;
        this.dotsPerPixel = dotsPerPixel;

        this.baseURL = baseURL;
        this.namespaceHandler = namespaceHandler;

        this.title = title;

        this.accessor = accessor;
        this.language = language;

        this.css = evaluateAccessorConfiguration(DocetDocumentPlaceholder.PDF_CSS, DEFAULT_CSS);

        /* ************************* */
        /* *** DATA PLACEHOLDERS *** */
        /* ************************* */

        placeholders = calculatePlaceholders();


        /* *********************** */
        /* *** HEADER & FOOTER *** */
        /* *********************** */

        pageHead     = calculatePageHead();
        pageHeadBody = calculatePageHeadBody();


        /* ************* */
        /* *** COVER *** */
        /* ************* */

        if (renderCover) {
            cover = calculateCover();
        } else {
            cover = null;
        }

        /* ******************************** */
        /* *** OUTPUTDEVICE AND CONTEXT *** */
        /* ******************************** */

        outputDevice = new ITextOutputDevice(dotsPerPoint);
        ClassloaderAwareUserAgent userAgent = new ClassloaderAwareUserAgent(outputDevice);

        sharedContext = new SharedContext();
        sharedContext.setUserAgentCallback(userAgent);
        sharedContext.setCss(new StyleReference(userAgent));

        userAgent.setSharedContext(sharedContext);
        outputDevice.setSharedContext(sharedContext);

        ITextFontResolver fontResolver = new ITextFontResolver(sharedContext);
        sharedContext.setFontResolver(fontResolver);

        ITextReplacedElementFactory replacedElementFactory = new ITextReplacedElementFactory(outputDevice);
        sharedContext.setReplacedElementFactory(replacedElementFactory);

        sharedContext.setTextRenderer(new ITextTextRenderer());
        sharedContext.setDPI(72 * dotsPerPoint);
        sharedContext.setDotsPerPixel(dotsPerPixel);
        sharedContext.setPrint(true);
        sharedContext.setInteractive(false);

        sharedContext.setDebug_draw_boxes(debug);
        sharedContext.setDebug_draw_font_metrics(debug);
        sharedContext.setDebug_draw_inline_boxes(debug);
        sharedContext.setDebug_draw_line_boxes(debug);

    }

    private final String evaluateAccessorConfiguration(DocetDocumentPlaceholder placeholder, String fallback) {
        final String value = accessor.getPlaceholderForDocument(placeholder, language);
        if (value == null) {
            return fallback;
        }
        return value;
    }

    private final String evaluateAccessorConfiguration(DocetDocumentPlaceholder placeholder, Supplier supplier) {
        final String value = accessor.getPlaceholderForDocument(placeholder, language);
        if (value == null) {
            return supplier.get();
        }
        return value;
    }

    private Map calculatePlaceholders() {

        Map placeholders = new EnumMap<>(HTMLPlaceholder.class);

        placeholders.put(HTMLPlaceholder.PRODUCT_NAME,
                evaluateAccessorConfiguration(DocetDocumentPlaceholder.PRODUCT_NAME, ""));

        placeholders.put(HTMLPlaceholder.PRODUCT_VERSION,
                evaluateAccessorConfiguration(DocetDocumentPlaceholder.PRODUCT_VERSION, ""));

        placeholders.put(HTMLPlaceholder.TITLE, title);

        placeholders.put(HTMLPlaceholder.SUBTITLE,
                evaluateAccessorConfiguration(DocetDocumentPlaceholder.PDF_COVER_SUBTITLE_1, ""));


        placeholders.put(HTMLPlaceholder.FOOTER_TEXT,
                evaluateAccessorConfiguration(DocetDocumentPlaceholder.PDF_FOOTER_PAGE, () -> {
                    if (title == null || title.isEmpty()) {
                        return placeholders.get(HTMLPlaceholder.PRODUCT_NAME) + " "
                                + placeholders.get(HTMLPlaceholder.PRODUCT_VERSION);
                    } else {
                        return placeholders.get(HTMLPlaceholder.PRODUCT_NAME) + " "
                                + placeholders.get(HTMLPlaceholder.PRODUCT_VERSION) + " - " + title;
                    }
                }));

        placeholders.put(HTMLPlaceholder.COVER_FOOTER_TEXT,
                evaluateAccessorConfiguration(DocetDocumentPlaceholder.PDF_FOOTER_COVER, DEFAULT_FOOTER_COVER));

        placeholders.put(HTMLPlaceholder.COVER_IMAGE,
                evaluateAccessorConfiguration(DocetDocumentPlaceholder.PDF_COVER_IMAGE, DEFAULT_COVER_IMAGE));

        return placeholders;

    }

    private Element calculatePageHead() {
        Element pageHead = new Document(baseURL);
        pageHead.append("");
        pageHead.append("");

        return pageHead;
    }

    private Element calculatePageHeadBody() throws DocetDocumentParsingException {
        return parseFragment("docet/pdf/page/docetpdf-header-footer.html", placeholders).body();
    }


    private Document calculateCover() throws DocetDocumentParsingException {

        Document coverHead = new Document(baseURL);
        coverHead.append("");
        coverHead.append("");

//        Node bookmarkHead = generateBookmarkHead("cover",placeholders.get(HTMLPlaceholder.TITLE));
//        Node bookmarkAnchor = generateBookmarkAnchor("cover",placeholders.get(HTMLPlaceholder.TITLE));

//        coverHead.appendChild(bookmarkHead);

        Document cover = parseFragment("docet/pdf/cover/docetpdf-cover.html", placeholders);
        normaliseHTML(cover);

        injectHTML(cover, coverHead.childNodesCopy(), true, null, false);
//        injectHTML(cover, coverHead.childNodesCopy(), true, Collections.singletonList(bookmarkAnchor), false);

        return cover;

    }


    /* ************************* */
    /* *** PUBLIC INTERFACE  *** */
    /* ************************* */


    /**
     * NOTE: Caller is responsible for cleaning up the OutputStream if something goes wrong.
     */
    public void createPDF(OutputStream os) throws DocetDocumentParsingException {

        createPDF(os, 1);
    }

    public void addSection(String html, String id, String name, String parentId) throws DocetDocumentParsingException {

        final DocumentPart parent;
        if (parentId != null) {
            parent = documents.get(parentId);
            if (parent == null) {
                throw new DocetDocumentParsingException("Unknown parent " + parentId);
            }
        } else {
            parent = null;
        }

        addSection(html, id, name, parent);

    }

    /* *********************** */
    /* *** PRIVATE METHODS *** */
    /* *********************** */

    private DocumentPart addSection(String html, String id, String name, DocumentPart parent) throws DocetDocumentParsingException {

        final Document rawHtml = Jsoup.parse(html, baseURL, PDFDocumentHandler.HTML_PARSER);

        DocumentPart section = generatePages(rawHtml, id, name, parent);

        documents.put(id, section);

        return section;

    }

    private DocumentPart generatePages(Document document, String id, String name, DocumentPart parent) throws DocetDocumentParsingException {

        document.select("#main").get(0).before("");

        normaliseHTML(document);

//        Node bookmarkHead = generateBookmarkHead(id,name);
//        Node bookmarkAnchor = generateBookmarkAnchor(id, name);

        List head = pageHead.childNodesCopy();
//        head.add(bookmarkHead);

        List body = pageHeadBody.childNodesCopy();
//        body.add(bookmarkAnchor);

        injectHTML(document, head, true, body, false);

        DocumentPart part = generateDocumentPart(document, name, parent);

        return part;

    }

    private void writeTOCBookmarks(Collection documents, PdfWriter writer) {

        writer.setViewerPreferences(PdfWriter.PageModeUseOutlines);

        PdfOutline root = writer.getRootOutline();

        Map outlines = new HashMap<>();

        for(DocumentPart document : documents) {

            PdfOutline parent = outlines.getOrDefault(document.parent, root);

            PageBox page = document.pages.get(0);

//            PdfDestination dest = new PdfDestination(PdfDestination.XYZ, 0, 0, 0);
            PdfDestination dest = new PdfDestination(PdfDestination.FIT);

            LOGGER.log(Level.FINE, "Writing bookmark {0} - {1} to page {2}",
                    new Object[] {title, document.name, document.startPageNo + page.getPageNo()});

            dest.addPage(writer.getPageReference(document.startPageNo + page.getPageNo()));

            PdfOutline outline = new PdfOutline(parent, dest, document.name, true);

            outlines.put(document, outline);

        }

    }

/* Original code from ITextOutputDevice.writeBookmark */
//    private void writeBookmark(RenderingContext c, Box root, PdfOutline parent, Bookmark bookmark) {
//        String href = bookmark.getHRef();
//        PdfDestination target = null;
//        Box box = bookmark.getBox();
//        if (href.length() > 0 && href.charAt(0) == '#') {
//            box = sharedContext.getBoxById(href.substring(1));
//        }
//        if (box != null) {
//            PageBox page = root.getLayer().getPage(c, getPageRefY(box));
//            int distanceFromTop = page.getMarginBorderPadding(c, CalculatedStyle.TOP);
//            distanceFromTop += box.getAbsY() - page.getTop();
//            target = new PdfDestination(PdfDestination.XYZ, 0, normalizeY(distanceFromTop / _dotsPerPoint), 0);
//            target.addPage(writer.getPageReference(startPageNo + page.getPageNo() + 1));
//        }
//        if (target == null) {
//            target = _defaultDestination;
//        }
//        PdfOutline outline = new PdfOutline(parent, target, bookmark.getName());
//        writeBookmarks(c, root, outline, bookmark.getChildren());
//    }


    private Node generateBookmarkHead(String id, String name) {
        return Parser.parseXmlFragment(
                "", baseURL).get(0);
    }

    private Node generateBookmarkAnchor(String id, String name) {
        return Parser.parseXmlFragment("", baseURL).get(0);
    }

    /**
     * Parse an {@code html} fragment and replace given placeholders
     */
    private Document parseFragment(
            String name,
            Map placeholders) throws DocetDocumentParsingException {

        String fragment;

        final InputStream is = this.getClass().getClassLoader().getResourceAsStream(name);

        if (is == null) {
            throw new DocetDocumentParsingException("Failed to load fragment " + name);
        }

        try {
            try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {

                byte[] buffer = new byte[1024];
                int length;
                while ((length = is.read(buffer)) != -1) {
                    os.write(buffer, 0, length);
                }

                fragment = os.toString(StandardCharsets.UTF_8.name());

            } finally {
                is.close();
            }

        } catch (IOException e) {
            throw new DocetDocumentParsingException("Failed to load fragment " + name, e);
        }

        for(Map.Entry entry : placeholders.entrySet()) {
            fragment = REPLACEMENT_PATTERNS.get(entry.getKey())
                    .matcher(fragment)
                    .replaceAll(entry.getValue());
        }

        return Jsoup.parse(fragment, baseURL, HTML_PARSER);
    }

    /**
     * Externally received HTML ist just body content, we need to wrap such data into a standard html
     * structure
     *
     * @param document
     * @throws DocetDocumentParsingException
     */
    private void normaliseHTML(Document document) throws DocetDocumentParsingException {

        final List children = new ArrayList<>(document.childNodes());

        if (document.getElementsByTag("html").isEmpty()) {
            /* Plain data without HTML*/

            Element html = document.appendElement("html");
            html.appendElement("head");

            Element body = html.appendElement("body");

            for(Node child : children) {
                child.remove();
                body.appendChild(child);
            }

        }

        sanitizeHTML(document);
    }


    private void sanitizeHTML(Document doc) {

        Elements msgInUls = doc.select("ul>li>div.msg");
        for (Element msg: msgInUls) {
            Element li = msg.parent();
            Element ul = li.parent();
            msg.remove();
            li.remove();
            final Element novelUl = new Element(Tag.valueOf("ul"), "");
            novelUl.appendChild(li);
            ul.before(novelUl);
            novelUl.after(msg);
        }

        Elements msgs = doc.select(".msg");
        for (Element msg : msgs) {
            unbreakingWrap(msg);
        }

        Elements pres = doc.select("pre");
        for (Element pre: pres) {
            /* Cleanup leading and trailing spaces and newlines */
            pre.html(pre.html().trim());
            unbreakingWrap(createElementWrap(pre, "pre"));
        }

        Elements codes = doc.select("code");
        for (Element code: codes) {
            createElementWrap("", code, false, "code");
        }

        Elements headings = doc.select("h1, h2, h3, h4, h5, h6");

        /* HTMLOutline.generate say that it suffice on just the html tag but isn't true */
        headings.attr("data-pdf-bookmark", "exclude");


        Elements imgs = doc.select("img:not(.inline)");
        for (Element img: imgs) {
            img.addClass("docetimage").before(new Element(Tag.valueOf("br"), "")).after(new Element(Tag.valueOf("br"), ""));
            createElementWrap(img);
        }
    }


    private static Element unbreakingWrap(final Element toWrap) {
        /*
         * Wrap in a "non breaking" div and his content in a normal div (this prevent some inline display of
         * first div)
         */
        return createElementWrap(
                createElementWrap(toWrap,
                        false, "avoid-break", "wide"),
                false, "wide");
    }

    private static Element createElementWrap(final Element toWrap, String... cssClasses) {
        return createElementWrap(toWrap, true, cssClasses);
    }

    private static Element createElementWrap(final Element toWrap, boolean copyClasses, String... cssClasses) {
        return createElementWrap("
", toWrap, copyClasses, cssClasses); } private static Element createElementWrap(String wrapping, final Element wrapped, boolean copyClasses, String... cssClasses) { Element wrap = wrapped.wrap(wrapping).parent(); if (copyClasses) { wrapped.classNames().stream().forEach(cssClass -> wrap.addClass(cssClass)); } Arrays.asList(cssClasses).forEach(cssClass -> wrap.addClass(cssClass)); return wrap; } private void injectHTML( Document document, List headerHTML, boolean headerAppend, List bodyHTML, boolean bodyAppend) { if (headerHTML != null && !headerHTML.isEmpty()) { Element head = document.head(); if (headerAppend) { head.insertChildren(head.childNodeSize(), headerHTML); } else { head.insertChildren(0, headerHTML); } } if (bodyHTML != null && !bodyHTML.isEmpty()) { Element body = document.body(); if (bodyAppend) { body.insertChildren(body.childNodeSize(), bodyHTML); } else { body.insertChildren(0, bodyHTML); } } } private static Document jsoupFromSrc(String src, String url) { return Jsoup.parseBodyFragment(src, url); } /** * This method will handle given document as is * * @param document * @param name * @param url * @param nsh * @return * @throws DocetDocumentParsingException */ @SuppressWarnings("unchecked") private DocumentPart generateDocumentPart(Document document, String name, DocumentPart parent) throws DocetDocumentParsingException { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER, "Rendering {0} - {1} as {2}:\n", new Object[] {title, name, document}); } org.w3c.dom.Document doc = W3CDOM.fromJsoup(document); getFontResolver().flushFontFaceFonts(); /* * Do NOT reset the context! * * sharedContext.reset(); */ if (Configuration.isTrue("xr.cache.stylesheets", true)) { sharedContext.getCss().flushStyleSheets(); } else { sharedContext.getCss().flushAllStyleSheets(); } sharedContext.setBaseURL(baseURL); sharedContext.setNamespaceHandler(namespaceHandler); sharedContext.getCss().setDocumentContext(sharedContext, sharedContext.getNamespaceHandler(), doc, NullUserInterface.INSTANCE); getFontResolver().importFontFaces(sharedContext.getCss().getFontFaceRules()); LayoutContext c = newLayoutContext(); BlockBox root = BoxBuilder.createRootBox(c, doc); root.setContainingBlock(new ViewportBox(getInitialExtents(c))); root.layout(c); Dimension dim = root.getLayer().getPaintingDimension(c); root.getLayer().trimEmptyPages(c, dim.height); root.getLayer().layoutPages(c); DocumentPart part = new DocumentPart(); part.doc = doc; part.name = name; part.root = root; part.pages = root.getLayer().getPages(); if (parent != null) { part.parent = parent; } return part; } private DocumentPart generateTOC(Collection parts, int initialPageNo) throws DocetDocumentParsingException { Document document = jsoupFromSrc(buildTOC(parts, initialPageNo), baseURL); normaliseHTML(document); // Node bookmarkHead = generateBookmarkHead("toc","Table of Contents"); // Node bookmarkAnchor = generateBookmarkAnchor("toc","Table of Contents"); List head = pageHead.childNodesCopy(); // head.add(bookmarkHead); List body = pageHeadBody.childNodesCopy(); // body.add(bookmarkAnchor); injectHTML(document, head, true, body, false); return generateDocumentPart(document, "Table of Contents", null); } private DocumentPart generateCover() throws DocetDocumentParsingException { return generateDocumentPart(cover, placeholders.get(HTMLPlaceholder.TITLE), null); } private void appendTOC(List nodes, StringBuilder builder) { if (nodes.isEmpty()) { return; } builder.append("
    "); for(TOCNode node : nodes) { builder .append("
  1. ") .append("") .append("") .append("") .append("") .append("") .append("") .append("") .append("
    ").append(node.bullet).append("").append(node.document.name).append("").append("").append(node.page).append("
    "); appendTOC(node.children, builder); builder.append("
  2. "); } builder.append("
"); } private String buildTOC(Collection parts, int initialPageNo) { List roots = new ArrayList<>(); Map nodes = new HashMap<>(); int pageNo = initialPageNo; for(DocumentPart part : parts) { final TOCNode parent = nodes.get(part.parent); if (parent == null) { TOCNode node = new TOCNode(part,pageNo,Integer.toString(roots.size() + 1)); nodes.put(part, node); roots.add(node); } else { TOCNode node = new TOCNode(part,pageNo,parent.bullet + '.' + Integer.toString(parent.children.size() + 1)); nodes.put(part, node); parent.children.add(node); } pageNo += part.pages.size(); } StringBuilder builder = new StringBuilder(); builder .append("
") .append("

Table of contents

") .append("
"); appendTOC(roots, builder); builder .append("
") .append("
") ; return builder.toString(); } private void createPDF(OutputStream os, int initialPageNo) throws DocetDocumentParsingException { if (documents.isEmpty() ) { throw new DocetDocumentParsingException("No available pages to parse"); } if (initialPageNo < 1) { initialPageNo = 1; } int coversize = 0; DocumentPart cover = null; if (renderCover) { cover = generateCover(); coversize = 1; } int tocsize = 0; DocumentPart toc = null; if (renderToc) { toc = generateTOC(documents.values(), initialPageNo + coversize + 1 /* Cover & TOC part*/); /* Regenerate TOC if more than one page */ if (toc.pages.size() > 1) { toc = generateTOC(documents.values(), initialPageNo + coversize + toc.pages.size()); } tocsize = toc.pages.size(); } int totalPages = documents.values().stream().mapToInt(p -> p.pages.size()).sum() + coversize + tocsize; com.lowagie.text.Document pdf = null; PdfWriter writer = null; try { /* Uses the first page added to evaluate page size and create the writer */ DocumentPart firstSection = documents.values().stream().findFirst().get(); PageBox firstPage = firstSection.pages.get(0); RenderingContext renderingContext = newRenderingContext(firstSection.root); com.lowagie.text.Rectangle firstPageSize = new com.lowagie.text.Rectangle(0, 0, firstPage.getWidth(renderingContext) / dotsPerPoint, firstPage.getHeight(renderingContext) / dotsPerPoint); pdf = new com.lowagie.text.Document(firstPageSize, 0, 0, 0, 0); try { writer = PdfWriter.getInstance(pdf, os); } catch (DocumentException e) { throw new DocetDocumentParsingException("Cannot create a pdf writer", e); } pdf.addTitle(placeholders.get(HTMLPlaceholder.TITLE)); pdf.open(); /* Write each document */ try { if (renderCover) { writePDF(cover, totalPages, pdf, writer); } if (renderToc) { writePDF(toc, totalPages, pdf, writer); } for(DocumentPart part : documents.values()) { writePDF(part, totalPages, pdf, writer); } } catch (DocumentException e) { throw new DocetDocumentParsingException("Cannot write document " + placeholders.get(HTMLPlaceholder.TITLE) + " to pdf", e); } /* Terminate writing bookmarks with collected pages */ if (renderBookmarks) { writeTOCBookmarks(documents.values(), writer); } } finally { if (pdf != null) { pdf.close(); } if (writer != null) { writer.close(); } } } private void writePDF( DocumentPart part, int totalPages, com.lowagie.text.Document pdf, PdfWriter writer) throws DocumentException { int initialPageNo = writer.getPageNumber(); LOGGER.log(Level.FINE, "Writing {0} - {1}: {3} pages from {2}", new Object[] {title, part.name, initialPageNo, part.pages.size(), initialPageNo}); part.startPageNo = initialPageNo; RenderingContext renderingContext = newRenderingContext(part.root); renderingContext.setInitialPageNo(initialPageNo); if (initialPageNo > 0) { renderingContext.setPageCount(totalPages - initialPageNo + 1); } else { renderingContext.setPageCount(totalPages); } outputDevice.setRoot(part.root); outputDevice.start(part.doc); outputDevice.setWriter(writer); outputDevice.setStartPageNo(initialPageNo- 1); part.root.getLayer().assignPagePaintingPositions(renderingContext, Layer.PAGED_MODE_PRINT); int pageNo = 0; for(PageBox page : part.pages) { if (Thread.currentThread().isInterrupted()) throw new RuntimeException("Timeout occured"); com.lowagie.text.Rectangle pageSize = new com.lowagie.text.Rectangle(0, 0, page.getWidth(renderingContext) / dotsPerPoint, page.getHeight(renderingContext) / dotsPerPoint); pdf.setPageSize(pageSize); outputDevice.initializePage(writer.getDirectContent(), pageSize.getHeight()); renderingContext.setPage(pageNo++, page); paintPage(renderingContext, writer, page, part.root); outputDevice.finishPage(); /* Advance page */ pdf.newPage(); } outputDevice.finish(renderingContext, part.root); cleanOutputDevice(); } /** Hack to be able to clean bookmarks */ private static final Field BOOKMARKS_OUTPUT_DEVICE_FIELD; static { Field field = null; try { field = ITextOutputDevice.class.getDeclaredField("_bookmarks"); field.setAccessible(true); } catch (NoSuchFieldException | SecurityException e) { LOGGER.log(Level.WARNING, "Cannot prepare ITextOutputDevice bookmarks cleaner", e); } BOOKMARKS_OUTPUT_DEVICE_FIELD = field; } /** * Cleanup output device for the next html page to write. This method expecially clean up bookmarks that * are left over in the device (or they will be printed more times). */ private void cleanOutputDevice() { if (BOOKMARKS_OUTPUT_DEVICE_FIELD != null) { try { List bookmarks = (List) BOOKMARKS_OUTPUT_DEVICE_FIELD.get(outputDevice); bookmarks.clear(); } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) { LOGGER.log(Level.WARNING, "Cannot clear ITextOutputDevice bookmarks", e); } } } private ITextFontResolver getFontResolver() { return (ITextFontResolver) sharedContext.getFontResolver(); } private Rectangle getInitialExtents(LayoutContext c) { PageBox first = Layer.createPageBox(c, "first"); return new Rectangle(0, 0, first.getContentWidth(c), first.getContentHeight(c)); } private RenderingContext newRenderingContext(BlockBox _root) { RenderingContext result = sharedContext.newRenderingContextInstance(); result.setFontContext(new ITextFontContext()); result.setOutputDevice(outputDevice); sharedContext.getTextRenderer().setup(result.getFontContext()); result.setRootLayer(_root.getLayer()); return result; } private LayoutContext newLayoutContext() { LayoutContext result = sharedContext.newLayoutContextInstance(); result.setFontContext(new ITextFontContext()); sharedContext.getTextRenderer().setup(result.getFontContext()); return result; } private void paintPage(RenderingContext c, PdfWriter writer, PageBox page, BlockBox _root) { page.paintBackground(c, 0, Layer.PAGED_MODE_PRINT); page.paintMarginAreas(c, 0, Layer.PAGED_MODE_PRINT); page.paintBorder(c, 0, Layer.PAGED_MODE_PRINT); Shape working = outputDevice.getClip(); Rectangle content = page.getPrintClippingBounds(c); outputDevice.clip(content); int top = -page.getPaintingTop() + page.getMarginBorderPadding(c, CalculatedStyle.TOP); int left = page.getMarginBorderPadding(c, CalculatedStyle.LEFT); outputDevice.translate(left, top); _root.getLayer().paint(c); outputDevice.translate(-left, -top); outputDevice.setClip(working); } /* *********************** */ /* *** PRIVATE CLASSES *** */ /* *********************** */ private static enum HTMLPlaceholder { PRODUCT_NAME, PRODUCT_VERSION, TITLE, SUBTITLE, FOOTER_TEXT, COVER_FOOTER_TEXT, COVER_IMAGE; } /** * Working document data. * * @author diego.salvi */ private static final class DocumentPart { private org.w3c.dom.Document doc; private String name; private BlockBox root; private List pages; private int startPageNo; private DocumentPart parent; private transient int level = 0; public int getLevel() { if (level == 0) { level = (parent == null? 0 : parent.getLevel() ) + 1; } return level; } @Override public String toString() { return "DocumentPart [name=" + name + "]"; } } /** A Table of Content node */ private static final class TOCNode { final DocumentPart document; final List children; final String bullet; final int page; public TOCNode(DocumentPart document, int page, String bullet) { super(); this.document = document; this.page = page; this.bullet = bullet; this.children = new ArrayList<>(); } } /** A dummy user interface */ private static final class NullUserInterface implements UserInterface { static final UserInterface INSTANCE = new NullUserInterface(); @Override public boolean isHover(org.w3c.dom.Element e) { return false; } @Override public boolean isActive(org.w3c.dom.Element e) { return false; } @Override public boolean isFocus(org.w3c.dom.Element e) { return false; } } /** * An user agent capable to resolve URLs from current {@link ClassLoader}. *

* Not thread safe and keeps memory of resolved uris. *

*/ private static final class ClassloaderAwareUserAgent extends ITextUserAgent { public ClassloaderAwareUserAgent(ITextOutputDevice outputDevice) { super(outputDevice); } private final Map alreadyResolved = new HashMap<>(); @Override public String resolveURI(String uri) { String resolved = alreadyResolved.get(uri); if (resolved != null) { LOGGER.log(Level.FINE, "Resolved uri in cache from {0} to {1}", new Object[] {uri, resolved}); return resolved; } final URL resource = Thread.currentThread().getContextClassLoader().getResource(uri); if (resource == null) { resolved = super.resolveURI(uri); LOGGER.log(Level.FINE, "Resolved uri externally from {0} to {1}", new Object[] {uri, resolved}); } else { resolved = resource.toString(); LOGGER.log(Level.FINE, "Resolved uri internally from {0} to {1}", new Object[] {uri, resolved}); } alreadyResolved.put(uri,resolved); return resolved; } } private static final class BuilderImpl implements Builder { private boolean debug = DEFAULT_DEBUG; private boolean cover = DEFAULT_COVER; private boolean toc = DEFAULT_TOC; private boolean bookmarks = DEFAULT_BOOKMARKS; private String title = DEFAULT_TITLE; private DocetDocumentResourcesAccessor accessor = DEFAULT_ACCESSOR; private DocetLanguage language = DEFAULT_LANGUAGE; private float dotsPerPoint = DEFAULT_DOTS_PER_POINT; private int dotsPerPixel = DEFAULT_DOTS_PER_PIXEL; private String baseURL = DEFAULT_BASE_URL; private NamespaceHandler namespaceHandler; @Override public Builder debug(boolean debug) { this.debug = debug; return this; } @Override public Builder cover(boolean cover) { this.cover = cover; return this; } @Override public Builder toc(boolean toc) { this.toc = toc; return this; } @Override public Builder bookmarks(boolean bookmarks) { this.bookmarks = bookmarks; return this; } @Override public Builder title(String title) { this.title = title; return this; } @Override public Builder placeholders(DocetDocumentResourcesAccessor accessor) { this.accessor = accessor; return this; } @Override public Builder language(DocetLanguage language) { this.language = language; return this; } @Override public Builder dotsPerPoint(float dotsPerPoint) { this.dotsPerPoint = dotsPerPoint; return this; } @Override public Builder dotsPerPixel(int dotsPerPixel) { this.dotsPerPixel = dotsPerPixel; return this; } @Override public Builder baseURL(String baseURL) { this.baseURL = baseURL; return this; } @Override public Builder namespaceHandler(NamespaceHandler namespaceHandler) { this.namespaceHandler = namespaceHandler; return this; } @Override public PDFDocumentHandler create() throws DocetDocumentParsingException { if (namespaceHandler == null) { namespaceHandler = new XhtmlNamespaceHandler(); } return new PDFDocumentHandler( debug, cover, toc, bookmarks, dotsPerPoint, dotsPerPixel, baseURL, namespaceHandler, title, accessor, language); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy