#!/usr/bin/env python3 """ Builds the synthetic file library the store screenshots are taken over. python3 tools/make-demo-library.py [output-dir] # default: ./demo-files Why this exists: published screenshots are public. Shooting them over a real device's storage would put someone's real file names, folder names and document titles on the Play listing. Everything here is generated, so every pixel of every screenshot is fake by construction. Output lands in ./demo-files/ *inside* the repo and is git-ignored: it is regenerated on demand and never committed. Push it to a device with: adb push demo-files /sdcard/Download/PlainFiles-Demo """ import os import shutil import sys import zipfile from PIL import Image, ImageDraw, ImageFont FONT_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" # Distinct hues so the grid view reads as eight different pictures rather than a wall of one # colour. (name, background hue, accent hue, motif) SCENES = [ ("dawn-ridge", (28, 78, 150), (255, 214, 140), "peaks"), ("pine-lake", (18, 96, 92), (198, 240, 200), "waves"), ("desert-road", (196, 120, 48), (255, 236, 196), "peaks"), ("neon-city", (78, 40, 130), (240, 120, 220), "bars"), ("quiet-harbour", (36, 62, 110), (170, 220, 255), "waves"), ("autumn-field", (150, 84, 36), (255, 208, 120), "bars"), ("snow-pass", (110, 124, 148), (245, 250, 255), "peaks"), ("orchard", (52, 108, 60), (214, 246, 170), "bars"), ] def font(path, size): try: return ImageFont.truetype(path, size) except OSError: return ImageFont.load_default() def vertical_gradient(width, height, top, bottom): image = Image.new("RGB", (width, height), top) draw = ImageDraw.Draw(image) for y in range(height): blend = y / max(height - 1, 1) draw.line( [(0, y), (width, y)], fill=tuple(int(top[i] + (bottom[i] - top[i]) * blend) for i in range(3)), ) return image def draw_motif(draw, width, height, kind, colour): if kind == "peaks": base = height * 0.78 for offset, scale in ((0.05, 0.42), (0.42, 0.58), (0.74, 0.34)): apex = base - height * scale draw.polygon( [ (width * offset, base), (width * (offset + 0.20), apex), (width * (offset + 0.40), base), ], fill=colour, ) elif kind == "waves": for index in range(5): y = height * (0.55 + index * 0.08) amplitude = height * 0.03 points = [ (x, y + amplitude * ((x // 40) % 2 * 2 - 1)) for x in range(0, width + 40, 40) ] draw.line(points, fill=colour, width=6) else: # bars for index in range(7): x = width * (0.10 + index * 0.12) bar = height * (0.20 + (index % 3) * 0.14) draw.rounded_rectangle( [x, height * 0.80 - bar, x + width * 0.06, height * 0.80], radius=8, fill=colour, ) def make_photos(root): folder = os.path.join(root, "Photos") os.makedirs(folder, exist_ok=True) label_font = font(FONT_BOLD, 54) for index, (name, top, accent, motif) in enumerate(SCENES, start=1): width, height = 1600, 1200 image = vertical_gradient(width, height, top, tuple(int(c * 0.45) for c in top)) draw = ImageDraw.Draw(image) draw.ellipse([width * 0.72, height * 0.12, width * 0.92, height * 0.32], fill=accent) draw_motif(draw, width, height, motif, accent) draw.text((60, height - 120), f"Sample {index:02d} - {name}", font=label_font, fill=accent) image.save(os.path.join(folder, f"sample-{index:02d}-{name}.png"), "PNG") def write(path, text): with open(path, "w", encoding="utf-8") as handle: handle.write(text) def make_text_files(root): folder = os.path.join(root, "Documents") os.makedirs(folder, exist_ok=True) write( os.path.join(folder, "offsite-notes.txt"), "Team offsite - notes\n" "====================\n\n" "Nothing in this file is real: the whole library the screenshots are taken over\n" "is generated by tools/make-demo-library.py, so publishing them leaks nobody's\n" "data.\n\n" "1. Agenda\n2. Travel\n3. Follow-ups\n", ) write( os.path.join(folder, "budget.csv"), "item,quantity,unit_cost\n" "laptop stand,2,39.00\n" "usb-c hub,3,54.50\n" "headset,1,229.00\n" "markers,12,2.15\n", ) write( os.path.join(folder, "itinerary.md"), "# Itinerary\n\n| Day | Item |\n| --- | --- |\n" "| 1 | Arrival |\n| 2 | Workshops |\n| 3 | Departure |\n", ) # Stand-ins for binary formats: what matters for a screenshot is the icon and the name # in the list, and a real PDF or DOCX would add a build dependency for no visible gain. write(os.path.join(folder, "quarterly-report.pdf"), "%PDF-1.4\n% demo placeholder\n") write(os.path.join(folder, "service-agreement.docx"), "PK demo placeholder\n") def make_sized_file(path, megabytes): """A file of a given size, so the usage screen has something worth ranking.""" chunk = b"\x00" * (1024 * 1024) with open(path, "wb") as handle: for _ in range(megabytes): handle.write(chunk) def make_media(root): video = os.path.join(root, "Video") os.makedirs(video, exist_ok=True) for index in range(1, 4): make_sized_file(os.path.join(video, f"clip-{index:02d}.mp4"), 24 + index * 6) music = os.path.join(root, "Music") os.makedirs(music, exist_ok=True) for index in range(1, 6): make_sized_file(os.path.join(music, f"track-{index:02d}.mp3"), 4 + index) def make_archive(root): folder = os.path.join(root, "Backups") os.makedirs(folder, exist_ok=True) with zipfile.ZipFile(os.path.join(folder, "config-backup.zip"), "w", zipfile.ZIP_DEFLATED) as bundle: bundle.writestr("settings.json", '{\n "theme": "system",\n "sort": "name"\n}\n') bundle.writestr("bookmarks.txt", "Photos\nDocuments\nBackups\n") bundle.writestr("notes/readme.txt", "Synthetic contents.\n") def make_downloads(root): folder = os.path.join(root, "Download") os.makedirs(folder, exist_ok=True) write(os.path.join(folder, "welcome.txt"), "Generated by tools/make-demo-library.py\n") make_sized_file(os.path.join(folder, "installer.bin"), 18) def main(): repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) root = sys.argv[1] if len(sys.argv) > 1 else os.path.join(repo, "demo-files") if os.path.isdir(root): shutil.rmtree(root) os.makedirs(root) make_photos(root) make_text_files(root) make_media(root) make_archive(root) make_downloads(root) total = 0 for base, _, names in os.walk(root): for name in names: total += os.path.getsize(os.path.join(base, name)) print(f"Wrote {root} ({total / (1024 * 1024):.1f} MB)") if __name__ == "__main__": main()