ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
Character Palettes

🎨 Character palettes for video-to-ASCII conversion More...

Files

file  palette.c
 🎨 Terminal color palette management with Unicode character width detection
 

Detailed Description

🎨 Character palettes for video-to-ASCII conversion

Palette README

Overview

Welcome to the Palette Management module! This is where we turn video frames into beautiful ASCII art. The palette system is all about choosing the right characters to represent different brightness levels—from dark spaces to light block characters, we've got options for every terminal and every aesthetic.

What does the palette system do?

The Palette Management module provides comprehensive ASCII character palette management for converting video frames to ASCII art in ascii-chat. Here's what it gives you:

  • Five built-in palette types (standard, blocks, digital, minimal, cool) so you can choose the style that fits your terminal
  • Custom user-defined palettes via command-line options (if you want something unique, you can create your own)
  • UTF-8 character support with proper encoding handling (for terminals that support Unicode)
  • Automatic UTF-8 capability detection and compatibility checking (we detect what your terminal supports and pick the right palette automatically)
  • Palette validation to ensure proper character ordering (so your custom palettes work correctly)
  • Luminance mapping for optimal brightness-to-character conversion (so dark pixels become dark characters and bright pixels become bright characters)
  • Client compatibility checking and automatic fallback (if your terminal doesn't support UTF-8, we fall back to ASCII automatically)

Implementation: lib/palette.h

Built-in Palettes

ascii-chat provides five built-in palette types, each optimized for different visual styles and terminal capabilities. Think of them as different "art styles" for your ASCII art—some are classic and simple, others are modern and smooth. Let's explore what each one offers:

Standard Palette

The Standard palette is the classic ASCII art style—if you've ever seen ASCII art in movies or old-school terminal demos, this is probably what they were using. It's simple, reliable, and works everywhere.

Characters: ‘" ...’,;:clodxkO0KXNWM"`

This palette uses traditional ASCII characters ordered from darkest (spaces) to lightest (capital letters) for optimal brightness mapping. It's the "tried and true" option—nothing fancy, but it works great.

What makes it special?

  • Pure ASCII: No UTF-8 required, so it works in any terminal, even ancient ones
  • 23 characters total: A good balance between detail and simplicity
  • Wide compatibility: Works in all terminals, even terminals that don't support UTF-8
  • Classic ASCII art aesthetic: The traditional look that everyone recognizes

This is probably what you want if you're not sure which palette to use—it's the safe default that works everywhere.

Blocks Palette

The Blocks palette is our high-quality option for terminals that support UTF-8. It uses Unicode block characters to create smooth gradients that look almost like real video—it's really impressive what you can do with block characters!

Characters: " ░░▒▒▓▓██"

This palette uses Unicode half-block and full-block characters to create a smooth gradient effect. The blocks range from light to dark, creating a continuous gradient that looks much smoother than ASCII characters.

What makes it special?

  • Requires UTF-8 support: Your terminal needs to support UTF-8 for this to work, but most modern terminals do
  • 10 characters total: Just spaces and 4 block pairs (light half-block, dark half-block, light full-block, dark full-block)
  • Smooth gradient effect: The blocks create a much smoother gradient than ASCII characters
  • Best visual quality for UTF-8 terminals: If your terminal supports UTF-8, this is probably the best-looking option

This is what you want if you have a modern terminal and want the best visual quality!

Digital Palette

The Digital palette has a unique glitch/digital aesthetic—think "Matrix" meets "cyberpunk terminal". It uses mathematical and geometric Unicode characters to create a technical, modern look.

Characters: " -=≡≣▰▱◼"

This palette combines mathematical symbols (equals, equivalent) with geometric shapes (squares, blocks) to create a unique visual style. It's not for everyone, but if you like the "cyberpunk hacker" aesthetic, this is for you!

What makes it special?

  • Requires UTF-8 support: Needs UTF-8 for the special characters
  • 10 characters total: A mix of mathematical and geometric symbols
  • Unique glitch/digital aesthetic: Very distinctive look
  • Technical appearance: Looks like something from a sci-fi movie

This is for when you want something unique and technical-looking.

Minimal Palette

The Minimal palette is for when you want the absolute simplest possible rendering. It uses just seven characters (spaces, dots, dashes, plus signs, asterisks, and hashes), so it's fast, simple, and works everywhere.

Characters: " .-+*#"

This is the "less is more" palette—maximum simplicity, maximum compatibility. If you're running on an ancient terminal or want the absolute fastest rendering, this is what you want.

What makes it special?

  • Pure ASCII: No UTF-8 required, works everywhere
  • 7 characters total: Just the basics—spaces, dots, dashes, plus, asterisk, hash
  • Maximum simplicity: As simple as it gets while still being usable
  • Fast rendering: Fewer characters means faster conversion

This is for minimalism lovers and performance purists.

Cool Palette

The Cool palette is another UTF-8 option that uses ascending block characters to create a smooth gradient. It's similar to the Blocks palette, but uses a different set of characters that create a slightly different look.

Characters: " ▁▂▃▄▅▆▇█"

This palette uses Unicode ascending block characters (think "stair steps going up") to create a visually appealing gradient from dark to light. It's smooth, modern, and looks great in UTF-8 terminals.

What makes it special?

  • Requires UTF-8 support: Needs UTF-8 for the ascending block characters
  • 10 characters total: Spaces plus 7 ascending block characters
  • Smooth gradient effect: Creates a nice smooth transition from dark to light
  • Modern visual style: Clean, modern look

This is another great option if you have UTF-8 support and want smooth gradients!

UTF-8 Support

UTF-8 Capability Detection

The system automatically detects client UTF-8 support using multiple methods:

Detection Methods:

  • $TERM environment variable analysis
  • Locale encoding detection (checks LC_ALL, LC_CTYPE, LANG)
  • Terminal type compatibility checking
  • User override via --utf8 flag

Detection Logic:

utf8_capabilities_t caps;
if (caps.utf8_support) {
// Client supports UTF-8, can use UTF-8 palettes
palette = select_compatible_palette(PALETTE_BLOCKS, true);
} else {
// Client doesn't support UTF-8, use ASCII palette
palette = select_compatible_palette(PALETTE_STANDARD, false);
}
}
bool detect_client_utf8_support(utf8_capabilities_t *caps)
Definition palette.c:148
palette_type_t select_compatible_palette(palette_type_t requested, bool client_utf8)
Definition palette.c:219

UTF-8 Character Encoding

UTF-8 palettes properly handle multi-byte characters:

Character Information:

  • Byte Sequence: Up to 4 bytes per character
  • Byte Length: 1-4 bytes depending on character
  • Display Width: 1-2 character cells (for full-width characters)

UTF-8 Palette Structure:

utf8_palette_t *palette = utf8_palette_create(" ░░▒▒▓▓██");
size_t char_count = utf8_palette_get_char_count(palette); // 10 characters
// Get character at index
const utf8_char_info_t *char_info = utf8_palette_get_char(palette, 3);
// char_info->bytes = "░"
// char_info->byte_len = 3
// char_info->display_width = 1
void utf8_palette_destroy(utf8_palette_t *palette)
Definition palette.c:485
size_t utf8_palette_get_char_count(const utf8_palette_t *palette)
Definition palette.c:502
const utf8_char_info_t * utf8_palette_get_char(const utf8_palette_t *palette, size_t index)
Definition palette.c:494
utf8_palette_t * utf8_palette_create(const char *palette_string)
Definition palette.c:363

Important Notes:

  • Character count (not byte count) is used for palette indexing
  • Display width ensures proper terminal rendering
  • Multi-byte characters are handled transparently

Compatibility Checking

The system automatically selects compatible palettes based on client capabilities:

// Request UTF-8 palette
palette_type_t requested = PALETTE_BLOCKS;
// Check client UTF-8 support
utf8_capabilities_t caps;
// Select compatible palette
palette_type_t compatible = select_compatible_palette(
requested,
caps.utf8_support
);
// compatible will be PALETTE_BLOCKS if UTF-8 supported,
// otherwise falls back to PALETTE_STANDARD

Automatic Fallback:

  • UTF-8 palettes (BLOCKS, DIGITAL, COOL) require UTF-8 support
  • If client doesn't support UTF-8, automatically falls back to STANDARD
  • ASCII palettes (STANDARD, MINIMAL) always work
  • Custom palettes are validated for compatibility

Custom Palettes

Defining Custom Palettes

Users can define custom palettes via command-line options:

Command-Line Usage:

# Use custom palette characters
asciichat-client --palette-chars " .:;+=xX$&"
# Or specify custom palette in config file
palette_type = "custom"
palette_chars = " ░▒▓█"

Programmatic Usage:

// Apply custom palette
const char *custom_chars = " .:;+=xX$&";
asciichat_error_t err = apply_palette_config(
PALETTE_CUSTOM,
custom_chars
);
if (err != ASCIICHAT_OK) {
log_error("Failed to apply custom palette");
return err;
}
int apply_palette_config(palette_type_t type, const char *custom_chars)
Definition palette.c:250

Custom Palette Validation

Custom palettes are validated to ensure proper formatting:

Validation Rules:

  • Non-empty character sequence
  • Proper character ordering (if applicable)
  • Valid UTF-8 encoding (for multi-byte characters)
  • Compatible with client capabilities

Validation Example:

const char *custom_chars = " .:;+=xX$&";
size_t len = strlen(custom_chars);
// Validate palette
if (!validate_palette_chars(custom_chars, len)) {
log_error("Invalid custom palette");
return ERROR_INVALID_PALETTE;
}
// Check UTF-8 requirements
if (palette_requires_utf8_encoding(custom_chars, len)) {
// Check client UTF-8 support before using
utf8_capabilities_t caps;
if (!caps.utf8_support) {
log_warn("Custom palette requires UTF-8 but client doesn't support it");
return ERROR_PALETTE_INCOMPATIBLE;
}
}
bool palette_requires_utf8_encoding(const char *chars, size_t len)
Definition palette.c:55
bool validate_palette_chars(const char *chars, size_t len)
Definition palette.c:71

Palette Ordering:

  • Palettes should be ordered from darkest to lightest character
  • First character = darkest (for low brightness pixels)
  • Last character = lightest (for high brightness pixels)
  • Ordering affects luminance mapping quality

Luminance Mapping

Luminance mapping is the "secret sauce" that makes video-to-ASCII conversion fast. Instead of calculating which character to use for each pixel every time, we pre-compute a lookup table that maps brightness values directly to character indices. This makes the conversion super fast—we're talking O(1) lookup time!

Luminance Mapping Overview

Luminance mapping converts pixel brightness values (0-255) directly to palette character indices using a pre-computed lookup table. This eliminates the need for searching or calculation during video-to-ASCII conversion, which makes everything significantly faster.

How does it work?

Mapping Process:

  1. Pixel brightness extracted (0-255 range): We calculate how bright each pixel is (from completely black at 0 to completely white at 255)
  2. Direct lookup in luminance mapping table (256 entries): We use the brightness value as an index into a lookup table—no calculation needed!
  3. Character index retrieved (0 to palette_length-1): The lookup table tells us which character index to use
  4. Character selected from palette: We grab that character from the palette
  5. Character rendered to terminal: Finally, we render it to the terminal

The key insight is that we pre-compute the lookup table once during initialization, so during rendering we just do a simple array lookup. This is way faster than calculating which character to use for each pixel every time!

Building Luminance Maps

Luminance mapping tables are built from palette character sequences:

// Get palette characters
const palette_def_t *palette = get_builtin_palette(PALETTE_STANDARD);
// Build luminance mapping
char luminance_mapping[256];
asciichat_error_t err = build_client_luminance_palette(
palette->chars,
palette->length,
luminance_mapping
);
// luminance_mapping[0] = 0 (darkest character)
// luminance_mapping[128] = palette->length / 2 (mid-tone character)
// luminance_mapping[255] = palette->length - 1 (lightest character)
int build_client_luminance_palette(const char *palette_chars, size_t palette_len, char luminance_mapping[256])
Definition palette.c:279
const palette_def_t * get_builtin_palette(palette_type_t type)
Definition palette.c:47

Mapping Distribution:

  • Brightness 0 → Character index 0 (darkest)
  • Brightness 128 → Character index length/2 (mid-tone)
  • Brightness 255 → Character index length-1 (lightest)
  • Mapping is proportional across the full brightness range

Performance Benefits:

  • O(1) lookup time (constant time)
  • No calculation required during rendering
  • Pre-computed for optimal performance
  • Single array access per pixel

Using Luminance Maps

Luminance maps are used during video-to-ASCII conversion:

// During video frame conversion
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// Get pixel brightness (0-255)
uint8_t brightness = calculate_pixel_brightness(pixels, x, y);
// Direct lookup in luminance mapping
char char_index = luminance_mapping[brightness];
// Select character from palette
char ascii_char = palette_chars[char_index];
// Render to terminal
render_char(ascii_char);
}
}

Initialization and Configuration

Client Palette Initialization

Complete client palette initialization combines palette selection, validation, and luminance mapping creation:

// Initialize client palette with full configuration
char client_palette_chars[256];
size_t client_palette_len;
char client_luminance_palette[256];
asciichat_error_t err = initialize_client_palette(
PALETTE_BLOCKS, // Palette type
NULL, // Custom chars (NULL for built-in)
client_palette_chars, // Output: palette characters
&client_palette_len, // Output: palette length
client_luminance_palette // Output: luminance mapping
);
if (err != ASCIICHAT_OK) {
log_error("Failed to initialize client palette");
return err;
}
// Use palette for video conversion
// client_palette_chars contains character sequence
// client_luminance_palette contains brightness-to-index mapping
int initialize_client_palette(palette_type_t palette_type, const char *custom_chars, char client_palette_chars[256], size_t *client_palette_len, char client_luminance_palette[256])
Definition palette.c:299

Applying Palette Configuration

Global palette configuration is applied using apply_palette_config():

// Apply built-in palette globally
asciichat_error_t err = apply_palette_config(
PALETTE_STANDARD,
NULL // NULL for built-in palettes
);
// Apply custom palette globally
PALETTE_CUSTOM,
" .:;+=xX$&" // Custom character sequence
);

Configuration Steps:

  1. Validate palette (if custom)
  2. Check UTF-8 requirements
  3. Verify client compatibility
  4. Apply palette globally
  5. Build luminance mapping
  6. Make available for rendering

Architecture

Data Structures

Palette Definition:

typedef struct {
const char *name; // Human-readable name
const char *chars; // Character sequence
size_t length; // Number of characters
bool requires_utf8; // UTF-8 requirement flag
bool is_validated; // Validation status
} palette_def_t;

UTF-8 Character Information:

typedef struct {
char bytes[4]; // UTF-8 byte sequence (max 4 bytes)
uint8_t byte_len; // Number of bytes (1-4)
uint8_t display_width; // Terminal display width (1-2)
} utf8_char_info_t;
int display_width(const char *text)

UTF-8 Palette:

typedef struct {
utf8_char_info_t *chars; // Array of character info
size_t char_count; // Number of characters (not bytes!)
size_t total_bytes; // Total byte length of string
char *raw_string; // Original palette string
} utf8_palette_t;

UTF-8 Capabilities:

typedef struct {
bool utf8_support; // Detected UTF-8 support
bool forced_utf8; // User-forced UTF-8 flag
char terminal_type[32]; // $TERM value
char locale_encoding[16]; // Locale encoding (e.g., "UTF-8")
} utf8_capabilities_t;

API Functions

Palette Management:

UTF-8 Palette Functions:

UTF-8 Detection:

Usage Examples

Basic Palette Usage

Using Built-in Palette:

#include "palette.h"
// Get built-in palette
const palette_def_t *palette = get_builtin_palette(PALETTE_STANDARD);
// Check UTF-8 requirements
if (palette->requires_utf8) {
utf8_capabilities_t caps;
if (!detect_client_utf8_support(&caps) || !caps.utf8_support) {
log_error("Palette requires UTF-8 but terminal doesn't support it");
return ERROR_PALETTE_INCOMPATIBLE;
}
}
// Apply palette globally
asciichat_error_t err = apply_palette_config(PALETTE_STANDARD, NULL);

Custom Palette Usage

Defining and Using Custom Palette:

// Define custom palette
const char *custom_chars = " .:-=+*#%@";
// Validate palette
if (!validate_palette_chars(custom_chars, strlen(custom_chars))) {
log_error("Invalid custom palette");
return ERROR_INVALID_PALETTE;
}
// Apply custom palette
asciichat_error_t err = apply_palette_config(
PALETTE_CUSTOM,
custom_chars
);
if (err != ASCIICHAT_OK) {
log_error("Failed to apply custom palette");
return err;
}

UTF-8 Palette Usage

Working with UTF-8 Palettes:

// Create UTF-8 palette
utf8_palette_t *palette = utf8_palette_create(" ░░▒▒▓▓██");
// Get character count (not byte count!)
size_t char_count = utf8_palette_get_char_count(palette);
// char_count = 10 (characters, not bytes)
// Get character at index
const utf8_char_info_t *char_info = utf8_palette_get_char(palette, 3);
// char_info->bytes = "░"
// char_info->byte_len = 3
// char_info->display_width = 1
// Find character index
size_t index = utf8_palette_find_char_index(palette, "▒", 3);
// index = 5 (5th character in palette)
// Clean up
size_t utf8_palette_find_char_index(const utf8_palette_t *palette, const char *utf8_char, size_t char_bytes)
Definition palette.c:526

Client Palette Initialization

Complete Client Palette Setup:

// Detect client UTF-8 support
utf8_capabilities_t caps;
// Select compatible palette
palette_type_t palette_type = select_compatible_palette(
PALETTE_BLOCKS, // Requested
caps.utf8_support
);
// Initialize complete palette configuration
char client_palette_chars[256];
size_t client_palette_len;
char client_luminance_palette[256];
asciichat_error_t err = initialize_client_palette(
palette_type,
NULL, // Built-in palette
client_palette_chars,
&client_palette_len,
client_luminance_palette
);
// Use palette for rendering
// client_palette_chars: character sequence
// client_luminance_palette: brightness-to-index mapping

Best Practices

Character Ordering

Always Order from Dark to Light:

  • First character = darkest (for black/low brightness)
  • Last character = lightest (for white/high brightness)
  • Proper ordering ensures accurate luminance mapping
  • Incorrect ordering produces inverted or incorrect output

Example Ordering:

// ✅ CORRECT: Dark to light
" .:-=+*#" // spaces (dark) → # (light)
// ❌ WRONG: Light to dark
"#*+=:-. " // Inverted order

UTF-8 Compatibility

Always Check UTF-8 Requirements:

  • Check if palette requires UTF-8 before using
  • Verify client UTF-8 support for UTF-8 palettes
  • Provide fallback to ASCII palettes
  • Use select_compatible_palette() for automatic selection

Example:

// Check UTF-8 requirements
utf8_capabilities_t caps;
if (!caps.utf8_support) {
log_warn("UTF-8 palette incompatible with client");
// Fall back to ASCII palette
palette = get_builtin_palette(PALETTE_STANDARD);
}
}

Always Validate Custom Palettes

Validate Before Use:

  • Always validate custom palettes
  • Check for empty or invalid sequences
  • Verify UTF-8 encoding for multi-byte characters
  • Ensure proper character ordering

Example:

// Validate before applying
if (!validate_palette_chars(custom_chars, len)) {
log_error("Invalid custom palette");
return ERROR_INVALID_PALETTE;
}
// Apply only if valid
apply_palette_config(PALETTE_CUSTOM, custom_chars);

Performance Optimization

Use Luminance Mapping:

  • Always build luminance mapping for performance
  • Use direct lookup instead of calculation
  • Pre-compute mappings during initialization
  • Cache mappings for repeated use

Example:

// Build mapping once during initialization
char luminance_mapping[256];
build_client_luminance_palette(palette_chars, palette_len, luminance_mapping);
// Use direct lookup during rendering (fast!)
char char_index = luminance_mapping[pixel_brightness];
char ascii_char = palette_chars[char_index];

Performance Characteristics

Palette performance is all about speed—we're converting video frames to ASCII in real-time, so every optimization matters. The luminance mapping system makes this super fast, but let's look at what you can expect:

How fast is palette lookup?

Palette Lookup:

  • Luminance mapping: O(1) constant time lookup—that means it takes the same amount of time regardless of how many characters are in the palette
  • No calculation required during rendering: We just do a simple array lookup—no math, no searching, just grab the value from the table
  • Single array access per pixel: One lookup, one character selection, done!
  • Optimal for real-time video conversion: Fast enough that you can convert video frames at 60 FPS without any problems

How fast is UTF-8 processing?

UTF-8 Processing:

  • UTF-8 palette creation: O(n) where n is byte length—this happens once during initialization, so it's not a bottleneck
  • Character index lookup: O(1) with pre-computed structure—once the palette is built, lookups are instant
  • UTF-8 detection: O(1) environment variable checks—we just check a few environment variables, which is essentially free

How much memory does it use?

Memory Usage:

  • ASCII palette: ~23 bytes (STANDARD palette)—super tiny!
  • UTF-8 palette: ~40-100 bytes depending on character count (still very small, even for larger palettes)
  • Luminance mapping: 256 bytes (fixed size)—this is the lookup table, and it's always 256 bytes regardless of palette size
  • UTF-8 character info: ~8 bytes per character—just a tiny bit of metadata per character to track byte length and display width

Memory usage is minimal—even with UTF-8 palettes, you're looking at well under a kilobyte per palette. The luminance mapping table is always 256 bytes (one byte per possible brightness value), which is nothing by modern standards.

Integration with Video-to-ASCII

The palette module integrates with the video conversion pipeline:

Conversion Pipeline:

  1. Video frame captured (RGB pixels)
  2. Pixel brightness calculated (0-255)
  3. Luminance mapping lookup (brightness → char index)
  4. Character selected from palette
  5. Character rendered to terminal

Example Integration:

// Initialize palette
char palette_chars[256];
size_t palette_len;
char luminance_mapping[256];
initialize_client_palette(PALETTE_STANDARD, NULL,
palette_chars, &palette_len, luminance_mapping);
// Convert frame to ASCII
for (int y = 0; y < frame_height; y++) {
for (int x = 0; x < frame_width; x++) {
// Get pixel brightness
uint8_t brightness = get_pixel_brightness(frame, x, y);
// Lookup character index
size_t char_index = luminance_mapping[brightness];
// Select character
char ascii_char = palette_chars[char_index];
// Render
render_char(ascii_char);
}
}
See also
palette.h
Video to ASCII Conversion
Video Frame Buffers