簡體   English   中英

Java位圖字體:使用不同顏色的1位圖像

[英]Java bitmap font: blitting 1-bit image with different colors

我想在基於Java AWT的應用程序中實現一個簡單的位圖字體繪圖。 應用程序在Graphics對象上繪制,我想在其中實現一個簡單的算法:

1)加載一個文件(可能使用ImageIO.read(new File(fileName)) ),它是1位PNG,看起來像這樣:

8 * 8位圖字體

即它是16 * 16(或16 *多,如果我想支持Unicode)矩陣8 * 8字符。 黑色對應於背景顏色,白色對應於前景。

2)逐字符繪制字符串,將此位圖的相關部分blit到目標Graphics 到目前為止,我只是成功了:

    int posX = ch % 16;
    int posY = ch / 16;

    int fontX = posX * CHAR_WIDTH;
    int fontY = posY * CHAR_HEIGHT;

    g.drawImage(
            font,
            dx, dy, dx + CHAR_WIDTH, dy + CHAR_HEIGHT,
            fontX, fontY, fontX + CHAR_WIDTH, fontY + CHAR_HEIGHT,
            null
    );

它可以工作,但是,唉,它按原樣使文本快照,即我無法用所需的前景和背景顏色替換黑白,我甚至無法使背景透明。

所以,問題是:在Java中是否存在一種簡單(快速!)的方式將一個1位位圖的一部分與另一個位圖相關聯,在blitting過程中將其着色(即用一個給定顏色和所有1個像素替換所有0個像素)和另外一個)?

我研究了幾個解決方案,所有這些解決方案對我來說都不是最理想的:

  • 使用自定義着色BufferedImageOp如此解決方案中所述 - 它應該可以工作,但似乎在每個blit操作之前重新着色位圖是非常低效的。
  • 使用多個32位RGBA PNG,Alpha通道設置為0表示黑色像素,最大值設置為前景。 每個所需的前景色都應該有自己的預渲染位圖。 這樣我可以使背景透明並在blitting之前單獨繪制為矩形,然后用我的字體選擇一個位圖,使用所需顏色預先着色並在該矩形上繪制一部分。 對我來說似乎是一個巨大的矯枉過正 - 以及使這個選項變得更糟的原因 - 它將前景顏色的數量限制到相對較小的數量(即我可以逼真地加載和保持數百或數千個位圖,而不是數百萬)
  • 捆綁和加載自定義字體, 如此解決方案中所述,但就我在Font#createFont文檔中看到的情況而言 ,AWT的Font似乎只適用於基於矢量的字體,而不適用於基於位圖的字體。

可能已經有任何庫實現了這樣的功能嗎? 或者是時候讓我切換到某種更高級的圖形庫,比如lwjgl

基准測試結果

我在一個簡單的測試中測試了幾個算法:我有2個字符串,每個字符71個字符,並且一個接一個地連續繪制它們,就在同一個地方:

    for (int i = 0; i < N; i++) {
        cv.putString(5, 5, STR, Color.RED, Color.BLUE);
        cv.putString(5, 5, STR2, Color.RED, Color.BLUE);
    }

然后我測量所花費的時間並計算速度:每秒字符串和每秒字符數。 到目前為止,我測試的各種實現產生了以下結果:

  • 位圖字體,16 * 16個字符位圖:10991個字符串/秒,780391個字符/秒
  • 位圖字體,預分割圖像:11048字符串/秒,784443字符/秒
  • g.drawString():8952個字符串/秒,635631個字符/秒
  • 彩色位圖字體,使用LookupOp和ByteLookupTable着色:404字符串/秒,28741字符/秒

您可以將每個位圖轉換為Shape (或其中許多)並繪制Shape 請參閱平滑鋸齒狀路徑以獲取Shape的過程。

例如

500+ FPS?!?

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import java.util.Random;

/* Gain the outline of an image for further processing. */
class ImageShape {

    private BufferedImage image;

    private BufferedImage ImageShape;
    private Area areaOutline = null;
    private JLabel labelOutline;

    private JLabel output;
    private BufferedImage anim;
    private Random random = new Random();
    private int count = 0;
    private long time = System.currentTimeMillis();
    private String rate = "";

    public ImageShape(BufferedImage image) {
        this.image = image;
    }

    public void drawOutline() {
        if (areaOutline!=null) {
            Graphics2D g = ImageShape.createGraphics();
            g.setColor(Color.WHITE);
            g.fillRect(0,0,ImageShape.getWidth(),ImageShape.getHeight());

            g.setColor(Color.RED);
            g.setClip(areaOutline);
            g.fillRect(0,0,ImageShape.getWidth(),ImageShape.getHeight());
            g.setColor(Color.BLACK);
            g.setClip(null);
            g.draw(areaOutline);

            g.dispose();
        }
    }

    public Area getOutline(Color target, BufferedImage bi) {
        // construct the GeneralPath
        GeneralPath gp = new GeneralPath();

        boolean cont = false;
        int targetRGB = target.getRGB();
        for (int xx=0; xx<bi.getWidth(); xx++) {
            for (int yy=0; yy<bi.getHeight(); yy++) {
                if (bi.getRGB(xx,yy)==targetRGB) {
                    if (cont) {
                        gp.lineTo(xx,yy);
                        gp.lineTo(xx,yy+1);
                        gp.lineTo(xx+1,yy+1);
                        gp.lineTo(xx+1,yy);
                        gp.lineTo(xx,yy);
                    } else {
                        gp.moveTo(xx,yy);
                    }
                    cont = true;
                } else {
                    cont = false;
                }
            }
            cont = false;
        }
        gp.closePath();

        // construct the Area from the GP & return it
        return new Area(gp);
    }

