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

I would like to implement a simple bitmap font pattern in a Java AWT application. The application uses a Graphics object, where I would like to implement a simple algorithm:

1) Download the file (possibly using ImageIO.read(new File(fileName)) ), which is a 1-bit PNG that looks something like this:

8 * 8 bitmap font

those. it is 16 * 16 (or 16 * a lot if I want to support Unicode) a matrix of 8 * 8 characters. Black corresponds to the background color, white corresponds to the leading edge.

2) Draw lines by character, mixing the corresponding parts of this bitmap with the target Graphics . So far, I have managed to achieve something similar:

  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 passes the text as it is, i.e. I cannot replace the black-and-white desired color of the foreground and background, and I cannot even make the background transparent.

So, the question arises: is there a simple (and quick!) Way in Java to split part of one single-bit raster image into another by coloring it during the blitting process (i.e. replacing all 0 pixels with one given color and all 1 pixel with another)?

I researched a couple of solutions, they all look suboptimal to me:

  • Using custom BufferedImageOp coloring as described in this solution - it should work, but it seems that it would be very inefficient to repaint the bitmap before each bright operation.
  • Using multiple 32-bit RGBA PNGs, with the alpha channel set to 0 for black pixels and maximum for the foreground. Each desired foreground color should have its own preliminary rendering of the bitmap. Thus, I can make a transparent background and draw it as a rectangle separately before blitting, and then select one bitmap with my font pre-painted with the desired color and draw part of it above this rectangle. It seems like a huge kink for me - and what makes this option even worse - it limits the number of foreground colors to a relatively small amount (i.e. I can actually load and hold like hundreds or thousands of bitmaps, not millions).
  • Combining and loading a custom font, as described in this solution , may work, but as far as I see in Font # createFont , AWT Font seems to work only with vector fonts, not bitmap fonts.

Maybe there are libraries that implement such functions? Or is it time for me to move on to some more advanced graphics library, something like lwjgl ?

Benchmarking results

I tested a couple of algorithms in a simple test: I have 2 lines, 71 characters each, and drag them one after another, one after another, right in 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 the time and calculate the speed: line per second and characters per second. So far, the different implementation I tested gives the following results:

  • raster font, 16 * 16 characters raster: 10991 lines / sec, 780391 characters / sec
  • raster font, pre-divided images: 11048 lines / sec, 784443 characters / sec
  • g.drawString (): 8952 lines / second, 635631 characters / second
  • color bitmap font colored with LookupOp and ByteLookupTable: 404 lines / second, 28741 characters / second
+6
source share
2 answers

Well, it looks like I have found a better solution. The key to success was access to the original pixel arrays in the underlying AWT structures. Initialization happens like this:

 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 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 that, you have buffer , that you can blit on canvas updates and the base array of four-byte int ARG - data .

A single character can be made as follows:

 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 the first index selects the character, and the second index iterates over the raw 1-bit character pixel data (true => foreground, false => background).

I will try to publish the complete solution as part of my Java emulation emulation class soon.

This solution focuses on an impressive 26007 lines / sec or 1846553 characters / sec - it is 2.3 times faster than the previous best, not painted drawImage() .

+1
source

You can turn each bitmap into a Shape (or many of them) and draw a Shape . See Smoothing the Toothed Path for the Shape Process.

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

I need to understand that in the FPS count in the upper left corner of the blue image there is a factor of ten errors. 50 FPS I could believe it, but 500 FPS seems to be ..wrong.

+3
source

Source: https://habr.com/ru/post/896679/


All Articles