Tatuagem (Portuguese for “tattoo”) stamps a big ASCII-art banner onto every source file in a directory, as a comment in each file’s own language. It’s a code signature, and a boastful one. It is on PyPI.
This was a fun collaboration between me and Derek Dickerson. The idea is simple: you have a directory of source files, and you want to stamp a text banner (“CONFIDENTIAL”, “DRAFT”, “COPYRIGHT 2026 ACME CORP”, whatever) across every single file, recursively. A tattoo for your code.
Why This Exists
I kept running into the same annoyance: corporate projects that require license headers, confidentiality banners, or copyright notices at the top of every source file. You can write a bash one-liner with find and sed, sure. But then you need to handle different file types (some use # comments, some use //, some use /* */), you need to avoid double-stamping files that already have the banner, and you need to leave binary files alone. It is the kind of task that feels like it should take five minutes and actually takes an hour of edge cases.
Tatuagem handles all of this in one command.
How It Works
Rendering. Each character of your phrase is drawn with Pillow’s ImageFont/ImageDraw onto a 64×64 canvas in memory, and the result is read back as a NumPy array. Every pixel is either ink or background: ink becomes the --text character (default 1), background becomes the --backsplash character (default 0), or a repeating --pattern if you want wallpaper. Empty columns are trimmed so the glyphs sit tight together. Here is tatuagem hi --text '#' --backsplash ' ', shortened:
### ###
### ###
###
### ######## ###
############# ###
##### #### ###
#### #### ###
### #### ###
### #### ###
It ships with Arial Unicode and Poppins, and --font takes a path to any .ttf/.otf, so accents, symbols and scripts like Greek, Cyrillic and Chinese render. Each character is drawn on its own, with no text shaping, so Hebrew and Arabic letters come out in reverse order, Arabic letters don’t join, and combining marks such as Devanagari vowel signs and Thai tone marks are drawn as separate characters.
Tattooing. With --recurse-path, Tatuagem walks the tree and works out each file’s language from its name (Makefile, Dockerfile), its extension, and for the ambiguous ones its content: a .pl with :- rules is Prolog, not Perl, and a .m with #import is Objective-C, not MATLAB. Extensionless scripts are identified by their shebang. The banner is then written in that language’s comment syntax, with a few safety rules:
- A block comment (
/* */,<!-- -->,""") is only used when the banner can’t close it early. If your art contains*/, C files get//line comments instead. If a language has no comment form that can hold the banner safely, the file is skipped rather than broken. - Anything that has to stay at the top stays there: shebangs,
<?php,<?xml ?>declarations, Python/Ruby encoding cookies, and Dockerfile# syntax=directives. - Hidden directories (
.git,.venv) are never touched, and.tatignoreexcludes paths with the same rules as.gitignore. - It’s idempotent: files that already carry a tattoo are skipped, and
--overwriteswaps an old banner for a new one.
Usage
pip install tatuagem
Print a banner:
tatuagem "CONFIDENTIAL"
tatuagem "L'appel du vide" --text '@' --backsplash '!'
tatuagem "Tatuagem" --pattern '`:,.'
Stamp a codebase, previewing first:
tatuagem "COPYRIGHT 2026 ACME" --recurse-path ./src --dry-run
tatuagem "COPYRIGHT 2026 ACME" --recurse-path ./src
tatuagem "DRAFT v2.0" --font ./myfont.ttf --recurse-path ./docs --overwrite
Use pre-made art (or a multi-line license header) from a file as-is:
tatuagem --file ./license_header.txt --recurse-path ./src
Enforce it in CI. --check exits non-zero if any file is missing its tattoo:
tatuagem --file ./license_header.txt --recurse-path . --check
Does It Actually Work?
The repo has a corpus of hello-worlds in 450 language folders, and CI keeps Tatuagem honest in three ways:
- Language coverage. Tatuagem tattoos the whole corpus and counts a language only if the comment it wrote is valid in that language. That’s 91.9% of the 394 languages that can hold a comment at all, COBOL, Visual Basic and AppleScript included. Languages that can’t hold one, like JSON, Piet (whose programs are images) and Brainfuck (which has no comment delimiter, so a
+or.in the art would run as code), are listed but not counted. A weaker test, that each folder merely has an entry in a lookup table, would measure nothing. - It still compiles. A tattooed copy of the corpus goes back through the syntax checks of gcc, g++, node, python, ruby, perl, bash, php, swiftc and cobc: 1,813 files, none broken, with both a rendered banner and a hand-drawn one full of quotes and backslashes.
- Dogfooding. Every source file in Tatuagem carries a tattoo, and CI fails if one doesn’t.
Design Choices
-
Pillow for text rendering: I could have used a FIGlet-style ASCII font, but rasterizing real TrueType fonts means any font works, and the banner actually looks good.
-
Refuse rather than corrupt: A tool that edits every file in your repo has to be boring about safety. When a language’s comment rules are unclear, Tatuagem leaves the file alone.
-
File-based text input: With
--recurse-path, the--fileflag reads the stamp from a file and uses it verbatim, which is how you get multi-line license headers or hand-made art like the piece at the end of this post.
Derek handled Windows support and cleaned up the comment-syntax table, while I focused on the recursive file walking and the overwrite detection. Good division of labor for a weekend project.
A hand-drawn stamp of the kind --file takes:
▋▄▄▄▄▂▉▋▎ ▏▎▋▉▂▄▄▄▃▌
▇█████▅▅█▇▃▉▍ ▏▌ ▌ ▍▉▃▆▆▄▆█████▄
▁███▆▇▃▇███▅▅▅▁▌▏ ▍▍ ▌▍ ▏▌▂▆▅▅███▆▄▆▆███▋
▄██▂▄████▅▊▃███▇▁▍ ▏▍▎ ▎▍▏ ▍▂▇███▁▉▆████▄▃██▁
▏▂█▇██▉▍▍▌▊▅▁▍▍▁▇█▅▊▏ ▏▍▎ ▎▍▏ ▏▊▅█▆▁▍▌▂▅▊▌▍▍▁██▇▇▉
▉██▇▃▌▌▍▍▌▋▌▏ ▏▋▇█▇▉▏ ▎▋▋▎ ▏▉▇█▆▋▏ ▏▌▋▌▍▍▍▋▄▇██▋
▏▇█▇▊▍▍▍▍▍▍▌▋▋▌▍▆███▇▊▂██▉▉▇██▇▂▌▋▋▊▌▍▍▍▍▍▍▁▇█▅
▍██▃▎ ▏▍▍▌▋▂▆▇█████▇▇████████▆▄▋▍▍▍▏ ▏▍▄██▎
▁█▇▁▌▍▍▎▏ ▏▊▇█████▆▇█████▂▎ ▏▎▍▍▋▂██▊
▏▃█▆▊▋▉▂▄▆▇▇████████████████████▇▇▆▄▂▉▋▉▇█▉
▋▃▆▇▇▇██▄▁▁▁▄▊▉█████████▇▊▋▃▁▂▂▄██▆▇▇▆▃▌
▉██▂▍▍▍▊▎ ▁████████▉ ▎▋▍▍▍▄█▇▊
▌██▇▌▍▍▎▊▏▍▊▎▃▇█████▂▎▋▍▏▋▎▍▍▌███▎
▋███▋▎▌▎▊▍▌▏▍▎▆████▅▌▎▏▌▍▋▎▌▍▂███▍
▆█▇█▄▎▊▏▏▋ ▋ █████▅▏▋ ▋▏▎▊▌▅█▇█▄
▏▄████▅▋▂▏ ▊ ▇████▅ ▊ ▎▁▋▅████▃
▌▅█▇█▇█▆▅▅▊▇████▇▉▆▅▆█▇▇██▅▎
▎▃▅▇█▇▇██████████▇▇█▇▅▃▎
▍▄▆████████████▆▃▎
▏▉▇▂▏▉▇████████▇▉▏▂▇▉▏
▎▊▃███▄ ▏▊▅████▅▊▏ ▃███▃▊▎
▏▍▊▃▆██████▅▊ ▏▉▇▇▊ ▋▅██████▆▃▊▍▏
▏▌▁▄▇██████████▆▎▊▏▎▊▅██▅▊▎▏▊▎▆███████████▄▁▌▏
▏▄████████████████▏▏▊▌ ▍██▍ ▍▊▏▎████████████████▄
▌█████████████████▊ ▃██▃ ▊█████████████████▌
▃█████████████████▄ ▏████▏ ▄█████████████████▂
▏▇██████████████████▎ ▌████▍ ▎██████████████████▆
▍███████████████████▁ ▊████▊ ▁███████████████████▏
▊███████████████████▇▏ ▁████▁ ▏▇███████▆▃▉▄████████▌
▁████████████████████▉ ▂████▂ ▉█████▄▁▊▉▁▂▆████████▊
▃████████████████████▇▏▄████▄▏▇████████████████████▁
▄█████████████████████▁▄████▄▁█████████████████████▃
▅██████████████████████▇████▇██████████████████████▄
▆██████████████████████████████████████████████████▅
▇██████████████████████████████████████████████████▅
▇████████▂████████████████████████████████▂████████▅
▇███████▆▍████████████████████████████████▎▆███████▅
▇███████▁ ▆██████████████████████████████▆ ▁███████▅
▇███████▅ ▅██████████████████████████████▅ ▄███████▇
████████▉ ▅██████████████████████████████▄ ▉████████
▄▄▄▄▄▄▄▅▂ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▂ ▂▄▄▄▄▄▄▄▃