Skip to content

Commit 92143ea

Browse files
committed
[feat]: image operations
1 parent 568b7e1 commit 92143ea

File tree

3 files changed

+189
-6
lines changed

3 files changed

+189
-6
lines changed

Sources/ScriptToolkit/FileExtension.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public extension File {
5151
throw OperationError.renameFailed(self)
5252
}
5353
}
54-
54+
5555
////////////////////////////////////////////////////////////////////////////////
5656
// MARK: - Modification date
5757

@@ -151,7 +151,7 @@ public extension File {
151151
try FileManager.default.removeItem(atPath: newName)
152152
}
153153

154-
run(ScriptToolkit.soxPath, path, newName, "tempo", "-s", String(percent))
154+
runAndDebug(ScriptToolkit.soxPath, path, newName, "tempo", "-s", String(percent))
155155
return try File(path: newName)
156156
}
157157

@@ -161,7 +161,7 @@ public extension File {
161161
try FileManager.default.removeItem(atPath: newName)
162162
}
163163

164-
run(ScriptToolkit.ffmpegPath, "-i", path, "-sample_rate", "44100", newName.deletingPathExtension + ".wav")
164+
runAndDebug(ScriptToolkit.ffmpegPath, "-i", path, "-sample_rate", "44100", newName.deletingPathExtension + ".wav")
165165
return try File(path: newName)
166166
}
167167

@@ -171,7 +171,7 @@ public extension File {
171171
try FileManager.default.removeItem(atPath: newName)
172172
}
173173

174-
run(ScriptToolkit.soxPath, path, "-r", "44100", newName)
174+
runAndDebug(ScriptToolkit.soxPath, path, "-r", "44100", "--channels", "2", newName)
175175
return try File(path: newName)
176176
}
177177

@@ -181,7 +181,7 @@ public extension File {
181181
try FileManager.default.removeItem(atPath: newName)
182182
}
183183

184-
run(ScriptToolkit.ffmpegPath, "-i", path, "-sample_rate", "44100", newName.deletingPathExtension + ".m4a")
184+
runAndDebug(ScriptToolkit.ffmpegPath, "-i", path, "-sample_rate", "44100", newName.deletingPathExtension + ".m4a")
185185
return try File(path: newName)
186186
}
187187

@@ -253,6 +253,7 @@ public extension File {
253253
try silencedFile75.delete()
254254
try silencedFile90.delete()
255255
try silencedFile100.delete()
256+
try normWavFile.delete()
256257
}
257258

258259
////////////////////////////////////////////////////////////////////////////////
@@ -276,7 +277,7 @@ public extension File {
276277
if !overwrite { return try File(path: newName) }
277278
try FileManager.default.removeItem(atPath: newName)
278279
}
279-
280+
280281
let left = Int(insets.left)
281282
let top = Int(insets.top)
282283
let bottom = Int(insets.bottom)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// NSColorExtension.swift
3+
// ScriptToolkit
4+
//
5+
// Created by Dan Cech on 05.03.2019.
6+
//
7+
8+
import Foundation
9+
import AppKit
10+
11+
public extension NSColor {
12+
convenience init(hexString: String) {
13+
let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
14+
let scanner = Scanner(string: hexString)
15+
16+
if (hexString.hasPrefix("#")) {
17+
scanner.scanLocation = 1
18+
}
19+
20+
var color: UInt32 = 0
21+
scanner.scanHexInt32(&color)
22+
23+
let mask = 0x000000FF
24+
let r = Int(color >> 16) & mask
25+
let g = Int(color >> 8) & mask
26+
let b = Int(color) & mask
27+
28+
let red = CGFloat(r) / 255.0
29+
let green = CGFloat(g) / 255.0
30+
let blue = CGFloat(b) / 255.0
31+
32+
self.init(red:red, green:green, blue:blue, alpha:1)
33+
}
34+
35+
func toHexString() -> String {
36+
var r:CGFloat = 0
37+
var g:CGFloat = 0
38+
var b:CGFloat = 0
39+
var a:CGFloat = 0
40+
41+
getRed(&r, green: &g, blue: &b, alpha: &a)
42+
43+
let rgb:Int = (Int)(r*255)<<16 | (Int)(g*255)<<8 | (Int)(b*255)<<0
44+
45+
return String(format:"#%06x", rgb)
46+
}
47+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//
2+
// NSImageExtension.swift
3+
// ScriptToolkit
4+
//
5+
// Created by Dan Cech on 05.03.2019.
6+
//
7+
8+
import Foundation
9+
import AppKit
10+
11+
public extension NSImage {
12+
13+
/// Returns the height of the current image.
14+
var height: CGFloat {
15+
return self.size.height
16+
}
17+
18+
/// Returns the width of the current image.
19+
var width: CGFloat {
20+
return self.size.width
21+
}
22+
23+
/// Returns a png representation of the current image.
24+
var PNGRepresentation: Data? {
25+
if let tiff = self.tiffRepresentation, let tiffData = NSBitmapImageRep(data: tiff) {
26+
return tiffData.representation(using: .png, properties: [:])
27+
}
28+
29+
return nil
30+
}
31+
32+
/// Copies the current image and resizes it to the given size.
33+
///
34+
/// - parameter size: The size of the new image.
35+
///
36+
/// - returns: The resized copy of the given image.
37+
func copy(size: NSSize) throws -> NSImage {
38+
// Create a new rect with given width and height
39+
let frame = NSMakeRect(0, 0, size.width, size.height)
40+
41+
// Get the best representation for the given size.
42+
guard let rep = self.bestRepresentation(for: frame, context: nil, hints: nil) else {
43+
throw ScriptError.generalError(message: "Unable to resize image")
44+
}
45+
46+
// Create an empty image with the given size.
47+
let img = NSImage(size: size)
48+
49+
// Set the drawing context and make sure to remove the focus before returning.
50+
img.lockFocus()
51+
defer { img.unlockFocus() }
52+
53+
// Draw the new image
54+
if rep.draw(in: frame) {
55+
return img
56+
}
57+
58+
// Return nil in case something went wrong.
59+
throw ScriptError.generalError(message: "Unable to resize image")
60+
}
61+
62+
/// Saves the PNG representation of the current image to the HD.
63+
///
64+
/// - parameter url: The location url to which to write the png file.
65+
func savePNGRepresentationToURL(url: URL) throws {
66+
if let png = self.PNGRepresentation {
67+
try png.write(to: url, options: .atomicWrite)
68+
}
69+
}
70+
71+
func combine(withImage image: NSImage) throws -> NSImage {
72+
guard
73+
let firstImageData = image.tiffRepresentation,
74+
let firstImage = CIImage(data: firstImageData),
75+
let secondImageData = tiffRepresentation,
76+
let secondImage = CIImage(data: secondImageData)
77+
else {
78+
throw ScriptError.generalError(message: "Image processing error")
79+
}
80+
81+
let filter = CIFilter(name: "CISourceOverCompositing")!
82+
filter.setDefaults()
83+
84+
filter.setValue(firstImage, forKey: "inputImage")
85+
filter.setValue(secondImage, forKey: "inputBackgroundImage")
86+
87+
let resultImage = filter.outputImage
88+
89+
let rep = NSCIImageRep(ciImage: resultImage!)
90+
let finalResult = NSImage(size: rep.size)
91+
finalResult.addRepresentation(rep)
92+
93+
return finalResult
94+
}
95+
96+
func image(withText text: String, attributes: [NSAttributedString.Key: Any]) -> NSImage {
97+
let image = self
98+
let text = text as NSString
99+
let options: NSString.DrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading]
100+
101+
let textSize = text.boundingRect(with: image.size, options: options, attributes: attributes).size
102+
103+
let x = (image.size.width - textSize.width) / 2
104+
let y = (image.size.height - textSize.height) / 2
105+
let point = NSMakePoint(x, y * 0.2)
106+
107+
image.lockFocus()
108+
text.draw(at: point, withAttributes: attributes)
109+
image.unlockFocus()
110+
111+
return image
112+
}
113+
114+
func annotate(text: String, size: CGFloat, fill: NSColor, stroke: NSColor) -> NSImage {
115+
116+
// Solid color text
117+
let fillText = image(
118+
withText: text,
119+
attributes: [
120+
.foregroundColor: fill,
121+
.font: NSFont(name: "Impact", size: size)!
122+
])
123+
124+
// Add strokes
125+
return fillText.image(
126+
withText: text,
127+
attributes: [
128+
.foregroundColor: fill,
129+
.strokeColor: stroke,
130+
.strokeWidth: 1,
131+
.font: NSFont(name: "Impact", size: size)!
132+
])
133+
}
134+
}
135+

0 commit comments

Comments
 (0)