Image to ASCII Art: Tools, Code, and Character Maps
ASCII art converts an image into a grid of text characters, where each character represents a small block of pixels. Dark pixels become dense characters like @ and #. Light pixels become sparse ones like . and :. White areas become spaces. The result is a picture you can paste into a terminal, a GitHub README, a text email, or a code comment.
The technique dates back to the 1960s when printers had no graphics mode — text characters were the only output. That constraint produced an art form that persists because it is both technically interesting and visually striking. ASCII art works everywhere text works: terminals, plain-text email, source code, chat messages, and monospaced text files.
This article covers the full pipeline: how the conversion algorithm works, which character sets produce the best results, the CLI and Python tools that do it for you, and practical use cases beyond nostalgia.
Quick Reference: Image to ASCII Art Tools
| Tool | Type | Color Support | Batch | Output | Best For |
|---|---|---|---|---|---|
| Python Pillow (v10.3.0) | Script | Grayscale | Yes (scripted) | Text file / terminal | Full control, custom character maps |
| jp2a (v1.1.0) | CLI | ANSI color | Yes (glob) | Terminal / text file | Quick terminal previews |
| libcaca (v0.99.beta20) | Library/CLI | ANSI + HTML | Yes | Terminal / HTML / SVG | Color ASCII, HTML export |
| ASCII Art Studio (v2.2.1) | GUI (Windows) | Grayscale | No | Text file | Manual editing, fine-tuning |
| Online converters | Web | Varies | No | Text | No install, one-off use |
Before converting, get your source image into good shape. Resize to a smaller dimension (80-120px wide works well for terminal output), and make sure you are starting from a clean, well-contrasted image. A blurry or washed-out photo produces muddy ASCII art regardless of which tool you use.
Try it yourself
Convert between any image format instantly — free, instant, no signup. Your images never leave your browser.
How Image-to-ASCII Conversion Works
The algorithm is straightforward. Every image-to-ASCII converter follows the same four steps, whether it is a 10-line Python script or a compiled C library.
Step 1: Resize
ASCII characters in a monospaced font are taller than they are wide — roughly a 2:1 height-to-width ratio. If you map one pixel to one character without correcting for this, the output looks vertically stretched. The fix: resize the image so the width matches your target column count (say, 100 characters) and the height is scaled by approximately 0.55 to compensate for character aspect ratio.
Use Pixotter's resize tool to get the image to the right dimensions before feeding it into a converter, or let the script handle it programmatically.
Step 2: Convert to Grayscale
Each pixel needs a single brightness value, not three color channels. The standard luminosity formula from grayscale conversion applies:
brightness = 0.299 × R + 0.587 × G + 0.114 × B
This BT.601 weighting matches human perception — green contributes the most because the eye is most sensitive to it. The result is a value from 0 (black) to 255 (white) for every pixel.
Step 3: Map Brightness to Characters
This is where the art happens. You define a string of characters ordered from darkest (most ink) to lightest (least ink). Each pixel's brightness value selects a character from this string.
A simple 10-character ramp:
@%#*+=-:.
@ is the densest (darkest), then %, then #, down to . and finally a space for pure white. The pixel brightness (0-255) is divided into equal-sized bins. A pixel with brightness 25 maps to @. A pixel with brightness 230 maps to .. A pixel with brightness 255 maps to a space.
For more detail, use an extended ramp with finer gradations:
$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,"^`'.
This 70-character ramp (credited to Paul Bourke) provides smoother tonal transitions. The tradeoff: more characters means more visual noise at small sizes, where a simpler ramp reads more clearly.
Step 4: Assemble the Output
Write the characters row by row, inserting a newline at the end of each row. The output is plain text — you can print it, save it to a .txt file, or embed it in HTML inside a <pre> block with a monospaced font.
Python Pillow: Build Your Own Converter (v10.3.0)
Here is a complete Python script that converts any image to ASCII art. It uses Pillow v10.3.0 for image loading and manipulation — no other dependencies.
# ascii_art.py
# Requires: Pillow==10.3.0
# Install: pip install Pillow==10.3.0
from PIL import Image
import sys
# Character ramps — ordered from darkest to lightest
SIMPLE_RAMP = "@%#*+=-:. "
DETAILED_RAMP = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
def image_to_ascii(
image_path: str,
width: int = 100,
ramp: str = SIMPLE_RAMP,
invert: bool = False,
) -> str:
"""Convert an image file to an ASCII art string."""
img = Image.open(image_path)
# Calculate height with aspect ratio correction
# Monospaced characters are ~2x taller than wide
aspect_ratio = img.height / img.width
height = int(width * aspect_ratio * 0.55)
# Resize and convert to grayscale
img = img.resize((width, height))
img = img.convert("L") # Luminosity (BT.601 weighted)
pixels = list(img.getdata())
ramp_len = len(ramp)
if invert:
ramp = ramp[::-1]
# Map each pixel to a character
chars = []
for i, pixel in enumerate(pixels):
# Map 0-255 brightness to character index
index = pixel * (ramp_len - 1) // 255
chars.append(ramp[index])
if (i + 1) % width == 0:
chars.append("\n")
return "".join(chars)
def main():
if len(sys.argv) < 2:
print("Usage: python ascii_art.py <image_path> [width] [detail]")
print(" width: character columns (default: 100)")
print(" detail: 'simple' or 'detailed' (default: simple)")
sys.exit(1)
image_path = sys.argv[1]
width = int(sys.argv[2]) if len(sys.argv) > 2 else 100
ramp_name = sys.argv[3] if len(sys.argv) > 3 else "simple"
ramp = DETAILED_RAMP if ramp_name == "detailed" else SIMPLE_RAMP
result = image_to_ascii(image_path, width=width, ramp=ramp)
print(result)
if __name__ == "__main__":
main()
Running the Script
# Basic conversion — 100 columns, simple character ramp
python ascii_art.py photo.jpg
# Wider output with detailed character ramp
python ascii_art.py photo.jpg 150 detailed
# Save to a text file
python ascii_art.py photo.jpg 120 simple > portrait.txt
Tuning the Output
Width controls detail. At 80 columns, you get a compact image suitable for a terminal window. At 200 columns, you get fine detail but need a wide viewport or small font to see the full picture.
Character ramp choice depends on the image. The simple 10-character ramp works best for high-contrast images — logos, silhouettes, and graphics with bold shapes. The detailed 70-character ramp handles photographs better because it preserves subtle tonal gradations in skin, fabric, and sky.
Invert reverses the mapping — useful when displaying light text on a dark terminal background. Without inversion, dark pixels become dense characters (dark on white background). With inversion, dark pixels become sparse characters (light on dark background), which looks more natural in a terminal with a dark color scheme.
Font matters. ASCII art only looks correct in a monospaced font. If you paste it into a proportional-font editor, the alignment breaks. Use Courier New, Consolas, JetBrains Mono, or any terminal font.
jp2a: Command-Line ASCII Art (v1.1.0)
jp2a is a dedicated CLI tool that converts JPEG (and with --output, other formats) to ASCII art directly in your terminal. It is fast, well-maintained, and handles the resize/grayscale/mapping pipeline in a single command.
Installation
# Debian/Ubuntu
sudo apt install jp2a
# macOS (Homebrew)
brew install jp2a
# Verify version
jp2a --version
# jp2a 1.1.0
Basic Usage
# Convert a JPEG, auto-fitted to terminal width
jp2a photo.jpg
# Set specific width (characters)
jp2a --width=120 photo.jpg
# Use a custom character ramp (darkest to lightest)
jp2a --chars=" .:-=+*#%@" photo.jpg
# Invert for dark terminal backgrounds
jp2a --invert photo.jpg
# ANSI color output (terminal only)
jp2a --colors photo.jpg
# Save to a text file instead of printing
jp2a --width=100 --output=art.txt photo.jpg
# Convert a PNG by piping through ImageMagick
convert input.png jpg:- | jp2a --width=100 -
Batch Conversion
# Convert all JPEGs in a directory
for f in images/*.jpg; do
jp2a --width=100 --output="ascii/$(basename "$f" .jpg).txt" "$f"
done
jp2a reads JPEG natively. For PNG, WebP, or other formats, convert the image to JPEG first, or pipe through ImageMagick as shown above.
libcaca: Color ASCII Art (v0.99.beta20)
libcaca goes beyond monochrome. It renders images using ANSI color codes, producing colored ASCII art in the terminal. It can also export to HTML, SVG, and IRC-formatted text.
Installation
# Debian/Ubuntu
sudo apt install caca-utils
# macOS (Homebrew)
brew install libcaca
# Verify
cacaview --version
# libcaca 0.99.beta20
Usage
# Interactive viewer — opens image as color ASCII in terminal
cacaview photo.jpg
# Export to HTML (opens in any browser)
img2txt -f html -W 120 photo.jpg > art.html
# Export to ANSI text
img2txt -f utf8 -W 100 photo.jpg > art.txt
# Export to SVG
img2txt -f svg -W 100 photo.jpg > art.svg
The HTML export is particularly useful — it produces a self-contained HTML file with inline CSS that displays the colored ASCII art in any browser. You can embed this in a web page or share it directly.
When to Use libcaca vs jp2a
jp2a is better for monochrome terminal art — it is simpler, faster, and produces cleaner output with a plain character ramp. libcaca wins when you want color or need to export to HTML/SVG. The two tools serve different purposes and complement each other.
Character Ramp Design
The character ramp is the soul of ASCII art. Choosing the right set of characters — and ordering them correctly by visual density — makes the difference between a recognizable portrait and an unreadable mess.
Density, Not Shape
What matters is how much ink (or lit pixels) each character puts on screen, not what the character looks like. @ is dense because it fills most of its bounding box. . is sparse because it is a single dot in the lower-left corner. A space is the sparsest — zero ink.
Pre-Built Ramps
| Name | Characters | Levels | Best For |
|---|---|---|---|
| Minimal | @#. |
4 | Logos, silhouettes, high-contrast art |
| Simple | @%#*+=-:. |
10 | General-purpose, readable at small sizes |
| Standard | @%#*+=-:. with &WMoahk additions |
20 | Balanced detail and readability |
| Detailed (Bourke) | $@B%8&WM#*oahkbd... (70 chars) |
70 | Photographs, gradients, subtle tones |
| Block | █▓▒░ |
5 | Retro terminal look, Unicode required |
Building a Custom Ramp
If the pre-built ramps do not suit your aesthetic, build your own:
- Pick 8-15 characters that span a range of visual densities.
- Render each character in your target monospaced font at the target size.
- Count the dark pixels in each character's bounding box (or estimate visually).
- Sort from most dark pixels to fewest.
For example, in Consolas 12pt, the character W is denser than M because of its wider strokes. In Courier New, the reverse might be true. The ramp should match the font the viewer will use.
Use Cases for ASCII Art
Terminal Art and MOTD Banners
System administrators have used ASCII art in /etc/motd (Message of the Day) and login banners for decades. A company logo rendered in ASCII art greets every SSH session. Generate it once with jp2a, save the output, and paste it into your MOTD file.
GitHub README Decoration
ASCII art in a README renders correctly on every device because GitHub uses a monospaced font for code blocks. Drop your logo or mascot into a fenced code block:
```
.---.
/ \
| () () |
\ ^ /
'---'
pixotter
```
For more complex images, generate the art at 60-80 columns wide so it fits within typical code-block viewports without horizontal scrolling.
Text-Only Email Signatures
Some corporate environments strip HTML from emails, leaving only plain text. An ASCII art logo in your signature survives every email client, every forwarding chain, and every print-to-PDF conversion. Keep it small — 5-8 lines, 40-60 characters wide.
Retro Aesthetics and Creative Projects
ASCII art evokes the look of early computing — green phosphor terminals, BBS culture, demoscene demos. Use it in game splash screens, loading animations, poster designs, or social media posts for a distinctive retro feel.
Accessibility Text Fallback
When an image cannot be displayed (broken link, text-only browser, screen reader in verbose mode), an ASCII art fallback preserves some visual meaning. This is a niche case, but it demonstrates that the technique has practical utility beyond decoration. For details on how image resolution affects visual quality in both pixel and text representations, see our guide on image resolution.
Advanced: Block-Averaging for Larger Images
The basic algorithm maps one pixel to one character. For large images, this produces output that is thousands of characters wide — useless without extreme zoom. The solution: average blocks of pixels.
Instead of resizing the image to the target width and mapping pixel-by-pixel, divide the original image into rectangular blocks (e.g., 8x16 pixels per block, matching the aspect ratio of a monospaced character). Calculate the average brightness of each block, then map that average to a character.
# Block-averaging extension for the Python script above
# Add to ascii_art.py — requires Pillow==10.3.0
def image_to_ascii_blocks(
image_path: str,
cols: int = 100,
ramp: str = SIMPLE_RAMP,
block_w: int = 8,
block_h: int = 16,
) -> str:
"""Convert using block averaging — better for large/detailed images."""
img = Image.open(image_path).convert("L")
w, h = img.size
rows = int((h / block_h) * (cols * block_w / w))
pixels = list(img.getdata())
chars = []
for row in range(rows):
for col in range(cols):
# Calculate source block boundaries
x_start = int(col * w / cols)
x_end = int((col + 1) * w / cols)
y_start = int(row * h / rows)
y_end = int((row + 1) * h / rows)
# Average all pixels in the block
total = 0
count = 0
for y in range(y_start, min(y_end, h)):
for x in range(x_start, min(x_end, w)):
total += pixels[y * w + x]
count += 1
avg = total // max(count, 1)
index = avg * (len(ramp) - 1) // 255
chars.append(ramp[index])
chars.append("\n")
return "".join(chars)
Block averaging preserves more detail from high-resolution source images because it considers every pixel in the block rather than downsampling first and discarding information. The tradeoff is speed — it is significantly slower than resize-then-map for very large images.
Tips for Better ASCII Art Output
Start with high contrast. The best source images for ASCII conversion have strong edges and clear tonal separation. Portraits with dramatic lighting, logos with solid fills, and line drawings all convert well. Flat, evenly-lit photographs with subtle detail tend to produce muddy results.
Resize before converting. Giving the converter a pre-sized image (via Pixotter's resize tool or Pillow) means you control the downsampling quality. Lanczos resampling preserves edges better than the bilinear default in many CLI tools.
Match the ramp to the background. On a white background (paper, light terminal), use a standard dark-to-light ramp. On a dark background (most terminals), invert the ramp so dense characters represent bright areas. Every tool in this article has an --invert flag or equivalent.
Test at actual display size. Zoom out until the ASCII art fills the same space on screen that the original image would. Up close, you see characters. From the right distance, you see the image. The "right distance" varies with font size and column count.
Crop and convert to a clean format first. Remove unnecessary background from the source image. A subject surrounded by white space wastes characters on empty space. Crop tight, convert to JPEG or PNG, then feed it to the converter.
Coloring Book and Line Art Connections
ASCII art is one of several ways to transform a photograph into a stylized, reduced representation. If you are interested in related techniques, the concepts overlap with converting photos to line drawings (edge detection instead of brightness mapping) and creating coloring pages from images (threshold and outline extraction). For a related grid-based art form, see our guide on converting images to pixel art — both ASCII art and pixel art reduce an image to a constrained grid, but pixel art uses colored blocks instead of characters. All three start with the same step: understanding and manipulating pixel brightness, which is why grayscale conversion shows up in every pipeline.
FAQ
What is the best character set for image-to-ASCII-art conversion?
For most images, the 10-character simple ramp (@%#*+=-:. ) produces the cleanest results. It has enough tonal levels to represent photographs while remaining readable at small sizes. Use the 70-character Bourke ramp only when your output is wide enough (150+ columns) to benefit from the extra gradations — at smaller sizes, the additional characters add visual noise rather than detail.
How wide should my ASCII art output be?
Match the output width to where you plan to display it. Terminal: 80-120 characters (fits a standard terminal window). GitHub README: 60-80 characters (avoids horizontal scrolling in code blocks). Email signature: 40-60 characters. Poster or print: 200+ characters at a small font size. Wider output preserves more detail but requires a wider viewport.
Can I generate colored ASCII art?
Yes. libcaca (v0.99.beta20) renders color ASCII art using ANSI escape codes for terminal output and inline CSS for HTML export. jp2a (v1.1.0) supports ANSI color with the --colors flag. The Python script in this article outputs grayscale only, but you can extend it by reading the original RGB values and wrapping each character in an ANSI color code: \033[38;2;R;G;Bm for 24-bit true color.
Why does my ASCII art look stretched vertically?
Monospaced font characters are taller than they are wide — typically about a 2:1 ratio. If your converter maps one pixel to one character without correcting for this aspect ratio, the output appears stretched. The fix is to scale the height by approximately 0.55 during the resize step. Every tool and script in this article handles this correction automatically.
Can I convert a PNG or WebP image, not just JPEG?
jp2a natively reads JPEG only, but you can pipe other formats through ImageMagick (convert input.png jpg:- | jp2a -). The Python Pillow script and libcaca both accept PNG, WebP, BMP, TIFF, GIF, and most common image formats directly. If you need to change the format beforehand, Pixotter handles that in your browser with no upload required.
How do I make ASCII art look good on a dark terminal background?
Invert the character ramp. By default, dense characters like @ represent dark pixels — which looks correct on a white background (dark ink on light paper). On a dark terminal, you want dense characters to represent bright pixels instead. Use --invert with jp2a, reverse the ramp string in Python (ramp[::-1]), or use libcaca's built-in dark-background handling.
Is there a way to generate ASCII art in HTML for a webpage?
libcaca's img2txt -f html command produces a self-contained HTML file with colored ASCII art rendered in a <pre> block. For monochrome art, generate plain text with any tool and wrap it in <pre style="font-family: monospace; line-height: 1.2;"> in your HTML. Set line-height to around 1.0-1.2 to prevent excessive vertical spacing between rows.
What image types work best as ASCII art source material?
High-contrast images with clear edges produce the best results: portraits with dramatic lighting, logos, silhouettes, simple illustrations, and bold graphic designs. Busy photographs with fine detail and low contrast (e.g., a foggy landscape or a flat product shot) tend to lose their subject in the conversion. Pre-processing helps — boost contrast and sharpen edges before converting. Starting from a line drawing also produces cleaner ASCII output than starting from a full-color photograph.
Try it yourself
Ready to convert formats? Drop your image and get results in seconds — free, instant, no signup. Your images never leave your browser.