From a48753c8e35655782ed0747a8fb3e49fc47a31e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Sexenian?= <99925035+tomas-sexenian@users.noreply.github.com> Date: Wed, 18 Dec 2024 00:54:19 +0700 Subject: [PATCH] Pdf box memory usage (#918) * Encode with UTF-8 if getString has funny characters * Fix streams concurrency issues Issue:202643 * Bump PDFBox to 3.0.3 and improve memory usage Issue:202734 * Fix font loading --------- Co-authored-by: iroqueta <46004974+iroqueta@users.noreply.github.com> --- java/pom.xml | 2 +- .../genexus/internet/HttpClientJavaLib.java | 17 +- .../com/genexus/reports/PDFReportPDFBox.java | 213 ++++++++++-------- 3 files changed, 123 insertions(+), 109 deletions(-) diff --git a/java/pom.xml b/java/pom.xml index 82af66eff..d02843cf3 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -120,7 +120,7 @@ org.apache.pdfbox pdfbox - 2.0.27 + 3.0.3 org.jsoup diff --git a/java/src/main/java/com/genexus/internet/HttpClientJavaLib.java b/java/src/main/java/com/genexus/internet/HttpClientJavaLib.java index 608ed9285..9927fd1f7 100644 --- a/java/src/main/java/com/genexus/internet/HttpClientJavaLib.java +++ b/java/src/main/java/com/genexus/internet/HttpClientJavaLib.java @@ -718,23 +718,16 @@ public String getString() { return ""; try { this.setEntity(); - ContentType contentType = ContentType.getOrDefault(entity); - Charset charset; - if (contentType.equals(ContentType.DEFAULT_TEXT)) { - charset = StandardCharsets.UTF_8; - } else { - charset = contentType.getCharset(); - if (charset == null) { - charset = StandardCharsets.UTF_8; - } - } + Charset charset = ContentType.getOrDefault(entity).getCharset(); String res = EntityUtils.toString(entity, charset); + if (res.matches(".*[Ã-ÿ].*")) { + res = EntityUtils.toString(entity, StandardCharsets.UTF_8); + } eof = true; return res; } catch (IOException e) { setExceptionsCatch(e); - } catch (IllegalArgumentException e) { - } + } catch (IllegalArgumentException e) {} return ""; } diff --git a/java/src/main/java/com/genexus/reports/PDFReportPDFBox.java b/java/src/main/java/com/genexus/reports/PDFReportPDFBox.java index 0522cae00..f3ed4e8a3 100644 --- a/java/src/main/java/com/genexus/reports/PDFReportPDFBox.java +++ b/java/src/main/java/com/genexus/reports/PDFReportPDFBox.java @@ -4,10 +4,7 @@ import java.awt.image.BufferedImage; import java.io.*; import java.net.URL; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import com.genexus.ApplicationContext; @@ -28,7 +25,6 @@ import org.apache.pdfbox.pdmodel.*; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.*; -import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode; @@ -61,10 +57,9 @@ public class PDFReportPDFBox extends GXReportPDFCommons{ ConcurrentHashMap documentImages; public int runDirection = 0; private int page; - private final float DEFAULT_PDFBOX_LEADING = 1.2f; - private Set supportedHTMLTags = new HashSet<>(); + private PDPageContentStream currentPageContentStream; static { log = org.apache.logging.log4j.LogManager.getLogger(PDFReportPDFBox.class); @@ -86,10 +81,6 @@ protected void init() { } } - private PDPageContentStream getNewPDPageContentStream() throws IOException { - return new PDPageContentStream(document, document.getPage(page - 1), PDPageContentStream.AppendMode.APPEND,false); - } - private void drawRectangle(PDPageContentStream cb, float x, float y, float w, float h, int styleTop, int styleBottom, int styleRight, int styleLeft, float radioTL, float radioTR, float radioBL, float radioBR, float penAux, boolean hideCorners) { @@ -228,41 +219,40 @@ private void roundRectangle(PDPageContentStream cb, float x, float y, float w, f public void GxDrawRect(int left, int top, int right, int bottom, int pen, int foreRed, int foreGreen, int foreBlue, int backMode, int backRed, int backGreen, int backBlue, int styleTop, int styleBottom, int styleRight, int styleLeft, int cornerRadioTL, int cornerRadioTR, int cornerRadioBL, int cornerRadioBR) { - try (PDPageContentStream cb = getNewPDPageContentStream()){ - - float penAux = (float)convertScale(pen); - float rightAux = (float)convertScale(right); - float bottomAux = (float)convertScale(bottom); - float leftAux = (float)convertScale(left); - float topAux = (float)convertScale(top); + try { + PDPageContentStream cb = currentPageContentStream; + float penAux = (float) convertScale(pen); + float rightAux = (float) convertScale(right); + float bottomAux = (float) convertScale(bottom); + float leftAux = (float) convertScale(left); + float topAux = (float) convertScale(top); cb.saveGraphicsState(); float x1, y1, x2, y2; x1 = leftAux + leftMargin; - y1 = pageSize.getUpperRightY() - bottomAux - topMargin -bottomMargin; + y1 = pageSize.getUpperRightY() - bottomAux - topMargin - bottomMargin; x2 = rightAux + leftMargin; - y2 = pageSize.getUpperRightY() - topAux - topMargin -bottomMargin; + y2 = pageSize.getUpperRightY() - topAux - topMargin - bottomMargin; cb.setLineWidth(penAux); cb.setLineCapStyle(2); - if (cornerRadioBL==0 && cornerRadioBR==0 && cornerRadioTL==0 && cornerRadioTR==0 && styleBottom==0 && styleLeft==0 && styleRight==0 && styleTop==0) { + if (cornerRadioBL == 0 && cornerRadioBR == 0 && cornerRadioTL == 0 && cornerRadioTR == 0 && styleBottom == 0 && styleLeft == 0 && styleRight == 0 && styleTop == 0) { if (pen > 0) cb.setStrokingColor(foreRed, foreGreen, foreBlue); else - cb.setStrokingColor (backRed, backGreen, backBlue); + cb.setStrokingColor(backRed, backGreen, backBlue); cb.addRect(x1, y1, x2 - x1, y2 - y1); - if (backMode!=0) { + if (backMode != 0) { cb.setNonStrokingColor(new Color(backRed, backGreen, backBlue)); cb.fillAndStroke(); } cb.closePath(); cb.stroke(); - } - else { + } else { float w = x2 - x1; float h = y2 - y1; if (w < 0) { @@ -274,18 +264,18 @@ public void GxDrawRect(int left, int top, int right, int bottom, int pen, int fo h = -h; } - float cRadioTL = (float)convertScale(cornerRadioTL); - float cRadioTR = (float)convertScale(cornerRadioTR); - float cRadioBL = (float)convertScale(cornerRadioBL); - float cRadioBR = (float)convertScale(cornerRadioBR); + float cRadioTL = (float) convertScale(cornerRadioTL); + float cRadioTR = (float) convertScale(cornerRadioTR); + float cRadioBL = (float) convertScale(cornerRadioBL); + float cRadioBR = (float) convertScale(cornerRadioBR); - int max = (int)Math.min(w, h); - cRadioTL = Math.max(0, Math.min(cRadioTL, max/2)); - cRadioTR = Math.max(0, Math.min(cRadioTR, max/2)); - cRadioBL = Math.max(0, Math.min(cRadioBL, max/2)); - cRadioBR = Math.max(0, Math.min(cRadioBR, max/2)); + int max = (int) Math.min(w, h); + cRadioTL = Math.max(0, Math.min(cRadioTL, max / 2)); + cRadioTR = Math.max(0, Math.min(cRadioTR, max / 2)); + cRadioBL = Math.max(0, Math.min(cRadioBL, max / 2)); + cRadioBR = Math.max(0, Math.min(cRadioBR, max / 2)); - if (backMode!=0) { + if (backMode != 0) { cb.setStrokingColor(backRed, backGreen, backBlue); cb.setLineWidth(0); roundRectangle(cb, x1, y1, w, h, @@ -312,7 +302,7 @@ public void GxDrawRect(int left, int top, int right, int bottom, int pen, int fo } public void GxDrawLine(int left, int top, int right, int bottom, int width, int foreRed, int foreGreen, int foreBlue, int style) { - try (PDPageContentStream cb = getNewPDPageContentStream()){ + try (PDPageContentStream cb = currentPageContentStream){ float widthAux = (float)convertScale(width); float rightAux = (float)convertScale(right); @@ -351,7 +341,7 @@ public void GxDrawLine(int left, int top, int right, int bottom, int width, int } public void GxDrawBitMap(String bitmap, int left, int top, int right, int bottom, int aspectRatio) { - try (PDPageContentStream cb = getNewPDPageContentStream()){ + try (PDPageContentStream cb = currentPageContentStream){ PDImageXObject image; try { if (documentImages != null && documentImages.containsKey(bitmap)) { @@ -417,6 +407,14 @@ public void GxDrawBitMap(String bitmap, int left, int top, int right, int bottom } } + private static final int MAX_FONT_CACHE_SIZE = 50; + private final Map fontCache = new LinkedHashMap(MAX_FONT_CACHE_SIZE, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_FONT_CACHE_SIZE; + } + }; + public void GxAttris(String fontName, int fontSize, boolean fontBold, boolean fontItalic, boolean fontUnderline, boolean fontStrikethru, int Pen, int foreRed, int foreGreen, int foreBlue, int backMode, int backRed, int backGreen, int backBlue) { boolean isCJK = false; boolean embeedFont = isEmbeddedFont(fontName); @@ -482,10 +480,8 @@ public void GxAttris(String fontName, int fontSize, boolean fontBold, boolean fo } } baseFont = createPDType1FontFromName(fontName); - if (baseFont != null) - baseFontName = baseFont.getName(); if (baseFont == null){ - baseFont = PDType0Font.load(document, new File(getFontLocation(fontName))); + baseFont = getOrLoadFont(fontName, document); baseFontName = fontName; } @@ -506,19 +502,20 @@ public void GxAttris(String fontName, int fontSize, boolean fontBold, boolean fo String fontPath = getFontLocation(fontName); boolean foundFont = true; if (fontPath.equals("")) { - fontPath = PDFFontDescriptor.getTrueTypeFontLocation(fontName, props); + fontName = fontName.endsWith(style) ? fontName.substring(0, fontName.length() - style.length()) : fontName; + fontPath = getFontLocation(fontName); + baseFont = getOrLoadFont(fontName, document); + baseFontName = fontName; if (fontPath.equals("")) { - baseFont = PDType1Font.HELVETICA; - baseFontName = baseFont.getName(); + baseFont = new PDType1Font(Standard14Fonts.FontName.HELVETICA); + baseFontName = "Helvetica"; foundFont = false; } } if (foundFont){ baseFont = createPDType1FontFromName(fontName); - if (baseFont != null) - baseFontName = baseFont.getName(); - else{ - baseFont = PDType0Font.load(document, new File(getFontLocation(fontName))); + if (baseFont == null){ + baseFont = getOrLoadFont(fontName, document); baseFontName = fontName; } } @@ -532,33 +529,33 @@ public void GxAttris(String fontName, int fontSize, boolean fontBold, boolean fo private static PDType1Font createPDType1FontFromName(String fontName) { switch (fontName) { case "Times-Roman": - return PDType1Font.TIMES_ROMAN; + return new PDType1Font(Standard14Fonts.FontName.TIMES_ROMAN); case "Times-Bold": - return PDType1Font.TIMES_BOLD; + return new PDType1Font(Standard14Fonts.FontName.TIMES_BOLD); case "Times-Italic": - return PDType1Font.TIMES_ITALIC; + return new PDType1Font(Standard14Fonts.FontName.TIMES_ITALIC); case "Times-BoldItalic": - return PDType1Font.TIMES_BOLD_ITALIC; + return new PDType1Font(Standard14Fonts.FontName.TIMES_BOLD_ITALIC); case "Helvetica": - return PDType1Font.HELVETICA; + return new PDType1Font(Standard14Fonts.FontName.HELVETICA); case "Helvetica-Bold": - return PDType1Font.HELVETICA_BOLD; + return new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD); case "Helvetica-Oblique": - return PDType1Font.HELVETICA_OBLIQUE; + return new PDType1Font(Standard14Fonts.FontName.HELVETICA_OBLIQUE); case "Helvetica-BoldOblique": - return PDType1Font.HELVETICA_BOLD_OBLIQUE; + return new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD_OBLIQUE); case "Courier": - return PDType1Font.COURIER; + return new PDType1Font(Standard14Fonts.FontName.COURIER); case "Courier-Bold": - return PDType1Font.COURIER_BOLD; + return new PDType1Font(Standard14Fonts.FontName.COURIER_BOLD); case "Courier-Oblique": - return PDType1Font.COURIER_OBLIQUE; + return new PDType1Font(Standard14Fonts.FontName.COURIER_OBLIQUE); case "Courier-BoldOblique": - return PDType1Font.COURIER_BOLD_OBLIQUE; + return new PDType1Font(Standard14Fonts.FontName.COURIER_BOLD_OBLIQUE); case "Symbol": - return PDType1Font.SYMBOL; + return new PDType1Font(Standard14Fonts.FontName.SYMBOL); case "ZapfDingbats": - return PDType1Font.ZAPF_DINGBATS; + return new PDType1Font(Standard14Fonts.FontName.ZAPF_DINGBATS); default: return null; } @@ -566,18 +563,34 @@ private static PDType1Font createPDType1FontFromName(String fontName) { public void setAsianFont(String fontName, String style) { try { - String fontPath = getFontLocation(fontName); - baseFont = PDType0Font.load(document, new File(fontPath)); - baseFontName = fontName; + baseFont = getOrLoadFont(fontName, document); } catch(Exception e) { log.error("setAsianFont failed: ", e); } } + + private PDFont getOrLoadFont(String fontName, PDDocument doc) throws IOException { + PDFont cachedFont = fontCache.get(fontName); + if (cachedFont != null) { + return cachedFont; + } + PDFont font = createPDType1FontFromName(fontName); + if (font == null) { + String fontPath = getFontLocation(fontName); + if (!fontPath.isEmpty()) { + font = PDType0Font.load(doc, new File(fontPath)); + } else { + font = new PDType1Font(Standard14Fonts.FontName.HELVETICA); + } + } + fontCache.put(fontName, font); + return font; + } + public void GxDrawText(String sTxt, int left, int top, int right, int bottom, int align, int htmlformat, int border, int valign) { - PDPageContentStream cb = null; try { - cb = getNewPDPageContentStream(); + PDPageContentStream cb = currentPageContentStream; boolean printRectangle = false; if (props.getBooleanGeneralProperty(Const.BACK_FILL_IN_CONTROLS, true)) printRectangle = true; @@ -588,9 +601,7 @@ public void GxDrawText(String sTxt, int left, int top, int right, int bottom, in sTxt = CommonUtil.rtrim(sTxt); - PDFont font = createPDType1FontFromName(baseFont.getFontDescriptor().getFontName()); - if (font == null) - font = PDType0Font.load(document, new File(getFontLocation(baseFontName))); + PDFont font = getOrLoadFont(baseFontName, document); cb.setFont(font,fontSize); cb.setNonStrokingColor(foreColor); @@ -655,8 +666,7 @@ else if (valign == PDFReportPDFBox.VerticalAlign.BOTTOM.value()) GxEndPage(); GxStartPage(); - cb.close(); - cb = getNewPDPageContentStream(); + cb = currentPageContentStream; } if (this.supportedHTMLTags.contains(element.normalName())) processHTMLElement(cb, htmlRectangle, spaceHandler, element); @@ -739,11 +749,10 @@ else if (valign == PDFReportPDFBox.VerticalAlign.BOTTOM.value()) rectangle.setUpperRightY(this.pageSize.getUpperRightY() - topAux - topMargin -bottomMargin); break; } - PDPageContentStream contentStream = getNewPDPageContentStream(); - contentStream.setNonStrokingColor(backColor); - contentStream.addRect(rectangle.getLowerLeftX(), rectangle.getLowerLeftY(),rectangle.getWidth(), rectangle.getHeight()); - contentStream.fill(); - contentStream.close(); + cb.setNonStrokingColor(backColor); + cb.addRect(rectangle.getLowerLeftX(), rectangle.getLowerLeftY(),rectangle.getWidth(), rectangle.getHeight()); + cb.fill(); + cb.setNonStrokingColor(foreColor); } float underlineSeparation = lineHeight / 5; @@ -773,11 +782,11 @@ else if (valign == PDFReportPDFBox.VerticalAlign.BOTTOM.value()) underline.setUpperRightY(this.pageSize.getUpperRightY() - bottomAux - topMargin -bottomMargin + startHeight - underlineHeight); break; } - PDPageContentStream contentStream = getNewPDPageContentStream(); + PDPageContentStream contentStream = currentPageContentStream; contentStream.setNonStrokingColor(foreColor); contentStream.addRect(underline.getLowerLeftX(), underline.getLowerLeftY(),underline.getWidth(), underline.getHeight()); contentStream.fill(); - contentStream.close(); + cb.setNonStrokingColor(foreColor); } if (fontStrikethru) { @@ -804,11 +813,10 @@ else if (valign == PDFReportPDFBox.VerticalAlign.BOTTOM.value()) underline.setUpperRightY(this.pageSize.getUpperRightY() - bottomAux - topMargin -bottomMargin + startHeight - underlineHeight + strikethruSeparation); break; } - PDPageContentStream contentStream = getNewPDPageContentStream(); + PDPageContentStream contentStream = currentPageContentStream; contentStream.setNonStrokingColor(foreColor); contentStream.addRect(underline.getLowerLeftX(), underline.getLowerLeftY() - strikethruSeparation * 1/3, underline.getWidth(), underline.getHeight()); contentStream.fill(); - contentStream.close(); } if(sTxt.trim().equalsIgnoreCase("{{Pages}}")) { @@ -866,13 +874,6 @@ else if (valign == PDFReportPDFBox.VerticalAlign.BOTTOM.value()) } } catch (Exception ioe){ log.error("GxDrawText failed: ", ioe); - } finally { - try { - if (cb != null) cb.close(); - } - catch (IOException ioe) { - log.error("GxDrawText failed to close a content stream to one of it's pages: ", ioe); - } } } @@ -892,7 +893,7 @@ private void loadSupportedHTMLTags(){ private void processHTMLElement(PDPageContentStream cb, PDRectangle htmlRectangle, SpaceHandler spaceHandler, Element blockElement) throws Exception{ this.fontBold = false; String tagName = blockElement.normalName(); - PDFont htmlFont = PDType1Font.TIMES_ROMAN; + PDFont htmlFont = new PDType1Font(Standard14Fonts.FontName.TIMES_ROMAN); if (tagName.equals("div") || tagName.equals("span")) { for (Node child : blockElement.childNodes()) @@ -905,7 +906,7 @@ private void processHTMLElement(PDPageContentStream cb, PDRectangle htmlRectangl return; } - float lineHeight = (PDType1Font.TIMES_ROMAN.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize) * DEFAULT_PDFBOX_LEADING; + float lineHeight = (new PDType1Font(Standard14Fonts.FontName.TIMES_ROMAN).getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize) * DEFAULT_PDFBOX_LEADING; float leading = (float)(Double.valueOf(props.getGeneralProperty(Const.LEADING)).doubleValue()); float llx = htmlRectangle.getLowerLeftX(); @@ -1029,7 +1030,7 @@ public void setAvailableSpace(float availableSpace) { private float renderHTMLContent(PDPageContentStream contentStream, String text, float fontSize, float llx, float lly, float urx, float ury) { try { - PDFont defaultHTMLFont = PDType1Font.TIMES_ROMAN; + PDFont defaultHTMLFont = new PDType1Font(Standard14Fonts.FontName.TIMES_ROMAN); List lines = new ArrayList<>(); String[] words = text.split(" "); StringBuilder currentLine = new StringBuilder(); @@ -1075,12 +1076,14 @@ private float renderHTMLContent(PDPageContentStream contentStream, String text, private void resolveTextStyling(PDPageContentStream contentStream, String text, float x, float y, boolean isWrapped){ try { + contentStream.setNonStrokingColor(foreColor); + contentStream.setRenderingMode(RenderingMode.FILL); if (this.fontBold && this.fontItalic){ contentStream.setStrokingColor(foreColor); contentStream.setLineWidth(fontSize * 0.05f); contentStream.setRenderingMode(RenderingMode.FILL_STROKE); contentStream.beginText(); - contentStream.moveTextPositionByAmount(x, y); + contentStream.newLineAtOffset(x, y); contentStream.setTextMatrix(new Matrix(1, 0, 0.2f, 1, x + 0.2f * y, y)); contentStream.newLineAtOffset(-0.2f * y, 0); } else if (this.fontBold && !this.fontItalic){ @@ -1088,10 +1091,10 @@ private void resolveTextStyling(PDPageContentStream contentStream, String text, contentStream.setLineWidth(fontSize * 0.05f); contentStream.setRenderingMode(RenderingMode.FILL_STROKE); contentStream.beginText(); - contentStream.moveTextPositionByAmount(x, y); + contentStream.newLineAtOffset(x, y); } else if (!this.fontBold && this.fontItalic){ contentStream.beginText(); - contentStream.moveTextPositionByAmount(x, y); + contentStream.newLineAtOffset(x, y); contentStream.setTextMatrix(new Matrix(1, 0, 0.2f, 1, x + 0.2f * y, y)); contentStream.newLineAtOffset(-0.2f * y, 0); } else { @@ -1359,10 +1362,28 @@ public void GxEndDocument() { log.error("GxEndDocument failed: ", e);; } } + public void GxStartPage() { - document.addPage(new PDPage(this.pageSize)); - pages = pages + 1; - page = page + 1; + PDPage page = new PDPage(this.pageSize); + document.addPage(page); + pages++; + this.page++; + try { + currentPageContentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, false); + } catch (IOException e) { + log.error("Failed to start page content stream: ", e); + } + } + + public void GxEndPage() { + try { + if (currentPageContentStream != null) { + currentPageContentStream.close(); + currentPageContentStream = null; + } + } catch (IOException e) { + log.error("Failed to close page content stream: ", e); + } } } \ No newline at end of file