JDK-8212744 : [macos] Overlapping glyphs with harfbuzz + scaled AAT fonts
  • Type: Bug
  • Component: client-libs
  • Sub-Component: 2d
  • Affected Version: 9,10,11
  • Priority: P4
  • Status: Closed
  • Resolution: Duplicate
  • OS: os_x
  • Submitted: 2018-10-22
  • Updated: 2021-07-07
  • Resolved: 2021-07-07
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdResolved
Related Reports
Duplicate :  
Relates :  
Description
Scaling a font with 

deriveFont(AffineTransform.getScaleInstance(scaleFactor, scaleFactor))

fails, once the scaleFactor crosses a certain threshold. Glyphs are then drawn on top of each other, not after one another (see attached screenshot).

Demo Code:
==========

import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

public class FontScalingIssues {

    public static void main(final String[] args) {
        createFrame(true);
        createFrame(false);
    }

    private static void createFrame(final boolean scaleWithTransform) {
        final String xY = "XY";

        final JFrame frame = new JFrame("scaleWithTransform: " + scaleWithTransform);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.getContentPane().setLayout(new BorderLayout());
        final JPanel iconPanel = new JPanel(new FlowLayout());
        frame.getContentPane().add(iconPanel, BorderLayout.CENTER);
        final TextIcon xYIcon = new TextIcon(xY, scaleWithTransform);
        iconPanel.add(xYIcon);
        final JSlider slider = new JSlider(1, 2000, 1);
        slider.setSnapToTicks(false);
        final JPanel sliderPanel = new JPanel(new FlowLayout());
        sliderPanel.add(slider);
        final JLabel scaleFactorLabel = new JLabel("1.0");
        sliderPanel.add(scaleFactorLabel);
        frame.getContentPane().add(sliderPanel, BorderLayout.SOUTH);

        slider.addChangeListener(e -> {
            final float scaleFactor = slider.getValue()/100f;
            xYIcon.setScaleFactor(scaleFactor);
            scaleFactorLabel.setText("" + scaleFactor);
            frame.invalidate();
        });

        SwingUtilities.invokeLater(() -> {
            final int y = scaleWithTransform ? 100 : 500;
            frame.setBounds(100, y, 500, 300);
            frame.setVisible(true);
        });
    }

    private static class TextIcon extends JLabel {

        private final String text;
        private final boolean scaleWithTransform;

        public TextIcon(final String text, final boolean scaleWithTransform) {
            this.text = text;
            this.scaleWithTransform = scaleWithTransform;
            setIcon(createIconWithText(1f));
        }

        public void setScaleFactor(final float scaleFactor) {
            setIcon(createIconWithText(scaleFactor));
        }

        private ImageIcon createIconWithText(final float scaleFactor) {
            final int width = 400;
            final int height = 200;
            final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            final Graphics2D g2d = (Graphics2D)image.getGraphics();

            g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            if (scaleWithTransform) {
                g2d.setFont(g2d.getFont().deriveFont(AffineTransform.getScaleInstance(scaleFactor, scaleFactor)));
            } else {
                final float originalSize = g2d.getFont().getSize2D();
                g2d.setFont(g2d.getFont().deriveFont(originalSize * scaleFactor));
            }

            final FontMetrics newMetrics = g2d.getFontMetrics();

            // fill whole background
            g2d.setColor(Color.GREEN);
            g2d.fillRect(0, 0, width, height);

            // draw string bounds as RED background
            final Rectangle2D rect = newMetrics.getStringBounds(text, g2d);
            g2d.setColor(Color.RED);
            g2d.fillRect((width - (int) rect.getWidth()) / 2, 0, (int)rect.getWidth(), (int)rect.getHeight());

            // draw string
            g2d.setColor(Color.BLACK);
            g2d.drawString(text, (width - (int) rect.getWidth()) / 2, newMetrics.getAscent());
            g2d.dispose();
            return new ImageIcon(image);
        }
    }
}


The code opens two JFrames that allow to scale some characters using a slider. The scaling method differs. Frame 1 scales with an AffineTransform, frame 2 uses deriveFont(newSize). Once you scale beyond a certain value, the AffineTransform version draws glyphs on top of each other. Despite this, the bounding box (drawn in RED) stays correct.

The bug may be connected to https://bugs.openjdk.java.net/browse/JDK-8212743
Comments
This problem is cured as of JDK 17 b24 due to the fix for JDK-8256372 which starts to use the harfbuzz AAT font support rather than having harfbuzz to coretext.
07-07-2021

This is problem specific to MacOS when we use harfbuzz with an AAT font. On JDK 9 it can be worked around with -Dsun.font.layoutengine=icu On JDK10 and later that isn't available but you can replace the font with an OpenType one, eg g2d.setFont(new Font("Times New Roman", Font.PLAIN, 12)); and the problem also goes away.
22-10-2018