1
Fork 0

Add the rest of the supported Photoshop blend modes, fix clipping

This is not the final blending code, it will go through another refactor
but it's an improvement from before.
This commit is contained in:
Joshua Goins 2022-05-19 11:00:51 -04:00
parent 985241a8b3
commit acda007eee
4 changed files with 176 additions and 67 deletions

View file

@ -52,7 +52,23 @@
kPSDBlendModeLighter, kPSDBlendModeLighter,
kPSDBlendModeVividLight, kPSDBlendModeVividLight,
kPSDBlendModeHardLight kPSDBlendModeHardLight,
kPSDBlendModeColor,
kPSDBlendModeAdd,
kPSDBlendModeLighterColor,
kPSdBlendModeLinearLight,
kPSDBlendModePinLight,
kPSDBlendModeHardMix,
kPSDBlendModeDivide,
kPSDBlendModeOverlay,
kPSDBlendSoftLight,
kPSDBlendModeDifference,
kPSDBlendModeExclusion,
kPSDBlendModeSubtract,
kPSDBlendModeHue,
kPSDBlendModeSaturation,
kPSDBlendModeLuminosity,
}; };
@property (nonatomic, assign) NSInteger blendMode; @property (nonatomic, assign) NSInteger blendMode;

View file

@ -43,6 +43,19 @@ char blendModeLumKey[4] = {'l','u','m',' '};
char blendModevLiteKey[4] = {'v','L','i','t'}; char blendModevLiteKey[4] = {'v','L','i','t'};
char blendModehLiteKey[4] = {'h','L','i','t'}; char blendModehLiteKey[4] = {'h','L','i','t'};
char blendModeAddKey[4] = {'l', 'd', 'd', 'g'};
char blendModeLigColorKey[4] = {'l', 'g', 'C', 'l'};
char blendModeLinColorKey[4] = {'l', 'L', 'i', 't'};
char blendModePinLightKey[4] = {'p', 'L', 'i', 't'};
char blendModeHardMixKey[4] = {'h', 'M', 'i', 'x'};
char blendModeDivideKey[4] = {'f', 'd', 'i', 'v'};
char blendModeOverlayKey[4] = {'o', 'v', 'e', 'r'};
char blendModeSofLightKey[4] = {'s', 'L', 'i', 't'};
char blendModeDiffKey[4] = {'d', 'i', 'f', 'f'};
char blendModeExclKey[4] = {'s', 'm', 'u', 'd'};
char blendModeSubKey[4] = {'f', 's', 'u', 'b'};
char blendModeSatuKey[4] = {'s', 'a', 't', ' '};
char blendModeLuminKey[4] = {'l', 'u', 'm', ' '};
char *blendModes[36] = char *blendModes[36] =
{ &blendModeNormKey, { &blendModeNormKey,
@ -63,6 +76,22 @@ char *blendModes[36] =
&blendModevLiteKey, &blendModevLiteKey,
&blendModehLiteKey, &blendModehLiteKey,
&blendModeColKey,
&blendModeAddKey,
&blendModeLigColorKey,
&blendModeLinColorKey,
&blendModePinLightKey,
&blendModeHardMixKey,
&blendModeDivideKey,
&blendModeOverlayKey,
&blendModeSofLightKey,
&blendModeDiffKey,
&blendModeExclKey,
&blendModeSubKey,
&blendModeHueKey,
&blendModeSatuKey,
&blendModeLuminKey,
0 }; 0 };

View file

