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.
Metadata
Release files for appshots 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| appshots-0.1.0.tar.gz | 10.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| appshots-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 21.6 kB
Release files / appshots-0.1.0.tar.gz
| Download URL | appshots-0.1.0.tar.gz |
|---|---|
| Size | 10.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1c39477d074948c8f84ccac4d220b48ca180c37ad3fea3988ca495ab461cb9b2
|
|
BLAKE2b-256 checksum How to use checksums |
6a7030c5c72203e8e72c96a9ee6c95af8a4e4a6c9d0be0157ead19af618b95e1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 13, 2026.
Transparency logRelease files / appshots-0.1.0-py3-none-any.whl
| Download URL | appshots-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bd67e22dad37e8826e13db4525e61c406e31f4e9a8a004b9229339aea64a8364
|
|
BLAKE2b-256 checksum How to use checksums |
80f778aa772a64a045ea776ddacee2fb92c7307cdb29d5b2b172aa36d468600b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 13, 2026.
Transparency log