From 57f39af15566378f06ccbae8e409297a025c12d6 Mon Sep 17 00:00:00 2001 From: Condy Date: Fri, 29 Mar 2024 16:25:34 +0800 Subject: [PATCH] Add some filters and render view --- .../Views/CustomViews.swift | 2 +- .../Modules/UnitTestViewController.swift | 30 ++-- .../Base.lproj/Main.storyboard | 4 +- Harbeth.podspec | 2 +- Harbeth.xcodeproj/project.pbxproj | 20 +++ README.md | 11 ++ Sources/Basic/Core/Device.swift | 6 +- Sources/Basic/Core/Filtering.swift | 6 + Sources/Basic/Core/Rendering.swift | 2 +- Sources/Basic/Extensions/C7Image+Ext.swift | 5 +- Sources/Basic/Extensions/C7Image+macOS.swift | 16 +- Sources/Basic/Extensions/CGImage+Ext.swift | 21 +-- Sources/Basic/Matrix/Matrix3x3.swift | 2 +- Sources/Basic/Matrix/Matrix4x4.swift | 2 +- Sources/Basic/Matrix/Matrix4x5.swift | 2 +- Sources/Basic/Matrix/Vector3.swift | 2 +- Sources/Basic/Matrix/Vector4.swift | 2 +- Sources/Basic/Outputs/HugeImage.swift | 18 ++ Sources/Basic/Outputs/RenderView.swift | 18 ++ Sources/Basic/Outputs/Renderable.swift | 158 ++++++++++++++++++ Sources/Basic/Setup/Error.swift | 13 +- Sources/Basic/Setup/Typealias.swift | 20 +-- Sources/CoreImage/CIColorCube.swift | 9 +- Sources/CoreImage/CINoiseReduction.swift | 27 +++ Sources/CoreImage/CIPhotoEffect.swift | 68 ++++++++ 25 files changed, 404 insertions(+), 62 deletions(-) create mode 100644 Sources/Basic/Outputs/HugeImage.swift create mode 100644 Sources/Basic/Outputs/RenderView.swift create mode 100644 Sources/Basic/Outputs/Renderable.swift create mode 100644 Sources/CoreImage/CINoiseReduction.swift create mode 100644 Sources/CoreImage/CIPhotoEffect.swift diff --git a/Demo/Harbeth-SwiftUI-Demo/Views/CustomViews.swift b/Demo/Harbeth-SwiftUI-Demo/Views/CustomViews.swift index 4ff40101..d0a8c869 100644 --- a/Demo/Harbeth-SwiftUI-Demo/Views/CustomViews.swift +++ b/Demo/Harbeth-SwiftUI-Demo/Views/CustomViews.swift @@ -27,7 +27,7 @@ struct CustomViews: View { var body: some View { VStack { - FilterableView(image: inputImage, filters: [filtering(value)], content: { image in + HarbethView(image: inputImage, filters: [filtering(value)], content: { image in image.resizable() .aspectRatio(contentMode: .fit) .frame(idealHeight: R.width-30 / 2 * 3) diff --git a/Demo/Harbeth-iOS-Demo/Modules/UnitTestViewController.swift b/Demo/Harbeth-iOS-Demo/Modules/UnitTestViewController.swift index c2bd4129..720ee3b3 100644 --- a/Demo/Harbeth-iOS-Demo/Modules/UnitTestViewController.swift +++ b/Demo/Harbeth-iOS-Demo/Modules/UnitTestViewController.swift @@ -12,13 +12,14 @@ class UnitTestViewController: UIViewController { let originImage = R.image("AX") - lazy var ImageView: UIImageView = { - let imageView = UIImageView(image: originImage) + lazy var renderView: RenderView = { + let view = RenderView.init(image: originImage) + view.keepAroundForSynchronousRender = false //imageView.contentMode = .scaleAspectFit - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.layer.borderColor = UIColor.background2?.cgColor - imageView.layer.borderWidth = 0.5 - return imageView + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.borderColor = UIColor.background2?.cgColor + view.layer.borderWidth = 0.5 + return view }() lazy var leftBarButton: UIBarButtonItem = { @@ -51,13 +52,13 @@ class UnitTestViewController: UIViewController { title = "Unit" navigationItem.rightBarButtonItem = leftBarButton view.backgroundColor = UIColor.background - view.addSubview(ImageView) + view.addSubview(renderView) NSLayoutConstraint.activate([ - ImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -20), - ImageView.heightAnchor.constraint(equalTo: ImageView.widthAnchor, multiplier: 1.0), - //ImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100), - ImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 15), - ImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -15), + renderView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -20), + renderView.heightAnchor.constraint(equalTo: renderView.widthAnchor, multiplier: 1.0), + //renderView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100), + renderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 15), + renderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -15), ]) } @@ -78,7 +79,8 @@ extension UnitTestViewController { func unitTest() { let filter = C7Storyboard(ranks: 2) - let dest = BoxxIO.init(element: originImage, filters: [filter]) - ImageView.image = try? dest.output() + //let dest = BoxxIO.init(element: originImage, filters: [filter]) + //renderView.image = try? dest.output() + renderView.filters = [filter] } } diff --git a/Demo/Harbeth-macOS-Demo/Base.lproj/Main.storyboard b/Demo/Harbeth-macOS-Demo/Base.lproj/Main.storyboard index 6da2a1bb..3a1c840e 100644 --- a/Demo/Harbeth-macOS-Demo/Base.lproj/Main.storyboard +++ b/Demo/Harbeth-macOS-Demo/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + diff --git a/Harbeth.podspec b/Harbeth.podspec index a7821c74..05f61bcd 100644 --- a/Harbeth.podspec +++ b/Harbeth.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'Harbeth' - s.version = '1.1.2' + s.version = '1.1.3' s.summary = 'About image and video add filter for metal.' # This description is used to generate tags and improve search results. diff --git a/Harbeth.xcodeproj/project.pbxproj b/Harbeth.xcodeproj/project.pbxproj index fe64034f..57a09063 100644 --- a/Harbeth.xcodeproj/project.pbxproj +++ b/Harbeth.xcodeproj/project.pbxproj @@ -245,6 +245,7 @@ 875EEBAE2994E0C300EC62B3 /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 875EEBAD2994E0C300EC62B3 /* MetalKit.framework */; }; 875EEBB02994E0D800EC62B3 /* CoreImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 875EEBAF2994E0D700EC62B3 /* CoreImage.framework */; }; 875EEBB22994E0DF00EC62B3 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 875EEBB12994E0DF00EC62B3 /* ImageIO.framework */; }; + 87660ADA2BB25A500065717A /* Renderable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87660AD92BB25A500065717A /* Renderable.swift */; }; 87671B7E2A8DAB7800C0A5F0 /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87671B7D2A8DAB7800C0A5F0 /* Files.swift */; }; 87671B812A8E01B000C0A5F0 /* C7CircleBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87671B802A8E01B000C0A5F0 /* C7CircleBlur.swift */; }; 87671B832A8E01B800C0A5F0 /* C7CircleBlur.metal in Sources */ = {isa = PBXBuildFile; fileRef = 87671B822A8E01B800C0A5F0 /* C7CircleBlur.metal */; }; @@ -257,12 +258,16 @@ 87901EF12A726588008FCB47 /* C7ColorGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87901EF02A726588008FCB47 /* C7ColorGradient.swift */; }; 87901EF32A7265A1008FCB47 /* C7ColorGradient.metal in Sources */ = {isa = PBXBuildFile; fileRef = 87901EF22A7265A1008FCB47 /* C7ColorGradient.metal */; }; 8795CE1A2B96C4C50094BE4F /* CIColorMonochrome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8795CE192B96C4C50094BE4F /* CIColorMonochrome.swift */; }; + 87A249E92BAA75B900AF7D1A /* HugeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A249E82BAA75B900AF7D1A /* HugeImage.swift */; }; 87C72F8D29AF3F0900C4C2E6 /* README_CN.md in Resources */ = {isa = PBXBuildFile; fileRef = 87C72F8B29AF3F0900C4C2E6 /* README_CN.md */; }; 87C72F8E29AF3F0900C4C2E6 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 87C72F8C29AF3F0900C4C2E6 /* README.md */; }; 87C72F9029AF3F1800C4C2E6 /* Harbeth.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 87C72F8F29AF3F1800C4C2E6 /* Harbeth.podspec */; }; 87C874A12A7BAA2A00EC8093 /* MPSHistogram.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C874A02A7BAA2A00EC8093 /* MPSHistogram.swift */; }; 87CFF2652A81CD1700330A0B /* C7CombinationBeautiful.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CFF2642A81CD1700330A0B /* C7CombinationBeautiful.swift */; }; 87CFF2682A81D2B100330A0B /* C7CombinationBeautiful.metal in Sources */ = {isa = PBXBuildFile; fileRef = 87CFF2672A81D2B100330A0B /* C7CombinationBeautiful.metal */; }; + 87DBC3102BB6980900B55909 /* CIPhotoEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DBC30F2BB6980900B55909 /* CIPhotoEffect.swift */; }; + 87DC84722BB10D2A0071D816 /* CINoiseReduction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DC84712BB10D2A0071D816 /* CINoiseReduction.swift */; }; + 87DC84742BB110E40071D816 /* RenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DC84732BB110E40071D816 /* RenderView.swift */; }; 87F038C92A8322E700F67638 /* MTLSize+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F038C82A8322E700F67638 /* MTLSize+Ext.swift */; }; 87F038CB2A833C8400F67638 /* C7CombinationBilateralBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F038CA2A833C8400F67638 /* C7CombinationBilateralBlur.swift */; }; 87F038D02A835E7D00F67638 /* C7BilateralBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F038CE2A835E7D00F67638 /* C7BilateralBlur.swift */; }; @@ -524,6 +529,7 @@ 875EEBAD2994E0C300EC62B3 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.0.sdk/System/iOSSupport/System/Library/Frameworks/MetalKit.framework; sourceTree = DEVELOPER_DIR; }; 875EEBAF2994E0D700EC62B3 /* CoreImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreImage.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.0.sdk/System/Library/Frameworks/CoreImage.framework; sourceTree = DEVELOPER_DIR; }; 875EEBB12994E0DF00EC62B3 /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.0.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; }; + 87660AD92BB25A500065717A /* Renderable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Renderable.swift; sourceTree = ""; }; 87671B7D2A8DAB7800C0A5F0 /* Files.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Files.swift; sourceTree = ""; }; 87671B802A8E01B000C0A5F0 /* C7CircleBlur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = C7CircleBlur.swift; sourceTree = ""; }; 87671B822A8E01B800C0A5F0 /* C7CircleBlur.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = C7CircleBlur.metal; sourceTree = ""; }; @@ -536,12 +542,16 @@ 87901EF02A726588008FCB47 /* C7ColorGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = C7ColorGradient.swift; sourceTree = ""; }; 87901EF22A7265A1008FCB47 /* C7ColorGradient.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = C7ColorGradient.metal; sourceTree = ""; }; 8795CE192B96C4C50094BE4F /* CIColorMonochrome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIColorMonochrome.swift; sourceTree = ""; }; + 87A249E82BAA75B900AF7D1A /* HugeImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HugeImage.swift; sourceTree = ""; }; 87C72F8B29AF3F0900C4C2E6 /* README_CN.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README_CN.md; sourceTree = ""; }; 87C72F8C29AF3F0900C4C2E6 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 87C72F8F29AF3F1800C4C2E6 /* Harbeth.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Harbeth.podspec; sourceTree = ""; }; 87C874A02A7BAA2A00EC8093 /* MPSHistogram.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPSHistogram.swift; sourceTree = ""; }; 87CFF2642A81CD1700330A0B /* C7CombinationBeautiful.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = C7CombinationBeautiful.swift; sourceTree = ""; }; 87CFF2672A81D2B100330A0B /* C7CombinationBeautiful.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = C7CombinationBeautiful.metal; sourceTree = ""; }; + 87DBC30F2BB6980900B55909 /* CIPhotoEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIPhotoEffect.swift; sourceTree = ""; }; + 87DC84712BB10D2A0071D816 /* CINoiseReduction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CINoiseReduction.swift; sourceTree = ""; }; + 87DC84732BB110E40071D816 /* RenderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderView.swift; sourceTree = ""; }; 87F038C82A8322E700F67638 /* MTLSize+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLSize+Ext.swift"; sourceTree = ""; }; 87F038CA2A833C8400F67638 /* C7CombinationBilateralBlur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = C7CombinationBilateralBlur.swift; sourceTree = ""; }; 87F038CE2A835E7D00F67638 /* C7BilateralBlur.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = C7BilateralBlur.swift; sourceTree = ""; }; @@ -689,6 +699,8 @@ F7621A922A7FA45400614542 /* CIGaussianBlur.swift */, 87265FBA2994D82100E612C7 /* CIHighlight.swift */, F7621AAA2A7FE13800614542 /* CILookupTable.swift */, + 87DC84712BB10D2A0071D816 /* CINoiseReduction.swift */, + 87DBC30F2BB6980900B55909 /* CIPhotoEffect.swift */, F7621A962A7FABAD00614542 /* CISaturation.swift */, F7621A982A7FABF300614542 /* CIShadows.swift */, F7621A9A2A7FAC7000614542 /* CISharpen.swift */, @@ -784,8 +796,11 @@ children = ( 87265FF82994D82100E612C7 /* BoxxIO.swift */, 87265FF52994D82100E612C7 /* Destype.swift */, + 87A249E82BAA75B900AF7D1A /* HugeImage.swift */, 87265FC62994D82100E612C7 /* Operators.swift */, 87265FF62994D82100E612C7 /* Outputable.swift */, + 87660AD92BB25A500065717A /* Renderable.swift */, + 87DC84732BB110E40071D816 /* RenderView.swift */, ); path = Outputs; sourceTree = ""; @@ -1237,6 +1252,8 @@ 872661122994D82200E612C7 /* C7Sharpen.metal in Sources */, 872660C72994D82200E612C7 /* Device.swift in Sources */, 872661612994D82200E612C7 /* C7Posterize.swift in Sources */, + 87DC84722BB10D2A0071D816 /* CINoiseReduction.swift in Sources */, + 87DC84742BB110E40071D816 /* RenderView.swift in Sources */, 872660D72994D82200E612C7 /* Matrix4x5.swift in Sources */, 87671B812A8E01B000C0A5F0 /* C7CircleBlur.swift in Sources */, 8726615E2994D82200E612C7 /* C7Luminance.metal in Sources */, @@ -1368,8 +1385,10 @@ 872660C92994D82200E612C7 /* Matrix3x3.swift in Sources */, 872660D22994D82200E612C7 /* Size.swift in Sources */, 872661192994D82200E612C7 /* C7Sobel.swift in Sources */, + 87DBC3102BB6980900B55909 /* CIPhotoEffect.swift in Sources */, 872660E02994D82200E612C7 /* Matrix4x4.swift in Sources */, 872661942994D82200E612C7 /* C7RedMonochromeBlur.metal in Sources */, + 87660ADA2BB25A500065717A /* Renderable.swift in Sources */, 872661642994D82200E612C7 /* C7Opacity.metal in Sources */, 872661572994D82200E612C7 /* C7LuminanceThreshold.swift in Sources */, 872661832994D82200E612C7 /* C7BlendLinearBurn.metal in Sources */, @@ -1385,6 +1404,7 @@ 8726619B2994D82200E612C7 /* C7ShiftGlitch.metal in Sources */, 872660CC2994D82200E612C7 /* Error.swift in Sources */, 872661682994D82200E612C7 /* C7Haze.swift in Sources */, + 87A249E92BAA75B900AF7D1A /* HugeImage.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/README.md b/README.md index 3acba532..e1b42903 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,17 @@ dest.transmitOutput(success: { [weak self] image in }) ``` +### ImageView + +- Just use the [RenderView](https://github.com/yangKJ/Harbeth/blob/master/Sources/Basic/Outputs/RenderView.swift) and set the filters. + +``` +// It's used the same way as NSImageView. +let renderView = RenderView.init(image: originImage) + +renderView.filters = [C7Storyboard(ranks: 2)] +``` + ### Camera - 📸 Camera capture generates pictures. diff --git a/Sources/Basic/Core/Device.swift b/Sources/Basic/Core/Device.swift index 06cfdfbf..f5116a0a 100644 --- a/Sources/Basic/Core/Device.swift +++ b/Sources/Basic/Core/Device.swift @@ -27,7 +27,7 @@ public final class Device: Cacheable { /// Transform using color space lazy var colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB() /// We are likely to encounter images with wider colour than sRGB - lazy var workingColorSpace: CGColorSpace? = CGColorSpace(name: CGColorSpace.extendedLinearSRGB) + lazy var workingColorSpace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB) /// CIContexts lazy var contexts = [CGColorSpace: CIContext]() @@ -50,7 +50,7 @@ public final class Device: Cacheable { self.harbethLibrary = Device.makeFrameworkLibrary(device, for: "Harbeth") if defaultLibrary == nil && harbethLibrary == nil { - C7FailedErrorInDebug("Could not load library") + HarbethError.failed("Could not load library") } } @@ -61,7 +61,7 @@ public final class Device: Cacheable { extension Device { - static func makeFrameworkLibrary(_ device: MTLDevice, for resource: String) -> MTLLibrary? { + public static func makeFrameworkLibrary(_ device: MTLDevice, for resource: String) -> MTLLibrary? { #if SWIFT_PACKAGE /// Fixed the Swift PM cannot read the `.metal` file. /// https://stackoverflow.com/questions/63237395/generating-resource-bundle-accessor-type-bundle-has-no-member-module diff --git a/Sources/Basic/Core/Filtering.swift b/Sources/Basic/Core/Filtering.swift index 24630098..ad958129 100644 --- a/Sources/Basic/Core/Filtering.swift +++ b/Sources/Basic/Core/Filtering.swift @@ -104,6 +104,12 @@ public protocol CoreImageProtocol: C7FilterProtocol { func coreImageApply(filter: CIFilter, input ciImage: CIImage) throws -> CIImage } +extension CoreImageProtocol { + public func coreImageApply(filter: CIFilter, input ciImage: CIImage) throws -> CIImage { + return ciImage + } +} + // MARK: - render filter protocol public protocol RenderProtocol: C7FilterProtocol { /// Setup the vertex shader parameters. diff --git a/Sources/Basic/Core/Rendering.swift b/Sources/Basic/Core/Rendering.swift index e43c2940..57fa4fc9 100644 --- a/Sources/Basic/Core/Rendering.swift +++ b/Sources/Basic/Core/Rendering.swift @@ -35,7 +35,7 @@ internal struct Rendering { renderPass.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 0.65, 0.8, 1) guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPass) else { - C7FailedErrorInDebug("Could not create render encoder") + HarbethError.failed("Could not create render encoder") return } let device = Device.device() diff --git a/Sources/Basic/Extensions/C7Image+Ext.swift b/Sources/Basic/Extensions/C7Image+Ext.swift index 5169aea5..a8c7ae64 100644 --- a/Sources/Basic/Extensions/C7Image+Ext.swift +++ b/Sources/Basic/Extensions/C7Image+Ext.swift @@ -216,11 +216,10 @@ extension HarbethWrapper where Base: C7Image { #if os(macOS) return base #else - let imageOrientation = base.imageOrientation - guard imageOrientation != .up, let cgImage = base.cgImage else { + guard base.imageOrientation != .up, let cgImage = base.cgImage else { return base } - return cgImage.c7.fixOrientation(from: imageOrientation).c7.drawing(refImage: base) + return cgImage.c7.drawing(refImage: base) #endif } } diff --git a/Sources/Basic/Extensions/C7Image+macOS.swift b/Sources/Basic/Extensions/C7Image+macOS.swift index 626e9962..bb42bf74 100644 --- a/Sources/Basic/Extensions/C7Image+macOS.swift +++ b/Sources/Basic/Extensions/C7Image+macOS.swift @@ -15,11 +15,25 @@ import AppKit extension C7Image { public convenience init(cgImage: CGImage) { + //self.init(cgImage: cgImage, scale: 1.0, orientation: .up) self.init(cgImage: cgImage, size: .zero) } + public convenience init(cgImage: CGImage, scale: CGFloat, orientation: C7ImageOrientation) { + let cgImage: CGImage = { + orientation != .up ? cgImage.c7.fixOrientation(from: orientation) : cgImage + }() + let imageRep = NSBitmapImageRep(cgImage: cgImage) + let scale = max(1.0, scale) + let width = CGFloat(imageRep.pixelsWide) / scale + let height = CGFloat(imageRep.pixelsHigh) / scale + self.init(cgImage: cgImage, size: .init(width: width, height: height)) + self.addRepresentation(imageRep) + } + public var cgImage: CGImage? { - return self.cgImage(forProposedRect: nil, context: nil, hints: nil) + var rect = NSRect(origin: .zero, size: self.size) + return self.cgImage(forProposedRect: &rect, context: nil, hints: nil) } public var scale: CGFloat { diff --git a/Sources/Basic/Extensions/CGImage+Ext.swift b/Sources/Basic/Extensions/CGImage+Ext.swift index 5d93f29a..d0de6630 100644 --- a/Sources/Basic/Extensions/CGImage+Ext.swift +++ b/Sources/Basic/Extensions/CGImage+Ext.swift @@ -156,13 +156,10 @@ extension HarbethWrapper where Base: CGImage { let finalImageRef = base.cropping(to: rect) return finalImageRef ?? base } -} - -#if os(iOS) || os(tvOS) || os(watchOS) -extension HarbethWrapper where Base: CGImage { - /// Fixed image rotation direction. - public func fixOrientation(from orientation: UIImage.Orientation) -> CGImage { + /// We need to calculate the proper transformation to make the image upright. + /// We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored. + public func fixTransform(from orientation: C7ImageOrientation) -> CGAffineTransform { let width = CGFloat(base.width) let height = CGFloat(base.height) var transform = CGAffineTransform.identity @@ -179,7 +176,6 @@ extension HarbethWrapper where Base: CGImage { default: break } - switch orientation { case .upMirrored, .downMirrored: transform = transform.translatedBy(x: width, y: 0) @@ -190,10 +186,17 @@ extension HarbethWrapper where Base: CGImage { default: break } - + return transform + } + + /// Fixed image rotation direction. + public func fixOrientation(from orientation: C7ImageOrientation) -> CGImage { guard let colorSpace = base.colorSpace else { return base } + let width = CGFloat(base.width) + let height = CGFloat(base.height) + let transform = base.c7.fixTransform(from: orientation) let context = CGContext(data: nil, width: Int(width), height: Int(height), @@ -208,8 +211,6 @@ extension HarbethWrapper where Base: CGImage { default: context?.draw(base, in: CGRect(x: 0, y: 0, width: width, height: height)) } - return context?.makeImage() ?? base } } -#endif diff --git a/Sources/Basic/Matrix/Matrix3x3.swift b/Sources/Basic/Matrix/Matrix3x3.swift index d6955578..8f2713c3 100644 --- a/Sources/Basic/Matrix/Matrix3x3.swift +++ b/Sources/Basic/Matrix/Matrix3x3.swift @@ -16,7 +16,7 @@ public struct Matrix3x3: Matrix { public init(values: [Float]) { if values.count != 9 { - C7FailedErrorInDebug("There must be nine values for 3x3 Matrix.") + HarbethError.failed("There must be nine values for 3x3 Matrix.") } self.values = values } diff --git a/Sources/Basic/Matrix/Matrix4x4.swift b/Sources/Basic/Matrix/Matrix4x4.swift index 55fbeca9..e6b972a0 100644 --- a/Sources/Basic/Matrix/Matrix4x4.swift +++ b/Sources/Basic/Matrix/Matrix4x4.swift @@ -16,7 +16,7 @@ public struct Matrix4x4: Matrix { public init(values: [Float]) { if values.count != 16 { - C7FailedErrorInDebug("There must be 16 values for 4x4 Matrix.") + HarbethError.failed("There must be 16 values for 4x4 Matrix.") } self.values = values } diff --git a/Sources/Basic/Matrix/Matrix4x5.swift b/Sources/Basic/Matrix/Matrix4x5.swift index 7cf727b4..1c45c096 100644 --- a/Sources/Basic/Matrix/Matrix4x5.swift +++ b/Sources/Basic/Matrix/Matrix4x5.swift @@ -15,7 +15,7 @@ public struct Matrix4x5 { public init(values: [Float]) { if values.count != 20 { - C7FailedErrorInDebug("There must be twenty values for 4x5 Matrix.") + HarbethError.failed("There must be twenty values for 4x5 Matrix.") } var matrix = [Float]() var vector = [Float]() diff --git a/Sources/Basic/Matrix/Vector3.swift b/Sources/Basic/Matrix/Vector3.swift index 11a44824..7b4bd369 100644 --- a/Sources/Basic/Matrix/Vector3.swift +++ b/Sources/Basic/Matrix/Vector3.swift @@ -17,7 +17,7 @@ public struct Vector3: Matrix { public init(values: [Float]) { if values.count != 3 { - C7FailedErrorInDebug("There must be three values for Vector3.") + HarbethError.failed("There must be three values for Vector3.") } self.values = values } diff --git a/Sources/Basic/Matrix/Vector4.swift b/Sources/Basic/Matrix/Vector4.swift index 53500a41..0f0c4d61 100644 --- a/Sources/Basic/Matrix/Vector4.swift +++ b/Sources/Basic/Matrix/Vector4.swift @@ -19,7 +19,7 @@ public struct Vector4: Matrix { public init(values: [Float]) { if values.count != 4 { - C7FailedErrorInDebug("There must be four values for Vector4.") + HarbethError.failed("There must be four values for Vector4.") } self.values = values } diff --git a/Sources/Basic/Outputs/HugeImage.swift b/Sources/Basic/Outputs/HugeImage.swift new file mode 100644 index 00000000..5dccb657 --- /dev/null +++ b/Sources/Basic/Outputs/HugeImage.swift @@ -0,0 +1,18 @@ +// +// HugeImage.swift +// Harbeth +// +// Created by Condy on 2024/3/20. +// + +import Foundation + +/// 超大图像 +public struct HugeImage { + + public let image: C7Image + + public init(image: C7Image) { + self.image = image + } +} diff --git a/Sources/Basic/Outputs/RenderView.swift b/Sources/Basic/Outputs/RenderView.swift new file mode 100644 index 00000000..3cde9e6a --- /dev/null +++ b/Sources/Basic/Outputs/RenderView.swift @@ -0,0 +1,18 @@ +// +// RenderView.swift +// Harbeth +// +// Created by Condy on 2024/3/20. +// + +import Foundation +import MetalKit + +public final class RenderView: C7ImageView, Renderable { + + public override var image: C7Image? { + didSet { + changedInputSource() + } + } +} diff --git a/Sources/Basic/Outputs/Renderable.swift b/Sources/Basic/Outputs/Renderable.swift new file mode 100644 index 00000000..00d4a3b7 --- /dev/null +++ b/Sources/Basic/Outputs/Renderable.swift @@ -0,0 +1,158 @@ +// +// Renderable.swift +// Harbeth +// +// Created by Condy on 2024/3/20. +// + +import Foundation +import ObjectiveC + +public protocol Renderable: AnyObject { + associatedtype Element + + var filters: [C7FilterProtocol] { get set } + + var keepAroundForSynchronousRender: Bool { get set } + + var inputSource: Element? { get set } + + func setupInputSource() + + func changedInputSource() + + func filtering() + + func setupOutputDest(_ dest: Element) +} + +fileprivate var C7ATRenderableSetFiltersContext: UInt8 = 0 +fileprivate var C7ATRenderableInputSourceContext: UInt8 = 0 +fileprivate var C7ATRenderableKeepAroundForSynchronousRenderContext: UInt8 = 0 + +extension Renderable { + public var filters: [C7FilterProtocol] { + get { + return synchronizedRenderable { + if let object = objc_getAssociatedObject(self, &C7ATRenderableSetFiltersContext) as? [C7FilterProtocol] { + return object + } else { + let object = [C7FilterProtocol]() + objc_setAssociatedObject(self, &C7ATRenderableSetFiltersContext, object, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return object + } + } + } + set { + synchronizedRenderable { + setupInputSource() + objc_setAssociatedObject(self, &C7ATRenderableSetFiltersContext, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + filtering() + } + } + } + + public var keepAroundForSynchronousRender: Bool { + get { + return synchronizedRenderable { + if let object = objc_getAssociatedObject(self, &C7ATRenderableKeepAroundForSynchronousRenderContext) as? Bool { + return object + } else { + objc_setAssociatedObject(self, &C7ATRenderableKeepAroundForSynchronousRenderContext, true, .OBJC_ASSOCIATION_ASSIGN) + return true + } + } + } + set { + synchronizedRenderable { + objc_setAssociatedObject(self, &C7ATRenderableKeepAroundForSynchronousRenderContext, newValue, .OBJC_ASSOCIATION_ASSIGN) + } + } + } + + public var inputSource: Element? { + get { + synchronizedRenderable { + objc_getAssociatedObject(self, &C7ATRenderableInputSourceContext) as? Element + } + } + set { + synchronizedRenderable { + objc_setAssociatedObject(self, &C7ATRenderableInputSourceContext, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + } + + public func filtering() { + guard let image = inputSource, filters.count > 0 else { + return + } + let dest = BoxxIO(element: image, filters: filters) + if self.keepAroundForSynchronousRender { + if let image_ = try? dest.output() { + self.lockedSource = true + self.setupOutputDest(image_) + self.lockedSource = false + } + } else { + dest.transmitOutput { [weak self] image_ in + DispatchQueue.main.async { + self?.lockedSource = true + self?.setupOutputDest(image_) + self?.lockedSource = false + } + } + } + } + + public func changedInputSource() { + if lockedSource { + return + } + self.setupInputSource() + self.filtering() + } +} + +fileprivate var C7ATRenderableLockedSourceContext: UInt8 = 0 + +extension Renderable { + private var lockedSource: Bool { + get { + return synchronizedRenderable { + if let object = objc_getAssociatedObject(self, &C7ATRenderableLockedSourceContext) as? Bool { + return object + } else { + objc_setAssociatedObject(self, &C7ATRenderableLockedSourceContext, false, .OBJC_ASSOCIATION_ASSIGN) + return false + } + } + } + set { + synchronizedRenderable { + objc_setAssociatedObject(self, &C7ATRenderableLockedSourceContext, newValue, .OBJC_ASSOCIATION_ASSIGN) + } + } + } + + private func synchronizedRenderable( _ action: () -> T) -> T { + objc_sync_enter(self) + let result = action() + objc_sync_exit(self) + return result + } +} + +// MARK: - image view +extension Renderable where Self: C7ImageView, Element == C7Image { + + public func setupInputSource() { + if inputSource == nil { + self.inputSource = image + } + } + + public func setupOutputDest(_ dest: C7Image) { + self.image = dest + } +} diff --git a/Sources/Basic/Setup/Error.swift b/Sources/Basic/Setup/Error.swift index 80706c3d..a31c2a79 100644 --- a/Sources/Basic/Setup/Error.swift +++ b/Sources/Basic/Setup/Error.swift @@ -45,6 +45,8 @@ extension HarbethError: CustomStringConvertible, LocalizedError { /// A textual representation of `self`, suitable for debugging. public var localizedDescription: String { switch self { + case .unknown: + return "Unknown error occurred." case .error(let error): return error.localizedDescription case .image2Texture: @@ -85,8 +87,6 @@ extension HarbethError: CustomStringConvertible, LocalizedError { return "Bitmap Data Not Found." case .image2CGImage: return "Input image transform CGImage failed." - default: - return "Unknown error occurred." } } @@ -156,6 +156,15 @@ extension HarbethError { return .error(error) } } + + // Wrong printing. + static func failed(_ message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) { + #if DEBUG + fatalError(message(), file: file, line: line) + #else + print("\(file):\(line): \(message())") + #endif + } } extension Error { diff --git a/Sources/Basic/Setup/Typealias.swift b/Sources/Basic/Setup/Typealias.swift index ff9f10ea..b76d1148 100644 --- a/Sources/Basic/Setup/Typealias.swift +++ b/Sources/Basic/Setup/Typealias.swift @@ -11,6 +11,7 @@ import Foundation @_exported import CoreImage @_exported import CoreMedia @_exported import AVFoundation +import ImageIO #if os(iOS) || os(tvOS) || os(watchOS) import UIKit @@ -18,12 +19,16 @@ public typealias C7View = UIView public typealias C7Color = UIColor public typealias C7Image = UIImage public typealias C7EdgeInsets = UIEdgeInsets +public typealias C7ImageView = UIImageView +public typealias C7ImageOrientation = UIImage.Orientation #elseif os(macOS) import AppKit public typealias C7View = NSView public typealias C7Color = NSColor public typealias C7Image = NSImage public typealias C7EdgeInsets = NSEdgeInsets +public typealias C7ImageView = NSImageView +public typealias C7ImageOrientation = CGImagePropertyOrientation #endif public typealias C7InputTextures = [MTLTexture] @@ -32,15 +37,6 @@ public typealias C7FilterImageCallback = (_ image: C7Image) -> Void typealias C7KernelFunction = String // Make sure to run on the main thread. -@inline(__always) func make_run_on_main_thread() { - assert(Thread.isMainThread) -} - -// Wrong printing. -func C7FailedErrorInDebug(_ message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) { - #if DEBUG - fatalError(message(), file: file, line: line) - #else - print("\(file):\(line): \(message())") - #endif -} +//@inline(__always) func make_run_on_main_thread() { +// assert(Thread.isMainThread) +//} diff --git a/Sources/CoreImage/CIColorCube.swift b/Sources/CoreImage/CIColorCube.swift index df750951..16db8885 100644 --- a/Sources/CoreImage/CIColorCube.swift +++ b/Sources/CoreImage/CIColorCube.swift @@ -20,10 +20,6 @@ public struct CIColorCube: C7FilterProtocol, CoreImageProtocol { } } - public static let range: ParameterRange = .init(min: 0.0, max: 1.0, value: 1.0) - - @Clamping(range.min...range.max) public var intensity: Float = range.value - public var modifier: Modifier { return .coreimage(CIName: "CIColorCubeWithColorSpace") } @@ -35,7 +31,6 @@ public struct CIColorCube: C7FilterProtocol, CoreImageProtocol { filter.setValue(cubeResource.data, forKey: "inputCubeData") filter.setValue(cubeResource.dimension, forKey: "inputCubeDimension") filter.setValue(Device.colorSpace(), forKey: "inputColorSpace") - filter.setValue(intensity, forKey: kCIInputIntensityKey) return ciImage } @@ -55,9 +50,9 @@ extension CIColorCube.Resource { /// Read Cube file resources. /// - Parameter name: File name /// - Returns: Cube resource - public static func readCubeResource(_ name: String) -> CIColorCube.Resource? { + public static func readCubeResource(_ name: String, bundle: Bundle = .main) -> CIColorCube.Resource? { let paths = ["cube", "CUBE"].compactMap { - Bundle.main.path(forResource: name, ofType: $0) + bundle.path(forResource: name, ofType: $0) } guard let path = paths.first, let contents = try? String(contentsOfFile: path), diff --git a/Sources/CoreImage/CINoiseReduction.swift b/Sources/CoreImage/CINoiseReduction.swift new file mode 100644 index 00000000..1d6f9c31 --- /dev/null +++ b/Sources/CoreImage/CINoiseReduction.swift @@ -0,0 +1,27 @@ +// +// CINoiseReduction.swift +// Harbeth +// +// Created by Condy on 2024/3/25. +// + +import Foundation +import CoreImage + +public struct CINoiseReduction: C7FilterProtocol, CoreImageProtocol { + + public var noiseLevel: Float = 0.02 + + public var modifier: Modifier { + return .coreimage(CIName: "CINoiseReduction") + } + + public func coreImageApply(filter: CIFilter, input ciImage: CIImage) throws -> CIImage { + filter.setValue(noiseLevel, forKey: "inputNoiseLevel") + return ciImage + } + + public init(noiseLevel: Float = 0.02) { + self.noiseLevel = noiseLevel + } +} diff --git a/Sources/CoreImage/CIPhotoEffect.swift b/Sources/CoreImage/CIPhotoEffect.swift new file mode 100644 index 00000000..2872957d --- /dev/null +++ b/Sources/CoreImage/CIPhotoEffect.swift @@ -0,0 +1,68 @@ +// +// CIPhotoEffect.swift +// Harbeth +// +// Created by Condy on 2024/3/20. +// + +import Foundation +import CoreImage + +public struct CIPhotoEffect: C7FilterProtocol, CoreImageProtocol { + public enum EffectType { + /// Imitate vintage photography film with exaggerated color. + case chrome + /// Imitate vintage photography film with diminished color. + case fade + /// Imitate vintage photography film with distorted colors. + case instant + /// Imitate black-and-white photography film with low contrast. + case mono + /// Imitate black-and-white photography film with exaggerated contrast. + case noir + /// Imitate vintage photography film with emphasized cool colors. + case process + /// Imitate black-and-white photography film without significantly altering contrast. + case tonal + /// Imitate vintage photography film with emphasized warm colors. + case transfer + } + + public var modifier: Modifier { + return .coreimage(CIName: effectType.kernel) + } + + private let effectType: EffectType + + public init(with type: EffectType) { + self.effectType = type + } +} + +extension CIPhotoEffect.EffectType: Hashable, Identifiable { + + public var id: String { + kernel + } + + var kernel: String { + switch self { + case .chrome: + return "CIPhotoEffectChrome" + case .fade: + return "CIPhotoEffectFade" + case .instant: + return "CIPhotoEffectInstant" + case .mono: + return "CIPhotoEffectMono" + case .noir: + return "CIPhotoEffectNoir" + case .process: + return "CIPhotoEffectProcess" + case .tonal: + return "CIPhotoEffectTonal" + case .transfer: + return "CIPhotoEffectTransfer" + } + } +}