Java tutorial
/* * 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; }