Skip to content

format: an SVG drawing can be any size you ask for - #87

Merged
donislawdev merged 4 commits into
mainfrom
format/svg-dimensions
Sep 8, 2026
Merged

format: an SVG drawing can be any size you ask for#87
donislawdev merged 4 commits into
mainfrom
format/svg-dimensions

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

Two new settings on svg: width and height, a whole number of pixels from 1 to 20000 each. They default to the 800 by 600 these drawings have always been, so a recipe that says nothing gets the same bytes it got before.

tfg generate --format svg --size 20kb --set width=1920 --set height=1080

Where the ceiling comes from

Not from the renderer - it never broke. Measured headless with Inkscape 1.4.4, dimensions read back from the rendered PNG with Pillow:

dimensions megapixels render PNG
10000x8000 80 5.3 s 339 kB
20000x20000 400 25.6 s 1.6 MB
32000x32000 1024 43.6 s 4.1 MB
65536x65536 4295 165.3 s 17 MB

The line worth crossing belongs to a reader instead: Pillow refuses an image over PIL.Image.MAX_IMAGE_PIXELS, measured at 89478485 px, as a decompression bomb. 20000 per axis reaches 400 megapixels, four and a half times over that line, so a set can hold files on both sides of it. That is how the CSV column ceiling was chosen too.

65536 per axis was rejected with a number rather than out of caution: Inkscape manages it, but that is 165 seconds and 17 MB in somebody's CI for one line of recipe, and no measured reader has a threshold between 400 and 4295 megapixels.

There is no joint megapixel limit, unlike the picture formats, because nothing is drawn into pixels here - a picture format holds the raster in memory while it encodes, and this one writes text.

Two defects the settings would otherwise have introduced

Both invisible while the dimensions were constants, and both found by analysis before the code was written:

  • the shape code assumes a margin of up to eighty units, and rand.IntN panics on a non-positive argument - measured by running it. --set width=50 would have been a panic on a value that looks entirely legal.
  • a shape whose lower edge runs past the drawing paints over the label. That is the defect the strip along the bottom was added for: about one shape in ten used to land on the label, 11.0% and 10.3% measured. The strip fixed it for one canvas size, and it has to hold for every size now.

Both clamps are inert at 800 by 600 - the widest rect reaches 798 of 800, the tallest 542 of 544, two units of slack on every axis - so the guards press the sizes where they are not.

Bytes

D11 holds. Four sizes at seed 7 hash identically before and after, and stating the default out loud gives the same bytes as leaving it out.

A drawing shorter than 57 pixels has no room for the label along its bottom edge. It is still produced and still named, and the run says which files those were - the same sentence the picture formats use for a picture too small to write on.

Evidence

  • four new guards in internal/guard/svgdimensions_test.go, seventeen canvases each, every case chosen for a reason
  • five new mutations, all caught
  • four existing SVG mutations repointed after the constants they aimed at became functions - they had gone stale silently, which is a SKIP that reads as proven
  • oracle: four files rendered and measured, 1920x1080, 400x40, 50x600, 1x1, every one landing on the dimensions ordered
  • full suite green, gofmt and go vet clean, 300 of 300 binary checks

🤖 Generated with Claude Code

donislawdev and others added 4 commits September 8, 2026 08:57
A total printed as 2516582400 B. Eleven digits with nothing to hold on
to, and this tool prints byte counts everywhere - it is the whole point
of it, so the one number a person came for was the hardest to read.

It now prints 2 516 582 400 B, in every message that names bytes: the
minimum in tfg formats, the summary a run prints, what a preset says
its budget is, and what tfg validate reports.

A space rather than a comma. A comma is a thousands mark in some
countries and a decimal point in others, and this tool is read in both.

Machine output is untouched. Nothing in a manifest or under --json goes
through here, because a number there is a number rather than a
sentence, so no script anybody has written sees any of this.

One guard read the old shape and had to be taught the new one, and what
it said while it was wrong is worth keeping: it split the line on the
first space, read "1 220 B" as one byte, and reported that the tool
refuses the minimum it advertises. It was right about what it saw and
wrong about what it meant, which is what a parser splitting on the
wrong thing always is. Two tools outside this repository read the same
line and needed the same lesson.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI went red on eight tests, all of them guards reading a byte count out
of the command line's own output. I had written in this branch's own
message that a change to a human sentence has a blast radius equal to
the number of things parsing it, and then shipped without running the
suite, so the branch proved its own point.

Three places, three different shapes:

- sizeText built the expected text with strconv.FormatInt, so it looked
  for "36415" in a report saying "36 415 B" and found nothing. It asks
  core.ExactBytes now, WITH the unit - which is stronger than what it
  replaced rather than merely equal, because bare digits could match
  inside a longer number and "36415" does match in "136415".

- The boundary announcement is pinned as literal text, so it moves to
  the grouped spelling.

- The sizes a person writes are pinned as literal text as well, and
  deliberately not asked of core.ExactBytes: that guard is over what a
  PERSON reads, so the spelling is half of what it holds, and a guard
  built from the same function the program prints with cannot tell the
  two apart.

Every expected string was checked against what the program actually
prints rather than worked out by hand: 1 610 612 736 B, 10 485 760 B,
1 048 576 B, 716 800 B, 0 B, 15 728 640 B.

One subtest cannot run on this machine and it is not this change: 1.5gib
is refused with exit 6, the free space code, because the disk has 1.1 GB
left. The tool is behaving correctly and the runner has room.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two new settings on svg: width and height, a whole number of pixels from
1 to 20000 each. They default to the 800 by 600 these drawings have always
been, so a recipe that says nothing gets the same bytes it got before.

    tfg generate --format svg --size 20kb --set width=1920 --set height=1080

There is no joint limit on the two, unlike the picture formats, because
nothing is drawn into pixels here - the file only says how big it is. That
makes a small file that claims to be enormous, which is the point: a 3 kB
drawing declaring 20000 by 20000 asks whether whatever opens it has a limit
on picture size and not only on file size.

The ceiling is not the renderer's. Measured headless: Inkscape draws 4295
megapixels in 165 s without complaint. The line worth crossing belongs to a
reader instead - Pillow refuses an image over 89478485 px as a decompression
bomb - and 20000 per axis reaches 400 megapixels, so a set can hold files on
both sides of it.

A drawing shorter than 57 pixels has no room for the label along its bottom
edge. It is still produced and still named, and the run says which files
those were.

Two defects the settings would otherwise have introduced, both invisible
while the dimensions were constants:

  - the shape code assumed a margin of up to eighty units, and rand.IntN
    panics on a non-positive argument, so a narrow canvas would have been a
    panic on a value that looks entirely legal;
  - a shape whose lower edge ran past the drawing would paint over the
    label, which is the defect the strip along the bottom was added for in
    the first place.

Both clamps are inert at 800 by 600 - the widest shape lands two units short
- so the stored byte hashes are unchanged and the guards press the sizes
where the clamps are not.

Four new guards, five new mutations, and four existing SVG mutations
repointed after the constants they aimed at became functions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
golangci-lint's errorlint rule refuses == against a sentinel error, because
it fails on a wrapped one. Reproduced locally with the pinned v2.13.2 rather
than read off the runner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit 79dd0f4 into main Sep 8, 2026
18 checks passed
@donislawdev
donislawdev deleted the format/svg-dimensions branch September 8, 2026 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant