Java ScreenShot
Screenshot      

Screenshot Core Java 2: Volume I - Fundamentals

Table of Contents
 7.  Graphics Programming


Text and Fonts

The "Not a Hello, World" program at the beginning of this chapter displayed a string in the default font. Often, you want to show text in a different font. You specify a font by its font face name (or font name for short). A font face name is composed of a font family name, such as "Helvetica," and an optional suffix such as "Bold." For example, the font faces "Helvetica" and "Helvetica Bold" are both considered to be part of the family named "Helvetica." To find out which fonts are available on a particular computer, call the getAvailableFontFamilyNames method of the GraphicsEnvironment class. The method returns an array of strings that contains the names of all available fonts. To obtain an instance of the GraphicsEnvironment class that describes the graphics environment of the user's system, use the static getLocalGraphicsEnvironment method. Thus, the following program gives you a printout of the names of all fonts on your system:

import java.awt.*;
public class ListFonts
{
 public static void main(String[] args)
 {
 String[] fontNames = GraphicsEnvironment
 .getLocalGraphicsEnvironment()
 .getAvailableFontFamilyNames();
 for (int i = 0; i < fontNames.length; i++)
 System.out.println(fontNames[i]);
 }
}


On one system, the list starts out like this:

Abadi MT Condensed Light Arial Arial Black Arial Narrow Arioso Baskerville Binner Gothic
. . .


and goes on for another 70 fonts or so.

Java graphics notes_icon.gif

The SDK documentation claims that suffixes such as "heavy," "medium," "oblique," or "gothic" are variations inside within a single family. In our experience, that is not the case. The "Bold," "Italic," and "Bold Italic" suffixes are recognized as family variations, but other suffixes aren't.

Unfortunately, there is no absolute way of knowing whether a user has a font with a particular "look" installed. Font names can be trademarked, and font designs can be copyrighted in some jurisdictions. Thus, the distribution of fonts often involves royalty payments to a font foundry. Of course, just as there are inexpensive imitations of famous perfumes, there are lookalikes for name-brand fonts. For example, the Helvetica and Times Roman imitations that are shipped with Windows are called Arial and Times New Roman. To establish a common baseline, the AWT defines five logical font names:
SansSerif Serif Monospaced Dialog DialogInput


These font names are always mapped to fonts that actually exist on the client machine. For example, on a Windows system, SansSerif is mapped to Arial. To draw characters in a font, you must first create an object of the class Font. You specify the font name, the font style, and the point size. Here is an example of how you construct a Font object:

Font helvb14 = new Font("Helvetica", Font.BOLD, 14);


The third argument is the point size. You can use a logical font name in the place of a font face name in the Font constructor. You specify the style (plain, bold, italic, or bold italic) by setting the second Font constructor argument to one of the following values:

Font.PLAIN Font.BOLD Font.ITALIC Font.BOLD + Font.ITALIC


Here is an example:

Font sansbold14 = new Font("SansSerif", Font.BOLD, 14)


Java graphics notes_icon.gif

Prior versions of Java used the names Helvetica, TimesRoman, Courier, and ZapfDingbats as logical font names. For backward compatibility, these font names are still treated as logical font names even though Helvetica is really a font face name and TimesRoman and ZapfDingbats are not font names at all—the actual font face names are "Times Roman" and "Zapf Dingbats."

Java graphics exclamatory_icon.gif

Starting with SDK version 1.3, you can read TrueType fonts. You need an input stream for the font—typically from a disk file or URL. (See for more information on streams.) Then call the static Font.createFont method:

URL url = new URL("http://www.fonts.com/Wingbats.ttf");
InputStream in = url.openStream();
Font f = Font.createFont(Font.TRUETYPE_FONT, in);


The font is plain with a font size of 1 point. Use the deriveFont method to get a font of the desired size:

Font df = f.deriveFont(14.0F);
Java graphics caution_icon.gif

There are two overloaded versions of the deriveFont method. One of them (with a float parameter) sets the font size, the other (with an int parameter) sets the font style. Thus, f.deriveFont(14) sets the style and not the size! (The result is to make an italic font because it happens that the binary representation of 14 sets the ITALIC bit but not the BOLD bit).

The Java fonts contain the usual ASCII characters as well as symbols. For example, if you print the character '\u2297' in the Dialog font, then you get a Screenshotcharacter. Only those symbols that are defined in the Unicode character set are available. (See the sidebar at the end of this section for more information about available symbols and adding more fonts.) Here's the code that displays the string "Hello, World!" in the standard sans serif font on your system, using 14-point bold type:
Font sansbold14 = new Font("SansSerif", Font.BOLD, 14);
g2.setFont(sansbold14);
String message = "Hello, World!";
g2.drawString(message, 75, 100);


Next, let's center the string in its panel rather than drawing it at an arbitrary position. We need to know the width and height of the string in pixels. These dimensions depend on three factors:

  • The font used (in our case, sans serif, bold, 14-point);
  • The string (in our case, "Hello, World!");
  • The device on which the font is drawn (in our case, the user's screen).

To obtain an object that represents the font characteristics of the screen device, you call the getFontRenderContext method of the Graphics2D class. It returns an object of the FontRenderContext class. You simply pass that object to the getStringBounds method of the Font class:

FontRenderContext context = g2.getFontRenderContext();
Rectangle2D bounds = f.getStringBounds(message, context);


The getStringBounds method returns a rectangle that encloses the string. To interpret the dimensions of that rectangle, it is helpful to consider some basic typesetting terms (see Screenshot-13). The baseline is the imaginary line where, for example, the bottom of a character like "e" rests. The ascent is the distance from the baseline to the top of an ascender, which is the upper part of a letter like "b" or "k," or an uppercase character. The descent is the distance from the baseline to a descender, which is the lower portion of a letter like "p" or "g."

Screenshot-13. Typesetting terms illustrated

Java graphics 07fig13.gif


Leading is the space between the descent of one line and the ascent of the next line. (The term has its origin from the strips of lead that typesetters used to separate lines.) The height of a font is the distance between successive baselines, which is the same as descent + leading + ascent. The width of the rectangle that the getStringBounds method returns is the horizontal extent of the string. The height of the rectangle is the sum of ascent, descent, and leading. The rectangle has its origin at the baseline of the string. The top y-coordinate of the rectangle is negative. Thus, you can obtain string width, height, and ascent as follows:

double stringWidth = bounds.getWidth();
double stringHeight = bounds.getHeight();
double ascent = -bounds.getY();


If you need to know the descent or leading, you need to use the getLineMetrics method of the Font class. That method returns an object of the LineMetrics class, which has methods to obtain the descent and leading:

LineMetrics metrics = f.getLineMetrics(message, context);
float descent = metrics.getDescent();
float leading = metrics.getLeading();


The following code uses all this information to center a string in its surrounding panel:

FontRenderContext context = g2.getFontRenderContext();
Rectangle2D bounds = f.getStringBounds(message, context);
// (x,y) = top left corner of text double x = (getWidth() - bounds.getWidth()) / 2;
double y = (getHeight() - bounds.getHeight()) / 2;
// add ascent to y to reach the baseline double ascent = -bounds.getY();
double baseY = y + ascent;
g2.drawString(message, (int)x, (int)(baseY));


To understand the centering, consider that getWidth() returns the width of the panel. A portion of that width, namely bounds.getWidth(), is occupied by the message string. The remainder should be equally distributed on both sides. Therefore the blank space on each side is half of the difference. The same reasoning applies to the height. Finally, the program draws the baseline and the bounding rectangle. Screenshot-14 shows the screen display; Example 7-6 is the program listing.

Example 7-6 FontTest.java
 1. import java.awt.*;
 2. import java.awt.font.*;
 3. import java.awt.geom.*;
 4. import javax.swing.*;
 5.
 6. public class FontTest
 7. {
 8. public static void main(String[] args)
 9. {
10. FontFrame frame = new FontFrame();
11. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
12. frame.show();
13. }
14. }
15.
16. /**
17. A frame with a text message panel
18. */
19. class FontFrame extends JFrame
20. {
21. public FontFrame()
22. {
23. setTitle("FontTest");
24. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
25.
26. // add panel to frame
27.
28. FontPanel panel = new FontPanel();
29. Container contentPane = getContentPane();
30. contentPane.add(panel);
31. }
32.
33. public static final int DEFAULT_WIDTH = 300;
34. public static final int DEFAULT_HEIGHT = 200;
35. }
36.
37. /**
38. A panel that shows a centered message in a box.
39. */
40. class FontPanel extends JPanel
41. {
42. public void paintComponent(Graphics g)
43. {
44. super.paintComponent(g);
45. Graphics2D g2 = (Graphics2D)g;
46.
47. String message = "Hello, World!";
48.
49. Font f = new Font("Serif", Font.BOLD, 36);
50. g2.setFont(f);
51.
52. // measure the size of the message
53.
54. FontRenderContext context = g2.getFontRenderContext();
55. Rectangle2D bounds = f.getStringBounds(message, context);
56.
57. // set (x,y) = top left corner of text
58.
59. double x = (getWidth() - bounds.getWidth()) / 2;
60. double y = (getHeight() - bounds.getHeight()) / 2;
61.
62. // add ascent to y to reach the baseline
63.
64. double ascent = -bounds.getY();
65. double baseY = y + ascent;
66.
67. // draw the message
68.
69. g2.drawString(message, (int)x, (int)(baseY));
70.
71. g2.setPaint(Color.GRAY);
72.
73. // draw the baseline
74.
75. g2.draw(new Line2D.Double(x, baseY,
76. x + bounds.getWidth(), baseY));
77.
78. // draw the enclosing rectangle
79.
80. Rectangle2D rect = new Rectangle2D.Double(x, y,
81. bounds.getWidth(),
82. bounds.getHeight());
83. g2.draw(rect);
84. }
85. }


Screenshot-14. Drawing the baseline and string bounds

Java graphics 07fig14.gif


java.awt.Font 1.0

Java graphics api_icon.gif
  • Font(String name, int style, int size)

    creates a new font object.

    Parameters:

    name

    The font name. This is either a font face name (such as "Helvetica Bold") or a logical font name (such as "Serif", "SansSerif")

     

    style

    The style (Font.PLAIN, Font.BOLD, Font.ITALIC or Font.BOLD + Font.ITALIC)

     

    size

    The point size (for example, 12)

  • String getFontName()

    gets the font face name (such as "Helvetica Bold").

  • String getFamily()

    gets the font family name (such as "Helvetica")

  • String getName()

    gets the logical name (such as "SansSerif") if the font was created with a logical font name; otherwise gets the font face name.

  • Rectangle2D getStringBounds(String s, FontRenderContext context) 1.2

    returns a rectangle that encloses the string. The origin of the rectangle falls on the baseline. The top y-coordinate of the rectangle equals the negative of the ascent. The height of the rectangle equals the sum of ascent, descent, and leading. The width equals the string width.

  • LineMetrics getLineMetrics(String s, FontRenderContext context) 1.2

    returns a line metrics object to determine the extent of the string.

  • Font deriveFont(int style) 1.2
  • Font deriveFont(float size) 1.2
  • Font deriveFont(int style, float size) 1.2

    return a new font that equals this font, except that it has the given size and style.

java.awt.font.LineMetrics 1.2

Java graphics api_icon.gif
  • float getAscent() gets the font ascent— the distance from the baseline to the tops of uppercase characters.
  • float getDescent() gets the font descent— the distance from the baseline to the bottoms of descenders.
  • float getLeading() gets the font leading— the space between the bottom of one line of text and the top of the next line.
  • float getHeight() gets the total height of the font— the distance between the two baselines of text (descent + leading + ascent).

java.awt.Graphics 1.0

Java graphics api_icon.gif
  • void setFont(Font font)

    selects a font for the graphics context. That font will be used for subsequent text-drawing operations.

    Parameters:

    font

    A font

  • void drawString(String str, int x, int y)

    draws a string in the current font and color.

    Parameters:

    str

    The string to be drawn

     

    x

    The x-coordinate of the start of the string

     

    y

    The y-coordinate of the baseline of the string

java.awt.Graphics2D 1.2

Java graphics api_icon.gif
  • FontRenderContext getFontRenderContext()

    gets a font render context that specifies font characteristics in this graphics context.

  • void drawString(String str, float x, float y)

    draws a string in the current font and color.

    Parameters:

    str

    The string to be drawn

     

    x

    The x-coordinate of the start of the string

     

    y

    The y-coordinate of the baseline of the string

Fonts and the font.properties File

Sun's Java runtime looks at the font.properties file in the jre/lib directory to find out which logical fonts are available and which symbol sets are contained in a specific font. To be able to use more logical fonts in Java than the default ones, you need to modify this file. Let us look at a typical entry in this file:

[View full width]
serif.0=Times New Roman,ANSI_CHARSET serif.1=WingDings,SYMBOL_CHARSET,NEED_CONVERTED serif.2=Symbol,SYMBOL_CHARSET,NEED_CONVERTED exclusion.serif.0=0100-ffff fontcharset.serif.1=sun.awt.windows.
Java graphics ccc.gifCharToByteWingDingsfontcharset.serif.2=sun.awt.
Java graphics ccc.gifCharToByteSymbolfontcharset.serif.2=sun.awt.CharToByteSymbol


This means: To render a character in the serif font, first check that it is in the "0" range, that is, it is not in the excluded area 0100-ffff. If it is not excluded, use the Times New Roman font to render it. Next, check that the class sun.awt.windows.CharToByteWingDings will accept the character. This (undocumented) class extends the (equally undocumented) class sun.io.CharToByteConverter. There are two key methods in these classes that you work with. The method call:

boolean canConvert(char)


tests whether Java can convert a character. To actually perform the conversion, you need to supply two arrays: one for source characters, and one for the target bytes. Then you call:

int convert(char[] input, int inStart, int inPastEnd,
 byte[] output, int outStart, int outPastEnd)


This method converts the inPastEnd - inStart characters in the array input, starting from inStart, and places them into the byte array output, starting at outStart. It also fills out at most outPastEnd - outStart bytes. (You may wonder why the convert method uses arrays instead of simply converting one character into a byte. The reason is that in some encoding schemes, such as the Japanese JIS code, some characters are encoded as single bytes, others as multiple bytes, with control codes switching between character sets.) If the canConvert method returns true, then Java will render the character in the WingDings font. Otherwise, Java tries the Symbol font. If this fails as well, Java renders the character as a ? (question mark). Note that font descriptions are dependent on the operating system. The lines in these examples describe fonts for Windows. In other operating systems, the font descriptions will be different. For example, in Solaris, the description of the Times font looks like this:

serif.plain.0=
 -linotype-times-medium-r-normal—*-%d-*-*-p-*-iso8859-1


You can add your own fonts to the font.properties file. For example, if you run Java under Windows, add the following lines:

oldstyle.0=Bookman Old Style,ANSI_CHARSET exclusion.oldstyle.0=0100-ffff


You can then make a font:

new Font("OldStyle", Font.PLAIN, 12)


You can even add your own font maps. Let us consider the most common case, that is, when characters in your font are described by single bytes. To add your own font map:

  • Extend the (undocumented and poorly named) class sun.io.CharToByte8859_1. (ISO 8859-1 is the Latin-1 8-bit character set, one of ten 8-bit character sets in the ISO 8859 standard. This Java class can be used as the base class for any Unicode to 8-bit code conversion, not just Latin-1.)
  • Override two methods: canConvert, which returns true for those Unicode characters that are part of your font, and convert, which converts a character array into the equivalent byte arrays.

Here is a practical example. Suppose that you have a Russian font in ISO 8859-5 format. Omitting a couple of technical exceptions, the mapping from Unicode to ISO 8859-5 is simple:

'\u0021'...'\u007E': ch -> ch
'\u0401'...'\u045F': ch -> ch - 0x0360


Here is a converter class that does the conversion.

public class CharToByteRussian extends sun.io.CharToByte8859_1
{
 public boolean canConvert(char ch)
 {
 return 0x0021 <= ch && ch <= 0x007E
 || 0x0401 <= ch && ch <= 0x45F;
 }
 public int convert(char[] input, int inStart, int inPastEnd,
 byte[] output, int outStart, int outPastEnd)
 throws ConversionBufferFullException
 {
 int outIndex = outStart;
 for (int i = inStart; i < inPastEnd; i++)
 {
 char ch = input[i];
 byte b = 0;
 if (0x0021 <= ch && ch <= 0x007E)
 b = (byte)ch;
 if (0x0401 <= ch && ch <= 0x45F)
 b = (byte)(ch - 0x0360);
 if (b != 0)
 {
 if (outIndex >= outPastEnd)
 throw new ConversionBufferFullException();
 output[outIndex] = b;
 outIndex++;
 }
 }
 return outIndex - outStart;
 }
}


To add the Russian font to the Java runtime, you need to place the class CharToByteRussian somewhere on your class path. Then, add the following lines to font.properties:

russian.0=Cyrillic,SYMBOL_CHARSET,NEED_CONVERTED fontcharset.russian.0=CharToByteRussian

Java ScreenShot
Screenshot      
Top
 

Comments