I just shipped four iOS apps to TestFlight. Each needs screenshots for the App Store.
iPhone screenshots are 1170×2532px (or 1284×2778px for Pro). iPad screenshots are 2048×2732px. Apple requires both. But I only have iPhone screenshots, and manually scaling them up in Figma for each app is... tedious.
So I wrote a script that takes iPhone screenshots, pads them to iPad dimensions, and uploads them to App Store Connect. Five lines of ImageMagick per image. Fully automated.
Here's the full script.
The problem
App Store Connect wants:
- iPhone: 1170×2532 (portrait)
- iPad Pro: 2048×2732 (portrait)
I have iPhone screenshots. Scaling them down to iPad proportions loses detail. Padding them preserves the image and fills the borders with a color.
The math:
- iPhone: 1170w × 2532h, aspect 0.46
- iPad: 2048w × 2732h, aspect 0.75
If I paste the iPhone screenshot (unscaled) into an iPad canvas and center it, I get ~439px of padding on left+right, ~100px on top+bottom.
ImageMagick's -resize, -gravity, and -extent do this in one command.
The one-liner
convert input.png \
-resize 1170x2532 \
-background white \
-gravity center \
-extent 2048x2732 \
output.png
Breaking it down:
-
-resize 1170x2532: Ensure the iPhone screenshot fits within these dimensions (preserve aspect, don't stretch) -
-background white: Color for the padding (you can use#f0f0f0ortransparentfor alpha) -
-gravity center: Center the image in the canvas (don't left-align it) -
-extent 2048x2732: Expand the canvas to iPad dimensions, centering the image
This gives you a 2048×2732 iPad screenshot with the iPhone image centered and white padding around it.
Full Python script
Here's the automation layer that processes a folder of iPhone screenshots and uploads them to App Store Connect:
import os
import subprocess
import json
from pathlib import Path
import requests
from jwt_handler import generate_jwt # assumes your JWT generation is elsewhere
class ScreenshotPadder:
"""Convert iPhone screenshots to iPad dimensions using ImageMagick."""
IPHONE_DIM = "1170x2532"
IPAD_DIM = "2048x2732"
BACKGROUND = "white"
def __init__(self, input_dir, output_dir):
self.input_dir = Path(input_dir)
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
def pad_screenshot(self, input_path, output_path, bg_color=None):
"""Convert iPhone screenshot to iPad dimensions with padding."""
bg = bg_color or self.BACKGROUND
cmd = [
"convert",
str(input_path),
"-resize", self.IPHONE_DIM,
"-background", bg,
"-gravity", "center",
"-extent", self.IPAD_DIM,
str(output_path)
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
print(f"✓ Padded {input_path.name} → {output_path.name}")
return True
except subprocess.CalledProcessError as e:
print(f"✗ Failed to pad {input_path.name}: {e.stderr}")
return False
def process_batch(self, bg_color=None):
"""Process all PNG files in input directory."""
png_files = sorted(self.input_dir.glob("*.png"))
if not png_files:
print(f"No PNG files found in {self.input_dir}")
return []
results = []
for png_file in png_files:
output_file = self.output_dir / f"{png_file.stem}_ipad.png"
if self.pad_screenshot(png_file, output_file, bg_color):
results.append(output_file)
print(f"\n✓ Processed {len(results)} screenshots")
return results
class ASCScreenshotUploader:
"""Upload padded screenshots to App Store Connect."""
def __init__(self, app_id):
self.app_id = app_id
self.jwt_token = generate_jwt()
self.headers = {
"Authorization": f"Bearer {self.jwt_token}",
"Content-Type": "application/json"
}
self.api_base = "https://api.appstoreconnect.apple.com/v1"
def get_version_screenshots(self, version_id):
"""Fetch current screenshots for an app version."""
url = f"{self.api_base}/appStoreVersions/{version_id}/appScreenshots"
resp = requests.get(url, headers=self.headers)
resp.raise_for_status()
return resp.json()["data"]
def upload_screenshot(self, version_id, screenshot_path, display_type="APP_IPAD_PRO"):
"""Upload a single screenshot to ASC."""
# Step 1: Create screenshot resource
create_payload = {
"data": {
"type": "appScreenshots",
"attributes": {
"fileSize": os.path.getsize(screenshot_path),
"fileName": Path(screenshot_path).name,
"sourceFileChecksums": None,
"displayType": display_type
},
"relationships": {
"appStoreVersion": {
"data": {"id": version_id, "type": "appStoreVersions"}
}
}
}
}
r1 = requests.post(
f"{self.api_base}/appScreenshots",
json=create_payload,
headers=self.headers
)
r1.raise_for_status()
screenshot_id = r1.json()["data"]["id"]
# Step 2: Get upload URL
r2_url = f"{self.api_base}/appScreenshots/{screenshot_id}"
r2 = requests.get(r2_url, headers=self.headers)
r2.raise_for_status()
upload_detail = r2.json()["data"]["attributes"].get("uploadOperations")
if not upload_detail:
print(f"✗ No upload URL for screenshot {screenshot_id}")
return False
# Step 3: Upload file
upload_url = upload_detail[0]["url"]
with open(screenshot_path, "rb") as f:
r3 = requests.put(upload_url, data=f, headers={"Content-Type": "image/png"})
r3.raise_for_status()
print(f"✓ Uploaded {Path(screenshot_path).name} as {display_type}")
return True
def upload_batch(self, version_id, screenshot_dir):
"""Upload all padded screenshots from a directory."""
screenshots = sorted(Path(screenshot_dir).glob("*_ipad.png"))
for screenshot in screenshots:
try:
self.upload_screenshot(version_id, screenshot, display_type="APP_IPAD_PRO")
except Exception as e:
print(f"✗ Error uploading {screenshot.name}: {e}")
continue
def main():
"""Full pipeline: pad iPhone screenshots → upload to ASC."""
# Configuration
INPUT_DIR = "./screenshots_iphone"
OUTPUT_DIR = "./screenshots_ipad"
APP_ID = os.getenv("ASC_APP_ID")
VERSION_ID = os.getenv("ASC_VERSION_ID")
# Step 1: Pad screenshots
padder = ScreenshotPadder(INPUT_DIR, OUTPUT_DIR)
padded_files = padder.process_batch(bg_color="white")
if not padded_files:
print("No screenshots to upload")
return
# Step 2: Upload to ASC
uploader = ASCScreenshotUploader(APP_ID)
uploader.upload_batch(VERSION_ID, OUTPUT_DIR)
print(f"\n✓ Pipeline complete: {len(padded_files)} screenshots processed and uploaded")
if __name__ == "__main__":
main()
How to use it
# 1. Install ImageMagick
brew install imagemagick # macOS
# or: apt-get install imagemagick # Linux
# 2. Prepare your iPhone screenshots in ./screenshots_iphone/
# Expected: 1170×2532px PNGs
# 3. Set your credentials
export ASC_APP_ID="1234567890"
export ASC_VERSION_ID="abcd1234"
export ASC_KEY_ID="your-key-id"
export ASC_ISSUER_ID="your-issuer-id"
# 4. Run the pipeline
python3 screenshot_pipeline.py
Output: padded iPad screenshots in ./screenshots_ipad/, plus uploaded to ASC.
Key parameters you can tweak
# Different background color
padder.process_batch(bg_color="#f5f5f5") # light gray
padder.process_batch(bg_color="transparent") # alpha channel
# Different display type for upload
uploader.upload_screenshot(version_id, path, display_type="APP_IPAD") # regular iPad
uploader.upload_screenshot(version_id, path, display_type="APP_IPHONE_6_5") # iPhone 14 Pro Max
Why this pattern scales
For one app, this saves 20 minutes of Figma work. For four apps, with 4–5 screenshots each, it's an hour of tedious clicking gone.
The script is:
- Deterministic: Same input → same output, every time
- Batchable: Process all 20 screenshots in <30 seconds
-
Recoverable: Failed uploads can retry from the
output_dir - Auditable: Logs each screenshot as it processes
I added this to my app release checklist. Now, 2 hours before submission, I copy the iPhone screenshots to a folder, run the script, and move on.
The gotcha: check your ImageMagick policy
ImageMagick ships with a policy.xml that may restrict file sizes or formats. If you get "attempt to perform an operation not allowed by the security policy", edit /etc/ImageMagick-6/policy.xml (or wherever it is on your system) and comment out the restrictive PDF/PS rules.
<!-- Comment this out if it's blocking you -->
<!-- <policy domain="coder" rights="none" pattern="PDF" /> -->
Full script is in my App Store tooling repo.
If you're managing screenshots for multiple iOS apps, drop a comment — curious how other devs handle the app store asset sprawl.
References: ImageMagick convert documentation · ImageMagick GitHub repository
Top comments (0)