Store assets: App Store header, search results, iPhone 6.3 and foldable sizes
Check PR / check (pull_request) Successful in 59s
Adds ios/header, ios/search-results, ios/iphone-6.3in and ios/iphone-fold-* to compose.py and the README upload table. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@@ -34,8 +34,18 @@ Schibsted Grotesk / Instrument Sans), flat shapes, alternating light and dark.
|
||||
| Google Play 7-inch tablet, landscape | `android/tablet-7in-landscape/` | 1920 × 1200 |
|
||||
| Google Play 10-inch tablet, landscape | `android/tablet-10in-landscape/` | 2560 × 1600 |
|
||||
| App Store iPad screenshots, landscape (13" slot) | `ios/ipad-landscape/` | 2752 × 2064 |
|
||||
| App Store iPhone 6.1" / 6.3" screenshots | `ios/iphone-6.3in/` | 1206 × 2622 |
|
||||
| App Store iPhone Duo (foldable), portrait | `ios/iphone-fold-portrait/` | 2007 × 2853 |
|
||||
| App Store iPhone Duo (foldable), landscape | `ios/iphone-fold-landscape/` | 2853 × 2007 |
|
||||
| App Store header | `ios/header/` | 5244 × 2950, 3840 × 1646 |
|
||||
| App Store search results | `ios/search-results/` | 5244 × 2950, 3840 × 2560, 1920 × 1280 |
|
||||
| iOS icon reference | `ios/icon-1024.png` | 1024 × 1024 |
|
||||
|
||||
The iPad 13" slot (2064 × 2752 and 2752 × 2064) is covered by `ios/ipad/` and
|
||||
`ios/ipad-landscape/`. The header and search-results banners are one brand
|
||||
design (name, tagline, iPad and iPhone) rendered at each size. App previews
|
||||
(video) are not produced here.
|
||||
|
||||
Upload in numeric order. The App Store uses the icon embedded in the uploaded
|
||||
build; `ios/icon-1024.png` is only a reference copy. Screenshots, the feature
|
||||
graphic and the iOS icon are opaque RGB PNGs; the Play icon is 32-bit RGBA.
|
||||
|
||||
|
After Width: | Height: | Size: 431 KiB |
|
After Width: | Height: | Size: 684 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 350 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 397 KiB |
|
After Width: | Height: | Size: 333 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 392 KiB |
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 489 KiB |
|
After Width: | Height: | Size: 684 KiB |
@@ -359,6 +359,186 @@
|
||||
"mode": "RGB",
|
||||
"story": "external"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-6.3in/01-accounts.png",
|
||||
"size": [
|
||||
1206,
|
||||
2622
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "accounts"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-6.3in/02-locks.png",
|
||||
"size": [
|
||||
1206,
|
||||
2622
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "locks"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-6.3in/03-sync.png",
|
||||
"size": [
|
||||
1206,
|
||||
2622
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "sync"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-6.3in/04-sharing.png",
|
||||
"size": [
|
||||
1206,
|
||||
2622
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "sharing"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-6.3in/05-external.png",
|
||||
"size": [
|
||||
1206,
|
||||
2622
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "external"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-fold-portrait/01-accounts.png",
|
||||
"size": [
|
||||
2007,
|
||||
2853
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "accounts"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-fold-portrait/02-locks.png",
|
||||
"size": [
|
||||
2007,
|
||||
2853
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "locks"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-fold-portrait/03-sync.png",
|
||||
"size": [
|
||||
2007,
|
||||
2853
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "sync"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-fold-portrait/04-sharing.png",
|
||||
"size": [
|
||||
2007,
|
||||
2853
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "sharing"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-fold-portrait/05-external.png",
|
||||
"size": [
|
||||
2007,
|
||||
2853
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "external"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-fold-landscape/01-accounts.png",
|
||||
"size": [
|
||||
2853,
|
||||
2007
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "accounts"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-fold-landscape/02-locks.png",
|
||||
"size": [
|
||||
2853,
|
||||
2007
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "locks"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-fold-landscape/03-sync.png",
|
||||
"size": [
|
||||
2853,
|
||||
2007
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "sync"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-fold-landscape/04-sharing.png",
|
||||
"size": [
|
||||
2853,
|
||||
2007
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "sharing"
|
||||
},
|
||||
{
|
||||
"file": "ios/iphone-fold-landscape/05-external.png",
|
||||
"size": [
|
||||
2853,
|
||||
2007
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "external"
|
||||
},
|
||||
{
|
||||
"file": "ios/header/header-5244x2950.png",
|
||||
"size": [
|
||||
5244,
|
||||
2950
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "banner"
|
||||
},
|
||||
{
|
||||
"file": "ios/header/header-3840x1646.png",
|
||||
"size": [
|
||||
3840,
|
||||
1646
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "banner"
|
||||
},
|
||||
{
|
||||
"file": "ios/search-results/search-5244x2950.png",
|
||||
"size": [
|
||||
5244,
|
||||
2950
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "banner"
|
||||
},
|
||||
{
|
||||
"file": "ios/search-results/search-3840x2560.png",
|
||||
"size": [
|
||||
3840,
|
||||
2560
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "banner"
|
||||
},
|
||||
{
|
||||
"file": "ios/search-results/search-1920x1280.png",
|
||||
"size": [
|
||||
1920,
|
||||
1280
|
||||
],
|
||||
"mode": "RGB",
|
||||
"story": "banner"
|
||||
},
|
||||
{
|
||||
"file": "android/icon-512.png",
|
||||
"size": [
|
||||
|
||||
@@ -60,6 +60,19 @@ TARGETS = [
|
||||
('android/tablet-7in-landscape', 'atabletl', (1920, 1200), 'landscape'),
|
||||
('android/tablet-10in-landscape', 'atabletl', (2560, 1600), 'landscape'),
|
||||
('ios/ipad-landscape', 'ipadl', (2752, 2064), 'landscape'),
|
||||
# Newer App Store slots. iPad 13" (2064x2752, 2752x2064) is the two above.
|
||||
('ios/iphone-6.3in', 'iphone', (1206, 2622), 'phone'),
|
||||
('ios/iphone-fold-portrait', 'ipad', (2007, 2853), 'tablet'),
|
||||
('ios/iphone-fold-landscape', 'ipadl', (2853, 2007), 'landscape'),
|
||||
]
|
||||
|
||||
# App Store product-page banners: (folder, file name, size).
|
||||
HEROES = [
|
||||
('ios/header', 'header-5244x2950.png', (5244, 2950)),
|
||||
('ios/header', 'header-3840x1646.png', (3840, 1646)),
|
||||
('ios/search-results', 'search-5244x2950.png', (5244, 2950)),
|
||||
('ios/search-results', 'search-3840x2560.png', (3840, 2560)),
|
||||
('ios/search-results', 'search-1920x1280.png', (1920, 1280)),
|
||||
]
|
||||
|
||||
|
||||
@@ -215,6 +228,56 @@ def story_canvas(story, size, kind, device):
|
||||
return im
|
||||
|
||||
|
||||
def hero_canvas(size):
|
||||
"""Brand banner: name and tagline left, iPad (landscape) and iPhone right."""
|
||||
t = THEMES['light']
|
||||
w, h = size
|
||||
im = background(size, t)
|
||||
d = ImageDraw.Draw(im)
|
||||
margin = round(min(w * .06, h * .13))
|
||||
colw = round(w * (.30 if w / h > 2 else .34))
|
||||
# Text column, vertically centered as a block.
|
||||
icon_px = round(h * .105)
|
||||
head = round(h * .092)
|
||||
sub_size = round(h * .034)
|
||||
block = icon_px + round(h * .05) + round(head * 2.35) + round(sub_size * 4.4)
|
||||
y = (h - block) // 2
|
||||
icon = Image.open(ROOT / 'assets/icon/app_icon.png').convert('RGBA').resize((icon_px, icon_px), Image.Resampling.LANCZOS)
|
||||
im.paste(icon, (margin, y), rounded_mask((icon_px, icon_px), round(icon_px * .24)))
|
||||
spaced(d, (margin + icon_px + round(h * .025), y + icon_px // 2 - round(h * .02)), 'NOOCLOUD',
|
||||
font(round(h * .034), 'Bold', 'InstrumentSans'), t['accent_text'], h * .006)
|
||||
y += icon_px + round(h * .05)
|
||||
for index, line in enumerate(('The Nextcloud app', 'that just works.')):
|
||||
fnt, _ = fit(d, line, font, head, colw)
|
||||
d.text((margin, y), line, font=fnt, fill=t['accent_text'] if index else t['fg'])
|
||||
y += round(head * 1.12)
|
||||
y += round(head * .2)
|
||||
fnt = body_font(sub_size, 'Regular')
|
||||
chars = max(16, int(colw / (sub_size * .52)))
|
||||
for line in textwrap.wrap('Multiple accounts, locks, offline sync, sharing and external storage.', chars):
|
||||
d.text((margin, y), line, font=fnt, fill=t['fg2'])
|
||||
y += round(sub_size * 1.4)
|
||||
# Devices: iPad landscape with the iPhone overlapping its bottom-right corner.
|
||||
x0 = margin + colw + round(w * .03)
|
||||
region_w = w - x0 - margin
|
||||
ipad_h = round(h * .72)
|
||||
ipad_w = round(ipad_h * 4 / 3)
|
||||
phone_h = round(ipad_h * 1.02)
|
||||
phone_w = round(phone_h * 0.4614)
|
||||
total = ipad_w + round(phone_w * .7)
|
||||
if total > region_w:
|
||||
k = region_w / total
|
||||
ipad_h, ipad_w, phone_h, phone_w = (round(v * k) for v in (ipad_h, ipad_w, phone_h, phone_w))
|
||||
total = ipad_w + round(phone_w * .7)
|
||||
x = x0 + (region_w - total) // 2
|
||||
top = (h - ipad_h) // 2 - round(h * .03)
|
||||
paste_panel(im, OUT / 'source/ipadl/external.png', (x, top, ipad_w, ipad_h), round(h * .028), t)
|
||||
paste_panel(im, OUT / 'source/iphone/offline.png',
|
||||
(x + ipad_w - round(phone_w * .3), top + ipad_h - phone_h + round(h * .07), phone_w, phone_h),
|
||||
round(h * .035), t)
|
||||
return im
|
||||
|
||||
|
||||
def check(path, size, mode):
|
||||
image = Image.open(path)
|
||||
assert image.size == tuple(size) and image.mode == mode, path
|
||||
@@ -234,6 +297,13 @@ for folder, device, size, kind in TARGETS:
|
||||
manifest.append({'file': str(path.relative_to(OUT)), 'size': list(size), 'mode': 'RGB',
|
||||
'story': story['key']})
|
||||
|
||||
for folder, name, size in HEROES:
|
||||
dest = OUT / folder
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
path = dest / name
|
||||
hero_canvas(size).save(path, optimize=True)
|
||||
manifest.append({'file': str(path.relative_to(OUT)), 'size': list(size), 'mode': 'RGB', 'story': 'banner'})
|
||||
|
||||
# Store icons: Play wants a 512 px PNG, App Store Connect takes the icon from
|
||||
# the uploaded build (this 1024 px copy is only a reference).
|
||||
for platform, px, source in [
|
||||
|
||||