    public JPanel getGui() {
        JPanel images = new JPanel(new GridLayout(1,2,2,2));
        JPanel  gui = new JPanel(new BorderLayout(3,3));

        JPanel originalImage =  new JPanel(new BorderLayout(2,2));
        final JLabel originalLabel = new JLabel(new ImageIcon(image));

        originalImage.add(originalLabel);


        images.add(originalImage);

        ImageShape = new BufferedImage(
            image.getWidth(),
            image.getHeight(),
            BufferedImage.TYPE_INT_RGB
            );

        labelOutline = new JLabel(new ImageIcon(ImageShape));
        images.add(labelOutline);

        anim = new BufferedImage(
            image.getWidth()*2,
            image.getHeight()*2,
            BufferedImage.TYPE_INT_RGB);
        output = new JLabel(new ImageIcon(anim));
        gui.add(output, BorderLayout.CENTER);

        updateImages();

        gui.add(images, BorderLayout.NORTH);

        animate();

        ActionListener al = new ActionListener() {
            public void actionPerformed(ActionEvent ae) {
                animate();
            }
        };
        Timer timer = new Timer(1,al);
        timer.start();

        return gui;
    }

    private void updateImages() {
        areaOutline = getOutline(Color.BLACK, image);

        drawOutline();
    }

    private void animate() {
        Graphics2D gr = anim.createGraphics();
        gr.setColor(Color.BLUE);
        gr.fillRect(0,0,anim.getWidth(),anim.getHeight());

        count++;
        if (count%100==0) {
            long now = System.currentTimeMillis();
            long duration = now-time;
            double fraction = (double)duration/1000;
            rate = "" + (double)100/fraction;
            time  = now;
        }
        gr.setColor(Color.WHITE);
        gr.translate(0,0);
        gr.drawString(rate, 20, 20);

        int x = random.nextInt(image.getWidth());
        int y = random.nextInt(image.getHeight());
        gr.translate(x,y);

        int r = 128+random.nextInt(127);
        int g = 128+random.nextInt(127);
        int b = 128+random.nextInt(127);
        gr.setColor(new Color(r,g,b));

        gr.draw(areaOutline);

        gr.dispose();
        output.repaint();
    }

    public static void main(String[] args) throws Exception {
        int size = 150;
        final BufferedImage outline = javax.imageio.ImageIO.read(new java.io.File("img.gif"));

        ImageShape io = new ImageShape(outline);

        JFrame f = new JFrame("Image Outline");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(io.getGui());
        f.pack();
        f.setResizable(false);
        f.setLocationByPlatform(true);
        f.setVisible(true);
    }
}

我必須指出藍色圖像左上角的FPS計數有十倍的誤差。 50 FPS我可以相信,但500 FPS似乎......錯了。

好的,看起來我找到了最好的解決方案。 成功的關鍵是訪問底層AWT結構中的原始像素陣列。 初始化就是這樣的:

public class ConsoleCanvas extends Canvas {
    protected BufferedImage buffer;
    protected int w;
    protected int h;
    protected int[] data;

    public ConsoleCanvas(int w, int h) {
        super();
        this.w = w;
        this.h = h;
    }

    public void initialize() {
        data = new int[h * w];

        // Fill data array with pure solid black
        Arrays.fill(data, 0xff000000);

        // Java's endless black magic to get it working
        DataBufferInt db = new DataBufferInt(data, h * w);
        ColorModel cm = ColorModel.getRGBdefault();
        SampleModel sm = cm.createCompatibleSampleModel(w, h);
        WritableRaster wr = Raster.createWritableRaster(sm, db, null);
        buffer = new BufferedImage(cm, wr, false, null);
    }

    @Override
    public void paint(Graphics g) {
        update(g);
    }

    @Override
    public void update(Graphics g) {
        g.drawImage(buffer, 0, 0, null);
    }
}

在這個之后,你有一個buffer ,你可以在畫布更新和ARGB 4字節整數的基礎數組 - data上blit。

可以像這樣繪制單個字符:

private void putChar(int dx, int dy, char ch, int fore, int back) {
    int charIdx = 0;
    int canvasIdx = dy * canvas.w + dx;
    for (int i = 0; i < CHAR_HEIGHT; i++) {
        for (int j = 0; j < CHAR_WIDTH; j++) {
            canvas.data[canvasIdx] = font[ch][charIdx] ? fore : back;
            charIdx++;
            canvasIdx++;
        }
        canvasIdx += canvas.w - CHAR_WIDTH;
    }
}

這個使用一個簡單的boolean[][]數組,其中第一個索引選擇字符,第二個索引迭代原始的1位字符像素數據(true => foreground,false => background)。

我將盡快發布一個完整的解決方案作為我的Java終端仿真類集的一部分。

這個解決方案基准測試令人印象深刻的26007弦/秒或1846553個字符/秒 - 比以前最好的非彩色drawImage()快2.3倍。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM