diff --git a/docs/store/screenshots/android/01_home.png b/docs/store/screenshots/android/01_home.png new file mode 100644 index 0000000..c32aa97 Binary files /dev/null and b/docs/store/screenshots/android/01_home.png differ diff --git a/docs/store/screenshots/android/02_player.png b/docs/store/screenshots/android/02_player.png new file mode 100644 index 0000000..1981c4c Binary files /dev/null and b/docs/store/screenshots/android/02_player.png differ diff --git a/docs/store/screenshots/android/03_album.png b/docs/store/screenshots/android/03_album.png new file mode 100644 index 0000000..2430862 Binary files /dev/null and b/docs/store/screenshots/android/03_album.png differ diff --git a/docs/store/screenshots/android/04_explore.png b/docs/store/screenshots/android/04_explore.png new file mode 100644 index 0000000..29f328d Binary files /dev/null and b/docs/store/screenshots/android/04_explore.png differ diff --git a/docs/store/screenshots/android/05_sound.png b/docs/store/screenshots/android/05_sound.png new file mode 100644 index 0000000..524de19 Binary files /dev/null and b/docs/store/screenshots/android/05_sound.png differ diff --git a/docs/store/screenshots/ios/01_home.png b/docs/store/screenshots/ios/01_home.png new file mode 100644 index 0000000..4b39feb Binary files /dev/null and b/docs/store/screenshots/ios/01_home.png differ diff --git a/docs/store/screenshots/ios/02_player.png b/docs/store/screenshots/ios/02_player.png new file mode 100644 index 0000000..a2efb38 Binary files /dev/null and b/docs/store/screenshots/ios/02_player.png differ diff --git a/docs/store/screenshots/ios/03_album.png b/docs/store/screenshots/ios/03_album.png new file mode 100644 index 0000000..5bdf212 Binary files /dev/null and b/docs/store/screenshots/ios/03_album.png differ diff --git a/docs/store/screenshots/ios/04_explore.png b/docs/store/screenshots/ios/04_explore.png new file mode 100644 index 0000000..4193745 Binary files /dev/null and b/docs/store/screenshots/ios/04_explore.png differ diff --git a/tools/store_screenshots.py b/tools/store_screenshots.py new file mode 100644 index 0000000..cf03e28 --- /dev/null +++ b/tools/store_screenshots.py @@ -0,0 +1,111 @@ +"""Turn raw phone screenshots into captioned store screenshots. + + python tools/store_screenshots.py + +Writes docs/store/screenshots/android/*.png (1080x1920, Google Play) and +docs/store/screenshots/ios/*.png (1320x2868, App Store 6.9-inch). 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', (1320, 2868))): + (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()