diff --git a/qqq-backend-core/pom.xml b/qqq-backend-core/pom.xml index 686049ca..87a88d69 100644 --- a/qqq-backend-core/pom.xml +++ b/qqq-backend-core/pom.xml @@ -97,6 +97,23 @@ java-dotenv 5.2.2 + + org.apache.velocity + velocity-engine-core + 2.3 + + + + + org.jsoup + jsoup + 1.15.3 + + + org.xhtmlrenderer + flying-saucer-pdf-openpdf + 9.1.22 + diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfAction.java new file mode 100644 index 00000000..38780bd0 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfAction.java @@ -0,0 +1,99 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 . + */ + +package com.kingsrook.qqq.backend.core.actions.templates; + + +import java.nio.file.Path; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.templates.ConvertHtmlToPdfInput; +import com.kingsrook.qqq.backend.core.model.templates.ConvertHtmlToPdfOutput; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.xhtmlrenderer.layout.SharedContext; +import org.xhtmlrenderer.pdf.ITextRenderer; + + +/******************************************************************************* + ** Action to convert a string of HTML to a PDF! + ** + ** Much credit to https://www.baeldung.com/java-html-to-pdf + *******************************************************************************/ +public class ConvertHtmlToPdfAction extends AbstractQActionFunction +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public ConvertHtmlToPdfOutput execute(ConvertHtmlToPdfInput input) throws QException + { + try + { + ConvertHtmlToPdfOutput output = new ConvertHtmlToPdfOutput(); + + ////////////////////////////////////////////////////////////////// + // convert the input HTML to XHTML, as needed for ITextRenderer // + ////////////////////////////////////////////////////////////////// + Document document = Jsoup.parse(input.getHtml()); + document.outputSettings().syntax(Document.OutputSettings.Syntax.xml); + + ////////////////////////////// + // convert the XHTML to PDF // + ////////////////////////////// + ITextRenderer renderer = new ITextRenderer(); + SharedContext sharedContext = renderer.getSharedContext(); + sharedContext.setPrint(true); + sharedContext.setInteractive(false); + + if(input.getBasePath() != null) + { + String baseUrl = input.getBasePath().toUri().toURL().toString(); + renderer.setDocumentFromString(document.html(), baseUrl); + } + else + { + renderer.setDocumentFromString(document.html()); + } + + ////////////////////////////////////////////////// + // register any custom fonts the input supplied // + ////////////////////////////////////////////////// + for(Map.Entry entry : CollectionUtils.nonNullMap(input.getCustomFonts()).entrySet()) + { + renderer.getFontResolver().addFont(entry.getValue().toAbsolutePath().toString(), entry.getKey(), "UTF-8", true, null); + } + + renderer.layout(); + renderer.createPDF(input.getOutputStream()); + + return (output); + } + catch(Exception e) + { + throw (new QException("Error converting html to pdf", e)); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/ConvertHtmlToPdfInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/ConvertHtmlToPdfInput.java new file mode 100644 index 00000000..1390b1e5 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/ConvertHtmlToPdfInput.java @@ -0,0 +1,207 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 . + */ + +package com.kingsrook.qqq.backend.core.model.templates; + + +import java.io.OutputStream; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ConvertHtmlToPdfInput extends AbstractActionInput +{ + private String html; + private OutputStream outputStream; + + private Path basePath; + private Map customFonts = new HashMap<>(); + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ConvertHtmlToPdfInput(QInstance instance) + { + super(instance); + } + + + + /******************************************************************************* + ** Getter for html + ** + *******************************************************************************/ + public String getHtml() + { + return html; + } + + + + /******************************************************************************* + ** Setter for html + ** + *******************************************************************************/ + public void setHtml(String html) + { + this.html = html; + } + + + + /******************************************************************************* + ** Fluent setter for html + ** + *******************************************************************************/ + public ConvertHtmlToPdfInput withHtml(String html) + { + this.html = html; + return (this); + } + + + + /******************************************************************************* + ** Getter for outputStream + ** + *******************************************************************************/ + public OutputStream getOutputStream() + { + return outputStream; + } + + + + /******************************************************************************* + ** Setter for outputStream + ** + *******************************************************************************/ + public void setOutputStream(OutputStream outputStream) + { + this.outputStream = outputStream; + } + + + + /******************************************************************************* + ** Fluent setter for outputStream + ** + *******************************************************************************/ + public ConvertHtmlToPdfInput withOutputStream(OutputStream outputStream) + { + this.outputStream = outputStream; + return (this); + } + + + + /******************************************************************************* + ** Getter for basePath + ** + *******************************************************************************/ + public Path getBasePath() + { + return basePath; + } + + + + /******************************************************************************* + ** Setter for basePath + ** + *******************************************************************************/ + public void setBasePath(Path basePath) + { + this.basePath = basePath; + } + + + + /******************************************************************************* + ** Fluent setter for basePath + ** + *******************************************************************************/ + public ConvertHtmlToPdfInput withBasePath(Path basePath) + { + this.basePath = basePath; + return (this); + } + + + + /******************************************************************************* + ** Getter for customFonts + ** + *******************************************************************************/ + public Map getCustomFonts() + { + return customFonts; + } + + + + /******************************************************************************* + ** Setter for customFonts + ** + *******************************************************************************/ + public void setCustomFonts(Map customFonts) + { + this.customFonts = customFonts; + } + + + + /******************************************************************************* + ** Fluent setter for customFonts + ** + *******************************************************************************/ + public ConvertHtmlToPdfInput withCustomFonts(Map customFonts) + { + this.customFonts = customFonts; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for customFonts + ** + *******************************************************************************/ + public ConvertHtmlToPdfInput withCustomFont(String name, Path path) + { + if(this.customFonts == null) + { + this.customFonts = new HashMap<>(); + } + this.customFonts.put(name, path); + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/ConvertHtmlToPdfOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/ConvertHtmlToPdfOutput.java new file mode 100644 index 00000000..95f71773 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/ConvertHtmlToPdfOutput.java @@ -0,0 +1,69 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 . + */ + +package com.kingsrook.qqq.backend.core.model.templates; + + +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ConvertHtmlToPdfOutput extends AbstractActionOutput +{ + private String result; + + + + /******************************************************************************* + ** Getter for result + ** + *******************************************************************************/ + public String getResult() + { + return result; + } + + + + /******************************************************************************* + ** Setter for result + ** + *******************************************************************************/ + public void setResult(String result) + { + this.result = result; + } + + + + /******************************************************************************* + ** Fluent setter for result + ** + *******************************************************************************/ + public ConvertHtmlToPdfOutput withResult(String result) + { + this.result = result; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java index 3a51e661..c75fa727 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java @@ -436,13 +436,29 @@ public class CollectionUtils ** ** Meant to help avoid null checks on foreach loops. *******************************************************************************/ - public static Collection nonNullCollection(Collection list) + public static Collection nonNullCollection(Collection c) { - if(list == null) + if(c == null) { return (new ArrayList<>()); } - return (list); + return (c); + } + + + + /******************************************************************************* + ** Returns the input map, unless it was null - in which case a new HashMap is returned. + ** + ** Meant to help avoid null checks on foreach loops. + *******************************************************************************/ + public static Map nonNullMap(Map map) + { + if(map == null) + { + return (new HashMap<>()); + } + return (map); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfActionTest.java new file mode 100644 index 00000000..4a55a56c --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/ConvertHtmlToPdfActionTest.java @@ -0,0 +1,104 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 . + */ + +package com.kingsrook.qqq.backend.core.actions.templates; + + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.model.templates.ConvertHtmlToPdfInput; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for ConvertHtmlToPdfAction + *******************************************************************************/ +class ConvertHtmlToPdfActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException, IOException + { + QInstance instance = TestUtils.defineInstance(); + ConvertHtmlToPdfInput input = new ConvertHtmlToPdfInput(instance); + input.setSession(new QSession()); + + input.setHtml(""" + + + + + + +
+

+ + Hello QQQ! +

+
+

This is a test of converting HTML to PDF!!

+

(btw, is this in SF-Pro???)

+
+
+ + + """); + + OutputStream outputStream = new FileOutputStream("/tmp/file.pdf"); + input.setOutputStream(outputStream); + + String resourceDir = "src/test/resources/actions/templates"; + input.withCustomFont("SF-Pro", Path.of(resourceDir + "/fonts/SF-Pro-Rounded-Regular.otf")); + input.withCustomFont("Helvetica", Path.of(resourceDir + "/fonts/Helvetica.ttc")); + input.withBasePath(Path.of(resourceDir)); + + new ConvertHtmlToPdfAction().execute(input); + System.out.println("Wrote /tmp/file.pdf"); + + outputStream.close(); + + ///////////////////////////////////////////////////////////////////////// + // for local dev on a mac, turn this on to auto-open the generated PDF // + ///////////////////////////////////////////////////////////////////////// + // Runtime.getRuntime().exec(new String[] { "/usr/bin/open", "/tmp/file.pdf" }); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/resources/actions/templates/fonts/Helvetica.ttc b/qqq-backend-core/src/test/resources/actions/templates/fonts/Helvetica.ttc new file mode 100644 index 00000000..c2c74cbe Binary files /dev/null and b/qqq-backend-core/src/test/resources/actions/templates/fonts/Helvetica.ttc differ diff --git a/qqq-backend-core/src/test/resources/actions/templates/fonts/SF-Pro-Rounded-Regular.otf b/qqq-backend-core/src/test/resources/actions/templates/fonts/SF-Pro-Rounded-Regular.otf new file mode 100755 index 00000000..35272149 Binary files /dev/null and b/qqq-backend-core/src/test/resources/actions/templates/fonts/SF-Pro-Rounded-Regular.otf differ diff --git a/qqq-backend-core/src/test/resources/actions/templates/images/qqq-logo-2.png b/qqq-backend-core/src/test/resources/actions/templates/images/qqq-logo-2.png new file mode 100644 index 00000000..da00c917 Binary files /dev/null and b/qqq-backend-core/src/test/resources/actions/templates/images/qqq-logo-2.png differ diff --git a/qqq-backend-core/src/test/resources/actions/templates/styles/styles.css b/qqq-backend-core/src/test/resources/actions/templates/styles/styles.css new file mode 100644 index 00000000..c5c60ce7 --- /dev/null +++ b/qqq-backend-core/src/test/resources/actions/templates/styles/styles.css @@ -0,0 +1,26 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 . + */ + +h1 +{ + text-align: center; + border-bottom: 1px solid red; +} \ No newline at end of file