Created
January 5, 2012 17:57
-
-
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!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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