Java 位图字体:用不同颜色位图传输 1 位图像

发布于 2024-12-03 11:30:06 字数 2502 浏览 2 评论 0 原文

我想在基于 Java AWT 的应用程序中实现一个简单的位图字体绘制。应用程序利用 Graphics 对象,我想在其中实现一个简单的算法:

1)加载文件(可能使用 ImageIO.read(new File(fileName)) ),它是 1 位 PNG,看起来像这样:

8*8 位图字体

即它是 16*16 (或16*很多(如果我想支持Unicode)8*8字符的矩阵。黑色对应于背景色,白色对应于前景。

2) 逐个字符地绘制字符串,将该位图的相关部分传输到目标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 位位图的一部分位图传输到另一个位图,并在位图传输过程中对其进行着色(即用一种给定颜色替换所有 0 像素,用一种给定颜色替换所有 1 像素)与另一个)?

我研究了几个解决方案,所有这些对我来说看起来都不理想:

  • 使用自定义着色 BufferedImageOp,如 this 中所述解决方案 - 它应该可以工作,但似乎在每次 blit 操作之前重新着色位图效率非常低。
  • 使用多个 32 位 RGBA PNG,将黑色像素的 alpha 通道设置为 0,将前景设置为最大值。每个所需的前景色都应该有自己的预渲染位图。这样,我可以使背景透明,并在位图传输之前将其单独绘制为矩形,然后使用我的字体选择一个位图,用所需的颜色进行预着色,并将其一部分绘制在该矩形上。对我来说似乎是一个巨大的杀伤力 - 是什么让这个选项变得更糟 - 它将前景色的数量限制为相对较小的数量(即我实际上可以加载并保存数百或数千个位图,而不是数百万个
  • )自定义字体,如此解决方案中所述可以工作,但据我在字体# 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 字符/
  • 秒.drawString():8952 个字符串/秒,635631 个字符/秒
  • 彩色位图字体,使用 LookupOp 和 ByteLookupTable 着色:404 个字符串/秒,28741 个字符/秒

I'd like to implement a simple bitmap font drawing in Java AWT-based application. Application draws on a Graphics object, where I'd like to implement a simple algorithm:

1) Load a file (probably using ImageIO.read(new File(fileName))), which is 1-bit PNG that looks something like that:

8*8 bitmap font

I.e. it's 16*16 (or 16*many, if I'd like to support Unicode) matrix of 8*8 characters. Black corresponds to background color, white corresponds to foreground.

2) Draw strings character-by-character, blitting relevant parts of this bitmap to target Graphics. So far I've only succeeded with something like that:

    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
    );

It works, but, alas, it blits the text as is, i.e. I can't substitute black and white with desired foreground and background colors, and I can't even make background transparent.

So, the question is: is there a simple (and fast!) way in Java to blit part of one 1-bit bitmap to another, colorizing it in process of blitting (i.e. replacing all 0 pixels with one given color and all 1 pixels with another)?

I've researched into a couple of solutions, all of them look suboptimal to me:

  • Using a custom colorizing BufferedImageOp, as outlined in this solution - it should work, but it seems that it would be very inefficient to recolorize a bitmap before every blit operation.
  • Using multiple 32-bit RGBA PNG, with alpha channel set to 0 for black pixels and to maximum for foreground. Every desired foreground color should get its own pre-rendered bitmap. This way I can make background transparent and draw it as a rectangle separately before blitting and then select one bitmap with my font, pre-colorized with desired color and draw a portion of it over that rectangle. Seems like a huge overkill to me - and what makes this option even worse - it limits number of foreground colors to a relatively small amount (i.e. I can realistically load up and hold like hundreds or thousands of bitmaps, not millions)
  • Bundling and loading a custom font, as outlined in this solution could work, but as far as I see in Font#createFont documentation, AWT's Font seems to work only with vector-based fonts, not with bitmap-based.

May be there's already any libraries that implement such functionality? Or it's time for me to switch to some sort of more advanced graphics library, something like lwjgl?

Benchmarking results

I've tested a couple of algorithms in a simple test: I have 2 strings, 71 characters each, and draw them continuously one after another, right on the same place:

    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);
    }

Then I measure time taken and calculate speed: string per second and characters per second. So far, various implementation I've tested yield the following results:

  • bitmap font, 16*16 characters bitmap: 10991 strings / sec, 780391 chars / sec
  • bitmap font, pre-split images: 11048 strings / sec, 784443 chars / sec
  • g.drawString(): 8952 strings / sec, 635631 chars / sec
  • colored bitmap font, colorized using LookupOp and ByteLookupTable: 404 strings / sec, 28741 chars / sec

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

甩你一脸翔 2024-12-10 11:30:06

您可以将每个位图转换为一个Shape(或多个)并绘制Shape。请参阅平滑锯齿状路径了解获得形状的过程。

EG

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 似乎......错误。

You might turn each bitmap into a Shape (or many of them) and draw the Shape. See Smoothing a jagged path for the process of gaining the Shape.

E.G.

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);
    }
}

I have to figure there is a factor of ten error in the FPS count on the top left of the blue image though. 50 FPS I could believe, but 500 FPS seems ..wrong.

〆凄凉。 2024-12-10 11:30:06

好吧,看来我已经找到了最好的解决方案。成功的关键是访问底层 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);
    }
}

在此之后,您将获得一个可以在画布更新上进行 blit 的 buffer 和 ARGB 4 字节整数的底层数组 - data

单个字符可以这样绘制:

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 => 前景,假=>背景)。

我将尝试尽快发布一个完整的解决方案作为我的 Java 终端仿真类集的一部分。

该解决方案的基准测试结果为令人印象深刻的 26007 个字符串/秒或 1846553 个字符/秒 - 比以前最好的非彩色 drawImage() 快 2.3 倍。

Okay, looks like I've found the best solution. The key to success was accessing raw pixel arrays in underlying AWT structures. Initialization goes something like that:

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);
    }
}

After this one, you've got both a buffer that you can blit on canvas updates and underlying array of ARGB 4-byte ints - data.

Single character can be drawn like that:

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;
    }
}

This one uses a simple boolean[][] array, where first index chooses character and second index iterates over raw 1-bit character pixel data (true => foreground, false => background).

I'll try to publish a complete solution as a part of my Java terminal emulation class set soon.

This solution benchmarks for impressive 26007 strings / sec or 1846553 chars / sec - that's 2.3x times faster than previous best non-colorized drawImage().

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文