Scaled line borders in Swing - possible error

As mentioned in the related question :

There are many (many) questions about calculating the size (width or height) of a string that should be colored in the Swing component. And there are many suggested solutions.

However, the most commonly used and recommended solution (and from my experience so far, at least, calculates the correct estimates for most cases) once again shows a rather strange behavior under certain conditions.

The following is an example that shows what I now view as a common mistake:

import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.Locale;

public class StringBoundsBugTest
{
    public static void main(String[] args)
    {
        Font font = new Font("Dialog", Font.PLAIN, 10);
        BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = bi.createGraphics();
        g.setRenderingHint(
            RenderingHints.KEY_FRACTIONALMETRICS,  
            RenderingHints.VALUE_FRACTIONALMETRICS_ON);

        for (int i=1; i<30; i++)
        {
            double scaling = 1.0 / i;

            AffineTransform oldAt = g.getTransform();
            g.scale(scaling, scaling);
            FontRenderContext fontRenderContext = g.getFontRenderContext();
            Rectangle2D bounds = 
                font.getStringBounds("Test", fontRenderContext);
            g.setTransform(oldAt);

            System.out.printf(Locale.ENGLISH, 
                "Scaling %8.5f, width %8.5f\n",
                scaling, bounds.getWidth());
        }

    }
}

Graphics2D ( , JComponent BufferedImage), , FontRenderContext .

, ( - , , , , ).

, ( JDK 1.8.0_31)

Scaling  1.00000, width 19.44824
Scaling  0.50000, width 19.44824
Scaling  0.33333, width 19.32669
Scaling  0.25000, width 19.44824
Scaling  0.20000, width 19.44824
Scaling  0.16667, width 19.32669
Scaling  0.14286, width 19.14436
Scaling  0.12500, width 19.44824
Scaling  0.11111, width 19.14436
Scaling  0.10000, width 19.44824
Scaling  0.09091, width 19.38747
Scaling  0.08333, width 18.96204
Scaling  0.07692, width 18.96204
Scaling  0.07143, width 18.71893
Scaling  0.06667, width 19.14436
Scaling  0.06250, width 19.44824
Scaling  0.05882, width 18.59738
Scaling  0.05556, width 18.59738
Scaling  0.05263, width 18.47583
Scaling  0.05000, width 19.44824
Scaling  0.04762, width  0.00000
Scaling  0.04545, width  0.00000
Scaling  0.04348, width  0.00000
Scaling  0.04167, width  0.00000
Scaling  0.04000, width  0.00000
Scaling  0.03846, width  0.00000
Scaling  0.03704, width  0.00000
Scaling  0.03571, width  0.00000
Scaling  0.03448, width  0.00000 

, ~ 18-19. , "", , , , - , , .

, , . , , , .

, : - Swing, FontRenderContext .., , , .., int. ( , , ).

FontRenderContext , , . , , Graphics: FontRenderContext, , .

- Swing , ?

+4
2

FontRenderContext 4

public class FontRenderContext {
    private transient AffineTransform tx;
    private transient Object aaHintValue;
    private transient Object fmHintValue;
    private transient boolean defaulting;

, . 1/3, , .

, AffineTransform (, ) FontRenderContext.

FontRenderContext frc=new FontRenderContext(g.getTransform(), //of just replace with new AffineTransform(),
            g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING),
            g.getRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS));

GlyphVector. .

Font.getStringBounds()

public Rectangle2D getStringBounds( String str, FontRenderContext frc) {
    char[] array = str.toCharArray();
    return getStringBounds(array, 0, array.length, frc);
}

public Rectangle2D getStringBounds(char [] chars,
                                int beginIndex, int limit,
                                   FontRenderContext frc) {
//some checks skipped

    boolean simple = values == null ||
        (values.getKerning() == 0 && values.getLigatures() == 0 &&
          values.getBaselineTransform() == null);
    if (simple) {
        simple = ! FontUtilities.isComplexText(chars, beginIndex, limit);
    }

    if (simple) {
        GlyphVector gv = new StandardGlyphVector(this, chars, beginIndex,
                                                 limit - beginIndex, frc);
        return gv.getLogicalBounds();

, , StandardGlyphVector ( , , RTL-). TextLayout.

:

private static Rectangle2D getBounds(Graphics2D g, String text) {
    FontRenderContext frc=new FontRenderContext(new AffineTransform(),
            g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING),
            g.getRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS));
    GlyphVector gv = new StandardGlyphVector(g.getFont(), text.toCharArray(), 0,
            text.length(), frc);
    return gv.getLogicalBounds();
}
+2

