Calculate the Display Width of a String in Java

Calculate the display width of a string in Java

If you just want to use AWT, then use Graphics.getFontMetrics (optionally specifying the font, for a non-default one) to get a FontMetrics and then FontMetrics.stringWidth to find the width for the specified string.

For example, if you have a Graphics variable called g, you'd use:

int width = g.getFontMetrics().stringWidth(text);

For other toolkits, you'll need to give us more information - it's always going to be toolkit-dependent.

How do I calculate the width of a string in pixels?

There are a number of ways to achieve what you want, based on what it is you want to achieve, for example...

BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
FontMetrics fm = g2d.getFontMetrics();
System.out.println(fm.stringWidth("This is a simple test"));
g2d.dispose();

But this only has relevence for the BufferedImage and it's Graphics context, it will not translate back to say, something like a screen or printer.

However, so long as you have a Graphics context, you can achieve the same result.

This example, obviously, uses the default font installed for the Graphics context, which you can change if you need to...

How to calculate the font's width?

For a single string, you can obtain the metrics for the given drawing font, and use that to calculate the string size. For example:

String      message = new String("Hello, StackOverflow!");
Font defaultFont = new Font("Helvetica", Font.PLAIN, 12);
FontMetrics fontMetrics = new FontMetrics(defaultFont);
//...
int width = fontMetrics.stringWidth(message);

If you have more complex text layout requirements, such as flowing a paragraph of text within a given width, you can create a java.awt.font.TextLayout object, such as this example (from the docs):

Graphics2D g = ...;
Point2D loc = ...;
Font font = Font.getFont("Helvetica-bold-italic");
FontRenderContext frc = g.getFontRenderContext();
TextLayout layout = new TextLayout("This is a string", font, frc);
layout.draw(g, (float)loc.getX(), (float)loc.getY());

Rectangle2D bounds = layout.getBounds();
bounds.setRect(bounds.getX()+loc.getX(),
bounds.getY()+loc.getY(),
bounds.getWidth(),
bounds.getHeight());
g.draw(bounds);

How to calculate the pixel width of a String in JavaFX?

If you are just measuring the default font without CSS:

  1. Place the String to be measured in a Text object.
  2. Get the width of the Text object's layout bounds.

If you need to apply CSS:

  1. Place the String to be measured in a Text object.
  2. Create a throwaway Scene and place the Text object in the Scene.
  3. Take a snapshot of the Text (if you are using Java 7) or call applyCss for Java 8.
  4. Get the width of the Text object's layout bounds.

This works because it forces a layout pass on the Text which calculates it's layout bounds.
The scene in step 2 is required because that is just the way the CSS processor works (it needs a node to be located in a Scene to be able to do its job). Definitely read the linked javadoc for applyCss if you want to understand the processing further.

Sample Code

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.text.Text;
import javafx.stage.Stage;

// displays the width in pixels of an arbitrary piece of text.
public class MeasureText extends Application {
public static void main(String[] args) { launch(args); }
@Override public void start(Stage stage) throws Exception {
final Text text = new Text("XYZZY");
new Scene(new Group(text));

// java 7 =>
// text.snapshot(null, null);
// java 8 =>
text.applyCss();

final double width = text.getLayoutBounds().getWidth();

stage.setScene(new Scene(new Label(Double.toString(width))));
stage.show();
}
}

Sample program output (displays the width in pixels of an arbitrary piece of text):

Sample Program Output

How (if at all) would this change if the text was printed to a graphicscontext with a set font?

Apply the font to a text object containing the same message you will plot to the canvas. Unlike when you are measuring text plotted to the scene graph, items plotted to a canvas do not have CSS applied to them, so you don't need to place the Text object in a scene and have CSS applied to it before measuring the text. You can measure the layout bounds of your text object and it will be the same as the bounds of the text plotted within the canvas with the same font.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.*;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.text.*;
import javafx.stage.Stage;

// displays the width in pixels of an arbitrary piece of text (which has been plotted on a canvas).
public class MeasureText extends Application {
@Override
public void start(Stage stage) throws Exception {
final String msg = "XYZZY";
final Text text = new Text(msg);
Font font = Font.font("Arial", 20);
text.setFont(font);

final double width = text.getLayoutBounds().getWidth();

Canvas canvas = new Canvas(200, 50);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFont(font);
gc.fillText(msg, 0, 40);

stage.setScene(new Scene(
new VBox(new Label(Double.toString(width)), canvas))
);
stage.show();
}

public static void main(String[] args) {
launch(args);
}
}

Calculate correct width of a text

Unfortunately the question and comments merely include (by running the sample project) the actual result for two source documents and the description

The annotating text should be center aligned on the top and bottom marker, aligned to the left on the right marker and aligned to the right on the left marker. The alignment is not working for me, as the font.getSTringWidth( .. ) returns only a fraction of what it seems to be. And the discrepance seems to be different in both PDFs.

but not a concrete sample discrepancy to repair.

There are several issues in the code, though, which may lead to such observations (and other ones, too!). Fixing them should be done first; this may already resolve the issues observed by the OP.

Which box to take

The code of the OP derives several values from the media box:

PDRectangle pageSize = page.findMediaBox();
float pageWidth = pageSize.getWidth();
float pageHeight = pageSize.getHeight();
float lineWidth = Math.max(pageWidth, pageHeight) / 1000;
float markerRadius = lineWidth * 10;
float fontSize = Math.min(pageWidth, pageHeight) / 20;
float fontPadding = Math.max(pageWidth, pageHeight) / 100;

These seem to be chosen to be optically pleasing in relation to the page size. But the media box is not, in general, the final displayed or printed page size, the crop box is. Thus, it should be

PDRectangle pageSize = page.findCropBox();

(Actually the trim box, the intended dimensions of the finished page after trimming, might even be more apropos; the trim box defaults to the crop box. For details read here.)

This is not relevant for the given sample documents as they do not contain explicit crop box definitions, so the crop box defaults to the media box. It might be relevant for other documents, though, e.g. those the OP could not include.

Which PDPageContentStream constructor to use

The code of the OP adds a content stream to the page at hand using this constructor:

PDPageContentStream contentStream = new PDPageContentStream(doc, page, true, true);

This constructor appends (first true) and compresses (second true) but unfortunately it continues in the graphics state left behind by the pre-existing content.

Details of the graphics state of importance for the observations at hand:

  • Transformation matrix - it may have been changed to scale (or rotate, skew, move ...) any new content added
  • Character spacing - it may have been changed to put any new characters added nearer to or farther from each other
  • Word spacing - it may have been changed to put any new words added nearer to or farther from each other
  • Horizontal scaling - it may have been changed to scale any new characters added
  • Text rise - it may have been changed to displace any new characters added vertically

Thus, a constructor should be chosen which also resets the graphics state:

PDPageContentStream contentStream = new PDPageContentStream(doc, page, true, true, true);

The third true tells PDFBox to reset the graphics state, i.e. to surround the former content with a save-state/restore-state operator pair.

This is relevant for the given sample documents, at least the transformation matrix is changed.

Setting and using the CalRGB color space

The OP's code sets the stroking and non-stroking color spaces to a calibrated color space:

contentStream.setStrokingColorSpace(new PDCalRGB());
contentStream.setNonStrokingColorSpace(new PDCalRGB());

Unfortunately new PDCalRGB() does not create a valid CalRGB color space object, its required WhitePoint value is missing. Thus, before selecting a calibrated color space, initialize it properly.

Thereafter the OP's code sets the colors using

contentStream.setStrokingColor(marker.color.r, marker.color.g, marker.color.b);
contentStream.setNonStrokingColor(marker.color.r, marker.color.g, marker.color.b);

These (int, int, int) overloads unfortunately use the RG and rg operators implicitly selecting the DeviceRGB color space. To not overwrite the current color space, use the (float[]) overloads with normalized (0..1) values instead.

While this is not relevant for the observed issue, it causes error messages by PDF viewers.

Calculating the width of a drawn string

The OP's code calculates the width of a drawn string using

float textWidth = font.getStringWidth(marker.id) * 0.043f;

and the OP is surprised

The * 0.043f works as an approximation for one document, but fails for the next.

There are two factors building this "magic" number:

  • As the OP has remarked the glyph coordinate space is set up in a 1/1000 of the user coordinate space and that number is in glyph space, thus a factor of 0.001.

  • As the OP has ignored he wants the width for the string using the font size he selected. But the font object has no knowledge of the current font size and returns the width for a font size of 1. As the OP selects the font size dynamically as Math.min(pageWidth, pageHeight) / 20, this factor varies. In case of the two given sample documents about 42 but probably totally different in other documents.

Positioning text

The OP's code positions the text like this starting from identity text matrices:

contentStream.moveTextPositionByAmount(
marker.endX + marker.getXTextOffset(textWidth, fontPadding),
marker.endY + marker.getYTextOffset(fontSize, fontPadding));

using methods getXTextOffset and getYTextOffset:

public float getXTextOffset(float textWidth, float fontPadding) {
if (getLocation() == Location.TOP)
return (textWidth / 2 + fontPadding) * -1;
else if (getLocation() == Location.BOTTOM)
return (textWidth / 2 + fontPadding) * -1;
else if (getLocation() == Location.RIGHT)
return 0 + fontPadding;
else
return (textWidth + fontPadding) * -1;
}

public float getYTextOffset(float fontSize, float fontPadding) {
if (getLocation() == Location.TOP)
return 0 + fontPadding;
else if (getLocation() == Location.BOTTOM)
return (fontSize + fontPadding) * -1f;
else
return fontSize / 2 * -1;
}

In case of getXTextOffset I doubt that adding fontPadding for Location.TOP and Location.BOTTOM makes sense, especially in the light of the OP's desire

The annotating text should be center aligned on the top and bottom marker

For the text to be centered it should not be shifted off-center.

The case of getYTextOffset is more difficult. The OP's code is built upon two misunderstandings: It assumes

  • that the text position selected by moveTextPositionByAmount is the lower left, and
  • that the font size is the character height.

Actually the text position is positioned on the base line, the glyph origin of the next drawn glyph will be positioned there, e.g.

Glyph origin, width, and bounding box for 'g'

Thus, the y positioned either has to be corrected to take the descent into account (for centering on the whole glyph height) or only use the ascent (for centering on the above-baseline glyph height).

And a font size does not denote the actual character height but is arranged so that the nominal height of tightly spaced lines of text is 1 unit for font size 1. "Tightly spaced" implies that some small amount of additional inter-line space is contained in the font size.

In essence for centering vertically one has to decide what to center on, whole height or above-baseline height, first letter only, whole label, or all font glyphs. PDFBox does not readily supply the necessary information for all cases but methods like PDFont.getFontBoundingBox() should help.



Related Topics



Leave a reply



Submit