QUICKUPSCALE

PRACTICAL GUIDE

Tilesets, animation and atlas handoff

Convert a GIF to a sheet and retain frame delays

Start here

For the included original GIF, run python3 export_sheet.py partial-disposal --columns 3. The recorded export produced a 24×12 PNG sheet containing six complete 8×6 display frames, plus timing.json with raw GIF timing and separate decoder observations.

Expected result:

Every saved cell matched its independent literal RGBA reference. The PNG alone has no playback timing: keep the GIF and JSON beside it, especially when a delay is zero or a timing extension is absent.

What you need

Download the source project and its original six-frame GIF. This is a controlled recipe for ten named, byte-pinned originals, not a general GIF upload converter. It accepts no arbitrary input path.

Use an existing Python environment with Pillow 12.3.0. The measured run used Python 3.14.8 on macOS ARM64. The package contains source and inputs, no Python executable, dependency installer or native output cache.

Tested with: Python 3.14.8 and Pillow 12.3.0, native CLI on macOS ARM64; independent Node 24.22.0 PNG/literal verification. Retained execution, no new browser decoder or player-timing test.

1. Export full canvases into declared cells

Extract the package into your own directory. The GIFs and contract are already included. From its root, the executed export command was:

python3 export_sheet.py partial-disposal --columns 3

It writes outputs/sheets/partial-disposal/sheet.png and timing.json. Use --out for an empty directory you own. Existing identical files are preserved; differing, extra or nonregular destinations are rejected before output writes.

The six cells are row-major: (0,0), (8,0), (16,0), (0,6), (8,6), (16,6). Each stores a complete 8×6 canvas, including pixels carried over from earlier partial rectangles.

Six complete native GIF canvases arranged in three columns and two rows
24×12px. Actual Pillow CLI export; not a GUI capture. The original contains partial rectangles and disposal 2/3.

2. Retain source timing without inventing FPS

Inspect the actual timing sidecar. Each frame records its cell, source rectangle, disposal, gcePresent, delayCs and transparency declaration. Raw centisecond delays are [4, 0, 7, 13, 2, 1]; the separately observed Pillow durations are [40, 0, 70, 130, 20, 10] milliseconds.

Zero is a source value, not an instruction to assign a uniform frame rate. The missing-GCE control instead records gcePresent: false and delayCs: null; its observed duration is also null. Absence stays distinct from an explicit zero delay.

loopExtensions keeps raw application IDs and counts. The original six-frame input declares NETSCAPE count 0, while another control declares 2 and the absent-loop control has []. The sidecar does not reinterpret these as a player repeat API. It is this recipe’s local JSON, not an application-wide animation manifest.

3. Check alpha and failure boundaries

The transparent-disposal original produced four complete 6×5 frames in a three-column sheet, with two empty cells. Independent checks compared all four channels, including transparent areas, against the declared references.

Four native transparent-disposal frames arranged in the retained three-column sheet
18×10px. Actual transparent-disposal export, three columns and two rows; not a GUI capture.

The recipe admits only columns 1–8 and no more columns than frames. The recorded native unit tests rejected invalid columns and unknown case IDs without output writes. A cropped GIF image rectangle is not a complete display frame; the disposal diagnostic shows the visible failure from ignoring that distinction.

node generate.mjs
python3 native.test.py
node verify.mjs outputs

The Node verification command expects all ten sheet exports and three diagnostics under outputs, as described in the package README; one sheet alone is incomplete. The retained run used columns min(3, frameCount) for each case.

The retained native tests and independent Node verification covered ten original GIFs and 24 full canvases: partial disposal, transparent and nonzero backgrounds, local palettes, interlaced rows, timing/loop absence and tiny black/white controls. These results do not establish arbitrary GIF validity, large-file memory limits, cancellation, player timing, or preservation of unspecified hidden RGB. No browser GIF converter was executed for this recipe.

Sources and next steps