(, , ). , :

Font#getStringBounds FontRenderContext Graphics2D , .

"default" () FontRenderContext ( StanislavL ) ( ), , , , ( 0,5).

, , birds : 1,0, 'm , FontMetrics .

:

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

public class StringBoundsUtils
{
    private static final Graphics2D DEFAULT_GRAPHICS;
    static
    {
        BufferedImage bi = new BufferedImage(1,1,BufferedImage.TYPE_INT_ARGB);
        DEFAULT_GRAPHICS = bi.createGraphics();
        DEFAULT_GRAPHICS.setRenderingHint(
            RenderingHints.KEY_FRACTIONALMETRICS,
            RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    }

    public static Rectangle2D computeStringBounds(String string, Font font)
    {
        return computeStringBounds(string, font, new Rectangle2D.Double());
    }

    public static Rectangle2D computeStringBounds(
        String string, Font font, Rectangle2D result)
    {
        final float helperFontSize = 1000.0f;
        final float fontSize = font.getSize2D();
        final float scaling = fontSize / helperFontSize;
        Font helperFont = font.deriveFont(helperFontSize);
        FontMetrics fontMetrics = DEFAULT_GRAPHICS.getFontMetrics(helperFont);
        double stringWidth = fontMetrics.stringWidth(string) * scaling;
        double stringHeight = fontMetrics.getHeight() * scaling;
        if (result == null)
        {
            result = new Rectangle2D.Double();
        }
        result.setRect(
            0, -fontMetrics.getAscent() * scaling,
            stringWidth, stringHeight);
        return result;

    }
}

( / Graphics2D, , FontMetrics, ...)

, , : , , FontMetrics , , (, , ), ... .

( , ):

import java.awt.Font;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.util.Locale;

public class StringBoundsUtilsPerformance
{
    public static void main(String[] args)
    {
        String strings[] = {
            "a", "AbcXyz", "AbCdEfGhIjKlMnOpQrStUvWxYz",
            "AbCdEfGhIjKlMnOpQrStUvWxYz" +
            "AbCdEfGhIjKlMnOpQrStUvWxYz" +
            "AbCdEfGhIjKlMnOpQrStUvWxYz" +
            "AbCdEfGhIjKlMnOpQrStUvWxYz" +
            "AbCdEfGhIjKlMnOpQrStUvWxYz" };
        float fontSizes[] = { 1.0f, 10.0f, 100.0f };
        int runs = 1000000;

        long before = 0;
        long after = 0;
        double resultA = 0;
        double resultB = 0;

        for (float fontSize : fontSizes)
        {
            Font font = new Font("Dialog", Font.PLAIN, 10).deriveFont(fontSize);
            for (String string : strings)
            {
                before = System.nanoTime();
                for (int i=0; i<runs; i++)
                {
                    Rectangle2D r = computeStringBoundsDefault(string, font);
                    resultA += r.getWidth();
                }
                after  = System.nanoTime();
                resultA /= runs;
                System.out.printf(Locale.ENGLISH,
                    "A: time %14.4f result %14.4f, fontSize %3.1f, length %d\n",
                    (after-before)/1e6, resultA, fontSize, string.length());

                before = System.nanoTime();
                for (int i=0; i<runs; i++)
                {
                    Rectangle2D r =
                        StringBoundsUtils.computeStringBounds(string, font);
                    resultB += r.getWidth();
                }
                after  = System.nanoTime();
                resultB /= runs;
                System.out.printf(Locale.ENGLISH,
                    "B: time %14.4f result %14.4f, fontSize %3.1f, length %d\n",
                    (after-before)/1e6, resultB, fontSize, string.length());
            }
        }
    }

    private static final FontRenderContext DEFAULT_FONT_RENDER_CONTEXT =
        new FontRenderContext(null, true, true);
    public static Rectangle2D computeStringBoundsDefault(
        String string, Font font)
    {
        return font.getStringBounds(string, DEFAULT_FONT_RENDER_CONTEXT);
    }
}

, :

A: time      1100.4441 result        14.7813, fontSize 1.0, length 26
B: time       218.6409 result        14.7810, fontSize 1.0, length 26
...
A: time      1167.1569 result       147.8125, fontSize 10.0, length 26
B: time       200.6532 result       147.8100, fontSize 10.0, length 26
...
A: time      1179.7873 result      1478.1253, fontSize 100.0, length 26
B: time       208.9414 result      1478.1003, fontSize 100.0, length 26

, StringBoundsUtils , Font#getStringBounds 5 ( ).

result , , Font#getStringBounds , StringBoundsUtils, .

, , widhts, . :

StringBoundsUtilsTest01

, " " - , , StringBoundsUtils , 0,5.

: ( Viewer, JAR Maven)

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.Locale;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import de.javagl.viewer.Painter;
import de.javagl.viewer.Viewer;

public class StringBoundsUtilsTest
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static final Font DEFAULT_FONT =
        new Font("Dialog", Font.PLAIN, 10);
    private static Font font = DEFAULT_FONT.deriveFont(10f);

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame("Viewer");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new BorderLayout());

        Viewer viewer = new Viewer();

        String string = "AbcXyz";
        viewer.addPainter(new Painter()
        {
            @Override
            public void paint(Graphics2D g, AffineTransform worldToScreen,
                double w, double h)
            {
                AffineTransform at = g.getTransform();
                g.setColor(Color.BLACK);
                g.setRenderingHint(
                    RenderingHints.KEY_FRACTIONALMETRICS,
                    RenderingHints.VALUE_FRACTIONALMETRICS_ON);
                g.setRenderingHint(
                    RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);

                Rectangle2D boundsA =
                    StringBoundsUtilsPerformance.computeStringBoundsDefault(
                        string, font);
                Rectangle2D boundsB =
                    StringBoundsUtils.computeStringBounds(string, font);

                g.setFont(new Font("Monospaced", Font.BOLD, 12));
                g.setColor(Color.GREEN);
                g.drawString(createString(boundsA), 10, 20);
                g.setColor(Color.RED);
                g.drawString(createString(boundsB), 10, 40);

                g.setFont(font);
                g.transform(worldToScreen);
                g.drawString(string, 0, 0);
                g.setTransform(at);

                g.setColor(Color.GREEN);
                g.draw(worldToScreen.createTransformedShape(boundsA));
                g.setColor(Color.RED);
                g.draw(worldToScreen.createTransformedShape(boundsB));
            }
        });
        f.getContentPane().add(viewer, BorderLayout.CENTER);

        f.getContentPane().add(
            new JLabel("Mouse wheel: Zoom, "
                + "Right mouse drags: Move, "
                + "Left mouse drags: Rotate"),
            BorderLayout.NORTH);

        JSpinner fontSizeSpinner =
            new JSpinner(new SpinnerNumberModel(10.0, 0.1, 100.0, 0.1));
        fontSizeSpinner.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent e)
            {
                Object object = fontSizeSpinner.getValue();
                Number number = (Number)object;
                float fontSize = number.floatValue();
                font = DEFAULT_FONT.deriveFont(fontSize);
                viewer.repaint();
            }
        });
        JPanel p = new JPanel();
        p.add(new JLabel("Font size"), BorderLayout.WEST);
        p.add(fontSizeSpinner, BorderLayout.CENTER);
        f.getContentPane().add(p, BorderLayout.SOUTH);


        viewer.setPreferredSize(new Dimension(1000,500));
        viewer.setDisplayedWorldArea(-15,-15,30,30);
        f.pack();
        viewer.setPreferredSize(null);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static String createString(Rectangle2D r)
    {
        return String.format(Locale.ENGLISH,
            "x=%12.4f y=%12.4f w=%12.4f h=%12.4f",
            r.getX(), r.getY(), r.getWidth(), r.getHeight());
    }

}
+2
source

Source: https://habr.com/ru/post/1612636/


All Articles