Image to Unicode Quadrant ANSI: Principles, Implementation and a Browser Tool
Image to Unicode Quadrant ANSI: Principles, Implementation and a Browser Tool
Search
Ask the AI

Image to Unicode Quadrant ANSI: Principles, Implementation and a Browser Tool

I already had a truecolor half-block version: scale the image down to the terminal width, then use the upper and lower half-block characters to store the colours of two pixel rows. That approach is stable and widely compatible, but each character can only express two sample points. To make an image finer in the command line, the character cell has to be split further, which means moving to Unicode quadrant block characters.

This article explains how the new converter works. The entry point is on the Tools page: drop an image in, generate a preview, download the .ans file, then run cat filename.ans in a terminal that supports 24-bit truecolor.

1. Why four pixels cannot simply be packed into one character

An ANSI terminal cell normally carries one foreground colour and one background colour at a time. A Unicode quadrant character decides which quadrants of that cell show the foreground and which show the background, but it cannot give the upper-left, upper-right, lower-left and lower-right quadrants four independent colours.

So “quadrant full colour” works under a real constraint: every 2 x 2 pixel block can be compressed into at most two colours. The converter’s job is not to pretend nothing is lost, but to find the two-colour representation with the smallest error under that constraint.

2. How a quadrant character encodes 2 x 2 pixels

A character cell is treated as four positions: upper-left, upper-right, lower-left and lower-right. Each position belongs either to the foreground or to the background, which gives 16 combinations in total. The common characters include the space, upper half, lower half, left half, right half, diagonal blocks and the full block, for example ▘ ▝ ▀ ▖ ▌ ▞ ▛ ▗ ▚ ▐ ▜ ▄ ▙ ▟ █.

The converter first scales the source image onto a sampling grid sized for the target terminal width. If the output width is 180 terminal characters, the sampling grid is 360 pixels wide; every two horizontal and two vertical sample points combine into one terminal character. The height is computed automatically from the terminal font ratio, treating a terminal character as roughly 2:1 in height to width by default.

3. Choosing the foreground colour, background colour and character

For each 2 x 2 pixel block the converter enumerates all 16 quadrant masks. A given mask splits the four pixels into two groups: a foreground group and a background group. The foreground group takes its mean RGB as the foreground colour, the background group takes its mean RGB as the background colour, and the squared error between the four original pixels and the two representative colours is then computed.

The combination with the smallest error wins:

  1. Enumerate the 16 quadrant masks.
  2. Split the four sampled pixels into a foreground group and a background group according to the mask.
  3. Compute the mean RGB of each group.
  4. Compute the reconstruction error across the four pixels.
  5. Emit the Unicode character and the ANSI truecolor foreground/background pair with the smallest error.

This adds horizontal shape choices, not a guarantee of better appearance or a larger file for every image. Byte count also depends on repeated colours, UTF-8 glyph length and resets. The numerical comparison below fixes the input grid.

4. What is inside the generated .ans file

