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

326 lines
14 KiB
Swift
Raw Normal View History

2020-03-10 14:23:54 -04:00
import Cocoa
2021-09-29 17:27:13 -04:00
import ZIPFoundation
import AVFoundation
2020-03-10 14:23:54 -04:00
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
2021-09-29 17:27:13 -04:00
var exporter: AVAssetExportSession?
@IBAction func showInfoAction(_ sender: Any) {
NSApplication.shared.keyWindow?.contentViewController?.performSegue(withIdentifier: "showInfo", sender: self)
}
2021-05-09 21:44:04 -04:00
@IBAction func showTimelapseAction(_ sender: Any) {
NSApplication.shared.keyWindow?.contentViewController?.performSegue(withIdentifier: "showTimelapse", sender: self)
}
func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
2021-05-09 21:44:04 -04:00
// Show timelapse and show info buttons
if(item.tag == 67 || item.tag == 68 || item.tag == 100 || item.tag == 101 || item.tag == 102) {
2020-03-11 22:15:05 -04:00
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 {
switch(layer.data.blendMode) {
case .Normal:
return kPSDBlendModeNormal
case .Multiply:
return kPSDBlendModeMultiply
case .Screen:
return kPSDBlendModeScreen
case .Add:
return kPSDBlendModeAdd
case .Lighten:
return kPSDBlendModeLighten
case .Exclusion:
return kPSDBlendModeExclusion
case .Difference:
return kPSDBlendModeDifference
case .Subtract:
return kPSDBlendModeSubtract
case .LinearBurn:
return kPSDBlendModeLinearBurn
case .ColorDodge:
return kPSDBlendModeColorDodge
case .ColorBurn:
return kPSDBlendModeColorBurn
case .Overlay:
return kPSDBlendModeOverlay
case .HardLight:
return kPSDBlendModeHardLight
case .Color:
return kPSDBlendModeColor
case .Luminosity:
return kPSDBlendModeLuminosity
case .Hue:
return kPSDBlendModeHue
case .Saturation:
return kPSDBlendModeSaturation
case .SoftLight:
return kPSDBlendSoftLight
case .Darken:
return kPSDBlendModeDarken
case .HardMix:
return kPSDBlendModeHardMix
case .VividLight:
return kPSDBlendModeVividLight
case .LinearLight:
return kPSdBlendModeLinearLight
case .PinLight:
return kPSDBlendModePinLight
case .LighterColor:
return kPSDBlendModeLighterColor
case .DarkerColor:
return kPSDBlendModeDarkerColor
case .Divide:
return kPSDBlendModeDivide
}
}
@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"])
2022-02-14 12:10:08 -04:00
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 {
var degreesToRotate = 0.0
if document!.info.orientation == 3 {
degreesToRotate = 90
} else if document!.info.orientation == 4 {
degreesToRotate = -90
}
var flipHoriz = false
var flipVert = false
if document!.info.flippedHorizontally && (document!.info.orientation == 1 || document!.info.orientation == 2) {
flipHoriz = true
} else if document!.info.flippedHorizontally && (document!.info.orientation == 3 || document!.info.orientation == 4) {
flipVert = true
} else if document!.info.flippedVertically && (document!.info.orientation == 1 || document!.info.orientation == 2) {
flipVert = true
} else if !document!.info.flippedVertically && (document!.info.orientation == 3 || document!.info.orientation == 4) {
flipHoriz = true
}
var rect = document!.info.cgRect()
rect.size.width = document!.info.cgSize().height
rect.size.height = document!.info.cgSize().width
let writer = PSDWriter(documentSize: rect.size)
writer?.shouldUnpremultiplyLayerData = true
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big)
let ccgContext = CGContext(data: nil, width: Int(rect.size.width), height: Int(rect.size.height), bitsPerComponent: 8, bytesPerRow: Int(rect.size.width) * 4, space: document!.info.colorSpace, bitmapInfo: bitmapInfo.rawValue)
ccgContext?.setFillColor(document!.info.backgroundColor)
ccgContext?.fill(rect)
writer?.addLayer(with: ccgContext?.makeImage(), andName: "Background", andOpacity: 1.0, andOffset: .zero)
let engine = SilicaEngine()
for layer in document!.info.layers.reversed() {
let finalCgImage = engine.simpleDrawLayer(document: document!.info, layer: layer.data)!.rotate(angle: self.degreesToRadians(degreesToRotate), flipHorizontally: flipHoriz, flipVertically: flipVert)
if(layer.mask != nil) {
let mask = engine.simpleDrawLayer(document: document!.info, layer: layer.data)!.rotate(angle: self.degreesToRadians(degreesToRotate), flipHorizontally: flipHoriz, flipVertically: flipVert)
writer?.addLayer(with: mask, 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!)
}
}
}
}
func degreesToRadians(_ value: Double) -> Double {
return value * .pi / 180
}
@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!)
}
}
}
2021-09-29 17:27:13 -04:00
@IBAction func exportTimelapseAction(_ sender: Any) {
2021-09-29 17:54:04 -04:00
guard let originalWindow = NSApplication.shared.keyWindow else {
return
}
let document = originalWindow.windowController?.document as? Document;
2021-09-29 17:27:13 -04:00
let savePanel = NSSavePanel()
savePanel.title = "Save Timelapse"
savePanel.allowedFileTypes = ["public.mpeg-4"]
savePanel.nameFieldStringValue = (document?.getIdealFilename())!
addAccessoryView(savePanel, fileTypes: ["mp4", "mov", "heic"])
2021-09-29 17:27:13 -04:00
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] = []
2021-09-29 17:54:04 -04:00
var duration = CMTime.zero
2021-09-29 17:27:13 -04:00
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)
2021-09-29 17:54:04 -04:00
guard let track = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else {
return
2021-09-29 17:27:13 -04:00
}
2021-09-29 17:54:04 -04:00
try? track.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration),
of: asset.tracks(withMediaType: .video)[0],
at: duration)
2021-09-29 17:27:13 -04:00
duration = CMTimeAdd(duration, asset.duration)
2021-09-29 17:54:04 -04:00
let opacityInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
opacityInstruction.setOpacity(0.0, at: duration + asset.duration)
2021-09-29 17:27:13 -04:00
2021-09-29 17:54:04 -04:00
instructions.append(opacityInstruction)
2021-09-29 17:27:13 -04:00
}
}
let mainInstruction = AVMutableVideoCompositionInstruction()
2021-09-29 17:54:04 -04:00
mainInstruction.timeRange = CMTimeRangeMake(start: .zero, duration: duration)
2021-09-29 17:27:13 -04:00
mainInstruction.layerInstructions = instructions
let mainComposition = AVMutableVideoComposition()
mainComposition.instructions = [mainInstruction]
mainComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
2021-09-29 17:54:04 -04:00
mainComposition.renderSize = CGSize(width: document!.info.videoFrame.0, height: document!.info.videoFrame.1)
2021-09-29 17:27:13 -04:00
2022-06-15 18:28:06 -04:00
self.exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetMediumQuality)
2021-09-29 17:27:13 -04:00
self.exporter?.outputURL = savePanel.url!
self.exporter?.videoComposition = mainComposition
2022-06-15 18:28:06 -04:00
try? FileManager.default.removeItem(at: (self.exporter?.outputURL)!)
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;
}
2021-09-29 17:54:04 -04:00
let alert = NSAlert()
alert.messageText = "Exporting timelapse..."
alert.addButton(withTitle: "Cancel")
2022-06-15 18:28:06 -04:00
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
alert.messageText = String(format: "Exporting timelapse... %2.0f%%", self.exporter!.progress * 100.0)
2021-09-29 17:54:04 -04:00
}
2021-09-29 17:27:13 -04:00
self.exporter?.exportAsynchronously {
2022-06-15 18:28:06 -04:00
timer.invalidate()
DispatchQueue.main.sync {
if self.exporter?.status == .completed {
2021-09-29 17:54:04 -04:00
alert.window.close()
2022-06-15 18:28:06 -04:00
} else {
if let errorMessage = self.exporter?.error?.localizedDescription {
alert.messageText = errorMessage
} else {
alert.messageText = "An unknown error occurred."
}
2021-09-29 17:54:04 -04:00
}
}
2021-09-29 17:27:13 -04:00
}
2022-06-15 18:28:06 -04:00
alert.beginSheetModal(for: originalWindow) { (resonse) in
self.exporter?.cancelExport()
alert.window.close()
}
2021-09-29 17:27:13 -04:00
}
}
}
}