← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


Scripts/screenshots-ios-compose.swift (3942B)

1#!/usr/bin/env swift
2
3import AppKit
4import CoreGraphics
5import CoreText
6import Foundation
7import ImageIO
8
9guard CommandLine.arguments.count >= 4 else {
10    fputs(
11        "Usage: screenshots-ios-compose.swift <input.png> <output.png> <text> [width height]\n",
12        stderr)
13    exit(1)
14}
15
16let inputPath = CommandLine.arguments[1]
17let outputPath = CommandLine.arguments[2]
18let text = CommandLine.arguments[3]
19
20// Output dimensions (default: 6.9" iPhone for App Store)
21let canvasWidth: CGFloat =
22    CommandLine.arguments.count >= 5 ? CGFloat(Int(CommandLine.arguments[4]) ?? 1320) : 1320
23let canvasHeight: CGFloat =
24    CommandLine.arguments.count >= 6 ? CGFloat(Int(CommandLine.arguments[5]) ?? 2868) : 2868
25
26// Background: rgb(30, 16, 40)
27let bgColor = CGColor(red: 30.0 / 255.0, green: 16.0 / 255.0, blue: 40.0 / 255.0, alpha: 1.0)
28
29// Load framed device image
30guard let dataProvider = CGDataProvider(filename: inputPath),
31    let deviceImage = CGImage(
32        pngDataProviderSource: dataProvider, decode: nil, shouldInterpolate: true,
33        intent: .defaultIntent)
34else {
35    fputs("Failed to load image: \(inputPath)\n", stderr)
36    exit(1)
37}
38
39let deviceWidth = CGFloat(deviceImage.width)
40let deviceHeight = CGFloat(deviceImage.height)
41
42// Create canvas
43guard
44    let context = CGContext(
45        data: nil,
46        width: Int(canvasWidth),
47        height: Int(canvasHeight),
48        bitsPerComponent: 8,
49        bytesPerRow: 0,
50        space: CGColorSpaceCreateDeviceRGB(),
51        bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
52    )
53else {
54    fputs("Failed to create graphics context\n", stderr)
55    exit(1)
56}
57
58// Fill background
59context.setFillColor(bgColor)
60context.fill(CGRect(x: 0, y: 0, width: canvasWidth, height: canvasHeight))
61
62// Scale device image to fit width with padding, leaving space for text
63let isCompact = canvasWidth < 600
64let isLandscape = canvasWidth > canvasHeight
65let bottomPadding: CGFloat = isCompact ? -60 : isLandscape ? -350 : 60
66let sidePadding: CGFloat = isCompact ? 20 : isLandscape ? 20 : 60
67let topReserved: CGFloat = isCompact ? 90 : isLandscape ? 200 : 300
68let maxDeviceWidth = canvasWidth - sidePadding * 2
69let maxDeviceHeight = canvasHeight - bottomPadding - topReserved
70let scale = min(maxDeviceWidth / deviceWidth, maxDeviceHeight / deviceHeight, 1.0)
71let scaledWidth = deviceWidth * scale
72let scaledHeight = deviceHeight * scale
73let deviceX = (canvasWidth - scaledWidth) / 2
74let deviceY = bottomPadding  // CG origin is bottom-left
75
76context.draw(
77    deviceImage, in: CGRect(x: deviceX, y: deviceY, width: scaledWidth, height: scaledHeight))
78
79// Draw text centered above device
80let fontSize: CGFloat = isCompact ? canvasWidth * 0.08 : isLandscape ? canvasHeight * 0.06 : canvasWidth * 0.0545
81let font = NSFont.systemFont(ofSize: fontSize, weight: .bold)
82let attributes: [NSAttributedString.Key: Any] = [
83    .font: font,
84    .foregroundColor: NSColor.white,
85]
86let attrString = NSAttributedString(string: text, attributes: attributes)
87let ctLine = CTLineCreateWithAttributedString(attrString)
88let textBounds = CTLineGetBoundsWithOptions(ctLine, .useOpticalBounds)
89
90let textAreaTop = isLandscape ? canvasHeight - 40 : canvasHeight
91let textAreaBottom = deviceY + scaledHeight
92let textX = (canvasWidth - textBounds.width) / 2 - textBounds.origin.x
93let textY = (textAreaTop + textAreaBottom) / 2 - textBounds.height / 2 - textBounds.origin.y
94
95context.textPosition = CGPoint(x: textX, y: textY)
96CTLineDraw(ctLine, context)
97
98// Save
99guard let resultImage = context.makeImage(),
100    let destination = CGImageDestinationCreateWithURL(
101        URL(fileURLWithPath: outputPath) as CFURL,
102        "public.png" as CFString,
103        1,
104        nil
105    )
106else {
107    fputs("Failed to create output\n", stderr)
108    exit(1)
109}
110
111CGImageDestinationAddImage(destination, resultImage, nil)
112guard CGImageDestinationFinalize(destination) else {
113    fputs("Failed to write output\n", stderr)
114    exit(1)
115}