├── GIFPop
├── gifsicle
├── dropGifHere.png
├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ ├── GIFPopIcon16.png
│ │ ├── GIFPopIcon32.png
│ │ ├── GIFPopIcon64.png
│ │ ├── GIFPopIcon1024.png
│ │ ├── GIFPopIcon128.png
│ │ ├── GIFPopIcon256-1.png
│ │ ├── GIFPopIcon256.png
│ │ ├── GIFPopIcon32-1.png
│ │ ├── GIFPopIcon512-1.png
│ │ ├── GIFPopIcon512.png
│ │ └── Contents.json
├── AboutGIFPop.swift
├── Info.plist
├── HelpText.rtf
├── AppDelegate.swift
├── Gifsicle.swift
├── GIFPreview.swift
├── Resizer.swift
└── Base.lproj
│ └── MainMenu.xib
├── gifPopDemo.gif
├── GIFPop.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── project.pbxproj
├── .gitignore
└── README.md
/GIFPop/gifsicle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/gifsicle
--------------------------------------------------------------------------------
/gifPopDemo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/gifPopDemo.gif
--------------------------------------------------------------------------------
/GIFPop/dropGifHere.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/dropGifHere.png
--------------------------------------------------------------------------------
/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon16.png
--------------------------------------------------------------------------------
/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon32.png
--------------------------------------------------------------------------------
/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon64.png
--------------------------------------------------------------------------------
/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon1024.png
--------------------------------------------------------------------------------
/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon128.png
--------------------------------------------------------------------------------
/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon256-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon256-1.png
--------------------------------------------------------------------------------
/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon256.png
--------------------------------------------------------------------------------
/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon32-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon32-1.png
--------------------------------------------------------------------------------
/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon512-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon512-1.png
--------------------------------------------------------------------------------
/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/GIFPop/HEAD/GIFPop/Assets.xcassets/AppIcon.appiconset/GIFPopIcon512.png
--------------------------------------------------------------------------------
/GIFPop.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Build generated
2 | build/
3 | DerivedData/
4 | .DS_Store
5 | ## Various settings
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata/
15 |
16 | ## Other
17 | *.moved-aside
18 | *.xcuserstate
19 |
20 | ## Obj-C/Swift specific
21 | *.hmap
22 | *.ipa
23 | *.dSYM.zip
24 | *.dSYM
25 |
26 | ## GIFPop
27 | GIFPop/GIFPopIcon*
28 |
--------------------------------------------------------------------------------
/GIFPop/AboutGIFPop.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AboutGIFPop.swift
3 | // GIFPop
4 | //
5 | // Created by Matt Reagan on 10/18/16.
6 | // Copyright © 2016 Matt Reagan. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import Foundation
11 |
12 | class AboutGIFPop: NSObject {
13 | @IBOutlet weak var aboutWindow: NSWindow!
14 | @IBOutlet weak var aboutIcon: NSImageView!
15 | @IBOutlet var aboutTextView: NSTextView!
16 | @IBOutlet weak var aboutOKButton: NSButton!
17 | @IBOutlet weak var resizer: Resizer!
18 |
19 | @IBAction func helpButtonClicked(_ sender: AnyObject) {
20 | aboutTextView.readRTFD(fromFile: Bundle.main.path(forResource: "HelpText", ofType: "rtf")!)
21 | aboutIcon.image = NSImage.init(imageLiteralResourceName: "AppIcon")
22 | resizer.resizerWindow.beginSheet(aboutWindow) { (response: NSModalResponse) in }
23 | aboutWindow.makeFirstResponder(aboutOKButton)
24 | }
25 |
26 | @IBAction func aboutOKClicked(_ sender: AnyObject) {
27 | resizer.resizerWindow?.endSheet(aboutWindow)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/GIFPop/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "GIFPopIcon16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "GIFPopIcon32-1.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "GIFPopIcon32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "GIFPopIcon64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "GIFPopIcon128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "GIFPopIcon256-1.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "GIFPopIcon256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "GIFPopIcon512-1.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "GIFPopIcon512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "GIFPopIcon1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/GIFPop/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDocumentTypes
8 |
9 |
10 | CFBundleTypeExtensions
11 |
12 | gif
13 |
14 | CFBundleTypeMIMETypes
15 |
16 | image/gif
17 |
18 | CFBundleTypeName
19 | Animated GIFs
20 | CFBundleTypeRole
21 | Viewer
22 | LSTypeIsPackage
23 | 0
24 |
25 |
26 | CFBundleExecutable
27 | $(EXECUTABLE_NAME)
28 | CFBundleIconFile
29 |
30 | CFBundleIdentifier
31 | $(PRODUCT_BUNDLE_IDENTIFIER)
32 | CFBundleInfoDictionaryVersion
33 | 6.0
34 | CFBundleName
35 | $(PRODUCT_NAME)
36 | CFBundlePackageType
37 | APPL
38 | CFBundleShortVersionString
39 | 0.9
40 | CFBundleVersion
41 | .9
42 | LSMinimumSystemVersion
43 | $(MACOSX_DEPLOYMENT_TARGET)
44 | NSHumanReadableCopyright
45 | Copyright © 2016 Matt Reagan. All rights reserved.
46 | NSMainNibFile
47 | MainMenu
48 | NSPrincipalClass
49 | NSApplication
50 |
51 |
52 |
--------------------------------------------------------------------------------
/GIFPop/HelpText.rtf:
--------------------------------------------------------------------------------
1 | {\rtf1\ansi\ansicpg1252\cocoartf1504
2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;}
3 | {\colortbl;\red255\green255\blue255;\red155\green44\blue1;\red77\green77\blue77;\red52\green52\blue52;
4 | \red0\green0\blue0;}
5 | {\*\expandedcolortbl;\csgray\c100000;\csgenericrgb\c60784\c17255\c392;\csgray\c37475;\csgray\c26515;
6 | \csgenericrgb\c0\c0\c0;}
7 | \margl1440\margr1440\vieww10800\viewh8400\viewkind0
8 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
9 |
10 | \f0\b\fs48 \cf2 GIFPop
11 | \i !
12 | \i0\b0\fs24 \cf0 \
13 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\sl336\slmult1\pardirnatural\partightenfactor0
14 | \cf3 Animated GIF Resizer for macOS\cf0 \
15 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
16 |
17 | \fs26 \cf0 \
18 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\sl288\slmult1\pardirnatural\partightenfactor0
19 |
20 | \b \cf4 GIFPop
21 | \b0 \cf5 is a simplified UI wrapper for\
22 |
23 | \b \cf4 Gifsicle
24 | \b0 \cf5 by Eddie Kohler \
25 | Homepage: {\field{\*\fldinst{HYPERLINK "https://www.lcdf.org/gifsicle/"}}{\fldrslt lcdf.org/gifsicle/}}\cf0 \
26 | \
27 |
28 | \b \cf4 GIFPop
29 | \b0 \cf0 by {\field{\*\fldinst{HYPERLINK "http://sound-of-silence.com"}}{\fldrslt Matt Reagan}}\
30 | GitHub: {\field{\*\fldinst{HYPERLINK "http://github.com/matthewreagan/GIFPop"}}{\fldrslt github.com/matthewreagan/GIFPop}}\
31 | \
32 |
33 | \b \cf4 How To Use:
34 | \b0 \cf0 \
35 | 1. Drag a .gif onto the preview pane\
36 | 2. Drag slider to resize, and choose color, trim, or optimization options\
37 | 3. Click
38 | \i Save GIF...}
--------------------------------------------------------------------------------
/GIFPop/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // GIFPop
4 | //
5 | // Created by Matt Reagan on 10/9/16.
6 | // Copyright © 2016 Matt Reagan. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | @NSApplicationMain
12 |
13 | class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
14 | @IBOutlet weak var window: NSWindow!
15 | @IBOutlet weak var resizer: Resizer!
16 | @IBOutlet weak var aboutGIFPop: AboutGIFPop!
17 |
18 | func applicationDidFinishLaunching(_ notification: Notification) {
19 | let hasShownAboutWindowForFirstLaunchDefaultsKey =
20 | "GIFPopHasShownAboutWindowForFirstLaunch"
21 |
22 | let hasShownAbout = UserDefaults.standard.bool(forKey: hasShownAboutWindowForFirstLaunchDefaultsKey)
23 |
24 | if !hasShownAbout {
25 | aboutGIFPop.helpButtonClicked(self)
26 | UserDefaults.standard.set(true, forKey: hasShownAboutWindowForFirstLaunchDefaultsKey)
27 | }
28 |
29 | window.delegate = self
30 | }
31 |
32 | func applicationWillTerminate(_ aNotification: Notification) {
33 | /* Clean up our copied GIF (in /tmp) if we can */
34 |
35 | if let gifPath = resizer.inputGIFPath {
36 | try? FileManager.default.removeItem(atPath: gifPath)
37 | }
38 | }
39 |
40 | func application(_ sender: NSApplication, openFile filename: String) -> Bool {
41 | guard let utiType = try? NSWorkspace.shared().type(ofFile: filename) else { return false }
42 |
43 | if utiType == (kUTTypeGIF as String) {
44 | resizer.loadGIFAtPath(pathToGIF: filename)
45 |
46 | return true
47 | }
48 |
49 | return false
50 | }
51 |
52 | func windowWillClose(_ notification: Notification) {
53 | NSApp.terminate(nil)
54 | }
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # GIFPop!
4 |
5 | GIFPop is a simple & free animated [.GIF](https://en.wikipedia.org/wiki/GIF) editor for Mac, written in [Swift](https://developer.apple.com/swift/). Its minimal UI wraps [gifsicle](https://github.com/kohler/gifsicle) under the hood, and currently supports the following:
6 |
7 | - Resizing
8 | - Optimizing
9 | - Trimming frames
10 | - Reducing colors
11 |
12 | ## How To Use
13 |
14 | 1. Drag a .gif onto the preview pane (or hit `Cmd-O` to choose a file)
15 | 2. Adjust the size, trimming, and other options if needed _(Note: only resizing is reflected in the preview, trimming and other options will be visible in the saved GIF)_
16 | 3. Click 'Save GIF...' to export the new GIF
17 |
18 | 
19 |
20 | ## Installation
21 |
22 | - Either download and run the .xcodeproj, or you can use the precompiled app (below)
23 | - If you have `gifsicle` installed, GIFPop will use your existing build for processing
24 | - If you don't have `gifsicle` installed, GIFPop will use the precompiled version bundled with app
25 |
26 | ### System Requirements
27 |
28 | - GIFPop currently targets macOS 10.9+
29 |
30 | ## Precompiled Binary
31 |
32 | GIFPop is also available as a prebuilt (and unsigned) app here: [http://sound-of-silence.com/apps/GIFPop.zip](http://sound-of-silence.com/apps/GIFPop.zip) _(Last update: 10/16/2016)_
33 |
34 | ## Gifsicle
35 |
36 | **GIFPop** provides a simple GUI for a few commonly-used features of [gifsicle](https://github.com/kohler/gifsicle), however it barely scratches the surface of what Gifsicle can do. For more options, you can run Gifsicle from the command-line.
37 |
38 | ## ToDo's
39 |
40 | GIFPop is still a work-in-progress. Current ToDo's include:
41 |
42 | - [ ] Process GIFs in background and provide live previews (so that trimming, optimization, etc. are visible in the preview)
43 | - [ ] Support export of resized gif by drag-and-drop
44 | - [x] Improve constraints when very large GIFs are loaded
45 | - [ ] Add options for frame delay changes
46 | - [ ] Remember previously used options on launch (default optimization, etc)?
47 | - [ ] Fix drag of new GIFs onto preview pane
48 | - [ ] Provide a spinner or progress bar while processing large GIFs
49 |
50 | ## Author
51 |
52 | **Matt Reagan** - Website: [http://sound-of-silence.com/](http://sound-of-silence.com/) - Twitter: [@hmblebee](https://twitter.com/hmblebee)
53 |
54 |
55 | ## License
56 |
57 | **GIFPop**'s source code and related resources are Copyright (C) Matthew Reagan 2016. The source code is released under the [MIT License](https://opensource.org/licenses/MIT). **Gifsicle** is distributed under the GNU General Public License, Version 2.
58 |
59 | ## Acknowledgments
60 |
61 | * GIFPop is just a UI wrapper for the very awesome Gifsicle tool. For more info visit the [gifsicle Homepage](http://www.lcdf.org/gifsicle/)
62 |
--------------------------------------------------------------------------------
/GIFPop/Gifsicle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Gifsicle.swift
3 | // GIFPop
4 | //
5 | // Created by Matt Reagan on 10/14/16.
6 | // Copyright © 2016 Matt Reagan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public typealias GifInfo = (numberOfFrames: Int, numberOfColors: Int, frameDelay: Double)
12 |
13 | /****************************************************************************/
14 | /* Gifsicle */
15 | /****************************************************************************/
16 |
17 | class Gifsicle {
18 | //MARK: - Properties -
19 |
20 | let pathToWhich = "/usr/bin/which"
21 | let gifsicleExecutableName = "gifsicle"
22 | var pathToGifsicle: String?
23 |
24 | init() {
25 | pathToGifsicle = runSystemTaskForStringOutput(executablePath: pathToWhich, arguments: [gifsicleExecutableName])
26 |
27 | if pathToGifsicle == nil ||
28 | pathToGifsicle!.characters.count == 0 ||
29 | FileManager.default.fileExists(atPath: pathToGifsicle!) == false {
30 | pathToGifsicle = Bundle.main.path(forResource: gifsicleExecutableName, ofType: nil)
31 | }
32 | }
33 | }
34 |
35 | extension Gifsicle {
36 | //MARK: - Gifsicle wrapper functions -
37 |
38 | func runGifsicle(inputImage: String,
39 | resizeTo: NSSize!,
40 | optimize: Int?,
41 | limitColors: Int?,
42 | trimmedFrames: String?,
43 | outputPath: String) {
44 | assert((pathToGifsicle?.characters.count)! > 0, "No path to Gifsicle executable")
45 |
46 | /* Here the basic arguments for gifsicle are plugged in for the process
47 | See: https://www.lcdf.org/gifsicle/ */
48 |
49 | var arguments = ["-i", inputImage]
50 |
51 | if trimmedFrames != nil {
52 | let trimmedFramesArgument = "#\(trimmedFrames!)"
53 |
54 | arguments.append(trimmedFramesArgument)
55 | }
56 |
57 | if resizeTo != nil {
58 | arguments.append("--resize")
59 | arguments.append("\(Int(resizeTo!.width))x\(Int(resizeTo!.height))")
60 | }
61 |
62 | if optimize != nil && optimize != 0 {
63 | assert(optimize! <= 3 && optimize! >= 1, "Only optimization levels O1-O3 supported")
64 | arguments.append("-O\(optimize!)")
65 | }
66 |
67 | if limitColors != nil && limitColors != 0 {
68 | arguments.append("--colors")
69 | arguments.append("\(limitColors!)")
70 | }
71 |
72 | arguments.append("--output")
73 | arguments.append(outputPath)
74 |
75 | let _ = runSystemTask(executablePath: pathToGifsicle!,
76 | arguments: arguments)
77 | }
78 |
79 | func getGifsicleInfo(inputImage: String) -> (GifInfo) {
80 | assert((pathToGifsicle?.characters.count)! > 0, "No path to Gifsicle executable")
81 |
82 | let gifInfo =
83 | runSystemTaskForStringOutput(executablePath: pathToGifsicle!,
84 | arguments: ["--info",
85 | inputImage])
86 |
87 | var numberOfFrames = 0
88 | var totalColors = 0
89 | var frameDelay = 0.0
90 | var totalFrameDelaysComputed = 0
91 |
92 | gifInfo?.enumerateLines(invoking: { (line: String, stop: inout Bool) in
93 | let cleanLine = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
94 |
95 | if cleanLine.hasPrefix("+ image #") {
96 | numberOfFrames += 1
97 | } else if cleanLine.hasPrefix("disposal") {
98 | if let lastWord = (line.components(separatedBy: " ").last as String?) {
99 | if let delay = Double(lastWord.trimmingCharacters(in: CharacterSet.decimalDigits.inverted)) {
100 | frameDelay += delay
101 | totalFrameDelaysComputed += 1
102 | }
103 | }
104 | } else if cleanLine.contains("color table") {
105 | if let lastWord = (line.components(separatedBy: " ").last as String?) {
106 | if let colors = Int(lastWord.trimmingCharacters(in: CharacterSet.decimalDigits.inverted)) {
107 | totalColors = colors
108 | }
109 | }
110 | }
111 | })
112 |
113 | if totalFrameDelaysComputed == 0 || frameDelay == 0.0 {
114 | print("Frame delay information unavailable, using default 0.1s")
115 | frameDelay = 0.1
116 | } else {
117 | frameDelay /= Double(totalFrameDelaysComputed)
118 | }
119 |
120 | return (numberOfFrames, totalColors, frameDelay)
121 | }
122 | }
123 |
124 | /****************************************************************************/
125 | /* NSTask (Process) Utility */
126 | /****************************************************************************/
127 |
128 | extension Gifsicle {
129 | //MARK: - Gifsicle process utility functions -
130 |
131 | func runSystemTaskForStringOutput(executablePath: String, arguments: [String]) -> String? {
132 | let data = runSystemTask(executablePath: executablePath, arguments: arguments)
133 | let outputString = String.init(data: data, encoding: String.Encoding.utf8)
134 |
135 | return outputString
136 | }
137 |
138 | func runSystemTask(executablePath: String, arguments: [String]) -> Data {
139 | if FileManager.default.fileExists(atPath: executablePath) == false {
140 | return Data()
141 | }
142 |
143 | let task = Process()
144 |
145 | task.launchPath = executablePath
146 | task.arguments = arguments
147 |
148 | let pipe = Pipe()
149 | task.standardOutput = pipe
150 | let fileHandle = pipe.fileHandleForReading
151 |
152 | task.launch()
153 | task.waitUntilExit()
154 |
155 | let outputData = fileHandle.readDataToEndOfFile()
156 |
157 | return outputData
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/GIFPop/GIFPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GIFPreview.swift
3 | // GIFPop
4 | //
5 | // Created by Matt Reagan on 10/9/16.
6 | // Copyright © 2016 Matt Reagan. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import CoreServices
11 |
12 | protocol GIFPreviewDelegate: class {
13 | func gifPreview(preview: GIFPreview, receivedGIF pathToGIF: String)
14 | func gifPreview(shouldAnimatePreview: GIFPreview) -> Bool
15 | }
16 |
17 | /****************************************************************************/
18 | /* GIFPreview View */
19 | /****************************************************************************/
20 |
21 | class GIFPreview: NSView {
22 |
23 | //MARK: - Properties -
24 |
25 | lazy var dropHereImage: NSImage = { return NSImage(named: "dropGifHere")! }()
26 | var gifView: NSImageView?
27 | var gifWidthConstraint: NSLayoutConstraint?
28 | var gifHeightConstraint: NSLayoutConstraint?
29 | weak var delegate: GIFPreviewDelegate?
30 |
31 | var animates: Bool {
32 | set(newAnimates) {
33 | gifView?.animates = newAnimates && self.delegate!.gifPreview(shouldAnimatePreview: self)
34 | }
35 |
36 | get {
37 | if gifView != nil {
38 | return gifView!.animates
39 | }
40 |
41 | return false
42 | }
43 | }
44 |
45 | //MARK: - Displaying GIFs -
46 |
47 | func displayAimatedGIF(gif: NSImage?, atSize size: NSSize) {
48 | if gif != nil {
49 | if gifView == nil {
50 | gifView = createGIFImageView()
51 | self.addSubview(gifView!)
52 |
53 | applyGIFViewConstraints(atSize: size)
54 | } else {
55 | gifWidthConstraint?.constant = size.width
56 | gifHeightConstraint?.constant = size.height
57 | }
58 |
59 | gifView?.image = gif
60 | } else {
61 | gifView?.removeFromSuperview()
62 | gifView = nil
63 | }
64 |
65 | self.setNeedsDisplay(bounds)
66 | }
67 |
68 | func createGIFImageView() -> NSImageView {
69 | let gifView = NSImageView.init(frame: self.bounds)
70 | gifView.imageScaling = NSImageScaling.scaleProportionallyDown
71 | gifView.animates = true
72 | gifView.canDrawSubviewsIntoLayer = true
73 | gifView.layerContentsRedrawPolicy = NSViewLayerContentsRedrawPolicy.duringViewResize
74 | gifView.translatesAutoresizingMaskIntoConstraints = false
75 | gifView.isEditable = false
76 | gifView.setContentCompressionResistancePriority(NSLayoutPriorityWindowSizeStayPut - 1, for: .horizontal)
77 | gifView.setContentCompressionResistancePriority(NSLayoutPriorityWindowSizeStayPut - 1, for: .vertical)
78 |
79 | return gifView
80 | }
81 |
82 | func applyGIFViewConstraints(atSize size: NSSize) {
83 | self.addConstraint(NSLayoutConstraint.init(item: gifView!,
84 | attribute: .centerX,
85 | relatedBy: .equal,
86 | toItem: self,
87 | attribute: .centerX,
88 | multiplier: 1.0,
89 | constant: 0.0))
90 |
91 | self.addConstraint(NSLayoutConstraint.init(item: gifView!,
92 | attribute: .centerY,
93 | relatedBy: .equal,
94 | toItem: self,
95 | attribute: .centerY,
96 | multiplier: 1.0,
97 | constant: 0.0))
98 |
99 | self.addConstraint(NSLayoutConstraint.init(item: gifView!,
100 | attribute: .width,
101 | relatedBy: .lessThanOrEqual,
102 | toItem: self,
103 | attribute: .width,
104 | multiplier: 1.0,
105 | constant: 0.0))
106 |
107 | self.addConstraint(NSLayoutConstraint.init(item: gifView!,
108 | attribute: .height,
109 | relatedBy: .lessThanOrEqual,
110 | toItem: self,
111 | attribute: .height,
112 | multiplier: 1.0,
113 | constant: 0.0))
114 |
115 | gifWidthConstraint = NSLayoutConstraint.init(item: gifView!,
116 | attribute: .width,
117 | relatedBy: .lessThanOrEqual,
118 | toItem: nil,
119 | attribute: .width,
120 | multiplier: 1.0,
121 | constant: size.width)
122 |
123 | gifHeightConstraint = NSLayoutConstraint.init(item: gifView!,
124 | attribute: .height,
125 | relatedBy: .lessThanOrEqual,
126 | toItem: self,
127 | attribute: .height,
128 | multiplier: 1.0,
129 | constant: size.height)
130 |
131 | self.addConstraint(gifWidthConstraint!)
132 | self.addConstraint(gifHeightConstraint!)
133 | }
134 |
135 | //MARK: - View Overrides -
136 |
137 | override func awakeFromNib() {
138 | setUpDragSupport()
139 | beginObserving()
140 | }
141 |
142 | //MARK: - Drag and Drop -
143 |
144 | func setUpDragSupport() {
145 | self.register(forDraggedTypes: [NSFilenamesPboardType])
146 | }
147 |
148 | override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
149 | return NSDragOperation.every;
150 | }
151 |
152 | override func draggingExited(_ sender: NSDraggingInfo?) {
153 |
154 | self.setNeedsDisplay(self.bounds)
155 | }
156 |
157 | override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
158 | let pasteboard = sender.draggingPasteboard()
159 |
160 | guard pasteboard.types?.contains(NSFilenamesPboardType) == true else {
161 | return false
162 | }
163 |
164 | guard let imagePath = (pasteboard.propertyList(forType: NSFilenamesPboardType) as? NSArray)?.firstObject as? String else {
165 | return false
166 | }
167 |
168 | guard let utiType = try? NSWorkspace.shared().type(ofFile: imagePath) else {
169 | return false
170 | }
171 |
172 | if utiType == (kUTTypeGIF as String) {
173 | delegate?.gifPreview(preview: self, receivedGIF: imagePath)
174 | } else {
175 | DispatchQueue.main.async {
176 | let alert = NSAlert.init()
177 | alert.messageText = "Animatd GIFs only, please"
178 | alert.informativeText = "This file type (\(utiType)) is not allowed. GIFPop works only with animated .gif files."
179 | alert.runModal()
180 | }
181 | }
182 |
183 | return true
184 | }
185 |
186 | override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
187 | return true;
188 | }
189 |
190 | //MARK: - Drawing -
191 |
192 | override func draw(_ dirtyRect: NSRect) {
193 | if gifView == nil {
194 | let myWidth = NSWidth(bounds)
195 | let myHeight = NSHeight(bounds)
196 | let imageSize: CGFloat = min(160.0, min(myWidth, myHeight))
197 | let imageRect = NSMakeRect(floor((myWidth - imageSize) / 2.0),
198 | floor((myHeight - imageSize) / 2.0),
199 | imageSize, imageSize)
200 |
201 | dropHereImage.draw(in: imageRect, from: NSZeroRect, operation: NSCompositingOperation.sourceOver, fraction: 0.7)
202 | }
203 |
204 | NSColor.init(white: 0.0, alpha: 0.08).setFill()
205 | NSFrameRectWithWidth(bounds, 1.0)
206 | }
207 |
208 | //MARK: - Notifications -
209 |
210 | func beginObserving() {
211 | NotificationCenter.default.addObserver(self,
212 | selector: #selector(GIFPreview.windowBeganResizing),
213 | name: NSNotification.Name.NSWindowWillStartLiveResize,
214 | object: self.window)
215 |
216 | NotificationCenter.default.addObserver(self,
217 | selector: #selector(GIFPreview.windowFinishedResizing),
218 | name: NSNotification.Name.NSWindowDidEndLiveResize,
219 | object: self.window)
220 | }
221 |
222 | func windowBeganResizing() { animates = false }
223 |
224 | func windowFinishedResizing() { animates = true }
225 |
226 | }
227 |
--------------------------------------------------------------------------------
/GIFPop/Resizer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Resizer.swift
3 | // GIFPop
4 | //
5 | // Created by Matt Reagan on 10/9/16.
6 | // Copyright © 2016 Matt Reagan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Cocoa
11 |
12 | /****************************************************************************/
13 | /* Resizer Class */
14 | /****************************************************************************/
15 |
16 | class Resizer : NSObject, GIFPreviewDelegate {
17 | enum FrameTrimmingOption: Int {
18 | case dontChange = 0, trimBeginning, trimEnd
19 | }
20 |
21 | //MARK: - UI Outlets -
22 |
23 | @IBOutlet weak var newWidthTextField: NSTextField!
24 | @IBOutlet weak var newHeightTextField: NSTextField!
25 | @IBOutlet weak var frameTrimTextField: NSTextField!
26 | @IBOutlet weak var gifInfoLabel: NSTextField!
27 | @IBOutlet weak var framesPopUp: NSPopUpButton!
28 | @IBOutlet weak var optimizationPopUp: NSPopUpButton!
29 | @IBOutlet weak var newColorsPopUp: NSPopUpButton!
30 | @IBOutlet weak var animatePreviewCheckbox: NSButton!
31 |
32 | @IBOutlet weak var optionsBox: NSBox!
33 | @IBOutlet weak var newSizeSlider: NSSlider!
34 | @IBOutlet weak var saveGIFButton: NSButton!
35 | @IBOutlet weak var preview: GIFPreview!
36 | @IBOutlet weak var resizerWindow: NSWindow!
37 |
38 | //MARK: - Properties -
39 |
40 | var inputGIFImage: NSImage?
41 | let gifsicle: Gifsicle = Gifsicle()
42 | var inputGIFInfo: GifInfo = (0, 0, 0.0)
43 | var originalGIFPath: String?
44 |
45 | var inputGIFPath: String? {
46 | didSet {
47 | inputGIFImage = NSImage.init(contentsOf: URL.init(fileURLWithPath: inputGIFPath!))
48 | refreshUI(shouldReset: true)
49 | }
50 | }
51 |
52 | var originalGIFSize: NSSize {
53 | return (inputGIFImage == nil) ? .zero : inputGIFImage!.size
54 | }
55 |
56 | var proposedSize: NSSize {
57 | let newWidth: CGFloat = max(CGFloat(newWidthTextField.doubleValue), 1.0)
58 | let newHeight: CGFloat = max(CGFloat(newHeightTextField.doubleValue), 1.0)
59 |
60 | return NSSize(width: newWidth > 0 ? newWidth : originalGIFSize.width,
61 | height: newHeight > 0 ? newHeight : originalGIFSize.height)
62 | }
63 | }
64 |
65 | /****************************************************************************/
66 | /* XIB, UI, and other utilities */
67 | /****************************************************************************/
68 |
69 | extension Resizer {
70 | //MARK: - Setup -
71 |
72 | override func awakeFromNib() {
73 | preview.delegate = self
74 | refreshUI()
75 | }
76 |
77 | //MARK: - Preview Delegate -
78 |
79 | func gifPreview(preview: GIFPreview, receivedGIF pathToGIF: String) {
80 | loadGIFAtPath(pathToGIF: pathToGIF)
81 | }
82 |
83 | func gifPreview(shouldAnimatePreview: GIFPreview) -> Bool {
84 | return (self.animatePreviewCheckbox.state == NSOnState)
85 | }
86 |
87 | //MARK: - UI Helper Functions -
88 |
89 | func loadGIFAtPath(pathToGIF: String) {
90 | let defaultTempGIFName = "GIFPopResizeOriginal.gif"
91 | let tempGIFPath = (NSTemporaryDirectory() as NSString).appendingPathComponent(defaultTempGIFName)
92 |
93 | try? FileManager.default.removeItem(atPath: tempGIFPath)
94 | try? FileManager.default.copyItem(atPath: pathToGIF, toPath: tempGIFPath)
95 |
96 | inputGIFInfo = gifsicle.getGifsicleInfo(inputImage: tempGIFPath)
97 | originalGIFPath = pathToGIF
98 | inputGIFPath = tempGIFPath
99 | }
100 |
101 | func refreshUIWithPreviewAnimationDisabled() {
102 | /* Disables the animation of the GIF NSImageView during preview resizing */
103 |
104 | preview.animates = false
105 | refreshUI()
106 | preview.animates = true
107 | }
108 |
109 | func refreshUI() {
110 | refreshUI(shouldReset: false)
111 | }
112 |
113 | func refreshUI(shouldReset: Bool) {
114 | let hasGIF = ((inputGIFPath != nil) && (inputGIFImage != nil))
115 |
116 | if hasGIF {
117 | gifInfoLabel.stringValue = (originalGIFPath! as NSString).lastPathComponent + "\n" +
118 | "Dimensions: \(Int(self.originalGIFSize.width)) x \(Int(self.originalGIFSize.height))" +
119 | "\tFile size: \(calculateFileSizeOfInputGIFInKB()) kb\n" +
120 | "Frames: \(inputGIFInfo.numberOfFrames) @ \(Int(inputGIFInfo.frameDelay * 1000.0))ms delay\n" +
121 | "Colors: \(inputGIFInfo.numberOfColors)"
122 | gifInfoLabel.alignment = .left
123 |
124 | if shouldReset {
125 | newWidthTextField.integerValue = Int(self.originalGIFSize.width)
126 | newHeightTextField.integerValue = Int(self.originalGIFSize.height)
127 | newSizeSlider.doubleValue = newSizeSlider.maxValue
128 | frameTrimTextField.stringValue = ""
129 | framesPopUp.selectItem(withTag: 0)
130 | optimizationPopUp.selectItem(withTag: 0)
131 | newColorsPopUp.selectItem(withTag: 0)
132 | }
133 |
134 | preview.displayAimatedGIF(gif: inputGIFImage, atSize: self.proposedSize)
135 | } else {
136 | gifInfoLabel.stringValue = "No GIF loaded"
137 | gifInfoLabel.alignment = .center
138 | preview.displayAimatedGIF(gif: nil, atSize: NSSize.zero)
139 | }
140 |
141 | setOptionsEnabled(hasGIF)
142 | validateFrameTrimField()
143 | }
144 |
145 | func setOptionsEnabled(_ enabled: Bool) {
146 | /* A bit heavy handed but we just filter on any NSControls in the NSBox: */
147 |
148 | let controls = self.optionsBox.contentView?.subviews.flatMap { $0 as? NSControl }
149 | for control in controls! { control.isEnabled = enabled }
150 | saveGIFButton.isEnabled = enabled
151 | }
152 |
153 | func validateFrameTrimField() {
154 | let selectedTrimOption = framesPopUp.selectedTag()
155 |
156 | if selectedTrimOption == FrameTrimmingOption.trimBeginning.rawValue ||
157 | selectedTrimOption == FrameTrimmingOption.trimEnd.rawValue {
158 | frameTrimTextField.isEnabled = true
159 | } else {
160 | frameTrimTextField.isEnabled = false
161 | frameTrimTextField.stringValue = ""
162 | }
163 | }
164 |
165 | //MARK: - Misc Helper Functions -
166 |
167 | func calculateFileSizeOfInputGIFInKB() -> UInt64 {
168 | var fileSizeOfGIFInKB: UInt64 = 0
169 | let fileAttributes = try? FileManager.default.attributesOfItem(atPath: inputGIFPath!)
170 |
171 | if fileAttributes != nil {
172 | fileSizeOfGIFInKB = (fileAttributes! as NSDictionary).fileSize()
173 | fileSizeOfGIFInKB /= 1024
174 | }
175 |
176 | return fileSizeOfGIFInKB
177 | }
178 | }
179 |
180 | /****************************************************************************/
181 | /* Gifsicle */
182 | /****************************************************************************/
183 |
184 | extension Resizer {
185 | //MARK: - Gifsicle -
186 |
187 | func runGifsicle(outputPath: String) {
188 | if let gifPath = inputGIFPath {
189 | let newSize = NSSize(width: newWidthTextField.integerValue,
190 | height: newHeightTextField.integerValue)
191 | let optimization = optimizationPopUp.selectedTag()
192 | let colorLimit = newColorsPopUp.selectedTag()
193 | let totalFrames = self.inputGIFInfo.numberOfFrames
194 | let frameTrimCount = max(0, min(frameTrimTextField.integerValue, totalFrames - 1))
195 | var trimmedFrames: String? = nil
196 |
197 | if frameTrimCount > 0 {
198 | switch framesPopUp.selectedTag()
199 | {
200 | case FrameTrimmingOption.trimBeginning.rawValue:
201 | trimmedFrames = "\(frameTrimCount)-\(totalFrames - 1)"
202 | case FrameTrimmingOption.trimEnd.rawValue:
203 | trimmedFrames = "0-\(totalFrames - 1 - frameTrimCount)"
204 | default:
205 | trimmedFrames = "0-\(totalFrames - 1)"
206 | }
207 | }
208 |
209 | self.gifsicle.runGifsicle(inputImage: gifPath,
210 | resizeTo: newSize,
211 | optimize: optimization,
212 | limitColors: colorLimit,
213 | trimmedFrames: trimmedFrames,
214 | outputPath: outputPath)
215 | }
216 | }
217 | }
218 |
219 | /****************************************************************************/
220 | /* IBActions, UI glue */
221 | /****************************************************************************/
222 |
223 | extension Resizer {
224 |
225 | //MARK: - Actions -
226 |
227 | @IBAction func animatePreviewToggled(_ sender: AnyObject) {
228 | preview.animates = ((sender as! NSButton).state == NSOnState)
229 | }
230 |
231 | @IBAction func saveResizedClicked(_ sender: AnyObject) {
232 | if inputGIFImage != nil && inputGIFPath != nil {
233 | let gifExtension = "gif"
234 |
235 | let savePanel = NSSavePanel()
236 | savePanel.isExtensionHidden = false
237 | savePanel.canSelectHiddenExtension = true
238 | savePanel.allowedFileTypes = [gifExtension]
239 | savePanel.allowsOtherFileTypes = false
240 |
241 | let originalName = (originalGIFPath! as NSString).lastPathComponent
242 | let newName = (originalName as NSString).deletingPathExtension
243 |
244 | savePanel.nameFieldStringValue = newName + " Resized.gif"
245 | savePanel.nameFieldLabel = "Save resized GIF as:"
246 |
247 | savePanel.begin { (result: Int) in
248 | if result == NSFileHandlingPanelOKButton {
249 | if let url = savePanel.url {
250 | self.runGifsicle(outputPath: url.path)
251 | }
252 | }
253 | }
254 | }
255 | }
256 |
257 | @IBAction func newWidthFieldChanged(_ sender: NSTextField) {
258 | let newWidth = min(Double(originalGIFSize.width), max(sender.doubleValue, 1.0))
259 | let ratio = newWidth / Double(originalGIFSize.width)
260 | let newHeight = Double(originalGIFSize.height) * ratio
261 | updateNewDimensionValues(width: newWidth, height: newHeight)
262 | updateSizeSlider(ratio)
263 | }
264 |
265 | @IBAction func newHeightFieldChanged(_ sender: NSTextField) {
266 | let newHeight = min(Double(originalGIFSize.height), max(sender.doubleValue, 1.0))
267 | let ratio = newHeight / Double(originalGIFSize.height)
268 | let newWidth = Double(originalGIFSize.width) * ratio
269 | updateNewDimensionValues(width: newWidth, height: newHeight)
270 | updateSizeSlider(ratio)
271 | }
272 |
273 | @IBAction func framePopUpChanged(_ sender: NSPopUpButton) {
274 | validateFrameTrimField()
275 | }
276 |
277 | @IBAction func sizeSliderChanged(_ sender: AnyObject) {
278 | var width = Double(self.originalGIFSize.width)
279 | var height = Double(self.originalGIFSize.height)
280 |
281 | let ratio = self.newSizeSlider.doubleValue / 100.0
282 |
283 | width *= ratio
284 | height *= ratio
285 |
286 | width = max(width, 1.0)
287 | height = max(height, 1.0)
288 |
289 | updateNewDimensionValues(width: width, height: height)
290 | }
291 |
292 | //MARK: - Menu Actions -
293 |
294 | @IBAction func closeMenuItemSelected(_ sender: AnyObject) {
295 | NSApp.terminate(nil)
296 | }
297 |
298 | @IBAction func saveMenuItemSelected(_ sender: AnyObject) {
299 | saveResizedClicked(sender)
300 | }
301 |
302 | @IBAction func openMenuItemSelected(_ sender: AnyObject) {
303 | let openPanel = NSOpenPanel()
304 | openPanel.allowedFileTypes = ["gif"]
305 | openPanel.allowsOtherFileTypes = false
306 | openPanel.allowsMultipleSelection = false
307 | openPanel.nameFieldLabel = "Select a .gif:"
308 |
309 | openPanel.begin { (result: Int) in
310 | if result == NSFileHandlingPanelOKButton {
311 | if let path = openPanel.url?.path {
312 | self.loadGIFAtPath(pathToGIF: path)
313 | }
314 | }
315 | }
316 | }
317 |
318 | //MARK: - Utility
319 |
320 | func updateSizeSlider(_ ratio: Double) {
321 | newSizeSlider.doubleValue = ratio * newSizeSlider.maxValue
322 | }
323 |
324 | func updateNewDimensionValues(width: Double, height: Double) {
325 | newWidthTextField.integerValue = Int(width.rounded())
326 | newHeightTextField.integerValue = Int(height.rounded())
327 | refreshUIWithPreviewAnimationDisabled()
328 | }
329 | }
330 |
--------------------------------------------------------------------------------
/GIFPop.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | C99360401DB1B91B00959296 /* gifsicle in Resources */ = {isa = PBXBuildFile; fileRef = C993603F1DB1B91B00959296 /* gifsicle */; };
11 | C99360421DB217F900959296 /* HelpText.rtf in Resources */ = {isa = PBXBuildFile; fileRef = C99360411DB217F900959296 /* HelpText.rtf */; };
12 | C9CB3DB11DB7165B00AD2E2B /* AboutGIFPop.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9CB3DB01DB7165B00AD2E2B /* AboutGIFPop.swift */; };
13 | C9CE07691DB1B50400F17506 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9CE07681DB1B50400F17506 /* AppDelegate.swift */; };
14 | C9CE076B1DB1B50400F17506 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9CE076A1DB1B50400F17506 /* Assets.xcassets */; };
15 | C9CE076E1DB1B50400F17506 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = C9CE076C1DB1B50400F17506 /* MainMenu.xib */; };
16 | C9CE07781DB1B54C00F17506 /* GIFPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9CE07751DB1B54C00F17506 /* GIFPreview.swift */; };
17 | C9CE07791DB1B54C00F17506 /* Gifsicle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9CE07761DB1B54C00F17506 /* Gifsicle.swift */; };
18 | C9CE077A1DB1B54C00F17506 /* Resizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9CE07771DB1B54C00F17506 /* Resizer.swift */; };
19 | C9CE077C1DB1B56C00F17506 /* dropGifHere.png in Resources */ = {isa = PBXBuildFile; fileRef = C9CE077B1DB1B56C00F17506 /* dropGifHere.png */; };
20 | /* End PBXBuildFile section */
21 |
22 | /* Begin PBXFileReference section */
23 | C993603F1DB1B91B00959296 /* gifsicle */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = gifsicle; sourceTree = ""; };
24 | C99360411DB217F900959296 /* HelpText.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = HelpText.rtf; sourceTree = ""; };
25 | C9CB3DB01DB7165B00AD2E2B /* AboutGIFPop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutGIFPop.swift; sourceTree = ""; };
26 | C9CE07651DB1B50400F17506 /* GIFPop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GIFPop.app; sourceTree = BUILT_PRODUCTS_DIR; };
27 | C9CE07681DB1B50400F17506 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
28 | C9CE076A1DB1B50400F17506 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
29 | C9CE076D1DB1B50400F17506 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
30 | C9CE076F1DB1B50400F17506 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
31 | C9CE07751DB1B54C00F17506 /* GIFPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = GIFPreview.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
32 | C9CE07761DB1B54C00F17506 /* Gifsicle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Gifsicle.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
33 | C9CE07771DB1B54C00F17506 /* Resizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Resizer.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
34 | C9CE077B1DB1B56C00F17506 /* dropGifHere.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dropGifHere.png; sourceTree = ""; };
35 | /* End PBXFileReference section */
36 |
37 | /* Begin PBXFrameworksBuildPhase section */
38 | C9CE07621DB1B50400F17506 /* Frameworks */ = {
39 | isa = PBXFrameworksBuildPhase;
40 | buildActionMask = 2147483647;
41 | files = (
42 | );
43 | runOnlyForDeploymentPostprocessing = 0;
44 | };
45 | /* End PBXFrameworksBuildPhase section */
46 |
47 | /* Begin PBXGroup section */
48 | C9CE075C1DB1B50400F17506 = {
49 | isa = PBXGroup;
50 | children = (
51 | C9CE07671DB1B50400F17506 /* GIFPop */,
52 | C9CE07661DB1B50400F17506 /* Products */,
53 | );
54 | sourceTree = "";
55 | };
56 | C9CE07661DB1B50400F17506 /* Products */ = {
57 | isa = PBXGroup;
58 | children = (
59 | C9CE07651DB1B50400F17506 /* GIFPop.app */,
60 | );
61 | name = Products;
62 | sourceTree = "";
63 | };
64 | C9CE07671DB1B50400F17506 /* GIFPop */ = {
65 | isa = PBXGroup;
66 | children = (
67 | C9CE076F1DB1B50400F17506 /* Info.plist */,
68 | C9CE07681DB1B50400F17506 /* AppDelegate.swift */,
69 | C9CE07771DB1B54C00F17506 /* Resizer.swift */,
70 | C9CE07761DB1B54C00F17506 /* Gifsicle.swift */,
71 | C9CE07751DB1B54C00F17506 /* GIFPreview.swift */,
72 | C9CB3DB01DB7165B00AD2E2B /* AboutGIFPop.swift */,
73 | C9CE076A1DB1B50400F17506 /* Assets.xcassets */,
74 | C9CE076C1DB1B50400F17506 /* MainMenu.xib */,
75 | C9DD19E81DB324BA00A94BDD /* Resources */,
76 | );
77 | path = GIFPop;
78 | sourceTree = "";
79 | };
80 | C9DD19E81DB324BA00A94BDD /* Resources */ = {
81 | isa = PBXGroup;
82 | children = (
83 | C9CE077B1DB1B56C00F17506 /* dropGifHere.png */,
84 | C99360411DB217F900959296 /* HelpText.rtf */,
85 | C993603F1DB1B91B00959296 /* gifsicle */,
86 | );
87 | name = Resources;
88 | sourceTree = "";
89 | };
90 | /* End PBXGroup section */
91 |
92 | /* Begin PBXNativeTarget section */
93 | C9CE07641DB1B50400F17506 /* GIFPop */ = {
94 | isa = PBXNativeTarget;
95 | buildConfigurationList = C9CE07721DB1B50400F17506 /* Build configuration list for PBXNativeTarget "GIFPop" */;
96 | buildPhases = (
97 | C9CE07611DB1B50400F17506 /* Sources */,
98 | C9CE07621DB1B50400F17506 /* Frameworks */,
99 | C9CE07631DB1B50400F17506 /* Resources */,
100 | );
101 | buildRules = (
102 | );
103 | dependencies = (
104 | );
105 | name = GIFPop;
106 | productName = GIFPop;
107 | productReference = C9CE07651DB1B50400F17506 /* GIFPop.app */;
108 | productType = "com.apple.product-type.application";
109 | };
110 | /* End PBXNativeTarget section */
111 |
112 | /* Begin PBXProject section */
113 | C9CE075D1DB1B50400F17506 /* Project object */ = {
114 | isa = PBXProject;
115 | attributes = {
116 | LastSwiftUpdateCheck = 0800;
117 | LastUpgradeCheck = 0820;
118 | ORGANIZATIONNAME = "Matt Reagan";
119 | TargetAttributes = {
120 | C9CE07641DB1B50400F17506 = {
121 | CreatedOnToolsVersion = 8.0;
122 | ProvisioningStyle = Automatic;
123 | };
124 | };
125 | };
126 | buildConfigurationList = C9CE07601DB1B50400F17506 /* Build configuration list for PBXProject "GIFPop" */;
127 | compatibilityVersion = "Xcode 3.2";
128 | developmentRegion = English;
129 | hasScannedForEncodings = 0;
130 | knownRegions = (
131 | en,
132 | Base,
133 | );
134 | mainGroup = C9CE075C1DB1B50400F17506;
135 | productRefGroup = C9CE07661DB1B50400F17506 /* Products */;
136 | projectDirPath = "";
137 | projectRoot = "";
138 | targets = (
139 | C9CE07641DB1B50400F17506 /* GIFPop */,
140 | );
141 | };
142 | /* End PBXProject section */
143 |
144 | /* Begin PBXResourcesBuildPhase section */
145 | C9CE07631DB1B50400F17506 /* Resources */ = {
146 | isa = PBXResourcesBuildPhase;
147 | buildActionMask = 2147483647;
148 | files = (
149 | C9CE076B1DB1B50400F17506 /* Assets.xcassets in Resources */,
150 | C9CE077C1DB1B56C00F17506 /* dropGifHere.png in Resources */,
151 | C99360401DB1B91B00959296 /* gifsicle in Resources */,
152 | C9CE076E1DB1B50400F17506 /* MainMenu.xib in Resources */,
153 | C99360421DB217F900959296 /* HelpText.rtf in Resources */,
154 | );
155 | runOnlyForDeploymentPostprocessing = 0;
156 | };
157 | /* End PBXResourcesBuildPhase section */
158 |
159 | /* Begin PBXSourcesBuildPhase section */
160 | C9CE07611DB1B50400F17506 /* Sources */ = {
161 | isa = PBXSourcesBuildPhase;
162 | buildActionMask = 2147483647;
163 | files = (
164 | C9CE077A1DB1B54C00F17506 /* Resizer.swift in Sources */,
165 | C9CB3DB11DB7165B00AD2E2B /* AboutGIFPop.swift in Sources */,
166 | C9CE07691DB1B50400F17506 /* AppDelegate.swift in Sources */,
167 | C9CE07791DB1B54C00F17506 /* Gifsicle.swift in Sources */,
168 | C9CE07781DB1B54C00F17506 /* GIFPreview.swift in Sources */,
169 | );
170 | runOnlyForDeploymentPostprocessing = 0;
171 | };
172 | /* End PBXSourcesBuildPhase section */
173 |
174 | /* Begin PBXVariantGroup section */
175 | C9CE076C1DB1B50400F17506 /* MainMenu.xib */ = {
176 | isa = PBXVariantGroup;
177 | children = (
178 | C9CE076D1DB1B50400F17506 /* Base */,
179 | );
180 | name = MainMenu.xib;
181 | sourceTree = "";
182 | };
183 | /* End PBXVariantGroup section */
184 |
185 | /* Begin XCBuildConfiguration section */
186 | C9CE07701DB1B50400F17506 /* Debug */ = {
187 | isa = XCBuildConfiguration;
188 | buildSettings = {
189 | ALWAYS_SEARCH_USER_PATHS = NO;
190 | CLANG_ANALYZER_NONNULL = YES;
191 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
192 | CLANG_CXX_LIBRARY = "libc++";
193 | CLANG_ENABLE_MODULES = YES;
194 | CLANG_ENABLE_OBJC_ARC = YES;
195 | CLANG_WARN_BOOL_CONVERSION = YES;
196 | CLANG_WARN_CONSTANT_CONVERSION = YES;
197 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
198 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
199 | CLANG_WARN_EMPTY_BODY = YES;
200 | CLANG_WARN_ENUM_CONVERSION = YES;
201 | CLANG_WARN_INFINITE_RECURSION = YES;
202 | CLANG_WARN_INT_CONVERSION = YES;
203 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
204 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
205 | CLANG_WARN_SUSPICIOUS_MOVES = YES;
206 | CLANG_WARN_UNREACHABLE_CODE = YES;
207 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
208 | CODE_SIGN_IDENTITY = "-";
209 | COPY_PHASE_STRIP = NO;
210 | DEBUG_INFORMATION_FORMAT = dwarf;
211 | ENABLE_STRICT_OBJC_MSGSEND = YES;
212 | ENABLE_TESTABILITY = YES;
213 | GCC_C_LANGUAGE_STANDARD = gnu99;
214 | GCC_DYNAMIC_NO_PIC = NO;
215 | GCC_NO_COMMON_BLOCKS = YES;
216 | GCC_OPTIMIZATION_LEVEL = 0;
217 | GCC_PREPROCESSOR_DEFINITIONS = (
218 | "DEBUG=1",
219 | "$(inherited)",
220 | );
221 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
222 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
223 | GCC_WARN_UNDECLARED_SELECTOR = YES;
224 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
225 | GCC_WARN_UNUSED_FUNCTION = YES;
226 | GCC_WARN_UNUSED_VARIABLE = YES;
227 | MACOSX_DEPLOYMENT_TARGET = 10.12;
228 | MTL_ENABLE_DEBUG_INFO = YES;
229 | ONLY_ACTIVE_ARCH = YES;
230 | SDKROOT = macosx;
231 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
232 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
233 | };
234 | name = Debug;
235 | };
236 | C9CE07711DB1B50400F17506 /* Release */ = {
237 | isa = XCBuildConfiguration;
238 | buildSettings = {
239 | ALWAYS_SEARCH_USER_PATHS = NO;
240 | CLANG_ANALYZER_NONNULL = YES;
241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
242 | CLANG_CXX_LIBRARY = "libc++";
243 | CLANG_ENABLE_MODULES = YES;
244 | CLANG_ENABLE_OBJC_ARC = YES;
245 | CLANG_WARN_BOOL_CONVERSION = YES;
246 | CLANG_WARN_CONSTANT_CONVERSION = YES;
247 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
248 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
249 | CLANG_WARN_EMPTY_BODY = YES;
250 | CLANG_WARN_ENUM_CONVERSION = YES;
251 | CLANG_WARN_INFINITE_RECURSION = YES;
252 | CLANG_WARN_INT_CONVERSION = YES;
253 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
254 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
255 | CLANG_WARN_SUSPICIOUS_MOVES = YES;
256 | CLANG_WARN_UNREACHABLE_CODE = YES;
257 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
258 | CODE_SIGN_IDENTITY = "-";
259 | COPY_PHASE_STRIP = NO;
260 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
261 | ENABLE_NS_ASSERTIONS = NO;
262 | ENABLE_STRICT_OBJC_MSGSEND = YES;
263 | GCC_C_LANGUAGE_STANDARD = gnu99;
264 | GCC_NO_COMMON_BLOCKS = YES;
265 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
266 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
267 | GCC_WARN_UNDECLARED_SELECTOR = YES;
268 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
269 | GCC_WARN_UNUSED_FUNCTION = YES;
270 | GCC_WARN_UNUSED_VARIABLE = YES;
271 | MACOSX_DEPLOYMENT_TARGET = 10.12;
272 | MTL_ENABLE_DEBUG_INFO = NO;
273 | SDKROOT = macosx;
274 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
275 | };
276 | name = Release;
277 | };
278 | C9CE07731DB1B50400F17506 /* Debug */ = {
279 | isa = XCBuildConfiguration;
280 | buildSettings = {
281 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
282 | COMBINE_HIDPI_IMAGES = YES;
283 | INFOPLIST_FILE = GIFPop/Info.plist;
284 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
285 | MACOSX_DEPLOYMENT_TARGET = 10.9;
286 | PRODUCT_BUNDLE_IDENTIFIER = mattreagan.GIFPop;
287 | PRODUCT_NAME = "$(TARGET_NAME)";
288 | SWIFT_VERSION = 3.0;
289 | };
290 | name = Debug;
291 | };
292 | C9CE07741DB1B50400F17506 /* Release */ = {
293 | isa = XCBuildConfiguration;
294 | buildSettings = {
295 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
296 | COMBINE_HIDPI_IMAGES = YES;
297 | INFOPLIST_FILE = GIFPop/Info.plist;
298 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
299 | MACOSX_DEPLOYMENT_TARGET = 10.9;
300 | PRODUCT_BUNDLE_IDENTIFIER = mattreagan.GIFPop;
301 | PRODUCT_NAME = "$(TARGET_NAME)";
302 | SWIFT_VERSION = 3.0;
303 | };
304 | name = Release;
305 | };
306 | /* End XCBuildConfiguration section */
307 |
308 | /* Begin XCConfigurationList section */
309 | C9CE07601DB1B50400F17506 /* Build configuration list for PBXProject "GIFPop" */ = {
310 | isa = XCConfigurationList;
311 | buildConfigurations = (
312 | C9CE07701DB1B50400F17506 /* Debug */,
313 | C9CE07711DB1B50400F17506 /* Release */,
314 | );
315 | defaultConfigurationIsVisible = 0;
316 | defaultConfigurationName = Release;
317 | };
318 | C9CE07721DB1B50400F17506 /* Build configuration list for PBXNativeTarget "GIFPop" */ = {
319 | isa = XCConfigurationList;
320 | buildConfigurations = (
321 | C9CE07731DB1B50400F17506 /* Debug */,
322 | C9CE07741DB1B50400F17506 /* Release */,
323 | );
324 | defaultConfigurationIsVisible = 0;
325 | defaultConfigurationName = Release;
326 | };
327 | /* End XCConfigurationList section */
328 | };
329 | rootObject = C9CE075D1DB1B50400F17506 /* Project object */;
330 | }
331 |
--------------------------------------------------------------------------------
/GIFPop/Base.lproj/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 |
520 |
521 |
522 |
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 |
596 |
597 |
598 |
611 |
621 |
622 |
623 |
624 |
625 |
626 |
627 |
628 |
629 |
630 |
631 |
632 |
633 |
634 |
635 |
636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 |
644 |
645 |
655 |
665 |
666 |
667 |
668 |
669 |
670 |
671 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
713 |
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 |
724 |
725 |
726 |
727 |
728 |
729 |
730 |
731 |
732 |
733 |
734 |
735 |
736 |
737 |
738 |
739 |
740 |
741 |
742 |
743 |
744 |
745 |
746 |
747 |
748 |
762 |
763 |
764 |
765 |
766 |
767 |
768 |
769 |
770 |
771 |
772 |
--------------------------------------------------------------------------------