org.opensingular.lib.commons.pdf.PDFUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.opensingular.lib.commons.pdf.PDFUtil.java

Source

/*
 * Copyright (C) 2016 Singular Studios (a.k.a Atom Tecnologia) - www.opensingular.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 org.opensingular.lib.commons.pdf;

import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.opensingular.internal.lib.commons.util.TempFileProvider;
import org.opensingular.lib.commons.util.Loggable;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Classe utilitria para a manipulao de PDF's.
 */
@SuppressWarnings("UnusedDeclaration")
public abstract class PDFUtil implements Loggable {

    /**
     * A constante BEGIN_COMMAND.
     */
    private static final String BEGIN_COMMAND = "/";

    /**
     * A constante SET_FONT.
     */
    private static final String SET_FONT = "Tf\n";

    /**
     * A constante SPACE.
     */
    private static final int SPACE = 32;

    public static final String SINGULAR_WKHTML2PDF_HOME = "singular.wkhtml2pdf.home";

    /**
     * A constante "username".
     */
    protected static String username = null;

    /**
     * A constante "password".
     */
    protected static String password = null;

    /**
     * A constante "proxy".
     */
    protected static String proxy = null;

    /**
     * O caminho no sistema de arquivos para o local onde se encontram as bibliotecas nativas utilizadas
     * por este utilitrio de manipulao de PDF's.
     */
    private static File wkhtml2pdfHome;

    /**
     * O tamanho da pgina. O valor padro  {@link PageSize#PAGE_A4}.
     */
    private PageSize pageSize = null;

    /**
     * A orientao da pgina. O valor padro  {@link PageOrientation#PAGE_PORTRAIT}.
     */
    private PageOrientation pageOrientation = null;

    /**
     * Indica quando necessrio esperar por execuo javascript na pgina.
     */
    private int javascriptDelay = 0;

    /**
     * Localiza a implementao correta para o Sistema operacional atual.
     */
    @Nonnull
    private static PDFUtil fabric() {
        if (isWindows()) {
            return new PDFUtilWin();
        }
        return new PDFUtilUnix();
    }

    public final static boolean isWindows() {
        String os = System.getProperty("os.name").toLowerCase();
        return os.contains("win");
    }

    final static boolean isMac() {
        String os = System.getProperty("os.name").toLowerCase();
        return os.contains("mac os");
    }

    /**
     * Cria a verso correspondente ao sistema operacional atual.
     */
    @Nonnull
    public static PDFUtil getInstance() {
        return fabric();
    }

    /**
     * Converte o cdigo HTML em um arquivo PDF com o cabealho e rodap especificados.
     *
     * @param html   o cdigo HTML.
     * @param header o cdigo HTML do cabealho.
     * @param footer o cdigo HTML do rodap.
     * @return O arquivo PDF retornado  temporrio e deve ser apagado pelo solicitante para no deixa lixo.
     */
    @Nonnull
    public File convertHTML2PDF(@Nonnull String html, @Nullable String header, @Nullable String footer)
            throws SingularPDFException {
        return convertHTML2PDF(html, header, footer, null);
    }

    /**
     * Converte o cdigo HTML em um arquivo PDF com o cabealho e rodap especificados.
     *
     * @param html             o cdigo HTML.
     * @param header           o cdigo HTML do cabealho.
     * @param footer           o cdigo HTML do rodap.
     * @param additionalConfig configuraes adicionais.
     * @return O arquivo PDF retornado  temporrio e deve ser apagado pelo solicitante para no deixa lixo.
     */
    @Nonnull
    public final File convertHTML2PDF(@Nonnull String rawHtml, @Nullable String rawHeader,
            @Nullable String rawFooter, @Nullable List<String> additionalConfig) throws SingularPDFException {
        getWkhtml2pdfHome(); // Fora verifica se o Home est configurado corretamente

        final String html = safeWrapHtml(rawHtml);
        final String header = safeWrapHtml(rawHeader);
        final String footer = safeWrapHtml(rawFooter);

        try (TempFileProvider tmp = TempFileProvider.createForUseInTryClause(this)) {

            File htmlFile = tmp.createTempFile("content.html");
            writeToFile(htmlFile, html);

            List<String> commandAndArgs = new ArrayList<>(0);
            commandAndArgs.add(getHomeAbsolutePath("bin", fixExecutableName("wkhtmltopdf")));

            if (additionalConfig != null) {
                commandAndArgs.addAll(additionalConfig);
            } else {
                addDefaultPDFCommandArgs(commandAndArgs);
            }

            if (header != null) {
                File headerFile = tmp.createTempFile("header.html");
                writeToFile(headerFile, header);
                commandAndArgs.add("--header-html");
                commandAndArgs.add(fixPathArg(headerFile));
                addDefaultHeaderCommandArgs(commandAndArgs);
            }

            if (footer != null) {
                File footerFile = tmp.createTempFile("footer.html");
                writeToFile(footerFile, footer);
                commandAndArgs.add("--footer-html");
                commandAndArgs.add(fixPathArg(footerFile));
                addDefaultFooterCommandArgs(commandAndArgs);
            }

            File pdfFile = tmp.createTempFileByDontPutOnDeleteList("result.pdf");
            commandAndArgs.add(fixPathArg(htmlFile));
            commandAndArgs.add(pdfFile.getAbsolutePath());

            return runProcess(commandAndArgs, pdfFile);
        }
    }

