Для версии 1.1 дневника Lanternly мне нужна была картинка в два новых места App Store Connect: шапку страницы продукта и результаты поиска. Нового дизайна под неё не было, переснимать приложение тоже не хотелось. Зато были опубликованные скриншоты iPhone на английском и русском. Из них я и собрал один файл 5244 × 2950, который подходит в оба места.
Размеры, форматы и правила Apple я уже разбирал в статье про Creative Assets, здесь их повторять не буду. Эта статья про сборку и про ошибку, которую я нашёл в собственной обложке уже после загрузки.
Почему из скриншотов#
Скриншоты прошли проверку и точно показывают то, что есть в приложении. Если взять из них экраны, обложка не пообещает лишнего. И её не надо отдельно рисовать на каждом языке: экраны уже локализованы, переводить остаётся только заголовок и одну строку под ним.
Композиция простая: заголовок и подзаголовок сверху по центру, под ними три телефона («Сегодня» в центре, чат с Луной и календарь по бокам), а над центральным телефоном маскот Луна. Ничего, кроме iPhone, в кадре нет, и это принципиально.
С этим связана история. Первая версия того же скрипта делала и кадры для iPhone Duo: брала скриншоты iPad и обрезала их под размер Duo. Так нельзя. В слоте устройства должен быть снимок с этого устройства, иначе скриншот показывает не то, что человек получит. Я удалил эти наборы из App Store Connect. Слот Duo пустой, пока не появится возможность снять кадры в симуляторе Duo.
Как собирается файл#
Скрипт на Python делает три шага.
Вырезает телефоны из скриншотов. На артборде App Store телефон стоит в тёмной рамке на светлом фоне. Скрипт ищет первую строку, где тёмных пикселей заметно много: это верх рамки. По этой строке находит левый и правый край и вырезает телефон до низа артборда. Скруглённые углы закрывает маской с радиусом 12,5 % ширины, чтобы за ними был прозрачный фон, а не кусок старого артборда.
Собирает HTML-страницу. Фон, мягкое пятно света, три телефона с тенью и поворотом на ±6°, заголовок шрифтом Lora и строка Inter — те же шрифты, что в приложении. Луна — векторный SVG из исходников стикеров.
Рендерит её в headless Chrome. Страница свёрстана в 2622 × 1475 CSS-пикселей и снимается с коэффициентом 2:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--headless=new --hide-scrollbars --allow-file-access-from-files \
--virtual-time-budget=3000 --force-device-scale-factor=2 \
--window-size=2622,1475 --screenshot=header-en.png \
file:///path/to/_header-en.htmlНа выходе ровно 5244 × 2950. --virtual-time-budget даёт шрифтам и SVG загрузиться до снимка, иначе на картинке окажется запасной шрифт.
Важная мелочь: PNG должен быть без альфа-канала, Apple не принимает прозрачность. Скриншот Chrome при непрозрачном фоне страницы альфу не содержит, но после любой пост-обработки я пересохраняю файл в RGB.
Ошибка, которую я нашёл после загрузки#
Про обрезку Apple пишет только общими словами:
Be sure your focal point artwork is within the center of your composition to prevent any unwanted clipping.
Точной геометрии нет. Если исходить из того, что универсальный ассет 16:9 режется по центру, получается так:
| Где | Соотношение | Что остаётся от 5244 × 2950 | Что срезается |
|---|---|---|---|
| Шапка | 21:9 | вся ширина, 2247 px высоты | по 351 px сверху и снизу |
| Поиск | 3:2 | вся высота, 4425 px ширины | по 410 px слева и справа |
Я наложил эти две рамки на свою обложку и увидел, что заголовок начинается на 240-м пикселе сверху. В шапке 21:9 верхняя граница проходит на 351-м. Если обрезка центральная, заголовок «A journal that stays yours» режется пополам.
Первую версию я успел загрузить в App Store Connect раньше, чем проверил. На ревью она не ушла: версия 1.1 ещё не отправлена.
Проверку теперь делаю до загрузки. Вот скрипт, который рисует обе рамки поверх обложки и сохраняет уменьшенное превью:
#!/usr/bin/env python3
"""Рамки обрезки 21:9 и 3:2 на ассете 16:9: python3 crop_check.py header.png preview.png"""
import sys
from PIL import Image, ImageDraw
src, preview = sys.argv[1], sys.argv[2]
im = Image.open(src).convert("RGB")
W, H = im.size
d = ImageDraw.Draw(im)
h21 = round(W * 9 / 21) # шапка: вся ширина
top = (H - h21) // 2
d.rectangle((0, top, W - 1, top + h21), outline=(255, 0, 0), width=14)
w32 = round(H * 3 / 2) # поиск: вся высота
left = (W - w32) // 2
d.rectangle((left, 0, left + w32, H - 1), outline=(0, 0, 255), width=14)
im.resize((W // 4, H // 4)).save(preview)
print(f"21:9 y {top}..{top + h21}, 3:2 x {left}..{left + w32}")Красная рамка — шапка, синяя — поиск. Всё, что должно читаться, обязано попасть в пересечение двух рамок.
Как исправил без пересборки#
Исходные артборды к этому моменту я уже удалил, поэтому собирать обложку заново было не из чего. Помогло то, что верх композиции — пустой фон. Я сдвинул всю картинку на 260 пикселей вниз, а освободившуюся полосу сверху заполнил исходными первыми 260 строками того же фона:
from PIL import Image
im = Image.open("header-en.png").convert("RGB")
W, H = im.size
D = 260
out = Image.new("RGB", (W, H))
out.paste(im.crop((0, 0, W, D)), (0, 0)) # полоса фона сверху
out.paste(im.crop((0, 0, W, H - D)), (0, D)) # вся композиция ниже на D
out.save("header-en-v2.png", optimize=True)Заголовок теперь начинается на 500-м пикселе, внутри обеих рамок. Снизу уходит 260 пикселей нижних частей телефонов, но экраны и так продолжаются за край кадра, так что это ничего не ломает. Шов на 260-й строке не виден: там мягкий градиент без деталей.
Такой трюк работает, только если у края, с которого вы сдвигаете, однородный фон. Будь сверху узор или фото, пришлось бы собирать заново.
Чего я не знаю#
- Как именно Apple режет ассет. Центральная обрезка — моё предположение, подкреплённое фразой про «center of your composition». Точную картину покажет только «Предпросмотр» в App Store Connect, и проверять в нём нужно каждую локаль: длина заголовка на разных языках разная.
- Как ассет выглядит на iPad, Mac и Vision. Предпросмотр показывает iPhone и iPad. На остальных устройствах я его не видел.
- Будет ли обложка работать лучше скриншотов в выдаче. Это покажет только статистика после выхода версии.
Если делаете похожую обложку, наложите рамки 21:9 и 3:2 до первой загрузки. Это занимает минуту, а мне бы сэкономило лишний круг. Как я переводил остальные поля карточки на 39 языков, рассказано в отдельной статье.



