Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Runic — an L-system raster graphics engine in C

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.

Koch snowflake rendered by Runic, captioned with the built-in BDF font renderer

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.

What it does

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).

Build & run

make build      # or: gcc -Wall -g -o runic main.c -lm
./runic < examples/demo.txt

Commands are read from standard input, so the program works both interactively and as a script runner.

Quick demo

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

L-system file format

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°.

Undo/redo

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

Bit-level engraving check

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)

Implementation notes

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.

Project structure

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

License

MIT — see LICENSE.

About

A dependency-free raster graphics engine in C99: L-system expansion, turtle graphics rasterised with Bresenham, binary PPM I/O, BDF bitmap font rendering, bit-level engraving analysis, and full undo/redo.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages