Why I made it
I wrote this script for one simple purpose: to Rickroll my friends.
How it works
This is the workflow:
- It fetches a .png frame from a PHP endpoint on a website I own. The frame number is provided through a
qGET parameter. - Instead of saving the downloaded frame to disk, the script keeps it in memory using Pillow.
- It converts the image to ASCII using ascii-magic and stores the result in a list.
There are 28 total frames. The script loops through all of them, incrementing q on each request, generates ASCII versions, then plays them in an endless loop with a short delay. It keeps running until manually stopped with Ctrl+C.
Problems
There are two issues. My question is focused on the first one:
- The main issue is how choppy and flickery the animation looks. The rendering is inconsistent at higher playback speeds, and reducing the speed makes it smoother but still doesn’t eliminate the flicker.
- The second issue is that the script takes around 30–40 seconds to generate all 28 ASCII frames before playback. I’m okay with this delay because it adds suspense.
Dependencies
- Python 3
- requests
- pillow (Pillow)
- ascii-magic
- A Unix or Windows terminal (for clearing and redraw)
Code
Here is the Python script:
"""
Live ASCII Rickroll generator.
Downloads 28 PNG frames from a PHP endpoint, converts them to ASCII using
ascii-magic, stores them in memory, and plays them in a continuous loop.
"""
import os
import time
import requests
from io import BytesIO
from PIL import Image
from ascii_magic import AsciiArt
def clear_screen():
"""Clear the terminal screen based on the operating system."""
if os.name == 'nt':
os.system('cls')
else:
os.system('clear')
# Get terminal size
size = os.get_terminal_size()
width = size.columns
frame = []
try:
# Loop to download and process images
at_frame = 1
while at_frame <= 28:
try:
url = f"http://chipz1.atwebpages.com/rick.php?q={at_frame}"
response = requests.get(url, allow_redirects=True)
response.raise_for_status()
# Open image in memory
img = Image.open(BytesIO(response.content))
# Convert to ASCII and store
frame.append(AsciiArt.from_pillow_image(img))
at_frame += 1
except OSError as e:
print(f'Could not load the image, server said: {e}')
# Infinite playback loop
while True:
frame_num = 0
while frame_num < len(frame):
frame[frame_num].to_terminal(columns=int(width / 2), width_ratio=2)
frame_num += 1
time.sleep(0.05)
clear_screen()
except KeyboardInterrupt:
clear_screen()
except Exception as e:
print(f"An error has occurred: {e}")
And here is the PHP endpoint that serves each PNG frame:
<?php
$q = isset($_GET['q']) ? intval($_GET['q']) : 0;
if ($q >= 1 && $q <= 28) {
$file = __DIR__ . "/frame{$q}.png";
if (file_exists($file)) {
header("Content-Type: image/png");
readfile($file);
exit;
}
}
http_response_code(404);
echo "Not found";

q... \$\endgroup\$