From 11b374430d6d6e381e05703fb0473b42145d3e01 Mon Sep 17 00:00:00 2001 From: Thomas Gutmann <41593722+speckyspooky@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:21:02 +0100 Subject: [PATCH] PdfEmitter, Enhancement: Enable options to set the PDF/Version & archiving formats PDF/A (A1A & A1B) #1486 (#1487) PdfEmitter, Enhancement: Enable options to set the PDF/Version & archiving formats PDF/A (A1A & A1B) (#1486) --- .../report/engine/emitter/pdf/PDFPage.java | 19 +- .../engine/emitter/pdf/PDFPageDevice.java | 461 ++++++++++++++++-- 2 files changed, 434 insertions(+), 46 deletions(-) diff --git a/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/PDFPage.java b/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/PDFPage.java index 24a65f60d62..1fd16e32b0c 100644 --- a/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/PDFPage.java +++ b/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/PDFPage.java @@ -464,9 +464,24 @@ private void drawText(String text, float textX, float textY, FontInfo fontInfo, BaseFont font = getBaseFont(fontInfo); float fontSize = fontInfo.getFontSize(); try { + // PDF/A: if font not embeddable then use the configured PDF/A fallback font + if (this.pageDevice.isPdfAFormat() && fontInfo.getBaseFont() != null + && !fontInfo.getBaseFont().isEmbedded()) { + try { + String defaultFontPdfA = this.pageDevice.getDefaultFontPdfA(); + if (defaultFontPdfA != null) { + font = BaseFont.createFont(defaultFontPdfA, "", true); + } + logger.log(Level.WARNING, + "PDF/A: " + fontInfo.getFontName() + " not embeddable, fallback font used."); + } catch (Exception e) { + logger.log(Level.WARNING, + "PDF/A: " + fontInfo.getFontName() + " not embeddable." + e.getMessage()); + } + } contentByte.setFontAndSize(font, fontSize); - } catch (IllegalArgumentException e) { - logger.log(Level.WARNING, e.getMessage()); + } catch (IllegalArgumentException iae) { + logger.log(Level.WARNING, iae.getMessage()); // close to zero , increase by one MIN_FONT_SIZE step contentByte.setFontAndSize(font, MIN_FONT_SIZE * 2); } diff --git a/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/PDFPageDevice.java b/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/PDFPageDevice.java index ed09ec3729a..c9a974c690f 100644 --- a/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/PDFPageDevice.java +++ b/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/PDFPageDevice.java @@ -1,12 +1,11 @@ /******************************************************************************* * Copyright (c) 2004 Actuate Corporation. - * + * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * https://www.eclipse.org/legal/epl-2.0/. - * + * * SPDX-License-Identifier: EPL-2.0 - * * * Contributors: * Actuate Corporation - initial API and implementation @@ -15,6 +14,8 @@ package org.eclipse.birt.report.engine.emitter.pdf; import java.awt.Color; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_Profile; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; @@ -49,15 +50,57 @@ import com.lowagie.text.DocumentException; import com.lowagie.text.Rectangle; import com.lowagie.text.pdf.BaseFont; +import com.lowagie.text.pdf.PdfArray; import com.lowagie.text.pdf.PdfContentByte; +import com.lowagie.text.pdf.PdfDictionary; +import com.lowagie.text.pdf.PdfICCBased; import com.lowagie.text.pdf.PdfImportedPage; +import com.lowagie.text.pdf.PdfName; import com.lowagie.text.pdf.PdfOutline; import com.lowagie.text.pdf.PdfReader; +import com.lowagie.text.pdf.PdfString; import com.lowagie.text.pdf.PdfTemplate; import com.lowagie.text.pdf.PdfWriter; +/** + * Definition of the PDF emitter page + * + * @since 3.3 + * + */ public class PDFPageDevice implements IPageDevice { + /** PDF possible PDF version (catalog) */ + /** PDF version 1.3 */ + private static final String PDF_VERSION_1_3 = "1.3"; + /** PDF version 1.4 */ + private static final String PDF_VERSION_1_4 = "1.4"; + /** PDF version 1.5 */ + private static final String PDF_VERSION_1_5 = "1.5"; + /** PDF version 1.6 */ + private static final String PDF_VERSION_1_6 = "1.6"; + /** PDF version 1.7 */ + private static final String PDF_VERSION_1_7 = "1.7"; + + /** PDFX/PDFA conformance */ + /** PDF conformance PDF Standard */ + private static final String PDF_CONFORMANCE_STANDARD = "PDF.Standard"; + /** PDF conformance PDF X-3:2002 */ + private static final String PDF_CONFORMANCE_X32002 = "PDF.X32002"; + /** PDF conformance PDF A1A */ + private static final String PDF_CONFORMANCE_A1A = "PDF.A1A"; + /** PDF conformance PDF A1B */ + private static final String PDF_CONFORMANCE_A1B = "PDF.A1B"; + /** PDF conformance PDF X-1a:2001, unsupported (TODO: CMYK of PDF.X1A2001 */ + + /** PDF ICC color profile */ + /** PDF ICC default color profile RGB */ + private static final String PDF_ICC_PROFILE_DEFAULT = "sRGB IEC61966-2.1"; + /** PDF ICC color profile RGB */ + private static final String PDF_ICC_COLOR_RGB = "RGB"; + /** PDF ICC color profile CMYK */ + private static final String PDF_ICC_COLOR_CMYK = "CMYK"; + /** * The pdf Document object created by iText */ @@ -88,10 +131,65 @@ public class PDFPageDevice implements IPageDevice { protected final static int MAX_PAGE_WIDTH = 14400000; // 200 inch protected final static int MAX_PAGE_HEIGHT = 14400000; // 200 inch - // Property names for list of files to append or prepend to PDF output - private static String APPEND_PROPERTY_NAME = "AppendList"; - private static String PREPEND_PROPERTY_NAME = "PrependList"; + /** PDF emitter: user properties */ + + /** PDF prepend document: names for list of files to prepend */ + private final static String PDF_PREPEND_DOCUMENTS = "PdfEmitter.PrependDocumentList"; + private final static String PREPEND_PROPERTY_NAME = "PrependList"; + + /** PDF append document: names for list of files to append */ + private final static String PDF_APPEND_DOCUMENTS = "PdfEmitter.AppendDocumentList"; + private final static String APPEND_PROPERTY_NAME = "AppendList"; + + /** PDF version: allowed version, 1.3 - 1.7 */ + private final static String PDF_VERSION = "PdfEmitter.Version"; + + /** + * PDF conformance: PDF.X1A2001, PDF.X32002, PDF.A1A, PDF.A1B + */ + private final static String PDF_CONFORMANCE = "PdfEmitter.Conformance"; + + /** + * PDF/A ICC color profile (default: sRGB IEC61966-2.1, standard by: HP & + * Microsoft) + */ + private final static String PDF_ICC_PROFILE_EXTERNAL_FILE = "PdfEmitter.IccProfileFile"; + + /** PDF/A ICC color type CMYK or RGB (default: RGB) */ + private static final String PDF_ICC_COLOR_TYPE = "PdfEmitter.IccColorType"; + + /** + * PDF/A with document title cause validation issue of PDF/A by openPDF 1.3.30 + * issue based on XMP metadata "dc:title" (default: without title) + */ + private final static String PDFA_ADD_DOCUMENT_TITLE = "PdfEmitter.PDFA.AddDocumentTitle"; + + private final static String PDFA_FALLBACK_FONT = "PdfEmitter.PDFA.FallbackFont"; + + protected Map userProperties; + + private char pdfVersion = '0'; + + private int pdfConformance = PdfWriter.PDFXNONE; + private boolean isPdfAFormat = false; + + private boolean addPdfADocumentTitle = false; + + private String defaultFontPdfA = null; + + /** + * + * Constructor to define the PDF + * + * @param output output stream of the document + * @param title title of the document + * @param author author of the document + * @param subject subject of the document + * @param description description of the document + * @param context context of the document + * @param report report object of the document + */ public PDFPageDevice(OutputStream output, String title, String author, String subject, String description, IReportContext context, IReportContent report) { this.context = context; @@ -99,18 +197,35 @@ public PDFPageDevice(OutputStream output, String title, String author, String su doc = new Document(); try { writer = PdfWriter.getInstance(doc, new BufferedOutputStream(output)); - writer.setFullCompression(); - writer.setRgbTransparencyBlending(true); EngineResourceHandle handle = new EngineResourceHandle(ULocale.forLocale(context.getLocale())); + this.userProperties = report.getDesign().getUserProperties(); + + // PDF version user property based + this.setPdfVersion(); + // PDF/A & PDF/X conformance user property based + this.setPdfConformance(); + // PDF/A, set the default font of not embeddable fonts + this.setDefaultFontPdfA(); + + // PDF/A (A1A, A1B), avoid compression and transparency + if (!this.isPdfAFormat) { + writer.setFullCompression(); + writer.setRgbTransparencyBlending(true); + } + String creator = handle.getMessage(MessageConstants.PDF_CREATOR, versionInfo); doc.addCreator(creator); if (null != author) { doc.addAuthor(author); } - if (null != title) { - doc.addTitle(title); + // openPDF 1.3.30: title of PDF/A won't be set correctly, + // issue on xmp meta data at "dc:title" + if (!this.isPdfAFormat || this.addPdfADocumentTitle) { + if (null != title) { + doc.addTitle(title); + } } if (null != subject) { doc.addSubject(subject); @@ -127,16 +242,12 @@ public PDFPageDevice(OutputStream output, String title, String author, String su // this is where we will test the merge List pdfs = new ArrayList<>(); - // removed using the runtime instance of the report and switched to using the - // designtime - // instance per request. - // String list = (String) context.getReportRunnable().getProperty("AppendList"); - // String list = (String) context.getDesignHandle().getProperty("AppendList"); - Map props = report.getDesign().getUserProperties(); - // added null check - if (props != null) { - Object listObject = props.get(PDFPageDevice.PREPEND_PROPERTY_NAME); + if (userProperties != null) { + Object listObject = userProperties.get(PDFPageDevice.PREPEND_PROPERTY_NAME); + if (userProperties.containsKey(PDFPageDevice.PDF_PREPEND_DOCUMENTS)) { + listObject = userProperties.get(PDFPageDevice.PDF_PREPEND_DOCUMENTS); + } if (listObject != null) { Expression exp = (Expression) listObject; @@ -225,7 +336,7 @@ public PDFPageDevice(OutputStream output, String title, String author, String su } /** - * constructor for test + * Constructor for test * * @param output */ @@ -238,22 +349,50 @@ public PDFPageDevice(OutputStream output) { } } - public void setPDFTemplate(Float scale, PdfTemplate totalPageTemplate) { - templateMap.put(scale, totalPageTemplate); + /** + * Set pdf template + * + * @param key the inex key of the pdf template + * @param totalPageTemplate pdf template document + */ + public void setPDFTemplate(Float key, PdfTemplate totalPageTemplate) { + templateMap.put(key, totalPageTemplate); } + /** + * Get all pdf templates + * + * @return Return all pdf templates + */ public HashMap getTemplateMap() { return templateMap; } - public PdfTemplate getPDFTemplate(Float scale) { - return templateMap.get(scale); + /** + * Get the pdf template + * + * @param key index of the pdf template + * @return Return the index based pdf template + */ + public PdfTemplate getPDFTemplate(Float key) { + return templateMap.get(key); } - public boolean hasTemplate(Float scale) { - return templateMap.containsKey(scale); + /** + * Check if a template was used + * + * @param key key of the template + * @return Return the result of the template check + */ + public boolean hasTemplate(Float key) { + return templateMap.containsKey(key); } + /** + * Get the image cache + * + * @return Return the image cache + */ public HashMap getImageCache() { return imageCache; } @@ -271,24 +410,22 @@ public void close() throws Exception { // this is where we will test the merge List pdfs = new ArrayList<>(); - // removed using the runtime instance of the report and switched to using the - // designtime - // instance per request. - // String list = (String) context.getReportRunnable().getProperty("AppendList"); - // String list = (String) context.getDesignHandle().getProperty("AppendList"); - Map props = report.getDesign().getUserProperties(); + Map userProperties = report.getDesign().getUserProperties(); // added null check - if (props != null) { - Object listObject = props.get(PDFPageDevice.APPEND_PROPERTY_NAME); + if (userProperties != null) { + Object listObject = userProperties.get(PDFPageDevice.APPEND_PROPERTY_NAME); + if (userProperties.containsKey(PDFPageDevice.PDF_APPEND_DOCUMENTS)) { + listObject = userProperties.get(PDFPageDevice.PDF_APPEND_DOCUMENTS); + } if (listObject != null) { Expression exp = (Expression) listObject; Object result = context.evaluate(exp); - // there are two options here. 1 is the user property "AppendList" is a - // comma-seperated - // string list. If so, check that it is a String, and split it. + // 2 options to append pdf + // option 1: is the user property "AppendList" is a comma-seperated string list + // If so, check that it is a String, and split it. if (result instanceof String) { String list = (String) result; @@ -322,9 +459,8 @@ public void close() throws Exception { } } - // The other is a "Named Expression", which is basically a user property that is - // the result - // of an expression instead of a string literal. This should be set as an + // option 2: "Named Expression", which is basically a user property that is the + // result of an expression instead of a string literal. This should be set as an // arraylist through // BIRT script if (result instanceof ArrayList) { @@ -358,7 +494,11 @@ public void close() throws Exception { } } } - // End Modification + + if (this.isPdfAFormat) { + // PDF/A: set color profile and metadata + this.setPdfIccXmp(); + } writer.setPageEmpty(false); if (doc.isOpen()) { @@ -379,6 +519,11 @@ protected PDFPage createPDFPage(int pageWidth, int pageHeight) { return new PDFPage(pageWidth, pageHeight, doc, writer, this); } + /** + * Create the TOC of the pdf document + * + * @param bookmarks bookmarks of the TOC + */ public void createTOC(Set bookmarks) { // we needn't create the TOC if there is no page in the PDF file. // the doc is opened only if the user invokes newPage. @@ -487,11 +632,239 @@ public void concatPDFs(List streamOfPDFFiles, boolean paginate) { } pageOfCurrentReaderPDF = 0; } - // outputStream.flush(); - // document.close(); - // outputStream.close(); } catch (Exception e) { e.printStackTrace(); } } + + /** + * Set the PDF version based on the user property + */ + private void setPdfVersion() { + // PDF version based on user property + if (this.userProperties != null && this.userProperties.containsKey(PDFPageDevice.PDF_VERSION)) { + String userPdfVersion = this.userProperties.get(PDFPageDevice.PDF_VERSION).toString(); + this.setPdfVersion(userPdfVersion); + } + } + + /** + * Set the PDF version + * + * @param version key to set the PDF version (e.g. 1.7) + */ + public void setPdfVersion(String version) { + switch (version) { + case PDFPageDevice.PDF_VERSION_1_3: + this.pdfVersion = PdfWriter.VERSION_1_3; + break; + case PDFPageDevice.PDF_VERSION_1_4: + this.pdfVersion = PdfWriter.VERSION_1_3; + break; + case PDFPageDevice.PDF_VERSION_1_5: + this.pdfVersion = PdfWriter.VERSION_1_3; + break; + case PDFPageDevice.PDF_VERSION_1_6: + this.pdfVersion = PdfWriter.VERSION_1_3; + break; + case PDFPageDevice.PDF_VERSION_1_7: + this.pdfVersion = PdfWriter.VERSION_1_7; + break; + } + // version only set if the PDF version exists + if (this.pdfVersion != '0') { + writer.setAtLeastPdfVersion(this.pdfVersion); + } + } + + /** + * Get the PDF version + * + * @return Return the PDF version (e.g. 1.7) + */ + public String getPdfVersion() { + switch (this.pdfVersion) { + case PdfWriter.VERSION_1_3: + return PDFPageDevice.PDF_VERSION_1_3; + case PdfWriter.VERSION_1_4: + return PDFPageDevice.PDF_VERSION_1_4; + case PdfWriter.VERSION_1_5: + return PDFPageDevice.PDF_VERSION_1_5; + case PdfWriter.VERSION_1_6: + return PDFPageDevice.PDF_VERSION_1_6; + case PdfWriter.VERSION_1_7: + return PDFPageDevice.PDF_VERSION_1_7; + default: + return PDFPageDevice.PDF_VERSION_1_5; + } + } + + /** + * Set the PDF conformance user property based + */ + private void setPdfConformance() { + // PDFA & PDFX conformance, based on user property + if (this.userProperties != null && this.userProperties.containsKey(PDFPageDevice.PDF_CONFORMANCE)) { + String userPdfConformance = this.userProperties.get(PDFPageDevice.PDF_CONFORMANCE).toString().toUpperCase(); + switch (userPdfConformance) { + case PDFPageDevice.PDF_CONFORMANCE_X32002: + this.pdfConformance = PdfWriter.PDFX32002; + this.isPdfAFormat = false; + break; + case PDFPageDevice.PDF_CONFORMANCE_A1A: + this.pdfConformance = PdfWriter.PDFA1A; + this.isPdfAFormat = true; + break; + case PDFPageDevice.PDF_CONFORMANCE_A1B: + this.pdfConformance = PdfWriter.PDFA1B; + this.isPdfAFormat = true; + break; + default: + this.pdfConformance = PdfWriter.PDFXNONE; + this.isPdfAFormat = false; + break; + } + this.setPdfConformance(this.pdfConformance); + } + + // PDFA: overwrite to get the document title independent of the openPDF + // issue of PDF/A-conformance + if (this.userProperties != null && this.userProperties.containsKey(PDFPageDevice.PDFA_ADD_DOCUMENT_TITLE)) { + String pdfaUseTitleOverwrite = this.userProperties.get(PDFPageDevice.PDFA_ADD_DOCUMENT_TITLE).toString() + .toLowerCase(); + if (pdfaUseTitleOverwrite.equals("true")) { + this.addPdfADocumentTitle = true; + } + } + } + + /** + * Set the PDF conformance + * + * @param pdfConformance conformance of the PDF document + */ + public void setPdfConformance(int pdfConformance) { + writer.setPDFXConformance(pdfConformance); + writer.setTagged(); + } + + /** + * Get the PDF conformance + * + * @return Return the PDF conformance (e.g. PDF.A1A) + */ + public String getPdfConformance() { + switch (this.pdfConformance) { + case PdfWriter.PDFX32002: + return PDFPageDevice.PDF_CONFORMANCE_X32002; + case PdfWriter.PDFA1A: + return PDFPageDevice.PDF_CONFORMANCE_A1A; + case PdfWriter.PDFA1B: + return PDFPageDevice.PDF_CONFORMANCE_A1B; + default: + return PDFPageDevice.PDF_CONFORMANCE_STANDARD; + } + } + + /** + * Check if PDF/A format + * + * @return Return the check result of PDF/A format + */ + public boolean isPdfAFormat() { + return this.isPdfAFormat; + } + + /** + * Set the PDF icc color profile and the XMP meta data + */ + private void setPdfIccXmp() { + + // PDF/A: set ICC color profile and XMP metadata + try { + // PDF ICC standard color profile + PdfDictionary outi = new PdfDictionary(PdfName.OUTPUTINTENT); + outi.put(PdfName.OUTPUTCONDITIONIDENTIFIER, new PdfString(PDFPageDevice.PDF_ICC_PROFILE_DEFAULT)); + outi.put(PdfName.INFO, new PdfString(PDFPageDevice.PDF_ICC_PROFILE_DEFAULT)); + outi.put(PdfName.S, PdfName.GTS_PDFA1); + + // PDF ICC color profile + boolean iccProfileExternal = false; + ICC_Profile iccProfile = null; + File iccFile = null; + String fullFileNameIcc = ""; + if (this.userProperties != null + && userProperties.containsKey(PDFPageDevice.PDF_ICC_PROFILE_EXTERNAL_FILE)) { + fullFileNameIcc = userProperties.get(PDFPageDevice.PDF_ICC_PROFILE_EXTERNAL_FILE).toString().trim(); + try { + iccFile = new File(fullFileNameIcc); + if (!iccFile.exists()) { + // get the file using context.getResource() for relative or universal paths + URL url = context.getResource(fullFileNameIcc); + iccFile = new File(url.toURI()); + } + iccProfileExternal = true; + } catch (Exception e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + + // PDF color RGB / CMYK + int colorSpace = ColorSpace.CS_sRGB; + String iccColorType = PDFPageDevice.PDF_ICC_COLOR_RGB; + if (this.userProperties != null && userProperties.containsKey(PDFPageDevice.PDF_ICC_COLOR_TYPE)) { + iccColorType = userProperties.get(PDFPageDevice.PDF_ICC_COLOR_TYPE).toString().toUpperCase().trim(); + if (iccColorType.equals(PDFPageDevice.PDF_ICC_COLOR_CMYK)) { + colorSpace = ColorSpace.TYPE_CMYK; + } + } + if (iccProfileExternal) { + iccProfile = ICC_Profile.getInstance(iccFile.getAbsolutePath()); + } else { + iccProfile = ICC_Profile.getInstance(colorSpace); + } + PdfICCBased iccPdf = new PdfICCBased(iccProfile); + iccPdf.remove(PdfName.ALTERNATE); + outi.put(PdfName.DESTOUTPUTPROFILE, writer.addToBody(iccPdf).getIndirectReference()); + writer.getExtraCatalog().put(PdfName.OUTPUTINTENTS, new PdfArray(outi)); + + } catch (Exception icce) { + logger.log(Level.WARNING, icce.getMessage(), icce); + } + + try { + // PDF create the xmp metaddata based on the document information + writer.createXmpMetadata(); + } catch (Exception e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + + /** + * Set the default font of PDF/A user property based + */ + private void setDefaultFontPdfA() { + if (this.userProperties != null && this.userProperties.containsKey(PDFPageDevice.PDFA_FALLBACK_FONT)) { + String defaultFont = this.userProperties.get(PDFPageDevice.PDFA_FALLBACK_FONT).toString(); + this.defaultFontPdfA = defaultFont; + } + } + + /** + * Set the default font of PDF/A + * + * @param defaultFont default font of PDF/A + */ + public void setDefaultFontPdfA(String defaultFont) { + this.defaultFontPdfA = defaultFont; + } + + /** + * Get the default font of PDF/A + * + * @return Return the default font of PDF/A + */ + public String getDefaultFontPdfA() { + return this.defaultFontPdfA; + } }