Last active
January 7, 2020 22:00
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = "[email protected]" | |
} 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