Thursday, December 29, 2011

ASCII Art

/**
 * Groovy ASCII Art. Converts an image into ASCII.
 * This doesn't work under the web console due to missing AWT classes.
 *
 * Author  : Cedric Champeau (http://twitter.com/CedricChampeau)
 * Updated : Steven Olsen (http://crazy4groovy.blogspot.com)
 */
import java.awt.color.ColorSpace as CS
import java.awt.geom.AffineTransform
import javax.imageio.ImageIO
import java.awt.image.*

String nl = System.getProperty("line.separator")
String slash = System.getProperty("file.separator")
def input = System.console().&readLine

def charset1 = /#@$&%*o=^|-:,'. / //16 chars
//def charset2 = /#@$&%*o=^|-:,'. / //16 chars
def charset2 = /ABCDEFGHIJKLMNOP/ //16 chars

/////////CLI///////////
def cli = new CliBuilder(usage:'asciiArt [options] [path/file/url]', header:'Options:')
cli.h    (longOpt:'help', 'print this message')
cli.bw    (longOpt:'blackWhiteText', 'set normal black/white text')
cli.ctxt(longOpt:'colourText', 'set html colour (text)')
cli.cbg    (longOpt:'colourBackground', 'set html colour (background)')
cli.ics    (longOpt:'isCharSequ', 'output char map in sequence')
cli.vf    (longOpt:'verifyFile', 'verify each file write with a confirmation message')
cli.r    (longOpt:'recursiveFiles', 'recursively iterate through all files in a directory')
cli.cm    (longOpt:'characterMapping', args:1, argName:'percent', 'set custom char map (16)')
cli.s    (longOpt:'scale', args:1, argName:'percentage', 'scale image resolution for output processing (default=40)')
cli.ext    (longOpt:'fileExtension', args:1, argName:'ext', 'name of file extension (default=txt or html)')
cli.incl(longOpt:'fileExtensionInclude', args:1, argName:'regex', 'regex -- name of file extensions to include (default=jpe?g|png|gif)')
cli.outDir    (longOpt:'outputDir', args:1, argName:'...\\dir\\', 'output into dir')
cli.outFile    (longOpt:'outputFile', args:1, argName:'...\\file', 'output into file')

def opt = cli.parse(args)

if (opt.h) {
    cli.usage(); return
}
/////////CLI///////////

List srcs
if (opt.arguments().size() >= 1)
    srcs = opt.arguments()
else
    srcs = [input('image file (local or http): ')] ?: ['http://gordondickens.com/images/groovy.png']

srcs = srcs.collect{ it.replaceAll('"','').split(',') }.flatten()

Set imgSrcList = [] as SortedSet
String filter = opt.incl ?: 'jpe?g|png|gif'
imgSrcList.metaClass.leftShift { if (it.split('\\.')[-1] ==~ "(${filter})") { delegate.add it } }

srcs.each { s ->
    boolean isRemote = s ==~ 'https?://.*'
    if (isRemote) {
        imgSrcList.add s // bypass meatclass filter with .add
        return
    }
    
    def f = new File(s)
    if (!f.directory == true && f.name.split('\\.')[-1] ==~ '(jpe?g|png)') {
        imgSrcList << s
        return
    }
    
    if (opt.r)
        f.eachFileRecurse { fi -> 
            imgSrcList << fi.path }
    else
        f.eachFile { fi -> 
            imgSrcList << fi.path }
}

boolean isMultiImg = (imgSrcList.size() > 1)

