From b9bcc3c9d2dc9dc83d9c0d4542d30fd1100f456d Mon Sep 17 00:00:00 2001
From: tiagosalvado10 <9038083+tiagosalvado10@users.noreply.github.com>
Date: Tue, 6 Feb 2024 16:26:25 +0000
Subject: [PATCH] [MNT-23960] Added new options (pdfFont, pdfFontSize) to
textToPdf transformer (#885)
* [MNT-23960] Added options (pdfFont, pdfFontSize) and NotoSans fonts to textToPdf transformer
* [MNT-23960] Added 'MISC_PDFBOX_DEFAULT_FONT' and 'transform.core.misc.pdfbox.defaultFont' configuration to core-aio and misc T-Engines
* [MNT-23960] Added NotoSans fonts to core-aio and misc T-Engine images
* [MNT-23960] Improved logging: added messages, using placeholders
* [MNT-23960] Added DEFAULT_FONT constant (NotoSans-Regular)
* [MNT-23960] Splitted getFont(PDDocument, String) code into 3 methods. Added Javadoc.
* [MNT-23960] Return TransformCheckResult on transformTextAndCheck methods. Added assertion to testUTF8WithBOM test.
---
docs/external-engine-configuration.md | 4 +-
engines/aio/Dockerfile | 1 +
.../main/resources/application-default.yaml | 3 +
.../alfresco/transform/aio/AIOTikaTest.java | 4 +-
engines/misc/Dockerfile | 1 +
engines/misc/LICENSES.md | 1 +
.../TextToPdfContentTransformer.java | 296 ++++++++++++++++--
.../main/resources/application-default.yaml | 5 +
.../fonts/NotoSans/NotoSans-Bold.ttf | Bin 0 -> 582604 bytes
.../fonts/NotoSans/NotoSans-BoldItalic.ttf | Bin 0 -> 596992 bytes
.../fonts/NotoSans/NotoSans-Italic.ttf | Bin 0 -> 597000 bytes
.../fonts/NotoSans/NotoSans-Regular.ttf | Bin 0 -> 582748 bytes
.../main/resources/licenses/3rd-party/OFL.txt | 93 ++++++
.../main/resources/misc_engine_config.json | 4 +-
.../TextToPdfContentTransformerTest.java | 193 ++++++++++--
.../transform/common/RequestParamMap.java | 2 +
16 files changed, 555 insertions(+), 52 deletions(-)
create mode 100644 engines/misc/src/main/resources/fonts/NotoSans/NotoSans-Bold.ttf
create mode 100644 engines/misc/src/main/resources/fonts/NotoSans/NotoSans-BoldItalic.ttf
create mode 100644 engines/misc/src/main/resources/fonts/NotoSans/NotoSans-Italic.ttf
create mode 100644 engines/misc/src/main/resources/fonts/NotoSans/NotoSans-Regular.ttf
create mode 100644 engines/misc/src/main/resources/licenses/3rd-party/OFL.txt
diff --git a/docs/external-engine-configuration.md b/docs/external-engine-configuration.md
index 33cde1e1..606c787f 100644
--- a/docs/external-engine-configuration.md
+++ b/docs/external-engine-configuration.md
@@ -39,6 +39,7 @@ The following externalized T-engines properties are available:
| ACTIVEMQ_PASSWORD | ActiveMQ Password. | admin |
| FILE_STORE_URL | T-Engine Port. | http://localhost:8099/alfresco/api/-default-/private/sfs/versions/1/file |
| TRANSFORM_ENGINE_REQUEST_QUEUE | T-Engine queue used for async requests. | org.alfresco.transform.engine.misc.acs |
+| MISC_PDFBOX_DEFAULT_FONT | Default font used by PdfBox | NotoSans-Regular |
## Libreoffice
| Property | Description | Default value |
@@ -96,4 +97,5 @@ The following externalized T-engines properties are available:
| IMAGEMAGICK_DYN | Path to Imagemagick DYLD. | /usr/lib64/ImageMagick-7.0.10/lib |
| IMAGEMAGICK_EXE | Path to Imagemagick EXE. | /usr/bin/convert |
| IMAGEMAGICK_CODERS | Path to Imagemagick custom coders. | |
-| IMAGEMAGICK_CONFIG | Path to Imagemagick custom config. | |
\ No newline at end of file
+| IMAGEMAGICK_CONFIG | Path to Imagemagick custom config. | |
+| MISC_PDFBOX_DEFAULT_FONT | Default font used by PdfBox | NotoSans-Regular |
\ No newline at end of file
diff --git a/engines/aio/Dockerfile b/engines/aio/Dockerfile
index df305972..47fef57c 100644
--- a/engines/aio/Dockerfile
+++ b/engines/aio/Dockerfile
@@ -76,6 +76,7 @@ ADD target/generated-resources/licenses /licenses
ADD target/generated-resources/licenses.xml /licenses/
ADD target/generated-sources/license/THIRD-PARTY.txt /licenses/
COPY target/classes/licenses/3rd-party/ /
+COPY target/classes/fonts/NotoSans /usr/local/share/fonts/NotoSans
RUN groupadd -g ${GROUPID} ${GROUPNAME} && \
useradd -u ${USERID} -G ${GROUPNAME} ${AIOUSERNAME} && \
diff --git a/engines/aio/src/main/resources/application-default.yaml b/engines/aio/src/main/resources/application-default.yaml
index 51e31f8d..e6b06b77 100644
--- a/engines/aio/src/main/resources/application-default.yaml
+++ b/engines/aio/src/main/resources/application-default.yaml
@@ -24,3 +24,6 @@ transform:
exifTool:
windowsOS: 'exiftool -args -G1 -sep "|||" #{"$"}{INPUT}'
unixOS: 'env FOO=#{"$"}{OUTPUT} exiftool -args -G1 -sep "|||" #{"$"}{INPUT}'
+ misc:
+ pdfBox:
+ defaultFont: ${MISC_PDFBOX_DEFAULT_FONT:NotoSans-Regular}
\ No newline at end of file
diff --git a/engines/aio/src/test/java/org/alfresco/transform/aio/AIOTikaTest.java b/engines/aio/src/test/java/org/alfresco/transform/aio/AIOTikaTest.java
index a1007538..40b3068e 100644
--- a/engines/aio/src/test/java/org/alfresco/transform/aio/AIOTikaTest.java
+++ b/engines/aio/src/test/java/org/alfresco/transform/aio/AIOTikaTest.java
@@ -71,7 +71,9 @@ public class AIOTikaTest extends TikaTest
"startPage",
"targetEncoding",
"thumbnail",
- "width"
+ "width",
+ "pdfFont",
+ "pdfFontSize"
),
getOptionNames(controller.transformConfig(0).getBody().getTransformOptions()));
}
diff --git a/engines/misc/Dockerfile b/engines/misc/Dockerfile
index 863835b0..9085bc94 100644
--- a/engines/misc/Dockerfile
+++ b/engines/misc/Dockerfile
@@ -19,6 +19,7 @@ ADD target/generated-resources/licenses /licenses
ADD target/generated-resources/licenses.xml /licenses/
ADD target/generated-sources/license/THIRD-PARTY.txt /licenses/
COPY target/classes/licenses/3rd-party/ /
+COPY target/classes/fonts/NotoSans /usr/local/share/fonts/NotoSans
RUN groupadd -g ${GROUPID} ${GROUPNAME} && \
useradd -u ${USERID} -G ${GROUPNAME} ${MISCUSERNAME} && \
diff --git a/engines/misc/LICENSES.md b/engines/misc/LICENSES.md
index 3fe03d83..8c7aeeaf 100644
--- a/engines/misc/LICENSES.md
+++ b/engines/misc/LICENSES.md
@@ -7,3 +7,4 @@
* commons-compress, PDFBox and poi-ooxml are from Apache. See the license at http://www.apache.org/licenses/LICENSE-2.0 or the
[Apache 2.0.txt](src/main/resources/licenses/3rd-party/Apache%202.0.txt)
file placed in the root directory of the docker image.
+* NotoSans https://openfontlicense.org/open-font-license-official-text/
\ No newline at end of file
diff --git a/engines/misc/src/main/java/org/alfresco/transform/misc/transformers/TextToPdfContentTransformer.java b/engines/misc/src/main/java/org/alfresco/transform/misc/transformers/TextToPdfContentTransformer.java
index 77ad0321..c8643d0d 100644
--- a/engines/misc/src/main/java/org/alfresco/transform/misc/transformers/TextToPdfContentTransformer.java
+++ b/engines/misc/src/main/java/org/alfresco/transform/misc/transformers/TextToPdfContentTransformer.java
@@ -26,16 +26,10 @@
*/
package org.alfresco.transform.misc.transformers;
-import org.alfresco.transform.base.TransformManager;
-import org.alfresco.transform.base.util.CustomTransformerFileAdaptor;
-import org.apache.pdfbox.pdmodel.PDDocument;
-import org.apache.pdfbox.pdmodel.PDPage;
-import org.apache.pdfbox.pdmodel.PDPageContentStream;
-import org.apache.pdfbox.pdmodel.font.PDType1Font;
-import org.apache.pdfbox.tools.TextToPDF;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.stereotype.Component;
+import static org.alfresco.transform.common.RequestParamMap.PAGE_LIMIT;
+import static org.alfresco.transform.common.RequestParamMap.PDF_FONT;
+import static org.alfresco.transform.common.RequestParamMap.PDF_FONT_SIZE;
+import static org.alfresco.transform.common.RequestParamMap.SOURCE_ENCODING;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
@@ -48,12 +42,31 @@ import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.io.Reader;
+import java.net.URI;
import java.nio.charset.Charset;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
-import static org.alfresco.transform.common.RequestParamMap.PAGE_LIMIT;
-import static org.alfresco.transform.common.RequestParamMap.SOURCE_ENCODING;
+import org.alfresco.transform.base.TransformManager;
+import org.alfresco.transform.base.util.CustomTransformerFileAdaptor;
+import org.apache.fontbox.ttf.TrueTypeFont;
+import org.apache.fontbox.util.autodetect.FontFileFinder;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDPageContentStream;
+import org.apache.pdfbox.pdmodel.font.FontMappers;
+import org.apache.pdfbox.pdmodel.font.FontMapping;
+import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.font.PDType0Font;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.pdfbox.tools.TextToPDF;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import jakarta.annotation.PostConstruct;
/**
*
@@ -77,20 +90,30 @@ public class TextToPdfContentTransformer implements CustomTransformerFileAdaptor
private static final byte EF = (byte) 0xEF;
private static final byte BB = (byte) 0xBB;
private static final byte BF = (byte) 0xBF;
-
+ private static final String DEFAULT_FONT = "NotoSans-Regular";
+ private static final int DEFAULT_FONT_SIZE = 10;
private final PagedTextToPDF transformer;
+ @Value("${transform.core.misc.pdfBox.defaultFont:NotoSans-Regular}")
+ private String pdfBoxDefaultFont;
+
public TextToPdfContentTransformer()
{
transformer = new PagedTextToPDF();
}
+ @PostConstruct
+ public void init()
+ {
+ transformer.setDefaultFont(pdfBoxDefaultFont);
+ }
+
public void setStandardFont(String fontName)
{
try
{
- transformer.setFont(PagedTextToPDF.getStandardFont(fontName));
+ transformer.setFont(fontName);
}
catch (Throwable e)
{
@@ -112,6 +135,11 @@ public class TextToPdfContentTransformer implements CustomTransformerFileAdaptor
}
}
+ public String getUsedFont()
+ {
+ return transformer.getFontName();
+ }
+
@Override
public String getTransformerName()
{
@@ -130,6 +158,25 @@ public class TextToPdfContentTransformer implements CustomTransformerFileAdaptor
{
pageLimit = parseInt(stringPageLimit, PAGE_LIMIT);
}
+ String pdfFont = transformOptions.get(PDF_FONT);
+ if (pdfFont == null || pdfFont.isBlank())
+ {
+ pdfFont = pdfBoxDefaultFont;
+ }
+ String pdfFontSize = transformOptions.get(PDF_FONT_SIZE);
+ Integer fontSize = null;
+ if (pdfFontSize != null && !pdfFontSize.isBlank())
+ {
+ try
+ {
+ fontSize = parseInt(pdfFontSize, PDF_FONT_SIZE);
+ }
+ catch (Exception e)
+ {
+ fontSize = DEFAULT_FONT_SIZE;
+ logger.error("Error parsing font size {}, going to set it as {}", pdfFontSize, fontSize, e);
+ }
+ }
PDDocument pdf = null;
try (InputStream is = new FileInputStream(sourceFile);
@@ -138,7 +185,7 @@ public class TextToPdfContentTransformer implements CustomTransformerFileAdaptor
{
//TransformationOptionLimits limits = getLimits(reader, writer, options);
//TransformationOptionPair pageLimits = limits.getPagesPair();
- pdf = transformer.createPDFFromText(ir, pageLimit);
+ pdf = transformer.createPDFFromText(ir, pageLimit, pdfFont, fontSize);
pdf.save(os);
}
finally
@@ -231,22 +278,34 @@ public class TextToPdfContentTransformer implements CustomTransformerFileAdaptor
}
//duplicating until here
+ private String fontName = null;
+ private String defaultFont = null;
+
// The following code is based on the code in TextToPDF with the addition of
// checks for page limits.
// The calling code must close the PDDocument once finished with it.
- public PDDocument createPDFFromText(Reader text, int pageLimit)
+ public PDDocument createPDFFromText(Reader text, int pageLimit, String pdfFontName, Integer pdfFontSize)
throws IOException
{
PDDocument doc = null;
int pageCount = 0;
try
{
+ doc = new PDDocument();
+
+ final PDFont font = getFont(doc, pdfFontName);
+ final int fontSize = pdfFontSize != null ? pdfFontSize : getFontSize();
+
+ fontName = font.getName();
+
+ logger.debug("Going to use font {} with size {}", fontName, fontSize);
+
final int margin = 40;
- float height = getFont().getFontDescriptor().getFontBoundingBox().getHeight() / 1000;
+ float height = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000;
//calculate font height and increase by 5 percent.
- height = height * getFontSize() * 1.05f;
- doc = new PDDocument();
+ height = height * fontSize * 1.05f;
+
BufferedReader data = (text instanceof BufferedReader) ? (BufferedReader) text : new BufferedReader(text);
String nextLine;
PDPage page = new PDPage();
@@ -280,8 +339,8 @@ public class TextToPdfContentTransformer implements CustomTransformerFileAdaptor
{
String lineWithNextWord = nextLineToDraw.toString() + lineWords[lineIndex];
lengthIfUsingNextWord =
- (getFont().getStringWidth(
- lineWithNextWord) / 1000) * getFontSize();
+ (font.getStringWidth(
+ lineWithNextWord) / 1000) * fontSize;
}
}
while (lineIndex < lineWords.length &&
@@ -304,7 +363,7 @@ public class TextToPdfContentTransformer implements CustomTransformerFileAdaptor
contentStream.close();
}
contentStream = new PDPageContentStream(doc, page);
- contentStream.setFont(getFont(), getFontSize());
+ contentStream.setFont(font, fontSize);
contentStream.beginText();
y = page.getMediaBox().getHeight() - margin + height;
contentStream.moveTextPositionByAmount(margin, y);
@@ -344,6 +403,199 @@ public class TextToPdfContentTransformer implements CustomTransformerFileAdaptor
}
return doc;
}
+
+ public void setFont(String aFontName)
+ {
+ PDType1Font font = PagedTextToPDF.getStandardFont(aFontName);
+
+ if (font != null)
+ {
+ super.setFont(font);
+ this.fontName = aFontName;
+ }
+ }
+
+ /**
+ * Gets the font that will be used in document transformation using the following approaches:
+ *
+ * - Standard font map
+ *
- Font Mappers
+ *
- File system fonts
+ *
- Transformer default font
+ *
- PdfBox default font
+ *
+ *
+ * @param doc
+ * the document that will be transformed
+ * @param fontName
+ * the font name that will be used in transformation
+ *
+ * @return the font that was found
+ */
+ private PDFont getFont(PDDocument doc, String fontName)
+ {
+ if (fontName == null)
+ {
+ fontName = fontName != null ? fontName : getDefaultFont();
+ }
+
+ // First, it tries to get the font from PdfBox STANDARD_14 map
+ PDFont font = getFromStandardFonts(fontName);
+
+ // If not found, tries to get the font from FontMappers
+ if (font == null)
+ {
+ font = getFromFontMapper(fontName, doc);
+
+ // If still not found, tries to get the font from file system
+ if (font == null)
+ {
+ font = getFromFileSystem(fontName);
+
+ // If font is still null:
+ // - it will recursively get the transformer default font
+ // - Otherwise, it will use the PdfBox default font (Helvetica)
+ if (font == null)
+ {
+ if (defaultFont != null && !fontName.equals(defaultFont))
+ {
+ font = getFont(doc, defaultFont);
+ }
+ else
+ {
+ font = getFont();
+ }
+ }
+ }
+
+ }
+
+ return font;
+ }
+
+ /**
+ * Gets the font from PdfBox standard fonts map
+ *
+ * @param fontName
+ * the font name to obtain
+ *
+ * @return the font object that has been found, otherwise null
+ */
+ private PDFont getFromStandardFonts(String fontName)
+ {
+ return PagedTextToPDF.getStandardFont(fontName);
+ }
+
+ /**
+ * Gets the font from {@link FontMappers} instance
+ *
+ * @param fontName
+ * the font name to obtain
+ * @param doc
+ * the PDF document
+ *
+ * @return the font object that has been found, otherwise null
+ */
+ private PDFont getFromFontMapper(String fontName, PDDocument doc)
+ {
+ PDFont font = null;
+ FontMapping mapping = FontMappers.instance().getTrueTypeFont(fontName, null);
+
+ if (mapping != null && mapping.getFont() != null && !mapping.isFallback())
+ {
+ try
+ {
+ font = PDType0Font.load(doc, mapping.getFont().getOriginalData());
+ }
+ catch (Exception e)
+ {
+ logger.error("Error loading font mapping {}", fontName, e);
+ }
+ }
+
+ return font;
+ }
+
+ /**
+ * Gets the font from existing file system fonts
+ *
+ * @param fontName
+ * the font name to obtain
+ * @return the font object that has been found, otherwise null
+ */
+ private PDFont getFromFileSystem(String fontName)
+ {
+ PDFont font = null;
+ String nameWithExtension = fontName + ".ttf";
+
+ FontFileFinder fontFileFinder = new FontFileFinder();
+ List uris = fontFileFinder.find();
+
+ for (URI uri : uris)
+ {
+ if (uri.getPath().contains(nameWithExtension))
+ {
+ InputStream fontIS = null;
+ try
+ {
+ fontIS = new FileInputStream(new File(uri));
+ if (null != fontIS)
+ {
+ PDDocument documentMock = new PDDocument();
+ font = PDType0Font.load(documentMock, fontIS);
+ break;
+ }
+ }
+ catch (IOException ioe)
+ {
+ logger.error("Error loading font {} from filesystem", fontName, ioe);
+ }
+ finally
+ {
+ if (fontIS != null)
+ {
+ try
+ {
+ fontIS.close();
+ }
+ catch (Exception e)
+ {
+ logger.error("Error closing font inputstream", e);
+ }
+ }
+ }
+ }
+ }
+
+ return font;
+ }
+
+ public String getFontName()
+ {
+ return this.fontName;
+ }
+
+ public String getDefaultFont()
+ {
+ if (defaultFont == null || defaultFont.isBlank())
+ {
+ return TextToPdfContentTransformer.DEFAULT_FONT;
+ }
+
+ return defaultFont;
+ }
+
+ public void setDefaultFont(String name)
+ {
+ if (name == null || name.isBlank())
+ {
+ defaultFont = TextToPdfContentTransformer.DEFAULT_FONT;
+ }
+ else
+ {
+ this.defaultFont = name;
+ }
+ }
}
private int parseInt(String s, String paramName)
diff --git a/engines/misc/src/main/resources/application-default.yaml b/engines/misc/src/main/resources/application-default.yaml
index 62e91ec6..54cc2404 100644
--- a/engines/misc/src/main/resources/application-default.yaml
+++ b/engines/misc/src/main/resources/application-default.yaml
@@ -1,2 +1,7 @@
queue:
engineRequestQueue: ${TRANSFORM_ENGINE_REQUEST_QUEUE:org.alfresco.transform.engine.misc.acs}
+transform:
+ core:
+ misc:
+ pdfBox:
+ defaultFont: ${MISC_PDFBOX_DEFAULT_FONT:NotoSans-Regular}
\ No newline at end of file
diff --git a/engines/misc/src/main/resources/fonts/NotoSans/NotoSans-Bold.ttf b/engines/misc/src/main/resources/fonts/NotoSans/NotoSans-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..d84248ed106af3688ac9eae31578e0493c73b069
GIT binary patch
literal 582604
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