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> futures = new ArrayList<>(); + List failures = Collections.synchronizedList(new ArrayList<>()); + + try { + for (int i = 0; i < THREADS; i++) { + final int id = i; + Callable 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 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(); + } +} \ No newline at end of file diff --git a/engine/org.eclipse.birt.report.engine.emitter.pdf.tests/test/org/eclipse/birt/report/engine/emitter/pdf/glm-manual-check.rptdesign b/engine/org.eclipse.birt.report.engine.emitter.pdf.tests/test/org/eclipse/birt/report/engine/emitter/pdf/glm-manual-check.rptdesign new file mode 100644 index 0000000000..d10172b42f --- /dev/null +++ b/engine/org.eclipse.birt.report.engine.emitter.pdf.tests/test/org/eclipse/birt/report/engine/emitter/pdf/glm-manual-check.rptdesign @@ -0,0 +1,82 @@ + + + Eclipse BIRT Designer Version 4.25.0.qualifier + English + BIRT Issue 2444 - manual glyph layout check + mm + ltr + 96 + en_US + 1.7 + + + + + + a4 + + + + + + + + + + + + + + + + + + + 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 2329d8cc5d..671c446123 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 @@ -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 @@ -45,7 +45,6 @@ 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; @@ -53,8 +52,8 @@ 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; @@ -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(); @@ -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()); @@ -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(); - } - } - } } 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 fa3d539ad7..c8bf7b39e2 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,4 +1,5 @@ /******************************************************************************* + * Copyright (c) 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 @@ -26,6 +27,7 @@ import java.net.URL; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -48,13 +50,18 @@ import org.eclipse.birt.report.engine.ir.Expression; import org.eclipse.birt.report.engine.layout.emitter.IPage; import org.eclipse.birt.report.engine.layout.emitter.IPageDevice; +import org.eclipse.birt.report.engine.layout.pdf.font.FontMappingManagerFactory; import org.eclipse.birt.report.engine.nLayout.area.IArea; import org.eclipse.birt.report.engine.nLayout.area.impl.CellArea; import org.eclipse.birt.report.engine.nLayout.area.impl.ContainerArea; import org.openpdf.text.Document; import org.openpdf.text.DocumentException; +import org.openpdf.text.Font; +import org.openpdf.text.FontFactory; import org.openpdf.text.Rectangle; import org.openpdf.text.pdf.BaseFont; +import org.openpdf.text.pdf.GlyphLayoutFontManager.FontOptions; +import org.openpdf.text.pdf.GlyphLayoutManager; import org.openpdf.text.pdf.PdfArray; import org.openpdf.text.pdf.PdfBoolean; import org.openpdf.text.pdf.PdfContentByte; @@ -132,6 +139,14 @@ public class PDFPageDevice implements IPageDevice { private static final String PDF_UA_CONFORMANCE_2 = "PDF.UA-2"; private static final String PDF_UA_CONFORMANCE_NONE = "none"; + /** + * Font size passed to {@code loadFont}. The value is irrelevant for layout: it + * is only used to build the returned {@code Font} wrapper, which is discarded — + * the manager keys its AWT font map on the {@code BaseFont}, and the actual + * size is applied per draw via {@code setFontAndSize}. + */ + private static final float FONT_LOAD_SIZE = 1f; + /** PDF ICC color profile */ /** PDF ICC default color profile RGB */ private static final String PDF_ICC_PROFILE_DEFAULT = "sRGB IEC61966-2.1"; @@ -145,6 +160,29 @@ public class PDFPageDevice implements IPageDevice { */ protected Document doc = null; + /** + * The per-document glyph layout manager (issue #2444). It is null when the + * complex font layout feature is disabled. + */ + private GlyphLayoutManager glyphLayoutManager; + + /** Whether kerning and ligatures are enabled in the font configuration. */ + private boolean useKerningAndLigatures; + + /** Whether the kerning and ligature configuration has been read yet. */ + private boolean configRead; + + /** Fonts known NOT loadable into the manager - decided once, never retried. */ + private final Set managerUnloadableFonts = new HashSet<>(); + + /** + * Maps the base font BIRT resolved to the one the glyph layout manager created + * when loading it. The manager only recognises its own instance, so drawing + * must use the substitute for the layout to be applied. Also serves as the + * record of which fonts have been loaded. + */ + private final Map managerFontSubstitutes = new HashMap<>(); + /** * The Pdf Writer */ @@ -221,6 +259,15 @@ public class PDFPageDevice implements IPageDevice { /** System property of the JavaScript version */ private static final String PDF_GLYPH_SUBSTITUTION_PROPERTY_KEY = "birt.pdf.glyph.substitution.enabled"; //$NON-NLS-1$ + /** User property to enable per-document complex font layout */ + private final static String PDF_COMPLEX_FONT_LAYOUT = "PdfEmitter.ComplexFontLayoutEnabled"; //$NON-NLS-1$ + + /** + * System property to enable per-document complex font layout + * (GlyphLayoutManager) + */ + private static final String PDF_COMPLEX_FONT_LAYOUT_PROPERTY_KEY = "birt.pdf.complex.font.layout.enabled"; //$NON-NLS-1$ + protected Map userProperties; private String pdfVersion = "0"; @@ -1781,4 +1828,167 @@ private boolean isPdfFontGlypSubstitutionEnabled() { return enableGlyphSubstitution; } + + /** + * Evaluate whether per-document complex font layout (OpenPDF's + * {@code GlyphLayoutManager}) is enabled. Disabled by default; can be enabled + * via the system property {@code birt.pdf.complex.font.layout.enabled} or the + * report user property {@code PdfEmitter.ComplexFontLayoutEnabled}. + * + * @return {@code true} if complex font layout should be used + */ + private boolean isPdfComplexFontLayoutEnabled() { + boolean enabled = Boolean.getBoolean(PDF_COMPLEX_FONT_LAYOUT_PROPERTY_KEY); + + if (userProperties != null && !enabled && userProperties.containsKey(PDFPageDevice.PDF_COMPLEX_FONT_LAYOUT)) { + enabled = Boolean.parseBoolean(userProperties.get(PDFPageDevice.PDF_COMPLEX_FONT_LAYOUT).toString()); + } + return enabled; + } + + /** + * Decide whether the given font should be drawn through the glyph layout + * manager, loading it into the manager on first use. Only TrueType/OpenType + * fonts whose file can be resolved are eligible; base-14, Type1 and symbolic + * fonts return {@code false} and are drawn on the normal path, because + * {@code supportsFont} throws for any font not loaded through the manager. The + * outcome is cached per font, so each font is resolved and loaded only once. + * + * @param font the base font about to be drawn + * @return {@code true} if the manager should be attached for this font. Callers + * that attach the manager must also draw with + * {@link #getManagerFont(BaseFont)}. + */ + boolean useManagerForFont(BaseFont font) { + if (font == null) { + return false; + } + ensureGlyphLayoutInitialised(); + if (glyphLayoutManager == null) { + return false; + } + + if (managerFontSubstitutes.containsKey(font)) { + return true; + } + if (managerUnloadableFonts.contains(font)) { + return false; + } + // Only TrueType/OpenType fonts can be loaded into the manager. + if (font.getFontType() != BaseFont.FONT_TYPE_TTUNI) { + managerUnloadableFonts.add(font); + return false; + } + Object path = FontFactory.getFontImp().getFontPath(font.getPostscriptFontName()); + if (!(path instanceof String)) { + managerUnloadableFonts.add(font); + return false; + } + File file = new File((String) path); + if (!isOpenTypeFontFile(file)) { + managerUnloadableFonts.add(font); + return false; + } + return tryLoadFont(file, font); + } + + /** + * Get the per-document glyph layout manager, used by the page to attach or + * detach it per draw. Returns {@code null} when complex font layout is + * disabled. + * + * @return the glyph layout manager, or {@code null} + */ + GlyphLayoutManager getGlyphLayoutManager() { + return glyphLayoutManager; + } + + /** + * Get the base font to draw with. The glyph layout manager creates its own base + * font when loading, and only recognises that instance, so drawing must use it + * rather than the one BIRT resolved. Returns the given font unchanged when it + * is not handled by the manager. + * + * @param font the base font BIRT resolved for the text + * @return the font to pass to the content byte + */ + BaseFont getManagerFont(BaseFont font) { + BaseFont substitute = managerFontSubstitutes.get(font); + return substitute == null ? font : substitute; + } + + /** + * Test whether a file is a TrueType/OpenType font file (by {@code .ttf} / + * {@code .otf} extension). + * + * @param file the file to test + * @return {@code true} if it is a loadable TrueType/OpenType font file + */ + private boolean isOpenTypeFontFile(File file) { + if (file == null || !file.isFile()) { + return false; + } + String n = file.getName().toLowerCase(Locale.ROOT); + return n.endsWith(".ttf") || n.endsWith(".otf"); + } + + /** + * Attempt to load a single font file into the glyph layout manager, applying + * kerning and ligatures if they are enabled in the font configuration. On + * success the base font the manager created is recorded against the given font, + * so drawing can use it; on failure the font is recorded as unloadable. Either + * way the decision is made only once per font. + *

+ * 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); - } - } - } - } }