import Foundation import AppKit // Force AppKit initialization so all system cursor images are populated let _ = NSApplication.shared struct CursorAsset: Codable { let dataUrl: String let hotspotX: Double let hotspotY: Double let width: Double let height: Double } let minimumCursorRenderScale: CGFloat = 12 /// Reference pixel height that all cursor images are normalized to after trimming. /// Kept high so large in-app cursor scaling still preserves raster detail up to 10x. let referencePixelHeight: CGFloat = 1024 func bestCursorCGImage(for image: NSImage) -> CGImage? { var proposedRect = CGRect(origin: .zero, size: image.size) if let cgImage = image.cgImage(forProposedRect: &proposedRect, context: nil, hints: nil) { return cgImage } let sortedRepresentations = image.representations.sorted { lhs, rhs in let lhsPixels = lhs.pixelsWide * lhs.pixelsHigh let rhsPixels = rhs.pixelsWide * rhs.pixelsHigh return lhsPixels > rhsPixels } for representation in sortedRepresentations { if let bitmap = representation as? NSBitmapImageRep, let cgImage = bitmap.cgImage { return cgImage } } return nil } func makeRetinaImage(from cgImage: CGImage, pointSize: NSSize) -> NSImage { let bitmap = NSBitmapImageRep(cgImage: cgImage) bitmap.size = pointSize let image = NSImage(size: pointSize) image.addRepresentation(bitmap) return image } /// Find the bounding box of non-transparent pixels in a bitmap. /// Returns (x, y, width, height) in pixel coordinates (origin top-left). func findOpaqueBounds(_ bitmap: NSBitmapImageRep) -> (x: Int, y: Int, width: Int, height: Int)? { let w = bitmap.pixelsWide let h = bitmap.pixelsHigh guard let data = bitmap.bitmapData else { return nil } let bpr = bitmap.bytesPerRow var minX = w, minY = h, maxX = -1, maxY = -1 for y in 0.. 2 { if x < minX { minX = x } if x > maxX { maxX = x } if y < minY { minY = y } if y > maxY { maxY = y } } } } if maxX < 0 { return nil } return (minX, minY, maxX - minX + 1, maxY - minY + 1) } func makeAsset(for cursor: NSCursor) -> CursorAsset? { let baseImage = cursor.image let pointSize = baseImage.size guard let sourceCGImage = bestCursorCGImage(for: baseImage) else { return nil } let sourcePixelWidth = max(1, sourceCGImage.width) let sourcePixelHeight = max(1, sourceCGImage.height) let sourceScaleX = CGFloat(sourcePixelWidth) / max(1, pointSize.width) let sourceScaleY = CGFloat(sourcePixelHeight) / max(1, pointSize.height) let sourceScale = max(sourceScaleX, sourceScaleY) let image = makeRetinaImage(from: sourceCGImage, pointSize: pointSize) let renderScale = max( minimumCursorRenderScale, referencePixelHeight / max(1, CGFloat(sourcePixelHeight)) ) let pixelWidth = max(1, Int((CGFloat(sourcePixelWidth) * renderScale).rounded(.up))) let pixelHeight = max(1, Int((CGFloat(sourcePixelHeight) * renderScale).rounded(.up))) // Render at full resolution guard let bitmap = NSBitmapImageRep( bitmapDataPlanes: nil, pixelsWide: pixelWidth, pixelsHigh: pixelHeight, bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0 ) else { return nil } bitmap.size = NSSize(width: CGFloat(pixelWidth), height: CGFloat(pixelHeight)) NSGraphicsContext.saveGraphicsState() guard let context = NSGraphicsContext(bitmapImageRep: bitmap) else { NSGraphicsContext.restoreGraphicsState() return nil } NSGraphicsContext.current = context context.imageInterpolation = .none context.cgContext.interpolationQuality = .none image.draw( in: NSRect(x: 0, y: 0, width: CGFloat(pixelWidth), height: CGFloat(pixelHeight)), from: .zero, operation: .copy, fraction: 1 ) context.flushGraphics() NSGraphicsContext.restoreGraphicsState() // Find opaque bounds and crop away transparent padding guard let bounds = findOpaqueBounds(bitmap) else { return nil } guard let fullCG = bitmap.cgImage else { return nil } let cropRect = CGRect(x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height) guard let croppedCG = fullCG.cropping(to: cropRect) else { return nil } // Scale cropped image to referencePixelHeight let aspect = CGFloat(bounds.width) / CGFloat(bounds.height) let outH = Int(referencePixelHeight) let outW = max(1, Int((referencePixelHeight * aspect).rounded(.up))) guard let outBitmap = NSBitmapImageRep( bitmapDataPlanes: nil, pixelsWide: outW, pixelsHigh: outH, bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0 ) else { return nil } outBitmap.size = NSSize(width: outW, height: outH) NSGraphicsContext.saveGraphicsState() guard let outCtx = NSGraphicsContext(bitmapImageRep: outBitmap) else { NSGraphicsContext.restoreGraphicsState() return nil } NSGraphicsContext.current = outCtx outCtx.imageInterpolation = .none outCtx.cgContext.interpolationQuality = .none let nsImage = NSImage(cgImage: croppedCG, size: NSSize(width: bounds.width, height: bounds.height)) nsImage.draw( in: NSRect(x: 0, y: 0, width: outW, height: outH), from: .zero, operation: .copy, fraction: 1 ) outCtx.flushGraphics() NSGraphicsContext.restoreGraphicsState() guard let pngData = outBitmap.representation(using: .png, properties: [:]) else { return nil } let base64 = pngData.base64EncodedString() // Adjust hotspot using the extracted source image scale so 2x cursor reps keep the correct anchor. let hotspot = cursor.hotSpot let hotspotPxX = hotspot.x * sourceScale * renderScale - CGFloat(bounds.x) let hotspotPxY = hotspot.y * sourceScale * renderScale - CGFloat(bounds.y) let scaleToOut = referencePixelHeight / CGFloat(bounds.height) let outHotspotX = hotspotPxX * scaleToOut let outHotspotY = hotspotPxY * scaleToOut return CursorAsset( dataUrl: "data:image/png;base64,\(base64)", hotspotX: Double(outHotspotX), hotspotY: Double(outHotspotY), width: Double(outW), height: Double(outH) ) } let cursorMap: [String: NSCursor] = [ "arrow": .arrow, "text": .iBeam, "pointer": .pointingHand, "crosshair": .crosshair, "open-hand": .openHand, "closed-hand": .closedHand, "resize-ew": .resizeLeftRight, "resize-ns": .resizeUpDown, "not-allowed": .operationNotAllowed, ] var assets: [String: CursorAsset] = [:] for (key, cursor) in cursorMap { if let asset = makeAsset(for: cursor) { assets[key] = asset } } let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys] let data = try encoder.encode(assets) FileHandle.standardOutput.write(data)