Skip to content

Instantly share code, notes, and snippets.

@msanders
Last active January 7, 2020 22:00
Show Gist options
  • Save msanders/5d1f0ad521d0e100cddf2f6cea3f36d2 to your computer and use it in GitHub Desktop.
Save msanders/5d1f0ad521d0e100cddf2f6cea3f36d2 to your computer and use it in GitHub Desktop.
Rough sketch of tool to generate an icns image from a file's icon.
import AppKit
import Darwin
var stderr = FileHandle.standardError
extension FileHandle: TextOutputStream {
public func write(_ string: String) {
guard let data = string.data(using: .utf8) else { return }
write(data)
}
}
extension NSWorkspace {
func icon(forFileURL fileURL: URL) throws -> NSImage? {
_ = try fileURL.checkResourceIsReachable()
return icon(forFile: fileURL.relativePath)
}
}
extension NSImageRep {
func generateBitmapImageRep() -> NSBitmapImageRep? {
guard let rep = NSBitmapImageRep(bitmapDataPlanes: nil,
pixelsWide: pixelsWide,
pixelsHigh: pixelsHigh,
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: colorSpaceName,
bytesPerRow: 0,
bitsPerPixel: 0) else {
return nil
}
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
draw()
NSGraphicsContext.restoreGraphicsState()
return rep
}
}
func showVersion() {
print("read-icon 0.0.0")
}
func showUsage() {
print("""
Usage: read-icon <src-file> [-o|--output] <out-icon>
Read an icon from a file and save it as an Apple Icon Image file.
""")
}
func main() {
if CommandLine.argc > 1 {
if CommandLine.arguments[1] == "--version" || CommandLine.arguments[1] == "-v" {
showVersion()
exit(0)
} else if CommandLine.arguments[1] == "--help" || CommandLine.arguments[1] == "-h" {
showVersion()
showUsage()
exit(0)
}
}
if CommandLine.argc != 4 {
print("Error: Invalid number of arguments.", to: &stderr)
showUsage()
exit(1)
}
if CommandLine.arguments[2] != "-o" && CommandLine.arguments[2] != "--output" {
print("Error: Missing required flag '--output'.", to: &stderr)
showUsage()
exit(1)
}
let fileURL = URL(fileURLWithPath: CommandLine.arguments[1])
let iconURL = URL(fileURLWithPath: CommandLine.arguments[3])
do {
guard let icon = try NSWorkspace.shared.icon(forFileURL: fileURL) else {
print("Error: Could not read icon for \(fileURL.relativePath)", to: &stderr)
exit(1)
}
let tmpDirectory: URL
if #available(macOS 10.12, *) {
tmpDirectory = FileManager.default.temporaryDirectory
} else {
tmpDirectory = .init(fileURLWithPath: NSTemporaryDirectory())
}
let iconBaseName = iconURL.deletingPathExtension().lastPathComponent
let iconDirectory = tmpDirectory.appendingPathComponent("\(iconBaseName).iconset")
try FileManager.default.createDirectory(at: iconDirectory, withIntermediateDirectories: true)
for repr in icon.representations {
let size = repr.size
let scale = CGFloat(repr.pixelsWide) / repr.size.width
guard !repr.description.contains("AppearanceName=NSAppearanceNameDarkAqua") else {
// Avoid undocumented "Dark Mode" icons.
continue
}
guard scale == 1 else {
// Avoid duplicate resolutions at other scale factors.
//
// The ICNS format only supports the following sizes:
// 16 × 16, 32 × 32, 48 × 48, 128 × 128, 256 × 256, 512 × 512,
// and 1024 × 1024 pixels.
//
// All others are ignored by `iconutil`. See
// https://en.wikipedia.org/wiki/Apple_Icon_Image_format
continue
}
guard let data = repr.generateBitmapImageRep()?.representation(using: .png, properties: [:]) else {
print("Error: Unable to generate PNG representation of icon for \(fileURL.relativePath)")
exit(1)
}
let assetSuffix: String
if repr.size.width == 1024 {
// `iconutil` oddly only seems to accept a 1024x1024 format with
// this filename.
assetSuffix = "512x512@2x.png"
} else {
assetSuffix = String(format: "%.fx%.f", size.width, size.height)
}
let assetName = String(format: "icon_%@.png", assetSuffix)
let assetURL = iconDirectory.appendingPathComponent(assetName)
try data.write(to: assetURL, options: [.atomic])
}
let task = Process()
task.launchPath = "/usr/bin/iconutil"
task.arguments = [iconDirectory.relativePath, "--convert", "icns", "--output", iconURL.relativePath]
task.launch()
task.waitUntilExit()
exit(task.terminationStatus)
} catch {
var statusCode: Int = 1
let nsError: NSError = error as NSError
if nsError.domain == NSPOSIXErrorDomain {
statusCode = nsError.code
} else if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError,
underlyingError.domain == NSPOSIXErrorDomain {
statusCode = underlyingError.code
}
print("Error: Could not read icon for \(fileURL.relativePath). \(error.localizedDescription)", to: &stderr)
exit(Int32(statusCode))
}
}
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment