Compose App Store marketing screenshots from raw captures, backgrounds, and localized captions
Project description
appshots
Compose App Store marketing screenshots from raw captures, background images,
and localized captions — declaratively, from one config.json.
| Raw capture | Composed screenshot |
|---|---|
One Python file, one dependency (Pillow), one CLI. Change a caption or swap a screenshot, re-run one command, and every device size and every language regenerates — no Figma, no per-locale hand editing.
- Localized captions —
"caption": {"en": ..., "ru": ...}renders a full per-locale screenshot set, laid out the wayfastlane deliveruploads it - Device presets —
"iphone-6.9"instead of memorizing pixel sizes; warns when a custom size isn't one App Store Connect accepts - Auto-fitting text — long translations shrink to fit instead of overflowing
- Glyph safety — a Latin-only font with a Cyrillic caption fails loudly with the missing characters listed, instead of rendering an empty title
- Composition — background cover, vignette, device-corner rounding and
bezel, drop shadows, caption stroke/shadow, pixel-art-safe
nearestscaling
Install
pipx install git+https://github.com/DmitryKhryukin/appshots
# or run straight from a checkout:
pip install Pillow && python appshots.py --config CONFIG.json
Quick start
The repo ships a self-contained example (fake lorem-ipsum app, generated assets, EN + RU captions):
appshots --config example/config.json
open example/output/en/iphone-6.9-feed.png
Per-app folder layout
Each app keeps its own config and assets in its own folder (typically inside the app's repo):
my-app-screenshots/
├── config.json
├── fonts/MyFont.ttf
├── backgrounds/
│ ├── feature1.png
│ └── feature2.png
├── input/
│ ├── feature1.png
│ └── feature2.png
└── output/ # tool writes here
appshots --config /path/to/my-app-screenshots/config.json
Render a single screenshot, device, or locale:
appshots --config CONFIG.json --filter feature1
appshots --config CONFIG.json --device iphone-6.9
appshots --config CONFIG.json --locale ru
Config schema
{
"devices": ["iphone-6.9", "ipad-13"],
"output_dir": "./output",
"missing_background_color": "#1a1a1a",
"font": {
"path": "./fonts/MyFont.ttf",
"size": 140,
"color": "#FFFFFFFF",
"shadow": { "color": "#000000AA", "offset": [4, 6], "blur": 0 }
},
"layout": {
"screenshot_scale": 0.78,
"screenshot_max_height": 0.78,
"screenshot_offset_y": 0.04,
"screenshot_shadow": { "color": "#00000080", "offset": [0, 20], "blur": 30 },
"caption_position_y": 0.10,
"caption_max_width": 0.92,
"background_resample": "nearest",
"screenshot_resample": "nearest"
},
"screenshots": [
{
"id": "feature1",
"screenshot": "./input/feature1.png",
"background": "./backgrounds/feature1.png",
"caption": "DEFEND YOUR CASTLE!"
}
]
}
Devices
A device is a preset id or a custom {id, width, height} dict:
| Preset | Size (portrait) | Add -landscape for |
|---|---|---|
iphone-6.9 |
1320 × 2868 | 2868 × 1320 |
iphone-6.5 |
1284 × 2778 | 2778 × 1284 |
ipad-13 |
2064 × 2752 | 2752 × 2064 |
Custom dicts still work ({ "id": "my-device", "width": 2778, "height": 1284 });
a warning is printed when the size isn't one App Store Connect accepts.
Per-device screenshots
When iPhone and iPad screenshots differ, pass a dict keyed by device id:
{
"screenshot": {
"iphone-6.9": "./input/iphone/feature1.png",
"ipad-13": "./input/ipad/feature1.png"
}
}
Localized captions
A caption can be a plain string (same text for every locale) or a dict keyed by locale:
{
"caption": {
"en": "DEFEND YOUR CASTLE!",
"ru": "ЗАЩИТИ СВОЙ ЗАМОК!",
"de": "VERTEIDIGE DEINE BURG!"
}
}
As soon as any caption is a dict, output goes to per-locale subfolders —
output/{locale}/{device.id}-{screenshot.id}.png — the layout fastlane deliver uploads from. Locales are the union of keys across all captions;
a dict caption missing one of those locales fails that render loudly.
String captions still work per locale (rendered identically into each folder).
Long translations are handled automatically: the font size shrinks until the
caption fits layout.caption_max_width (fraction of canvas width, default 0.92).
If the font lacks glyphs for a locale's script (e.g. a Latin-only font with a
Cyrillic caption), the render fails with the list of missing characters instead
of silently drawing an empty title. Fix it with a per-locale font — font.path
as a dict with a default fallback:
{
"font": {
"path": {
"default": "./fonts/PatrickHand-Regular.ttf",
"ru": "./fonts/Neucha-Regular.ttf"
},
"size": 140
}
}
Notes
- All paths in
config.jsonare relative to the config file's directory (or absolute). - Resample defaults to
nearest— preserves pixel art. Uselanczosfor smooth photographic backgrounds. - If a background file is missing, the tool falls back to a solid color so layout work can continue.
Development
python tests/smoke.py # render the example, verify output
python example/make_fake_screenshot.py # regenerate the example's fake assets
License
MIT. The bundled example font (Neucha)
is licensed under the SIL Open Font License — see example/fonts/OFL.txt.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file appshots-0.1.0.tar.gz.
File metadata
- Download URL: appshots-0.1.0.tar.gz
- Upload date:
- Size: 10.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1c39477d074948c8f84ccac4d220b48ca180c37ad3fea3988ca495ab461cb9b2
|
|
| MD5 |
b3c452b063416d9298862a85bf353ae6
|
|
| BLAKE2b-256 |
6a7030c5c72203e8e72c96a9ee6c95af8a4e4a6c9d0be0157ead19af618b95e1
|
Provenance
The following attestation bundles were made for appshots-0.1.0.tar.gz:
Publisher:
publish.yml on DmitryKhryukin/appshots
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
appshots-0.1.0.tar.gz -
Subject digest:
1c39477d074948c8f84ccac4d220b48ca180c37ad3fea3988ca495ab461cb9b2 - Sigstore transparency entry: 2164321478
- Sigstore integration time:
-
Permalink:
DmitryKhryukin/appshots@695a9735618944ee7ceedae58a54c7942fcd79cd -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/DmitryKhryukin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@695a9735618944ee7ceedae58a54c7942fcd79cd -
Trigger Event:
push
-
Statement type:
File details
Details for the file appshots-0.1.0-py3-none-any.whl.
File metadata
- Download URL: appshots-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd67e22dad37e8826e13db4525e61c406e31f4e9a8a004b9229339aea64a8364
|
|
| MD5 |
d2e4b04117131ed269418468bdb95a0f
|
|
| BLAKE2b-256 |
80f778aa772a64a045ea776ddacee2fb92c7307cdb29d5b2b172aa36d468600b
|
Provenance
The following attestation bundles were made for appshots-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on DmitryKhryukin/appshots
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
appshots-0.1.0-py3-none-any.whl -
Subject digest:
bd67e22dad37e8826e13db4525e61c406e31f4e9a8a004b9229339aea64a8364 - Sigstore transparency entry: 2164321488
- Sigstore integration time:
-
Permalink:
DmitryKhryukin/appshots@695a9735618944ee7ceedae58a54c7942fcd79cd -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/DmitryKhryukin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@695a9735618944ee7ceedae58a54c7942fcd79cd -
Trigger Event:
push
-
Statement type: