172 lines
5.6 KiB
Python
172 lines
5.6 KiB
Python
"""Composite app screenshots into Apple App Store mockups with phone frames."""
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
import os, math, textwrap
|
|
|
|
SCREENSHOTS_DIR = '/tmp/nur-falah-screenshots'
|
|
OUTPUT_DIR = '/tmp/nur-falah-mockups'
|
|
|
|
# Device frame dimensions (iPhone 14 Pro proportions)
|
|
FRAME_W = 720
|
|
FRAME_H = 1480
|
|
BORDER_RADIUS = 80
|
|
BEZEL_COLOR = '#1A1A1A'
|
|
SCREEN_X = 24
|
|
SCREEN_Y = 24
|
|
SCREEN_W = FRAME_W - 48
|
|
SCREEN_H = FRAME_H - 48
|
|
SCREEN_RADIUS = 60
|
|
|
|
# Canvas for 6.7" display (1290x2796 logical @ 3x = mockup at 1x smooth)
|
|
CANVAS_W = 1290
|
|
CANVAS_H = 2796
|
|
|
|
FONTS_DIRS = ['/System/Library/Fonts', '/System/Library/Fonts/Supplemental']
|
|
|
|
def find_font(name, size):
|
|
for fd in FONTS_DIRS:
|
|
for root, dirs, files in os.walk(fd):
|
|
for f in files:
|
|
if name.lower() in f.lower() and f.endswith(('.ttf', '.ttc')):
|
|
try:
|
|
return ImageFont.truetype(os.path.join(root, f), size)
|
|
except:
|
|
pass
|
|
return ImageFont.load_default()
|
|
|
|
def draw_phone_frame(draw, x, y, w, h):
|
|
"""Draw an iPhone-like bezel frame."""
|
|
# Outer bezel (rounded rect)
|
|
draw.rounded_rectangle(
|
|
[x, y, x + w, y + h],
|
|
radius=BORDER_RADIUS,
|
|
fill=BEZEL_COLOR,
|
|
)
|
|
# Screen area (darker)
|
|
draw.rounded_rectangle(
|
|
[x + SCREEN_X, y + SCREEN_Y, x + w - SCREEN_X, y + h - SCREEN_Y],
|
|
radius=SCREEN_RADIUS,
|
|
fill='#000000',
|
|
)
|
|
# Dynamic Island (pill shape at top center)
|
|
island_w, island_h = 126, 36
|
|
island_x = x + w//2 - island_w//2
|
|
island_y = y + SCREEN_Y + 14
|
|
draw.rounded_rectangle(
|
|
[island_x, island_y, island_x + island_w, island_y + island_h],
|
|
radius=18,
|
|
fill='#000000',
|
|
)
|
|
# Home indicator bar
|
|
bar_w, bar_h = 134, 5
|
|
bar_x = x + w//2 - bar_w//2
|
|
bar_y = y + h - SCREEN_Y - 12
|
|
draw.rounded_rectangle(
|
|
[bar_x, bar_y, bar_x + bar_w, bar_y + bar_h],
|
|
radius=3,
|
|
fill='#333333',
|
|
)
|
|
|
|
def make_mockup(screenshot_path, output_path, label):
|
|
"""Create a single mockup: screenshot composited into phone frame."""
|
|
# Load screenshot
|
|
screenshot = Image.open(screenshot_path).convert('RGB')
|
|
sw, sh = screenshot.size
|
|
|
|
# Target screen area inside the phone frame
|
|
inner_w = FRAME_W - 2 * SCREEN_X
|
|
inner_h = FRAME_H - 2 * SCREEN_Y
|
|
|
|
# Scale screenshot to fit while maintaining aspect ratio
|
|
scale = min(inner_w / sw, inner_h / sh)
|
|
new_w = int(sw * scale)
|
|
new_h = int(sh * scale)
|
|
screenshot_resized = screenshot.resize((new_w, new_h), Image.LANCZOS)
|
|
|
|
# Create phone frame on transparent background
|
|
frame = Image.new('RGBA', (FRAME_W, FRAME_H), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(frame)
|
|
draw_phone_frame(draw, 0, 0, FRAME_W, FRAME_H)
|
|
|
|
# Paste screenshot centered in the screen area
|
|
paste_x = SCREEN_X + (inner_w - new_w) // 2
|
|
paste_y = SCREEN_Y + (inner_h - new_h) // 2
|
|
frame.paste(screenshot_resized, (paste_x, paste_y))
|
|
|
|
# Create final canvas with gradient background
|
|
canvas = Image.new('RGB', (CANVAS_W, CANVAS_H), '#070A0D')
|
|
|
|
# Subtle radial gradient
|
|
bg = ImageDraw.Draw(canvas)
|
|
for i in range(CANVAS_H):
|
|
# Vertical gradient: dark at edges, slightly lighter center
|
|
ratio = abs(i / CANVAS_H - 0.5) * 2
|
|
r = int(7 + (1 - ratio) * 3)
|
|
g = int(10 + (1 - ratio) * 4)
|
|
b = int(13 + (1 - ratio) * 4)
|
|
bg.line([(0, i), (CANVAS_W, i)], fill=(r, g, b))
|
|
|
|
# Position phone frame centered
|
|
phone_x = (CANVAS_W - FRAME_W) // 2
|
|
phone_y = 120
|
|
canvas.paste(frame, (phone_x, phone_y), frame)
|
|
|
|
# Add label text below the phone
|
|
draw2 = ImageDraw.Draw(canvas)
|
|
font = find_font('SFNS', 32) or find_font('Helvetica', 32) or ImageFont.load_default()
|
|
|
|
# Wrap label text
|
|
words = label.split()
|
|
lines = []
|
|
current_line = ''
|
|
for word in words:
|
|
test = current_line + ' ' + word if current_line else word
|
|
if len(test) * 20 < CANVAS_W - 200: # rough estimate
|
|
current_line = test
|
|
else:
|
|
lines.append(current_line)
|
|
current_line = word
|
|
if current_line:
|
|
lines.append(current_line)
|
|
|
|
y_offset = phone_y + FRAME_H + 60
|
|
for line in lines:
|
|
bbox = draw2.textbbox((0, 0), line, font=font)
|
|
tw = bbox[2] - bbox[0]
|
|
draw2.text(
|
|
((CANVAS_W - tw) // 2, y_offset),
|
|
line,
|
|
fill='#C9A84C',
|
|
font=font,
|
|
)
|
|
y_offset += 48
|
|
|
|
# Save
|
|
canvas.save(output_path, 'PNG', quality=95)
|
|
print(f' ✅ {output_path} ({canvas.size})')
|
|
|
|
def main():
|
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
|
|
mockups = [
|
|
('prayer.png', '1-6.7-prayer-times', 'Accurate Prayer Times with\n5 Calculation Methods'),
|
|
('qibla.png', '2-6.7-qibla-finder', 'Find the Qibla from\nAnywhere in the World'),
|
|
('quran.png', '3-6.7-holy-quran', 'The Complete Holy Quran —\nAll 114 Surahs'),
|
|
('names99.png', '4-6.7-99-names', 'Study and Reflect on the\n99 Beautiful Names of Allah'),
|
|
('tasbih.png', '5-6.7-tasbih-counter', 'Digital Tasbih —\nTap to Remember Allah'),
|
|
('hijri.png', '6-6.7-hijri-calendar', 'Stay Connected to the\nIslamic Calendar'),
|
|
]
|
|
|
|
for filename, output_name, label in mockups:
|
|
src = os.path.join(SCREENSHOTS_DIR, filename)
|
|
dst = os.path.join(OUTPUT_DIR, f'{output_name}.png')
|
|
if os.path.exists(src):
|
|
make_mockup(src, dst, label)
|
|
else:
|
|
print(f' ⚠️ Missing: {src}')
|
|
|
|
print(f'\n🎉 All mockups saved to {OUTPUT_DIR}/')
|
|
|
|
if __name__ == '__main__':
|
|
main()
|