/** * 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