Question

I have a region in my JPanel bounded by the point (0,0) and (width,height). It is a square.

I have a word

String s;

I'd like to find the maximum font size that I can use for s. Now, I know there is a way to do it using FontMetrics and making a for loop to keep increasing the size of the font until it doesn't fit inside the region. But this is SO inefficient and there must be a way to compute the font size of a given font type, such as "Courier" that will fit in this region.

Example of BAD way:

Font f = new Font("Courier", Font.PLAIN, 1);
FontMetrics fm = this.getFontMetrics(f); //this is a JPanel
do {
    f = new Font("Courier", Font.PLAIN, f.getSize()+1);
    fm = this.getFontMetrics(f);
while(fm.stringWidth(s) < width && fm.getHeight() < height);
Was it helpful?

Solution

I had the same problem and found a solution that is a little bit optimized, compared to just iterating over all font sizes. I try to converge towards the optimal font size by adjusting diffs that I either add or subtract until I find a diff font size below 1.

Graphics2D graphics = image.createGraphics();
graphics.setColor(Color.black);

if (subtitleFont == null) {
    //create rectangle first (from a separate library
    int[] rect = matrix.getEnclosingRectangle();
    // define the maximum rect for the text
    Rectangle2D maxRect = new Rectangle2D.Float(0, 0, w - 7, h - rect[0] - rect[3] - 10);

    subtitleX = 0;
    subtitleY = 0;
    // starting with a very big font due to a high res image
    float size = 80f * 4f;
    // starting with a diff half the size of the font
    float diff = size / 2;
    subtitleFont = graphics.getFont().deriveFont(Font.BOLD).deriveFont(size);
    FontMetrics fontMetrics = graphics.getFontMetrics(subtitleFont);
    Rectangle2D stringBounds = null;

    while (Math.abs(diff) > 1) {
        subtitleFont = subtitleFont.deriveFont(size);
        graphics.setFont(subtitleFont);
        fontMetrics = graphics.getFontMetrics(subtitleFont);
        stringBounds = fontMetrics.getStringBounds(options.subtitle, graphics);
        stringBounds = new Rectangle2D.Float(0f, 0f, (float) (stringBounds.getX() + stringBounds.getWidth()), (float) ( stringBounds.getHeight()));

        if (maxRect.contains(stringBounds)) {
            if (0 < diff) {
                diff = Math.abs(diff);
            } else if (diff < 0) {
                diff = Math.abs(diff) / 2;
            }
        } else {
            if (0 < diff) {
                diff = - Math.abs(diff) / 2;
            } else if (diff < 0) {
                if (size <= Math.abs(diff)) {
                    diff = - Math.abs(diff) / 2;
                } else {
                    diff = - Math.abs(diff);
                }
            }
        }
        size += diff;
    }

    subtitleX = (int) ((w/2) - (stringBounds.getWidth() / 2));
    subtitleY = (int) (h - maxRect.getHeight() + fontMetrics.getAscent());
}

graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
        RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
graphics.drawString(options.subtitle, subtitleX, subtitleY);

I have tried that with different resolutions of the image and the sizes of the font. It takes 10 to 12 iterations until a font is found that will fit the max rectangle. I hope it will be helpful to somebody.

OTHER TIPS

yes, I met the same problem as well, and I know what's your meaning of 'inefficient'.

I tried a solution that measuring the unit count for a string by font size: 1 (default).

for example:

String line = "This is a test";
Font font = Font.createFont(Font.TRUETYPE_FONT, new File("/var/fonts/times.ttf"));
FontRenderContext ctx = new FontRenderContext(font.getTransform(), false, false);
Rectangle2D rect = font.getStringBounds(line, ctx);
BigDecimal widthUnits = BigDecimal.valueof(rect.getWidth());

then, you get the widthUnits of line by font size 1. if it divided by region width and reserve the integer part (round down), you'll the the max font size in horizontally.

int maxSizeHor = BigDecimal.valueOf(width)
                 .divide(widthUnits, 1, BigDecimal.ROUND_DOWN)
                 .intValue();

But, it's not enougth. you should also calculate the max font size in vertically. Fortunately, each line with the same font style and font size has the same height. if there are multiple lines, you can use:

lines.length * each height in font size 1.

such as the following:

String[] lines = new String{"This is a test", "Hello World!"};
BigDecimal heightUnits = BigDecimal.valueOf(rect.getHeight())
                         .multiply(BigDecimal.valueOf(lines.length));
int maxSizeVer = BigDecimal.valueOf(height)
                 .divide(heightUnits
                         .multiply(BigDecimal.valueOf(lines.length)), 
                        1,BigDecimal.ROUND_DOWN)
                 .intValue();

Finally, compare and fetch the minium value as the maximum font size:

Math.min(maxSizeHor, maxSizeVer)

But, I met another problem: the single char 'i' as input string at font size:1, you will get the the width units: 0.0 . However, in actually, it won't be zero though it's a very tiny value. so I set 1000 as default font size, and divide 1000 after each progress. then I get the non-zero value.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top