Java 2D – Font chữ


Được đăng vào ngày 22/02/2016 | 0 bình luận
Đánh giá bài viết

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

Đoạn code dưới đây sẽ in ra toàn bộ font chữ hiện được hỗ trợ bởi hệ điều hành của bạn.
AllFontsEx.java
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()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);
Capture

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.

Capture

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 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.

Capture

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.

Capture