Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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).
*
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*/
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);
Comment thread
hyrahul marked this conversation as resolved.
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<Future<File>> futures = new ArrayList<>();
List<String> failures = Collections.synchronizedList(new ArrayList<>());

try {
for (int i = 0; i < THREADS; i++) {
final int id = i;
Callable<File> task = () -> {
try {
return renderOnce(id);
} catch (Throwable t) {
failures.add("thread " + id + ": " + t.getClass().getSimpleName() + ": " + t.getMessage());
t.printStackTrace();
return null;
}
};
futures.add(pool.submit(task));
}

pool.shutdown();
boolean finished = pool.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertTrue("Concurrent renders did not finish within " + TIMEOUT_SECONDS + "s", finished);
} finally {
pool.shutdownNow();
}

if (!failures.isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append(failures.size()).append(" of ").append(THREADS).append(" concurrent renders failed:");
for (String failure : failures) {
sb.append("\n ").append(failure);
}
fail(sb.toString());
}

// Every task must have produced a real, non-empty PDF.
for (Future<File> future : futures) {
File pdf = future.get();
assertNotNull("Render produced no output", pdf);
assertTrue("Missing output: " + pdf, pdf.isFile());
assertTrue("Empty output: " + pdf, pdf.length() > 0L);
}
}

/**
* Renders the shared design to a unique per-thread PDF against the shared
* engine. Uses {@link EngineCase#createRunAndRenderTask(String)}, which opens
* the design by path directly (no shared design file), so it is safe to call
* concurrently.
*/
private File renderOnce(int id) throws Exception {
File out = new File(outputDir, "render-" + id + ".pdf");

IRunAndRenderTask task = createRunAndRenderTask(DESIGN);
try {
PDFRenderOption options = new PDFRenderOption();
options.setOutputFormat("pdf");
options.setOutputFileName(out.getAbsolutePath());
task.setRenderOption(options);
task.run();
} finally {
task.close();
}
return out;
}

/** Deletes a file or directory tree, ignoring files that cannot be removed. */
private static void deleteRecursively(File file) {
if (file == null || !file.exists()) {
return;
}
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteRecursively(child);
}
}
file.delete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<report xmlns="http://www.eclipse.org/birt/2005/design" version="3.2.27" id="1">
<property name="createdBy">Eclipse BIRT Designer Version 4.25.0.qualifier</property>
<property name="language">English</property>
<text-property name="title">BIRT Issue 2444 - manual glyph layout check</text-property>
<property name="units">mm</property>
<property name="bidiLayoutOrientation">ltr</property>
<property name="imageDPI">96</property>
<property name="locale">en_US</property>
<property name="pdfVersion">1.7</property>
<styles>
<style name="report" id="2">
<property name="fontFamily">"Noto Sans"</property>
</style>
</styles>
<page-setup>
<simple-master-page name="NewSimpleMasterPage" id="3">
<property name="type">a4</property>
</simple-master-page>
</page-setup>
<body>
<label id="10">
<property name="fontSize">10pt</property>
<property name="marginBottom">8pt</property>
<text-property name="text">Manual check for issue #2444. Render this report twice: once with -Dbirt.pdf.complex.font.layout.enabled=false and once with =true, then compare the two PDFs side by side. Kerning is visible in the letter pairs below (AV, VA, WA, AW, Ma, Va).</text-property>
</label>

<label id="11">
<property name="fontSize">12pt</property>
<property name="fontWeight">bold</property>
<property name="marginTop">8pt</property>
<text-property name="text">Kerning (Latin, Noto Sans)</text-property>
</label>
<label id="12">
<property name="fontSize">40pt</property>
<property name="marginBottom">6pt</property>
<text-property name="text">AV cables VA Maya Vase WM WA AW with Kerning</text-property>
</label>
<label id="13">
<property name="fontSize">40pt</property>
<property name="fontWeight">bold</property>
<property name="marginBottom">6pt</property>
<text-property name="text">AV cables VA Maya Vase WM WA AW with Kerning</text-property>
</label>

