diff --git a/src/java.desktop/share/classes/sun/print/PSPrinterJob.java b/src/java.desktop/share/classes/sun/print/PSPrinterJob.java index eb981c92a12..61af64a3d58 100644 --- a/src/java.desktop/share/classes/sun/print/PSPrinterJob.java +++ b/src/java.desktop/share/classes/sun/print/PSPrinterJob.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -1224,120 +1224,123 @@ public class PSPrinterJob extends RasterPrinterJob { return (psFonts == null) ? 0 : psFonts.length; } - protected boolean textOut(Graphics g, String str, float x, float y, - Font mLastFont, FontRenderContext frc, - float width) { - boolean didText = true; - + protected boolean textOut(Graphics g, String str, float x, float y, + Font mLastFont, FontRenderContext frc, + float width) { + /* If we don't have fonts, use 2D path instead. */ if (mFontProps == null) { return false; - } else { - prepDrawing(); + } - /* On-screen drawString renders most control chars as the missing - * glyph and have the non-zero advance of that glyph. - * Exceptions are \t, \n and \r which are considered zero-width. - * Postscript handles control chars mostly as a missing glyph. - * But we use 'ashow' specifying a width for the string which - * assumes zero-width for those three exceptions, and Postscript - * tries to squeeze the extra char in, with the result that the - * glyphs look compressed or even overlap. - * So exclude those control chars from the string sent to PS. + /* On-screen drawString renders most control chars as the missing + * glyph and have the non-zero advance of that glyph. + * Exceptions are \t, \n and \r which are considered zero-width. + * Postscript handles control chars mostly as a missing glyph. + * But we use 'ashow' specifying a width for the string which + * assumes zero-width for those three exceptions, and Postscript + * tries to squeeze the extra char in, with the result that the + * glyphs look compressed or even overlap. + * So exclude those control chars from the string sent to PS. + */ + str = removeControlChars(str); + if (str.isEmpty()) { + return true; + } + + /* If AWT can't convert all chars, use 2D path instead. */ + FontAccess access = FontAccess.getFontAccess(); + PlatformFont peer = (PlatformFont) access.getFontPeer(mLastFont); + CharsetString[] acs = peer.makeMultiCharsetString(str, false); + if (acs == null) { + return false; + } + + /* Get an array of indices into our PostScript name + * table. If all of the runs can not be converted + * to PostScript fonts then null is returned and + * we'll want to fall back to printing the text + * as shapes. + */ + int[] psFonts = getPSFontIndexArray(mLastFont, acs); + if (psFonts == null) { + return false; + } + + /* Prepare graphics context, now that we know we can handle the text. */ + prepDrawing(); + + /* Draw each string segment. */ + for (int i = 0; i < acs.length; i++){ + CharsetString cs = acs[i]; + CharsetEncoder fontCS = cs.fontDescriptor.encoder; + + StringBuilder nativeStr = new StringBuilder(); + byte[] strSeg = new byte[cs.length * 2]; + int len = 0; + try { + ByteBuffer bb = ByteBuffer.wrap(strSeg); + fontCS.encode(CharBuffer.wrap(cs.charsetChars, + cs.offset, + cs.length), + bb, true); + bb.flip(); + len = bb.limit(); + } catch (IllegalStateException | CoderMalfunctionError xx){ + continue; + } + /* The width to fit to may either be specified, + * or calculated. Specifying by the caller is only + * valid if the text does not need to be decomposed + * into multiple calls. */ - str = removeControlChars(str); - if (str.length() == 0) { + float desiredWidth; + if (acs.length == 1 && width != 0f) { + desiredWidth = width; + } else { + Rectangle2D r2d = + mLastFont.getStringBounds(cs.charsetChars, + cs.offset, + cs.offset+cs.length, + frc); + desiredWidth = (float)r2d.getWidth(); + } + /* unprintable chars had width of 0, causing a PS error + */ + if (desiredWidth == 0) { return true; } - PlatformFont peer = (PlatformFont) FontAccess.getFontAccess() - .getFontPeer(mLastFont); - CharsetString[] acs = peer.makeMultiCharsetString(str, false); - if (acs == null) { - /* AWT can't convert all chars so use 2D path */ - return false; + nativeStr.append('<'); + for (int j = 0; j < len; j++){ + byte b = strSeg[j]; + // to avoid encoding conversion with println() + String hexS = Integer.toHexString(b); + int length = hexS.length(); + if (length > 2) { + hexS = hexS.substring(length - 2, length); + } else if (length == 1) { + hexS = "0" + hexS; + } else if (length == 0) { + hexS = "00"; + } + nativeStr.append(hexS); } - /* Get an array of indices into our PostScript name - * table. If all of the runs can not be converted - * to PostScript fonts then null is returned and - * we'll want to fall back to printing the text - * as shapes. - */ - int[] psFonts = getPSFontIndexArray(mLastFont, acs); - if (psFonts != null) { - - for (int i = 0; i < acs.length; i++){ - CharsetString cs = acs[i]; - CharsetEncoder fontCS = cs.fontDescriptor.encoder; - - StringBuilder nativeStr = new StringBuilder(); - byte[] strSeg = new byte[cs.length * 2]; - int len = 0; - try { - ByteBuffer bb = ByteBuffer.wrap(strSeg); - fontCS.encode(CharBuffer.wrap(cs.charsetChars, - cs.offset, - cs.length), - bb, true); - bb.flip(); - len = bb.limit(); - } catch (IllegalStateException | CoderMalfunctionError xx){ - continue; - } - /* The width to fit to may either be specified, - * or calculated. Specifying by the caller is only - * valid if the text does not need to be decomposed - * into multiple calls. - */ - float desiredWidth; - if (acs.length == 1 && width != 0f) { - desiredWidth = width; - } else { - Rectangle2D r2d = - mLastFont.getStringBounds(cs.charsetChars, - cs.offset, - cs.offset+cs.length, - frc); - desiredWidth = (float)r2d.getWidth(); - } - /* unprintable chars had width of 0, causing a PS error - */ - if (desiredWidth == 0) { - return didText; - } - nativeStr.append('<'); - for (int j = 0; j < len; j++){ - byte b = strSeg[j]; - // to avoid encoding conversion with println() - String hexS = Integer.toHexString(b); - int length = hexS.length(); - if (length > 2) { - hexS = hexS.substring(length - 2, length); - } else if (length == 1) { - hexS = "0" + hexS; - } else if (length == 0) { - hexS = "00"; - } - nativeStr.append(hexS); - } - nativeStr.append('>'); - /* This comment costs too much in output file size */ + nativeStr.append('>'); + /* This comment costs too much in output file size */ // mPSStream.println("% Font[" + mLastFont.getName() + ", " + // FontConfiguration.getStyleString(mLastFont.getStyle()) + ", " // + mLastFont.getSize2D() + "]"); - getGState().emitPSFont(psFonts[i], mLastFont.getSize2D()); + getGState().emitPSFont(psFonts[i], mLastFont.getSize2D()); - // out String - mPSStream.println(nativeStr.toString() + " " + - desiredWidth + " " + x + " " + y + " " + - DrawStringName); - x += desiredWidth; - } - } else { - didText = false; - } + // out String + mPSStream.println(nativeStr.toString() + " " + + desiredWidth + " " + x + " " + y + " " + + DrawStringName); + x += desiredWidth; } - return didText; - } + return true; + } + /** * Set the current path rule to be either * {@code FILL_EVEN_ODD} (using the @@ -1802,7 +1805,6 @@ public class PSPrinterJob extends RasterPrinterJob { return mClip == null || mClip.equals(clip); } - void emitPSClip(Shape clip) { if (clip != null && (mClip == null || mClip.equals(clip) == false)) { @@ -1937,7 +1939,8 @@ public class PSPrinterJob extends RasterPrinterJob { protected void deviceFill(PathIterator pathIter, Color color, AffineTransform tx, Shape clip) { - if (Double.isNaN(tx.getScaleX()) || + if (pathIter.isDone() || + Double.isNaN(tx.getScaleX()) || Double.isNaN(tx.getScaleY()) || Double.isNaN(tx.getShearX()) || Double.isNaN(tx.getShearY()) || diff --git a/test/jdk/javax/print/PostScriptLeanTest.java b/test/jdk/javax/print/PostScriptLeanTest.java new file mode 100644 index 00000000000..7a02cfb2c7a --- /dev/null +++ b/test/jdk/javax/print/PostScriptLeanTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Polygon; +import java.awt.geom.AffineTransform; +import java.awt.print.PageFormat; +import java.awt.print.Printable; +import java.awt.print.PrinterException; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +import javax.print.Doc; +import javax.print.DocFlavor; +import javax.print.DocPrintJob; +import javax.print.SimpleDoc; +import javax.print.StreamPrintService; +import javax.print.StreamPrintServiceFactory; +import javax.print.event.PrintJobAdapter; +import javax.print.event.PrintJobEvent; + +/* + * @test + * @bug 8349932 + * @summary Verifies that generated PostScript omits unnecessary graphics state commands. + */ +public class PostScriptLeanTest { + + public static void main(String[] args) throws Exception { + + DocFlavor flavor = DocFlavor.SERVICE_FORMATTED.PRINTABLE; + String mime = "application/postscript"; + StreamPrintServiceFactory[] factories = StreamPrintServiceFactory.lookupStreamPrintServiceFactories(flavor, mime); + if (factories.length == 0) { + throw new RuntimeException("Unable to find PostScript print service factory"); + } + + StreamPrintServiceFactory factory = factories[0]; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + StreamPrintService service = factory.getPrintService(output); + DocPrintJob job = service.createPrintJob(); + + PrintJobMonitor monitor = new PrintJobMonitor(); + job.addPrintJobListener(monitor); + + Printable printable = new TestPrintable(); + Doc doc = new SimpleDoc(printable, flavor, null); + job.print(doc, null); + monitor.waitForJobToFinish(); + + byte[] separator = System.lineSeparator().getBytes(StandardCharsets.UTF_8); + byte prefix = separator[separator.length - 1]; + byte postfix = separator[0]; + + int paths = 0; + byte[] ps = output.toByteArray(); + for (int i = 1; i + 1 < ps.length; i++) { + if (ps[i - 1] == prefix && ps[i] == 'N' && ps[i + 1] == postfix) { + paths++; // found a "newpath" command (aliased to "N") + } + } + + if (paths != 1) { + throw new RuntimeException("Expected 1 path, but found " + paths + " paths"); + } + } + + private static final class TestPrintable implements Printable { + @Override + public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException { + if (pageIndex > 0) { + return NO_SUCH_PAGE; + } + Font font1 = new Font("SansSerif", Font.PLAIN, 20); + Font font2 = font1.deriveFont(AffineTransform.getQuadrantRotateInstance(1)); + graphics.setFont(font1); + graphics.drawString("XX", 300, 300); // not ignored, adds a path + graphics.setFont(font2); + graphics.drawString("\r", 300, 350); // ignored, nothing to draw, no path added + graphics.drawString("\n", 300, 400); // ignored, nothing to draw, no path added + graphics.drawString("\t", 300, 450); // ignored, nothing to draw, no path added + graphics.drawPolygon(new Polygon()); // empty polygon, nothing to draw, no path added + return PAGE_EXISTS; + } + } + + private static class PrintJobMonitor extends PrintJobAdapter { + private boolean finished; + @Override + public void printJobCanceled(PrintJobEvent pje) { + finished(); + } + @Override + public void printJobCompleted(PrintJobEvent pje) { + finished(); + } + @Override + public void printJobFailed(PrintJobEvent pje) { + finished(); + } + @Override + public void printJobNoMoreEvents(PrintJobEvent pje) { + finished(); + } + private synchronized void finished() { + finished = true; + notify(); + } + public synchronized void waitForJobToFinish() { + try { + while (!finished) { + wait(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } +}