Trong phần này chúng ta sẽ tìm hiểu về hệ thống font chữ.
Vẽ kí tự lên màn hình là một chủ đề phức tạp, có khi viết nguyên một quyển sách cũng chưa hết, nên trong bài này mình chỉ bàn về một số thao tác cơ bản.
Có hai loại font là font vật lý (Physical Font) và font logic (Logical Font). Font vật lý là font thật, được lưu trong hệ điều hành, font logic không phải font thật mà thực ra chỉ là 5 hệ font được định nghĩa bởi Java: Serif, SansSerif, Monospaced, Dialog, DialogInput. Khi được gọi thì font logic sẽ được Java map vào font vật lý.
Một Font là một tập hợp các ký tự, một kí tự được gọi là TypeFace, tập hợp các TypeFace giống nhau nhưng khác kiểu font (hoặc các tính chất khác như độ lớn, độ nghiêng…) được gọi là Font-Family.
Font trong hệ điều hành
import java.awt.Font; import java.awt.GraphicsEnvironment; public class AllFontsEx { public static void main(String[] args) { GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); Font[] fonts = ge.getAllFonts(); for (Font font : fonts) { System.out.print(font.getFontName() + " : "); System.out.println(font.getFamily()); } } }
Bản thân Java không chứa thông tin về các kiểu font trong hệ điều hành, Java chỉ lấy thông tin về font của hệ điều hành để sử dụng. Mỗi hệ điều hành có bộ font khác nhau. Để lấy thông tin về font chữ trong hệ điều hành thì chúng ta sử dụng các lớp GraphicsEnvironment
, Font
.
Font[] fonts = ge.getAllFonts();
Phương thức getAllFonts()
trả về danh sách font hiện có trong GraphicsEnvironment
.
System.out.print(fonts[i].getFontName() + " : "); System.out.println(fonts[i].getFamily());
Chúng ta dùng phương thức getFontName()
và getFamimy()
để lấy về tên font và tên family.
... .VnArial : .VnArial .VnArial Bold : .VnArial .VnArial Bold Italic : .VnArial .VnArial Italic : .VnArial .VnArial Narrow : .VnArial Narrow .VnArial Narrow Bold : .VnArial Narrow .VnArial Narrow Italic : .VnArial Narrow .VnArial NarrowH : .VnArial NarrowH .VnArialH : .VnArialH .VnArialH Bold Italic : .VnArialH ...
In text lên màn hình
Trong ví dụ dưới đây, chúng ta sẽ in một đoạn lyric lên panel.
import java.awt.EventQueue; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import javax.swing.JFrame; import javax.swing.JPanel; class Surface extends JPanel { private void doDrawing(Graphics g) { Graphics2D g2d = (Graphics2D) g; RenderingHints rh = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); rh.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2d.setRenderingHints(rh); g2d.setFont(new Font("NewellsHand", Font.PLAIN, 18)); g2d.drawString("Most relationships seem so transitory", 20, 30); g2d.drawString("They're all good but not the permanent one", 20, 60); g2d.drawString("Who doesn't long for someone to hold", 20, 90); g2d.drawString("Who knows how to love you without being told", 20, 120); g2d.drawString("Somebody tell me why I'm on my own", 20, 150); g2d.drawString("If there's a soulmate for everyone", 20, 180); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); doDrawing(g); } } public class SoulmateEx extends JFrame { public SoulmateEx() { initUI(); } private void initUI() { setTitle("Soulmate"); add(new Surface()); setSize(420, 250); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { SoulmateEx ex = new SoulmateEx(); ex.setVisible(true); } }); } }
Để thiết lập kiểu font mong muốn thì chúng ta dùng phương thức setFont()
.
g2d.setFont(new Font("NewellsHand", Font.PLAIN, 18));
Để in một đoạn text thì chúng ta dùng phương thức Graphics2D.drawString()
.
g2d.drawString("Most relationships seem so transitory", 20, 30);
Tạo hiệu dứng đổ bóng cho chữ
import java.awt.Color; import java.awt.EventQueue; import java.awt.Font; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.font.TextLayout; import java.awt.image.BufferedImage; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JLabel; public class ShadowedTextEx extends JFrame { private final int width = 300; private final int height = 130; private final String text = "War is hell"; private TextLayout textLayout; public ShadowedTextEx() { initUI(); } private void initUI() { setTitle("Shadowed Text"); BufferedImage image = createImage(); add(new JLabel(new ImageIcon(image))); setSize(300, 130); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } private void setRenderingHints(Graphics2D g) { g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); } private BufferedImage createImage() { int x = 10; int y = 100; Font font = new Font("Georgia", Font.ITALIC, 50); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g1d = image.createGraphics(); setRenderingHints(g1d); textLayout = new TextLayout(text, font, g1d.getFontRenderContext()); g1d.setPaint(Color.WHITE); g1d.fillRect(0, 0, width, height); g1d.setPaint(new Color(150, 150, 150)); textLayout.draw(g1d, x+3, y+3); g1d.dispose(); float[] kernel = { 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f }; ConvolveOp op = new ConvolveOp(new Kernel(3, 3, kernel), ConvolveOp.EDGE_NO_OP, null); BufferedImage image2 = op.filter(image, null); Graphics2D g2d = image2.createGraphics(); setRenderingHints(g2d); g2d.setPaint(Color.BLACK); textLayout.draw(g2d, x, y); g2d.dispose(); return image2; } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { ShadowedTextEx ex = new ShadowedTextEx(); ex.setVisible(true); } }); } }
Để tạo bóng cho một đoạn text thì chúng ta vẽ hai đoạn text, một đoạn text chính và một đoạn text dùng làm bóng, trong đó đoạn text đổ bóng được làm mờ, có vị trí lệch một ít với đoạn text gốc.
textLayout = new TextLayout(text, font, g1d.getFontRenderContext());
Chúng ta dùng lớp TextLayout
để quy định các tính chất cho đoạn text, lớp này cho phép chúng ta thao tác sâu hơn với font.
textLayout.draw(g1d, x+3, y+3);
Chúng ta vẽ đoạn text lên màn hình có vị trí lệch 3 pixel so với đoạn text gốc.
float[] kernel = { 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f, 1f / 9f }; ConvolveOp op = new ConvolveOp(new Kernel(3, 3, kernel), ConvolveOp.EDGE_NO_OP, null);
Để tăng thêm hiệu ứng thì chúng ta áp dụng hiệu ứng mờ lên đoạn text.
BufferedImage image2 = op.filter(image, null);
Chúng ta áp dụng hiệu ứng mờ lên đoạn text gốc rồi gán vào đoạn text thứ hai dùng làm đổ bóng.
textLayout.draw(g2d, x, y);
Sau đó chúng ta vẽ đoạn text gốc để đảm bảo đoạn text gốc nằm đè lên đoạn text đổ bóng.
Một số tính chất khác
import java.awt.Color; import java.awt.EventQueue; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.font.TextAttribute; import java.text.AttributedString; import javax.swing.JFrame; import javax.swing.JPanel; class Surface extends JPanel { private final String words = "Valour fate kinship darkness"; private final String java = "Java TM"; private void doDrawing(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Font font = new Font("Serif", Font.PLAIN, 40); AttributedString as1 = new AttributedString(words); as1.addAttribute(TextAttribute.FONT, font); as1.addAttribute(TextAttribute.FOREGROUND, Color.red, 0, 6); as1.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, 7, 11); as1.addAttribute(TextAttribute.BACKGROUND, Color.LIGHT_GRAY, 12, 19); as1.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON, 20, 28); g2d.drawString(as1.getIterator(), 15, 60); AttributedString as2 = new AttributedString(java); as2.addAttribute(TextAttribute.SIZE, 40); as2.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER, 5, 7); g2d.drawString(as2.getIterator(), 130, 125); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); doDrawing(g); } } public class TextAttributesEx extends JFrame { public TextAttributesEx() { initUI(); } private void initUI() { add(new Surface()); setSize(620, 190); setTitle("Text attributes"); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { TextAttributesEx ex = new TextAttributesEx(); ex.setVisible(true); } }); } }
Để thay đổi các tính chất của text thì chúng ta dùng các lớp Font
, TextAttribute
và AttributedString
. Lớp Font
quy định kiểu font, lớp TextAttribute
quy định một số tính chất như màu chữ, màu nền… còn lớp AttributedString
lưu thông tin về đoạn text và các tính chất của nó.
AttributedString as1 = new AttributedString(words);
Đầu tiên chúng ta tạo một đối tượng AttributeString.
as1.addAttribute(TextAttribute.FOREGROUND, Color.red, 0, 6);
Chúng ta có thể thêm một số tính chất khác vào đoạn text thông qua phương thức addAttribute
()
, dòng trên có ý nghĩa là thiết lập màu chữ của các kí tự từ vị trí 0 đến vị trí 6 có màu đỏ.
g2d.drawString(as1.getIterator(), 15, 60);
Sau khi đã định nghĩa các tính chất mong muốn, chúng ta vẽ đoạn text lên nhưng chúng ta sẽ lấy đoạn text đó từ phương thức AttributedString.getIterator()
chứ không dùng đoạn string gốc.
Xoay chữ
Trong bài xử lý ảnh chúng ta đã học cách xoay các đối tượng hình học. Bản thân các ký tự được vẽ trong Java cũng là các đối tượng hình học, chúng ta sẽ lấy các đối tượng đó ra mà xoay rồi vẽ như vẽ một đối tượng hình học bình thường.
import java.awt.EventQueue; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import javax.swing.JFrame; import javax.swing.JPanel; class Surface extends JPanel { private void doDrawing(Graphics g) { Graphics2D g2d = (Graphics2D) g.create(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); String s = "Welcome to PhoCode"; Font font = new Font("Courier", Font.PLAIN, 13); g2d.translate(20, 20); FontRenderContext frc = g2d.getFontRenderContext(); GlyphVector gv = font.createGlyphVector(frc, s); int length = gv.getNumGlyphs(); for (int i = 0; i < length; i++) { Point2D p = gv.getGlyphPosition(i); double theta = (double) i / (double) (length - 1) * Math.PI / 3; AffineTransform at = AffineTransform.getTranslateInstance(p.getX(), p.getY()); at.rotate(theta); Shape glyph = gv.getGlyphOutline(i); Shape transformedGlyph = at.createTransformedShape(glyph); g2d.fill(transformedGlyph); } g2d.dispose(); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); doDrawing(g); } } public class RotatedTextEx extends JFrame { public RotatedTextEx() { initUI(); } private void initUI() { add(new Surface()); setTitle("Rotated text"); setSize(280, 210); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { RotatedTextEx ex = new RotatedTextEx(); ex.setVisible(true); } }); } }
Chúng ta sẽ dùng lớp FontRenderContext
và lớp GlyphVector,
lớp FontRenderContext
chứa các thông tin cần thiết để lấy kích thước của kí tự, lớp GlyphVector
lưu thông tin về các đặc điểm hình học của kí tự.
GlyphVector gv = font.createGlyphVector(frc, s);
Đầu tiên chúng ta tạo một đối tượng GlyphVector
, trong đó lưu những thông tin về hình ảnh, vị trí…
int length = gv.getNumGlyphs();
Mỗi kí tự trong đoạn text sẽ được lưu trong một đối tượng hình học riêng, nên chúng ta lấy về số lượng các kí tự (cũng như các đối tượng hình học).
Point2D p = gv.getGlyphPosition(i);
Sau đó chúng ta lặp qua từng kí tự, tại mỗi lần lặp chúng ta lấy ra tọa độ của đối tượng vẽ ra kí tự đó bằng phương thức getGlyphPosition().
double theta = (double) i / (double) (length - 1) * Math.PI / 3;
Tiếp theo chúng ta tính góc xoay cho đối tượng đó dựa vào thứ tự của nó trong chuỗi.
AffineTransform at = AffineTransform.getTranslateInstance(p.getX(), p.getY()); at.rotate(theta);
Tiếp theo chúng ta xoay bằng cách sử dụng lớp AffineTransform
.
Shape glyph = gv.getGlyphOutline(i); Shape transformedGlyph = at.createTransformedShape(glyph);
Cuối cùng chúng ta dùng phương thức getGlyphOutline() để lấy về đối tượng hình học của kí tự hiện tại rồi dùng phương thức createTransformedShape()
để tạo ra đối tượng đó ở trạng thái đã xoay.
g2d.fill(transformedGlyph);
Cuối cùng chúng ta vẽ đối tượng đó lên Panel.