112 lines
4.4 KiB
Python
112 lines
4.4 KiB
Python
"""Turn raw phone screenshots into captioned store screenshots.
|
|
|
|
python tools/store_screenshots.py <folder with 01_home.png ... from `adb exec-out screencap -p`>
|
|
|
|
Writes docs/store/screenshots/android/*.png (1080x1920, Google Play) and
|
|
docs/store/screenshots/ios/*.png (1206x2622, App Store iPhone 6.1/6.3-inch, the size App Store Connect asks for). The Android status and
|
|
navigation bars are cropped off, so the iOS set shows only the app. Screens marked
|
|
android_only are left out of the iOS set.
|
|
"""
|
|
import pathlib
|
|
import sys
|
|
|
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
OUT = ROOT / 'docs' / 'store' / 'screenshots'
|
|
FONT_BOLD = 'C:/Windows/Fonts/segoeuib.ttf'
|
|
FONT_SEMI = 'C:/Windows/Fonts/seguisb.ttf'
|
|
|
|
GROUND = (11, 26, 20)
|
|
EMBLEM = (31, 74, 46)
|
|
SUN = (245, 179, 53)
|
|
CREAM = (251, 247, 238)
|
|
MUTED = (220, 229, 223)
|
|
|
|
# file, headline, sub line, android_only
|
|
SCREENS = [
|
|
('01_home.png', 'Badaga songs from the Nilgiris', 'Free to listen, with no ads', False),
|
|
('02_player.png', 'The music of home, anywhere', 'Background play and lock-screen controls', False),
|
|
('03_album.png', 'Albums from Badaga singers', 'Download them and listen offline', False),
|
|
('04_explore.png', 'Songs for every occasion', 'Weddings, festivals, devotional and more', False),
|
|
('05_sound.png', 'Clear sound, even from old cassettes', 'Equalizer presets and volume boost', True),
|
|
]
|
|
|
|
# Phone is 1080x2340: status bar above, gesture/navigation bar below.
|
|
CROP = (0, 110, 1080, 2215)
|
|
|
|
|
|
def background(w, h):
|
|
img = Image.new('RGB', (w, h), GROUND)
|
|
glow = Image.new('RGB', (w, h), GROUND)
|
|
d = ImageDraw.Draw(glow)
|
|
d.ellipse((-w * 0.4, -h * 0.25, w * 1.4, h * 0.55), fill=EMBLEM)
|
|
return Image.blend(img, glow.filter(ImageFilter.GaussianBlur(w // 4)), 0.9)
|
|
|
|
|
|
def wrap(draw, text, font, max_w):
|
|
words, lines, line = text.split(), [], ''
|
|
for word in words:
|
|
test = f'{line} {word}'.strip()
|
|
if draw.textlength(test, font=font) <= max_w:
|
|
line = test
|
|
else:
|
|
lines.append(line)
|
|
line = word
|
|
return lines + [line]
|
|
|
|
|
|
def compose(shot, head, sub, w, h):
|
|
img = background(w, h)
|
|
d = ImageDraw.Draw(img)
|
|
margin = round(w * 0.08)
|
|
head_font = ImageFont.truetype(FONT_BOLD, round(w * 0.072))
|
|
sub_font = ImageFont.truetype(FONT_SEMI, round(w * 0.038))
|
|
|
|
y = round(h * 0.055)
|
|
d.rounded_rectangle((margin, y, margin + round(w * 0.09), y + round(w * 0.012)), radius=8, fill=SUN)
|
|
y += round(w * 0.05)
|
|
for line in wrap(d, head, head_font, w - 2 * margin):
|
|
d.text((margin, y), line, font=head_font, fill=CREAM)
|
|
y += round(head_font.size * 1.18)
|
|
y += round(w * 0.012)
|
|
d.text((margin, y), sub, font=sub_font, fill=MUTED)
|
|
y += round(sub_font.size * 2.8)
|
|
|
|
# Screenshot in a rounded phone-like frame, filling the rest of the height.
|
|
screen = shot.crop(CROP)
|
|
avail_h = h - y - round(h * 0.04)
|
|
scale = min(avail_h / screen.height, (w - 2 * margin) / screen.width)
|
|
sw, sh = round(screen.width * scale), round(screen.height * scale)
|
|
screen = screen.resize((sw, sh), Image.LANCZOS)
|
|
bezel = round(w * 0.014)
|
|
radius = round(w * 0.06)
|
|
x0 = (w - sw) // 2
|
|
shadow = Image.new('L', (w, h), 0)
|
|
ImageDraw.Draw(shadow).rounded_rectangle(
|
|
(x0 - bezel, y - bezel + 20, x0 + sw + bezel, y + sh + bezel + 20), radius=radius + bezel, fill=160)
|
|
img.paste((0, 0, 0), mask=shadow.filter(ImageFilter.GaussianBlur(30)))
|
|
d = ImageDraw.Draw(img)
|
|
d.rounded_rectangle((x0 - bezel, y - bezel, x0 + sw + bezel, y + sh + bezel),
|
|
radius=radius + bezel, fill=(5, 13, 9), outline=(42, 69, 55), width=3)
|
|
mask = Image.new('L', (sw, sh), 0)
|
|
ImageDraw.Draw(mask).rounded_rectangle((0, 0, sw, sh), radius=radius, fill=255)
|
|
img.paste(screen, (x0, y), mask)
|
|
return img
|
|
|
|
|
|
def main():
|
|
src = pathlib.Path(sys.argv[1])
|
|
for target, size in (('android', (1080, 1920)), ('ios', (1206, 2622))):
|
|
(OUT / target).mkdir(parents=True, exist_ok=True)
|
|
for name, head, sub, android_only in SCREENS:
|
|
if android_only and target == 'ios':
|
|
continue
|
|
shot = Image.open(src / name).convert('RGB')
|
|
compose(shot, head, sub, *size).save(OUT / target / name, optimize=True)
|
|
print(f'{target}: {sorted(p.name for p in (OUT / target).glob("*.png"))}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|