    /**
     * Converte o cdigo HTML em um arquivo PNG.
     *
     * @param html             o cdigo HTML.
     * @param additionalConfig configuraes adicionais.
     * @return O arquivo PDF retornado  temporrio e deve ser apagado pelo solicitante para no deixa lixo.
     */
    @Nonnull
    public final File convertHTML2PNG(@Nonnull String html, @Nullable List<String> additionalConfig)
            throws SingularPDFException {
        getWkhtml2pdfHome(); // Fora verifica se o Home est configurado corretamente

        try (TempFileProvider tmp = TempFileProvider.createForUseInTryClause(this)) {

            File htmlFile = tmp.createTempFile("content.html");
            writeToFile(htmlFile, html);

            List<String> commandAndArgs = new ArrayList<>();
            commandAndArgs.add(getHomeAbsolutePath("bin", fixExecutableName("wkhtmltoimage")));

            if (additionalConfig != null) {
                commandAndArgs.addAll(additionalConfig);
            } else {
                addDefaultPNGCommandArgs(commandAndArgs);
            }

            File pngFile = tmp.createTempFileByDontPutOnDeleteList("result.png");

            //File jarFile = tmp.createTempFile("cookie.txt", true);
            //commandAndArgs.add("--cookie-jar");
            //commandAndArgs.add(jarFile.getAbsolutePath());

            commandAndArgs.add(fixPathArg(htmlFile));
            commandAndArgs.add(pngFile.getAbsolutePath());

            return runProcess(commandAndArgs, pngFile);
        }
    }

    /**
     * Converte o cdigo HTML em um arquivo PDF.
     *
     * @param html o cdigo HTML.
     * @return O arquivo PDF retornado  temporrio e deve ser apagado pelo solicitante para no deixa lixo.
     */
    @Nonnull
    public File convertHTML2PDF(@Nonnull String html) throws SingularPDFException {
        return convertHTML2PDF(html, null);
    }

    /**
     * Converte o cdigo HTML em um arquivo PNG.
     *
     * @param html o cdigo HTML.
     * @return O arquivo PNG retornado  temporrio e deve ser apagado pelo solicitante para no deixa lixo.
     */
    @Nonnull
    public File convertHTML2PNG(@Nonnull String html) throws SingularPDFException {
        return convertHTML2PNG(html, null);
    }

    /**
     * Converte o cdigo HTML em um arquivo PDF com o cabealho especificado.
     *
     * @param html   o cdigo HTML.
     * @param header o cdigo HTML do cabealho.
     * @return O arquivo PDF retornado  temporrio e deve ser apagado pelo solicitante para no deixa lixo.
     */
    @Nonnull
    public File convertHTML2PDF(@Nonnull String html, @Nullable String header) throws SingularPDFException {
        return convertHTML2PDF(html, header, null);
    }

    /**
     * Adiciona os argumentos padres para a gerao do PDF.
     *
     * @param commandArgs o vetor com os argumentos.
     */
    private void addDefaultPDFCommandArgs(List<String> commandArgs) {
        commandArgs.add("--print-media-type");
        commandArgs.add("--load-error-handling");
        commandArgs.add("ignore");

        if (username != null) {
            commandArgs.add("--username");
            commandArgs.add(username);
        }
        if (password != null) {
            commandArgs.add("--password");
            commandArgs.add(password);
        }
        if (proxy != null) {
            commandArgs.add("--proxy");
            commandArgs.add(proxy);
        }

        if (pageSize != null) {
            commandArgs.add("--page-size");
            commandArgs.add(pageSize.getValue());
        }
        if (pageOrientation != null) {
            commandArgs.add("--orientation");
            commandArgs.add(pageOrientation.getValue());
        }
        if (javascriptDelay > 0) {
            commandArgs.add("--javascript-delay");
            commandArgs.add(String.valueOf(javascriptDelay));
        }

        addSmartBreakScript(commandArgs);

    }

