DEV Community

孫昊
孫昊

Posted on

Auto-padding iPhone screenshots to iPad dimensions with ImageMagick (full Python script)

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
Enter fullscreen mode Exit fullscreen mode

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 #f0f0f0 or transparent for 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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" /> -->
Enter fullscreen mode Exit fullscreen mode

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)