diff --git a/engine/org.eclipse.birt.report.engine.emitter.pdf.tests/test/org/eclipse/birt/report/engine/emitter/pdf/AllTests.java b/engine/org.eclipse.birt.report.engine.emitter.pdf.tests/test/org/eclipse/birt/report/engine/emitter/pdf/AllTests.java index 57c855d9bf..20cb8f49fa 100644 --- a/engine/org.eclipse.birt.report.engine.emitter.pdf.tests/test/org/eclipse/birt/report/engine/emitter/pdf/AllTests.java +++ b/engine/org.eclipse.birt.report.engine.emitter.pdf.tests/test/org/eclipse/birt/report/engine/emitter/pdf/AllTests.java @@ -1,6 +1,6 @@ /******************************************************************************* - * Copyright (c) 2004, 2005 Actuate Corporation. + * Copyright (c) 2004, 2026 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 @@ -29,6 +29,7 @@ public static Test suite() { /* in package: org.eclipse.birt.report.engine.emitter.pdf */ suite.addTestSuite(org.eclipse.birt.report.engine.emitter.pdf.PdfRenderTest.class); + suite.addTestSuite(org.eclipse.birt.report.engine.emitter.pdf.PdfConcurrentRenderTest.class); // $JUnit-END$ return suite; diff --git a/engine/org.eclipse.birt.report.engine.emitter.pdf.tests/test/org/eclipse/birt/report/engine/emitter/pdf/PdfConcurrentRenderTest.java b/engine/org.eclipse.birt.report.engine.emitter.pdf.tests/test/org/eclipse/birt/report/engine/emitter/pdf/PdfConcurrentRenderTest.java new file mode 100644 index 0000000000..1cbab22953 --- /dev/null +++ b/engine/org.eclipse.birt.report.engine.emitter.pdf.tests/test/org/eclipse/birt/report/engine/emitter/pdf/PdfConcurrentRenderTest.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * Copyright (c) 2026 Rahul Pal and others. + * + * 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: + * Rahul Pal - initial implementation (issue #2444) + *******************************************************************************/ +package org.eclipse.birt.report.engine.emitter.pdf; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.birt.report.engine.api.IRunAndRenderTask; +import org.eclipse.birt.report.engine.api.PDFRenderOption; + +/** + * Concurrency test for the PDF emitter font path (issue #2444). + * + *
+ * Renders the same report design to PDF from several threads in parallel, + * sharing the single {@code engine} created by {@link EngineCase} (the + * realistic multi-threaded usage). The test asserts that no render throws and + * that every output PDF is produced and non-empty. + *
+ * + *+ * This guards concurrent rendering through the font path after the removal of + * the process-global {@code LayoutProcessor} state. As with the other tests in + * this module, it asserts that rendering completes without error rather than + * verifying glyph-level output. Note that, by the nature of race conditions, a + * passing run does not by itself prove the absence of a race - the primary + * safety argument is the design (per-document state, no static state); this + * test is a regression guard and demonstration. + *
+ */ +public class PdfConcurrentRenderTest extends EngineCase { + + /** Reused existing design from this test module. */ + private static final String DESIGN = "test/org/eclipse/birt/report/engine/emitter/pdf/issue-2429.rptdesign"; + + /** Number of concurrent render tasks. */ + private static final int THREADS = 8; + + /** Upper bound on total render time before the test gives up. */ + private static final int TIMEOUT_SECONDS = 120; + + /** + * Output directory, cleaned at the start of each run so the PDFs from the last + * run remain available for manual inspection. + */ + private File outputDir; + + @Override + protected void setUp() throws Exception { + super.setUp(); // creates the shared engine (and configures fonts) + outputDir = new File(System.getProperty("java.io.tmpdir"), "birt-pdf-concurrent"); + deleteRecursively(outputDir); + outputDir.mkdirs(); + } + + /** + * Renders the same design concurrently on a shared engine and verifies that + * every render succeeds and produces a non-empty PDF. + * + * @throws Exception + */ + public void testConcurrentPdfRenders() throws Exception { + ExecutorService pool = Executors.newFixedThreadPool(THREADS); + List+ * The run direction is fixed to left-to-right because BIRT has already applied + * bidi reordering and shaping before the text reaches the emitter: + * right-to-left text arrives in visual order using Arabic presentation forms. + * Letting the manager detect direction from the text would reverse it a second + * time. + * + * @param file the font file to load + * @param font the base font being drawn, used as the key at draw time + * @return {@code true} if the font was loaded and can be drawn through the + * manager + */ + private boolean tryLoadFont(File file, BaseFont font) { + try { + FontOptions options = new FontOptions(); + // BIRT delivers text already reordered and shaped, so it must be drawn as-is. + options.setRunDirectionLtr(); + if (useKerningAndLigatures) { + options.setKerningOn().setLigaturesOn(); + } + Font loaded = glyphLayoutManager.loadFont(file.getAbsolutePath(), FONT_LOAD_SIZE, options); + if (loaded == null || loaded.getBaseFont() == null) { + managerUnloadableFonts.add(font); + return false; + } + managerFontSubstitutes.put(font, loaded.getBaseFont()); + return true; + } catch (Throwable t) { + managerUnloadableFonts.add(font); + logger.log(Level.FINE, "Font not loadable into GlyphLayoutManager: " + file + " - " + t.getMessage()); + return false; + } + } + + /** + * Create the glyph layout manager and read the kerning and ligature setting, + * once, on first use. Neither the report's user properties nor the font + * configuration are available when this device is constructed, so both are + * resolved lazily at draw time. + */ + private void ensureGlyphLayoutInitialised() { + if (configRead) { + return; + } + configRead = true; + if (!isPdfComplexFontLayoutEnabled()) { + return; + } + glyphLayoutManager = new GlyphLayoutManager(); + Locale locale = context == null ? Locale.getDefault() : context.getLocale(); + useKerningAndLigatures = FontMappingManagerFactory.getInstance().getFontMappingManager("pdf", locale) //$NON-NLS-1$ + .useFontKerningAndLigatures(); + } } diff --git a/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/README.md b/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/README.md index 47ec5dc31b..3789240e44 100644 --- a/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/README.md +++ b/engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/README.md @@ -201,3 +201,34 @@ A detailed description is given at the top level configuration file fontsConfig. Default false Reference see -Dbirt.pdf.glyph.substitution.enabled Since 4.20 + +**Complex Font Layout** + + Content configuration to enable per-document complex font layout for PDF output + using OpenPDF's GlyphLayoutManager which is created per document and holds + no static state, so it is safe for concurrent multi-document rendering. + When enabled, TrueType/OpenType fonts are rendered through the manager; + base-14 and Type1 fonts continue to use the standard rendering path. + Kerning and ligatures are applied only if they are also enabled by the + "kerning-and-ligatures" configuration tag. + This replaces the deprecated global LayoutProcessor. + Parameter -Dbirt.pdf.complex.font.layout.enabled + Location JVM + Data type boolean + Values true, complex font layout is enabled + false, complex font layout is disabled + Default false + Reference see PdfEmitter.ComplexFontLayoutEnabled, Kerning and Ligatures + Since 4.25 + +**PdfEmitter.ComplexFontLayoutEnabled** + + Content configuration to enable per-document complex font layout for PDF output + The user property works only if the global configuration of complex font layout is disabled. + Location report + Data type boolean + Values true, complex font layout is enabled + false, complex font layout is disabled + Default false + Reference see -Dbirt.pdf.complex.font.layout.enabled, Kerning and Ligatures + Since 4.25 diff --git a/engine/org.eclipse.birt.report.engine.tests/test/org/eclipse/birt/report/engine/layout/pdf/font/FontHandlingPdfTest.java b/engine/org.eclipse.birt.report.engine.tests/test/org/eclipse/birt/report/engine/layout/pdf/font/FontHandlingPdfTest.java deleted file mode 100644 index 02dc46f70c..0000000000 --- a/engine/org.eclipse.birt.report.engine.tests/test/org/eclipse/birt/report/engine/layout/pdf/font/FontHandlingPdfTest.java +++ /dev/null @@ -1,177 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2025 Thomas Gutmann - * - * 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: - * Thomas Gutmann - initial API and implementation - *******************************************************************************/ -package org.eclipse.birt.report.engine.layout.pdf.font; - -import java.awt.Color; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; - -import org.openpdf.text.Document; -import org.openpdf.text.DocumentException; -import org.openpdf.text.Font; -import org.openpdf.text.FontFactory; -import org.openpdf.text.Paragraph; -import org.openpdf.text.pdf.BaseFont; -import org.openpdf.text.pdf.LayoutProcessor; -import org.openpdf.text.pdf.PdfContentByte; -import org.openpdf.text.pdf.PdfWriter; - -/** - * This is a test class to create and execute manual tests for the combination - * of font handling and pdf configuration based on the layout processor which is - * used by openPDF. The primary target is to test the different options of - * kerning and ligatures. The class is not established to instantiate JUnits - * tests. This class support the creation of further test examples and the - * results must be validated manually. - * - * @since 3.3 - * - */ -public class FontHandlingPdfTest { - - /** - * Main method to start the creation of the demo pdf documents - * - * @param args argument - * @throws DocumentException - * @throws IOException - */ - public static void main(String[] args) throws DocumentException, IOException { - - createPDFLigaturePara("pflicht - wo spacing", "C:/temp/pdf_para_ligatures_enabled.pdf"); - - createPdfLigatureCB("pflicht", "C:/temp/pdf_cb_ligatures_enabled.pdf", "enableKernAndLig"); - - createPdfLigatureCB("pflicht", "C:/temp/pdf_cb_ligatures_disabled.pdf", "disableKernAndLig"); - - createPdfLigatureCB("pflicht", "C:/temp/pdf_cb_layout_processor_disabled.pdf", "disableLayoutProcessor"); - - System.out.println("PDF generated successfully!"); - } - - private static String baseFontName = "C:/temp/Fonts/calibri.ttf"; - - /** - * Set font name, which can be name or full font path - * - * @param font font name or full font path - */ - public static void setFont(String font) { - baseFontName = font; - } - - /** - * Get the font name - * - * @return font name - */ - public static String getFont() { - return baseFontName; - } - - /** - * Create a base font from the main class BaseFont - */ - private static BaseFont getBaseFontCreated() throws IOException { - return BaseFont.createFont(getFont(), BaseFont.IDENTITY_H, true); - } - - /** - * Fetch the base font from the font factory - */ - private static BaseFont getBaseFontFontFactory() { - return FontFactory.getFont(getFont(), BaseFont.IDENTITY_H, BaseFont.EMBEDDED, 14, 0).getBaseFont(); - } - - /** - * Create demo pdf to test ligature handling based on paragraph - * - * @param docText demo text for the document - * @param exportPath export path of the pdf file - * @throws IOException - * @throws DocumentException - * - */ - public static void createPDFLigaturePara(String docText, String exportPath) throws IOException, DocumentException { - - // Output PDF file - Document document = new Document(); - try { - PdfWriter.getInstance(document, new FileOutputStream(exportPath)); - } catch (DocumentException | FileNotFoundException e) { - e.printStackTrace(); - } - - document.open(); - - BaseFont bf = getBaseFontCreated(); - Font font = new Font(bf, 14); - if (!LayoutProcessor.isEnabled()) { - LayoutProcessor.enableKernLiga(); - } - document.add(new Paragraph(docText, font)); - - document.close(); - } - - /** - * Create demo pdf to test ligature handling based on content byte - * - * @param docText demo text for the document - * @param exportPath export path of the pdf file - * @param handlingMode mode of layout processor configuration - * - * @throws IOException - * @throws DocumentException - */ - public static void createPdfLigatureCB(String docText, String exportPath, String handlingMode) - throws IOException, DocumentException { - Document document = new Document(); - PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(exportPath)); - document.open(); - PdfContentByte cb = writer.getDirectContent(); - - BaseFont bf = getBaseFontFontFactory(); - bf.setIncludeCidSet(true); - float x = 36; - float y = 750; - - // Set character spacing to 5 points - cb.saveState(); - cb.setCharacterSpacing(5); - cb.setColorFill(Color.BLACK); - cb.concatCTM(1, 0, 0, 1, 20, 20); - cb.beginText(); - cb.setFontAndSize(bf, 20); - cb.setTextMatrix(x, y + 20); - - if (!LayoutProcessor.isEnabled()) { - if (handlingMode.equals("enableKernAndLig")) { - LayoutProcessor.enableKernLiga(); - - } else if (handlingMode.equals("disableKernAndLig")) { - LayoutProcessor.enable(0); - - } else if (handlingMode.equals("disableLayoutProcessor")) { - LayoutProcessor.enableKernLiga(); - } - } - cb.showText(docText); - cb.endText(); - cb.restoreState(); - - document.close(); - } -} \ No newline at end of file diff --git a/engine/org.eclipse.birt.report.engine/src/org/eclipse/birt/report/engine/layout/pdf/font/FontHandler.java b/engine/org.eclipse.birt.report.engine/src/org/eclipse/birt/report/engine/layout/pdf/font/FontHandler.java index aa07f14d83..47dc329442 100644 --- a/engine/org.eclipse.birt.report.engine/src/org/eclipse/birt/report/engine/layout/pdf/font/FontHandler.java +++ b/engine/org.eclipse.birt.report.engine/src/org/eclipse/birt/report/engine/layout/pdf/font/FontHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2004, 2008, 2025 Actuate Corporation and others + * Copyright (c) 2004, 2026 Actuate Corporation and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -25,7 +25,6 @@ import org.eclipse.birt.report.engine.layout.pdf.util.PropertyUtil; import org.openpdf.text.Font; import org.openpdf.text.pdf.BaseFont; -import org.openpdf.text.pdf.LayoutProcessor; import org.w3c.dom.css.CSSValueList; /** @@ -99,7 +98,6 @@ public FontHandler(FontMappingManager fontManager, ITextContent textContent, boo textContent) / PDFConstants.LAYOUT_TO_PDF_RATIO; if (!fontSubstitution) { - enableKerningAndLigatures(); for (int i = 0; i < fontFamilies.length; i++) { String fontName = fontManager.getAliasedFont(fontFamilies[i]); bf = fontManager.createFont(fontName, fontStyle); @@ -129,7 +127,6 @@ public FontHandler(FontMappingManager fontManager, String fontFamilies[], int fo this.fontSize = fontSize / PDFConstants.LAYOUT_TO_PDF_RATIO; if (!fontSubstitution) { - enableKerningAndLigatures(); for (int i = 0; i < fontFamilies.length; i++) { String fontName = fontManager.getAliasedFont(fontFamilies[i]); bf = fontManager.createFont(fontName, fontStyle); @@ -202,7 +199,6 @@ public BaseFont getMappedFont(char c) { } } // search in the font family to find one to display the character - enableKerningAndLigatures(); for (int i = 0; i < fontFamilies.length; i++) { // Translate the font alias to font family String fontFamily = fontManager.getAliasedFont(fontFamilies[i]); @@ -317,23 +313,4 @@ private String getEnglishName(String[][] names) { return tmp; } - - /** - * Enable the font mode to handle advanced kerning and ligatures. The formatting - * option has priority instead disabled behavior. The configuration controls the - * LayouProcessor of OpenPDF. - */ - private void enableKerningAndLigatures() { - synchronized (LayoutProcessor.class) { - if (!LayoutProcessor.isEnabled()) { - if (fontManager.useFontKerningAndLigatures()) { - if (!LayoutProcessor.isEnabled()) { - LayoutProcessor.enableKernLiga(); - } - } else { - LayoutProcessor.enable(0); - } - } - } - } }