2020-03-10 14:23:54 -04:00
|
|
|
import Cocoa
|
|
|
|
import ZIPFoundation
|
2020-03-11 10:24:52 -04:00
|
|
|
import CoreFoundation
|
2020-03-10 14:23:54 -04:00
|
|
|
|
2020-03-11 23:31:42 -04:00
|
|
|
struct SilicaChunk {
|
2020-03-12 07:55:12 -04:00
|
|
|
var x: Int = 0
|
|
|
|
var y: Int = 0
|
|
|
|
var image: NSImage = NSImage()
|
2020-03-11 23:31:42 -04:00
|
|
|
}
|
|
|
|
|
2020-03-11 10:24:52 -04:00
|
|
|
struct SilicaLayer {
|
2020-03-11 23:31:42 -04:00
|
|
|
var chunks: [SilicaChunk] = []
|
2020-03-11 10:24:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
struct SilicaDocument {
|
|
|
|
var trackedTime: Int = 0
|
2020-03-11 22:15:05 -04:00
|
|
|
var tileSize: Int = 0
|
2020-05-30 17:03:56 -04:00
|
|
|
var orientation: Int = 0
|
|
|
|
var flippedHorizontally: Bool = false
|
|
|
|
var flippedVertically: Bool = false
|
2020-03-11 10:24:52 -04:00
|
|
|
|
2020-03-11 23:31:42 -04:00
|
|
|
var width: Int = 0
|
|
|
|
var height: Int = 0
|
|
|
|
|
2020-03-11 10:24:52 -04:00
|
|
|
var layers: [SilicaLayer] = []
|
|
|
|
}
|
|
|
|
|
|
|
|
// Since this is a C-function we have to unsafe cast...
|
|
|
|
func objectRefGetValue(_ objectRef : CFTypeRef) -> UInt32 {
|
|
|
|
return _CFKeyedArchiverUIDGetValue(unsafeBitCast(objectRef, to: CFKeyedArchiverUIDRef.self))
|
2020-03-10 16:23:25 -04:00
|
|
|
}
|
|
|
|
|
2020-03-10 14:23:54 -04:00
|
|
|
class Document: NSDocument {
|
2020-03-11 22:44:52 -04:00
|
|
|
var data: Data? // oh no...
|
|
|
|
|
2020-03-11 10:24:52 -04:00
|
|
|
var dict: NSDictionary?
|
2020-03-12 08:04:37 -04:00
|
|
|
|
2020-03-11 10:24:52 -04:00
|
|
|
var info = SilicaDocument()
|
2020-03-12 07:55:12 -04:00
|
|
|
|
|
|
|
var rows: Int = 0
|
|
|
|
var columns: Int = 0
|
|
|
|
|
|
|
|
var remainderWidth: Int = 0
|
|
|
|
var remainderHeight: Int = 0
|
2020-03-10 16:23:25 -04:00
|
|
|
|
2020-03-10 14:23:54 -04:00
|
|
|
override init() {
|
|
|
|
super.init()
|
|
|
|
}
|
|
|
|
|
|
|
|
override func makeWindowControllers() {
|
|
|
|
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
|
|
|
|
let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as! NSWindowController
|
|
|
|
self.addWindowController(windowController)
|
|
|
|
}
|
2020-03-11 10:24:52 -04:00
|
|
|
|
|
|
|
/*
|
|
|
|
Pass in an object from the $object array, which always contains a $class key.
|
|
|
|
*/
|
|
|
|
func getDocumentClassName(dict: NSDictionary) -> String? {
|
|
|
|
let objectsArray = self.dict?["$objects"] as! NSArray
|
|
|
|
|
|
|
|
if let value = dict["$class"] {
|
|
|
|
let classObjectId = objectRefGetValue(value as CFTypeRef)
|
|
|
|
let classObject = objectsArray[Int(classObjectId)] as! NSDictionary
|
|
|
|
|
|
|
|
return classObject["$classname"] as? String
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
/*
|
|
|
|
Returns the correct tile size, taking into account the remainder between tile size and image size.
|
|
|
|
*/
|
|
|
|
func getTileSize(x: Int, y: Int) -> (Int, Int) {
|
|
|
|
var width: Int = info.tileSize
|
|
|
|
var height: Int = info.tileSize
|
|
|
|
|
|
|
|
if((x + 1) == columns) {
|
|
|
|
width = info.tileSize - remainderWidth
|
|
|
|
}
|
|
|
|
|
|
|
|
if(y == rows) {
|
|
|
|
height = info.tileSize - remainderHeight
|
|
|
|
}
|
|
|
|
|
|
|
|
return (width, height)
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Converts a CFKeyedArchiveUID from a NSKeyedArchive to a Int for indexing an array or dictionary.
|
|
|
|
*/
|
|
|
|
func getClassID(id: Any?) -> Int {
|
|
|
|
return Int(objectRefGetValue(id! as CFTypeRef))
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Parses the chunk filename, ex. 1~1 to integer coordinates.
|
|
|
|
*/
|
|
|
|
func parseChunkFilename(filename: String) -> (Int, Int)? {
|
|
|
|
let pathURL = URL(fileURLWithPath: filename)
|
|
|
|
let pathComponents = pathURL.lastPathComponent.replacingOccurrences(of: ".chunk", with: "").components(separatedBy: "~")
|
|
|
|
|
|
|
|
let x = Int(pathComponents[0])
|
|
|
|
let y = Int(pathComponents[1])
|
|
|
|
|
|
|
|
if x != nil && y != nil {
|
|
|
|
return (x!, y!)
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
2020-03-12 08:04:37 -04:00
|
|
|
|
2020-03-11 22:15:05 -04:00
|
|
|
func parseSilicaLayer(archive: Archive, dict: NSDictionary) {
|
|
|
|
let objectsArray = self.dict?["$objects"] as! NSArray
|
|
|
|
|
2020-03-11 10:24:52 -04:00
|
|
|
if getDocumentClassName(dict: dict) == LayerClassName {
|
2020-03-11 22:15:05 -04:00
|
|
|
var layer = SilicaLayer()
|
2020-03-11 22:44:52 -04:00
|
|
|
|
2020-03-11 22:15:05 -04:00
|
|
|
let UUIDKey = dict["UUID"]
|
2020-03-12 07:55:12 -04:00
|
|
|
let UUIDClassID = getClassID(id: UUIDKey)
|
|
|
|
let UUIDClass = objectsArray[UUIDClassID] as! NSString
|
2020-03-11 22:44:52 -04:00
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
var chunkPaths: [String] = []
|
2020-03-11 22:44:52 -04:00
|
|
|
|
2020-03-11 22:15:05 -04:00
|
|
|
archive.forEach { (entry: Entry) in
|
2020-03-11 22:44:52 -04:00
|
|
|
if entry.path.contains(String(UUIDClass)) {
|
2020-03-12 07:55:12 -04:00
|
|
|
chunkPaths.append(entry.path)
|
2020-03-11 22:44:52 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-11 23:31:42 -04:00
|
|
|
layer.chunks = Array(repeating: SilicaChunk(), count: chunkPaths.count)
|
2020-03-11 22:44:52 -04:00
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
let dispatchGroup = DispatchGroup()
|
|
|
|
let queue = DispatchQueue(label: "imageWork")
|
|
|
|
|
2020-03-11 22:44:52 -04:00
|
|
|
DispatchQueue.concurrentPerform(iterations: chunkPaths.count) { (i: Int) in
|
2020-03-12 07:55:12 -04:00
|
|
|
dispatchGroup.enter()
|
|
|
|
|
|
|
|
var threadArchive: Archive?
|
|
|
|
var threadEntry: Entry?
|
2020-03-11 23:31:42 -04:00
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
queue.sync {
|
|
|
|
threadArchive = Archive(data: self.data!, accessMode: Archive.AccessMode.read)
|
|
|
|
threadEntry = threadArchive?[chunkPaths[i]]
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let (x, y) = parseChunkFilename(filename: threadEntry!.path) else {
|
2020-03-11 22:44:52 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
let (width, height) = getTileSize(x: x, y: y)
|
|
|
|
let byteSize = width * height * 4
|
2020-03-11 22:44:52 -04:00
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
let uncompressedMemory = UnsafeMutablePointer<UInt8>.allocate(capacity: byteSize)
|
|
|
|
|
|
|
|
guard let lzoData = readData(archive: threadArchive!, entry: threadEntry!) else {
|
|
|
|
return
|
2020-03-11 22:44:52 -04:00
|
|
|
}
|
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
lzoData.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) -> Void in
|
|
|
|
var len = lzo_uint(byteSize)
|
2020-03-11 22:15:05 -04:00
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
lzo1x_decompress_safe(bytes.baseAddress!.assumingMemoryBound(to: uint8.self), lzo_uint(lzoData.count), uncompressedMemory, &len, nil)
|
2020-03-11 22:44:52 -04:00
|
|
|
})
|
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
let imageData = Data(bytes: uncompressedMemory, count: byteSize)
|
2020-03-11 22:44:52 -04:00
|
|
|
|
|
|
|
let render: CGColorRenderingIntent = CGColorRenderingIntent.defaultIntent
|
|
|
|
let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
|
2020-03-12 07:55:12 -04:00
|
|
|
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue).union(.byteOrder32Big)
|
|
|
|
let providerRef: CGDataProvider? = CGDataProvider(data: imageData as CFData)
|
|
|
|
|
|
|
|
guard let cgimage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: width * 4, space: rgbColorSpace, bitmapInfo: bitmapInfo, provider: providerRef!, decode: nil, shouldInterpolate: false, intent: render) else {
|
|
|
|
return
|
|
|
|
}
|
2020-03-11 22:44:52 -04:00
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
let image = NSImage(cgImage: cgimage, size: NSZeroSize)
|
2020-03-11 23:31:42 -04:00
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
queue.async(flags: .barrier) {
|
2020-03-11 23:31:42 -04:00
|
|
|
layer.chunks[i].image = image
|
2020-03-12 07:55:12 -04:00
|
|
|
layer.chunks[i].x = x
|
|
|
|
layer.chunks[i].y = y
|
|
|
|
|
|
|
|
dispatchGroup.leave()
|
2020-03-11 22:15:05 -04:00
|
|
|
}
|
|
|
|
}
|
2020-03-11 10:24:52 -04:00
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
dispatchGroup.wait()
|
|
|
|
|
2020-03-11 10:24:52 -04:00
|
|
|
info.layers.append(layer)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-11 22:15:05 -04:00
|
|
|
func parseSilicaDocument(archive: Archive, dict: NSDictionary) {
|
2020-03-11 10:24:52 -04:00
|
|
|
let objectsArray = self.dict?["$objects"] as! NSArray
|
|
|
|
|
|
|
|
if getDocumentClassName(dict: dict) == DocumentClassName {
|
|
|
|
info.trackedTime = (dict[TrackedTimeKey] as! NSNumber).intValue
|
2020-03-11 22:15:05 -04:00
|
|
|
info.tileSize = (dict[TileSizeKey] as! NSNumber).intValue
|
2020-05-30 17:03:56 -04:00
|
|
|
info.orientation = (dict[OrientationKey] as! NSNumber).intValue
|
|
|
|
info.flippedHorizontally = (dict[FlippedHorizontallyKey] as! NSNumber).boolValue
|
|
|
|
info.flippedVertically = (dict[FlippedVerticallyKey] as! NSNumber).boolValue
|
2020-03-11 22:15:05 -04:00
|
|
|
|
2020-03-11 23:31:42 -04:00
|
|
|
let sizeClassKey = dict[SizeKey]
|
2020-03-12 07:55:12 -04:00
|
|
|
let sizeClassID = getClassID(id: sizeClassKey)
|
|
|
|
let sizeString = objectsArray[sizeClassID] as! String
|
2020-03-11 23:31:42 -04:00
|
|
|
|
|
|
|
let sizeComponents = sizeString.replacingOccurrences(of: "{", with: "").replacingOccurrences(of: "}", with: "").components(separatedBy: ", ")
|
|
|
|
let width = Int(sizeComponents[0])
|
|
|
|
let height = Int(sizeComponents[1])
|
|
|
|
|
|
|
|
info.width = width!
|
|
|
|
info.height = height!
|
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
columns = Int(ceil(Float(info.width) / Float(info.tileSize)))
|
|
|
|
rows = Int(ceil(Float(info.height) / Float(info.tileSize)))
|
|
|
|
|
|
|
|
if info.width % info.tileSize != 0 {
|
|
|
|
remainderWidth = (columns * info.tileSize) - info.width
|
|
|
|
}
|
|
|
|
|
|
|
|
if info.height % info.tileSize != 0 {
|
|
|
|
remainderHeight = (rows * info.tileSize) - info.height
|
|
|
|
}
|
|
|
|
|
2020-03-11 10:24:52 -04:00
|
|
|
let layersClassKey = dict[LayersKey]
|
2020-03-12 07:55:12 -04:00
|
|
|
let layersClassID = getClassID(id: layersClassKey)
|
|
|
|
let layersClass = objectsArray[layersClassID] as! NSDictionary
|
2020-03-11 10:24:52 -04:00
|
|
|
|
|
|
|
let array = layersClass["NS.objects"] as! NSArray
|
|
|
|
|
|
|
|
for object in array {
|
2020-03-12 07:55:12 -04:00
|
|
|
let layerClassID = getClassID(id: object)
|
|
|
|
let layerClass = objectsArray[layerClassID] as! NSDictionary
|
2020-03-11 10:24:52 -04:00
|
|
|
|
2020-03-11 22:15:05 -04:00
|
|
|
parseSilicaLayer(archive: archive, dict: layerClass)
|
2020-03-11 10:24:52 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-11 22:15:05 -04:00
|
|
|
func parseDocument(archive: Archive, dict: NSDictionary) {
|
2020-03-11 10:24:52 -04:00
|
|
|
// double check if this archive is really correct
|
|
|
|
if let value = dict["$version"] {
|
2020-03-12 07:55:12 -04:00
|
|
|
if (value as! Int) != NSKeyedArchiveVersion {
|
2020-03-11 10:24:52 -04:00
|
|
|
Swift.print("This is not a valid document!")
|
2020-03-12 07:55:12 -04:00
|
|
|
return
|
2020-03-11 10:24:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
self.dict = dict
|
|
|
|
|
|
|
|
let objectsArray = dict["$objects"] as! NSArray
|
|
|
|
|
|
|
|
// let's read the $top class, which is always going to be SilicaDocument type.
|
|
|
|
let topObject = dict["$top"] as! NSDictionary
|
|
|
|
let topClassID = objectRefGetValue(topObject["root"] as CFTypeRef)
|
|
|
|
let topObjectClass = objectsArray[Int(topClassID)] as! NSDictionary
|
|
|
|
|
2020-03-11 22:15:05 -04:00
|
|
|
parseSilicaDocument(archive: archive, dict: topObjectClass)
|
2020-03-11 10:24:52 -04:00
|
|
|
}
|
|
|
|
}
|
2020-03-10 14:23:54 -04:00
|
|
|
|
|
|
|
override func read(from data: Data, ofType typeName: String) throws {
|
2020-03-11 22:44:52 -04:00
|
|
|
self.data = data
|
|
|
|
|
2020-03-10 14:23:54 -04:00
|
|
|
guard let archive = Archive(data: data, accessMode: Archive.AccessMode.read) else {
|
|
|
|
return
|
|
|
|
}
|
2020-03-10 16:23:25 -04:00
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
guard let documentEntry = archive[DocumentArchivePath] else {
|
2020-03-10 16:23:25 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
guard let documentData = readData(archive: archive, entry: documentEntry) else {
|
|
|
|
return
|
2020-03-10 16:23:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
var plistFormat = PropertyListSerialization.PropertyListFormat.binary
|
2020-03-12 07:55:12 -04:00
|
|
|
guard let propertyList = try? PropertyListSerialization.propertyList(from: documentData, options: [], format: &plistFormat) else {
|
|
|
|
return
|
2020-03-10 16:23:25 -04:00
|
|
|
}
|
2020-03-12 07:55:12 -04:00
|
|
|
|
|
|
|
parseDocument(archive: archive, dict: propertyList as! NSDictionary)
|
2020-03-10 14:23:54 -04:00
|
|
|
}
|
2020-03-11 23:31:42 -04:00
|
|
|
|
|
|
|
func makeComposite() -> NSImage {
|
2020-05-30 17:03:56 -04:00
|
|
|
var image = NSImage(size: NSSize(width: info.width, height: info.height))
|
2020-03-11 23:31:42 -04:00
|
|
|
image.lockFocus()
|
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
let color = NSColor.white
|
|
|
|
color.drawSwatch(in: NSRect(origin: .zero, size: image.size))
|
2020-03-11 23:31:42 -04:00
|
|
|
|
|
|
|
for layer in info.layers.reversed() {
|
|
|
|
for chunk in layer.chunks {
|
|
|
|
let x = chunk.x
|
|
|
|
var y = chunk.y
|
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
let (width, height) = getTileSize(x: x, y: y)
|
|
|
|
|
2020-03-11 23:31:42 -04:00
|
|
|
if y == rows {
|
|
|
|
y = 0
|
|
|
|
}
|
|
|
|
|
2020-03-12 07:55:12 -04:00
|
|
|
let rect = NSRect(x: info.tileSize * x, y: info.height - (info.tileSize * y), width: width, height: height)
|
2020-03-11 23:31:42 -04:00
|
|
|
|
|
|
|
chunk.image.draw(in: rect)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
image.unlockFocus()
|
2020-05-30 17:03:56 -04:00
|
|
|
|
|
|
|
if info.orientation == 3 {
|
|
|
|
image = image.imageRotatedByDegreess(degrees: 90)
|
|
|
|
} else if info.orientation == 4 {
|
|
|
|
image = image.imageRotatedByDegreess(degrees: -90)
|
|
|
|
}
|
|
|
|
|
|
|
|
if info.flippedHorizontally && (info.orientation == 1 || info.orientation == 2) {
|
|
|
|
image = image.flipHorizontally()
|
|
|
|
} else if info.flippedHorizontally && (info.orientation == 3 || info.orientation == 4) {
|
|
|
|
image = image.flipVertically()
|
|
|
|
} else if info.flippedVertically && (info.orientation == 1 || info.orientation == 2) {
|
|
|
|
image = image.flipVertically()
|
|
|
|
} else if !info.flippedVertically && (info.orientation == 3 || info.orientation == 4) {
|
|
|
|
image = image.flipHorizontally()
|
|
|
|
}
|
|
|
|
|
2020-03-11 23:31:42 -04:00
|
|
|
return image
|
|
|
|
}
|
2020-03-10 14:23:54 -04:00
|
|
|
}
|
|
|
|
|
2020-05-30 17:03:56 -04:00
|
|
|
public extension NSImage {
|
|
|
|
func imageRotatedByDegreess(degrees:CGFloat) -> NSImage {
|
|
|
|
var imageBounds = NSMakeRect(0.0, 0.0, size.width, size.height)
|
|
|
|
|
|
|
|
let pathBounds = NSBezierPath(rect: imageBounds)
|
|
|
|
var transform = NSAffineTransform()
|
|
|
|
transform.rotate(byDegrees: degrees)
|
|
|
|
pathBounds.transform(using: transform as AffineTransform)
|
|
|
|
|
|
|
|
let rotatedBounds:NSRect = NSMakeRect(NSZeroPoint.x, NSZeroPoint.y, pathBounds.bounds.size.width, pathBounds.bounds.size.height )
|
|
|
|
let rotatedImage = NSImage(size: rotatedBounds.size)
|
|
|
|
|
|
|
|
imageBounds.origin.x = NSMidX(rotatedBounds) - (NSWidth(imageBounds) / 2)
|
|
|
|
imageBounds.origin.y = NSMidY(rotatedBounds) - (NSHeight(imageBounds) / 2)
|
|
|
|
|
|
|
|
transform = NSAffineTransform()
|
|
|
|
transform.translateX(by: +(NSWidth(rotatedBounds) / 2 ), yBy: +(NSHeight(rotatedBounds) / 2))
|
|
|
|
transform.rotate(byDegrees: degrees)
|
|
|
|
transform.translateX(by: -(NSWidth(rotatedBounds) / 2 ), yBy: -(NSHeight(rotatedBounds) / 2))
|
|
|
|
|
|
|
|
rotatedImage.lockFocus()
|
|
|
|
transform.concat()
|
|
|
|
|
|
|
|
self.draw(in: imageBounds, from: .zero, operation: .copy, fraction: 1.0)
|
|
|
|
|
|
|
|
rotatedImage.unlockFocus()
|
|
|
|
|
|
|
|
return rotatedImage
|
|
|
|
}
|
|
|
|
|
|
|
|
func flipHorizontally() -> NSImage {
|
|
|
|
let flipedImage = NSImage(size: size)
|
|
|
|
flipedImage.lockFocus()
|
|
|
|
|
|
|
|
let transform = NSAffineTransform()
|
|
|
|
transform.translateX(by: size.width, yBy: 0.0)
|
|
|
|
transform.scaleX(by: -1.0, yBy: 1.0)
|
|
|
|
transform.concat()
|
|
|
|
|
|
|
|
let rect = NSMakeRect(0, 0, size.width, size.height)
|
|
|
|
self.draw(at: .zero, from: rect, operation: .sourceOver, fraction: 1.0)
|
|
|
|
|
|
|
|
flipedImage.unlockFocus()
|
|
|
|
|
|
|
|
return flipedImage
|
|
|
|
}
|
|
|
|
|
|
|
|
func flipVertically() -> NSImage {
|
|
|
|
let flipedImage = NSImage(size: size)
|
|
|
|
flipedImage.lockFocus()
|
|
|
|
|
|
|
|
let transform = NSAffineTransform()
|
|
|
|
transform.translateX(by: 0.0, yBy: size.height)
|
|
|
|
transform.scaleX(by: 1.0, yBy: -1.0)
|
|
|
|
transform.concat()
|
|
|
|
|
|
|
|
let rect = NSMakeRect(0, 0, size.width, size.height)
|
|
|
|
self.draw(at: .zero, from: rect, operation: .sourceOver, fraction: 1.0)
|
|
|
|
|
|
|
|
flipedImage.unlockFocus()
|
|
|
|
|
|
|
|
return flipedImage
|
|
|
|
}
|
|
|
|
}
|