1
Fork 0

Rewrite image loading to be cleaner, safer, and more efficient

This commit is contained in:
Joshua Goins 2020-03-12 07:55:12 -04:00 committed by redstrate
parent 39a0de2c75
commit af24aeef3d
3 changed files with 147 additions and 180 deletions

View file

@ -198,14 +198,14 @@ DQ
</button>
</subviews>
<constraints>
<constraint firstItem="2oe-to-Rcz" firstAttribute="top" secondItem="8HM-na-o1E" secondAttribute="bottom" constant="8" symbolic="YES" id="85g-Dp-ezi"/>
<constraint firstItem="8HM-na-o1E" firstAttribute="top" secondItem="u9u-el-oA4" secondAttribute="bottom" constant="8" symbolic="YES" id="CA1-bq-4Dc"/>
<constraint firstItem="u9u-el-oA4" firstAttribute="trailing" secondItem="8HM-na-o1E" secondAttribute="trailing" id="ILX-8S-pOI"/>
<constraint firstItem="u9u-el-oA4" firstAttribute="top" secondItem="3vu-Kd-l73" secondAttribute="top" constant="20" symbolic="YES" id="IQj-on-T5L"/>
<constraint firstItem="u9u-el-oA4" firstAttribute="leading" secondItem="8HM-na-o1E" secondAttribute="leading" id="KJd-X5-kxh"/>
<constraint firstAttribute="trailing" secondItem="u9u-el-oA4" secondAttribute="trailing" constant="20" symbolic="YES" id="PlA-zS-FpK"/>
<constraint firstItem="2oe-to-Rcz" firstAttribute="trailing" secondItem="8HM-na-o1E" secondAttribute="trailing" id="Whu-Lh-znF"/>
<constraint firstItem="u9u-el-oA4" firstAttribute="leading" secondItem="3vu-Kd-l73" secondAttribute="leading" constant="20" symbolic="YES" id="Xzy-ZR-1ha"/>
<constraint firstItem="2oe-to-Rcz" firstAttribute="top" secondItem="8HM-na-o1E" secondAttribute="bottom" constant="8" symbolic="YES" id="ZQM-5m-wzY"/>
<constraint firstItem="2oe-to-Rcz" firstAttribute="trailing" secondItem="8HM-na-o1E" secondAttribute="trailing" id="oGX-cy-Uhv"/>
</constraints>
</view>
<connections>
@ -247,49 +247,14 @@ DQ
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ipM-NH-wUM">
<rect key="frame" x="20" y="20" width="440" height="201"/>
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="chN-IE-x52"/>
</imageView>
<popUpButton verticalHuggingPriority="750" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ifc-N4-GCF">
<rect key="frame" x="18" y="226" width="101" height="25"/>
<constraints>
<constraint firstAttribute="width" constant="96" id="Csg-An-ug2"/>
</constraints>
<popUpButtonCell key="cell" type="push" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" id="P5l-BO-Pd1">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<menu key="menu" id="n0K-5D-di9"/>
</popUpButtonCell>
<connections>
<action selector="selectLayerAction:" target="5gI-5U-AMq" id="nzQ-6F-mUw"/>
</connections>
</popUpButton>
<popUpButton verticalHuggingPriority="750" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vY4-dQ-JOJ">
<rect key="frame" x="122" y="226" width="101" height="25"/>
<constraints>
<constraint firstAttribute="width" constant="96" id="FeE-01-aTL"/>
</constraints>
<popUpButtonCell key="cell" type="push" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" id="IVG-g2-g5C">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<menu key="menu" id="H7D-h4-TRv"/>
</popUpButtonCell>
<connections>
<action selector="selectChunkAction:" target="5gI-5U-AMq" id="JPQ-dG-2AN"/>
</connections>
</popUpButton>
</subviews>
<constraints>
<constraint firstItem="Ifc-N4-GCF" firstAttribute="top" secondItem="ERx-hH-rdd" secondAttribute="top" constant="20" symbolic="YES" id="BmT-9r-weG"/>
<constraint firstItem="vY4-dQ-JOJ" firstAttribute="leading" secondItem="Ifc-N4-GCF" secondAttribute="trailing" constant="8" symbolic="YES" id="hgm-Np-cYB"/>
<constraint firstItem="vY4-dQ-JOJ" firstAttribute="baseline" secondItem="Ifc-N4-GCF" secondAttribute="baseline" id="uHT-fd-ze3"/>
</constraints>
</view>
<connections>
<outlet property="chunkPopup" destination="vY4-dQ-JOJ" id="o1u-J8-bIL"/>
<outlet property="imageView" destination="ipM-NH-wUM" id="RT5-09-Hwj"/>
<outlet property="layerPopup" destination="Ifc-N4-GCF" id="7hY-Va-SMT"/>
<segue destination="wda-Mt-beD" kind="sheet" identifier="showInfo" id="obo-Yt-yny"/>
</connections>
</viewController>