    /**
     * adiciona um script minificado de break de texto com mais de 1000 caracteres de forma automatica,
     * segue em comentario a verso original
     * <p>
     * (function () {
     * function preventBreakWrap(value) {
     * return '<span style=\'page-break-inside: avoid\'>' + value + '</span>';
     * }
     * function breakInBlocks(value, size) {
     * if (value.length > size) {
     * return preventBreakWrap(value.substr(0, size)) + breakInBlocks(value.substr(size, value.length), size);
     * }
     * return value;
     * }
     * function visitLeafs(root, visitor) {
     * if (root.children.length == 0) {
     * visitor(root);
     * } else {
     * for (var i = 0; i < root.children.length; i += 1) {
     * visitLeafs(root.children[i], visitor);
     * }
     * }
     * }
     * visitLeafs(document.getElementsByTagName('body')[0], function(e) {
     * e.innerHTML = breakInBlocks(e.innerHTML, 1000);
     * });
     * })();
     *
     * @param commandArgs os argumentos
     */
    private void addSmartBreakScript(List<String> commandArgs) {
        final String minificado = "\"!function(){function a(a){return'<span style=\\\'page-break-inside: avoid\\\'>'+"
                + "a+'</span>'}function b(c,d){return c.length>d?a(c.substr(0,d))+b(c.substr(d,c.length),d):c}function "
                + "c(a,b){if(0==a.children.length)b(a);else for(var d=0;d<a.children.length;d+=1)c(a.children[d],b)}c(d"
                + "ocument.getElementsByTagName('body')[0],function(a){a.innerHTML=b(a.innerHTML,600)})}();\"";
        commandArgs.add("--run-script");
        commandArgs.add(minificado);
    }

    /**
     * Adiciona os argumentos padres para a gerao do PNG.
     *
     * @param commandArgs o vetor com os argumentos.
     */
    private void addDefaultPNGCommandArgs(List<String> commandArgs) {
        commandArgs.add("--format");
        commandArgs.add("png");
        commandArgs.add("--load-error-handling");
        commandArgs.add("ignore");

        if (username != null) {
            commandArgs.add("--username");
            commandArgs.add(username);
        }
        if (password != null) {
            commandArgs.add("--password");
            commandArgs.add(password);
        }
        if (proxy != null) {
            commandArgs.add("--proxy");
            commandArgs.add(proxy);
        }
        if (javascriptDelay > 0) {
            commandArgs.add("--javascript-delay");
            commandArgs.add(String.valueOf(javascriptDelay));
        }
    }

    /**
     * Adiciona os argumentos padres para a gerao do cabealho.
     *
     * @param commandArgs o vetor com os argumentos.
     */
    private void addDefaultHeaderCommandArgs(List<String> commandArgs) {
        commandArgs.add("--header-spacing");
        commandArgs.add("5");
    }

    /**
     * Adiciona os argumentos padres para a gerao do rodap.
     *
     * @param commandArgs o vetor com os argumentos.
     */
    private void addDefaultFooterCommandArgs(List<String> commandArgs) {
        commandArgs.add("--footer-spacing");
        commandArgs.add("5");
    }

    /**
     * Altera o valor do atributo {@link #pageSize}.
     *
     * @param pageSize o novo valor a ser utilizado para "page size".
     */
    public void setPageSize(PageSize pageSize) {
        this.pageSize = pageSize;
    }

    /**
     * Altera o valor do atributo {@link #pageOrientation}.
     *
     * @param pageOrientation o novo valor a ser utilizado para "page orientation".
     */
    public void setPageOrientation(PageOrientation pageOrientation) {
        this.pageOrientation = pageOrientation;
    }

    /**
     * Altera o valor do atributo {@link #javascriptDelay}.
     *
     * @param javascriptDelay o novo valor a ser utilizado para "javascript delay".
     */
    public void setJavascriptDelay(int javascriptDelay) {
        this.javascriptDelay = javascriptDelay;
    }

