Guides
Preparing a screenshot for upload
Accepted formats, the size cap, and the 2000 pixel long edge rule, checked in the code, plus the order of the checks and where your image lands.
Updated 27 August 2026
Three formats are accepted
PNG, JPEG, and WebP. The filter the file picker uses is those same three, and the upload validation lets through those same three. Checking the two against each other confirms that what you can pick in the dialog matches what actually passes. Screenshot uploads and background image uploads both use the same three.
A format outside the list, such as HEIC or SVG, is filtered out by the picker, and dragging one in is stopped by the check.
The size cap is 25MiB
The constant is 25 times 1024 times 1024, which is exactly 26,214,400 bytes. A file over that is turned back without any attempt to decode it.
| Item | Value |
|---|---|
| Accepted formats | PNG, JPEG, WebP |
| Size cap | 26,214,400 bytes (25MiB) |
| As written in the message | 26MB |
| Working bitmap long edge cap | 2000 pixels |
The checks run in this order
The order has a reason. Looking at size first avoids bogging the tab down decoding a large file.
| Order | Check | If it does not pass |
|---|---|---|
| 1 | Is the file size at or under the cap | Ends with a size message |
| 2 | Is the file type on the accepted list | Ends with a format message |
| 3 | Can the browser decode the image | Ends with a message to try another file |
| 4 | Is the long edge at or under 2000 pixels | Moves on to the downscale step in the next section |
Whichever step stops it, the screen shows a short message and the edit state is left as it was. Even on failure, any bitmap created along the way is closed to free the memory.
Work happens at a 2000 pixel long edge
A long edge over 2000 pixels is scaled down with the ratio preserved. The notable part is when the scaling happens. Rather than expanding the original into memory and shrinking it afterwards, the code calls decoding again and passes the target size along with it. That keeps the large intermediate bitmap out of memory, and the resize quality is set to high.
The arithmetic is plain. The ratio is 2000 divided by the long edge, width and height are each multiplied by that ratio and rounded, and neither side is allowed below 1 pixel.
| Uploaded size | Long edge | Working size |
|---|---|---|
| 1920 × 1080 | 1920 | 1920 × 1080 (unchanged) |
| 1170 × 2532 | 2532 | 924 × 2000 |
| 1290 × 2796 | 2796 | 923 × 2000 |
| 2048 × 1536 | 2048 | 2000 × 1500 |
| 3840 × 2160 | 3840 | 2000 × 1125 |
Once the downscale is done, the original bitmap is closed. That stops the reduced copy and the original sitting in memory together and doubling the cost.
2000 pixels is not the output resolution
This is where the common mix up happens. 2000 pixels is the cap on the working bitmap you hold while editing, not the size of the file you export. Output size is the canvas size of the format you picked multiplied by the export scale.
The two are related, though. However large the original was, the working bitmap has already been reduced to 2000 pixels, so raising the scale does not bring back detail that existed only in the original. How far raising the scale is worth it is covered separately in the article below.
Worth reading alongside
Which device does the image go into?
Drop it directly onto a particular device on the canvas and it goes into that device. Otherwise the target is picked in this order.
| Priority | Target |
|---|---|
| 1 | The device under the drop point (when dropped directly on the canvas) |
| 2 | The device that is selected |
| 3 | A device that is still empty |
| 4 | The first device |
Because of that order, in a format holding several devices you can pick the spot you want first and the image lands there. When you drop directly on the canvas, that device becomes selected so you can see which one it went to.
Uploading again onto a device that already holds an image reuses the existing identifier rather than making a new one. The store closes and replaces the old bitmap when a new one arrives under the same identifier, so swapping images repeatedly does not pile up bitmaps nobody uses.
Uploading with no device on the canvas leaves nowhere to put the image, so you get a message to add a device first.
The background image follows the same rules
An image used as a background goes through the same validation and downscale. The format and size limits are the same, and so is the long edge cap. A background is enlarged to cover the canvas, using the larger of the width and height ratios, so a background smaller than the canvas gets blurry by however much it was stretched.
How to prepare
- Pick PNG, JPEG, or WebP.
- Keep the file within 26,214,400 bytes.
- A long edge over 2000 pixels is allowed, but everything past it is dropped at the working stage.
- Crop an image meant for a device screen to the screen ratio and no letterboxing appears.
- When filling several devices, select the one you want first or drop directly onto it.
Every number on this page comes from this service's own implementation code. The evidence table at the bottom names the file and the constant behind each sentence.
What this article is based on
| Statement | Where it comes from |
|---|---|
| Accepted formats PNG, JPEG, and WebP | editorConstants.ACCEPTED_IMAGE_MIME, the accept attribute in panels/UploadPanel.tsx |
| The file picker filter and the validation list name the same three formats, confirmed by comparing the two | isAcceptedMime in engine/uploadImage.ts, which reads editorConstants.ACCEPTED_IMAGE_MIME, against the accept attribute in panels/UploadPanel.tsx and panels/BackgroundPanel.tsx. This is the result of comparing the two values, not a claim that one is derived from the other |
| Clipboard input skips the format check and goes on to decoding | the file instanceof File branch in engine/uploadImage.ts |
| The size cap of 26,214,400 bytes | editorConstants.MAX_UPLOAD_BYTES (25 * 1024 * 1024) |
| Why the message reads 26MB | Math.round(MAX_UPLOAD_BYTES / 1e6) in engine/uploadImage.ts |
| The order of the checks, size then format then decode then long edge | the order of the body of decodeAndDownscale in engine/uploadImage.ts |
| Intermediate bitmaps are cleaned up on failure | decoded.bitmap.close() in the catch branch of engine/ingestImage.ts |
| The 2000 pixel long edge cap | editorConstants.WORK_MAX_EDGE |
| Downscaling by passing a target size at the decode step, with quality set to high | createImageBitmap(probe, { resizeWidth, resizeHeight, resizeQuality: 'high' }) in engine/uploadImage.ts |
| The ratio formula and the 1 pixel floor | ratio and Math.max(1, Math.round(...)) in engine/uploadImage.ts |
| The original bitmap is closed after the downscale | probe.close() in engine/uploadImage.ts |
| A browser that cannot take a target size keeps the original | the resize failure catch branch in engine/uploadImage.ts |
| Output size is canvas size times scale | outW and outH in panels/ExportPanel.tsx, toCanvas({ pixelRatio }) in engine/exportImage.ts |
| The slot order, drop point then selection then empty slot then first node | the explicitValid branch and pickTargetId in engine/ingestImage.ts |
| A canvas drop switches the target device to selected | setSelection([targetId]) in engine/ingestImage.ts |
| Re-uploading into an existing slot reuses the identifier and replaces the old bitmap | targetNode?.imageId ?? makeImageId() in engine/ingestImage.ts, add in state/imageRegistry.ts |
| A message to add a device when there is none | the branch for a missing targetId in engine/ingestImage.ts |
| The background image uses the same validation and downscale | the decodeAndDownscale call in panels/BackgroundPanel.tsx |
| The background is enlarged to cover the canvas | Math.max(width / bitmap.width, height / bitmap.height) in canvas/BackgroundLayer.tsx |