View file

@ -3,15 +3,9 @@ import ZIPFoundation
import CoreFoundation
struct SilicaChunk {
init() {
x = 0
y = 0
image = NSImage()
}
var x: Int
var y: Int
var image: NSImage
var x: Int = 0
var y: Int = 0
var image: NSImage = NSImage()
}
struct SilicaLayer {
@ -38,6 +32,11 @@ class Document: NSDocument {
var dict: NSDictionary?
let NSKeyedArchiveVersion = 100000
let ThumbnailPath = "QuickLook/Thumbnail.png"
let DocumentArchivePath = "Document.archive"
let DocumentClassName = "SilicaDocument"
let TrackedTimeKey = "SilicaDocumentTrackedTimeKey"
let LayersKey = "layers"
@ -47,9 +46,13 @@ class Document: NSDocument {
let LayerClassName = "SilicaLayer"
var info = SilicaDocument()
var rows: Int = 0
var columns: Int = 0
var remainderWidth: Int = 0
var remainderHeight: Int = 0
var thumbnail: NSImage? = nil
override init() {
super.init()
}
@ -76,6 +79,64 @@ class Document: NSDocument {
return nil
}
/*
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
}
}
func readData(archive: Archive, entry: Entry) -> Data? {
var data = Data()
do {
let _ = try archive.extract(entry, consumer: { (d) in
data.append(d)
})
} catch {
Swift.print("Extracting entry from archive failed with error:\(error)")
return nil
}
return data
}
func parseSilicaLayer(archive: Archive, dict: NSDictionary) {
let objectsArray = self.dict?["$objects"] as! NSArray
@ -83,70 +144,76 @@ class Document: NSDocument {
var layer = SilicaLayer()
let UUIDKey = dict["UUID"]
let UUIDClassID = objectRefGetValue(UUIDKey as CFTypeRef)
let UUIDClass = objectsArray[Int(UUIDClassID)] as! NSString
let UUIDClassID = getClassID(id: UUIDKey)
let UUIDClass = objectsArray[UUIDClassID] as! NSString
var chunkPaths: [Entry] = []
var chunkPaths: [String] = []
archive.forEach { (entry: Entry) in
if entry.path.contains(String(UUIDClass)) {
chunkPaths.append(entry)
chunkPaths.append(entry.path)
}
}
layer.chunks = Array(repeating: SilicaChunk(), count: chunkPaths.count)
let dispatchGroup = DispatchGroup()
let queue = DispatchQueue(label: "imageWork")
DispatchQueue.concurrentPerform(iterations: chunkPaths.count) { (i: Int) in
let entry = chunkPaths[i]
dispatchGroup.enter()
var threadArchive: Archive?
var threadEntry: Entry?
let pathURL = URL(fileURLWithPath: entry.path)
let pathComponents = pathURL.lastPathComponent.replacingOccurrences(of: ".chunk", with: "").components(separatedBy: "~")
let x = Int(pathComponents[0])
let y = Int(pathComponents[1])
layer.chunks[i].x = x!
layer.chunks[i].y = y!
guard let archive = Archive(data: self.data!, accessMode: Archive.AccessMode.read) else {
queue.sync {
threadArchive = Archive(data: self.data!, accessMode: Archive.AccessMode.read)
threadEntry = threadArchive?[chunkPaths[i]]
}
guard let (x, y) = parseChunkFilename(filename: threadEntry!.path) else {
return
}
var lzo_data = Data()
let (width, height) = getTileSize(x: x, y: y)
let byteSize = width * height * 4
do {
try archive.extract(entry, consumer: { (d) in
lzo_data.append(d)
})
} catch {
Swift.print("Extracting entry from archive failed with error:\(error)")
let uncompressedMemory = UnsafeMutablePointer<UInt8>.allocate(capacity: byteSize)
guard let lzoData = readData(archive: threadArchive!, entry: threadEntry!) else {
return
}
let uint8Pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: info.tileSize * info.tileSize * 4)
lzo_data.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) -> Void in
var len = lzo_uint(info.tileSize * info.tileSize * 4)
lzoData.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) -> Void in
var len = lzo_uint(byteSize)
lzo1x_decompress_safe(bytes.baseAddress!.assumingMemoryBound(to: uint8.self), lzo_uint(lzo_data.count), uint8Pointer, &len, nil)
lzo1x_decompress_safe(bytes.baseAddress!.assumingMemoryBound(to: uint8.self), lzo_uint(lzoData.count), uncompressedMemory, &len, nil)
})
let image_data = Data(bytes: uint8Pointer, count: info.tileSize * info.tileSize * 4)
let imageData = Data(bytes: uncompressedMemory, count: byteSize)
let render: CGColorRenderingIntent = CGColorRenderingIntent.defaultIntent
let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue)
.union(.byteOrder32Big)
let providerRef: CGDataProvider? = CGDataProvider(data: image_data as CFData)
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue).union(.byteOrder32Big)
let providerRef: CGDataProvider? = CGDataProvider(data: imageData as CFData)
let cgimage: CGImage? = CGImage(width: info.tileSize, height: info.tileSize, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: info.tileSize * 4, space: rgbColorSpace, bitmapInfo: bitmapInfo, provider: providerRef!, decode: nil, shouldInterpolate: false, intent: render)
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
}
if cgimage != nil {
let image = NSImage(cgImage: cgimage!, size: NSZeroSize)
let image = NSImage(cgImage: cgimage, size: NSZeroSize)
queue.async(flags: .barrier) {
layer.chunks[i].image = image
layer.chunks[i].x = x
layer.chunks[i].y = y
dispatchGroup.leave()
}
}
dispatchGroup.wait()
info.layers.append(layer)
}
}
@ -159,8 +226,8 @@ class Document: NSDocument {
info.tileSize = (dict[TileSizeKey] as! NSNumber).intValue
let sizeClassKey = dict[SizeKey]
let sizeClassID = objectRefGetValue(sizeClassKey as CFTypeRef)
let sizeString = objectsArray[Int(sizeClassID)] as! String
let sizeClassID = getClassID(id: sizeClassKey)
let sizeString = objectsArray[sizeClassID] as! String
let sizeComponents = sizeString.replacingOccurrences(of: "{", with: "").replacingOccurrences(of: "}", with: "").components(separatedBy: ", ")
let width = Int(sizeComponents[0])
@ -169,15 +236,26 @@ class Document: NSDocument {
info.width = width!
info.height = height!
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
}
let layersClassKey = dict[LayersKey]
let layersClassID = objectRefGetValue(layersClassKey as CFTypeRef)
let layersClass = objectsArray[Int(layersClassID)] as! NSDictionary
let layersClassID = getClassID(id: layersClassKey)
let layersClass = objectsArray[layersClassID] as! NSDictionary
let array = layersClass["NS.objects"] as! NSArray
for object in array {
let layerClassID = objectRefGetValue(object as CFTypeRef)
let layerClass = objectsArray[Int(layerClassID)] as! NSDictionary
let layerClassID = getClassID(id: object)
let layerClass = objectsArray[layerClassID] as! NSDictionary
parseSilicaLayer(archive: archive, dict: layerClass)
}
@ -187,8 +265,9 @@ class Document: NSDocument {
func parseDocument(archive: Archive, dict: NSDictionary) {
// double check if this archive is really correct
if let value = dict["$version"] {
if (value as! Int) != 100000 {
if (value as! Int) != NSKeyedArchiveVersion {
Swift.print("This is not a valid document!")
return
}
self.dict = dict
@ -210,68 +289,42 @@ class Document: NSDocument {
guard let archive = Archive(data: data, accessMode: Archive.AccessMode.read) else {
return
}
// load thumbnail
guard let entry = archive["QuickLook/Thumbnail.png"] else {
return
}
var top_data = Data()
do {
try archive.extract(entry, consumer: { (d) in
top_data.append(d)
})
} catch {
Swift.print("Extracting entry from archive failed with error:\(error)")
}
thumbnail = NSImage(data: top_data)
// load doc info
guard let document_entry = archive["Document.archive"] else {
guard let documentEntry = archive[DocumentArchivePath] else {
return
}
var doc_data = Data()
do {
try archive.extract(document_entry, consumer: { (d) in
doc_data.append(d)
})
} catch {
Swift.print("Extracting entry from archive failed with error:\(error)")
guard let documentData = readData(archive: archive, entry: documentEntry) else {
return
}
// Document.archive is a binary plist (specifically a NSKeyedArchive), luckily swift has a built-in solution to decode it
var plistFormat = PropertyListSerialization.PropertyListFormat.binary
let plistBinary = doc_data
guard let propertyList = try? PropertyListSerialization.propertyList(from: plistBinary, options: [], format: &plistFormat) else {
fatalError("failed to deserialize")
guard let propertyList = try? PropertyListSerialization.propertyList(from: documentData, options: [], format: &plistFormat) else {
return
}
let dict = (propertyList as! NSDictionary);
parseDocument(archive: archive, dict: dict)
parseDocument(archive: archive, dict: propertyList as! NSDictionary)
}
func makeComposite() -> NSImage {
let image = NSImage(size: NSSize(width: info.width, height: info.height))
image.lockFocus()
let rows = Int(ceil(Float(info.height) / Float(info.tileSize)))
let color = NSColor.white
color.drawSwatch(in: NSRect(origin: .zero, size: image.size))
for layer in info.layers.reversed() {
for chunk in layer.chunks {
let x = chunk.x
var y = chunk.y
let (width, height) = getTileSize(x: x, y: y)
if y == rows {
y = 0
}
let rect = NSRect(x: info.tileSize * x, y: info.height - (info.tileSize * y), width: info.tileSize, height: info.tileSize)
let rect = NSRect(x: info.tileSize * x, y: info.height - (info.tileSize * y), width: width, height: height)
chunk.image.draw(in: rect)
}

View file

@ -3,62 +3,11 @@ import Cocoa
class ViewController: NSViewController {
@IBOutlet weak var imageView: NSImageView!
@IBOutlet weak var layerPopup: NSPopUpButton!
@IBOutlet weak var chunkPopup: NSPopUpButton!
var selectedLayer: Int = 0
var selectedChunk: Int = 0
func parseName(str: String) -> Int? {
if let int = Int(str.components(separatedBy: " ")[1]) {
return int
}
return nil
}
func loadCanvas() {
let document = self.view.window?.windowController?.document as? Document
imageView.image = document?.info.layers[selectedLayer].chunks[selectedChunk].image
}
@IBAction func selectLayerAction(_ sender: Any) {
selectedLayer = parseName(str: (layerPopup.titleOfSelectedItem)!)!
selectedChunk = 0
loadLayerChunks()
loadCanvas()
}
@IBAction func selectChunkAction(_ sender: Any) {
selectedChunk = parseName(str: (chunkPopup.titleOfSelectedItem)!)!
loadCanvas()
}
func loadLayerChunks() {
let document = self.view.window?.windowController?.document as? Document
chunkPopup.removeAllItems()
for (i, chunk) in (document?.info.layers[selectedLayer].chunks.enumerated())! {
chunkPopup.addItem(withTitle: "Chunk " + String(chunk.x) + " " + String(chunk.y))
}
}
override func viewWillAppear() {
let document = self.view.window?.windowController?.document as? Document
imageView.image = document?.makeComposite()
for (i, layer) in (document?.info.layers.enumerated())! {
layerPopup.addItem(withTitle: "Layer " + String(i))
}
loadLayerChunks()
}
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {