Initial commit

This commit is contained in:
Alex E. Tafoya
2026-09-17 15:15:45 -05:00
commit 87fe7927c4
36 changed files with 5113 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
// ImageExporter.swift
// GreenshotMac
//
// Port of Greenshot's ImageIO: encoding a capture to the configured output format
// and writing it to the configured directory using the filename pattern.
import AppKit
import UniformTypeIdentifiers
enum ImageExporter {
static func data(from image: CGImage, format: OutputFormat, jpegQuality: Int) -> Data? {
let rep = NSBitmapImageRep(cgImage: image)
rep.size = CGSize(width: image.width, height: image.height)
var properties: [NSBitmapImageRep.PropertyKey: Any] = [:]
if format == .jpg {
properties[.compressionFactor] = NSNumber(value: Double(jpegQuality) / 100.0)
}
return rep.representation(using: format.fileType, properties: properties)
}
/// Flattens an image onto white, needed for formats without an alpha channel.
static func flattened(_ image: CGImage) -> CGImage {
guard let context = CGContext(data: nil,
width: image.width,
height: image.height,
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue) else { return image }
context.setFillColor(NSColor.white.cgColor)
context.fill(CGRect(x: 0, y: 0, width: image.width, height: image.height))
context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
return context.makeImage() ?? image
}
/// The URL a capture would be saved to with the current configuration.
static func suggestedURL(for details: CaptureDetails,
format: OutputFormat = Configuration.shared.outputFileFormat) -> URL {
let configuration = Configuration.shared
let name = FilenamePattern.filename(pattern: configuration.outputFileFilenamePattern,
details: details,
format: format)
let directory = URL(fileURLWithPath: configuration.outputFilePath, isDirectory: true)
return uniqueURL(directory.appendingPathComponent(name))
}
/// Appends " (2)", " (3)", … until the URL is free.
static func uniqueURL(_ url: URL) -> URL {
guard FileManager.default.fileExists(atPath: url.path) else { return url }
let directory = url.deletingLastPathComponent()
let ext = url.pathExtension
let base = url.deletingPathExtension().lastPathComponent
var index = 2
while index < 1000 {
let candidate = directory
.appendingPathComponent("\(base) (\(index))")
.appendingPathExtension(ext)
if !FileManager.default.fileExists(atPath: candidate.path) { return candidate }
index += 1
}
return url
}
@discardableResult
static func write(_ image: CGImage, to url: URL, format: OutputFormat) throws -> URL {
let configuration = Configuration.shared
let source = (format == .jpg || format == .bmp) ? flattened(image) : image
guard let data = data(from: source, format: format, jpegQuality: configuration.outputFileJpegQuality) else {
throw CaptureError.captureFailed("Could not encode the image as \(format.displayName).")
}
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(),
withIntermediateDirectories: true)
try data.write(to: url, options: .atomic)
return url
}
static func format(forPathExtension pathExtension: String) -> OutputFormat {
switch pathExtension.lowercased() {
case "jpg", "jpeg": return .jpg
case "tif", "tiff": return .tiff
case "bmp": return .bmp
case "gif": return .gif
default: return .png
}
}
}