screenshots-ios-compose.swift (3688B)
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 let canvasWidth: CGFloat = 21 CommandLine.arguments.count >= 5 ? CGFloat(Int(CommandLine.arguments[4]) ?? 1320) : 1320 22 let canvasHeight: CGFloat = 23 CommandLine.arguments.count >= 6 ? CGFloat(Int(CommandLine.arguments[5]) ?? 2868) : 2868 24 25 let backgroundColor = CGColor( 26 red: 228.0 / 255.0, 27 green: 242.0 / 255.0, 28 blue: 255.0 / 255.0, 29 alpha: 1.0 30 ) 31 32 guard let dataProvider = CGDataProvider(filename: inputPath), 33 let deviceImage = CGImage( 34 pngDataProviderSource: dataProvider, 35 decode: nil, 36 shouldInterpolate: true, 37 intent: .defaultIntent 38 ) 39 else { 40 fputs("Failed to load image: \(inputPath)\n", stderr) 41 exit(1) 42 } 43 44 let deviceWidth = CGFloat(deviceImage.width) 45 let deviceHeight = CGFloat(deviceImage.height) 46 47 guard 48 let context = CGContext( 49 data: nil, 50 width: Int(canvasWidth), 51 height: Int(canvasHeight), 52 bitsPerComponent: 8, 53 bytesPerRow: 0, 54 space: CGColorSpaceCreateDeviceRGB(), 55 bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue 56 ) 57 else { 58 fputs("Failed to create graphics context\n", stderr) 59 exit(1) 60 } 61 62 context.setFillColor(backgroundColor) 63 context.fill(CGRect(x: 0, y: 0, width: canvasWidth, height: canvasHeight)) 64 65 let isLandscape = canvasWidth > canvasHeight 66 let isPad = canvasWidth >= 1800 67 let sidePadding: CGFloat = isPad ? 140 : 70 68 let bottomPadding: CGFloat = isPad ? 150 : 80 69 let topReserved: CGFloat = isPad ? 460 : (isLandscape ? 220 : 330) 70 let maxDeviceWidth = canvasWidth - sidePadding * 2 71 let maxDeviceHeight = canvasHeight - bottomPadding - topReserved 72 let scale = min(maxDeviceWidth / deviceWidth, maxDeviceHeight / deviceHeight, 1.0) 73 let scaledWidth = deviceWidth * scale 74 let scaledHeight = deviceHeight * scale 75 let deviceX = (canvasWidth - scaledWidth) / 2 76 let deviceY = bottomPadding 77 78 context.draw( 79 deviceImage, 80 in: CGRect(x: deviceX, y: deviceY, width: scaledWidth, height: scaledHeight) 81 ) 82 83 let fontSize: CGFloat = isPad ? 118 : (isLandscape ? canvasHeight * 0.06 : canvasWidth * 0.066) 84 let font = NSFont.systemFont(ofSize: fontSize, weight: .bold) 85 let attributes: [NSAttributedString.Key: Any] = [ 86 .font: font, 87 .foregroundColor: NSColor(calibratedRed: 17 / 255, green: 24 / 255, blue: 39 / 255, alpha: 1), 88 ] 89 let attrString = NSAttributedString(string: text, attributes: attributes) 90 let ctLine = CTLineCreateWithAttributedString(attrString) 91 let textBounds = CTLineGetBoundsWithOptions(ctLine, .useOpticalBounds) 92 93 let textAreaTop = canvasHeight - (isPad ? 80 : 40) 94 let textAreaBottom = deviceY + scaledHeight 95 let textX = (canvasWidth - textBounds.width) / 2 - textBounds.origin.x 96 let textY = (textAreaTop + textAreaBottom) / 2 - textBounds.height / 2 - textBounds.origin.y 97 98 context.textPosition = CGPoint(x: textX, y: textY) 99 CTLineDraw(ctLine, context) 100 101 guard let resultImage = context.makeImage(), 102 let destination = CGImageDestinationCreateWithURL( 103 URL(fileURLWithPath: outputPath) as CFURL, 104 "public.png" as CFString, 105 1, 106 nil 107 ) 108 else { 109 fputs("Failed to create output\n", stderr) 110 exit(1) 111 } 112 113 CGImageDestinationAddImage(destination, resultImage, nil) 114 guard CGImageDestinationFinalize(destination) else { 115 fputs("Failed to write output\n", stderr) 116 exit(1) 117 }