Notes › Tech Notes › App Store Screenshots Export at 1242×2688: 8 Canvas Presets, Noise at 24.83 Times the Size, and a Phone Frame Cropped 134.93 px by Default

App Store Screenshots Export at 1242×2688: 8 Canvas Presets, Noise at 24.83 Times the Size, and a Phone Frame Cropped 134.93 px by Default

Eight hardcoded canvas presets export sizes that contradict the copy: 1242×2688 lands by default, not 1080×1920. The width and height boxes only apply in the custom preset, the default phone frame loses 134.93 px at its bottom edge, and the noise texture swells the file to 24.83 times its size.

Lay out the pixel budget across the eight presets

Last month I made store screenshots for an app, and the upload was bounced back with the reason "wrong dimensions". The page copy advertised 1080×1920, yet I touched nothing, clicked the export button, and got 1242×2688 at 110440 B. The arithmetic on this page has to be done by hand.

I clicked through all eight presets on the App Store Screenshot Maker page one by one. The eight sets of values are hardcoded on template lines 258–267, and the size column below records the exported bytes with the texture turned off and no screenshot uploaded:

PresetHardcoded W×Hvs the 6.5 in presetExport size
iphone65 (default)1242×26880110440 B
iphone551242×2208less 59616094202 B
harmonyos1080×2340less 81129692081 B
oppo·vivo·xiaomi1080×2400less 74649693456 B
googleplay·custom1080×1920less 126489678985 B

The very first screen already defaults to iphone65 (line 257), so the 1080×1920 promised in the copy belongs only to googleplay and custom. OPPO, vivo, and Xiaomi all sit at 1080×2400, and their three exports of 93456 B are byte-for-byte identical: the button text is the only difference among those three presets, the pixels are not.

Canvas area (px) 6.5 in 3338496 5.5 in 2742336 HarmonyOS 2527200 Android trio 2592000 Tied presets 2073600

Figure 1: the eight presets collapse to only 5 distinct canvas areas.

The label reads 2000 × 1000, yet the file is still 1242×2688

The width and height fields canvas-w and canvas-h look like an entry point for a custom size, but the reading path lives inside the currentPreset === custom branch on lines 434–435. On the 6.5 in preset, editing the two fields to 2000 and 1000 flips the label to 2000 × 1000 at once, yet the canvas stays 1242×2688: the finished PNG's IHDR still declares [1242,2688] at 112233 B. Only after switching to custom and feeding in the same two numbers does it export [2000,1000] at 72954 B. Among the eight presets, seven behave this way, so the edit moves nothing but the label and gives no warning at all.

An uploaded screenshot is never re-cropped: the source rectangle is fixed

Changing the canvas size does not re-crop an already uploaded screenshot. Take one 1080×1920 image and swap the canvas through 540×960, 2160×3840, 400×2000, and 1080×1200: the four source-rectangle arguments passed to drawImage stay frozen at 71.70731707317071, 0, 936.5853658536586, 1920, and only the destination rectangle follows the canvas. The device body's height is always 2.05 times its width (line 513), so the crop ratio is locked at 1/2.05 = 0.48780487804878053.

A 6000×4000 image carries 24,000,000 pixels, but after decoding only 1,119,518 of them land in the canvas, that is 4.66%, so uploading a bigger image does not add a single output pixel.

The phone frame: width ratio constant, height ratio drifting by device

The inner width ratio of the device body is set only by shot-phone-scale, always equals scale×0.85, and is independent of the model, so the value 70 gives exactly 59.5000%. The height ratio is not conserved at all: the 6.5 in preset shows an inner height of 56.3590% while the googleplay preset shows 68.6109%, so the same scale differs by 12.2519 percentage points, because the body height is derived from the width by repeated multiplication rather than measured on its own.

The overflow under default parameters matters even more. At 1080×1920 the device body's bottom edge reaches 2054.93, so 134.93 px are cut off; at 1242×2208 it overflows by 153.97 px; yet 1242×2688 keeps 143.63 px of margin. At least two of the eight presets push the phone frame clean past the canvas edge straight out of the box.

