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-dotenv5.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