import AppKit import CoreGraphics import Foundation // App-Icons dürfen keinen Alphakanal und keine eigenen runden Ecken haben: // iOS maskiert selbst. Ein Logo mit weißem Rand und eingebrannter Rundung // bekäme sonst einen weißen Rahmen. Also Rand abschneiden, quadratisch // zuschneiden und die dann weißen Ecken mit der Randfarbe auffüllen. let arguments = CommandLine.arguments guard arguments.count >= 3 else { print("Aufruf: makeicon "); exit(1) } guard let source = NSImage(contentsOfFile: arguments[1]), let sourceImage = source.cgImage(forProposedRect: nil, context: nil, hints: nil) else { print("Quelle nicht lesbar"); exit(1) } let width = sourceImage.width, height = sourceImage.height let bytesPerRow = width * 4 var pixels = [UInt8](repeating: 0, count: bytesPerRow * height) guard let readContext = CGContext( data: &pixels, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { print("Puffer nicht anlegbar"); exit(1) } readContext.draw(sourceImage, in: CGRect(x: 0, y: 0, width: width, height: height)) @inline(__always) func offset(_ x: Int, _ y: Int) -> Int { y * bytesPerRow + x * 4 } @inline(__always) func isWhite(_ x: Int, _ y: Int) -> Bool { let i = offset(x, y) return pixels[i] > 242 && pixels[i + 1] > 242 && pixels[i + 2] > 242 } // Eine Reihe gilt als leer, wenn fast nichts darin farbig ist – die // abgerundete Ecke liefert sonst falsche Treffer. func rowIsBlank(_ y: Int) -> Bool { var coloured = 0 for x in stride(from: 0, to: width, by: 3) where !isWhite(x, y) { coloured += 1 if coloured >= 3 { return false } } return true } func columnIsBlank(_ x: Int) -> Bool { var coloured = 0 for y in stride(from: 0, to: height, by: 3) where !isWhite(x, y) { coloured += 1 if coloured >= 3 { return false } } return true } var top = 0, bottom = height - 1, left = 0, right = width - 1 while top < bottom, rowIsBlank(top) { top += 1 } while bottom > top, rowIsBlank(bottom) { bottom -= 1 } while left < right, columnIsBlank(left) { left += 1 } while right > left, columnIsBlank(right) { right -= 1 } print("Quelle \(width)x\(height) → Inhalt (\(left),\(top)) \(right - left + 1)x\(bottom - top + 1)") // Auf ein Quadrat erweitern, sonst verzerrt das Skalieren. let side = min(max(right - left + 1, bottom - top + 1), min(width, height)) let centreX = (left + right) / 2, centreY = (top + bottom) / 2 let cropX = max(0, min(width - side, centreX - side / 2)) let cropY = max(0, min(height - side, centreY - side / 2)) print("Quadrat: (\(cropX),\(cropY)) \(side)x\(side)") // Reparieren lohnt nicht: der weiche Übergang an der eingebrannten Rundung // lässt sich nicht sauber nachfärben. Stattdessen so weit hineinschneiden, // bis die vier Ecken im Hintergrund liegen. Das kostet ein paar Prozent // Bildfläche, dafür bleibt kein heller Rand stehen. @inline(__always) func isPale(_ x: Int, _ y: Int) -> Bool { let i = offset(x, y) return pixels[i] > 150 && pixels[i + 1] > 150 && pixels[i + 2] > 150 } var inset = 0 while inset < side / 5 { let x0 = cropX + inset, y0 = cropY + inset let x1 = cropX + side - 1 - inset, y1 = cropY + side - 1 - inset if !isPale(x0, y0) && !isPale(x1, y0) && !isPale(x0, y1) && !isPale(x1, y1) { break } inset += 2 } inset += 8 // Sicherheitsabstand gegen weiche Kanten let innerX = cropX + inset, innerY = cropY + inset let innerSide = side - 2 * inset print("Nach innen versetzt um \(inset) → (\(innerX),\(innerY)) \(innerSide)x\(innerSide)") // Den hellen Aussenbereich vom Bildrand her zusammenhängend markieren. // Über die Helligkeit allein ginge es nicht: das Wohnmobil und die Schrift // im Logo sind ebenfalls weiss, liegen aber innen und werden so nicht // erfasst. Der weiche Übergang an der Rundung gehört mit dazu, sonst // bleiben helle Ränder stehen. @inline(__always) func isBright(_ x: Int, _ y: Int) -> Bool { let i = offset(x, y) return pixels[i] > 200 && pixels[i + 1] > 200 && pixels[i + 2] > 200 } var isOutside = [Bool](repeating: false, count: width * height) var queue: [Int] = [] for x in 0..= 0, nx < width, ny >= 0, ny < height else { continue } let neighbour = ny * width + nx guard !isOutside[neighbour], isBright(nx, ny) else { continue } isOutside[neighbour] = true queue.append(neighbour) } } // Markierte Pixel durch die nächste erhaltene Stelle derselben Zeile // ersetzen, damit der Farbverlauf des Hintergrunds weiterläuft. var repaired = 0 for y in 0..= 0 else { continue } for x in 0.. lastKept ? lastKept : -1) guard sourceX >= 0 else { continue } let from = offset(sourceX, y), to = offset(x, y) pixels[to] = pixels[from] pixels[to + 1] = pixels[from + 1] pixels[to + 2] = pixels[from + 2] repaired += 1 } } print("Aussenbereich ersetzt: \(repaired) Pixel") guard let repairedImage = readContext.makeImage(), let cropped = repairedImage.cropping( to: CGRect(x: innerX, y: innerY, width: innerSide, height: innerSide)) else { print("Zuschnitt fehlgeschlagen"); exit(1) } // Auf 1024x1024 bringen, ohne Alphakanal. let target = 1024 guard let outputContext = CGContext( data: nil, width: target, height: target, bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue ) else { print("Ausgabe nicht anlegbar"); exit(1) } outputContext.interpolationQuality = .high outputContext.draw(cropped, in: CGRect(x: 0, y: 0, width: target, height: target)) guard let finalImage = outputContext.makeImage() else { print("Bild leer"); exit(1) } let rep = NSBitmapImageRep(cgImage: finalImage) guard let png = rep.representation(using: .png, properties: [:]) else { print("PNG nicht erzeugbar"); exit(1) } try png.write(to: URL(fileURLWithPath: arguments[2])) print("Geschrieben: \(arguments[2]) (\(target)x\(target))")