Scripts/screenshots-mac-desktop.swift (6526B)
1
2
3import AppKit
4import CoreGraphics
5import CoreText
6import Foundation
7import ImageIO
8
9guard CommandLine.arguments.count >= 3 else {
10 fputs("Usage: screenshots-mac-desktop.swift <window.png> <output.png> [wallpaper]\n", stderr)
11 exit(1)
12}
13
14let windowPath = CommandLine.arguments[1]
15let outputPath = CommandLine.arguments[2]
16let wallpaperPath =
17 CommandLine.arguments.count >= 4
18 ? CommandLine.arguments[3]
19 : "Marketing/sequoia-wallpaper.jpg"
20
21
22let canvasWidth: CGFloat = 2560
23let canvasHeight: CGFloat = 1664
24
25
26guard let wpData = try? Data(contentsOf: URL(fileURLWithPath: wallpaperPath)),
27 let wpSource = CGImageSourceCreateWithData(wpData as CFData, nil),
28 let wallpaper = CGImageSourceCreateImageAtIndex(wpSource, 0, nil)
29else {
30 fputs("Failed to load wallpaper: \(wallpaperPath)\n", stderr)
31 exit(1)
32}
33
34
35guard let winProvider = CGDataProvider(filename: windowPath),
36 let windowImage = CGImage(
37 pngDataProviderSource: winProvider, decode: nil, shouldInterpolate: true,
38 intent: .defaultIntent)
39else {
40 fputs("Failed to load window image: \(windowPath)\n", stderr)
41 exit(1)
42}
43
44
45guard
46 let context = CGContext(
47 data: nil,
48 width: Int(canvasWidth),
49 height: Int(canvasHeight),
50 bitsPerComponent: 8,
51 bytesPerRow: 0,
52 space: CGColorSpaceCreateDeviceRGB(),
53 bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
54 )
55else {
56 fputs("Failed to create graphics context\n", stderr)
57 exit(1)
58}
59
60
61let wpWidth = CGFloat(wallpaper.width)
62let wpHeight = CGFloat(wallpaper.height)
63let wpScale = canvasWidth / wpWidth
64let scaledWpHeight = wpHeight * wpScale
65
66let wpY = canvasHeight - scaledWpHeight
67context.draw(
68 wallpaper, in: CGRect(x: 0, y: wpY, width: canvasWidth, height: scaledWpHeight))
69
70
71let menuBarHeight: CGFloat = 60
72let menuBarY = canvasHeight - menuBarHeight
73context.setFillColor(CGColor(red: 1, green: 1, blue: 1, alpha: 0.25))
74context.fill(CGRect(x: 0, y: menuBarY, width: canvasWidth, height: menuBarHeight))
75
76
77func drawMenuSymbol(_ name: String, at x: CGFloat, size: CGFloat) -> CGFloat {
78 guard let image = NSImage(systemSymbolName: name, accessibilityDescription: nil) else {
79 return x
80 }
81 let config = NSImage.SymbolConfiguration(pointSize: size, weight: .regular)
82 let configured = image.withSymbolConfiguration(config) ?? image
83
84 let tinted = NSImage(size: configured.size, flipped: false) { rect in
85 menuTextColor.set()
86 rect.fill(using: .sourceOver)
87 configured.draw(in: rect, from: .zero, operation: .destinationIn, fraction: 1.0)
88 return true
89 }
90 let imgWidth = tinted.size.width
91 let imgHeight = tinted.size.height
92 let imgY = menuBarY + (menuBarHeight - imgHeight) / 2
93 guard let cgImage = tinted.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
94 return x
95 }
96 context.draw(cgImage, in: CGRect(x: x, y: imgY, width: imgWidth, height: imgHeight))
97 return x + imgWidth
98}
99
100
101let menuTextColor = NSColor(white: 0.0, alpha: 0.85)
102func drawMenuText(_ text: String, at x: CGFloat, size: CGFloat, weight: NSFont.Weight = .regular)
103 -> CGFloat
104{
105 let font = NSFont.systemFont(ofSize: size, weight: weight)
106 let attrs: [NSAttributedString.Key: Any] = [
107 .font: font,
108 .foregroundColor: menuTextColor,
109 ]
110 let attrStr = NSAttributedString(string: text, attributes: attrs)
111 let line = CTLineCreateWithAttributedString(attrStr)
112 let bounds = CTLineGetBoundsWithOptions(line, .useOpticalBounds)
113 let y = menuBarY + (menuBarHeight - bounds.height) / 2 - bounds.origin.y
114 context.textPosition = CGPoint(x: x, y: y)
115 CTLineDraw(line, context)
116 return x + bounds.width
117}
118
119
120let appleFont = NSFont(name: "SF Pro Display", size: 42) ?? NSFont.systemFont(ofSize: 42)
121let appleAttrs: [NSAttributedString.Key: Any] = [
122 .font: appleFont,
123 .foregroundColor: menuTextColor,
124]
125let appleString = NSAttributedString(string: "\u{F8FF}", attributes: appleAttrs)
126let appleLine = CTLineCreateWithAttributedString(appleString)
127let appleBounds = CTLineGetBoundsWithOptions(appleLine, .useOpticalBounds)
128let appleX: CGFloat = 32
129let appleTextY = menuBarY + (menuBarHeight - appleBounds.height) / 2 - appleBounds.origin.y
130context.textPosition = CGPoint(x: appleX, y: appleTextY)
131CTLineDraw(appleLine, context)
132
133var curX = appleX + appleBounds.width + 40
134curX = drawMenuText("Listless", at: curX, size: 28, weight: .bold) + 36
135curX = drawMenuText("File", at: curX, size: 28) + 36
136curX = drawMenuText("Edit", at: curX, size: 28) + 36
137curX = drawMenuText("View", at: curX, size: 28) + 36
138curX = drawMenuText("Window", at: curX, size: 28) + 36
139_ = drawMenuText("Help", at: curX, size: 28)
140
141
142var rightX = canvasWidth - 260
143_ = drawMenuText("Wed 9 Apr 9:41 AM", at: rightX, size: 28)
144rightX -= 20
145_ = drawMenuSymbol("switch.2", at: rightX - 44, size: 32)
146_ = drawMenuSymbol("magnifyingglass", at: rightX - 100, size: 28)
147
148
149let winWidth = CGFloat(windowImage.width)
150let winHeight = CGFloat(windowImage.height)
151let winScale = (canvasWidth * 0.4) / winWidth
152let scaledWinWidth = winWidth * winScale
153let scaledWinHeight = winHeight * winScale
154let winX = (canvasWidth - scaledWinWidth) / 2 + 400
155let availableHeight = canvasHeight - menuBarHeight
156let winY = (availableHeight - scaledWinHeight) / 2 + 150
157context.draw(
158 windowImage,
159 in: CGRect(x: winX, y: winY, width: scaledWinWidth, height: scaledWinHeight))
160
161
162guard let resultImage = context.makeImage(),
163 let destination = CGImageDestinationCreateWithURL(
164 URL(fileURLWithPath: outputPath) as CFURL,
165 "public.png" as CFString,
166 1,
167 nil
168 )
169else {
170 fputs("Failed to create output\n", stderr)
171 exit(1)
172}
173
174CGImageDestinationAddImage(destination, resultImage, nil)
175guard CGImageDestinationFinalize(destination) else {
176 fputs("Failed to write output\n", stderr)
177 exit(1)
178}