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
|
2020-03-10 16:23:25 -04:00
|
|
|
class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
|
2021-09-29 17:27:13 -04:00
|
|
|
var exporter: AVAssetExportSession?
|
|
|
|
|
2020-03-10 16:23:25 -04:00
|
|
|
@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)
|
|
|
|
}
|
|
|
|
|
2020-03-10 16:23:25 -04:00
|
|
|
func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
2021-05-09 21:44:04 -04:00
|
|
|
// Show timelapse and show info buttons
|
2022-02-14 12:38:08 -04:00
|
|
|
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
|
2020-03-10 16:23:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
2021-09-16 19:03:32 -04:00
|
|
|
|
2022-02-28 12:19:08 -04:00
|
|
|
func addAccessoryView(_ savePanel : NSSavePanel, fileTypes : Array<String>) {
|
2022-02-28 12:06:08 -04:00
|
|
|
let accessoryView = (ExportAccessoryView.fromNib()! as ExportAccessoryView)
|
|
|
|
accessoryView.setSavePanel(savePanel)
|
2022-02-28 12:19:08 -04:00
|
|
|
accessoryView.setPossibleOptions(fileTypes)
|
2022-02-28 12:06:08 -04:00
|
|
|
|
|
|
|
savePanel.accessoryView = accessoryView as NSView
|
|
|
|
}
|
|
|
|
|
2022-02-28 12:19:08 -04:00
|
|
|
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 {
|
2022-05-10 00:43:28 -04:00
|
|
|
return kPSDBlendModeHardLight
|
2022-02-28 12:19:08 -04:00
|
|
|
}
|
|
|
|
if layer.data.blendMode == 9 {
|
|
|
|
return kPSDBlendModeColorDodge
|
|
|
|
}
|
|
|
|
if layer.data.blendMode == 3 {
|
|
|
|
//blendMode = kPSDBlendModeSubtract
|
|
|
|
}
|
|
|
|
if layer.data.blendMode == 0 && layer.data.blendMode != layer.data.extendedBlend {
|
|
|
|
if layer.data.extendedBlend == 25 {
|
|
|
|
return kPSDBlendModeDarkerColor
|
|
|
|
}
|
|
|
|
if layer.data.extendedBlend == 24 {
|
|
|
|
//blendMode = kPSDBlendModeLighterColor
|
|
|
|
}
|
|
|
|
if layer.data.extendedBlend == 21 {
|
2022-05-10 00:43:28 -04:00
|
|
|
return kPSDBlendModeVividLight
|
2022-02-28 12:19:08 -04:00
|
|
|
}
|
|
|
|
if layer.data.extendedBlend == 22 {
|
|
|
|
//blendMode = kPSdBlendModeLinearLight
|
|
|
|
}
|
|
|
|
if layer.data.extendedBlend == 23 {
|
|
|
|
//blendMode = kPSDBlendModePinLight
|
|
|
|
}
|
|
|
|
if layer.data.extendedBlend == 20 {
|
|
|
|
//blendMode = kPSDBlendModeHardMix
|
|
|
|
}
|
|
|
|
if layer.data.extendedBlend == 26 {
|
|
|
|
//blendMode = kPSDBlendModeDivide
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if layer.data.blendMode == 11 {
|
|
|
|
//blendMode = kPSDBlendModeOverlay
|
|
|
|
}
|
|
|
|
if layer.data.blendMode == 17 {
|
|
|
|
//return .softLight
|
|
|
|
}
|
|
|
|
if layer.data.blendMode == 12 {
|
2022-05-10 00:43:28 -04:00
|
|
|
return kPSDBlendModeHardLight
|
2022-02-28 12:19:08 -04:00
|
|
|
}
|
|
|
|
if layer.data.blendMode == 6 {
|
|
|
|
//return .difference
|
|
|
|
}
|
|
|
|
if layer.data.blendMode == 5 {
|
|
|
|
//return .exclusion
|
|
|
|
}
|
|
|
|
if layer.data.blendMode == 7 {
|
|
|
|
//return .componentAdd
|
|
|
|
}
|
|
|
|
if layer.data.blendMode == 15 {
|
|
|
|
//return .hue
|
|
|
|
}
|
|
|
|
if layer.data.blendMode == 16 {
|
|
|
|
//return .saturation
|
|
|
|
}
|
|
|
|
if layer.data.blendMode == 13 {
|
|
|
|
//return .color
|
|
|
|
}
|
|
|
|
if layer.data.blendMode == 14 {
|
|
|
|
//return .luminosity
|
|
|
|
}
|
|
|
|
|
|
|
|
return kPSDBlendModeNormal
|
|
|
|
}
|
|
|
|
|
2021-09-16 19:03:32 -04:00
|
|
|
@IBAction func exportAction(_ sender: Any) {
|
|
|
|
let document = NSApplication.shared.keyWindow?.windowController?.document as? Document;
|
|
|
|
|
|
|
|
let savePanel = NSSavePanel()
|
|
|
|
savePanel.title = "Save"
|
2022-02-28 12:04:08 -04:00
|
|
|
savePanel.allowedFileTypes = ["tiff"]
|
|
|
|
savePanel.nameFieldStringValue = (document?.getIdealFilename())!
|
|
|
|
|
2022-02-28 12:19:08 -04:00
|
|
|
addAccessoryView(savePanel, fileTypes: ["png", "tiff", "jpeg", "psd"])
|
2022-02-14 12:10:08 -04:00
|
|
|
|
2021-09-16 19:03:32 -04:00
|
|
|
savePanel.begin { (result) in
|
|
|
|
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
|
2022-02-28 12:04:08 -04:00
|
|
|
if savePanel.allowedFileTypes![0] != "psd" {
|
|
|
|
let canvas = document?.makeComposite()
|
|
|
|
let canvasTiff = canvas?.tiffRepresentation
|
|
|
|
let bitmapImage = NSBitmapImageRep(data: canvasTiff!)
|
2022-02-28 12:19:08 -04:00
|
|
|
let canvasPng = bitmapImage!.representation(using: self.getBitmapType(savePanel.allowedFileTypes![0]), properties: [:])
|
2022-02-28 12:04:08 -04:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-02-28 12:19:08 -04:00
|
|
|
writer?.addLayer(with: finalCgImage, andName: layer.name, andOpacity: Float(layer.data.opacity), andOffset: .zero, andBlendMode: Int(self.getPSDBlendMode(layer).rawValue), andIsNonBaseLayer: layer.clipped)
|
2022-02-28 12:04:08 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
let data = writer?.createPSDData()
|
|
|
|
try? data?.write(to: savePanel.url!)
|
|
|
|
}
|
2021-09-16 19:03:32 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func exportThumbnailAction(_ sender: Any) {
|
|
|
|
let document = NSApplication.shared.keyWindow?.windowController?.document as? Document;
|
|
|
|
|
|
|
|
let savePanel = NSSavePanel()
|
|
|
|
savePanel.title = "Save Thumbnail"
|
2022-02-28 12:19:08 -04:00
|
|
|
savePanel.allowedFileTypes = ["png"]
|
2022-02-28 12:04:08 -04:00
|
|
|
savePanel.nameFieldStringValue = (document?.getIdealFilename())!
|
|
|
|
|
2022-02-28 12:19:08 -04:00
|
|
|
addAccessoryView(savePanel, fileTypes: ["png", "tiff", "jpeg"])
|
2022-02-28 12:04:08 -04:00
|
|
|
|
2021-09-16 19:03:32 -04:00
|
|
|
savePanel.begin { (result) in
|
|
|
|
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
|
|
|
|
let canvas = document?.makeThumbnail()
|
|
|
|
let canvasTiff = canvas?.tiffRepresentation
|
|
|
|
let bitmapImage = NSBitmapImageRep(data: canvasTiff!)
|
2022-02-28 12:19:08 -04:00
|
|
|
let canvasPng = bitmapImage!.representation(using: self.getBitmapType(savePanel.allowedFileTypes![0]), properties: [:])
|
2021-09-16 19:03:32 -04:00
|
|
|
|
|
|
|
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"]
|
2022-02-28 12:03:08 -04:00
|
|
|
savePanel.nameFieldStringValue = (document?.getIdealFilename())!
|
2022-02-28 12:06:08 -04:00
|
|
|
|
2022-02-28 12:19:08 -04:00
|
|
|
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
|
|
|
|
2021-09-29 17:54:04 -04:00
|
|
|
self.exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)
|
2021-09-29 17:27:13 -04:00
|
|
|
self.exporter?.outputURL = savePanel.url!
|
|
|
|
self.exporter?.videoComposition = mainComposition
|
|
|
|
|
2022-02-28 12:19:08 -04:00
|
|
|
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")
|
|
|
|
alert.beginSheetModal(for: originalWindow) { (resonse) in
|
|
|
|
self.exporter?.cancelExport()
|
|
|
|
alert.window.close()
|
|
|
|
}
|
|
|
|
|
2021-09-29 17:27:13 -04:00
|
|
|
self.exporter?.exportAsynchronously {
|
2021-09-29 17:54:04 -04:00
|
|
|
if self.exporter?.status != .cancelled {
|
|
|
|
DispatchQueue.main.sync {
|
|
|
|
alert.window.close()
|
|
|
|
}
|
|
|
|
}
|
2021-09-29 17:27:13 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-10 14:23:54 -04:00
|
|
|
}
|
|
|
|
|