@ -66,67 +66,67 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
return kPSDBlendModeScreen return kPSDBlendModeScreen
} }
if layer.data.blendMode == 13 { if layer.data.blendMode == 13 {
return kPSDBlendModeHardLight return kPSDBlendModeColor
} }
if layer.data.blendMode == 9 { if layer.data.blendMode == 9 {
return kPSDBlendModeColorDodge return kPSDBlendModeColorDodge
} }
if layer.data.blendMode == 3 { if layer.data.blendMode == 3 {
//blendMode = kPSDBlendModeSubtract return kPSDBlendModeAdd
} }
if layer.data.blendMode == 0 && layer.data.blendMode != layer.data.extendedBlend { if layer.data.blendMode == 0 && layer.data.blendMode != layer.data.extendedBlend {
if layer.data.extendedBlend == 25 { if layer.data.extendedBlend == 25 {
return kPSDBlendModeDarkerColor return kPSDBlendModeDarkerColor
} }
if layer.data.extendedBlend == 24 { if layer.data.extendedBlend == 24 {
//blendMode = kPSDBlendModeLighterColor return kPSDBlendModeLighterColor
} }
if layer.data.extendedBlend == 21 { if layer.data.extendedBlend == 21 {
return kPSDBlendModeVividLight return kPSDBlendModeVividLight
} }
if layer.data.extendedBlend == 22 { if layer.data.extendedBlend == 22 {
//blendMode = kPSdBlendModeLinearLight return kPSdBlendModeLinearLight
} }
if layer.data.extendedBlend == 23 { if layer.data.extendedBlend == 23 {
//blendMode = kPSDBlendModePinLight return kPSDBlendModePinLight
} }
if layer.data.extendedBlend == 20 { if layer.data.extendedBlend == 20 {
//blendMode = kPSDBlendModeHardMix return kPSDBlendModeHardMix
} }
if layer.data.extendedBlend == 26 { if layer.data.extendedBlend == 26 {
//blendMode = kPSDBlendModeDivide return kPSDBlendModeDivide
} }
} }
if layer.data.blendMode == 11 { if layer.data.blendMode == 11 {
//blendMode = kPSDBlendModeOverlay return kPSDBlendModeOverlay
} }
if layer.data.blendMode == 17 { if layer.data.blendMode == 17 {
//return .softLight return kPSDBlendSoftLight
} }
if layer.data.blendMode == 12 { if layer.data.blendMode == 12 {
return kPSDBlendModeHardLight return kPSDBlendModeHardLight
} }
if layer.data.blendMode == 6 { if layer.data.blendMode == 6 {
//return .difference return kPSDBlendModeDifference
} }
if layer.data.blendMode == 5 { if layer.data.blendMode == 5 {
//return .exclusion return kPSDBlendModeExclusion
} }
if layer.data.blendMode == 7 { if layer.data.blendMode == 7 {
//return .componentAdd return kPSDBlendModeSubtract
} }
if layer.data.blendMode == 15 { if layer.data.blendMode == 15 {
//return .hue return kPSDBlendModeHue
} }
if layer.data.blendMode == 16 { if layer.data.blendMode == 16 {
//return .saturation return kPSDBlendModeSaturation
} }
if layer.data.blendMode == 13 { if layer.data.blendMode == 13 {
//return .color return kPSDBlendModeColor
} }
if layer.data.blendMode == 14 { if layer.data.blendMode == 14 {
//return .luminosity return kPSDBlendModeLuminosity
} }
return kPSDBlendModeNormal return kPSDBlendModeNormal

View file

