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) { 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 kPSDBlendModeHardLight } 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 { return kPSDBlendModeVividLight } 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 { return kPSDBlendModeHardLight } 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 } @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() } } } } } } }