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)
@ -585,17 +592,31 @@ class Document: NSDocument {
let layerImage = layerContext?.makeImage() let layerImage = layerContext?.makeImage()
if layer.clipped && previousImage != nil { // apply image
let result = previousImage!.toGrayscale() return kernel!.apply(foreground: CIImage(cgImage: layerImage!), background: previousImage == nil ? CIImage(color: .clear) : CIImage(cgImage: previousImage!), colorSpace: info.colorSpace)!
let newImage = layerImage!.masking(result!)
previousImage = newImage
} else {
previousImage = layerImage
} }
// apply image // this returns all of the layers that are clipping onto this one
return kernel!.apply(foreground: CIImage(cgImage: previousImage!), background: CIImage(color: .clear), colorSpace: info.colorSpace)! 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,7 +647,10 @@ 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()
@ -651,14 +675,36 @@ class Document: NSDocument {
} }
} }
let clippingLayers = getAllClippingLayers(layer: layer)
if !clippingLayers.isEmpty {
let layerImage = layerContext?.makeImage() let layerImage = layerContext?.makeImage()
if layer.clipped && previousImage != nil { var clippedMaster: CGImage? = layerImage
let result = previousImage!.toGrayscale() for layer in clippingLayers {
let newImage = layerImage!.masking(result!) 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()
if layer.mask != nil && maskContext != nil {
Swift.print("masking " + layer.name + "...")
previousImage = newImage
} else if layer.mask != nil && maskContext != nil {
let maskImage = (maskContext?.makeImage())! let maskImage = (maskContext?.makeImage())!
let newImage = layerImage!.masking(maskImage)! let newImage = layerImage!.masking(maskImage)!
@ -779,34 +825,52 @@ public extension NSImage {
} }
} }
public extension CGImage { class ColorFilter: CIFilter {
// 2
var inputImage: CIImage?
// 3
static var kernel: CIKernel = { () -> CIColorKernel in
guard let url = Bundle.main.url(forResource: "default",
withExtension: "metallib"),
let data = try? Data(contentsOf: url) else {
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? { func toGrayscale() -> CGImage? {
let ciImage = CIImage(cgImage: self) let rect = CGRect(x: 0, y: 0, width: width, height: height)
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 {
return nil
}
guard let image = CIContext().createCGImage(intermediateImage, from: CGRect(origin: .zero, size: CGSize(width: self.width, height: self.height))) else {
return 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)
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()
} }
} }