listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

screenshots-ios-compose.swift (3942B)


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