├── .gitignore ├── Tests └── NukeWebPTests │ └── NukeWebPTests.swift ├── Sources ├── NukeWebPAdvanced │ ├── WebPError.swift │ ├── InternalRawRepresentable.swift │ ├── CGImage+Util.swift │ ├── AdvancedWebPDecoder.swift │ ├── WebPImageInspector.swift │ ├── WebPEncoder+CGImage.swift │ ├── WebPEncoder+Platform.swift │ ├── WebPDecoder+Platform.swift │ ├── WebPEncoder.swift │ ├── WebPDecoder.swift │ ├── WebPEncoderConfig.swift │ └── WebPDecoderConfig.swift ├── NukeWebP │ └── NukeWebP.swift └── NukeWebPBasic │ └── BasicWebPDecoder.swift ├── Package.resolved ├── LICENSE ├── README.md └── Package.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Tests/NukeWebPTests/NukeWebPTests.swift: -------------------------------------------------------------------------------- 1 | @testable import NukeWebP 2 | import XCTest 3 | import NukeWebPBasic 4 | import Nuke 5 | 6 | final class NukeWebPTests: XCTestCase { 7 | 8 | func testExample() throws { 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/WebPError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum WebPError: Error { 4 | case unexpectedPointerError // Something related pointer operation's error 5 | case unexpectedError(withMessage: String) // Something happened 6 | } 7 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/InternalRawRepresentable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol InternalRawRepresentable { 4 | associatedtype RawValue 5 | 6 | init?(rawValue: Self.RawValue) 7 | 8 | var rawValue: Self.RawValue { get } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/CGImage+Util.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(CoreGraphics) 4 | import CoreGraphics 5 | 6 | extension CGImage { 7 | func getBaseAddress() throws -> UnsafeMutablePointer { 8 | guard let dataProvider = dataProvider, 9 | let data = dataProvider.data else { 10 | throw WebPError.unexpectedPointerError 11 | } 12 | // This downcast always succeeds 13 | let mutableData = data as! CFMutableData 14 | return CFDataGetMutableBytePtr(mutableData) 15 | } 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "libwebp", 6 | "repositoryURL": "https://github.com/SDWebImage/libwebp-Xcode", 7 | "state": { 8 | "branch": null, 9 | "revision": "b2b1d20a90b14d11f6ef4241da6b81c1d3f171e4", 10 | "version": "1.3.2" 11 | } 12 | }, 13 | { 14 | "package": "Nuke", 15 | "repositoryURL": "https://github.com/kean/Nuke.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "1694798e876113d44f6ec6ead965d7286695981d", 19 | "version": "12.2.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/AdvancedWebPDecoder.swift: -------------------------------------------------------------------------------- 1 | import libwebp 2 | import NukeWebP 3 | import Foundation 4 | import CoreGraphics 5 | 6 | public final class AdvancedWebPDecoder: WebPDecoding, @unchecked Sendable { 7 | 8 | private lazy var decoder: WebPDecoder = WebPDecoder() 9 | private let options: WebPDecoderOptions 10 | 11 | public init(options: WebPDecoderOptions = .init()) { 12 | self.options = options 13 | } 14 | 15 | public func decode(data: Data) throws -> CGImage { 16 | return try decoder.decode(data, options: options) 17 | } 18 | 19 | public func decodei(data: Data) throws -> CGImage { 20 | return try decoder.decodei(data, options: options) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/WebPImageInspector.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libwebp 3 | 4 | public struct WebPImageInspector { 5 | 6 | public static func inspect(_ webPData: Data) throws -> WebPBitstreamFeatures { 7 | let cFeature = UnsafeMutablePointer.allocate(capacity: 1) 8 | defer { cFeature.deallocate() } 9 | 10 | let status = try webPData.withUnsafeBytes { rawPtr -> VP8StatusCode in 11 | guard let bindedBasePtr = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { 12 | throw WebPError.unexpectedPointerError 13 | } 14 | return WebPGetFeatures(bindedBasePtr, webPData.count, cFeature) 15 | } 16 | 17 | guard status == VP8_STATUS_OK else { 18 | throw WebPError.unexpectedError(withMessage: "Error VP8StatusCode=\(status.rawValue)") 19 | } 20 | 21 | guard let feature = WebPBitstreamFeatures(rawValue: cFeature.pointee) else { 22 | throw WebPError.unexpectedPointerError 23 | } 24 | 25 | return feature 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Maxim Kolesnik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NukeWebP 2 | 3 | WebP image decoder for [Nuke](https://github.com/kean/Nuke) written in swift. It support progressive WebP 4 | 5 | 6 | ## Usage 7 | 8 | For basic decoder: 9 | 10 | ```swift 11 | import NukeWebP 12 | import NukeWebPBasic 13 | 14 | WebPImageDecoder.enable(auto: BasicWebPDecoder()) 15 | 16 | ``` 17 | 18 | For advanced decoder: 19 | 20 | ```swift 21 | import NukeWebP 22 | import NukeWebPAdvanced 23 | 24 | WebPImageDecoder.enable(closure: { 25 | var options = WebPDecoderOptions() 26 | options.useThreads = true 27 | return AdvancedWebPDecoder(options: options) 28 | }) 29 | 30 | ``` 31 | 32 | ## Minimum Requirements 33 | 34 | | Swift | Xcode | iOS | macOS | tvOS | watchOS | 35 | |:-----:|:-----:|:---:|:-----:|:----:|:-------:| 36 | | 5.4 | 13.0 | 11.0 | 10.13 | 11.0 | 4.0 | 37 | 38 | ## Dependencies 39 | | [Nuke](https://github.com/kean/Nuke) | [libwebp](https://github.com/SDWebImage/libwebp-Xcode) | 40 | |:---:|:---:| 41 | | >= 12.0.0 | >=v1.2.1 | 42 | 43 | ## Author 44 | 45 | makleso6, makleso6@gmail.com 46 | 47 | ## License 48 | 49 | NukeWebP is available under the MIT license. See the LICENSE file for more info. 50 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.4 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "NukeWebP", 8 | platforms: [ 9 | .iOS(.v13), 10 | .tvOS(.v13), 11 | .macOS(.v10_15), 12 | .watchOS(.v6) 13 | ], 14 | products: [ 15 | .library( 16 | name: "NukeWebP", 17 | targets: ["NukeWebP"]), 18 | .library( 19 | name: "NukeWebPBasic", 20 | targets: ["NukeWebPBasic"]), 21 | .library( 22 | name: "NukeWebPAdvanced", 23 | targets: ["NukeWebPAdvanced"]), 24 | 25 | ], 26 | dependencies: [ 27 | .package(url: "https://github.com/kean/Nuke.git", from: "12.0.0"), 28 | .package(url: "https://github.com/SDWebImage/libwebp-Xcode", from: "1.3.2"), 29 | ], 30 | targets: [ 31 | .target( 32 | name: "NukeWebP", 33 | dependencies: [ 34 | .product(name: "Nuke", package: "Nuke"), 35 | .product(name: "libwebp", package: "libwebp-Xcode"), 36 | ]), 37 | .target( 38 | name: "NukeWebPBasic", 39 | dependencies: [ 40 | .target(name: "NukeWebP") 41 | ]), 42 | .target( 43 | name: "NukeWebPAdvanced", 44 | dependencies: [ 45 | .target(name: "NukeWebP") 46 | ]), 47 | .testTarget( 48 | name: "NukeWebPTests", 49 | dependencies: [ 50 | .target(name: "NukeWebP"), 51 | .target(name: "NukeWebPBasic"), 52 | .target(name: "NukeWebPAdvanced"), 53 | ]), 54 | ] 55 | ) 56 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/WebPEncoder+CGImage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(CoreGraphics) 4 | import CoreGraphics 5 | 6 | extension WebPEncoder { 7 | public func encode(RGB cgImage: CGImage, config: WebPEncoderConfig, resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 8 | return try encode(RGB: cgImage.getBaseAddress(), config: config, 9 | originWidth: cgImage.width, originHeight: cgImage.height, stride: cgImage.bytesPerRow) 10 | } 11 | 12 | public func encode(RGBA cgImage: CGImage, config: WebPEncoderConfig, resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 13 | return try encode(RGBA: cgImage.getBaseAddress(), config: config, 14 | originWidth: cgImage.width, originHeight: cgImage.height, stride: cgImage.bytesPerRow) 15 | } 16 | 17 | public func encode(RGBX cgImage: CGImage, config: WebPEncoderConfig, resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 18 | return try encode(RGBX: cgImage.getBaseAddress(), config: config, 19 | originWidth: cgImage.width, originHeight: cgImage.height, stride: cgImage.bytesPerRow) 20 | } 21 | 22 | public func encode(BGR cgImage: CGImage, config: WebPEncoderConfig, resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 23 | return try encode(BGR: cgImage.getBaseAddress(), config: config, 24 | originWidth: cgImage.width, originHeight: cgImage.height, stride: cgImage.bytesPerRow) 25 | } 26 | 27 | public func encode(BGRA cgImage: CGImage, config: WebPEncoderConfig, resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 28 | return try encode(BGRA: cgImage.getBaseAddress(), config: config, 29 | originWidth: cgImage.width, originHeight: cgImage.height, stride: cgImage.bytesPerRow) 30 | } 31 | 32 | public func encode(BGRX cgImage: CGImage, config: WebPEncoderConfig, resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 33 | return try encode(BGRX: cgImage.getBaseAddress(), config: config, 34 | originWidth: cgImage.width, originHeight: cgImage.height, stride: cgImage.bytesPerRow) 35 | } 36 | } 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/WebPEncoder+Platform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if os(macOS) 4 | import AppKit 5 | import CoreGraphics 6 | 7 | extension WebPEncoder { 8 | public func encode(_ image: NSImage, config: WebPEncoderConfig, width: Int = 0, height: Int = 0) throws -> Data { 9 | guard let data = image.tiffRepresentation else { 10 | throw WebPError.unexpectedError(withMessage: "Given image doesn't support TIFF representation.") 11 | } 12 | guard let bitmapData = NSBitmapImageRep(data: data)?.bitmapData else { 13 | throw WebPError.unexpectedError(withMessage: "NSBitmapImageRep couldn't interpret given image.") 14 | } 15 | 16 | let stride = Int(image.size.width) * MemoryLayout.size * 3 // RGB = 3byte 17 | let webPData = try encode(RGB: bitmapData, config: config, 18 | originWidth: Int(image.size.width), originHeight: Int(image.size.height), stride: stride, 19 | resizeWidth: width, resizeHeight: height) 20 | return webPData 21 | } 22 | } 23 | #endif 24 | 25 | #if os(iOS) 26 | import UIKit 27 | import CoreGraphics 28 | 29 | extension WebPEncoder { 30 | public func encode(_ image: UIImage, config: WebPEncoderConfig, width: Int = 0, height: Int = 0) throws -> Data { 31 | let cgImage = try convertUIImageToCGImageWithRGBA(image) 32 | let stride = cgImage.bytesPerRow 33 | let webPData = try encode(RGBA: cgImage.getBaseAddress(), config: config, 34 | originWidth: Int(image.size.width), originHeight: Int(image.size.height), stride: stride, 35 | resizeWidth: width, resizeHeight: height) 36 | return webPData 37 | } 38 | 39 | private func convertUIImageToCGImageWithRGBA(_ image: UIImage) throws -> CGImage { 40 | guard let inputCGImage = image.cgImage else { 41 | throw WebPError.unexpectedError(withMessage: "") 42 | } 43 | 44 | let colorSpace = CGColorSpaceCreateDeviceRGB() 45 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) 46 | 47 | guard let context = CGContext(data: nil, width: Int(image.size.width), height: Int(image.size.height), 48 | bitsPerComponent: 8, bytesPerRow: Int(image.size.width) * 4, 49 | space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else { 50 | throw WebPError.unexpectedError(withMessage: "Couldn't initialize CGContext") 51 | } 52 | 53 | context.draw(inputCGImage, in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) 54 | guard let cgImage = context.makeImage() else { 55 | throw WebPError.unexpectedError(withMessage: "Couldn't ") 56 | } 57 | 58 | return cgImage 59 | } 60 | } 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/NukeWebP/NukeWebP.swift: -------------------------------------------------------------------------------- 1 | import Nuke 2 | import Foundation 3 | 4 | #if !os(macOS) 5 | import UIKit.UIImage 6 | #else 7 | import AppKit.NSImage 8 | #endif 9 | 10 | #if os(watchOS) 11 | import ImageIO 12 | import CoreGraphics 13 | import WatchKit.WKInterfaceDevice 14 | #endif 15 | 16 | public protocol WebPDecoding: Sendable { 17 | func decode(data: Data) throws -> CGImage 18 | func decodei(data: Data) throws -> CGImage 19 | } 20 | 21 | private let _queue = DispatchQueue(label: "com.webp.decoder", autoreleaseFrequency: .workItem) 22 | 23 | public final class WebPImageDecoder: ImageDecoding, @unchecked Sendable { 24 | 25 | private let decoder: WebPDecoding 26 | private let context: ImageDecodingContext 27 | 28 | public init(decoder: WebPDecoding, context: ImageDecodingContext) { 29 | self.decoder = decoder 30 | self.context = context 31 | } 32 | private var defaultScale: CGFloat { 33 | #if os(iOS) || os(tvOS) 34 | return UIScreen.main.scale 35 | #elseif os(watchOS) 36 | return WKInterfaceDevice.current().screenScale 37 | #elseif os(macOS) 38 | return 1 39 | #endif 40 | } 41 | 42 | private var scale: CGFloat { 43 | context.request.userInfo[.scaleKey] as? CGFloat ?? defaultScale 44 | } 45 | 46 | public func decode(_ data: Data) throws -> ImageContainer { 47 | return try _queue.sync(execute: { 48 | let cgImage = try decoder.decode(data: data) 49 | #if !os(macOS) 50 | let image = PlatformImage(cgImage: cgImage, scale: scale, orientation: .up) 51 | #else 52 | let image = PlatformImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) 53 | #endif 54 | return ImageContainer(image: image, type: .webp, data: data) 55 | }) 56 | 57 | } 58 | 59 | public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { 60 | do { 61 | return try _queue.sync(execute: { 62 | let cgImage = try decoder.decodei(data: data) 63 | #if !os(macOS) 64 | let image = PlatformImage(cgImage: cgImage, scale: scale, orientation: .up) 65 | #else 66 | let image = PlatformImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) 67 | #endif 68 | return ImageContainer(image: image, type: .webp, data: data) 69 | }) 70 | } catch { 71 | return nil 72 | } 73 | } 74 | } 75 | 76 | // MARK: - check webp format data. 77 | extension WebPImageDecoder { 78 | 79 | public static func enable(closure: @escaping () -> WebPDecoding) { 80 | Nuke.ImageDecoderRegistry.shared.register { (context) -> ImageDecoding? in 81 | WebPImageDecoder.enable(context: context, closure: closure) 82 | } 83 | } 84 | 85 | public static func enable(auto closure: @escaping @autoclosure () -> WebPDecoding) { 86 | Nuke.ImageDecoderRegistry.shared.register { (context) -> ImageDecoding? in 87 | WebPImageDecoder.enable(context: context, closure: closure) 88 | } 89 | } 90 | public static func enable(context: ImageDecodingContext, closure: @escaping () -> WebPDecoding) -> Nuke.ImageDecoding? { 91 | /// Use native WebP decoder for decode image 92 | if #available(OSX 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) { 93 | return nil 94 | } else { 95 | let type = AssetType(context.data) 96 | if type == .webp { 97 | return WebPImageDecoder(decoder: closure(), context: context) 98 | } 99 | } 100 | return nil 101 | } 102 | } 103 | 104 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/WebPDecoder+Platform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libwebp 3 | 4 | #if os(macOS) || os(iOS) || os(watchOS) 5 | import CoreGraphics 6 | 7 | extension WebPDecoder { 8 | public func decode(_ webPData: Data, options: WebPDecoderOptions) throws -> CGImage { 9 | let feature = try WebPImageInspector.inspect(webPData) 10 | let height: Int = options.useScaling ? options.scaledHeight : feature.height 11 | let width: Int = options.useScaling ? options.scaledWidth : feature.width 12 | 13 | let decodedData: CFData = try decode(byRGBA: webPData, options: options) as CFData 14 | guard let provider = CGDataProvider(data: decodedData) else { 15 | throw WebPError.unexpectedError(withMessage: "Couldn't initialize CGDataProvider") 16 | } 17 | 18 | let bitmapInfo: CGBitmapInfo 19 | let bytesPerPixel = 4 20 | if feature.hasAlpha { 21 | bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.last.rawValue) 22 | } else { 23 | bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.noneSkipLast.rawValue) 24 | } 25 | let colorSpace = CGColorSpaceCreateDeviceRGB() 26 | 27 | if let cgImage = CGImage(width: width, 28 | height: height, 29 | bitsPerComponent: 8, 30 | bitsPerPixel: 8 * bytesPerPixel, 31 | bytesPerRow: bytesPerPixel * width, 32 | space: colorSpace, 33 | bitmapInfo: bitmapInfo, 34 | provider: provider, 35 | decode: nil, 36 | shouldInterpolate: false, 37 | intent: .defaultIntent) { 38 | return cgImage 39 | } 40 | 41 | throw WebPError.unexpectedError(withMessage: "Couldn't initialize CGImage") 42 | } 43 | 44 | public func decodei(_ webPData: Data, options: WebPDecoderOptions) throws -> CGImage { 45 | let feature = try WebPImageInspector.inspect(webPData) 46 | let height: Int = options.useScaling ? options.scaledHeight : feature.height 47 | let width: Int = options.useScaling ? options.scaledWidth : feature.width 48 | 49 | let decodedi = try decodei(byRGBA: webPData, options: options) 50 | let decodedData: CFData = decodedi.data as CFData 51 | guard let provider = CGDataProvider(data: decodedData) else { 52 | throw WebPError.unexpectedError(withMessage: "Couldn't initialize CGDataProvider") 53 | } 54 | 55 | let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue) 56 | let colorSpace = CGColorSpaceCreateDeviceRGB() 57 | let bytesPerPixel = 4 58 | let last_y = decodedi.last_y 59 | if let image = CGImage( 60 | width: Int(width), 61 | height: Int(last_y), 62 | bitsPerComponent: 8, 63 | bitsPerPixel: bytesPerPixel * 8, 64 | bytesPerRow: bytesPerPixel * Int(width), 65 | space: colorSpace, 66 | bitmapInfo: bitmapInfo, 67 | provider: provider, 68 | decode: nil, 69 | shouldInterpolate: false, 70 | intent: .defaultIntent 71 | ) { 72 | let canvasColorSpaceRef = CGColorSpaceCreateDeviceRGB() 73 | if let canvas = CGContext( 74 | data: nil, 75 | width: Int(width), 76 | height: Int(height), 77 | bitsPerComponent: 8, 78 | bytesPerRow: 0, 79 | space: canvasColorSpaceRef, 80 | bitmapInfo: bitmapInfo.rawValue 81 | ) { 82 | canvas.draw(image, 83 | in: .init(x: 0, y: Int(height) - Int(last_y), width: Int(width), height: Int(last_y))) 84 | if let newImageRef = canvas.makeImage() { 85 | return newImageRef 86 | } 87 | } 88 | } 89 | 90 | throw WebPError.unexpectedError(withMessage: "Couldn't initialize CGImage") 91 | } 92 | } 93 | #endif 94 | 95 | #if os(iOS) 96 | import UIKit 97 | 98 | extension WebPDecoder { 99 | public func decode(toUImage webPData: Data, options: WebPDecoderOptions) throws -> UIImage { 100 | let cgImage: CGImage = try decode(webPData, options: options) 101 | return UIImage(cgImage: cgImage) 102 | } 103 | 104 | public func decodei(toUImage webPData: Data, options: WebPDecoderOptions) throws -> UIImage { 105 | let cgImage: CGImage = try decodei(webPData, options: options) 106 | return UIImage(cgImage: cgImage) 107 | } 108 | } 109 | #endif 110 | 111 | #if os(macOS) 112 | import AppKit 113 | 114 | extension WebPDecoder { 115 | public func decode(toNSImage webPData: Data, options: WebPDecoderOptions) throws -> NSImage { 116 | let cgImage: CGImage = try decode(webPData, options: options) 117 | return NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) 118 | } 119 | 120 | public func decodei(toNSImage webPData: Data, options: WebPDecoderOptions) throws -> NSImage { 121 | let cgImage: CGImage = try decodei(webPData, options: options) 122 | return NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) 123 | } 124 | } 125 | #endif 126 | -------------------------------------------------------------------------------- /Sources/NukeWebPBasic/BasicWebPDecoder.swift: -------------------------------------------------------------------------------- 1 | import libwebp 2 | import NukeWebP 3 | import Foundation 4 | import CoreGraphics 5 | 6 | public enum BasicWebPDecoderError: Error { 7 | case unknownError 8 | case underlyingError(Error) 9 | } 10 | 11 | public final class BasicWebPDecoder: WebPDecoding, @unchecked Sendable { 12 | 13 | deinit { 14 | if idec != nil { 15 | WebPIDelete(idec) 16 | } 17 | } 18 | 19 | public init() { } 20 | 21 | public func decode(data: Data) throws -> CGImage { 22 | return try decodeCGImage(data: data) 23 | } 24 | 25 | public func decodei(data: Data) throws -> CGImage { 26 | return try decodeiCGImage(data: data) 27 | 28 | } 29 | 30 | private var idec: OpaquePointer? 31 | 32 | private func decodeiCGImage(data webPData: Data) throws -> CGImage { 33 | var mutableWebPData = webPData 34 | if idec == nil { 35 | idec = WebPINewRGB(MODE_rgbA, nil, 0, 0) 36 | } 37 | return try mutableWebPData.withUnsafeMutableBytes { rawPtr in 38 | guard let bindedBasePtr = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { 39 | throw BasicWebPDecoderError.unknownError 40 | } 41 | 42 | let status = WebPIUpdate(idec, bindedBasePtr, webPData.count) 43 | if status != VP8_STATUS_OK && status != VP8_STATUS_SUSPENDED { 44 | throw BasicWebPDecoderError.unknownError 45 | } 46 | var width: Int32 = 0 47 | var height: Int32 = 0 48 | var last_y: Int32 = 0 49 | var stride: Int32 = 0 50 | if let rgba = WebPIDecGetRGB(idec, &last_y, &width, &height, &stride) { 51 | 52 | if (0 < width + height && 0 < last_y && last_y <= height) { 53 | let rgbaSize = last_y * stride; 54 | 55 | let data = Data( 56 | bytesNoCopy: rgba, 57 | count: Int(rgbaSize), 58 | deallocator: .none 59 | ) 60 | 61 | guard let provider = CGDataProvider(data: data as CFData) else { 62 | throw BasicWebPDecoderError.unknownError 63 | } 64 | let colorSpaceRef = CGColorSpaceCreateDeviceRGB() 65 | let pixelLength: Int = 4 66 | 67 | let bitmapInfo: CGBitmapInfo = CGBitmapInfo(rawValue: (CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue)) 68 | 69 | if let image = CGImage( 70 | width: Int(width), 71 | height: Int(last_y), 72 | bitsPerComponent: 8, 73 | bitsPerPixel: pixelLength * 8, 74 | bytesPerRow: pixelLength * Int(width), 75 | space: colorSpaceRef, 76 | bitmapInfo: bitmapInfo, 77 | provider: provider, 78 | decode: nil, 79 | shouldInterpolate: false, 80 | intent: .defaultIntent 81 | ) { 82 | 83 | let canvasColorSpaceRef = CGColorSpaceCreateDeviceRGB() 84 | if let canvas = CGContext( 85 | data: nil, 86 | width: Int(width), 87 | height: Int(height), 88 | bitsPerComponent: 8, 89 | bytesPerRow: 0, 90 | space: canvasColorSpaceRef, 91 | bitmapInfo: bitmapInfo.rawValue 92 | ) { 93 | canvas.draw( 94 | image, 95 | in: .init( 96 | x: 0, 97 | y: Int(height) - Int(last_y), 98 | width: Int(width), 99 | height: Int(last_y) 100 | ) 101 | ) 102 | if let newImageRef = canvas.makeImage() { 103 | return newImageRef 104 | } 105 | } 106 | } 107 | } 108 | } 109 | throw BasicWebPDecoderError.unknownError 110 | } 111 | } 112 | 113 | private func decodeCGImage(data webPData: Data) throws -> CGImage { 114 | var mutableWebPData = webPData 115 | 116 | return try mutableWebPData.withUnsafeMutableBytes { rawPtr in 117 | 118 | guard let bindedBasePtr = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { 119 | throw BasicWebPDecoderError.unknownError 120 | } 121 | 122 | var features: libwebp.WebPBitstreamFeatures = .init() 123 | if WebPGetFeatures(bindedBasePtr, webPData.count, &features) != VP8_STATUS_OK { 124 | throw BasicWebPDecoderError.unknownError 125 | } 126 | 127 | var width: Int32 = 0 128 | var height: Int32 = 0 129 | let pixelLength: Int32 130 | let bitmapInfo: CGBitmapInfo 131 | 132 | let decoded: UnsafeMutablePointer 133 | if (features.has_alpha != 0) { 134 | pixelLength = 4 135 | guard let _decoded = WebPDecodeRGBA(bindedBasePtr, webPData.count, &width, &height) else { 136 | throw BasicWebPDecoderError.unknownError 137 | } 138 | decoded = _decoded 139 | bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.last.rawValue) 140 | } else { 141 | pixelLength = 3 142 | guard let _decoded = WebPDecodeRGB(bindedBasePtr, webPData.count, &width, &height) else { 143 | throw BasicWebPDecoderError.unknownError 144 | } 145 | decoded = _decoded 146 | bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.none.rawValue) 147 | } 148 | let data = Data( 149 | bytesNoCopy: decoded, 150 | count: Int(width * height * pixelLength), 151 | deallocator: .free 152 | ) 153 | 154 | guard let provider = CGDataProvider(data: data as CFData) else { 155 | throw BasicWebPDecoderError.unknownError 156 | } 157 | 158 | 159 | let colorSpaceRef = CGColorSpaceCreateDeviceRGB() 160 | if let image = CGImage( 161 | width: Int(width), 162 | height: Int(height), 163 | bitsPerComponent: 8, 164 | bitsPerPixel: Int(pixelLength) * 8, 165 | bytesPerRow: Int(pixelLength) * Int(width), 166 | space: colorSpaceRef, 167 | bitmapInfo: bitmapInfo, 168 | provider: provider, 169 | decode: nil, 170 | shouldInterpolate: false, 171 | intent: .defaultIntent 172 | ) { 173 | return image 174 | } 175 | throw BasicWebPDecoderError.unknownError 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/WebPEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libwebp 3 | 4 | // This is customised error that describes the pattern of error causes. 5 | // However, the error is unlikely to happen normally but it's still better to handle with throw-catch than fatal error. 6 | public enum WebPEncoderError: Error { 7 | case invalidParameter 8 | case versionMismatched 9 | } 10 | 11 | // This is the mapped error codes that libwebp.WebPEncode returns 12 | public enum WebPEncodeStatusCode: Int, Error { 13 | case ok = 0 14 | case outOfMemory // memory error allocating objects 15 | case bitstreamOutOfMemory // memory error while flushing bits 16 | case nullParameter // a pointer parameter is NULL 17 | case invalidConfiguration // configuration is invalid 18 | case badDimension // picture has invalid width/height 19 | case partition0Overflow // partition is bigger than 512k 20 | case partitionOverflow // partition is bigger than 16M 21 | case badWrite // error while flushing bytes 22 | case fileTooBig // file is bigger than 4G 23 | case userAbort // abort request by user 24 | case last // list terminator. always last. 25 | } 26 | 27 | public struct WebPEncoder { 28 | typealias WebPPictureImporter = (UnsafeMutablePointer, UnsafeMutablePointer, Int32) -> Int32 29 | 30 | public init() { 31 | } 32 | 33 | public func encode(RGB: UnsafeMutablePointer, config: WebPEncoderConfig, 34 | originWidth: Int, originHeight: Int, stride: Int, 35 | resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 36 | let importer: WebPPictureImporter = { picturePtr, data, stride in 37 | return WebPPictureImportRGB(picturePtr, data, stride) 38 | } 39 | return try encode(RGB, importer: importer, config: config, originWidth: originWidth, originHeight: originHeight, stride: stride) 40 | } 41 | 42 | public func encode(RGBA: UnsafeMutablePointer, config: WebPEncoderConfig, 43 | originWidth: Int, originHeight: Int, stride: Int, 44 | resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 45 | let importer: WebPPictureImporter = { picturePtr, data, stride in 46 | return WebPPictureImportRGBA(picturePtr, data, stride) 47 | } 48 | return try encode(RGBA, importer: importer, config: config, originWidth: originWidth, originHeight: originHeight, stride: stride) 49 | } 50 | 51 | public func encode(RGBX: UnsafeMutablePointer, config: WebPEncoderConfig, 52 | originWidth: Int, originHeight: Int, stride: Int, 53 | resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 54 | let importer: WebPPictureImporter = { picturePtr, data, stride in 55 | return WebPPictureImportRGBX(picturePtr, data, stride) 56 | } 57 | return try encode(RGBX, importer: importer, config: config, originWidth: originWidth, originHeight: originHeight, stride: stride) 58 | } 59 | 60 | public func encode(BGR: UnsafeMutablePointer, config: WebPEncoderConfig, 61 | originWidth: Int, originHeight: Int, stride: Int, 62 | resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 63 | let importer: WebPPictureImporter = { picturePtr, data, stride in 64 | return WebPPictureImportBGR(picturePtr, data, stride) 65 | } 66 | return try encode(BGR, importer: importer, config: config, originWidth: originWidth, originHeight: originHeight, stride: stride) 67 | } 68 | 69 | public func encode(BGRA: UnsafeMutablePointer, config: WebPEncoderConfig, 70 | originWidth: Int, originHeight: Int, stride: Int, 71 | resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 72 | let importer: WebPPictureImporter = { picturePtr, data, stride in 73 | return WebPPictureImportBGRA(picturePtr, data, stride) 74 | } 75 | return try encode(BGRA, importer: importer, config: config, originWidth: originWidth, originHeight: originHeight, stride: stride) 76 | } 77 | 78 | public func encode(BGRX: UnsafeMutablePointer, config: WebPEncoderConfig, 79 | originWidth: Int, originHeight: Int, stride: Int, 80 | resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 81 | let importer: WebPPictureImporter = { picturePtr, data, stride in 82 | return WebPPictureImportBGRX(picturePtr, data, stride) 83 | } 84 | return try encode(BGRX, importer: importer, config: config, originWidth: originWidth, originHeight: originHeight, stride: stride) 85 | } 86 | 87 | private func encode(_ dataPtr: UnsafeMutablePointer, importer: WebPPictureImporter, 88 | config: WebPEncoderConfig, originWidth: Int, originHeight: Int, stride: Int, 89 | resizeWidth: Int = 0, resizeHeight: Int = 0) throws -> Data { 90 | var config = config.rawValue 91 | if WebPValidateConfig(&config) == 0 { 92 | throw WebPEncoderError.invalidParameter 93 | } 94 | 95 | var picture = WebPPicture() 96 | if WebPPictureInit(&picture) == 0 { 97 | throw WebPEncoderError.invalidParameter 98 | } 99 | 100 | picture.use_argb = config.lossless == 0 ? 0 : 1 101 | picture.width = Int32(originWidth) 102 | picture.height = Int32(originHeight) 103 | 104 | let ok = importer(&picture, dataPtr, Int32(stride)) 105 | if ok == 0 { 106 | WebPPictureFree(&picture) 107 | throw WebPEncoderError.versionMismatched 108 | } 109 | 110 | if resizeHeight > 0 && resizeWidth > 0 { 111 | if (WebPPictureRescale(&picture, Int32(resizeWidth), Int32(resizeHeight)) == 0) { 112 | throw WebPEncodeStatusCode.outOfMemory 113 | } 114 | } 115 | 116 | var buffer = WebPMemoryWriter() 117 | WebPMemoryWriterInit(&buffer) 118 | let writeWebP: @convention(c) (UnsafePointer?, Int, UnsafePointer?) -> Int32 = { (data, size, picture) -> Int32 in 119 | return WebPMemoryWrite(data, size, picture) 120 | } 121 | picture.writer = writeWebP 122 | 123 | picture.custom_ptr = withUnsafeMutablePointer(to: &buffer, { 124 | return UnsafeMutableRawPointer($0) 125 | }) 126 | 127 | if WebPEncode(&config, &picture) == 0 { 128 | WebPPictureFree(&picture) 129 | 130 | let error = WebPEncodeStatusCode(rawValue: Int(picture.error_code.rawValue))! 131 | throw error 132 | } 133 | WebPPictureFree(&picture) 134 | 135 | return Data(bytesNoCopy: buffer.mem, count: buffer.size, deallocator: .free) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/WebPDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libwebp 3 | 4 | /// There's no definition of WebPDecodingError in libwebp. 5 | /// We map VP8StatusCode enum as WebPDecodingError instead. 6 | public enum WebPDecodingError: UInt32, Error { 7 | case ok = 0 // shouldn't be used as this is the succseed case 8 | case outOfMemory 9 | case invalidParam 10 | case bitstreamError 11 | case unsupportedFeature 12 | case suspended 13 | case userAbort 14 | case notEnoughData 15 | case unknownError = 9999 // This is an own error to deal with internal problems 16 | } 17 | 18 | public final class WebPDecoder { 19 | 20 | deinit { 21 | if idec != nil { 22 | WebPIDelete(idec) 23 | } 24 | } 25 | 26 | public init() { 27 | } 28 | 29 | public func decode(byRGB webPData: Data, options: WebPDecoderOptions) throws -> Data { 30 | var config = makeConfig(options, .RGB) 31 | try decode(webPData, config: &config) 32 | 33 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 34 | count: config.output.u.RGBA.size, 35 | deallocator: .free) 36 | } 37 | 38 | public func decode(byRGBA webPData: Data, options: WebPDecoderOptions) throws -> Data { 39 | var config = makeConfig(options, .RGBA) 40 | try decode(webPData, config: &config) 41 | 42 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 43 | count: config.output.u.RGBA.size, 44 | deallocator: .free) 45 | } 46 | 47 | public func decodei(byRGBA webPData: Data, options: WebPDecoderOptions) throws -> (data: Data, last_y: Int) { 48 | var config = makeConfig(options, .RGBA) 49 | try decodei(webPData, config: &config) 50 | if let rgba = config.output.u.RGBA.rgba { 51 | let data = Data(bytesNoCopy: rgba, 52 | count: config.output.u.RGBA.size, 53 | deallocator: .none) 54 | return (data: data, last_y: config.output.height) 55 | } else { 56 | throw WebPDecodingError.unknownError 57 | } 58 | } 59 | 60 | public func decode(byBGR webPData: Data, options: WebPDecoderOptions, 61 | width: Int, height: Int) throws -> Data { 62 | var config = makeConfig(options, .BGR) 63 | try decode(webPData, config: &config) 64 | 65 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 66 | count: config.output.u.RGBA.size, 67 | deallocator: .free) 68 | } 69 | 70 | public func decode(byBGRA webPData: Data, options: WebPDecoderOptions) throws -> Data { 71 | var config = makeConfig(options, .BGRA) 72 | try decode(webPData, config: &config) 73 | 74 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 75 | count: config.output.u.RGBA.size, 76 | deallocator: .free) 77 | } 78 | 79 | public func decode(byARGB webPData: Data, options: WebPDecoderOptions) throws -> Data { 80 | var config = makeConfig(options, .ARGB) 81 | try decode(webPData, config: &config) 82 | 83 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 84 | count: config.output.u.RGBA.size, 85 | deallocator: .free) 86 | } 87 | 88 | public func decode(byRGBA4444 webPData: Data, options: WebPDecoderOptions) throws -> Data { 89 | 90 | var config = makeConfig(options, .RGBA4444) 91 | try decode(webPData, config: &config) 92 | 93 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 94 | count: config.output.u.RGBA.size, 95 | deallocator: .free) 96 | } 97 | 98 | public func decode(byRGB565 webPData: Data, options: WebPDecoderOptions) throws -> Data { 99 | var config = makeConfig(options, .RGB565) 100 | try decode(webPData, config: &config) 101 | 102 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 103 | count: config.output.u.RGBA.size, 104 | deallocator: .free) 105 | } 106 | 107 | public func decode(byrgbA webPData: Data, options: WebPDecoderOptions) throws -> Data { 108 | var config = makeConfig(options, .rgbA) 109 | try decode(webPData, config: &config) 110 | 111 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 112 | count: config.output.u.RGBA.size, 113 | deallocator: .free) 114 | } 115 | 116 | public func decode(bybgrA webPData: Data, options: WebPDecoderOptions) throws -> Data { 117 | var config = makeConfig(options, .bgrA) 118 | try decode(webPData, config: &config) 119 | 120 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 121 | count: config.output.u.RGBA.size, 122 | deallocator: .free) 123 | } 124 | 125 | public func decode(byArgb webPData: Data, options: WebPDecoderOptions) throws -> Data { 126 | var config = makeConfig(options, .Argb) 127 | try decode(webPData, config: &config) 128 | 129 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 130 | count: config.output.u.RGBA.size, 131 | deallocator: .free) 132 | } 133 | 134 | public func decode(byrgbA4444 webPData: Data, options: WebPDecoderOptions) throws -> Data { 135 | var config = makeConfig(options, .rgbA4444) 136 | try decode(webPData, config: &config) 137 | 138 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 139 | count: config.output.u.RGBA.size, 140 | deallocator: .free) 141 | } 142 | 143 | public func decode(byYUV webPData: Data, options: WebPDecoderOptions) throws -> Data { 144 | fatalError("didn't implement this yet") 145 | } 146 | 147 | public func decode(byYUVA webPData: Data, options: WebPDecoderOptions) throws -> Data { 148 | fatalError("didn't implement this yet") 149 | } 150 | 151 | private func decode(_ webPData: Data, config: inout WebPDecoderConfig) throws { 152 | var mutableWebPData = webPData 153 | var rawConfig: libwebp.WebPDecoderConfig = config.rawValue 154 | 155 | try mutableWebPData.withUnsafeMutableBytes { rawPtr in 156 | 157 | guard let bindedBasePtr = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { 158 | throw WebPDecodingError.unknownError 159 | } 160 | 161 | let status = WebPDecode(bindedBasePtr, webPData.count, &rawConfig) 162 | if status != VP8_STATUS_OK { 163 | throw WebPDecodingError(rawValue: status.rawValue)! 164 | } 165 | } 166 | 167 | switch config.output.u { 168 | case .RGBA: 169 | config.output.u = WebPDecBuffer.Colorspace.RGBA(rawConfig.output.u.RGBA) 170 | case .YUVA: 171 | config.output.u = WebPDecBuffer.Colorspace.YUVA(rawConfig.output.u.YUVA) 172 | } 173 | } 174 | 175 | var idec: OpaquePointer? 176 | 177 | internal func decodei(_ webPData: Data, config: inout WebPDecoderConfig) throws { 178 | var mutableWebPData = webPData 179 | if idec == nil { 180 | idec = WebPINewRGB(MODE_rgbA, nil, 0, 0) 181 | } 182 | try mutableWebPData.withUnsafeMutableBytes { rawPtr in 183 | 184 | guard let bindedBasePtr = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { 185 | throw WebPDecodingError.unknownError 186 | } 187 | let status = WebPIUpdate(idec, bindedBasePtr, webPData.count) 188 | if status != VP8_STATUS_OK && status != VP8_STATUS_SUSPENDED { 189 | throw WebPDecodingError.unknownError 190 | } 191 | var width: Int32 = 0 192 | var height: Int32 = 0 193 | var last_y: Int32 = 0 194 | var stride: Int32 = 0 195 | guard let rgba = WebPIDecGetRGB(idec, &last_y, &width, &height, &stride) else { 196 | throw WebPDecodingError.unknownError 197 | } 198 | let rgbaSize = stride * last_y 199 | let buff = WebPRGBABuffer(rgba: rgba, stride: stride, size: Int(rgbaSize)) 200 | config.output.u = WebPDecBuffer.Colorspace.RGBA(buff) 201 | config.output.height = Int(last_y) 202 | config.output.width = Int(width) 203 | } 204 | } 205 | 206 | internal func makeConfig(_ options: WebPDecoderOptions, 207 | _ colorspace: ColorspaceMode) -> WebPDecoderConfig { 208 | var config = WebPDecoderConfig() 209 | config.options = options 210 | config.output.colorspace = colorspace 211 | return config 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/WebPEncoderConfig.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libwebp 3 | 4 | extension libwebp.WebPImageHint: ExpressibleByIntegerLiteral { 5 | /// Create an instance initialized to `value`. 6 | public init(integerLiteral value: Int) { 7 | switch UInt32(value) { 8 | case libwebp.WEBP_HINT_DEFAULT.rawValue: 9 | self = libwebp.WEBP_HINT_DEFAULT 10 | case libwebp.WEBP_HINT_PICTURE.rawValue: 11 | self = libwebp.WEBP_HINT_PICTURE 12 | case libwebp.WEBP_HINT_PHOTO.rawValue: 13 | self = libwebp.WEBP_HINT_PHOTO 14 | case libwebp.WEBP_HINT_GRAPH.rawValue: 15 | self = libwebp.WEBP_HINT_GRAPH 16 | default: 17 | fatalError() 18 | } 19 | } 20 | } 21 | 22 | 23 | // mapping from libwebp.WebPConfig 24 | public struct WebPEncoderConfig: InternalRawRepresentable { 25 | public enum WebPImageHint: libwebp.WebPImageHint { 26 | case `default` = 0 27 | case picture = 1 28 | case photo = 2 29 | case graph = 3 30 | } 31 | 32 | // Lossless encoding (0=lossy(default), 1=lossless). 33 | public var lossless: Int = 0 34 | 35 | // between 0 (smallest file) and 100 (biggest) 36 | public var quality: Float 37 | 38 | // quality/speed trade-off (0=fast, 6=slower-better) 39 | public var method: Int 40 | 41 | // Hint for image type (lossless only for now). 42 | public var imageHint: WebPImageHint = .default 43 | 44 | // Parameters related to lossy compression only: 45 | 46 | // if non-zero, set the desired target size in bytes. 47 | // Takes precedence over the 'compression' parameter. 48 | public var targetSize: Int = 0 49 | 50 | // if non-zero, specifies the minimal distortion to 51 | // try to achieve. Takes precedence over target_size. 52 | public var targetPSNR: Float = 0 53 | 54 | // maximum number of segments to use, in [1..4] 55 | public var segments: Int 56 | 57 | // Spatial Noise Shaping. 0=off, 100=maximum. 58 | public var snsStrength: Int 59 | 60 | // range: [0 = off .. 100 = strongest] 61 | public var filterStrength: Int 62 | 63 | // range: [0 = off .. 7 = least sharp] 64 | public var filterSharpness: Int 65 | 66 | // filtering type: 0 = simple, 1 = strong (only used 67 | // if filter_strength > 0 or autofilter > 0) 68 | public var filterType: Int 69 | 70 | // Auto adjust filter's strength [0 = off, 1 = on] 71 | public var autofilter: Int 72 | 73 | // Algorithm for encoding the alpha plane (0 = none, 74 | // 1 = compressed with WebP lossless). Default is 1. 75 | public var alphaCompression: Int = 1 76 | 77 | // Predictive filtering method for alpha plane. 78 | // 0: none, 1: fast, 2: best. Default if 1. 79 | public var alphaFiltering: Int 80 | 81 | // Between 0 (smallest size) and 100 (lossless). 82 | // Default is 100. 83 | public var alphaQuality: Int = 100 84 | 85 | // number of entropy-analysis passes (in [1..10]). 86 | public var pass: Int 87 | 88 | // if true, export the compressed picture back. 89 | // In-loop filtering is not applied. 90 | public var showCompressed: Bool 91 | 92 | // preprocessing filter: 93 | // 0=none, 1=segment-smooth, 2=pseudo-random dithering 94 | public var preprocessing: Int 95 | 96 | // log2(number of token partitions) in [0..3]. Default 97 | // is set to 0 for easier progressive decoding. 98 | public var partitions: Int = 0 99 | 100 | // quality degradation allowed to fit the 512k limit 101 | // on prediction modes coding (0: no degradation, 102 | // 100: maximum possible degradation). 103 | public var partitionLimit: Int 104 | 105 | // If true, compression parameters will be remapped 106 | // to better match the expected output size from 107 | // JPEG compression. Generally, the output size will 108 | // be similar but the degradation will be lower. 109 | public var emulateJpegSize: Bool 110 | 111 | // If non-zero, try and use multi-threaded encoding. 112 | public var threadLevel: Int 113 | 114 | // If set, reduce memory usage (but increase CPU use). 115 | public var lowMemory: Bool 116 | 117 | // Near lossless encoding [0 = max loss .. 100 = off 118 | // Int(default)]. 119 | public var nearLossless: Int = 100 120 | 121 | // if non-zero, preserve the exact RGB values under 122 | // transparent area. Otherwise, discard this invisible 123 | // RGB information for better compression. The default 124 | // value is 0. 125 | public var exact: Int 126 | 127 | public var qmin: Int = 0 128 | public var qmax: Int = 100 129 | 130 | // reserved for future lossless feature 131 | var useDeltaPalette: Bool 132 | // if needed, use sharp (and slow) RGB->YUV conversion 133 | var useSharpYUV: Bool 134 | 135 | static public func preset(_ preset: Preset, quality: Float) -> WebPEncoderConfig { 136 | let webPConfig = preset.webPConfig(quality: quality) 137 | return WebPEncoderConfig(rawValue: webPConfig)! 138 | } 139 | 140 | internal init?(rawValue: libwebp.WebPConfig) { 141 | lossless = Int(rawValue.lossless) 142 | quality = rawValue.quality 143 | method = Int(rawValue.method) 144 | imageHint = WebPImageHint(rawValue: rawValue.image_hint)! 145 | targetSize = Int(rawValue.target_size) 146 | targetPSNR = Float(rawValue.target_PSNR) 147 | segments = Int(rawValue.segments) 148 | snsStrength = Int(rawValue.sns_strength) 149 | filterStrength = Int(rawValue.filter_strength) 150 | filterSharpness = Int(rawValue.filter_sharpness) 151 | filterType = Int(rawValue.filter_type) 152 | autofilter = Int(rawValue.autofilter) 153 | alphaCompression = Int(rawValue.alpha_compression) 154 | alphaFiltering = Int(rawValue.alpha_filtering) 155 | alphaQuality = Int(rawValue.alpha_quality) 156 | pass = Int(rawValue.pass) 157 | showCompressed = rawValue.show_compressed != 0 ? true : false 158 | preprocessing = Int(rawValue.preprocessing) 159 | partitions = Int(rawValue.partitions) 160 | partitionLimit = Int(rawValue.partition_limit) 161 | emulateJpegSize = rawValue.emulate_jpeg_size != 0 ? true : false 162 | threadLevel = Int(rawValue.thread_level) 163 | lowMemory = rawValue.low_memory != 0 ? true : false 164 | nearLossless = Int(rawValue.near_lossless) 165 | exact = Int(rawValue.exact) 166 | useDeltaPalette = rawValue.use_delta_palette != 0 ? true : false 167 | useSharpYUV = rawValue.use_sharp_yuv != 0 ? true : false 168 | qmin = Int(rawValue.qmin) 169 | qmax = Int(rawValue.qmax) 170 | } 171 | 172 | internal var rawValue: libwebp.WebPConfig { 173 | let show_compressed = showCompressed ? Int32(1) : Int32(0) 174 | let emulate_jpeg_size = emulateJpegSize ? Int32(1) : Int32(0) 175 | let low_memory = lowMemory ? Int32(1) : Int32(0) 176 | let use_delta_palette = useDeltaPalette ? Int32(1) : Int32(0) 177 | let use_sharp_yuv = useSharpYUV ? Int32(1) : Int32(0) 178 | 179 | return libwebp.WebPConfig( 180 | lossless: Int32(lossless), 181 | quality: Float(quality), 182 | method: Int32(method), 183 | image_hint: imageHint.rawValue, 184 | target_size: Int32(targetSize), 185 | target_PSNR: Float(targetPSNR), 186 | segments: Int32(segments), 187 | sns_strength: Int32(snsStrength), 188 | filter_strength: Int32(filterStrength), 189 | filter_sharpness: Int32(filterSharpness), 190 | filter_type: Int32(filterType), 191 | autofilter: Int32(autofilter), 192 | alpha_compression: Int32(alphaCompression), 193 | alpha_filtering: Int32(alphaFiltering), 194 | alpha_quality: Int32(alphaQuality), 195 | pass: Int32(pass), 196 | show_compressed: show_compressed, 197 | preprocessing: Int32(preprocessing), 198 | partitions: Int32(partitions), 199 | partition_limit: Int32(partitionLimit), 200 | emulate_jpeg_size: emulate_jpeg_size, 201 | thread_level: Int32(threadLevel), 202 | low_memory: low_memory, 203 | near_lossless: Int32(nearLossless), 204 | exact: Int32(exact), 205 | use_delta_palette: Int32(use_delta_palette), 206 | use_sharp_yuv: Int32(use_sharp_yuv), 207 | qmin: Int32(qmin), 208 | qmax: Int32(qmax) 209 | ) 210 | } 211 | 212 | public enum Preset { 213 | case `default`, picture, photo, drawing, icon, text 214 | 215 | func webPConfig(quality: Float) -> libwebp.WebPConfig { 216 | var config = libwebp.WebPConfig() 217 | 218 | switch self { 219 | case .default: 220 | WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, quality) 221 | case .picture: 222 | WebPConfigPreset(&config, WEBP_PRESET_PICTURE, quality) 223 | case .photo: 224 | WebPConfigPreset(&config, WEBP_PRESET_PHOTO, quality) 225 | case .drawing: 226 | WebPConfigPreset(&config, WEBP_PRESET_DRAWING, quality) 227 | case .icon: 228 | WebPConfigPreset(&config, WEBP_PRESET_ICON, quality) 229 | case .text: 230 | WebPConfigPreset(&config, WEBP_PRESET_TEXT, quality) 231 | } 232 | 233 | return config 234 | } 235 | } 236 | 237 | } 238 | -------------------------------------------------------------------------------- /Sources/NukeWebPAdvanced/WebPDecoderConfig.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libwebp 3 | 4 | public struct WebPDecoderConfig: InternalRawRepresentable { 5 | public var input: WebPBitstreamFeatures? // Immutable bitstream features (optional) 6 | public var output: WebPDecBuffer // Output buffer (can point to external mem) 7 | public var options: WebPDecoderOptions // Decoding options 8 | 9 | public init() { 10 | var originConfig = libwebp.WebPDecoderConfig() 11 | if (libwebp.WebPInitDecoderConfig(&originConfig) == 0) { 12 | fatalError("can't init decoder config") 13 | } 14 | self.init(rawValue: originConfig)! 15 | } 16 | 17 | internal init?(rawValue: libwebp.WebPDecoderConfig) { 18 | self.input = WebPBitstreamFeatures(rawValue: rawValue.input) 19 | self.output = WebPDecBuffer(rawValue: rawValue.output)! 20 | self.options = WebPDecoderOptions(rawValue: rawValue.options)! 21 | } 22 | 23 | internal var rawValue: libwebp.WebPDecoderConfig { 24 | return libwebp.WebPDecoderConfig(input: (input?.rawValue)!, output: output.rawValue, options: options.rawValue) 25 | } 26 | } 27 | 28 | public struct WebPBitstreamFeatures: InternalRawRepresentable { 29 | public enum Format: Int { 30 | case undefined = 0 31 | case lossy 32 | case lossless 33 | } 34 | 35 | public var width: Int // Width in pixels, as read from the bitstream. 36 | 37 | public var height: Int // Height in pixels, as read from the bitstream. 38 | 39 | public var hasAlpha: Bool // True if the bitstream contains an alpha channel. 40 | 41 | public var hasAnimation: Bool // True if the bitstream is an animation. 42 | 43 | public var format: Format // 0 = undefined (/mixed), 1 = lossy, 2 = lossless 44 | 45 | public var pad: (Int, Int, Int, Int, Int) // padding for later use 46 | 47 | internal var rawValue: libwebp.WebPBitstreamFeatures { 48 | let has_alpha = hasAlpha ? 1 : 0 49 | let has_animation = hasAnimation ? 1 : 0 50 | 51 | return libwebp.WebPBitstreamFeatures( 52 | width: Int32(width), 53 | height: Int32(height), 54 | has_alpha: Int32(has_alpha), 55 | has_animation: Int32(has_animation), 56 | format: Int32(format.rawValue), 57 | pad: (UInt32(pad.0), UInt32(pad.1), UInt32(pad.2), UInt32(pad.3), UInt32(pad.4)) 58 | ) 59 | } 60 | 61 | internal init?(rawValue: libwebp.WebPBitstreamFeatures) { 62 | width = Int(rawValue.width) 63 | height = Int(rawValue.height) 64 | hasAlpha = rawValue.has_alpha != 0 65 | hasAnimation = rawValue.has_animation != 0 66 | format = Format(rawValue: Int(rawValue.format))! 67 | pad = (Int(rawValue.pad.0), Int(rawValue.pad.1), Int(rawValue.pad.2), Int(rawValue.pad.3), Int(rawValue.pad.4)) 68 | } 69 | } 70 | 71 | // Colorspaces 72 | // Note: the naming describes the byte-ordering of packed samples in memory. 73 | // For instance, MODE_BGRA relates to samples ordered as B,G,R,A,B,G,R,A,... 74 | // Non-capital names (e.g.:MODE_Argb) relates to pre-multiplied RGB channels. 75 | // RGBA-4444 and RGB-565 colorspaces are represented by following byte-order: 76 | // RGBA-4444: [r3 r2 r1 r0 g3 g2 g1 g0], [b3 b2 b1 b0 a3 a2 a1 a0], ... 77 | // RGB-565: [r4 r3 r2 r1 r0 g5 g4 g3], [g2 g1 g0 b4 b3 b2 b1 b0], ... 78 | // In the case WEBP_SWAP_16BITS_CSP is defined, the bytes are swapped for 79 | // these two modes: 80 | // RGBA-4444: [b3 b2 b1 b0 a3 a2 a1 a0], [r3 r2 r1 r0 g3 g2 g1 g0], ... 81 | // RGB-565: [g2 g1 g0 b4 b3 b2 b1 b0], [r4 r3 r2 r1 r0 g5 g4 g3], ... 82 | public enum ColorspaceMode: Int { 83 | case RGB = 0 84 | case RGBA = 1 85 | case BGR = 2 86 | case BGRA = 3 87 | case ARGB = 4 88 | case RGBA4444 = 5 89 | case RGB565 = 6 90 | 91 | // RGB-premultiplied transparent modes (alpha value is preserved) 92 | case rgbA = 7 93 | case bgrA = 8 94 | case Argb = 9 95 | case rgbA4444 = 10 96 | 97 | // YUV modes must come after RGB ones. 98 | case YUV = 11 99 | case YUVA = 12 100 | 101 | public var isPremultipliedMode: Bool { 102 | if self == .rgbA || self == .bgrA || self == .Argb || self == .rgbA4444 { 103 | return true 104 | } 105 | return false 106 | } 107 | 108 | public var isAlphaMode: Bool { 109 | if self == .RGBA || self == .BGRA || self == .ARGB || 110 | self == .RGBA4444 || self == .YUVA || isPremultipliedMode { 111 | return true 112 | } 113 | return false 114 | } 115 | 116 | public var isRGBMode: Bool { 117 | return rawValue < ColorspaceMode.YUV.rawValue; 118 | } 119 | } 120 | 121 | public struct WebPDecBuffer: InternalRawRepresentable { 122 | public enum Colorspace { 123 | case RGBA(WebPRGBABuffer) 124 | case YUVA(WebPYUVABuffer) 125 | 126 | var RGBA: WebPRGBABuffer { 127 | if case .RGBA(let buffer) = self { 128 | return buffer 129 | } 130 | fatalError("please use yuva") 131 | } 132 | 133 | var YUVA: WebPYUVABuffer { 134 | if case .YUVA(let buffer) = self { 135 | return buffer 136 | } 137 | fatalError("please use rgba") 138 | } 139 | } 140 | 141 | // Colorspace. 142 | public var colorspace: ColorspaceMode 143 | 144 | // Dimensions. 145 | public var width: Int 146 | public var height: Int 147 | 148 | // If non-zero, 'internal_memory' pointer is not 149 | // used. If value is '2' or more, the external 150 | // memory is considered 'slow' and multiple 151 | // read/write will be avoided. 152 | public var isExternalMemory: Bool 153 | 154 | public var u: Colorspace 155 | 156 | // Nameless union of buffer parameters. 157 | public var pad: (Int, Int, Int, Int) // padding for later use 158 | 159 | 160 | public var privateMemory: UnsafeMutablePointer? // Internally allocated memory (only when 161 | 162 | 163 | internal var rawValue: libwebp.WebPDecBuffer { 164 | let originU: libwebp.WebPDecBuffer.__Unnamed_union_u 165 | switch u { 166 | case .RGBA(let buffer): 167 | originU = libwebp.WebPDecBuffer.__Unnamed_union_u(RGBA: buffer) 168 | case .YUVA(let buffer): 169 | originU = libwebp.WebPDecBuffer.__Unnamed_union_u(YUVA: buffer) 170 | } 171 | // let u = colorspace.isRGBMode ? libwebp.WebPDecBuffer.__Unnamed_union_u(RGBA: u.RGBA) : libwebp.WebPDecBuffer.__Unnamed_union_u(YUVA: u.YUVA) 172 | return libwebp.WebPDecBuffer( 173 | colorspace: WEBP_CSP_MODE(rawValue: UInt32(colorspace.rawValue)), 174 | width: Int32(width), 175 | height: Int32(height), 176 | is_external_memory: Int32(isExternalMemory ? 1 : 0), 177 | u: originU, 178 | pad: (UInt32(pad.0), UInt32(pad.1), UInt32(pad.2), UInt32(pad.3)), 179 | private_memory: privateMemory 180 | ) 181 | } 182 | 183 | internal init?(rawValue: libwebp.WebPDecBuffer) { 184 | colorspace = ColorspaceMode(rawValue: Int(rawValue.colorspace.rawValue))! 185 | width = Int(rawValue.width) 186 | height = Int(rawValue.height) 187 | isExternalMemory = rawValue.is_external_memory != 0 188 | u = colorspace.isRGBMode ? Colorspace.RGBA(rawValue.u.RGBA) : Colorspace.YUVA(rawValue.u.YUVA) 189 | pad = (Int(rawValue.pad.0), Int(rawValue.pad.1), Int(rawValue.pad.2), Int(rawValue.pad.3)) 190 | privateMemory = rawValue.private_memory 191 | } 192 | } 193 | 194 | public struct WebPDecoderOptions: InternalRawRepresentable { 195 | public var bypassFiltering: Int // if true, skip the in-loop filtering 196 | 197 | public var noFancyUpsampling: Int // if true, use faster pointwise upsampler 198 | 199 | public var useCropping: Bool // if true, cropping is applied _first_ 200 | 201 | public var cropLeft: Int // top-left position for cropping. 202 | 203 | public var cropTop: Int 204 | 205 | // Will be snapped to even values. 206 | public var cropWidth: Int // dimension of the cropping area 207 | 208 | public var cropHeight: Int 209 | 210 | public var useScaling: Bool // if true, scaling is applied _afterward_ 211 | 212 | public var scaledWidth: Int // final resolution 213 | 214 | public var scaledHeight: Int 215 | 216 | public var useThreads: Bool // if true, use multi-threaded decoding 217 | 218 | public var ditheringStrength: Int // dithering strength (0=Off, 100=full) 219 | 220 | public var flip: Int // flip output vertically 221 | 222 | public var alphaDitheringStrength: Int // alpha dithering strength in [0..100] 223 | 224 | public var pad: (Int, Int, Int, Int, Int) // padding for later use 225 | 226 | internal var rawValue: libwebp.WebPDecoderOptions { 227 | let useCropping = self.useCropping ? 1 : 0 228 | let useScaling = self.useScaling ? 1 : 0 229 | let useThreads = self.useThreads ? 1 : 0 230 | 231 | return libwebp.WebPDecoderOptions( 232 | bypass_filtering: Int32(bypassFiltering), 233 | no_fancy_upsampling: Int32(noFancyUpsampling), 234 | use_cropping: Int32(useCropping), 235 | crop_left: Int32(cropLeft), 236 | crop_top: Int32(cropTop), 237 | crop_width: Int32(cropWidth), 238 | crop_height: Int32(cropHeight), 239 | use_scaling: Int32(useScaling), 240 | scaled_width: Int32(scaledWidth), 241 | scaled_height: Int32(scaledHeight), 242 | use_threads: Int32(useThreads), 243 | dithering_strength: Int32(ditheringStrength), 244 | flip: Int32(flip), 245 | alpha_dithering_strength: Int32(alphaDitheringStrength), 246 | pad: (UInt32(pad.0), UInt32(pad.1), UInt32(pad.2), UInt32(pad.3), UInt32(pad.4)) 247 | ) 248 | } 249 | 250 | internal init?(rawValue: libwebp.WebPDecoderOptions) { 251 | self.bypassFiltering = Int(rawValue.bypass_filtering) 252 | self.noFancyUpsampling = Int(rawValue.no_fancy_upsampling) 253 | self.useCropping = rawValue.use_cropping != 0 254 | self.cropLeft = Int(rawValue.crop_left) 255 | self.cropTop = Int(rawValue.crop_top) 256 | self.cropWidth = Int(rawValue.crop_width) 257 | self.cropHeight = Int(rawValue.crop_height) 258 | self.useScaling = rawValue.use_scaling != 0 259 | self.scaledWidth = Int(rawValue.scaled_width) 260 | self.scaledHeight = Int(rawValue.scaled_height) 261 | self.useThreads = rawValue.use_threads != 0 262 | self.ditheringStrength = Int(rawValue.dithering_strength) 263 | self.flip = Int(rawValue.flip) 264 | self.alphaDitheringStrength = Int(rawValue.alpha_dithering_strength) 265 | self.pad = (Int(rawValue.pad.0), Int(rawValue.pad.1), Int(rawValue.pad.2), Int(rawValue.pad.3), Int(rawValue.pad.4)) 266 | } 267 | 268 | public init() { 269 | self = WebPDecoderConfig().options 270 | } 271 | } 272 | --------------------------------------------------------------------------------