This is not the final blending code, it will go through another refactor but it's an improvement from before.
316 lines
13 KiB
Swift
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|