Tesla's Paint Shop reads custom wraps from a phone upload or a USB stick, and when a file breaks one of its rules it doesn't tell you: the wrap just never appears under Toybox → Paint Shop → Wraps. This post is a 40-line Python check that tests a PNG against the published rules before it goes anywhere near the car, and what it found on real files.
The rules Paint Shop enforces
Tesla publishes the templates and the file rules in its teslamotors/custom-wraps repository: one template per model and trim, twelve in total, from the original Model 3 to the Model Y L and Cybertruck. The requirements are short, and every one of them can make a wrap disappear:
| Rule | What Tesla specifies | A common way to break it |
|---|---|---|
| Format | PNG | exporting a JPG and renaming it .png
|
| Dimensions | 512–1024 px; use the template size (Cybertruck is 1024×768) | a 4K render or a photo straight from the camera roll |
| File size | no larger than 1 MB | detailed full-colour art saved as a truecolor PNG |
| File name | letters, digits, spaces, _ and -, max 30 characters |
emoji, accents, brackets, "final (2)" |
| Count | up to 10 from the app and up to 10 from USB | an eleventh file that never shows |
Two more apply only to USB: the drive needs a folder named exactly Wraps at the top level, formatted exFAT or FAT32 (not NTFS), and map or firmware update files on the same drive can stop the car from reading wraps.
In short: a Paint Shop wrap is a PNG between 512 and 1024 pixels (ideally the exact template size, 1024×768 for Cybertruck), no larger than 1 MB, named with letters, digits, spaces, dashes or underscores (30 characters max), uploaded from the Tesla app 4.59+ or placed in a top-level
Wrapsfolder on a USB drive.
None of these produce an error on the car, so a check on the computer is the only feedback you get.
The preflight script
The script uses Pillow and nothing else. It checks the four rules a single file can break on its own (format, dimensions, weight and name) and adds a shrink() helper for the one that's hardest to fix by hand.
#!/usr/bin/env python3
"""Preflight a Tesla Paint Shop wrap against the rules in teslamotors/custom-wraps."""
import os, re, sys
from PIL import Image
MAX_BYTES = 1_000_000 # "no larger than 1 MB" (decimal MB, to be safe)
NAME_OK = re.compile(r"^[A-Za-z0-9 _-]{1,30}$") # letters, digits, space, _ and -, max 30
def check(path, template=None):
problems = []
stem = os.path.splitext(os.path.basename(path))[0]
if not NAME_OK.match(stem):
problems.append(f"file name '{stem}' breaks the naming rule")
size = os.path.getsize(path)
if size > MAX_BYTES:
problems.append(f"{size / 1e6:.2f} MB is over the 1 MB limit")
with Image.open(path) as im:
if im.format != "PNG":
problems.append(f"actually a {im.format}, not a PNG")
w, h = im.size
if template:
with Image.open(template) as t:
if (w, h) != t.size:
problems.append(f"{w}x{h} does not match the template ({t.size[0]}x{t.size[1]})")
elif not (512 <= w <= 1024 and 512 <= h <= 1024):
problems.append(f"{w}x{h} is outside 512-1024 px")
return size, (w, h), problems
def shrink(src, dst, colors=256):
"""Palette-quantize instead of downscaling: same pixels on the car, far fewer bytes."""
im = Image.open(src).convert("RGBA")
im.quantize(colors=colors, method=Image.Quantize.FASTOCTREE,
dither=Image.Dither.FLOYDSTEINBERG).save(dst, optimize=True)
if __name__ == "__main__":
tpl = sys.argv[sys.argv.index("--template") + 1] if "--template" in sys.argv else None
for path in [a for a in sys.argv[1:] if a.endswith((".png", ".jpg")) and a != tpl]:
size, (w, h), problems = check(path, tpl)
status = "OK " if not problems else "FAIL"
print(f"{status} {os.path.basename(path):<28} {w}x{h} {size / 1e6:.2f} MB " + "; ".join(problems))
A few choices worth explaining:
-
Format is read from the file, not the extension.
Image.open()sniffs the header, so a JPEG renamed to.pngreportsJPEGwhatever the file is called. - Dimensions are compared with the template you pass in. That catches a 1024×768 Cybertruck file headed for a 1024×1024 Model Y, or a 4K render nobody resized. It can't tell two 1024×1024 templates apart: a wrap made for the 2020–2024 Model Y has the right size for a 2025 Model Y and still paints the wrong panels, because the two use different maps. That one needs eyes, see the last two sections.
- 1 MB is treated as 1,000,000 bytes. Tesla's README doesn't say whether it means 10⁶ or 2²⁰ bytes, so the script uses the smaller number.
-
The name rule is checked on the stem, the part before
.png.
Run it on a folder of candidates with the template you downloaded for your car:
python3 wrapcheck.py *.png --template template.png
What it caught on real files
I ran it against Tesla's own Model Y (2025+) Standard template and two of its example wraps, a full-page render saved at full size, a 1024×1024 crop of that render, and two deliberately broken copies:
OK Apocalypse.png 1024x1024 0.84 MB
OK Vintage_Stripes.png 1024x1024 0.08 MB
FAIL render_fullsize.png 1440x10288 4.11 MB 4.11 MB is over the 1 MB limit; 1440x10288 does not match the template (1024x1024)
OK render_1024_truecolor.png 1024x1024 0.45 MB
FAIL stripes_really_a_jpg.png 1024x1024 0.08 MB actually a JPEG, not a PNG
FAIL My wrap 🚗 (final v2).png 1024x1024 0.08 MB file name 'My wrap 🚗 (final v2)' breaks the naming rule
Tesla's examples pass, but Apocalypse.png, an RGBA image, already uses 84% of the 1 MB budget. The full-size render fails twice over: four times the weight limit and nowhere near the template's shape. The renamed JPEG and the emoji file name both look normal in Finder, and the car would skip both.
One result went the other way from what I expected: cropping that render to 1024×1024 gave a 0.45 MB truecolor PNG, comfortably under the limit. Screenshots and flat graphics compress well in PNG. Photographic textures, noise and soft gradients are what push a 1024×1024 file past 1 MB, and detailed full-colour wraps commonly land at 2–4 MB.
Each FAIL line lists every problem the script found in that file, so one run gives you the whole to-do list instead of one fix per trip to the car. Most of the fixes are quick: renaming takes seconds and re-exporting as a real PNG takes a minute. Weight is the one that usually needs a tool, which is what shrink() is for.
Shrinking without downscaling
When a wrap is too heavy, the obvious fix, resizing it, is the wrong one. The car maps pixels to the body by their position on the template, so a 1024×1024 wrap has to stay 1024×1024. What can change is how each pixel is stored.
shrink() converts the image to a 256-colour palette with Floyd–Steinberg dithering and saves it with PNG optimisation:
import wrapcheck
wrapcheck.shrink("Apocalypse.png", "Apocalypse_palette.png")
OK Apocalypse.png 1024x1024 0.84 MB
OK Apocalypse_palette.png 1024x1024 0.17 MB
That's 0.84 MB down to 0.17 MB in 0.04 seconds on a laptop, with the same dimensions and the same panel layout. The 1024×1024 render crop went from 0.45 MB to 0.05 MB the same way.
The colors argument is the knob. 256 is the most a PNG palette can hold, and lowering it shrinks the file further at the cost of more visible dithering, so start at 256 and step down only if a file is still over the limit. The quantiser is FASTOCTREE because it's one of the Pillow methods that accepts RGBA input, which keeps transparent areas transparent.
Palette quantisation has a cost: smooth gradients can band, and dithering adds a fine grain you can spot on a large flat panel such as the hood. Look at the result before you copy it over, preferably on a 3D model rather than on the flat template. That leads to the problem no file check can see.
The bug a file check can't catch: panel direction
A wrap PNG isn't a picture of a car. It's a UV map: every panel of the 3D model has been unfolded onto a flat sheet, and each one sits on that sheet in whatever orientation the unwrap left it.
That's why a wrap can pass every rule above and still look wrong on the car. A stripe painted left to right across the sheet runs along one panel and across the next. Text that reads correctly on the template can come out mirrored or upside down on a door. On Cybertruck the right side of the template is upside down, so artwork copied from the left side lands inverted unless you flip it.
The fix is to paint each panel in its own frame of reference: know which edge of each island faces the front of the car and which way is up, and orient the artwork per panel instead of across the whole sheet. That's the problem I work on in Tesla Wrap Generator: every generation gets a per-panel direction map derived from UV-mapped 3D models of each car, so stripes follow the body and lettering reads the right way up, including on the Cybertruck's upside-down side. (Disclosure: I build it.)
If you're painting by hand, there's a cheap test before the real design: put an arrow and the word FRONT on each panel, load the file once, and see where they end up.
If you'd rather not run Python
The same checks run in a browser. The free 3D preview on the Tesla Paint Shop wrap maker site takes any wrap PNG up to 10 MB, shows it on a 3D Tesla, checks it against Paint Shop's rules and fixes it in one click: resized to the template, trimmed to the panels, under 1 MB, with a safe file name. The file stays in your browser until you press Fix, and there's no signup.
Once a file passes, getting it onto the car takes a minute:
- Tesla app (v4.59.0 or later): Creations → Wrap → Upload.
-
USB: create a top-level folder named
Wrapson an exFAT or FAT32 drive with no update files on it, and copy up to ten PNGs into it. - In the car: Toybox → Paint Shop → Wraps tab.
If a wrap still doesn't appear, reboot the touchscreen and try one of Tesla's example files from the repository. If the example shows up and yours doesn't, the problem is the file, so run the preflight again.


Top comments (0)