first commit

This commit is contained in:
fototeddy
2026-09-04 21:30:51 +02:00
commit da64b1b6bd
160 changed files with 20664 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
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 <quelle> <ziel>"); 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..<width {
for y in [0, height - 1] where isBright(x, y) && !isOutside[y * width + x] {
isOutside[y * width + x] = true
queue.append(y * width + x)
}
}
for y in 0..<height {
for x in [0, width - 1] where isBright(x, y) && !isOutside[y * width + x] {
isOutside[y * width + x] = true
queue.append(y * width + x)
}
}
var head = 0
while head < queue.count {
let index = queue[head]; head += 1
let x = index % width, y = index / width
for (dx, dy) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
let nx = x + dx, ny = y + dy
guard nx >= 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..<height {
var firstKept = -1, lastKept = -1
for x in 0..<width where !isOutside[y * width + x] {
if firstKept < 0 { firstKept = x }
lastKept = x
}
guard firstKept >= 0 else { continue }
for x in 0..<width where isOutside[y * width + x] {
let sourceX = x < firstKept ? firstKept : (x > 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))")