diff --git a/openpdf-renderer/src/main/java/org/openpdf/renderer/core/OpenPdfCorePageRenderer.java b/openpdf-renderer/src/main/java/org/openpdf/renderer/core/OpenPdfCorePageRenderer.java index f362219b3..64f1c0421 100644 --- a/openpdf-renderer/src/main/java/org/openpdf/renderer/core/OpenPdfCorePageRenderer.java +++ b/openpdf-renderer/src/main/java/org/openpdf/renderer/core/OpenPdfCorePageRenderer.java @@ -81,7 +81,8 @@ * {@code cs}, {@code CS}, {@code sc}, {@code SC}, {@code scn}, {@code SCN} *
Fill-only text (the common case, mode 0) uses {@link Graphics2D#drawString} + * directly. Stroke modes (1, 2, 5, 6) need the glyph outlines so they can be + * stroked with the current line width / dash / join settings, so those go through + * {@link Font#createGlyphVector}. Modes 4-7 (add to clipping path) fall back to + * their non-clipping counterpart since text clipping isn't implemented.
+ */ + private void drawGlyphs(String text) { + boolean fill = fillsRenderMode(state.renderMode); + boolean stroke = strokesRenderMode(state.renderMode); + Font awtFont = mapFont(state.font, state.fontSize); + if (fill && !stroke) { + g2.setFont(awtFont); + g2.setColor(state.fillColor); + g2.drawString(text, 0f, 0f); + return; + } + Shape outline = awtFont.createGlyphVector(g2.getFontRenderContext(), text).getOutline(0f, 0f); + if (fill) { + g2.setColor(state.fillColor); + g2.fill(outline); + } + if (stroke) { + g2.setColor(state.strokeColor); + g2.setStroke(new BasicStroke( + effectiveLineWidth(), + state.lineCap, state.lineJoin, state.miterLimit, + state.dashPattern, state.dashPhase)); + g2.draw(outline); + } + } + + /** + * PDF §9.3.3 text rendering modes 3 and 7 render nothing (7 only adds the glyphs to + * the clipping path, which we don't implement, so it degrades to fully invisible). + */ + private static boolean isInvisibleRenderMode(int mode) { + return mode == 3 || mode == 7; + } + + private static boolean fillsRenderMode(int mode) { + return mode == 0 || mode == 2 || mode == 4 || mode == 6; + } + + private static boolean strokesRenderMode(int mode) { + return mode == 1 || mode == 2 || mode == 5 || mode == 6; + } + private void showTextArray(PdfArray array) { for (PdfObject obj : array.getElements()) { if (obj instanceof PdfString s) { @@ -1904,6 +1957,11 @@ private static final class GState { float leading; float horizontalScaling = 1.0f; float textRise; + // PDF §9.3.3 text rendering mode (Tr): 0=fill, 1=stroke, 2=fill+stroke, 3=invisible, + // 4-7 add the glyphs to the clipping path in addition to the 0-3 behavior. Clipping + // is not implemented; modes 4-7 fall back to their 0-3 counterpart (see + // fillsRenderMode/strokesRenderMode/isInvisibleRenderMode). + int renderMode; GState() { } @@ -1929,6 +1987,7 @@ private static final class GState { this.leading = other.leading; this.horizontalScaling = other.horizontalScaling; this.textRise = other.textRise; + this.renderMode = other.renderMode; // hasPendingClip / pendingClipRule are intentionally not copied: // the W / W* operators apply to the current path before any q/Q boundary. } diff --git a/openpdf-renderer/src/test/java/org/openpdf/renderer/core/OpenPdfCorePageRendererOperatorsTest.java b/openpdf-renderer/src/test/java/org/openpdf/renderer/core/OpenPdfCorePageRendererOperatorsTest.java index f63520123..404b73442 100644 --- a/openpdf-renderer/src/test/java/org/openpdf/renderer/core/OpenPdfCorePageRendererOperatorsTest.java +++ b/openpdf-renderer/src/test/java/org/openpdf/renderer/core/OpenPdfCorePageRendererOperatorsTest.java @@ -519,6 +519,78 @@ void rendersPdfPTableWithBordersFillsAndText() throws Exception { } } + /** + * PDF §9.3.3: text rendering mode 3 ({@code Tr 3}) means the glyphs are neither + * filled nor stroked — the "invisible text" mode used by scanned-PDF / + * OCR text layers, where an invisible text layer sits on top of a page-image + * XObject so the text stays selectable/searchable without being visible. Before + * {@code Tr} support, the renderer always filled text regardless of render mode, + * which would incorrectly paint OCR text layers over their background image. + */ + @Test + void invisibleTextRenderModeDoesNotPaintGlyphs() throws Exception { + byte[] pdf = buildPdf(cb -> { + org.openpdf.text.pdf.BaseFont bf = org.openpdf.text.pdf.BaseFont + .createFont(org.openpdf.text.pdf.BaseFont.HELVETICA, + org.openpdf.text.pdf.BaseFont.WINANSI, + org.openpdf.text.pdf.BaseFont.NOT_EMBEDDED); + cb.beginText(); + cb.setFontAndSize(bf, 48f); + cb.setTextRenderingMode(PdfContentByte.TEXT_RENDER_MODE_INVISIBLE); + cb.setTextMatrix(20f, 100f); + cb.showText("HIDDEN"); + cb.endText(); + }); + + try (OpenPdfCoreRenderer r = new OpenPdfCoreRenderer(pdf)) { + List