A .ans file is ordinary UTF-8 text that happens to contain ANSI escape sequences. Before each character the converter writes a control code such as ESC[38;2;R;G;B;48;2;R;G;Bm when needed, setting the foreground and background colours, and then writes one quadrant Unicode character. At the end of each line ESC[0m resets the style so the background colour does not bleed into the following shell prompt.

Confirm 24-bit colour and the required block glyphs in your actual terminal configuration before using this command. The terminal or operating-system name alone is not a compatibility test:

cat image-quadrant-180x120-truecolor.ans

If the image is too wide, the terminal’s automatic line wrapping will break the picture. Either widen the terminal window or reduce the output width during conversion.

5. Why the tool page does not upload images to the server

Image conversion does not need a server. Browsers already decode the common image formats and can read scaled pixels through a Canvas. Running the conversion in the front end has three benefits:

  • Privacy is simpler: the image never leaves the user’s device.
  • Storage pressure is lower: the server stores no uploads and needs no scheduled cleanup of source images.
  • Feedback is faster: after dropping an image in you can preview it, adjust the width and regenerate immediately.

A successful conversion schedules cleanup after approximately 30 minutes, revoking the Blob link and clearing preview references. Background-tab timers may run late; this is not secure erasure. Downloaded files remain. The converter does not upload images, but the site may still make logging, advertising or other network requests.

6. Boundaries in the implementation

First, the browser can only handle the formats it can decode itself. PNG, JPEG, WebP, GIF, AVIF and SVG are usually fine, but HEIC, corrupted files and SVGs that reference external resources can fail.

Second, quadrant characters do not mean four times the resolution without loss. Each character still carries only a foreground and a background colour; complex texture, noise and high-frequency detail are compressed into two representative colours.

Third, the terminal font affects the result. A good font makes block elements sit flush against each other with no visible seams; if you see grid lines or uneven character widths, switch to a different monospace font or adjust the terminal line spacing.

7. Why this version suits detailed images better than half-block

The strength of half-block is that each character stores the colour of two stacked rows, which keeps colour reproduction stable. The strength of the quadrant version is that structure can appear in the horizontal direction within a single character, so hair, outlines, small highlights and diagonal edges come out clearer. For anime portraits, icons and illustrations with defined edges, quadrants usually preserve more shape information at the same terminal width.

If the goal is maximum compatibility, half-block is still a good choice. If the goal is as much detail as possible in a modern terminal with Unicode and truecolor support, quadrants plus minimum-error two-colour clustering is the better fit.

8. ANSI conversion strategies compared

Strategy Information per character Suited to Main limitation
Plain ASCII Approximates brightness through character density only, with no real colour. High-contrast black and white images, and cases with very strict terminal compatibility needs. Loses the most colour and fine edge detail.
Half-block truecolor One character stores two stacked sample points, each with its own colour. Photographs, gradients, and colour images that need broad compatibility. Weak horizontal structure; diagonals and small outlines thicken easily.
Quadrant truecolor One character covers 2 x 2 sample points, approximated by two minimum-error colours. Portraits, icons, illustrations and images with defined outlines. Each cell still has only a foreground and a background colour, not four independent quadrant colours.
Full image output Keeps the original pixels and lets an image viewer render them. Cases needing exact fidelity, printing or later processing. Not generic character-and-SGR rendering; depends on an image viewer or terminal graphics protocol.

9. Terminal compatibility checklist

After generating a .ans file, do not draw conclusions from the browser preview alone. The browser uses a Canvas and the page font, while a real terminal is also affected by its font, line height, colour configuration and line wrapping. A test record you can re-check should record at least the terminal name, font, font size, output column width, the composite background used for the image, and the exact command run.

Check Passing condition First thing to adjust on failure
Truecolor support Gradients and photographic colour areas have not degraded into a 256-colour approximation. Check actual colour patches and configuration; COLORTERM is a hint, not a way to enable missing capability.
Unicode block elements Quadrant characters render with no missing glyphs and no visible seams. Switch to a different monospace font, reduce line spacing, or fall back to half-block.
Output width A single line of ANSI content is not wrapped automatically by the terminal. Lower the conversion width, or widen the terminal window and test again.
Transparent background Transparent areas sit close to the terminal background and produce no odd border. Pick a composite colour close to the theme background and export again.

10. Deriving the sampling dimensions

Let a terminal cell have width w and height h, with a=h/w. Each quadrant measures w/2 by h/2, so its aspect ratio is still a, not 2a. At a=2, a quadrant is twice as tall as it is wide, not four times.

The rendered image aspect is (rows/cols) × a. Matching src_h/src_w gives:

CELL_RATIO = 2.0
cols = target_cols
src_w, src_h = img.size
rows = max(1, int(cols * src_h / (src_w * CELL_RATIO) + 0.5))
img = img.resize((cols * 2, rows * 2))

An 800×600 image at 80 columns and a=2 gives 80×30 character cells and a 160×60 sample grid. Display height/width is 30×2/80=0.75. Omitting division by a stretches it vertically by two. Keeping at least one row prevents an ultra-wide panorama from rounding to zero height, but extreme shapes remain limited by integer cells. The web tool also caps output at 260 rows and 62000 cells, reducing requested columns when necessary.

Calibrate the actual terminal/font with a known square grid. The default 2.0 is not a measured constant for every terminal.

11. Two counterexamples in 256-colour quantisation

Both the web tool and this offline converter output truecolor: neither implements automatic 256-colour fallback. Unsupported 24-bit SGR may be ignored, mapped to a palette or rendered incorrectly, depending on the receiver. COLORTERM and TERM are hints; setting them cannot add missing capability.

This separate exercise chooses the minimum encoded-RGB squared error over default xterm entries 16–255. It excludes theme-defined system colours 0–15; redefining other entries also invalidates the default table.

LEVELS = (0, 95, 135, 175, 215, 255)
PALETTE = {
    16 + 36*r + 6*g + b: (LEVELS[r], LEVELS[g], LEVELS[b])
    for r in range(6) for g in range(6) for b in range(6)
}
PALETTE.update({232+i: (8+10*i,) * 3 for i in range(24)})

def rgb_to_256(r, g, b):
    rgb = (r, g, b)
    return min(PALETTE, key=lambda i:
               sum((a-b)**2 for a, b in zip(rgb, PALETTE[i])))

The old grey formula mapped (17,17,17) to index 232, or (8,8,8), with SSE=243. The nearest entry is 233, or (18,18,18), with SSE=3. Grey (95,95,95) exactly matches cube index 59: being grey does not justify excluding the cube. Across all 256 grey inputs, the new search strictly improves 67 and ties the remaining errors.

Another correction: v*5//256 at v=255 gives 4, not 6. It cannot reach bin 5; it does not overflow to 6. Default indices for pure red, green, blue, white and black are 196,46,21,231,16.

12. A reproducible four-pixel experiment

Pixel order is upper-left, upper-right, lower-left, lower-right: bits 0,1,2,3. The web implementation tries masks 0–15 and sums encoded-RGB squared error across four pixels and three channels. Group means use JavaScript Math.round, not Python’s ties-to-even round. This is neither linear-light error nor a perceptual colour difference. Equal costs keep the lower mask; complementary masks swap foreground/background without changing the reconstruction.

2×2 input, row order Selected mask / quadrant SSE Horizontal half SSE
All red 0, space with red background / 0 0
Red blue / red blue 5, ▌ / 0 130052
Red red / blue blue 3, ▀ / 0 0
Red blue / blue red 6, ▞ with blue foreground / 0 130052
Red green / blue white 1, ▘ / 130050 130052
Quadrant and horizontal-half reconstruction of the same four input pixels
Directly computed RGB reconstructions, not a terminal screenshot. Primary colours have one channel at 255 and the others at 0; white is (255,255,255). Each cell’s SSE sums all 12 channel values.

The horizontal-half comparator is mask=3 on the same 2×2 grid, not a claim about image-resampler quality. Since it is one of the 16 candidates, the minimum cannot have higher error. This does not guarantee better font rendering. Four distinct colours still leave a residual of 130050, exposing the two-colour limit. A solid patch represented by a space is not missing: the coloured background fills the cell.

Download and reproduce offline

Download the quadrant experiment package: converter, checks, reference colours and .ans samples, a web-source snapshot and execution records.

python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.txt
python ansi_lab.py input.png output.ans --columns 80 --aspect 2 --background '#050505'
python audit_ansi.py --output my-audit

Conversion needs Python and Pillow; full parity checks also require Node.js, selectable with –node. Existing output is protected unless –force is supplied, and input/output must differ. Successful verification prints ANSI_AUDIT_PASSED. Do not use Python -O, which disables assertions.

The reference uses Python 3.13.9 and Pillow 12.2.0. It matches the current web JavaScript core on 528 pixel cases and 104 dimensions. Thirteen additional cell cases independently enumerate channel centres 0–255 for each group to verify the minimum. Checks also cover complementary masks, all grey inputs, transparency, EXIF orientation, invalid files, overwrite protection and per-line resets.

reference/audit.json records source hashes and results. Eight solid-black cells over two lines need only two colour-setting sequences, not eight: cell count alone does not determine bytes. Only the allowed SGR, block glyphs and newlines are emitted by this implementation; that is not a safety claim about unknown .ans files.

Pillow decoding and Lanczos resampling differ from browser Canvas, so the same source image is not promised to produce byte-identical files. Parity covers identical sampled pixels, not real terminal fonts, browser resampling or all image formats. The offline version uses only the static first frame and includes no SVG/HEIC decoder or indexed-colour file output.

References: XTerm control sequences, Unicode Block Elements, and Pillow Image for decoder boundaries. Do not disable decompression-bomb protection to hide a failed input.

13. Practical suggestions

  • Use the actual terminal width, allowing room for borders or multiplexers.
  • If the terminal wraps lines, reduce the width rather than pushing for more detail.
  • For transparent PNGs, choose a composite colour close to the terminal background.
  • Complex photographs produce very large ANSI files; illustrations, icons and character portraits usually work better.

The full entry point is on the Tools page. The converter itself needs no account and no backend file storage; it simply puts image sampling, two-colour approximation and ANSI output into a single browser interface.

The quadrant-mask search and truecolor encoding described in this article are available as a ready-to-use web tool: Image to ANSI Art. Drop in an image to generate it; the conversion runs entirely in your own browser and the file is never uploaded to a server.

Leave a Reply

Scroll down