Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8278222

[macos] Inaccurate letter spacing (kerning) when printing via java.awt.print

XMLWordPrintable

    • 2d
    • os_x

      ADDITIONAL SYSTEM INFORMATION :
      Java 17.0.1 on MacOS 10.15.7.

      A DESCRIPTION OF THE PROBLEM :
      When using the java.awt.print.PrinterJob/Printable APIs to print documents to PDF files or a physical printer on MacOS, the FontRenderContext of the Graphics2D instance that is passed to Printable.print will always have a scaling transform corresponding to a DPI of 72 points-per-inch, rather than the actual DPI used by the printer or MacOS PDF engine. This leads to poor kerning between characters in text laid out via Graphics2D.drawString, Font.layoutGlyphVector, LineBreakMeasurer, or TextMeasurer.

      On Windows, by contrast, the FontRenderContext will have a transform corresponding to the actual DPI being used to print, e.g. 300 or 600. A quick test on Ubuntu Linux also gave a DPI of 600, and proper kerning.

      A workaround on MacOS is to create a new FontRenderContext with RenderingHints.VALUE_FRACTIONALMETRICS_ON, and pass that one when constructing the text layout via e.g. LineBreakMeasurer.

      See the comparison here: <LINK_IMAGE1>

      A fix would probably involve a change in the OpenJDK code that calls Printable.print on MacOS, so that the FontRenderContext either uses a larger default DPI (e.g. 300, instead of 72), or enables the VALUE_FRACTIONALMETRICS_ON hint.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      On MacOS X, with Java 17, run the attached test class, which will open a "Print" dialog. Click the "PDF" button dropdown and select "Save as PDF". Save and open the PDF. Zoom in on the first paragraph with small text. Compare the output on the first and the second page. (Screenshots in the links below:)

      <LINK_IMAGE2>
      <LINK_IMAGE3>


      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      Characters on the first page should be evenly spaced within each line of text, like on the second page. (The second page shows the workaround of forcing fractional metrics on.)

      And ideally, the reported DPI should match that used to print (e.g. "DPI=300").

      ACTUAL -
      Characters on the first page are unevenly spaced. For example, the letter "t" in the word "Although" is too close to the "l", and with a too wide gap before the "h".

      On MacOS, the reported DPI is always 72, regardless of actual print settings.


      ---------- BEGIN SOURCE ----------
      import java.awt.Color;
      import java.awt.Font;
      import java.awt.Graphics;
      import java.awt.Graphics2D;
      import java.awt.RenderingHints;
      import java.awt.font.FontRenderContext;
      import java.awt.font.TextAttribute;
      import java.awt.font.TextLayout;
      import java.awt.font.TextMeasurer;
      import java.awt.geom.Rectangle2D;
      import java.awt.print.PageFormat;
      import java.awt.print.Printable;
      import java.awt.print.PrinterException;
      import java.awt.print.PrinterJob;
      import java.text.AttributedString;
      import java.util.ArrayList;
      import java.util.Arrays;
      import java.util.List;
      import javax.print.PrintService;
      import javax.print.attribute.HashPrintRequestAttributeSet;
      import javax.print.attribute.PrintRequestAttributeSet;
      import javax.print.attribute.standard.DialogTypeSelection;
      import javax.print.attribute.standard.OrientationRequested;
      import javax.print.attribute.standard.PrinterResolution;
      import javax.swing.SwingUtilities;

      public class PrintDPIBugExhibit {
        private static String[] EXAMPLE_TEXT = new String[] {
            "Although many in NASA's Voyager program were supportive of the idea, there were",
            "concerns that taking a picture of Earth so close to the Sun risked damaging the",
            "spacecraft's imaging system irreparably. It was not until 1989 that Sagan's idea",
            "was put in motion, but then instrument calibrations delayed the operation",
            "further, and the personnel who devised and transmitted the radio commands to",
            "Voyager 1 were also being laid off or transferred to other projects. Finally,",
            "NASA Administrator Richard Truly interceded to ensure that the photograph was",
            "taken. A proposal to continue to photograph Earth as it orbited the Sun was",
            "rejected."
        };

        public static void main(String args[]) {
          System.out.println("java.version=" + System.getProperty("java.version"));
          SwingUtilities.invokeLater(() -> {
            mainEDT();
          });
        }

        private static void mainEDT() {
          if (PrinterJob.lookupPrintServices().length == 0)
            throw new RuntimeException("No printers available; cannot run test");
          PrinterJob printerJob = PrinterJob.getPrinterJob();
          PrintRequestAttributeSet printAttributes = new HashPrintRequestAttributeSet();
          printAttributes.add(DialogTypeSelection.NATIVE);
          printAttributes.add(OrientationRequested.PORTRAIT);
          printerJob.setPrintable(new SomePrintable());

          if (!printerJob.printDialog(printAttributes))
            return;

          PrintService printService = printerJob.getPrintService();
          if (printService == null)
            throw new RuntimeException("Got null printService");
          boolean reportsSupportsResolution =
              printService.isAttributeCategorySupported(PrinterResolution.class);
          System.out.println("isAttributeCategorySupported(PrinterResolution.class)=" +
              reportsSupportsResolution);
          PrinterResolution reportedResolution =
                (PrinterResolution) printAttributes.get(PrinterResolution.class);
          System.out.println("Got reported resolution " + reportedResolution);
          System.out.println("Now printing");
          try {
            printerJob.print(printAttributes);
          } catch (PrinterException e) {
            throw new RuntimeException(e);
          }
        }

        private static class SomePrintable implements Printable {
          @Override
          public int print(Graphics g0, PageFormat pageFormat, int pageIndex) throws PrinterException {
            Graphics2D g = (Graphics2D) g0;
            if (pageIndex > 1)
              return Printable.NO_SUCH_PAGE;
            FontRenderContext originalFRC = g.getFontRenderContext();
            boolean originalFractionalMetrics = RenderingHints.VALUE_FRACTIONALMETRICS_ON
                .equals(originalFRC.getFractionalMetricsHint());
            double dpi = originalFRC.getTransform().getScaleX() * 72;
            boolean forceFractionalMetrics = (pageIndex == 1);
            FontRenderContext useFRC;
            if (forceFractionalMetrics) {
              /* Create a modified FontRenderContext which is identical to the original one except
              forcing fractional metrics on. */
              useFRC = new FontRenderContext(originalFRC.getTransform(),
                  originalFRC.getAntiAliasingHint(), RenderingHints.VALUE_FRACTIONALMETRICS_ON);
            } else {
                useFRC = originalFRC;
            }

            System.out.println("Printing with DPI=" + dpi);
            g.setColor(Color.BLACK);
            final int x = 72;
            int y = 72;
            for (int fontSize : new int[] { 6, 12 }) {
              List<String> lines = new ArrayList<>();
              lines.add("DPI=" + dpi + ", originalFractionalMetrics=" + originalFractionalMetrics +
                  ", forceFractionalMetrics=" + forceFractionalMetrics);
              lines.add("java.version=" + System.getProperty("java.version") +
                  ", os.name=" + System.getProperty("os.name"));
              lines.add("");
              lines.addAll(Arrays.asList(EXAMPLE_TEXT));
              lines.add("");
              lines.add("");
              Font font = new Font("Arial", Font.PLAIN, fontSize);
              for (String line : lines) {
                y += (int) (fontSize * 1.2);
                if (line.isEmpty())
                  continue;
                /* Could also use LineBreakMeasurer or Font.layoutGlyphVector here; the result would be
                the same. */
                AttributedString as = new AttributedString(line);
                as.addAttribute(TextAttribute.FONT, font);
                TextMeasurer tm = new TextMeasurer(as.getIterator(), useFRC);
                TextLayout tl = tm.getLayout(0, line.length());
                tl.draw(g, x, y);
              }
            }
            // Draw lines 0.5pt thick at 1pt spacing.
            for (int i = 0; i < 40; i++) {
              g.fill(new Rectangle2D.Double(x, y, 72 * 3, 0.5));
              y++;
            }
            return Printable.PAGE_EXISTS;
          }
        }
      }
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      Clients may create a new FontRenderContext with RenderingHints.VALUE_FRACTIONALMETRICS_ON, and pass that one when constructing the text layout via e.g. LineBreakMeasurer. This appears to work on Java 11, Java 16, and Java 17, but not on Java 13 or 14.

      FREQUENCY : always


        1. badkerning.png
          badkerning.png
          182 kB
        2. macos_noworkaround.png
          macos_noworkaround.png
          100 kB
        3. macos_workaround.png
          macos_workaround.png
          99 kB
        4. PrintDPIBugExhibit.java
          6 kB

            psadhukhan Prasanta Sadhukhan
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: