The Source for Java Technology Collaboration
User: Password:
Register | Login help    

Search

Online Books:
java.net on MarkMail:


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 >> J2SE      
Comments
Comments are listed in date ascending order (oldest first)