    /**
     * O enumerador para o tamanho da pgina.
     */
    public enum PageSize {
        PAGE_A3("A3"), PAGE_A4("A4"), PAGE_LETTER("Letter");

        private String value;

        PageSize(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }
    }

    /**
     * O enumerador para a orientao da pgina.
     */
    public enum PageOrientation {
        PAGE_PORTRAIT("Portrait"), PAGE_LANDSCAPE("Landscape");

        private String value;

        PageOrientation(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }
    }

    /**
     * Concatena os pdf em um nico PDF.
     * @param pdfs
     * @return O arquivo retornado  temporrio e deve ser apagado pelo solicitante para no deixa lixo.
     */
    @Nonnull
    public File merge(@Nonnull List<InputStream> pdfs) throws SingularPDFException {
        try (TempFileProvider tmp = TempFileProvider.createForUseInTryClause(this)) {
            PDFMergerUtility pdfMergerUtility = new PDFMergerUtility();
            pdfs.forEach(pdfMergerUtility::addSource);

            File tempMergedFile = tmp.createTempFileByDontPutOnDeleteList("merge.pdf");

            try (FileOutputStream output = new FileOutputStream(tempMergedFile)) {
                pdfMergerUtility.setDestinationStream(output);
                pdfMergerUtility.mergeDocuments(MemoryUsageSetting.setupTempFileOnly());
                return tempMergedFile;
            }
        } catch (Exception e) {
            throw new SingularPDFException("Erro realizando merge de arquivos PDF", e);
        } finally {
            for (InputStream in : pdfs) {
                try {
                    in.close();
                } catch (IOException e) {
                    getLogger().error("Erro fechando inputStrem", e);
                }
            }
        }
    }

    private String safeWrapHtml(String html) {
        if (html == null || html.startsWith(("<!DOCTYPE"))) {
            return html;
        }
        String wraped = html;
        boolean needHTML = !html.startsWith("<html>");
        boolean needBody = needHTML && !html.startsWith("<body>");
        if (needBody) {
            wraped = "<body>" + wraped + "<body>";
        }
        if (needHTML) {
            wraped = "<!DOCTYPE HTML><html>" + wraped + "</html>";
        }
        return wraped;
    }

    protected static final @Nonnull File getWkhtml2pdfHome() {
        if (wkhtml2pdfHome == null) {
            String prop = System.getProperty(SINGULAR_WKHTML2PDF_HOME);

            if (prop == null) {
                throw new SingularPDFException("property 'singular.wkhtml2pdf.home' not set");
            }
            File file = new File(prop);
            if (!file.exists()) {
                throw new SingularPDFException("property '" + SINGULAR_WKHTML2PDF_HOME
                        + "' configured for a directory that nos exists: " + file.getAbsolutePath());
            }
            wkhtml2pdfHome = file;
        }
        return wkhtml2pdfHome;
    }

    final static void clearHome() {
        wkhtml2pdfHome = null;
    }

    @Nonnull
    private static final String getHomeAbsolutePath(@Nullable String subDir, @Nonnull String file)
            throws SingularPDFException {
        File arq = getWkhtml2pdfHome();
        if (subDir == null) {
            arq = new File(arq, file);
        } else {
            arq = new File(arq, subDir + File.separator + file);
        }
        if (!arq.exists()) {
            throw new SingularPDFException(
                    "Arquivo ou diretrio '" + arq.getAbsolutePath() + "' no encontrado.");
        }
        return arq.getAbsolutePath();
    }

    // -------------------------------------------------------------------
    // Mtodo para customizao de acordo com o sistema operacional
    // -------------------------------------------------------------------

    /** Permite ajustar o nome do executvel se necessrio no sistema operacional em questo. */
    protected String fixExecutableName(String executable) {
        return executable;
    }

    /** Permite ajustar o path do arquivo se necessrio no sistema operacional em questo. */
    protected @Nonnull String fixPathArg(@Nonnull File arq) {
        return arq.getAbsolutePath();
    }

    /**
     * Executa o comando inforamdo e verifica se o arquivo esperado foi de fato gerado. Dispara exception se houver erro
     * na execuo ou se o arquivo no for gerado.
     */
    protected abstract @Nonnull File runProcess(@Nonnull List<String> commandAndArgs, @Nonnull File expectedFile)
            throws SingularPDFException;

    /** Escreve o contedo informado no arquivo indicado. */
    protected abstract void writeToFile(File destination, String content) throws SingularPDFException;
}