Skip to main content

Big Severe Logging with Ascii Art

Posted by skelvin on August 24, 2004 at 3:46 AM PDT

A little Java 2D can give your log messages much bigger visibility. Or find yourself your own
excuse to have some fun with ascii art graphics using Java 2D...

 #########            ##  ##                             ##  
#########            ##  ##                             ## 
##                       ##                             ## 
##           #####   ##  ##  ##    ##  ## ##   ####     ## 
##          #######  ##  ##  ##    ##  #####  ######    ## 
########   ##    ##  ##  ##  ##    ##  ###   ###  ##    ## 
########       ####  ##  ##  ##    ##  ##    ##    ##   ## 
##          #######  ##  ##  ##    ##  ##    ########   ## 
##         ####  ##  ##  ##  ##    ##  ##    ########   ## 
##         ##    ##  ##  ##  ##    ##  ##    ##         ## 
##         ##   ###  ##  ##  ##   ###  ##    ###   ##      
##         ########  ##  ##  ########  ##     ######    ## 
##          #### ##  ##  ##   #### ##  ##      ####     ## 

I consider myself a lucky guy, being allowed to work full-time with Java and get paid for it.
Only occasionally I do miss the chance to step back a little from business demands and just
write some cool, yet more or less useless code.

So I jumped to the occasion when a day's work was almost done and I had the mean idea of putting
something cool to use: I just had spent some thirty minutes tracing a bug when I noticed that
single message in the enormous log file that revealed useful information. I cursed myself
for not noticing earlier that it was logged as "SEVERE" and caught myself thinking
"Couldn't the fellow who put that log message in there have done it in 40 pixels big bold face?"
Well, of course not, it's plain ascii text, ... but hey, wait - I can do that!

Those of you who have been around long enough probably remember the novelty services that accepted a
small photo and handed back to you an ascii print out that let you recognize the photo - if you
stepped back far enough for the black and white to get blurred into a gray distribution.
We really have come a long way with real-time photo-realistic graphics and powerful APIs like
Java 2D and 3D!

Well, right now I could use some very simple, black and white ascii art:
Create an image, draw the string to it, get the pixel raster and print out a space for a white
pixel and some other character for a black pixel. Ten minutes' job.

In fact, it turned out to be two hours fumbling with Java 2D and the logging system.
But that was due to my partial knowledge of those APIs on the one hand, and my inherent desire to
go beyond the quick and dirty initial implementation and deal correctly with line breaks, tabs
and null text on the other hand. Well, at least I stopped short of trying to support right-to-left,
bottom-to-top text. I'm leaving that as an exercise for the reader ;-)

Text to Ascii Art

So let's have a short walk through the most important parts of the code.

I'd like to have something like this:

public static String getAsciiArt(Font font, String text) {

Calling getAsciiArt(new Font("SansSerif", Font.PLAIN, 18), "Failure!") should give the string
you see above.

On we go: Before you can do anything sensible with text as graphics, you need a FontRenderContext to
specify exactly how a font is mapped to a pixel raster:

            FontRenderContext fontRenderContext = new FontRenderContext(null, false, false);

The output does not need to be transformed in any way (rotated, scaled, ...), so passing null
gets me an IdentityTransform. The two false literals specify no anti-aliasing and
non-fractional metrics (Does not make sense when you think of a pixel as a character, right?)
The usual way to get a FontRenderContext instance is from the Graphics2D object passed into
any paint() method of components, but this isn't the usual paint application, anyway.

Now before the text can be drawn into an image I need to know the image size. That resulted
in some heavy application of the engineering pattern called trial and error:

Most methods (e.g. TextLayout.getBounds()) were not working as expected, e.g. ignoring white
space at the text's start or end (which makes some sense, there's nothing to draw there).
Finally I found that creating a GlyphVector and using its logical bounds did what I wanted:

                GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, text);
                Rectangle2D bounds      = glyphVector.getLogicalBounds();

Next, simply create an image to paint to:

                BufferedImage image = new BufferedImage((int) bounds.getWidth(), 
                                                        (int) bounds.getHeight(),
                                                        BufferedImage.TYPE_BYTE_BINARY);

Drawing into the image is quick, just get a graphics object from the image and use it
to draw to the image:

                Graphics2D graphics = image.createGraphics();
                graphics.drawGlyphVector(glyphVector, 0, ascent);

I haven't explained the "ascent" yet, so what's that about? The drawGlyphVector() method
does not document at all what the x/y position passed into it is relative to. I expected
it to be the baseline, i.e. that line you write on in a lined text book with characters
like "y" extending further down. That assumption was correct, but unfortunately the GlyphVector
class has no easy way to retrieve the baseline.

Well, TextLayout seems to supply what I needed:

                TextLayout textLayout = new TextLayout(text, font, fontRenderContext);

Grmpf! textLayout.getBaseline() does not return the actual position of the baseline, but rather
its type - a stupid name for this method. I only learned about different types of baselines
after some unsuccessful attempts to position the text and I'd rather not elaborate on that here.
Still, TextLayout has a nice method called getAscent(), which is just what I need - the delta
from the top of the bounds to the baseline:

                float ascent = textLayout.getAscent();

What's left is to get the actual pixel raster and iterate over pixels appending characters
to the log:

                    Raster raster = image.getRaster();
                    ...
                    raster.getPixel(x, y, ints);
                    int pixelValue = ints[0];
                    buffer.append(pixelValue > 0 ? '#' : ' ');

There actually is quite a bit more code, but you can have a look at that for yourself in the complete
implementation below.

Logging in Ascii Art

We are using our own wrapper around log4j, but for this blog I'll stick to the standard Java
logging mechanism (from java.util.logging).

Before any text is actually "published" (the log system's lingo for finally outputting it),
it can be formatted. Well, that's nice because our ascii art output can be easily implemented
as a Formatter. On second thought, I make that a Formatter that wraps another Formatter, so
that the usual formatting options (which may include timestamp, thread name etc.) aren't lost.

So let's supply a base formatter instance plus a font used for ascii art and a minimum log level
to begin logging in ascii art. Oh yeah, and let those arguments default to something sensible
if nulls are passed:

public class AsciiArtFormatter extends Formatter {
    public AsciiArtFormatter(Formatter formatter, Level level, Font font) {
        this.formatter = (formatter == null ? new SimpleFormatter() : formatter);
        this.level     = level == null ? Level.SEVERE : level;
        this.font      = font == null ? new Font("SansSerif", Font.PLAIN, 18) : font;
    }

The one method that has to be implemented is, of course, format():

    public String format(LogRecord record) {
        String message = formatter.format(record);
        if(record.getLevel().intValue() >= level.intValue()) {
            message = message + getAsciiArt(font, record.getMessage());
        }
        return message;
    }

I have included the complete message (as obtained from the wrapper formatter's format() method) so that
the text can be found if you search the log file using your favorite text editor and then append
the ascii art string to it. The ascii art is indeed graphics - with quite low resolution - so that if
only that were output, you wouldn't be able to find anything if you were searching for some important text.

Code

Finally, here's the complete code, starting with a simple usage example:

import java.io.File;
import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Main {
    public static void main(String[] args) throws IOException {
        String userHome    = System.getProperty("user.home");
        File   logFile     = new File(userHome, "bigSevereText.log");
        String logFilePath = logFile.getPath();
        System.out.println("Logging to file " + logFilePath);   

        Logger logger = Logger.getLogger("default");
        logger.setLevel(Level.FINEST);
        FileHandler logFileHandler = new FileHandler(logFilePath);
        logFileHandler.setFormatter(new AsciiArtFormatter());
        logger.addHandler(logFileHandler);

        logger.fine("Fox prepares to jump.");
        logger.info("Fox is ready to jump.");
        logger.severe("Fox failed\nto jump over\nlazy dog.");
        logger.info("Dog is chasing fox.");
    }
}

I have made the getAsciiArt() method part of the formatter, but you might like to move it to some
other place (it is totally independent from logging after all):

import java.util.logging.Formatter;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
import java.util.logging.Level;
import java.util.Arrays;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.geom.Rectangle2D;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.TextLayout;

public class AsciiArtFormatter extends Formatter {
    private final Formatter formatter;
    private final Font      font;
    private final Level     level;

    public AsciiArtFormatter() {
        this(null);
    }

    public AsciiArtFormatter(Formatter formatter) {
        this(formatter, null, null);
    }

    /**
     * Creates a formatter that will additionally use ascii art to log the message in the
     * given font
if its log level is at least level.
*
* @param formatter used to format the message before appending ascii art message.
* If null a default SimpleFormatter is used.
* @param level minimum level needed to enable ascii art logging.
* If null Level.SEVERE is used.
* @param font font used for any ascii art output.
* If null a sans serif font of size 18 is used.
*/

public AsciiArtFormatter(Formatter formatter, Level level, Font font) {
this.formatter = (formatter == null ? new SimpleFormatter() : formatter);
this.level = level == null ? Level.SEVERE : level;
this.font = font == null ? new Font("SansSerif", Font.PLAIN, 18) : font;
}

public String format(LogRecord record) {
String message = formatter.format(record);
if(record.getLevel().intValue() >= level.intValue()) {
message = message + getAsciiArt(font, record.getMessage());
}
return message;
}

/**
* @return the text as 'Ascii Art' in the given font using a '#' character for
* each pixel (the text itself if any runtime exception occurs)
*/

public static String getAsciiArt(Font font, String text) {
try {
if(text == null) {
text = "(null)";
}
int tabWidth = 4;

text = text.replaceAll("\\t", repeat(' ', tabWidth));
FontRenderContext fontRenderContext = new FontRenderContext(null, false, false);
StringBuffer buffer = new StringBuffer();
String[] lines = text.split("\n|\r\n", 0);
for(int lineIndex = 0; lineIndex < lines.length; lineIndex++) {
// How stupid: TextLayout does not handle white space like I want to, and
// GlyphVector has no convenient method to calculate overall ascent/descent.
// So just use both...

String line = lines[lineIndex];
GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, line);
Rectangle2D bounds = glyphVector.getLogicalBounds();
int width = (int) bounds.getWidth();
int height = (int) bounds.getHeight();

if(line.length() == 0) {
buffer.append(repeat('\n', height));
continue;
}

TextLayout textLayout = new TextLayout(line, font, fontRenderContext);
float ascent = textLayout.getAscent();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY);
Graphics2D graphics = image.createGraphics();

try {
graphics.drawGlyphVector(glyphVector, 0, ascent);

Raster raster = image.getRaster();
int[] ints = new int[1];
for(int y = 0; y < height; ++y) {
for(int x = 0; x < width; ++x) {
raster.getPixel(x, y, ints);
int pixelValue = ints[0];
buffer.append(pixelValue > 0 ? '#' : ' ');
}
buffer.append('\n');
}
}
finally {
graphics.dispose();
}

}

return new String(buffer);
}
catch(RuntimeException e) {
// Don't want anything silly to happen only because using this fun method.
// Revert to plain output ;-(

return text;
}
}

/**
* @return a string consisting of the character c repeated count times.
*/

public static String repeat(char c, int count) {
char[] fill = new char[count];
Arrays.fill(fill, c);
return new String(fill);
}
}

Related Topics >>