Boolean isCharSequGlobal = null // once set to true/false, val will always be used
imgSrcList.eachWithIndex { src, i ->
try {

println "** IMAGE ${i+1} of ${imgSrcList.size()}: $src ..."

boolean isRemote = src ==~ 'https?://.*'
def imgSrc = ImageIO.read( isRemote ? new URL(src) : new File(src))

def scale = opt.s ? opt.s.toBigDecimal() / 100 : 0.4G
String fileName = opt.outFile ?: opt.outDir ? '' : 'SCREEN'

boolean convert = true
while (convert) {
    ////////////INPUT START////////////
    scale *= 100 // reset scale to %
    scale = isMultiImg ? scale : (input("scale of ascii art (percentage) [${scale}]: ") ?: scale)
    scale = scale.toBigDecimal() / 100 // prep scale for usage
    boolean isHtmlColour = opt.bw ? false : (opt.ctxt ?: opt.cbg ?: (input('html colour chars? [y/N]: ').toLowerCase().contains('y') ? true : false) )
    boolean isBgColour = false
    if (isHtmlColour)
        isBgColour = opt.cbg ?: isMultiImg ? false : (input('set background colour? [y/N]: ').toLowerCase().contains('y') ? true : false)
    String charsMapping = opt.cm ?: isMultiImg ? '' : (input('custom char set? (16): ') ?: null)
    if (isHtmlColour && charsMapping && charsMapping.size() < 16 && !isMultiImg)
        println "WARNING: custom char set size is less than 16 (${charsMapping.size()})\n -- must be output in order!"
    boolean isCharSequ = isCharSequGlobal ?: false
    if (isHtmlColour && charsMapping && charsMapping.size() >= 1) {
        isCharSequ = opt.ics ?: isCharSequGlobal ?: (input('output custom chars in order? [Y/n]:').toLowerCase().contains('n') ? false : true)
        isCharSequGlobal = isCharSequ
    }
    if (isHtmlColour && charsMapping && charsMapping.size() < 16 && !isCharSequ)
        println "ERROR: custom char set REJECTED -- size is less than 16 (${charsMapping.size()}) and output is not ordered.\n -- Using default char set."
    if (!isHtmlColour && charsMapping && charsMapping.size() < 16)
        if (i == 0) println "ERROR: non-colour custom char set REJECTED -- size is less than 16 (${charsMapping.size()})\n -- Using default char set."
    if (!isCharSequ && charsMapping?.size() < 16)
        charsMapping = isHtmlColour ? charset2 : charset1
    
    fileName = opt.outFile ?: opt.outDir ? '' : isMultiImg ? '' : input("save ascii art into File (SCREEN = print to screen) [${fileName}]: ") ?: fileName
    ////////////INPUT END////////////

    def yScaleOffset = isHtmlColour ? 0.7 : 0.6 // ascii imgs looked too "tall" -- dev tweakable!
    def cSpace = isHtmlColour ? CS.CS_sRGB : CS.CS_GRAY

    ////////GENERATE////////
    def img = imgSrc
    if (scale != 1.0) {
        def tx = new AffineTransform()
        tx.scale(scale, scale * yScaleOffset)
        def op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR)
        def scaled = new BufferedImage((int) (imgSrc.width * scale), (int) (imgSrc.height * scale * yScaleOffset), imgSrc.type)
        img = op.filter(imgSrc, scaled)
    }

    img = new ColorConvertOp(CS.getInstance(cSpace), null).filter(img, null)

    BigInteger pixelCntr = 0G
    def ascii = { rgb ->
        int r = (rgb & 0x00FF0000) >> 16
        int g = (rgb & 0x0000FF00) >> 8
        int b = (rgb & 0x000000FF)

        int gs
        if (isCharSequ)  gs = (pixelCntr++) % charsMapping.size()
        else  gs = ((int) ( r + g + b ) / 3) >> 4 // multiple of 16

        return [ charsMapping.charAt(gs), [r,g,b] ]
    }

    String preStyle = " style='opacity:1.0;font-size:0.8em;line-height:85%;${ isBgColour ? 'color:#FFF;' : '' }'"
    String spanStyle = isBgColour ? "background-color" : "color"

    StringBuilder sb = new StringBuilder()
    if(isHtmlColour) sb.append("<style>pre img{opacity:0.0;border:4px dotted #444} pre img:hover{opacity:0.95;}</style>"+nl+
        "<pre${preStyle}>"+nl+
        "<div style='position:absolute'><img src='${!isRemote ? 'file://' : ''}${src}' style='position:absolute;top:5px;left:5px;'/></div>"+nl)
    img.height.times { y ->
        img.width.times { x ->
            (chr, rgb) = ascii(img.getRGB(x, y))
            if (isBgColour || (isHtmlColour && chr != ' '))
                sb.append("<span style='${spanStyle}:rgb(${rgb.join(',')});'>${chr}</span>")
            else
                sb.append(chr)
        }
        sb.append(nl)
    }
    if(isHtmlColour) sb.append("</pre>"+(nl * 2))
    ////////GENERATE////////

    if (fileName == 'SCREEN') {
        println sb.toString()
    }
    else {
        if (!fileName) {
            File f = new File(src) // to get file name and parent fields
            String fExt = opt.outFile ? '' : '.ascii'
            fExt += opt.outFile ? '' : ('.' + (opt.ext ?: isHtmlColour ? 'html' : 'txt'))
            fileName =  (opt.outDir ?: f.parent) + slash + f.name + fExt
        }

        boolean ok = !opt.vf ?: input("WARNING: writing to file ${fileName} ok? [Y/n]").toLowerCase().contains('n') ? false : true
        File f = new File(fileName)
        if (ok) {
            f << sb.toString()
            println "\t>> ${f.name}"
        }
    }

    convert = isMultiImg ? false : (input('>> export this image to ascii format again? [y/N]: ').toLowerCase().contains('y') ? true : false)
    if (convert) println '=' * 40
}

} catch (Exception e) { println "\tERROR: ${e.toString()}"}
}


gist link

No comments:

Post a Comment