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>
This commit is contained in:
Ayushya Amitabh
2026-10-07 22:58:49 -04:00
co-authored by Claude Sonnet 5.5
parent 5837464581
commit 660d0bc869
25 changed files with 261 additions and 1 deletions
@@ -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 [