Subscribe Bookmark



Mar 21, 2013

Can I make ASCII Art with JMP?

 ASCII is the name of one of the original character encodings. A character encoding maps numbers into character shapes; the number 65 is a capital letter A in the ASCII scheme. ASCII only allows for about 95 printable characters and is very US-centric. But at the dawn of computing, before pictures were invented, there was ASCII Art. Often printed on a Teletype on yellow paper, JMP is going to use blue paper.


Walt Disney, source image here. Thanks wikimedia!

This script uses features in JMP 13. The first bit of code sets up some parameters; they are moved to the front of the script to make it easy to tweak the settings.

fontsize = 6; // bigger font has more variation in pixel density but ... is bigger
fontname = "Courier New"; // monospaced fixed width is important for display
fontcolor = RGB Color( 0, 0, 0 ); // black.  the characters never go completely black;
backgroundcolor = RGB Color( .75, 1, 1 ); // tinting the background helps darken the image
nlines = 100; // height of result, with big font you'll be scrolling
url = "";

// grab an image.  Thanks wikipedia!
image = Open( url );


Next, build a data table that describes how light or dark each character in the font is. This table has four columns, and each is a Formula column. The code column uses the row number (plus 31) to make an ASCII code. The char column turns the code into the printable character by making a one-element matrix of the code, converting that to binary, and converting that to character. You'll see another example below. The bitmap column is an expression column holding pictures of the characters. And finally, the important nBlack column holds the number of black pixels in the character; it retrieves the pixel matrix and adds them up. Because the characters are anti-aliased, the result is not an integer, but that makes it even better! Only the red pixel array is fetched from the bitmap because all three r,g,b arrays should be identical for black text on white background.

// build a table of ascii codes, sorted by pixel density
dt = New Table( "ASCII",
    Add Rows( 95 ), // space thru just before rub-out
    New Column( "code", numeric, formula( Row() + 31 ) ), // space is 32
    New Column( "char", Character, "Nominal", Formula( Blob To Char( Matrix To Blob( code::code, "uint", 1, "big" ) ) ) ), // num to ascii
    New Column( "bitmap", Expression, formula( Text Box( char, <<SetFontSize( fontsize ), <<SetFontName( fontname ) ) << getPicture ) ),
    New Column( "nBlack", Numeric, formula( Sum( 1.0 - (bitmap[] << getpixels( "r" ))[1] ) ) ) // count pixels.  edges may be alpha blended
dt << RunFormulas; // important before clearing formulas, make sure they ran!
dt:code << deleteFormula; // remove formulas before sorting or the sort will not be effective because
dt:char << deleteFormula; // the formulas will re-evaluate, putting you right back where you started.
dt:bitmap << deleteFormula;
dt:nBlack << deleteFormula;
dt:bitmap << SetDisplayWidth( 110 ); // force a nice width
dt << sort( By( nBlack ), ReplaceTable, Order( Descending ) ); // descending is needed to make small numbers darker


Remove some rows from the table. If there are no vowels, distracting letter combinations may not show up, and some characters, like underscore, have all their pixel weight off center, creating distracting blobs of white space. I wrote the loop backwards because I thought I'd be deleting rows along the way. Then I figured out how selectRows and deleteRows worked.

// remove vowels so words don't appear, and a couple of off-center characters so empty spots don't appear
For( r = N Rows( dt ), r >= 1, r--,
    If( Contains( "AEIOUYaeiouy_`", dt:char[r] ),
        dt << selectrows( r )
dt << DeleteRows;


Getting close now. Scale the image (loaded above), make sure it is gray scale, and use each gray scale pixel to select a letter from the table. Since the NRows(dt) is used in the index calculation, the deleted rows are accounted for. The variable indexes holds a 2D matrix of row numbers (indexes) in the data table. Matrix magic in the last line; dt:code[ ] is a 1D matrix, but indexed by a 2D matrix. The result is a 2D matrix of looked-up codes.

// prepare the image, size, grayscale, look up letters for pixels
{oldx, oldy} = image << getsize; // need the old size to make a scale factor for resizing
image << scale( (nlines * 1.3) / oldx, nlines / oldy ); // stretch horz because font stretches vert
{r, g, b} = image << getPixels( "rgb" ); // in case the image isn't gray scale
gray = .30 * r + .59 * g + .11 * b; // 0..1 gray scale with 0:black and 1:white
indexes = Round( 1 + (N Rows( dt ) - 1) * gray ); // get indexes into the data table (descending needed here!)
codes = dt:code[indexes]; // the heavy lifting is in this line: get the ascii codes (still numbers)


At this point the image is done; it just needs to be displayed. This will create a JMP window using display boxes that are forced to hold the same fixed width font the data table was planning for. You see the blobToChar/MatrixToBlob construction again, but this time operating on multiple codes/characters from a row of the codes matrix.  r,0 means all columns of row r.

// generate the displaybox tree
vl = V List Box( Button Box( url, Web( url ), <<underlinestyle( 1 ) ) ); // the display is built in this vlist, out of lines of textboxes
vl << append( Spacer Box( size( 5, 5 ) ) );
For( r = 1, r <= N Rows( codes ), r += 1, // codes is a 2D matrix of ascii codes
    vl << append(
        Text Box(
            Blob To Char( Matrix To Blob( codes[r, 0], "uint", 1, "big" ) ), // ascii conversion of entire line
            <<SetFontSize( fontsize ),
            <<SetFontName( fontname ),
            <<setwidth( 1000 ),
            <<fontColor( fontcolor ),
            <<BackgroundColor( backgroundcolor )

New Window( "Walt", vl ); // and show it


That's it. 

Article Tags