Java растровый шрифт: блиц 1-битное изображение с разными цветами - PullRequest
6 голосов
/ 06 сентября 2011

Я хотел бы реализовать простой рисунок растрового шрифта в приложении на базе Java AWT.Приложение рисует объект Graphics, где я хотел бы реализовать простой алгоритм:

1) Загрузить файл (возможно, с использованием ImageIO.read(new File(fileName))), который представляет собой 1-битный PNG, который выглядит примерно так:

8*8 bitmap font

Т.е. это 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 , как указано в , это решение - оно должно работать, но кажется, что было бы очень неэффективно перекрашивать растровое изображение перед каждой блит-операцией.
  • Использование нескольких 32-битных RGBA PNG с альфа-каналом, установленным на 0 для черных пикселей и на максимум для переднего плана.Каждый желаемый цвет переднего плана должен получить свое собственное предварительно обработанное растровое изображение.Таким образом, я могу сделать фон прозрачным и нарисовать его в виде прямоугольника отдельно, а затем выбрать одно растровое изображение с моим шрифтом, предварительно раскрасить нужным цветом и нарисовать его часть над этим прямоугольником.Мне кажется, это излишнее излишнее - и что делает эту опцию еще хуже - она ​​ограничивает количество цветов переднего плана относительно небольшим количеством (т.е. я могу реально загружать и хранить как сотни или тысячи растровых изображений, а не миллионы)
  • Объединение и загрузка пользовательского шрифта, как описано в , это решение может работать, но, насколько я вижу в документации 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 символ / сек

Ответы [ 2 ]

3 голосов
/ 08 сентября 2011

Вы можете превратить каждое растровое изображение в 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 в левом верхнем углу синего изображения в 10 раз больше ошибок. 50 кадров в секунду, я мог бы поверить, но 500 кадров в секунду кажется .. неправильным.

1 голос
/ 23 сентября 2011

Хорошо, похоже, я нашел лучшее решение.Ключом к успеху был доступ к необработанным пиксельным массивам в базовых структурах 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, который вы можете копировать на обновлениях холста, и базовый массив 4-байтовых целочисленных значений ARGB - 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 => foreground, false => background).

Я постараюсь в ближайшее время опубликовать законченное решение как часть моего набора классов эмуляции терминала Java.

Это решение демонстрирует впечатляющие результаты 26007строк / сек или 1846553 символов / сек - это в 2,3 раза быстрее, чем предыдущий лучший цвет без цвета drawImage().

...