Fix mirrored image export with CGBitmapContext renderer
Draw base CGImage without coordinate flip, apply flip+scale only for annotations. Save via direct NSBitmapImageRep access instead of tiffRepresentation to avoid transform issues. Also redirect clipboard images to saveFolder instead of temp directory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -242,24 +242,41 @@ final class AppCoordinator: ObservableObject {
|
|||||||
private func openEditor(image: NSImage, sourceURL: URL) {
|
private func openEditor(image: NSImage, sourceURL: URL) {
|
||||||
let controller = EditorWindowController(image: image, sourceURL: sourceURL) { [weak self] annotatedImage in
|
let controller = EditorWindowController(image: image, sourceURL: sourceURL) { [weak self] annotatedImage in
|
||||||
guard let self, let annotatedImage else { return }
|
guard let self, let annotatedImage else { return }
|
||||||
self.saveAnnotatedImage(annotatedImage, replacingFileAt: sourceURL)
|
let targetURL: URL
|
||||||
|
if sourceURL.path.hasPrefix(NSTemporaryDirectory()) {
|
||||||
|
try? FileManager.default.createDirectory(at: saveFolder, withIntermediateDirectories: true)
|
||||||
|
targetURL = saveFolder.appendingPathComponent(Self.filename(ext: imageFormat.fileExtension))
|
||||||
|
} else {
|
||||||
|
targetURL = sourceURL
|
||||||
|
}
|
||||||
|
self.saveAnnotatedImage(annotatedImage, to: targetURL)
|
||||||
}
|
}
|
||||||
editorController = controller
|
editorController = controller
|
||||||
controller.showEditor()
|
controller.showEditor()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveAnnotatedImage(_ image: NSImage, replacingFileAt url: URL) {
|
private func saveAnnotatedImage(_ image: NSImage, to url: URL) {
|
||||||
guard let tiffData = image.tiffRepresentation,
|
let bitmap: NSBitmapImageRep
|
||||||
let bitmap = NSBitmapImageRep(data: tiffData) else { return }
|
if let rep = image.representations.first as? NSBitmapImageRep {
|
||||||
|
bitmap = rep
|
||||||
|
} else if let cgRef = image.cgImage(forProposedRect: nil, context: nil, hints: nil) {
|
||||||
|
bitmap = NSBitmapImageRep(cgImage: cgRef)
|
||||||
|
} else {
|
||||||
|
NSLog("SnipMC: could not create bitmap for saving")
|
||||||
|
return
|
||||||
|
}
|
||||||
let fileType: NSBitmapImageRep.FileType = imageFormat == .jpg ? .jpeg : .png
|
let fileType: NSBitmapImageRep.FileType = imageFormat == .jpg ? .jpeg : .png
|
||||||
let properties: [NSBitmapImageRep.PropertyKey: Any] = fileType == .jpeg
|
let properties: [NSBitmapImageRep.PropertyKey: Any] = fileType == .jpeg
|
||||||
? [.compressionFactor: 0.9] : [:]
|
? [.compressionFactor: 0.9] : [:]
|
||||||
|
|
||||||
guard let data = bitmap.representation(using: fileType, properties: properties) else { return }
|
guard let data = bitmap.representation(using: fileType, properties: properties) else {
|
||||||
|
NSLog("SnipMC: could not encode image as \(imageFormat.rawValue)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try data.write(to: url, options: .atomic)
|
try data.write(to: url, options: .atomic)
|
||||||
|
NSLog("SnipMC: saved to \(url.path)")
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("SnipMC: failed to save annotated image: \(error)")
|
NSLog("SnipMC: failed to save annotated image: \(error)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,21 +26,39 @@ enum AnnotationRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func render(annotations: [AnnotationItem], onto image: NSImage) -> NSImage {
|
static func render(annotations: [AnnotationItem], onto image: NSImage) -> NSImage {
|
||||||
|
guard let srcCG = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return image }
|
||||||
|
let w = srcCG.width
|
||||||
|
let h = srcCG.height
|
||||||
let size = image.size
|
let size = image.size
|
||||||
let result = NSImage(size: size)
|
|
||||||
result.lockFocusFlipped(true)
|
|
||||||
|
|
||||||
image.draw(in: NSRect(origin: .zero, size: size),
|
guard let cs = srcCG.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB),
|
||||||
from: .zero, operation: .sourceOver, fraction: 1,
|
let ctx = CGContext(
|
||||||
respectFlipped: true, hints: nil)
|
data: nil, width: w, height: h,
|
||||||
|
bitsPerComponent: 8, bytesPerRow: 0, space: cs,
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||||
|
) else { return image }
|
||||||
|
|
||||||
|
ctx.draw(srcCG, in: CGRect(x: 0, y: 0, width: w, height: h))
|
||||||
|
|
||||||
|
ctx.translateBy(x: 0, y: CGFloat(h))
|
||||||
|
let sx = CGFloat(w) / size.width
|
||||||
|
let sy = CGFloat(h) / size.height
|
||||||
|
ctx.scaleBy(x: sx, y: -sy)
|
||||||
|
|
||||||
|
NSGraphicsContext.saveGraphicsState()
|
||||||
|
NSGraphicsContext.current = NSGraphicsContext(cgContext: ctx, flipped: true)
|
||||||
|
|
||||||
if let context = NSGraphicsContext.current?.cgContext {
|
|
||||||
for item in annotations {
|
for item in annotations {
|
||||||
draw(item, in: context)
|
draw(item, in: ctx)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.unlockFocus()
|
NSGraphicsContext.restoreGraphicsState()
|
||||||
|
|
||||||
|
guard let resultCG = ctx.makeImage() else { return image }
|
||||||
|
let bitmapRep = NSBitmapImageRep(cgImage: resultCG)
|
||||||
|
bitmapRep.size = size
|
||||||
|
let result = NSImage(size: size)
|
||||||
|
result.addRepresentation(bitmapRep)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user