From af6890dca814694748c885cb72cfaee2a4dbf8ff Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Thu, 16 Sep 2021 18:13:57 -0400 Subject: [PATCH] Add mask rendering functionality --- SilicaViewer/Document.swift | 149 +++++++++++++++++++++++++++++------- 1 file changed, 120 insertions(+), 29 deletions(-) diff --git a/SilicaViewer/Document.swift b/SilicaViewer/Document.swift index ca7b97b..b4be7f9 100644 --- a/SilicaViewer/Document.swift +++ b/SilicaViewer/Document.swift @@ -8,8 +8,16 @@ struct SilicaChunk { var image: NSImage = NSImage() } -struct SilicaLayer { +struct SilicaLayerData { + var blendMode: Int = 0 var chunks: [SilicaChunk] = [] + var opacity: Double = 1.0 + var hidden: Bool = false +} + +struct SilicaLayer { + var data: SilicaLayerData = SilicaLayerData() + var mask: SilicaLayerData? } struct SilicaDocument { @@ -112,16 +120,30 @@ class Document: NSDocument { } } - func parseSilicaLayer(archive: Archive, dict: NSDictionary) { + func parseSilicaLayer(archive: Archive, dict: NSDictionary, isMask: Bool) -> SilicaLayer? { let objectsArray = self.dict?["$objects"] as! NSArray if getDocumentClassName(dict: dict) == LayerClassName { var layer = SilicaLayer() + + dump(dict) let UUIDKey = dict["UUID"] let UUIDClassID = getClassID(id: UUIDKey) let UUIDClass = objectsArray[UUIDClassID] as! NSString + let maskKey = dict["mask"] + let maskClassID = getClassID(id: maskKey) + let maskClass = objectsArray[maskClassID] + + layer.data.blendMode = (dict["blend"] as? NSNumber)!.intValue + layer.data.opacity = (dict["opacity"] as? NSNumber)!.doubleValue + layer.data.hidden = (dict["hidden"] as? Bool)! + + if maskClassID != 0 { + layer.mask = parseSilicaLayer(archive: archive, dict: maskClass as! NSDictionary, isMask: true)?.data + } + var chunkPaths: [String] = [] archive.forEach { (entry: Entry) in @@ -130,7 +152,7 @@ class Document: NSDocument { } } - layer.chunks = Array(repeating: SilicaChunk(), count: chunkPaths.count) + layer.data.chunks = Array(repeating: SilicaChunk(), count: chunkPaths.count) let dispatchGroup = DispatchGroup() let queue = DispatchQueue(label: "imageWork") @@ -149,7 +171,13 @@ class Document: NSDocument { } let (width, height) = getTileSize(x: x, y: y) - let byteSize = width * height * 4 + + var numChannels = 4 + if isMask { + numChannels = 1 + } + + let byteSize = width * height * numChannels let uncompressedMemory = UnsafeMutablePointer.allocate(capacity: byteSize) @@ -166,20 +194,28 @@ class Document: NSDocument { let imageData = Data(bytes: uncompressedMemory, count: byteSize) let render: CGColorRenderingIntent = .defaultIntent - let rgbColorSpace = CGColorSpaceCreateDeviceRGB() - let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue).union(.byteOrder32Big) + var rgbColorSpace = CGColorSpaceCreateDeviceRGB() + + if isMask { + rgbColorSpace = CGColorSpaceCreateDeviceGray() + } + + var bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue).union(.byteOrder32Big) + if isMask { + bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.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 { + guard let cgimage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 8 * numChannels, bytesPerRow: width * numChannels, space: rgbColorSpace, bitmapInfo: bitmapInfo, provider: providerRef!, decode: nil, shouldInterpolate: false, intent: render) else { return } 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 + layer.data.chunks[i].image = image + layer.data.chunks[i].x = x + layer.data.chunks[i].y = y dispatchGroup.leave() } @@ -187,8 +223,10 @@ class Document: NSDocument { dispatchGroup.wait() - info.layers.append(layer) + return layer } + + return nil } func parseSilicaDocument(archive: Archive, dict: NSDictionary) { @@ -233,7 +271,8 @@ class Document: NSDocument { let layerClassID = getClassID(id: object) let layerClass = objectsArray[layerClassID] as! NSDictionary - parseSilicaLayer(archive: archive, dict: layerClass) + guard let layer = parseSilicaLayer(archive: archive, dict: layerClass, isMask: false) else { return } + info.layers.append(layer) } } } @@ -283,30 +322,82 @@ class Document: NSDocument { } func makeComposite() -> NSImage { - var image = NSImage(size: NSSize(width: info.width, height: info.height)) - image.lockFocus() - - let color = NSColor.white - color.drawSwatch(in: NSRect(origin: .zero, size: image.size)) + // create the final composite output image + let rgbColorSpace = CGColorSpaceCreateDeviceRGB() + let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big) + + let ccgContext = CGContext(data: nil, width: info.width, height: info.height, bitsPerComponent: 8, bytesPerRow: info.width * 4, space: rgbColorSpace, bitmapInfo: bitmapInfo.rawValue) + ccgContext?.setFillColor(.white) + ccgContext?.fill(CGRect(origin: .zero, size: CGSize(width: info.width, height: info.height))) + 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 - } + // start by creating a new layer composite image, needed for image masking + let layerContext = CGContext(data: nil, width: info.width, height: info.height, bitsPerComponent: 8, bytesPerRow: info.width * 4, space: rgbColorSpace, bitmapInfo: bitmapInfo.rawValue) - let rect = NSRect(x: info.tileSize * x, y: info.height - (info.tileSize * y), width: width, height: height) + let grayColorSpace = CGColorSpaceCreateDeviceGray() + let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big) + + let maskContext = CGContext(data: nil, width: info.width, height: info.height, bitsPerComponent: 16, bytesPerRow: 0, space: grayColorSpace, bitmapInfo: maskBitmapInfo.rawValue) - chunk.image.draw(in: rect) + if layer.mask != nil { + for chunk in layer.mask!.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: width, height: height) + + if !layer.data.hidden { + maskContext?.draw(chunk.image.cgImage(forProposedRect: nil, context: NSGraphicsContext.current, hints: nil)!, in: rect) + } + } + } else { + for chunk in layer.data.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: width, height: height) + + var op = CGBlendMode.copy + if layer.data.blendMode == 12 { + op = .hardLight + } + + layerContext?.setAlpha(CGFloat(layer.data.opacity)) + layerContext?.setBlendMode(op) + + if !layer.data.hidden { + layerContext?.draw(chunk.image.cgImage(forProposedRect: nil, context: NSGraphicsContext.current, hints: nil)!, in: rect) + } + } } + + var layerImage = (layerContext?.makeImage())! + if layer.mask != nil { + let maskImage = (maskContext?.makeImage())! + + layerImage = layerImage.masking(maskImage)! + } + + ccgContext?.setAlpha(1.0) + ccgContext?.setBlendMode(.sourceAtop) + + ccgContext?.draw(layerImage, in: CGRect(x: 0, y: 0, width: info.width, height: info.height)) } - image.unlockFocus() + var image = NSImage(cgImage: (ccgContext?.makeImage())!, size: NSSize(width: info.width, height: info.height)) if info.orientation == 3 { image = image.imageRotatedByDegreess(degrees: 90)