Keith Vetter 2009-07-27 : Back in the late 70's I remember at the Chicago Science Museum getting my picture taken and having it printed just using various text characters. I thought it was really cool.
I recently had some free time so I decided to recreate that technology. It works by tiling the image and mapping the average brightness of each tile into a text character. The size of the tile is controlled by the resolution parameter.
One problem is that all my images are large and the resulting ASCII image is huge. To (partially) handle this you can have the image first shrunken, via the subsample parameter, before converting to ASCII. It's better to use dedicated image software to better shrink and crop the image.
Jeff Smith 2021-04-19 : Below is an online demo using CloudTk. This demo runs "TkAsciiArt" in an Alpine Linux Docker Container. It is a 20.7MB image which is made up of Alpine Linux + tclkit + TkAsciiArt.kit + libx11 + libxft + fontconfig + font-ibm-type1. It is run under a user account in the Container. The Container is restrictive with permissions for "Other" removed for "execute" and "read" for certain directories.
##+########################################################################## # # TkAsciiArt -- convert image into text # by Keith Vetter, July 2009 # package require Tk 8.5 package require Img # Maps from brightness 0-100 to an ASCII character set asciiMap { 10 \# 17 @ 24 & 31 $ 38 % 45 | 52 ! 59 ; 66 : 73 ' 80 ` 87 . 999 " " } # Size of tile corresponding to one ASCII character set R [dict create UltraLow 8 Low 6 Normal 3 High 2 UltraHigh 1] array set S {title "tkAsciiArt" thumbHeight 200} array set A {width 0 height 0 imgName img2.jpg resolution Normal fontSize 4 drawn - subsample None size,org "?x?" size,sub "?x?" size,ascii "?x?" size,final "?x?" } catch {font delete myFont} font create myFont -family courier -size 4 image create bitmap ::img::star2 -data { #define plus_width 7 #define plus_height 7 static char plus_bits[] = { 0x49, 0x2a, 0x1c, 0x7f, 0x1c, 0x2a, 0x49} } proc DoDisplay {} { wm geom . +5+5 ::ttk::frame .ctrl -pad {.1i .1i 0 0} ::ttk::frame .left ::ttk::frame .right ::ttk::frame .img pack .ctrl -side bottom -fill x pack .left -in .ctrl -side left -fill y pack .right -in .ctrl -side left -fill both -expand 1 pack .img -side top -fill both -expand 1 set fontHeight [font metrics myFont -linespace] set h [expr {([winfo screenheight .] - 300) / $fontHeight}] set fontWidth [font measure myFont "m"] set w [expr {int(([winfo screenwidth .] * .8) / $fontWidth)}] set h [expr {min(133,$h)}] set w [expr {min(260,$w)}] ::ttk::scrollbar .sb_y -command {.t yview} -orient vertical ::ttk::scrollbar .sb_x -command {.t xview} -orient horizontal text .t -font myFont -width $w -height $h -wrap none \ -yscroll {.sb_y set} -xscroll {.sb_x set} grid .t .sb_y -in .img -sticky news grid .sb_x -in .img -sticky ew grid columnconfigure .img 0 -weight 1 grid rowconfigure .img 0 -weight 1 ::ttk::label .left.lname -text "Image" ::ttk::entry .left.ename -textvariable ::A(imgName) ::ttk::button .left.getname -image ::img::star2 -command GetFile ::ttk::label .left.orgsize -textvariable ::A(size,org) -anchor c ::ttk::label .left.lsub -text "Subsample" ::ttk::combobox .left.sub -textvariable ::A(subsample) \ -state readonly -values {None 2 3 4 5 10 15 20} -exportselection 0 bind .left.sub <<ComboboxSelected>> GetSize ::ttk::label .left.subsize -textvariable ::A(size,sub) -anchor c ::ttk::label .left.lresolution -text "Resolution" ::ttk::combobox .left.resolution -textvariable ::A(resolution) \ -state readonly -values [dict keys $::R] -exportselection 0 bind .left.resolution <<ComboboxSelected>> GetSize ::ttk::label .left.asciisize -textvariable ::A(size,ascii) -anchor c ::ttk::label .left.lfsize -text "Font size" ::ttk::combobox .left.fsize -textvariable ::A(fontSize) \ -state readonly -values {1 2 3 4 5 6 7 8 9 10 11 12} -exportselection 0 ::ttk::label .left.finalsize -textvariable ::A(size,final) -anchor c ::ttk::button .left.go -text Go -command Go ::ttk::button .left.about -text About -command About image create photo ::img::thumb -height 100 -width 100 ::ttk::label .right.thumb -image ::img::thumb -relief ridge grid .left.lname .left.ename .left.getname .left.orgsize -sticky ew grid .left.lsub .left.sub - .left.subsize -sticky ew grid .left.lresolution .left.resolution - .left.asciisize -sticky ew grid .left.lfsize .left.fsize - .left.finalsize -sticky ew grid .left.go - - .left.about -pady .1i pack .right.thumb -fill both -expand 1 -side left grid config .right.thumb -padx {.25i 0} update idletasks wm geom . [wm geom .] bind all <F2> {console show} } proc About {} { set msg "$::S(title)\nby Keith Vetter\nJuly 2009\n\n" append msg "$::S(title) converts an image file into ASCII\n" append msg "text by tiling the image and mapping the\n" append msg "average brightness of each tile to a text\n" append msg "character.\n\n" append msg "The Resolution parameter controls the tile size.\n" append msg "The Subsample parameter lets you shrink the\n" append msg "image before converting (to better handle large\n" append msg "digital photographs)." tk_messageBox -title "About $::S(title)" -message $msg } proc GetSize {} { global A set w $A(width) set h $A(height) set A(size,org) [MakeSize $w $h] set sub [expr {$A(subsample) eq "None" ? 1 : $A(subsample)}] set w1 [expr {$w/$sub}] set h1 [expr {$h/$sub}] set A(size,sub) [MakeSize $w1 $h1] set xStep [dict get $::R $A(resolution)] set xStop [expr {$w1 / $xStep}] set yStep [expr {$xStep * 2}] set yStop [expr {$h1 / $yStep}] set A(size,ascii) [MakeSize $xStop $yStop] set w3 [font measure myFont [string repeat "x" $xStop]] set h3 [expr {[font metric myFont -linespace] * $yStop}] set A(size,final) [MakeSize $w3 $h3] } proc MakeSize {w h} { return [Comma $w]x[Comma $h] } proc Comma { num } { while {[regsub {^([-+]?[0-9]+)([0-9][0-9][0-9])} $num {\1,\2} num]} {} return $num } proc ToASCII {iname subsample resolution W} { global h yStart w xStart y yPixel x xPixel global imgWidth imgHeight xStep yStep xStop yStop global sumBrightness sb bb if {$subsample != 1} { image create photo ::img::tmp -file $iname image create photo ::img::ascii ::img::ascii copy ::img::tmp -subsample $subsample $subsample image delete ::img::tmp } else { image create photo ::img::ascii -file $iname } set imgWidth [image width ::img::ascii] set imgHeight [image height ::img::ascii] set xStep [dict get $::R $resolution] set xStop [expr {$imgWidth / $xStep}] set yStep [expr {$xStep * 2}] set yStop [expr {$imgHeight / $yStep}] wm title . "$::S(title) :: $iname $resolution" $W delete 0.0 end for {set h 0} {$h < $yStop} {incr h} { set yStart [expr {$h * $yStep}] set line "" for {set w 0} {$w < $xStop} {incr w} { set xStart [expr {$w * $xStep}] set sumBrightness 0 for {set y 0} {$y < $yStep} {incr y} { set yPixel [expr {$y + $yStart}] for {set x 0} {$x < $xStep} {incr x} { set xPixel [expr {$x + $xStart}] lassign [::img::ascii get $xPixel $yPixel] r g b set b [expr {max($r,$g,$b)}] set b2 [expr {$b * 100 / 255}] incr sumBrightness $b2 } } set sb [expr {double($sumBrightness)/$xStep/$yStep}] foreach {val char} $::asciiMap { if {$sb < $val} break } append line $char } append line "\n" #puts "$h/$yStop" $W insert end $line $W see end update } set ::A(drawn) $iname:$subsample:$resolution image delete ::img::ascii .t see 1.0 } proc Go {} { set sub [expr {$::A(subsample) eq "None" ? 1 : $::A(subsample)}] set id $::A(imgName):$sub:$::A(resolution) set toDraw [expr {$id ne $::A(drawn)}] if {$toDraw} { .t delete 0.0 end } font config myFont -size $::A(fontSize) if {$toDraw} { ToASCII $::A(imgName) $sub $::A(resolution) .t } } proc GetFile {} { set types { {"All Image Files" {.gif .jpg .png .tiff .bmp .ico .ppm}} {"GIF" .gif} {"JPEG" .jpg} {"PNG" .png} {"TIFF" .tiff} {"BMP" .bmp} {"ICO" .ico} {"PPM" .ppm} {"All files" *.*} } set fname [tk_getOpenFile -title "$::S(title) Get Image" -filetypes $types \ -typevariable ::A(filter) \ -initialdir [file dirname $::A(imgName)]] if {$fname ne ""} { set ::A(imgName) $fname GetThumb GetSize } } proc GetThumb {} { set n [catch {image create photo ::img::tmp -file $::A(imgName)}] if {$n} return set w [image width ::img::tmp] set h [image height ::img::tmp] set dr [expr {int(ceil(double($h) / $::S(thumbHeight)))}] image delete ::img::thumb image create photo ::img::thumb ::img::thumb copy ::img::tmp -subsample $dr $dr image delete ::img::tmp .right.thumb config -image ::img::thumb set ::A(width) $w set ::A(height) $h } DoDisplay # Demo with the teapot image from ActiveState distribution set teapotImg [file join $tk_library demos images teapot.ppm] if {[file exists $teapotImg]} { set ::A(imgName) $teapotImg set ::A(resolution) "UltraHigh" GetThumb GetSize Go } return