1
Fork 0
This repository has been archived on 2025-04-12. You can view files and clone it, but cannot push or open issues or pull requests.
silica-viewer/SilicaViewer/AppDelegate.swift
Joshua Goins acda007eee Add the rest of the supported Photoshop blend modes, fix clipping
This is not the final blending code, it will go through another refactor
but it's an improvement from before.
2022-05-19 11:00:51 -04:00

316 lines
13 KiB
Swift

import Cocoa
import ZIPFoundation
import AVFoundation
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
var exporter: AVAssetExportSession?
@IBAction func showInfoAction(_ sender: Any) {
NSApplication.shared.keyWindow?.contentViewController?.performSegue(withIdentifier: "showInfo", sender: self)
}
@IBAction func showTimelapseAction(_ sender: Any) {
NSApplication.shared.keyWindow?.contentViewController?.performSegue(withIdentifier: "showTimelapse", sender: self)
}
func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
// Show timelapse and show info buttons
if(item.tag == 67 || item.tag == 68 || item.tag == 100 || item.tag == 101 || item.tag == 102) {
return NSApplication.shared.keyWindow != nil
}
return true
}
func addAccessoryView(_ savePanel : NSSavePanel, fileTypes : Array<String>) {
let accessoryView = (ExportAccessoryView.fromNib()! as ExportAccessoryView)
accessoryView.setSavePanel(savePanel)
accessoryView.setPossibleOptions(fileTypes)
savePanel.accessoryView = accessoryView as NSView
}
func getBitmapType(_ ext : String) -> NSBitmapImageRep.FileType {
switch(ext) {
case "tiff":
return .tiff
case "bmp":
return .bmp
case "jpeg":
return .jpeg
case "png":
return .png
default:
return .png
}
}
func getPSDBlendMode(_ layer : SilicaLayer) -> PSDBlendModes {
if layer.data.blendMode == 1 {
return kPSDBlendModeMultiply
}
if layer.data.blendMode == 10 {
return kPSDBlendModeColorBurn
}
if layer.data.blendMode == 19 {
return kPSDBlendModeDarken
}
if layer.data.blendMode == 8 {
return kPSDBlendModeLinearBurn
}
if layer.data.blendMode == 4 {
return kPSDBlendModeLighten
}
if layer.data.blendMode == 2 {
return kPSDBlendModeScreen
}
if layer.data.blendMode == 13 {
return kPSDBlendModeColor
}
if layer.data.blendMode == 9 {
return kPSDBlendModeColorDodge
}
if layer.data.blendMode == 3 {
return kPSDBlendModeAdd
}
if layer.data.blendMode == 0 && layer.data.blendMode != layer.data.extendedBlend {
if layer.data.extendedBlend == 25 {
return kPSDBlendModeDarkerColor
}
if layer.data.extendedBlend == 24 {
return kPSDBlendModeLighterColor
}
if layer.data.extendedBlend == 21 {
return kPSDBlendModeVividLight
}
if layer.data.extendedBlend == 22 {
return kPSdBlendModeLinearLight
}
if layer.data.extendedBlend == 23 {
return kPSDBlendModePinLight
}
if layer.data.extendedBlend == 20 {
return kPSDBlendModeHardMix
}
if layer.data.extendedBlend == 26 {
return kPSDBlendModeDivide
}
}
if layer.data.blendMode == 11 {
return kPSDBlendModeOverlay
}
if layer.data.blendMode == 17 {
return kPSDBlendSoftLight
}
if layer.data.blendMode == 12 {
return kPSDBlendModeHardLight
}
if layer.data.blendMode == 6 {
return kPSDBlendModeDifference
}
if layer.data.blendMode == 5 {
return kPSDBlendModeExclusion
}
if layer.data.blendMode == 7 {
return kPSDBlendModeSubtract
}
if layer.data.blendMode == 15 {
return kPSDBlendModeHue
}
if layer.data.blendMode == 16 {
return kPSDBlendModeSaturation
}
if layer.data.blendMode == 13 {
return kPSDBlendModeColor
}
if layer.data.blendMode == 14 {
return kPSDBlendModeLuminosity
}
return kPSDBlendModeNormal
}
@IBAction func exportAction(_ sender: Any) {
let document = NSApplication.shared.keyWindow?.windowController?.document as? Document;
let savePanel = NSSavePanel()
savePanel.title = "Save"
savePanel.allowedFileTypes = ["tiff"]
savePanel.nameFieldStringValue = (document?.getIdealFilename())!
addAccessoryView(savePanel, fileTypes: ["png", "tiff", "jpeg", "psd"])
savePanel.begin { (result) in
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
if savePanel.allowedFileTypes![0] != "psd" {
let canvas = document?.makeComposite()
let canvasTiff = canvas?.tiffRepresentation
let bitmapImage = NSBitmapImageRep(data: canvasTiff!)
let canvasPng = bitmapImage!.representation(using: self.getBitmapType(savePanel.allowedFileTypes![0]), properties: [:])
try? canvasPng?.write(to: savePanel.url!)
} else {
let writer = PSDWriter(documentSize: (document?.info.cgSize)!)
let cgImage = document?.makeComposite()?.cgImage(forProposedRect: nil, context: nil, hints: nil)
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big)
let ccgContext = CGContext(data: nil, width: document!.info.width, height: document!.info.height, bitsPerComponent: 8, bytesPerRow: document!.info.width * 4, space: document!.info.colorSpace, bitmapInfo: bitmapInfo.rawValue)
ccgContext?.setFillColor(document!.info.backgroundColor)
ccgContext?.fill(document!.info.cgRect)
let context = CIContext()
writer?.addLayer(with: ccgContext?.makeImage(), andName: "Background", andOpacity: 1.0, andOffset: .zero)
var masterImage = CIImage(cgImage: cgImage!)
for layer in document!.info.layers.reversed() {
var test : CGImage? = nil
masterImage = document!.blendLayer(layer, previousImage: &test)
let finalCgImage = context.createCGImage(masterImage, from: document!.info.cgRect, format: .RGBA8, colorSpace: document!.info.colorSpace)
if(layer.mask != nil) {
writer?.addLayer(with: document!.makeBlendImage(layer), andName: layer.name + " (Mask)", andOpacity: 1.0, andOffset: .zero)
}
writer?.addLayer(with: finalCgImage, andName: layer.name, andOpacity: Float(layer.data.opacity), andOffset: .zero, andBlendMode: Int(self.getPSDBlendMode(layer).rawValue), andIsNonBaseLayer: layer.clipped)
}
let data = writer?.createPSDData()
try? data?.write(to: savePanel.url!)
}
}
}
}
@IBAction func exportThumbnailAction(_ sender: Any) {
let document = NSApplication.shared.keyWindow?.windowController?.document as? Document;
let savePanel = NSSavePanel()
savePanel.title = "Save Thumbnail"
savePanel.allowedFileTypes = ["png"]
savePanel.nameFieldStringValue = (document?.getIdealFilename())!
addAccessoryView(savePanel, fileTypes: ["png", "tiff", "jpeg"])
savePanel.begin { (result) in
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
let canvas = document?.makeThumbnail()
let canvasTiff = canvas?.tiffRepresentation
let bitmapImage = NSBitmapImageRep(data: canvasTiff!)
let canvasPng = bitmapImage!.representation(using: self.getBitmapType(savePanel.allowedFileTypes![0]), properties: [:])
try? canvasPng?.write(to: savePanel.url!)
}
}
}
@IBAction func exportTimelapseAction(_ sender: Any) {
guard let originalWindow = NSApplication.shared.keyWindow else {
return
}
let document = originalWindow.windowController?.document as? Document;
let savePanel = NSSavePanel()
savePanel.title = "Save Timelapse"
savePanel.allowedFileTypes = ["public.mpeg-4"]
savePanel.nameFieldStringValue = (document?.getIdealFilename())!
addAccessoryView(savePanel, fileTypes: ["mp4", "mov", "heic"])
savePanel.begin { (result) in
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
guard let archive = Archive(data: (document?.data)!, accessMode: Archive.AccessMode.read) else {
return
}
let directory = NSTemporaryDirectory()
let mixComposition = AVMutableComposition()
var instructions: [AVMutableVideoCompositionLayerInstruction] = []
var duration = CMTime.zero
for entry in archive.makeIterator() {
if entry.path.contains(VideoPath) {
let fileName = NSUUID().uuidString + ".mp4"
// This returns a URL? even though it is an NSURL class method
let fullURL = NSURL.fileURL(withPathComponents: [directory, fileName])!
let _ = try? archive.extract(entry, to: fullURL)
let asset = AVAsset(url: fullURL)
guard let track = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else {
return
}
try? track.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration),
of: asset.tracks(withMediaType: .video)[0],
at: duration)
duration = CMTimeAdd(duration, asset.duration)
let opacityInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
opacityInstruction.setOpacity(0.0, at: duration + asset.duration)
instructions.append(opacityInstruction)
}
}
let mainInstruction = AVMutableVideoCompositionInstruction()
mainInstruction.timeRange = CMTimeRangeMake(start: .zero, duration: duration)
mainInstruction.layerInstructions = instructions
let mainComposition = AVMutableVideoComposition()
mainComposition.instructions = [mainInstruction]
mainComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
mainComposition.renderSize = CGSize(width: document!.info.videoFrame.0, height: document!.info.videoFrame.1)
self.exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)
self.exporter?.outputURL = savePanel.url!
self.exporter?.videoComposition = mainComposition
switch(savePanel.allowedFileTypes![0]) {
case "mp4":
self.exporter?.outputFileType = .mp4;
break;
case "mov":
self.exporter?.outputFileType = .mov;
break;
case "heic":
self.exporter?.outputFileType = .heic;
break;
default:
break;
}
let alert = NSAlert()
alert.messageText = "Exporting timelapse..."
alert.addButton(withTitle: "Cancel")
alert.beginSheetModal(for: originalWindow) { (resonse) in
self.exporter?.cancelExport()
alert.window.close()
}
self.exporter?.exportAsynchronously {
if self.exporter?.status != .cancelled {
DispatchQueue.main.sync {
alert.window.close()
}
}
}
}
}
}
}