A command-driven graphics engine written from scratch in C99, with no dependencies beyond the standard library and libm. It expands Lindenmayer systems into turtle-graphics paths, rasterises them onto PPM images with Bresenham's line algorithm, renders text using bitmap fonts parsed from BDF files, and supports full undo/redo across every operation.
Produced entirely by this program — see Quick demo. The outline is a third-iteration Koch snowflake drawn by the turtle; the caption is rendered glyph by glyph from a BDF bitmap font.
Academic archive. Coursework submitted for Programarea Calculatoarelor și Limbaje de Programare (PCLP), UNSTPB — Faculty of Automatic Control and Computer Science, CTI, first semester, 2025–2026.
The program reads commands from standard input and maintains a system state made of three loaded resources: an L-system ruleset, a PPM image, and a bitmap font. Every command that changes state pushes a full deep copy onto an undo stack, so any operation can be rolled back and replayed.
| Command | Effect |
|---|---|
LSYSTEM <file> |
Load an L-system (axiom + production rules) |
DERIVE <n> |
Expand the axiom for n iterations and print the resulting string |
LOAD <file.ppm> |
Load a binary P6 PPM image as the canvas |
TURTLE <x> <y> <speed> <angle> <step> <n> <r> <g> <b> |
Expand the L-system n times, walk the turtle, rasterise the path |
FONT <file.bdf> |
Load a bitmap font from a BDF file |
TYPE "<text>" <x> <y> <r> <g> <b> |
Render text onto the canvas from the loaded font |
BITCHECK |
Scan the image bitstream for engraving-unsafe bit patterns |
SAVE <file.ppm> |
Write the canvas back out as binary P6 |
UNDO / REDO |
Move backwards / forwards through the state history |
EXIT |
Quit |
The turtle understands F (move forward, drawing), + / - (turn by the angle step), and [ / ] (push / pop position and heading onto a stack, for branching structures).
make build # or: gcc -Wall -g -o runic main.c -lm
./runic < examples/demo.txtCommands are read from standard input, so the program works both interactively and as a script runner.
examples/demo.txt draws the image at the top of this README:
LSYSTEM examples/koch.lsystem
DERIVE 2
LOAD examples/canvas.ppm
TURTLE 70 278 8.9 0 60 3 255 190 60
FONT examples/runic.bdf
TYPE "RUNIC" 170 14 235 235 245
SAVE examples/snowflake.ppm
EXIT
$ ./runic < examples/demo.txt
Loaded examples/koch.lsystem (L-system with 1 rules)
F+F--F+F+F+F--F+F--F+F--F+F+F+F--F+F--F+F--F+F+F+F--F+F--F+F--F+F+F+F--F+F--...
Loaded examples/canvas.ppm (PPM image 400x400)
Drawing done
Loaded examples/runic.bdf (bitmap font -runic-mini-medium-r-normal--14-100-75-75-c-100-iso8859-1)
Text written
Saved examples/snowflake.ppm
F--F--F <- axiom
1 <- number of production rules
F F+F--F+F <- rule: symbol, then its replacement
examples/koch.lsystem is the Koch snowflake: three sides, each edge repeatedly replaced by a four-segment bump at 60°.
examples/undo_redo.txt shows the history stacks in action:
$ ./runic < examples/undo_redo.txt
Nothing to undo <- nothing on the stack yet
Loaded examples/koch.lsystem (L-system with 1 rules)
F+F--F+F--F+F--F+F--F+F--F+F <- DERIVE 1
No L-system loaded <- after UNDO, the ruleset is gone
Loaded examples/koch.lsystem (L-system with 1 rules) <- REDO replays the message
F+F--F+F--F+F--F+F--F+F--F+F <- and the ruleset is back
BITCHECK treats the image not as pixels but as one long bitstream and slides a 4-bit window across it, flagging the patterns 0010 and 1101 — sequences an engraver could misread. Because the window crosses byte and channel boundaries, a flagged bit can change how a pixel's red, green and blue values are read back:
$ ./runic < examples/bitcheck.txt
Loaded examples/tiny.ppm (PPM image 2x2)
Warning: pixel at (0, 0) may be read as (13, 0, 255)
Warning: pixel at (0, 0) may be read as (47, 0, 255)
Warning: pixel at (1, 0) may be read as (0, 32, 48)
Warning: pixel at (1, 0) may be read as (16, 0, 48)
Generic containers. The linked list and stack store void * payloads and take a void (*free_data)(void *) callback at teardown, so the same two structures hold turtle states, line segments, pixels, error records and whole system snapshots without duplication.
Undo/redo through deep copies. Rather than journalling reverse operations, each state-changing command clones the entire state — L-system, image and font — and pushes it. UNDO pops from the undo stack onto the redo stack; REDO moves it back. Each snapshot also carries the message its command printed, so a redone command reproduces its original output. This trades memory for correctness: no operation needs a hand-written inverse, and the image can never be left half-modified.
L-system expansion. Each iteration allocates a fresh buffer sized current_length × longest_replacement + 1 — an upper bound that avoids reallocating mid-pass — then walks the current string, appending each symbol's replacement or the symbol itself when no rule matches.
Turtle and rasterisation. The turtle keeps a position and heading in floating point and emits line segments; [ and ] push and pop onto a stack of saved states, which is what makes branching (plants, trees) possible. Segments are then rasterised with integer-only Bresenham. The turtle works in a mathematical coordinate system with Y pointing up, so drawing flips to image row order (height - 1 - y). Points outside the canvas are skipped rather than clamped — clamping would smear the off-screen part of a line into a solid wall along the border.
PPM parsing. The reader tokenises the header while skipping # comments, which may legally appear between any two header fields, then reads pixel data as raw bytes. Only binary P6 with a maximum channel value of 255 is accepted; anything else is rejected rather than half-parsed.
BDF fonts. The parser is a small state machine over three contexts: global header, inside a glyph, and inside a BITMAP block. Each bitmap row arrives as hexadecimal, MSB first and left-aligned to the glyph's bounding-box width, so the decoder unpacks nibbles into one byte per pixel and discards the padding bits. Glyph placement then has to reconcile BDF's baseline-relative offsets (BBX x/y offsets, DWIDTH advance) with the image's top-left origin.
Bit-level analysis. BITCHECK indexes the image as a flat bitstream: bit i belongs to pixel i / 24, channel (i % 24) / 8, bit 7 - (i % 8) within that byte. The sliding window is kept as a rolling 4-bit integer, and when a pattern matches, the three channel bytes are re-read with the suspect bit flipped to report exactly what colour the pixel might be misread as.
| File | Role |
|---|---|
main.c |
The whole program: containers, L-system, PPM, turtle, BDF, bitcheck, command dispatch |
Makefile |
build, run, clean targets |
examples/ |
L-system rules, canvas, bitmap font and runnable command scripts |
docs/snowflake.png |
The demo output, converted to PNG for display above |
NOTES.ro.md |
Original implementation notes, in Romanian |
MIT — see LICENSE.
