From 8e51ff70d896aeb5b35e6bb6b00f1818d67c99e7 Mon Sep 17 00:00:00 2001 From: Gennadiy Krivoshein Date: Thu, 24 Apr 2025 16:06:29 +0000 Subject: [PATCH] 8315113: Print request Chromaticity.MONOCHROME attribute does not work on macOS Reviewed-by: prr, psadhukhan --- .../classes/sun/lwawt/macosx/CPrinterJob.java | 13 +- .../sun/print/GrayscaleProxyGraphics2D.java | 211 ++++++++++++++ .../classes/sun/print/IPPPrintService.java | 13 +- .../print/attribute/MonochromePrintTest.java | 272 ++++++++++++++++++ 4 files changed, 505 insertions(+), 4 deletions(-) create mode 100644 src/java.desktop/share/classes/sun/print/GrayscaleProxyGraphics2D.java create mode 100644 test/jdk/javax/print/attribute/MonochromePrintTest.java diff --git a/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterJob.java b/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterJob.java index cb7333c79d6..1ca94eb3f51 100644 --- a/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterJob.java +++ b/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterJob.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 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 @@ -36,6 +36,7 @@ import java.util.concurrent.atomic.AtomicReference; import javax.print.*; import javax.print.attribute.PrintRequestAttributeSet; import javax.print.attribute.HashPrintRequestAttributeSet; +import javax.print.attribute.standard.Chromaticity; import javax.print.attribute.standard.Copies; import javax.print.attribute.standard.Destination; import javax.print.attribute.standard.Media; @@ -73,6 +74,8 @@ public final class CPrinterJob extends RasterPrinterJob { private Throwable printerAbortExcpn; + private boolean monochrome = false; + // This is the NSPrintInfo for this PrinterJob. Protect multi thread // access to it. It is used by the pageDialog, jobDialog, and printLoop. // This way the state of these items is shared across these calls. @@ -212,6 +215,11 @@ public final class CPrinterJob extends RasterPrinterJob { setPageRange(-1, -1); } } + + PrintService service = getPrintService(); + Chromaticity chromaticity = (Chromaticity)attributes.get(Chromaticity.class); + monochrome = chromaticity == Chromaticity.MONOCHROME && service != null && + service.isAttributeCategorySupported(Chromaticity.class); } private void setPageRangeAttribute(int from, int to, boolean isRangeSet) { @@ -788,6 +796,9 @@ public final class CPrinterJob extends RasterPrinterJob { Graphics2D pathGraphics = new CPrinterGraphics(delegate, printerJob); // Just stores delegate into an ivar Rectangle2D pageFormatArea = getPageFormatArea(page); initPrinterGraphics(pathGraphics, pageFormatArea); + if (monochrome) { + pathGraphics = new GrayscaleProxyGraphics2D(pathGraphics, printerJob); + } painter.print(pathGraphics, FlipPageFormat.getOriginal(page), pageIndex); delegate.dispose(); delegate = null; diff --git a/src/java.desktop/share/classes/sun/print/GrayscaleProxyGraphics2D.java b/src/java.desktop/share/classes/sun/print/GrayscaleProxyGraphics2D.java new file mode 100644 index 00000000000..25f32181b71 --- /dev/null +++ b/src/java.desktop/share/classes/sun/print/GrayscaleProxyGraphics2D.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, BELLSOFT. 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package sun.print; + + +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.LinearGradientPaint; +import java.awt.Paint; +import java.awt.RadialGradientPaint; +import java.awt.TexturePaint; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ImageObserver; +import java.awt.image.RenderedImage; +import java.awt.print.PrinterJob; + +/** + * Proxy class to print with grayscale. + * Convert Colors, Paints and Images to the grayscale. + * + */ +public class GrayscaleProxyGraphics2D extends ProxyGraphics2D { + + /** + * The new ProxyGraphics2D will forward all graphics + * calls to 'graphics'. + * + * @param graphics + * @param printerJob + */ + public GrayscaleProxyGraphics2D(Graphics2D graphics, PrinterJob printerJob) { + super(graphics, printerJob); + } + + @Override + public void setBackground(Color color) { + Color gcolor = getGrayscaleColor(color); + super.setBackground(gcolor); + } + + @Override + public void setColor(Color c) { + Color gcolor = getGrayscaleColor(c); + super.setColor(gcolor); + } + + @Override + public void setPaint(Paint paint) { + if (paint instanceof Color color) { + super.setPaint(getGrayscaleColor(color)); + } else if (paint instanceof TexturePaint texturePaint) { + super.setPaint(new TexturePaint(getGrayscaleImage(texturePaint.getImage()), texturePaint.getAnchorRect())); + } else if (paint instanceof GradientPaint gradientPaint) { + super.setPaint(new GradientPaint(gradientPaint.getPoint1(), + getGrayscaleColor(gradientPaint.getColor1()), + gradientPaint.getPoint2(), + getGrayscaleColor(gradientPaint.getColor2()), + gradientPaint.isCyclic())); + } else if (paint instanceof LinearGradientPaint linearGradientPaint) { + Color[] colors = new Color[linearGradientPaint.getColors().length]; + Color[] oldColors = linearGradientPaint.getColors(); + for (int i = 0; i < colors.length; i++) { + colors[i] = getGrayscaleColor(oldColors[i]); + } + super.setPaint(new LinearGradientPaint(linearGradientPaint.getStartPoint(), + linearGradientPaint.getEndPoint(), + linearGradientPaint.getFractions(), + colors, + linearGradientPaint.getCycleMethod(), + linearGradientPaint.getColorSpace(), + linearGradientPaint.getTransform() + )); + } else if (paint instanceof RadialGradientPaint radialGradientPaint) { + Color[] colors = new Color[radialGradientPaint.getColors().length]; + Color[] oldColors = radialGradientPaint.getColors(); + for (int i = 0; i < colors.length; i++) { + colors[i] = getGrayscaleColor(oldColors[i]); + } + super.setPaint(new RadialGradientPaint(radialGradientPaint.getCenterPoint(), + radialGradientPaint.getRadius(), + radialGradientPaint.getFocusPoint(), + radialGradientPaint.getFractions(), + colors, + radialGradientPaint.getCycleMethod(), + radialGradientPaint.getColorSpace(), + radialGradientPaint.getTransform())); + } else if (paint == null) { + super.setPaint(paint); + } else { + throw new IllegalArgumentException("Unsupported Paint"); + } + } + + @Override + public void drawRenderedImage(RenderedImage img, AffineTransform xform) { + BufferedImage grayImage = new BufferedImage(img.getWidth() + img.getTileWidth(), + img.getHeight() + img.getTileHeight(), BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g2 = grayImage.createGraphics(); + g2.drawRenderedImage(img, new AffineTransform()); + g2.dispose(); + super.drawRenderedImage(getGrayscaleImage(grayImage), xform); + } + + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, + Color bgcolor, ImageObserver observer) { + return super.drawImage(getGrayscaleImage(img), dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, + ImageObserver observer) { + return super.drawImage(getGrayscaleImage(img), dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) { + return super.drawImage(getGrayscaleImage(img), x, y, width, height, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { + return super.drawImage(getGrayscaleImage(img), x, y, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) { + return super.drawImage(getGrayscaleImage(img), x, y, width, height, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, ImageObserver observer) { + return super.drawImage(getGrayscaleImage(img), x, y, observer); + } + + @Override + public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { + super.drawImage(getGrayscaleImage(img), op, x, y); + } + + @Override + public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { + return super.drawImage(getGrayscaleImage(img), xform, obs); + } + + /** + * Returns grayscale variant of the input Color + * @param color color to transform to grayscale + * @return grayscale color + */ + private Color getGrayscaleColor(Color color) { + if (color == null) { + return null; + } + float[] gcolor = color.getComponents(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); + return switch (gcolor.length) { + case 1 -> new Color(gcolor[0], gcolor[0], gcolor[0]); + case 2 -> new Color(gcolor[0], gcolor[0], gcolor[0], gcolor[1]); + default -> throw new IllegalArgumentException("Unknown grayscale color. " + + "Expected 1 or 2 components, received " + gcolor.length + " components."); + }; + } + + /** + * Converts Image to a grayscale + * @param img colored image + * @return grayscale BufferedImage + */ + private BufferedImage getGrayscaleImage(Image img) { + if (img == null) { + return null; + } + BufferedImage grayImage = new BufferedImage(img.getWidth(null), img.getHeight(null), + BufferedImage.TYPE_BYTE_GRAY); + Graphics grayGraphics = grayImage.getGraphics(); + grayGraphics.drawImage(img, 0, 0, null); + grayGraphics.dispose(); + return grayImage; + } + +} diff --git a/src/java.desktop/unix/classes/sun/print/IPPPrintService.java b/src/java.desktop/unix/classes/sun/print/IPPPrintService.java index 53089401e33..96fcdef3a52 100644 --- a/src/java.desktop/unix/classes/sun/print/IPPPrintService.java +++ b/src/java.desktop/unix/classes/sun/print/IPPPrintService.java @@ -572,8 +572,15 @@ public class IPPPrintService implements PrintService, SunPrinterJobService { flavor.equals(DocFlavor.SERVICE_FORMATTED.PAGEABLE) || flavor.equals(DocFlavor.SERVICE_FORMATTED.PRINTABLE) || !isIPPSupportedImages(flavor.getMimeType())) { - Chromaticity[]arr = new Chromaticity[1]; - arr[0] = Chromaticity.COLOR; + Chromaticity[] arr; + if (PrintServiceLookupProvider.isMac()) { + arr = new Chromaticity[2]; + arr[0] = Chromaticity.COLOR; + arr[1] = Chromaticity.MONOCHROME; + } else { + arr = new Chromaticity[1]; + arr[0] = Chromaticity.COLOR; + } return (arr); } else { return null; @@ -1400,7 +1407,7 @@ public class IPPPrintService implements PrintService, SunPrinterJobService { flavor.equals(DocFlavor.SERVICE_FORMATTED.PAGEABLE) || flavor.equals(DocFlavor.SERVICE_FORMATTED.PRINTABLE) || !isIPPSupportedImages(flavor.getMimeType())) { - return attr == Chromaticity.COLOR; + return PrintServiceLookupProvider.isMac() || attr == Chromaticity.COLOR; } else { return false; } diff --git a/test/jdk/javax/print/attribute/MonochromePrintTest.java b/test/jdk/javax/print/attribute/MonochromePrintTest.java new file mode 100644 index 00000000000..841183f04bb --- /dev/null +++ b/test/jdk/javax/print/attribute/MonochromePrintTest.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, BELLSOFT. 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 javax.print.PrintService; +import javax.print.PrintServiceLookup; +import javax.print.attribute.HashPrintRequestAttributeSet; +import javax.print.attribute.PrintRequestAttributeSet; +import javax.print.attribute.Size2DSyntax; +import javax.print.attribute.standard.Chromaticity; +import javax.print.attribute.standard.MediaSize; +import javax.print.attribute.standard.MediaSizeName; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.ListCellRenderer; +import javax.swing.border.EmptyBorder; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GridLayout; +import java.awt.LinearGradientPaint; +import java.awt.MultipleGradientPaint; +import java.awt.Paint; +import java.awt.RadialGradientPaint; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.awt.print.PageFormat; +import java.awt.print.Printable; +import java.awt.print.PrinterException; +import java.awt.print.PrinterJob; +import java.util.ArrayList; +import java.util.List; + +/* + * @test + * @library /java/awt/regtesthelpers + * @build PassFailJFrame + * @bug 8315113 + * @key printer + * @requires (os.family == "mac") + * @summary javax.print: Support monochrome printing + * @run main/manual MonochromePrintTest + */ + +public class MonochromePrintTest { + + private static final String INSTRUCTIONS = """ + This test checks availability of the monochrome printing + on color printers. + To be able to run this test it is required to have a color + printer configured in your user environment. + Test's steps: + - Choose a printer. + - Press 'Print' button. + Visual inspection of the printed pages is needed. + A passing test will print two pages with + color and grayscale appearances + """; + + public static void main(String[] args) throws Exception { + PrintService[] availablePrintServices = getTestablePrintServices(); + if (availablePrintServices.length == 0) { + System.out.println("Available print services not found"); + return; + } + PassFailJFrame.builder() + .instructions(INSTRUCTIONS) + .testTimeOut(300) + .title("Monochrome printing") + .testUI(createTestWindow(availablePrintServices)) + .build() + .awaitAndCheck(); + } + + private static Window createTestWindow(final PrintService[] availablePrintServices) { + Window frame = new JFrame("Choose service to test"); + JPanel pnlMain = new JPanel(); + pnlMain.setBorder(new EmptyBorder(5,5,5,5)); + pnlMain.setLayout(new GridLayout(3, 1, 5, 5)); + JLabel lblServices = new JLabel("Available services"); + JComboBox cbServices = new JComboBox<>(); + JButton btnPrint = new JButton("Print"); + btnPrint.setEnabled(false); + cbServices.setRenderer(new ListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, PrintService value, + int index, boolean isSelected, boolean cellHasFocus) { + return new JLabel(value == null ? "" : value.getName()); + } + }); + cbServices.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + btnPrint.setEnabled(cbServices.getSelectedItem() != null); + } + }); + for (PrintService ps : availablePrintServices) { + cbServices.addItem(ps); + } + lblServices.setLabelFor(cbServices); + btnPrint.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + PrintService printService = (PrintService) cbServices.getSelectedItem(); + if (printService != null) { + cbServices.setEnabled(false); + btnPrint.setEnabled(false); + test(printService); + } + } + }); + pnlMain.add(lblServices); + pnlMain.add(cbServices); + pnlMain.add(btnPrint); + frame.add(pnlMain); + frame.pack(); + return frame; + } + + private static PrintService[] getTestablePrintServices() { + List testablePrintServices = new ArrayList<>(); + for (PrintService ps : PrintServiceLookup.lookupPrintServices(null,null)) { + if (ps.isAttributeValueSupported(Chromaticity.MONOCHROME, null, null) && + ps.isAttributeValueSupported(Chromaticity.COLOR, null, null)) { + testablePrintServices.add(ps); + } + } + return testablePrintServices.toArray(new PrintService[0]); + } + + private static void test(PrintService printService) { + try { + print(printService, Chromaticity.COLOR); + print(printService, Chromaticity.MONOCHROME); + } catch (PrinterException ex) { + throw new RuntimeException(ex); + } + } + + private static void print(PrintService printService, Chromaticity chromaticity) + throws PrinterException { + PrintRequestAttributeSet attr = new HashPrintRequestAttributeSet(); + attr.add(chromaticity); + PrinterJob job = PrinterJob.getPrinterJob(); + job.setPrintService(printService); + job.setJobName("Print with " + chromaticity); + job.setPrintable(new ChromaticityAttributePrintable(chromaticity)); + job.print(attr); + } + + private static class ChromaticityAttributePrintable implements Printable { + + private final Chromaticity chromaticity; + + public ChromaticityAttributePrintable(Chromaticity chromaticity) { + this.chromaticity = chromaticity; + } + + @Override + public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) { + + if (pageIndex != 0) { + return NO_SUCH_PAGE; + } + + final int sx = (int) Math.ceil(pageFormat.getImageableX()); + final int sy = (int) Math.ceil(pageFormat.getImageableY()); + + Graphics2D g = (Graphics2D) graphics; + + BufferedImage bufferdImage = getBufferedImage((int) Math.ceil(pageFormat.getImageableWidth() / 3), + (int) Math.ceil(pageFormat.getImageableHeight() / 7)); + g.drawImage(bufferdImage, null, sx, sy); + + double defaultMediaSizeWidth = MediaSize.getMediaSizeForName(MediaSizeName.ISO_A4) + .getX(Size2DSyntax.INCH) * 72; + double scale = pageFormat.getWidth() / defaultMediaSizeWidth; + + final int squareSideLenngth = (int)(50 * scale); + final int offset = (int)(10 * scale); + int imh = sy + (int) Math.ceil(pageFormat.getImageableHeight() / 7) + offset; + + g.setColor(Color.ORANGE); + g.drawRect(sx, imh, squareSideLenngth, squareSideLenngth); + imh = imh + squareSideLenngth + offset; + + g.setColor(Color.BLUE); + g.fillOval(sx, imh, squareSideLenngth, squareSideLenngth); + imh = imh + squareSideLenngth + offset; + + Paint paint = new LinearGradientPaint(0, 0, + squareSideLenngth>>1, offset>>1, new float[]{0.0f, 0.2f, 1.0f}, + new Color[]{Color.RED, Color.GREEN, Color.CYAN}, MultipleGradientPaint.CycleMethod.REPEAT); + g.setPaint(paint); + g.setStroke(new BasicStroke(squareSideLenngth)); + g.fillRect(sx, imh + offset, squareSideLenngth, squareSideLenngth); + imh = imh + squareSideLenngth + offset; + + paint = new RadialGradientPaint(offset, offset, offset>>1, new float[]{0.0f, 0.5f, 1.0f}, + new Color[]{Color.RED, Color.GREEN, Color.CYAN}, MultipleGradientPaint.CycleMethod.REPEAT); + g.setPaint(paint); + g.fillRect(sx, imh + offset, squareSideLenngth, squareSideLenngth); + imh = imh + squareSideLenngth + offset; + + g.setStroke(new BasicStroke(offset>>1)); + g.setColor(Color.PINK); + g.drawString("This page should be " + chromaticity, sx, imh + squareSideLenngth); + + return PAGE_EXISTS; + } + + private BufferedImage getBufferedImage(int width, int height) { + Color[] colors = new Color[]{ + Color.RED, Color.ORANGE, Color.BLUE, + Color.CYAN, Color.MAGENTA, Color.GREEN + }; + BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + final int secondSquareOffset = width / 3; + final int thirdSquareOffset = secondSquareOffset * 2; + final int squareHeight = height / 2; + + int offset = 0; + Color color; + for (int y = 0; y < height; y++) { + if (y > squareHeight) { + offset = 3; + } + for (int x = 0; x < width; x++) { + if (x >= thirdSquareOffset) { + color = colors[offset + 2]; + } else if (x >= secondSquareOffset) { + color = colors[offset + 1]; + } else { + color = colors[offset]; + } + bufferedImage.setRGB(x, y, color.getRGB()); + } + } + return bufferedImage; + } + } + +} \ No newline at end of file