Skip to content

Instantly share code, notes, and snippets.

@crazy4groovy
Created January 5, 2012 17:57
Show Gist options
  • Save crazy4groovy/1566375 to your computer and use it in GitHub Desktop.
Save crazy4groovy/1566375 to your computer and use it in GitHub Desktop.
Convert image(s) (http or local) to colour or b/w ascii art, via Groovy!
/**
* 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 = /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()}"}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment