Rendering Text to an Image in Java

1 week ago 9
ARTICLE AD BOX

This may be a case where things are just inherently difficult, but I've run into a roadblock. I'm working on a simple image to ASCII converter, and instead of writing to the console, I'd like to create a new image with the ASCII characters.

I've tried using the Graphics interface for a buffered image, but it has no native way to handle newline characters, and it seems not straightforward to handle that formatting on my own. Is there a straightforward way to do this? Here's a snippet of the unfinished method, compressBrightness() just converts from RGB values to grayscale, and compresses down to a range of 70 values since that's the amount of chars I have for conversion.

enter image description here

import javax.imageio.ImageIO; import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.Raster; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.Buffer; import java.util.Scanner; public class ImageToASCII { //all ASCII characters sorted by their relative amount of white/blackspace taken up private static final char[] gradient = {'$','@','B','%','8','&','W','M','#','*','o','a','h','k', 'b','d','p','q','w','m','Z','O','0','Q','L','C','J','U','Y','X', 'z','c','v','u','n','x','r','j','f','t','/','\\','|','(',')','1','{', '}','[',']','?','-','_','+','~','<','>','i','!','l','I',';',':',',', '"','^','\'','`','.', ' ' }; private static final boolean DEBUG = true; //default method will read from a console input private static BufferedImage getImage(){ System.out.println("Please input an image filepath: "); Scanner consoleInput = new Scanner(System.in); String filepath = consoleInput.nextLine(); BufferedImage image = getImage(filepath); return image; } //overloaded method which allows direct input of filepath private static BufferedImage getImage(String filepath){ BufferedImage image = null; try{ image = ImageIO.read(new File(filepath)); } catch(IOException ignored){ } return image; } //overloaded method which allows direct input of an image url private static BufferedImage getImage(URL imageURL){ BufferedImage image = null; try{ image = ImageIO.read(imageURL); } catch(IOException ignored){ } return image; } //grayscales an image, then compresses brightness into range of 70, which is how many ascii chars we are using private static int[] CompressBrightness(BufferedImage image){ int[] pixelVals = new int[image.getWidth()*image.getHeight()]; image.getRGB(0,0,image.getWidth(),image.getHeight(), pixelVals, 0,image.getWidth()); for(int i = 0; i < pixelVals.length; i++){ int pixel = pixelVals[i]; //in hex, a pixel is stored as AARRGGBB //>> operator ensures we drop the end of the byte so we only get the 2 int values that matter int blue = pixel &0xff; int green = pixel >> 8 & 0xff; int red = pixel >> 16 & 0xff; int alpha = pixel >> 24 & 0xff; /* generally agreed upon brightness formula is sqrt(0.299*R^2 + 0.587 *G^2 + 0.114*B^2) https://alienryderflex.com/hsp.html */ int brightness = (int) Math.sqrt( ((0.299 * Math.pow(red,2)) + (0.587 * Math.pow(green,2)) + (0.114 * Math.pow(blue,2))) ); //compresses brightness into a range of 70, which is the amount of ascii chars we are using //3.64 is the divisor, aka 255/70 brightness = (int) (brightness / 3.64); if(brightness > 70){brightness = 70;} pixelVals[i] = (alpha << 24) | (brightness << 16) | (brightness << 8) | brightness; } return pixelVals; } private static BufferedImage toASCII(BufferedImage image){ int[] brightnessVals = CompressBrightness(image); String pixelsAsASCII = ""; for (int i = 0; i < brightnessVals.length; i++) { int pixelBrightness = brightnessVals[i] &0xff; pixelsAsASCII += gradient[pixelBrightness]; if(i % image.getWidth() == 0 && i != 0){ pixelsAsASCII += "\n"; } } if(DEBUG){ System.out.println(pixelsAsASCII); } BufferedImage ASCIIImage = new BufferedImage(image.getWidth() * 8, image.getHeight() * 21, image.getType()); Graphics AASCIRenderer = ASCIIImage.getGraphics(); Font renderFont = new Font("Monospaced", Font.PLAIN,12); AASCIRenderer.setFont(renderFont); AASCIRenderer.setColor(Color.WHITE); if(DEBUG) { JFrame frame = new JFrame(); frame.setLayout(new FlowLayout()); frame.add(new JLabel(new ImageIcon(ASCIIImage))); frame.pack(); frame.setVisible(true); }; return null; } public static void main (String[] args) { BufferedImage image = getImage("C:\\rickroll.jpg"); toASCII(image); } }

EDIT: added full codebase. Also some clarification of purpose seems necessary: within the toASCII method, I have a print call which prints the "ASCII-fied" image to the console. Ideally I would like a saveable image file such as a .png or .jpg file, with ASCII characters instead of standard pixels.

I see two paths, both of which don't have a clear way to solve: a. I could use the text size to downsample the image, let's assume a (monospaced) character was 8x8 pixels, you could average the brightness of each 8x8 block and convert it to a char. However, Java's font object uses points for scaling, whose pixel size changes based on DPI, with no easy way to convert.

The other option would be to upscale the new image by a factor of the font size, but you run into much the same issue, where a given font doesn't have a consistent pixel size.

Read Entire Article