// 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 } } }