<label id="14">
<property name="fontSize">12pt</property>
<property name="fontWeight">bold</property>
<property name="marginTop">8pt</property>
<text-property name="text">Ligatures (Latin, Noto Sans)</text-property>
</label>
<label id="15">
<property name="fontSize">40pt</property>
<property name="marginBottom">6pt</property>
<text-property name="text">office waffle affix fjord flight</text-property>
</label>

<label id="16">
<property name="fontSize">12pt</property>
<property name="fontWeight">bold</property>
<property name="marginTop">8pt</property>
<text-property name="text">Arabic (needs an Arabic-capable font to be registered)</text-property>
</label>
<label id="17">
<property name="fontSize">28pt</property>
<property name="marginBottom">6pt</property>
<text-property name="text">مرحبا بالعالم العربية</text-property>
</label>

<label id="18">
<property name="fontSize">12pt</property>
<property name="fontWeight">bold</property>
<property name="marginTop">8pt</property>
<text-property name="text">Chinese (needs a CJK-capable font to be registered)</text-property>
</label>
<label id="19">
<property name="fontSize">28pt</property>
<property name="marginBottom">6pt</property>
<text-property name="text">中文测试 汉字 排版</text-property>
</label>
</body>
</report>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2004, 2007, 2024, 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
Expand Down Expand Up @@ -45,16 +45,15 @@
import org.openpdf.text.Image;
import org.openpdf.text.Rectangle;
import org.openpdf.text.pdf.BaseFont;
import org.openpdf.text.pdf.LayoutProcessor;
import org.openpdf.text.pdf.PdfAction;
import org.openpdf.text.pdf.PdfAnnotation;
import org.openpdf.text.pdf.PdfArray;
import org.openpdf.text.pdf.PdfBorderDictionary;
import org.openpdf.text.pdf.PdfContentByte;
import org.openpdf.text.pdf.PdfDestination;
import org.openpdf.text.pdf.PdfDictionary;
import org.openpdf.text.pdf.PdfObject;
import org.openpdf.text.pdf.PdfName;
import org.openpdf.text.pdf.PdfObject;
import org.openpdf.text.pdf.PdfRectangle;
import org.openpdf.text.pdf.PdfString;
import org.openpdf.text.pdf.PdfStructureElement;
Expand Down Expand Up @@ -614,7 +613,6 @@ private void drawText(String text, float textX, float textY, FontInfo fontInfo,
}

BaseFont font = getBaseFont(fontInfo);
validateSymbolicFont(font);
font.setIncludeCidSet(this.pageDevice.isIncludeCidSet());

float fontSize = fontInfo.getFontSize();
Expand All @@ -636,6 +634,16 @@ private void drawText(String text, float textX, float textY, FontInfo fontInfo,
"PDF/A: " + fontInfo.getFontName() + " not embeddable." + e.getMessage());
}
}
// issue #2444: OpenPDF's showText routes through the glyph layout manager when
// one is set on the writer. The manager only recognises the base font it
// created itself, so the font is swapped for its own instance; fonts it cannot
// load (base-14/Type1/symbolic) draw on the normal path with no manager set.
if (pageDevice.useManagerForFont(font)) {
writer.setGlyphLayoutManager(pageDevice.getGlyphLayoutManager());
font = pageDevice.getManagerFont(font);
} else {
writer.setGlyphLayoutManager(null);
}
contentByte.setFontAndSize(font, fontSize);
} catch (IllegalArgumentException iae) {
logger.log(Level.WARNING, iae.getMessage());
Expand Down Expand Up @@ -890,20 +898,4 @@ public void endArtifact() {
public boolean isInArtifact() {
return artifactDepth > 0;
}

/**
* Validate the font property of "specific". This is a marker of symbolic font
* and needs enabled handling of OpenPDF LayoutProcessor for kerning to display
* the font correctly.
*/
private void validateSymbolicFont(BaseFont font) {
synchronized (LayoutProcessor.class) {
if (font.isFontSpecific()) {
if (LayoutProcessor.isEnabled()) {
LayoutProcessor.disable();
}
LayoutProcessor.setKerning();
}
}
}
}
Loading
Loading