#!/usr/bin/env python3 """Lay out App Store artwork around untouched real-app screenshots. Requires Pillow.""" from pathlib import Path from PIL import Image, ImageDraw, ImageFont, ImageFilter import json, math, shutil ROOT = Path(__file__).resolve().parents[1] FONT = Path('/System/Library/Fonts/Supplemental') def font(size, bold=False): return ImageFont.truetype(str(FONT / ('Arial Bold.ttf' if bold else 'Arial.ttf')), round(size)) def backdrop(w, h, accent): small = Image.new('RGB', (220, max(1, round(h*220/w)))) pix = small.load() for y in range(small.height): for x in range(small.width): glow = math.exp(-(((x/small.width-.85)/.65)**2+((y/small.height-.32)/.35)**2)) pix[x,y] = tuple(round(base+glow*(a-base)*.45) for base,a in zip((5,9,25),accent)) return small.resize((w,h), Image.Resampling.BICUBIC) def rounded_paste(canvas, screen, box): x,y,w,h=map(round,box) radius=round(w*.065) shadow=Image.new('RGBA',canvas.size) ImageDraw.Draw(shadow).rounded_rectangle((x-8,y+12,x+w+8,y+h+20),radius,fill=(0,0,0,180)) shadow=shadow.filter(ImageFilter.GaussianBlur(max(12,w*.025))) canvas=Image.alpha_composite(canvas.convert('RGBA'),shadow) draw=ImageDraw.Draw(canvas) draw.rounded_rectangle((x-8,y-8,x+w+8,y+h+8),radius+8,fill=(92,106,149,255)) screen=screen.convert('RGB').resize((w,h),Image.Resampling.LANCZOS) mask=Image.new('L',(w,h));ImageDraw.Draw(mask).rounded_rectangle((0,0,w-1,h-1),radius,fill=255) canvas.paste(screen,(x,y),mask) return canvas.convert('RGB') SCENES=[ ('01-home','Your server.','At a glance.','Live speeds. Clear history. Total control.',(104,78,255)), ('02-library','Every transfer.','Beautifully organised.','Search, filter and manage your whole library.',(0,170,184)), ('03-details','Take control.','Keep the detail.','Pause, resume and inspect every transfer.',(60,113,255)), ('04-files','Choose what','matters.','Set file priorities from wherever you are.',(0,180,170)), ('05-settings','Your server.','Your settings.','Browse and adjust the options your server exposes.',(94,88,245)), ('06-add','Add a link.','Let your server work.','Magnet links and .torrent files. In one place.',(154,74,230)), ] icon=Image.open(ROOT/'icons/Tarnax-AppStore-1024.png').convert('RGB') for size in [512,256,180,128,64,32]: icon.resize((size,size),Image.Resampling.LANCZOS).save(ROOT/f'icons/Tarnax-{size}.png') manifest=[] for device in ['iphone-6.9','ipad-13']: for name,line1,line2,sub,accent in SCENES: src=ROOT/f'screenshots/raw/{device}/{name}.png' raw=Image.open(src).convert('RGB') w,h=raw.size canvas=backdrop(w,h,accent) d=ImageDraw.Draw(canvas) ipad=device=='ipad-13' pad=round(w*.075) logo=round(w*.045) canvas.paste(icon.resize((logo,logo),Image.Resampling.LANCZOS),(pad,round(h*.027))) d=ImageDraw.Draw(canvas) d.text((pad+logo+round(w*.015),round(h*.027)), 'Tarnax',font=font(w*.035,True),fill='white') title_size=w*(.068 if not ipad else .062) title_font=font(title_size,True) while max(d.textlength(line1,font=title_font),d.textlength(line2,font=title_font))>w-2*pad: title_size-=1;title_font=font(title_size,True) ty=round(h*.067) d.text((pad,ty),line1,font=title_font,fill='white') d.text((pad,ty+title_size*1.15),line2,font=title_font,fill=(170,185,255) if accent[2]>200 else (100,230,222)) sub_font=font(w*.025) while d.textlength(sub,font=sub_font)>w-2*pad:sub_font=font(sub_font.size-1) d.text((pad,ty+title_size*2.55),sub,font=sub_font,fill=(202,211,233)) sw=round(w*(.74 if ipad else .78));sh=round(sw*raw.height/raw.width) sy=round(h*(.225 if ipad else .185)) if sy+sh>h*.968: sh=round(h*.968-sy);sw=round(sh*raw.width/raw.height) canvas=rounded_paste(canvas,raw,((w-sw)//2,sy,sw,sh)) d=ImageDraw.Draw(canvas) footer='Requires your own qBittorrent server' ff=font(w*.018) d.text(((w-d.textlength(footer,font=ff))/2,h*.982),footer,font=ff,fill=(158,172,203),anchor='mm') dst=ROOT/f'screenshots/upload/{device}/{name}.png';canvas.save(dst,optimize=True) manifest.append({'file':str(dst.relative_to(ROOT)),'width':w,'height':h,'scene':name,'appearance':'dark' if name in ['01-home','04-files'] else 'light','source':str(src.relative_to(ROOT))}) # Press/social assets, deliberately separate from store-upload screenshots. for name,w,h in [('social-landscape',1200,630),('social-square',1080,1080),('press-header',2400,1200)]: canvas=backdrop(w,h,(110,70,245));d=ImageDraw.Draw(canvas) pad=round(w*.07);size=round(w*.15) canvas.paste(icon.resize((size,size),Image.Resampling.LANCZOS),(pad,round(h*.11))) d=ImageDraw.Draw(canvas) d.text((pad,round(h*.11)+size+round(h*.055)),'Your server.',font=font(w*.068,True),fill='white') d.text((pad,round(h*.11)+size+round(h*.055)+w*.078),'Your control.',font=font(w*.068,True),fill=(169,183,255)) d.text((pad,h*.86),'Tarnax ยท Remote for qBittorrent',font=font(w*.025),fill=(204,215,239)) if w/h>1.6: screen=Image.open(ROOT/'screenshots/raw/iphone-6.9/01-home.png') sh=round(h*.85);sw=round(sh*screen.width/screen.height) canvas=rounded_paste(canvas,screen,(w-sw-pad,h*.075,sw,sh)) canvas.save(ROOT/f'marketing/{name}.png',optimize=True) # Asset contact sheets for convenient visual QA. for device in ['iphone-6.9','ipad-13']: thumbs=[] for scene,*_ in SCENES: im=Image.open(ROOT/f'screenshots/upload/{device}/{scene}.png').convert('RGB');im.thumbnail((330,640));thumbs.append(im.copy()) sheet=Image.new('RGB',(3*350,2*680),(15,20,38)) for i,im in enumerate(thumbs):sheet.paste(im,(i%3*350+10,i//3*680+10)) sheet.save(ROOT/f'marketing/contact-sheet-{device}.jpg',quality=92) (ROOT/'asset-manifest.json').write_text(json.dumps({'screenshots':manifest,'sample_data':True,'production_fixture_included':False},indent=2)+'\n') assets=ROOT/'website/assets';assets.mkdir(exist_ok=True) shutil.copy2(ROOT/'icons/Tarnax-180.png',assets/'apple-touch-icon.png') shutil.copy2(ROOT/'icons/Tarnax-32.png',assets/'favicon.png') shutil.copy2(ROOT/'icons/Tarnax-AppStore-1024.png',assets/'icon.png') shutil.copy2(ROOT/'marketing/social-landscape.png',assets/'social.png') print('Created',len(manifest),'upload screenshots, icon derivatives, marketing images and contact sheets.')