@ -18,7 +18,11 @@ struct SilicaLayerData {
var hidden: Bool = false var hidden: Bool = false
} }
struct SilicaLayer { struct SilicaLayer : Equatable {
static func == (lhs: SilicaLayer, rhs: SilicaLayer) -> Bool {
return lhs.name == rhs.name && lhs.clipped == rhs.clipped
}
var name: String = "" var name: String = ""
var data: SilicaLayerData = SilicaLayerData() var data: SilicaLayerData = SilicaLayerData()
var mask: SilicaLayerData? var mask: SilicaLayerData?
@ -210,13 +214,13 @@ class Document: NSDocument {
return .screen return .screen
} }
if layer.data.blendMode == 13 { if layer.data.blendMode == 13 {
return .hardLight return .color
} }
if layer.data.blendMode == 9 { if layer.data.blendMode == 9 {
return .colorDodge return .colorDodge
} }
if layer.data.blendMode == 3 { if layer.data.blendMode == 3 {
return .subtract return .componentAdd
} }
if layer.data.blendMode == 0 && layer.data.blendMode != layer.data.extendedBlend { if layer.data.blendMode == 0 && layer.data.blendMode != layer.data.extendedBlend {
@ -259,7 +263,7 @@ class Document: NSDocument {
return .exclusion return .exclusion
} }
if layer.data.blendMode == 7 { if layer.data.blendMode == 7 {
return .componentAdd return .subtract
} }
if layer.data.blendMode == 15 { if layer.data.blendMode == 15 {
return .hue return .hue
@ -491,6 +495,7 @@ class Document: NSDocument {
let layerClass = objectsArray[layerClassID] as! NSDictionary let layerClass = objectsArray[layerClassID] as! NSDictionary
guard let layer = parseSilicaLayer(archive: archive, dict: layerClass, isMask: false) else { return } guard let layer = parseSilicaLayer(archive: archive, dict: layerClass, isMask: false) else { return }
info.layers.append(layer) info.layers.append(layer)
} }
} }
@ -574,6 +579,8 @@ class Document: NSDocument {
let kernel = getBlendKernel(layer) let kernel = getBlendKernel(layer)
Swift.print(layer.name + " - " + kernel!.name)
for chunk in layer.data.chunks { for chunk in layer.data.chunks {
layerContext?.setAlpha(CGFloat(layer.data.opacity)) layerContext?.setAlpha(CGFloat(layer.data.opacity))
layerContext?.setBlendMode(.normal) layerContext?.setBlendMode(.normal)
@ -582,20 +589,34 @@ class Document: NSDocument {
layerContext?.draw(chunk.image!, in: getChunkRect(chunk)) layerContext?.draw(chunk.image!, in: getChunkRect(chunk))
} }
} }
let layerImage = layerContext?.makeImage()
if layer.clipped && previousImage != nil { let layerImage = layerContext?.makeImage()
let result = previousImage!.toGrayscale()
let newImage = layerImage!.masking(result!)
previousImage = newImage
} else {
previousImage = layerImage
}
// apply image // apply image
return kernel!.apply(foreground: CIImage(cgImage: previousImage!), background: CIImage(color: .clear), colorSpace: info.colorSpace)! return kernel!.apply(foreground: CIImage(cgImage: layerImage!), background: previousImage == nil ? CIImage(color: .clear) : CIImage(cgImage: previousImage!), colorSpace: info.colorSpace)!
}
// this returns all of the layers that are clipping onto this one
func getAllClippingLayers(layer: SilicaLayer) -> [SilicaLayer] {
var clippingLayers : [SilicaLayer] = []
let layers : [SilicaLayer] = info.layers.reversed()
let index = layers.firstIndex(of: layer)! + 1
if index >= layers.count {
return clippingLayers
}
for layerIndex in index...layers.count - 1 {
//Swift.print("checking " + layers[layerIndex].name + " is clipping = " + layers[layerIndex].clipped.description)
if(layers[layerIndex].clipped) {
clippingLayers.append(layers[layerIndex])
} else {
break
}
}
return clippingLayers
} }
func makeComposite() -> NSImage? { func makeComposite() -> NSImage? {
@ -618,7 +639,7 @@ class Document: NSDocument {
var previousImage: CGImage? = nil var previousImage: CGImage? = nil
for layer in info.layers.reversed() { for layer in info.layers.reversed() {
if !layer.data.hidden { if !layer.data.hidden && !layer.clipped {
// start by creating a new layer composite image, needed for image masking // 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: info.colorSpace, bitmapInfo: bitmapInfo.rawValue) let layerContext = CGContext(data: nil, width: info.width, height: info.height, bitsPerComponent: 8, bytesPerRow: info.width * 4, space: info.colorSpace, bitmapInfo: bitmapInfo.rawValue)
@ -626,8 +647,11 @@ class Document: NSDocument {
var maskContext: CGContext? var maskContext: CGContext?
let kernel = getBlendKernel(layer) var kernel = getBlendKernel(layer)
//kernel = .sourceOver
Swift.print(layer.name + " - " + kernel!.name)
if layer.mask != nil { if layer.mask != nil {
let grayColorSpace = CGColorSpaceCreateDeviceGray() let grayColorSpace = CGColorSpaceCreateDeviceGray()
let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big) let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big)
@ -650,19 +674,41 @@ class Document: NSDocument {
layerContext?.draw(chunk.image!, in: getChunkRect(chunk)) layerContext?.draw(chunk.image!, in: getChunkRect(chunk))
} }
} }
let clippingLayers = getAllClippingLayers(layer: layer)
if !clippingLayers.isEmpty {
let layerImage = layerContext?.makeImage()
var clippedMaster: CGImage? = layerImage
for layer in clippingLayers {
Swift.print("- " + layer.name + " is clipping with us...")
// so we if we want to clip, we want to gather all of the clipping layers in order first...
Swift.print("processing clipped layer " + layer.name)
let temporaryClippedMaster = blendLayer(layer, previousImage: &clippedMaster)
clippedMaster = context.createCGImage(temporaryClippedMaster, from: info.cgRect, format: .RGBA8, colorSpace: info.colorSpace)
}
Swift.print("clipping " + layer.name + "...")
layerContext?.setAlpha(1.0)
layerContext?.setBlendMode(.sourceAtop)
layerContext?.draw(clippedMaster!, in: info.cgRect)
}
let layerImage = layerContext?.makeImage() let layerImage = layerContext?.makeImage()
if layer.clipped && previousImage != nil { if layer.mask != nil && maskContext != nil {
let result = previousImage!.toGrayscale() Swift.print("masking " + layer.name + "...")
let newImage = layerImage!.masking(result!)
let maskImage = (maskContext?.makeImage())!
previousImage = newImage let newImage = layerImage!.masking(maskImage)!
} else if layer.mask != nil && maskContext != nil {
let maskImage = (maskContext?.makeImage())!
let newImage = layerImage!.masking(maskImage)!
previousImage = newImage previousImage = newImage
} else { } else {
previousImage = layerImage previousImage = layerImage
} }
@ -779,34 +825,52 @@ public extension NSImage {
} }
} }
public extension CGImage { class ColorFilter: CIFilter {
func toGrayscale() -> CGImage? { // 2
let ciImage = CIImage(cgImage: self) var inputImage: CIImage?
let filter = CIFilter(name: "CIColorControls")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(5.0, forKey: kCIInputBrightnessKey)
filter?.setValue(0.0, forKey: kCIInputSaturationKey)
filter?.setValue(1.1, forKey: kCIInputContrastKey)
guard let intermediateImage = filter?.outputImage else { // 3
return nil static var kernel: CIKernel = { () -> CIColorKernel in
} guard let url = Bundle.main.url(forResource: "default",
withExtension: "metallib"),
guard let image = CIContext().createCGImage(intermediateImage, from: CGRect(origin: .zero, size: CGSize(width: self.width, height: self.height))) else { let data = try? Data(contentsOf: url) else {
return nil fatalError("Unable to load metallib")
} }
guard let kernel = try? CIColorKernel(
functionName: "colorFilterKernel",
fromMetalLibraryData: data) else {
fatalError("Unable to create color kernel")
}
return kernel
}()
// 4
override var outputImage: CIImage? {
guard let inputImage = inputImage else { return nil }
return ColorFilter.kernel.apply(
extent: inputImage.extent,
roiCallback: { _, rect in
return rect
},
arguments: [inputImage])
}
}
extension CGImage {
func toGrayscale() -> CGImage? {
let rect = CGRect(x: 0, y: 0, width: width, height: height)
let grayColorSpace = CGColorSpaceCreateDeviceGray() let grayColorSpace = CGColorSpaceCreateDeviceGray()
let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big) let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big)
let maskContext = CGContext(data: nil, width: self.width, height: self.height, bitsPerComponent: 16, bytesPerRow: 0, space: grayColorSpace, bitmapInfo: maskBitmapInfo.rawValue) let ccgContext = CGContext(data: nil, width: width, height: height, bitsPerComponent: 16, bytesPerRow: width * 2, space: grayColorSpace, bitmapInfo: maskBitmapInfo.rawValue)
maskContext?.setFillColor(.black) ccgContext?.setFillColor(.black)
maskContext?.fill(CGRect(origin: .zero, size: CGSize(width: self.width, height: self.height))) ccgContext?.fill(rect)
maskContext?.draw(image, in: CGRect(origin: .zero, size: CGSize(width: self.width, height: self.height))) ccgContext?.draw(self, in: rect)
return maskContext?.makeImage() return ccgContext?.makeImage()
} }
} }