Skip to content

Commit cc71fe7

Browse files
Initial PDFBox HTML support
1 parent c42808f commit cc71fe7

File tree

2 files changed

+235
-3
lines changed

2 files changed

+235
-3
lines changed

java/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@
122122
<version>2.0.27</version>
123123
</dependency>
124124
<dependency>
125+
<groupId>org.jsoup</groupId>
126+
<artifactId>jsoup</artifactId>
127+
<version>1.16.1</version>
128+
</dependency>
129+
<dependency>
125130
<groupId>com.google.zxing</groupId>
126131
<artifactId>core</artifactId>
127132
<version>3.5.1</version>

java/src/main/java/com/genexus/reports/PDFReportPDFBox.java

Lines changed: 230 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import java.io.*;
66
import java.net.URL;
77
import java.util.ArrayList;
8+
import java.util.HashSet;
89
import java.util.List;
10+
import java.util.Set;
911
import java.util.concurrent.ConcurrentHashMap;
1012

1113
import com.genexus.CommonUtil;
@@ -19,11 +21,14 @@
1921
import com.google.zxing.BarcodeFormat;
2022
import com.google.zxing.common.BitMatrix;
2123
import com.google.zxing.oned.Code128Writer;
24+
2225
import org.apache.pdfbox.cos.*;
2326
import org.apache.pdfbox.io.IOUtils;
2427
import org.apache.pdfbox.pdmodel.*;
2528
import org.apache.pdfbox.pdmodel.common.PDRectangle;
2629
import org.apache.pdfbox.pdmodel.font.*;
30+
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
31+
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
2732
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
2833
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
2934
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
@@ -32,6 +37,12 @@
3237
import org.apache.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences;
3338
import org.apache.pdfbox.util.Matrix;
3439

40+
import org.jsoup.Jsoup;
41+
import org.jsoup.nodes.Document;
42+
import org.jsoup.nodes.Element;
43+
import org.jsoup.nodes.Node;
44+
import org.jsoup.select.Elements;
45+
3546
public class PDFReportPDFBox extends GXReportPDFCommons{
3647
private PDRectangle pageSize;
3748
private PDFont baseFont;
@@ -49,6 +60,10 @@ public class PDFReportPDFBox extends GXReportPDFCommons{
4960
public int runDirection = 0;
5061
private int page;
5162

63+
private final float DEFAULT_PDFBOX_LEADING = 1.2f;
64+
65+
private Set<String> supportedHTMLTags = new HashSet<>();
66+
5267
static {
5368
log = org.apache.logging.log4j.LogManager.getLogger(PDFReportPDFBox.class);
5469
}
@@ -594,7 +609,38 @@ else if (valign == PDFReportPDFBox.VerticalAlign.BOTTOM.value())
594609
boolean autoResize = (align & 256) == 256;
595610

596611
if (htmlformat == 1) {
597-
//As for now, HTML printing is not supported
612+
log.debug("WARNING: HTML rendering is not natively supported by PDFBOX 2.0.27. Handcrafted support is provided but it is not intended to cover all possible use cases");
613+
try {
614+
bottomAux = (float)convertScale(bottom);
615+
topAux = (float)convertScale(top);
616+
float drawingPageHeight = this.pageSize.getUpperRightY() - topMargin - bottomMargin;
617+
618+
float llx = leftAux + leftMargin;
619+
float lly = drawingPageHeight - bottomAux;
620+
float urx = rightAux + leftMargin;
621+
float ury = drawingPageHeight - topAux;
622+
623+
// Define the rectangle where the content will be displayed
624+
PDRectangle htmlRectangle = new PDRectangle();
625+
htmlRectangle.setLowerLeftX(llx);
626+
htmlRectangle.setLowerLeftY(lly);
627+
htmlRectangle.setUpperRightX(urx);
628+
htmlRectangle.setUpperRightY(ury);
629+
cb.addRect(llx, lly, urx - llx, ury - lly);
630+
cb.stroke();
631+
SpaceHandler spaceHandler = new SpaceHandler(htmlRectangle.getUpperRightY(), htmlRectangle.getHeight());
632+
633+
loadSupportedHTMLTags();
634+
635+
Document document = Jsoup.parse(sTxt);
636+
Elements allElements = document.getAllElements();
637+
for (Element element : allElements)
638+
if (this.supportedHTMLTags.contains(element.normalName()))
639+
processHTMLElement(cb, htmlRectangle, spaceHandler, element);
640+
641+
} catch (Exception e) {
642+
log.error("GxDrawText failed to print HTML text : ", e);
643+
}
598644
}
599645
else
600646
if (barcodeType != null){
@@ -801,11 +847,191 @@ else if (valign == PDFReportPDFBox.VerticalAlign.BOTTOM.value())
801847
}
802848
}
803849
}
804-
} catch (IOException ioe){
850+
} catch (Exception ioe){
805851
log.error("GxDrawText failed: ", ioe);
806852
}
807853
}
808854

855+
private void loadSupportedHTMLTags(){
856+
this.supportedHTMLTags.add("p");
857+
this.supportedHTMLTags.add("ol");
858+
this.supportedHTMLTags.add("ul");
859+
this.supportedHTMLTags.add("div");
860+
this.supportedHTMLTags.add("h1");
861+
this.supportedHTMLTags.add("h2");
862+
this.supportedHTMLTags.add("h3");
863+
this.supportedHTMLTags.add("img");
864+
this.supportedHTMLTags.add("a");
865+
}
866+
867+
private void processHTMLElement(PDPageContentStream cb, PDRectangle htmlRectangle, SpaceHandler spaceHandler, Element blockElement) throws Exception{
868+
String tagName = blockElement.normalName();
869+
PDFont htmlFont = PDType1Font.TIMES_ROMAN;
870+
871+
if (tagName.equals("div")) {
872+
for (Node child : blockElement.childNodes())
873+
if (child instanceof Element)
874+
processHTMLElement(cb, htmlRectangle, spaceHandler, (Element) child);
875+
}
876+
877+
if (spaceHandler.getAvailableSpace() <= 0){
878+
log.error("You ran out of available space while rendering HTML");
879+
return;
880+
}
881+
882+
float lineHeight = (PDType1Font.TIMES_ROMAN.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize) * DEFAULT_PDFBOX_LEADING;
883+
float leading = (float)(Double.valueOf(props.getGeneralProperty(Const.LEADING)).doubleValue());
884+
885+
float llx = htmlRectangle.getLowerLeftX();
886+
float lly = htmlRectangle.getLowerLeftY();
887+
float urx = htmlRectangle.getUpperRightX();
888+
889+
float fontSize = 16f; // Default font size for the HTML <p> tag
890+
cb.setFont(htmlFont, 16f);
891+
this.fontBold = false;
892+
if (tagName.equals("h1")){
893+
cb.setFont(htmlFont, 32f);
894+
fontSize = 32f;
895+
tagName = "h";
896+
} else if (tagName.equals("h2")){
897+
cb.setFont(htmlFont, 24f);
898+
fontSize = 24f;
899+
tagName = "h";
900+
} else if (tagName.equals("h3")){
901+
cb.setFont(htmlFont, 18.72f);
902+
fontSize = 18.72f;
903+
tagName = "h";
904+
} else if (tagName.equals("h4")){
905+
cb.setFont(htmlFont, 16f);
906+
fontSize = 16.5f;
907+
tagName = "h";
908+
}
909+
if (tagName.equals("h")){
910+
this.fontBold = true;
911+
float lines = renderHTMLContent(cb, blockElement.text(), fontSize, llx, lly, urx, spaceHandler.getCurrentYPosition());
912+
float totalTextHeight = lineHeight * lines * DEFAULT_PDFBOX_LEADING * leading;
913+
spaceHandler.setCurrentYPosition(spaceHandler.getCurrentYPosition() - totalTextHeight);
914+
} else if (tagName.equals("p")) {
915+
float lines = renderHTMLContent(cb, blockElement.text(), fontSize, llx, lly, urx, spaceHandler.getCurrentYPosition());
916+
float totalTextHeight = lineHeight * lines * DEFAULT_PDFBOX_LEADING * leading;
917+
spaceHandler.setCurrentYPosition(spaceHandler.getCurrentYPosition() - totalTextHeight);
918+
} else if (tagName.equals("ul") || tagName.equals("ol")){
919+
int i = 0;
920+
for (Element listItem : blockElement.select("li")){
921+
String text = (tagName.equals("ul")) ? "• " + listItem.text() : i + ". " + listItem.text();
922+
i++;
923+
float lines = renderHTMLContent(cb, text, fontSize, llx, lly, urx, spaceHandler.getCurrentYPosition());
924+
float totalTextHeight = lineHeight * lines * DEFAULT_PDFBOX_LEADING;
925+
spaceHandler.setCurrentYPosition(spaceHandler.getCurrentYPosition() - totalTextHeight);
926+
}
927+
} else if (tagName.equals("a")){
928+
cb.setNonStrokingColor(new Color(0, 0, 255));
929+
float lines = renderHTMLContent(cb, blockElement.attr("href"), fontSize, llx, lly, urx, spaceHandler.getCurrentYPosition());
930+
float totalTextHeight = lineHeight * lines * DEFAULT_PDFBOX_LEADING * leading;
931+
spaceHandler.setCurrentYPosition(spaceHandler.getCurrentYPosition() - totalTextHeight);
932+
cb.setStrokingColor(new Color(0, 0, 0));
933+
} else if (tagName.equals("img")){
934+
String bitmap = blockElement.attr("src");
935+
PDImageXObject image;
936+
937+
try {
938+
if (!NativeFunctions.isWindows() && new File(bitmap).isAbsolute() && bitmap.startsWith(httpContext.getStaticContentBase()))
939+
bitmap = bitmap.replace(httpContext.getStaticContentBase(), "");
940+
if (!new File(bitmap).isAbsolute() && !bitmap.toLowerCase().startsWith("http:") && !bitmap.toLowerCase().startsWith("https:")) {
941+
if (bitmap.startsWith(httpContext.getStaticContentBase()))
942+
bitmap = bitmap.replace(httpContext.getStaticContentBase(), "");
943+
image = PDImageXObject.createFromFile(defaultRelativePrepend + bitmap,document);
944+
if(image == null) {
945+
bitmap = webAppDir + bitmap;
946+
image = PDImageXObject.createFromFile(bitmap,document);
947+
}
948+
else
949+
bitmap = defaultRelativePrepend + bitmap;
950+
}
951+
else
952+
image = PDImageXObject.createFromFile(bitmap,document);
953+
} catch(java.lang.IllegalArgumentException | FileNotFoundException e) {
954+
URL url= new java.net.URL(bitmap);
955+
image = PDImageXObject.createFromByteArray(document, IOUtils.toByteArray(url.openStream()),bitmap);
956+
}
957+
cb.drawImage(image, llx, spaceHandler.getCurrentYPosition() - image.getHeight(), image.getWidth(), image.getHeight());
958+
spaceHandler.setCurrentYPosition(spaceHandler.getCurrentYPosition() - image.getHeight() - 10f);
959+
}
960+
961+
float availableSpace = spaceHandler.getCurrentYPosition() - lly;
962+
spaceHandler.setAvailableSpace(availableSpace);
963+
}
964+
965+
private class SpaceHandler {
966+
float currentYPosition;
967+
float availableSpace;
968+
969+
public SpaceHandler(float currentYPosition, float availableSpace) {
970+
this.currentYPosition = currentYPosition;
971+
this.availableSpace = availableSpace;
972+
}
973+
974+
public float getCurrentYPosition() {
975+
return currentYPosition;
976+
}
977+
978+
public void setCurrentYPosition(float currentYPosition) {
979+
this.currentYPosition = currentYPosition;
980+
}
981+
982+
public float getAvailableSpace() {
983+
return availableSpace;
984+
}
985+
986+
public void setAvailableSpace(float availableSpace) {
987+
this.availableSpace = availableSpace;
988+
}
989+
}
990+
991+
private float renderHTMLContent(PDPageContentStream contentStream, String text, float fontSize, float llx, float lly, float urx, float ury) {
992+
try {
993+
PDFont defaultHTMLFont = PDType1Font.TIMES_ROMAN;
994+
List<String> lines = new ArrayList<>();
995+
String[] words = text.split(" ");
996+
StringBuilder currentLine = new StringBuilder();
997+
for (String word : words) {
998+
float currentLineWidth = defaultHTMLFont.getStringWidth(currentLine + " " + word) / 1000 * fontSize;
999+
if (currentLineWidth < urx - llx) {
1000+
if (currentLine.length() > 0) {
1001+
currentLine.append(" ");
1002+
}
1003+
currentLine.append(word);
1004+
} else {
1005+
lines.add(currentLine.toString());
1006+
currentLine.setLength(0);
1007+
currentLine.append(word);
1008+
}
1009+
}
1010+
lines.add(currentLine.toString());
1011+
1012+
float leading = lines.size() == 1 ? fontSize : DEFAULT_PDFBOX_LEADING * fontSize;
1013+
float startY = ury;
1014+
1015+
if (fontSize > 16f){
1016+
contentStream.setLineWidth(fontSize * 0.05f);
1017+
contentStream.setRenderingMode(RenderingMode.FILL_STROKE);
1018+
}
1019+
contentStream.beginText();
1020+
float lineHeight = (defaultHTMLFont.getFontDescriptor().getFontBoundingBox().getUpperRightY() - defaultHTMLFont.getFontDescriptor().getFontBoundingBox().getLowerLeftY())/ 1000 * fontSize;
1021+
contentStream.newLineAtOffset(llx, startY);
1022+
for (String line : lines) {
1023+
contentStream.showText(line);
1024+
startY = startY - leading - lineHeight;
1025+
contentStream.newLineAtOffset(0, startY);
1026+
}
1027+
contentStream.endText();
1028+
return lines.size();
1029+
} catch (IOException ioe) {
1030+
log.error("failed to draw wrapped text: ", ioe);
1031+
return -1;
1032+
}
1033+
}
1034+
8091035
private void resolveTextStyling(PDPageContentStream contentStream, String text, float x, float y, boolean isWrapped){
8101036
try {
8111037
if (this.fontBold && this.fontItalic){
@@ -836,6 +1062,7 @@ private void resolveTextStyling(PDPageContentStream contentStream, String text,
8361062
contentStream.endText();
8371063
contentStream.setLineWidth(1f); // Default line width for PDFBox 2.0.27
8381064
contentStream.setRenderingMode(RenderingMode.FILL); // Default text rendering mode for PDFBox 2.0.27
1065+
contentStream.moveTo(x,y);
8391066
} catch (IOException ioe) {
8401067
log.error("failed to apply text styling: ", ioe);
8411068
}
@@ -861,7 +1088,7 @@ private void showWrappedTextAligned(PDPageContentStream contentStream, PDFont fo
8611088
}
8621089
lines.add(currentLine.toString());
8631090

864-
float leading = lines.size() == 1 ? fontSize : 1.2f * fontSize;
1091+
float leading = lines.size() == 1 ? fontSize : DEFAULT_PDFBOX_LEADING * fontSize;
8651092
float totalTextHeight = fontSize * lines.size() + leading * (lines.size() - 1);
8661093
float startY = lines.size() == 1 ? lly + (ury - lly - totalTextHeight) / 2 : lly + (ury - lly - totalTextHeight) / 2 + (lines.size() - 1) * (fontSize + leading) + font.getFontDescriptor().getDescent() / 1000 * fontSize;
8671094

0 commit comments

Comments
 (0)