Canvas H Frame bottom 6.5 in 2544.37 spare 143.63 5.5 in 2361.97 over 153.97 Google 2054.93 over 134.93

Figure 2: the body's bottom edge against the canvas height under default parameters.

Preview and export: one draw path, diffPx=0

exportImage (lines 699–723) copies the whole #shot-canvas into a temporary canvas of the same size and then encodes it as PNG. Under the 6.5 in default state, comparing all 3338496 pixels point by point gives diffPx=0, so preview and output match on every channel. The two green drag-guide lines and the text-area and phone-area backing plates are burned into the finished image exactly as drawn, and the clear-the-handles step noted on lines 700/707/712 turns out to do nothing at all. The five files produced by exportAll behave the same way. This ledger only covers the canvas-to-PNG leg: the export neither submits anything nor triggers a repaint, so whether the canvas holds the latest text still depends on whether it had been rendered beforehand.

Texture is the largest hidden cost on this page

The same 1242×2688 canvas with no screenshot, toggling only the texture dropdown, moves none 110440 B to grid 115234 B (+4794), then dots 131365 B (+20925), then noise 2742315 B, which is 24.83 times larger. The noise option has to read the canvas back into memory and recolor each pixel, so the amount it changes equals the whole canvas pixel count.

Its randomness comes from Math.random() (line 673). For the 600×1000 group with a photo and a 234553 B baseline, the three texture steps add 1818, 4515, and 458584 B respectively, but clicking noise twice in a row yields 693137 B and 689989 B, two different sizes; pinning the value to 0.5 makes it byte-for-byte identical to none. That step is therefore unreproducible, and only the other three steps are idempotent, since turning the texture off and clicking three times gives a constant 659546 B.

A hard cap on canvas area: total silence past 268435456 px

The critical area reached by per-preset bisection is 268435456 px, which is exactly 16384×16384; in the same batch 16385 on a side draws nothing. The failure shape is the single most expensive boundary on this page: getContext still returns an object, canvas.width still holds the requested value, the render never throws, and the export receives an empty result, because no line of code anywhere checks the product of width and height.

Zoom and empty values

preview-zoom only changes CSS, not resolution (lines 438–441): from zoom 33 it is pinned by min(400/w, zoom) to 400×866, the backing canvas is always 1242×2688, yet the slider label keeps showing 100%, and both zoom levels export the identical file.

The clearing semantics of the two controls also disagree: a number field does not clamp, so -50 makes the label read "-50 × -50" while the canvas falls back to the default 300×150; a range slider clamps and re-centers instead, so clearing the text position yields 14 and clearing the phone position yields 48.

How to work with the stores when you actually publish is a separate matter. I keep that in another article, my older notes on App Store screenshots, and this one does not repeat it: here I only count the pixels and bytes on this single page.

Three follow-ups

Q: I changed the canvas width and height fields, why is the export still the old size?

Those two fields are read only under the custom preset (lines 434–435), so the other seven presets change the label and nothing else. The way to verify is to download the file and read the size declared in its header, not the number shown in the box.

Q: Why are the export images for the three Android models exactly identical?

All three presets hardcode the same 1080×2400, and the three PNGs are byte-for-byte identical; to pull them apart you have to switch to the custom preset and change the width and height yourself.

Q: Why does the finished image carry green handle lines and area labels?

Point-by-point comparison gives diffPx=0, so whatever sits on the canvas gets encoded, and those two lines are drawn onto the canvas every frame. To get a clean output you can only crop once more after exporting.

📎 Related articles

I Tested 16 Switch Combinations of an HTML Tag Stripper: It Reports 19 Tags but Deletes 148 Characters 2026-10-30 Linked Images Vanish When SVG Turns to PNG: A 594 Byte File Shows 3 Pictures in Preview and 1 in the Export 2026-10-30 Handing a Base64 string to someone else: 4 edits tried, 3 of them throw an error 2026-10-30 16 Keys In, 4 Columns Out: How json-to-excel Builds CSV Headers 2026-10-30 Untick Keep Comments and format-js Compresses a 24 Character URL Down to 13 Broken Characters 2026-10-30