├── .gitignore ├── .swiftpm ├── configuration │ └── Package.resolved └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Demo ├── SwiftWebPDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved └── SwiftWebPDemo │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── jiro.imageset │ │ ├── Contents.json │ │ └── jiro.jpg │ ├── ContentView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── SwiftWebPDemo.entitlements │ └── SwiftWebPDemoApp.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── WebP │ ├── CGImage+Util.swift │ ├── Info.plist │ ├── InternalRawRepresentable.swift │ ├── WebPDecoder+Platform.swift │ ├── WebPDecoder.swift │ ├── WebPDecoderConfig.swift │ ├── WebPEncoder+CGImage.swift │ ├── WebPEncoder+Platform.swift │ ├── WebPEncoder.swift │ ├── WebPEncoderConfig.swift │ ├── WebPError.swift │ └── WebPImageInspector.swift └── Tests ├── Info.plist └── WebPTests ├── ResourceAccessHelper.swift ├── Resources └── jiro.jpg ├── WebPEncoderCGImageTests.swift ├── WebPEncoderIOSTests.swift ├── WebPEncoderMacOSTests.swift └── WebPImageInspectorTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | 67 | .DS_Store 68 | 69 | Packages/ -------------------------------------------------------------------------------- /.swiftpm/configuration/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "libwebp", 6 | "repositoryURL": "https://github.com/SDWebImage/libwebp-Xcode.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "4f52fc9b29600a03de6e05af16df0d694cb44301", 10 | "version": "1.2.4" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | `WebP` adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## v0.6.0 (incomming) 7 | 8 | 9 | ## v0.5.0 10 | 11 | ### Enhanced 12 | 13 | * Bumped libwebp version to v1.2.0 or newer depending on libwebp-Xcode via SPM 14 | 15 | ### Changed 16 | 17 | * Switched the source of libwebp from git submodule to libwebp-Xcode 18 | * Demo app is updated in SwiftUI 19 | 20 | ## v0.4.0 21 | 22 | ### Enhanced 23 | 24 | * Bump embeded libwebp version to v1.1.0 (was v1.0.3) 25 | 26 | ## v0.3.0 27 | 28 | * Added `WebPDecoder.encode(RGBA cgImage: CGImage, ...)` and so on for ainame/Swift-WebP#40 29 | 30 | ## v0.2.0 31 | 32 | ### Enhanced 33 | 34 | * Make WebPImageInspector publicly exposed 35 | * Added `WebPDecoder.decode(toUIImage:, options:)` and `WebPDecoder.decode(toNSImage:, options:)` 36 | * Bump embeded libwebp version to v1.0.3 (was v1.0.0) 37 | * Add -fembed-bitcode flag to CFLAGS when compiling libwebp for iOS 38 | 39 | ## v0.1.0 40 | 41 | ### Changed 42 | 43 | * Add WebPImageInspector internally 44 | 45 | ### Bug fix 46 | 47 | * Fixed a memory issue in WebPDecoder+Platform.swift 48 | 49 | 50 | ## v0.0.10 51 | 52 | ### Changed 53 | 54 | * Support swift-tools-version 5.0 to build with swift package manager 55 | 56 | ## v0.0.9 57 | 58 | ### Changed 59 | 60 | * Support Xcode 10.2's build and Swift 5 61 | 62 | ## v0.0.8 63 | 64 | ### Bug fix 65 | 66 | Fixed wrong file paths of WebPDecoder 67 | 68 | ## v0.0.7 69 | 70 | ### Changed 71 | 72 | * Added WebPDecoder 73 | 74 | ### Removed 75 | 76 | * WebPSimple.decode 77 | 78 | ## v0.0.7 79 | 80 | ### Changed 81 | 82 | Support Xcode10 and Swift4.2 (nothing changed at all) 83 | 84 | ## v0.0.5 85 | 86 | ### Changed 87 | 88 | * Update libwebp v0.60 -> v1.0.0 89 | * Now WebPEncoder supports iOS platform 90 | 91 | ### Bug fix 92 | 93 | * Handle use_argb flag properly 94 | 95 | ### Removed 96 | 97 | * WebPSimple.encode 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributors are welcome. Please use issues and pull requests to contribute to the project. And update [CHANGELOG.md](CHANGELOG.md) when committing. 4 | 5 | ## Making a Change 6 | 7 | When you commit a change, please add a note to [CHANGELOG.md](CHANGELOG.md). 8 | 9 | ## Release Process 10 | 11 | 1. Confirm the build is [passing in travis](https://travis-ci.org/awesome_octocat/WebP) 12 | 1. This automatically checks the Podfile is building 13 | 2. Push a release commit 14 | 1. Create a new Master section at the top 15 | 2. Rename the old Master section like: 16 | ## [1.0.5](https://github.com/awesome_octocat/WebP/releases/tag/1.0.5) 17 | Released on 2016-02-14. 18 | 3. Update the Podspec version number 19 | 3. Create a GitHub release 20 | 1. Tag the release (like `1.0.5`) 21 | 2. Paste notes from [CHANGELOG.md](CHANGELOG.md) 22 | 3. Push the Podspec to CocoaPods 23 | 1. `pod trunk push` 24 | 4. Create Carthage binaries 25 | 1. `carthage build --no-skip-current` 26 | 2. `carthage archive WebP` 27 | 3. Add to the GitHub release 28 | -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 70; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FDB383D92C444EE3001F3852 /* WebP in Frameworks */ = {isa = PBXBuildFile; productRef = FDB383D82C444EE3001F3852 /* WebP */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXFileReference section */ 14 | FDB383C52C444E9B001F3852 /* SwiftWebPDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftWebPDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 15 | /* End PBXFileReference section */ 16 | 17 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 18 | FDB383C72C444E9B001F3852 /* SwiftWebPDemo */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SwiftWebPDemo; sourceTree = ""; }; 19 | /* End PBXFileSystemSynchronizedRootGroup section */ 20 | 21 | /* Begin PBXFrameworksBuildPhase section */ 22 | FDB383C22C444E9B001F3852 /* Frameworks */ = { 23 | isa = PBXFrameworksBuildPhase; 24 | buildActionMask = 2147483647; 25 | files = ( 26 | FDB383D92C444EE3001F3852 /* WebP in Frameworks */, 27 | ); 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXFrameworksBuildPhase section */ 31 | 32 | /* Begin PBXGroup section */ 33 | FDB383BC2C444E9B001F3852 = { 34 | isa = PBXGroup; 35 | children = ( 36 | FDB383C72C444E9B001F3852 /* SwiftWebPDemo */, 37 | FDB383C62C444E9B001F3852 /* Products */, 38 | ); 39 | sourceTree = ""; 40 | }; 41 | FDB383C62C444E9B001F3852 /* Products */ = { 42 | isa = PBXGroup; 43 | children = ( 44 | FDB383C52C444E9B001F3852 /* SwiftWebPDemo.app */, 45 | ); 46 | name = Products; 47 | sourceTree = ""; 48 | }; 49 | /* End PBXGroup section */ 50 | 51 | /* Begin PBXNativeTarget section */ 52 | FDB383C42C444E9B001F3852 /* SwiftWebPDemo */ = { 53 | isa = PBXNativeTarget; 54 | buildConfigurationList = FDB383D42C444E9C001F3852 /* Build configuration list for PBXNativeTarget "SwiftWebPDemo" */; 55 | buildPhases = ( 56 | FDB383C12C444E9B001F3852 /* Sources */, 57 | FDB383C22C444E9B001F3852 /* Frameworks */, 58 | FDB383C32C444E9B001F3852 /* Resources */, 59 | ); 60 | buildRules = ( 61 | ); 62 | dependencies = ( 63 | ); 64 | fileSystemSynchronizedGroups = ( 65 | FDB383C72C444E9B001F3852 /* SwiftWebPDemo */, 66 | ); 67 | name = SwiftWebPDemo; 68 | packageProductDependencies = ( 69 | FDB383D82C444EE3001F3852 /* WebP */, 70 | ); 71 | productName = SwiftWebPDemo; 72 | productReference = FDB383C52C444E9B001F3852 /* SwiftWebPDemo.app */; 73 | productType = "com.apple.product-type.application"; 74 | }; 75 | /* End PBXNativeTarget section */ 76 | 77 | /* Begin PBXProject section */ 78 | FDB383BD2C444E9B001F3852 /* Project object */ = { 79 | isa = PBXProject; 80 | attributes = { 81 | BuildIndependentTargetsInParallel = 1; 82 | LastSwiftUpdateCheck = 1600; 83 | LastUpgradeCheck = 1600; 84 | TargetAttributes = { 85 | FDB383C42C444E9B001F3852 = { 86 | CreatedOnToolsVersion = 16.0; 87 | }; 88 | }; 89 | }; 90 | buildConfigurationList = FDB383C02C444E9B001F3852 /* Build configuration list for PBXProject "SwiftWebPDemo" */; 91 | compatibilityVersion = "Xcode 15.0"; 92 | developmentRegion = en; 93 | hasScannedForEncodings = 0; 94 | knownRegions = ( 95 | en, 96 | Base, 97 | ); 98 | mainGroup = FDB383BC2C444E9B001F3852; 99 | packageReferences = ( 100 | FDB383D72C444EE3001F3852 /* XCLocalSwiftPackageReference "../../Swift-WebP" */, 101 | ); 102 | productRefGroup = FDB383C62C444E9B001F3852 /* Products */; 103 | projectDirPath = ""; 104 | projectRoot = ""; 105 | targets = ( 106 | FDB383C42C444E9B001F3852 /* SwiftWebPDemo */, 107 | ); 108 | }; 109 | /* End PBXProject section */ 110 | 111 | /* Begin PBXResourcesBuildPhase section */ 112 | FDB383C32C444E9B001F3852 /* Resources */ = { 113 | isa = PBXResourcesBuildPhase; 114 | buildActionMask = 2147483647; 115 | files = ( 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXResourcesBuildPhase section */ 120 | 121 | /* Begin PBXSourcesBuildPhase section */ 122 | FDB383C12C444E9B001F3852 /* Sources */ = { 123 | isa = PBXSourcesBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | ); 127 | runOnlyForDeploymentPostprocessing = 0; 128 | }; 129 | /* End PBXSourcesBuildPhase section */ 130 | 131 | /* Begin XCBuildConfiguration section */ 132 | FDB383D22C444E9C001F3852 /* Debug */ = { 133 | isa = XCBuildConfiguration; 134 | buildSettings = { 135 | ALWAYS_SEARCH_USER_PATHS = NO; 136 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 137 | CLANG_ANALYZER_NONNULL = YES; 138 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 139 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 140 | CLANG_ENABLE_MODULES = YES; 141 | CLANG_ENABLE_OBJC_ARC = YES; 142 | CLANG_ENABLE_OBJC_WEAK = YES; 143 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 144 | CLANG_WARN_BOOL_CONVERSION = YES; 145 | CLANG_WARN_COMMA = YES; 146 | CLANG_WARN_CONSTANT_CONVERSION = YES; 147 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 148 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 149 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 150 | CLANG_WARN_EMPTY_BODY = YES; 151 | CLANG_WARN_ENUM_CONVERSION = YES; 152 | CLANG_WARN_INFINITE_RECURSION = YES; 153 | CLANG_WARN_INT_CONVERSION = YES; 154 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 155 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 156 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 157 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 158 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 159 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 160 | CLANG_WARN_STRICT_PROTOTYPES = YES; 161 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 162 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 163 | CLANG_WARN_UNREACHABLE_CODE = YES; 164 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 165 | COPY_PHASE_STRIP = NO; 166 | DEBUG_INFORMATION_FORMAT = dwarf; 167 | ENABLE_STRICT_OBJC_MSGSEND = YES; 168 | ENABLE_TESTABILITY = YES; 169 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 170 | GCC_C_LANGUAGE_STANDARD = gnu17; 171 | GCC_DYNAMIC_NO_PIC = NO; 172 | GCC_NO_COMMON_BLOCKS = YES; 173 | GCC_OPTIMIZATION_LEVEL = 0; 174 | GCC_PREPROCESSOR_DEFINITIONS = ( 175 | "DEBUG=1", 176 | "$(inherited)", 177 | ); 178 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 179 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 180 | GCC_WARN_UNDECLARED_SELECTOR = YES; 181 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 182 | GCC_WARN_UNUSED_FUNCTION = YES; 183 | GCC_WARN_UNUSED_VARIABLE = YES; 184 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 185 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 186 | MTL_FAST_MATH = YES; 187 | ONLY_ACTIVE_ARCH = YES; 188 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 189 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 190 | }; 191 | name = Debug; 192 | }; 193 | FDB383D32C444E9C001F3852 /* Release */ = { 194 | isa = XCBuildConfiguration; 195 | buildSettings = { 196 | ALWAYS_SEARCH_USER_PATHS = NO; 197 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 198 | CLANG_ANALYZER_NONNULL = YES; 199 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 200 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 201 | CLANG_ENABLE_MODULES = YES; 202 | CLANG_ENABLE_OBJC_ARC = YES; 203 | CLANG_ENABLE_OBJC_WEAK = YES; 204 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 205 | CLANG_WARN_BOOL_CONVERSION = YES; 206 | CLANG_WARN_COMMA = YES; 207 | CLANG_WARN_CONSTANT_CONVERSION = YES; 208 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 209 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 210 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 211 | CLANG_WARN_EMPTY_BODY = YES; 212 | CLANG_WARN_ENUM_CONVERSION = YES; 213 | CLANG_WARN_INFINITE_RECURSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 217 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 219 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 220 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 221 | CLANG_WARN_STRICT_PROTOTYPES = YES; 222 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 223 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 224 | CLANG_WARN_UNREACHABLE_CODE = YES; 225 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 226 | COPY_PHASE_STRIP = NO; 227 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 228 | ENABLE_NS_ASSERTIONS = NO; 229 | ENABLE_STRICT_OBJC_MSGSEND = YES; 230 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 231 | GCC_C_LANGUAGE_STANDARD = gnu17; 232 | GCC_NO_COMMON_BLOCKS = YES; 233 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 234 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 235 | GCC_WARN_UNDECLARED_SELECTOR = YES; 236 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 237 | GCC_WARN_UNUSED_FUNCTION = YES; 238 | GCC_WARN_UNUSED_VARIABLE = YES; 239 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 240 | MTL_ENABLE_DEBUG_INFO = NO; 241 | MTL_FAST_MATH = YES; 242 | SWIFT_COMPILATION_MODE = wholemodule; 243 | }; 244 | name = Release; 245 | }; 246 | FDB383D52C444E9C001F3852 /* Debug */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 250 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 251 | CODE_SIGN_ENTITLEMENTS = SwiftWebPDemo/SwiftWebPDemo.entitlements; 252 | CODE_SIGN_STYLE = Automatic; 253 | CURRENT_PROJECT_VERSION = 1; 254 | DEVELOPMENT_ASSET_PATHS = "\"SwiftWebPDemo/Preview Content\""; 255 | DEVELOPMENT_TEAM = 2Q7H62SFG3; 256 | ENABLE_HARDENED_RUNTIME = YES; 257 | ENABLE_PREVIEWS = YES; 258 | GENERATE_INFOPLIST_FILE = YES; 259 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 260 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 261 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 262 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 263 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 264 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 265 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 266 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 267 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 268 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 269 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 270 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 271 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 272 | MACOSX_DEPLOYMENT_TARGET = 15.0; 273 | MARKETING_VERSION = 1.0; 274 | PRODUCT_BUNDLE_IDENTIFIER = ainame.tokyo.SwiftWebPDemo; 275 | PRODUCT_NAME = "$(TARGET_NAME)"; 276 | SDKROOT = auto; 277 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; 278 | SUPPORTS_MACCATALYST = NO; 279 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; 280 | SWIFT_EMIT_LOC_STRINGS = YES; 281 | SWIFT_VERSION = 5.0; 282 | TARGETED_DEVICE_FAMILY = "1,2,7"; 283 | XROS_DEPLOYMENT_TARGET = 2.0; 284 | }; 285 | name = Debug; 286 | }; 287 | FDB383D62C444E9C001F3852 /* Release */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 291 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 292 | CODE_SIGN_ENTITLEMENTS = SwiftWebPDemo/SwiftWebPDemo.entitlements; 293 | CODE_SIGN_STYLE = Automatic; 294 | CURRENT_PROJECT_VERSION = 1; 295 | DEVELOPMENT_ASSET_PATHS = "\"SwiftWebPDemo/Preview Content\""; 296 | DEVELOPMENT_TEAM = 2Q7H62SFG3; 297 | ENABLE_HARDENED_RUNTIME = YES; 298 | ENABLE_PREVIEWS = YES; 299 | GENERATE_INFOPLIST_FILE = YES; 300 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 301 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 302 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 303 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 304 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 305 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 306 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 307 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 308 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 309 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 310 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 311 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 312 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 313 | MACOSX_DEPLOYMENT_TARGET = 15.0; 314 | MARKETING_VERSION = 1.0; 315 | PRODUCT_BUNDLE_IDENTIFIER = ainame.tokyo.SwiftWebPDemo; 316 | PRODUCT_NAME = "$(TARGET_NAME)"; 317 | SDKROOT = auto; 318 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; 319 | SUPPORTS_MACCATALYST = NO; 320 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; 321 | SWIFT_EMIT_LOC_STRINGS = YES; 322 | SWIFT_VERSION = 5.0; 323 | TARGETED_DEVICE_FAMILY = "1,2,7"; 324 | XROS_DEPLOYMENT_TARGET = 2.0; 325 | }; 326 | name = Release; 327 | }; 328 | /* End XCBuildConfiguration section */ 329 | 330 | /* Begin XCConfigurationList section */ 331 | FDB383C02C444E9B001F3852 /* Build configuration list for PBXProject "SwiftWebPDemo" */ = { 332 | isa = XCConfigurationList; 333 | buildConfigurations = ( 334 | FDB383D22C444E9C001F3852 /* Debug */, 335 | FDB383D32C444E9C001F3852 /* Release */, 336 | ); 337 | defaultConfigurationIsVisible = 0; 338 | defaultConfigurationName = Release; 339 | }; 340 | FDB383D42C444E9C001F3852 /* Build configuration list for PBXNativeTarget "SwiftWebPDemo" */ = { 341 | isa = XCConfigurationList; 342 | buildConfigurations = ( 343 | FDB383D52C444E9C001F3852 /* Debug */, 344 | FDB383D62C444E9C001F3852 /* Release */, 345 | ); 346 | defaultConfigurationIsVisible = 0; 347 | defaultConfigurationName = Release; 348 | }; 349 | /* End XCConfigurationList section */ 350 | 351 | /* Begin XCLocalSwiftPackageReference section */ 352 | FDB383D72C444EE3001F3852 /* XCLocalSwiftPackageReference "../../Swift-WebP" */ = { 353 | isa = XCLocalSwiftPackageReference; 354 | relativePath = "../../Swift-WebP"; 355 | }; 356 | /* End XCLocalSwiftPackageReference section */ 357 | 358 | /* Begin XCSwiftPackageProductDependency section */ 359 | FDB383D82C444EE3001F3852 /* WebP */ = { 360 | isa = XCSwiftPackageProductDependency; 361 | productName = WebP; 362 | }; 363 | /* End XCSwiftPackageProductDependency section */ 364 | }; 365 | rootObject = FDB383BD2C444E9B001F3852 /* Project object */; 366 | } 367 | -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "f2bcc02bd306b65d02fa3c4b979176e919d032d12c12c6fce4eb6a26b57cd203", 3 | "pins" : [ 4 | { 5 | "identity" : "libwebp-xcode", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/SDWebImage/libwebp-Xcode.git", 8 | "state" : { 9 | "revision" : "b2b1d20a90b14d11f6ef4241da6b81c1d3f171e4", 10 | "version" : "1.3.2" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "scale" : "1x", 33 | "size" : "16x16" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "scale" : "2x", 38 | "size" : "16x16" 39 | }, 40 | { 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "32x32" 44 | }, 45 | { 46 | "idiom" : "mac", 47 | "scale" : "2x", 48 | "size" : "32x32" 49 | }, 50 | { 51 | "idiom" : "mac", 52 | "scale" : "1x", 53 | "size" : "128x128" 54 | }, 55 | { 56 | "idiom" : "mac", 57 | "scale" : "2x", 58 | "size" : "128x128" 59 | }, 60 | { 61 | "idiom" : "mac", 62 | "scale" : "1x", 63 | "size" : "256x256" 64 | }, 65 | { 66 | "idiom" : "mac", 67 | "scale" : "2x", 68 | "size" : "256x256" 69 | }, 70 | { 71 | "idiom" : "mac", 72 | "scale" : "1x", 73 | "size" : "512x512" 74 | }, 75 | { 76 | "idiom" : "mac", 77 | "scale" : "2x", 78 | "size" : "512x512" 79 | } 80 | ], 81 | "info" : { 82 | "author" : "xcode", 83 | "version" : 1 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo/Assets.xcassets/jiro.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "jiro.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo/Assets.xcassets/jiro.imageset/jiro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ainame/Swift-WebP/c7c0adebb374493a665136d98198b11437045fb4/Demo/SwiftWebPDemo/Assets.xcassets/jiro.imageset/jiro.jpg -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WebP 3 | import UIKit 4 | 5 | struct ContentView: View { 6 | @State var converted: UIImage? 7 | 8 | var body: some View { 9 | VStack { 10 | Button { 11 | convertImage() 12 | } label: { 13 | Text("Convert") 14 | } 15 | .buttonStyle(.borderedProminent) 16 | 17 | VStack { 18 | VStack { 19 | Text("Original Image") 20 | 21 | Image(.jiro) 22 | .resizable() 23 | .aspectRatio(contentMode: .fit) 24 | .frame(width: 350) 25 | } 26 | .padding() 27 | 28 | VStack { 29 | Text("Converted Image quality=10%") 30 | 31 | if let converted { 32 | Image(uiImage: converted) 33 | .resizable() 34 | .aspectRatio(contentMode: .fit) 35 | .frame(width: 350) 36 | } else { 37 | Color 38 | .black 39 | .opacity(0.2) 40 | .frame(width: 350, height: 200) 41 | } 42 | } 43 | .padding() 44 | } 45 | 46 | Spacer() 47 | } 48 | .containerRelativeFrame([.horizontal, .vertical]) 49 | } 50 | 51 | func convertImage() { 52 | let encoder = WebPEncoder() 53 | let decoder = WebPDecoder() 54 | let queue = DispatchQueue(label: "me.ainam.webp") 55 | let image = UIImage(named: "jiro")! 56 | 57 | queue.async { 58 | do { 59 | let data = try! encoder.encode(image, config: .preset(.picture, quality: 10)) 60 | var options = WebPDecoderOptions() 61 | options.scaledWidth = Int(image.size.width) 62 | options.scaledHeight = Int(image.size.height) 63 | let webpImage = try decoder.decode(toUImage: data, options: options) 64 | 65 | DispatchQueue.main.async { 66 | self.converted = webpImage 67 | } 68 | } catch let error { 69 | print(error) 70 | } 71 | } 72 | } 73 | } 74 | 75 | #Preview { 76 | ContentView() 77 | } 78 | -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo/SwiftWebPDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Demo/SwiftWebPDemo/SwiftWebPDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftWebPDemoApp.swift 3 | // SwiftWebPDemo 4 | // 5 | // Created by Satoshi Namai on 14/07/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SwiftWebPDemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Satoshi Namai 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 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "libwebp", 6 | "repositoryURL": "https://github.com/SDWebImage/libwebp-Xcode.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "4f52fc9b29600a03de6e05af16df0d694cb44301", 10 | "version": "1.2.4" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 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: "WebP", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v11) 11 | ], 12 | products: [ 13 | .library(name: "WebP", targets: ["WebP"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/SDWebImage/libwebp-Xcode.git", from: "1.2.0"), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "WebP", 21 | dependencies: [ 22 | .product(name: "libwebp", package: "libwebp-Xcode") 23 | ] 24 | ), 25 | .testTarget( 26 | name: "WebPTests", 27 | dependencies: ["WebP"], 28 | resources: [ 29 | .copy("Resources/jiro.jpg") 30 | ] 31 | ) 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift-WebP 2 | 3 | Swift-WebP provides libwebp APIs in Swift manner for both encoding and decoding. 4 | 5 | ### Support Versions: 6 | 7 | * libwebp: v1.2.0 8 | * iOS Deployment Target: 13.0 9 | * macOS Deployment Target: 11.0 10 | 11 | #### Features 12 | 13 | * [x] Support mutiplatform; iOS, macOS, and Linux (swift-docker) 14 | * [x] Support SPM 15 | * [x] [Advanced Encoder API](https://developers.google.com/speed/webp/docs/api#advanced_encoding_api) - WebPEncoder, WebPEncoderConfig 16 | * [x] [Advanced Decoding API](https://developers.google.com/speed/webp/docs/api#advanced_decoding_api) - WebPDecoder, WebPDecoderOptions 17 | * [x] Image inspection for WebP files - WebPImageInspector 18 | 19 | #### TODO 20 | 21 | * [ ] Progressively encoding/decoding option 22 | * [ ] Animated WebP 23 | 24 | 25 | ## Usage 26 | 27 | #### Encoding 28 | 29 | ```swift 30 | let image = UIImage(named: "demo") 31 | let encoder = WebPEncoder() 32 | let queue = DispatchQueue(label: "me.ainam.webp") 33 | 34 | // should encode in background 35 | queue.async { 36 | let data = try! encoder.encode(image, config: .preset(.picture, quality: 95)) 37 | // using webp binary data... 38 | } 39 | ``` 40 | 41 | #### Decoding 42 | 43 | ```swift 44 | let data: Data = loadWebPData() 45 | let encoder = WebPDecoder() 46 | let queue = DispatchQueue(label: "me.ainam.webp") 47 | 48 | // should decode in background 49 | queue.async { 50 | var options = WebPDecoderOptions() 51 | options.scaledWidth = Int(originalWidth / 2) 52 | options.scaledHeight = Int(originalHeight / 2) 53 | let cgImage = try! decoder.decode(data, options: options) 54 | let webpImage = UIImage(cgImage: cgImage) 55 | 56 | DispatchQueue.main.async { 57 | self.imageView.image = webpImage 58 | } 59 | } 60 | ``` 61 | 62 | 63 | ## Example 64 | 65 | Please check example project 66 | 67 | ## Installation 68 | 69 | Swift-WebP supports Swift Package Manager installation. 70 | 71 | ``` 72 | .package(url: "https://github.com/ainame/Swift-WebP.git", from: "0.5.0"), 73 | ``` 74 | 75 | 76 | ## Author 77 | 78 | ainame 79 | 80 | ## License 81 | 82 | Swift-WebP is available under the MIT license. See the LICENSE file for more info. 83 | -------------------------------------------------------------------------------- /Sources/WebP/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 | -------------------------------------------------------------------------------- /Sources/WebP/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.3.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/WebP/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/WebP/WebPDecoder+Platform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libwebp 3 | 4 | #if os(macOS) || os(iOS) 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(rawValue: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue) 19 | let colorSpace = CGColorSpaceCreateDeviceRGB() 20 | let renderingIntent = CGColorRenderingIntent.defaultIntent 21 | let bytesPerPixel = 4 22 | 23 | if let cgImage = CGImage(width: width, 24 | height: height, 25 | bitsPerComponent: 8, 26 | bitsPerPixel: 8 * bytesPerPixel, 27 | bytesPerRow: bytesPerPixel * width, 28 | space: colorSpace, 29 | bitmapInfo: bitmapInfo, 30 | provider: provider, 31 | decode: nil, 32 | shouldInterpolate: false, 33 | intent: renderingIntent) { 34 | return cgImage 35 | } 36 | 37 | throw WebPError.unexpectedError(withMessage: "Couldn't initialize CGImage") 38 | } 39 | } 40 | #endif 41 | 42 | #if os(iOS) 43 | import UIKit 44 | 45 | extension WebPDecoder { 46 | public func decode(toUImage webPData: Data, options: WebPDecoderOptions) throws -> UIImage { 47 | let cgImage: CGImage = try decode(webPData, options: options) 48 | return UIImage(cgImage: cgImage) 49 | } 50 | } 51 | #endif 52 | 53 | #if os(macOS) 54 | import AppKit 55 | 56 | extension WebPDecoder { 57 | public func decode(toNSImage webPData: Data, options: WebPDecoderOptions) throws -> NSImage { 58 | let cgImage: CGImage = try decode(webPData, options: options) 59 | return NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/WebP/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 struct WebPDecoder { 19 | public init() { 20 | } 21 | 22 | public func decode(byRGB webPData: Data, options: WebPDecoderOptions) throws -> Data { 23 | var config = makeConfig(options, .RGB) 24 | try decode(webPData, config: &config) 25 | 26 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 27 | count: config.output.u.RGBA.size, 28 | deallocator: .free) 29 | } 30 | 31 | public func decode(byRGBA webPData: Data, options: WebPDecoderOptions) throws -> Data { 32 | var config = makeConfig(options, .RGBA) 33 | try decode(webPData, config: &config) 34 | 35 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 36 | count: config.output.u.RGBA.size, 37 | deallocator: .free) 38 | } 39 | 40 | public func decode(byBGR webPData: Data, options: WebPDecoderOptions, 41 | width: Int, height: Int) throws -> Data { 42 | var config = makeConfig(options, .BGR) 43 | try decode(webPData, config: &config) 44 | 45 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 46 | count: config.output.u.RGBA.size, 47 | deallocator: .free) 48 | } 49 | 50 | public func decode(byBGRA webPData: Data, options: WebPDecoderOptions) throws -> Data { 51 | var config = makeConfig(options, .BGRA) 52 | try decode(webPData, config: &config) 53 | 54 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 55 | count: config.output.u.RGBA.size, 56 | deallocator: .free) 57 | } 58 | 59 | public func decode(byARGB webPData: Data, options: WebPDecoderOptions) throws -> Data { 60 | var config = makeConfig(options, .ARGB) 61 | try decode(webPData, config: &config) 62 | 63 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 64 | count: config.output.u.RGBA.size, 65 | deallocator: .free) 66 | } 67 | 68 | public func decode(byRGBA4444 webPData: Data, options: WebPDecoderOptions) throws -> Data { 69 | 70 | var config = makeConfig(options, .RGBA4444) 71 | try decode(webPData, config: &config) 72 | 73 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 74 | count: config.output.u.RGBA.size, 75 | deallocator: .free) 76 | } 77 | 78 | public func decode(byRGB565 webPData: Data, options: WebPDecoderOptions) throws -> Data { 79 | var config = makeConfig(options, .RGB565) 80 | try decode(webPData, config: &config) 81 | 82 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 83 | count: config.output.u.RGBA.size, 84 | deallocator: .free) 85 | } 86 | 87 | public func decode(byrgbA webPData: Data, options: WebPDecoderOptions) throws -> Data { 88 | var config = makeConfig(options, .rgbA) 89 | try decode(webPData, config: &config) 90 | 91 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 92 | count: config.output.u.RGBA.size, 93 | deallocator: .free) 94 | } 95 | 96 | public func decode(bybgrA webPData: Data, options: WebPDecoderOptions) throws -> Data { 97 | var config = makeConfig(options, .bgrA) 98 | try decode(webPData, config: &config) 99 | 100 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 101 | count: config.output.u.RGBA.size, 102 | deallocator: .free) 103 | } 104 | 105 | public func decode(byArgb webPData: Data, options: WebPDecoderOptions) throws -> Data { 106 | var config = makeConfig(options, .Argb) 107 | try decode(webPData, config: &config) 108 | 109 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 110 | count: config.output.u.RGBA.size, 111 | deallocator: .free) 112 | } 113 | 114 | public func decode(byrgbA4444 webPData: Data, options: WebPDecoderOptions) throws -> Data { 115 | var config = makeConfig(options, .rgbA4444) 116 | try decode(webPData, config: &config) 117 | 118 | return Data(bytesNoCopy: config.output.u.RGBA.rgba, 119 | count: config.output.u.RGBA.size, 120 | deallocator: .free) 121 | } 122 | 123 | public func decode(byYUV webPData: Data, options: WebPDecoderOptions) throws -> Data { 124 | fatalError("didn't implement this yet") 125 | } 126 | 127 | public func decode(byYUVA webPData: Data, options: WebPDecoderOptions) throws -> Data { 128 | fatalError("didn't implement this yet") 129 | } 130 | 131 | private func decode(_ webPData: Data, config: inout WebPDecoderConfig) throws { 132 | var mutableWebPData = webPData 133 | var rawConfig: libwebp.WebPDecoderConfig = config.rawValue 134 | 135 | try mutableWebPData.withUnsafeMutableBytes { rawPtr in 136 | 137 | guard let bindedBasePtr = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { 138 | throw WebPDecodingError.unknownError 139 | } 140 | 141 | let status = WebPDecode(bindedBasePtr, webPData.count, &rawConfig) 142 | if status != VP8_STATUS_OK { 143 | throw WebPDecodingError(rawValue: status.rawValue)! 144 | } 145 | } 146 | 147 | switch config.output.u { 148 | case .RGBA: 149 | config.output.u = WebPDecBuffer.Colorspace.RGBA(rawConfig.output.u.RGBA) 150 | case .YUVA: 151 | config.output.u = WebPDecBuffer.Colorspace.YUVA(rawConfig.output.u.YUVA) 152 | } 153 | } 154 | 155 | private func makeConfig(_ options: WebPDecoderOptions, 156 | _ colorspace: ColorspaceMode) -> WebPDecoderConfig { 157 | var config = WebPDecoderConfig() 158 | config.options = options 159 | config.output.colorspace = colorspace 160 | return config 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Sources/WebP/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 = WebP.WebPBitstreamFeatures(rawValue: rawValue.input) 19 | self.output = WebP.WebPDecBuffer(rawValue: rawValue.output)! 20 | self.options = WebP.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 | -------------------------------------------------------------------------------- /Sources/WebP/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/WebP/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/WebP/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 CWebP.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 | try withUnsafeMutableBytes(of: &buffer) { ptr in 124 | picture.custom_ptr = ptr.baseAddress 125 | 126 | if WebPEncode(&config, &picture) == 0 { 127 | WebPPictureFree(&picture) 128 | 129 | let error = WebPEncodeStatusCode(rawValue: Int(picture.error_code.rawValue))! 130 | throw error 131 | } 132 | } 133 | 134 | WebPPictureFree(&picture) 135 | 136 | return Data(bytesNoCopy: buffer.mem, count: buffer.size, deallocator: .free) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/WebP/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/WebP/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/WebP/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 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/WebPTests/ResourceAccessHelper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ResourceAccessHelper { 4 | static func getExamplImagePath(of filename: String = "jiro.jpg") -> String { 5 | let currentFileURL = URL(fileURLWithPath: String(#file)) 6 | return currentFileURL.deletingLastPathComponent() 7 | .deletingLastPathComponent() 8 | .deletingLastPathComponent() 9 | .appendingPathComponent("iOS Example") 10 | .appendingPathComponent("Resources") 11 | .appendingPathComponent(filename, isDirectory: false) 12 | .path 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/WebPTests/Resources/jiro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ainame/Swift-WebP/c7c0adebb374493a665136d98198b11437045fb4/Tests/WebPTests/Resources/jiro.jpg -------------------------------------------------------------------------------- /Tests/WebPTests/WebPEncoderCGImageTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | #if canImport(CoreGraphics) && canImport(CoreImage) 5 | import CoreGraphics 6 | import CoreImage 7 | 8 | @testable import WebP 9 | 10 | class WebPEncoderCGImageTests: XCTestCase { 11 | func testRGBAImageFromCGImage() throws { 12 | let imagePath = ResourceAccessHelper.getExamplImagePath() 13 | 14 | guard FileManager.default.fileExists(atPath: imagePath) else { 15 | XCTFail("Image couldn't be found at \(imagePath)") 16 | return 17 | } 18 | 19 | let inputURL = URL(fileURLWithPath: imagePath) 20 | let cgSource = CGImageSourceCreateWithURL(inputURL as CFURL, nil) 21 | let inputCGImage = CGImageSourceCreateImageAtIndex(cgSource!, 0, nil) 22 | let ciImage = CIImage(cgImage: inputCGImage!) 23 | let context = CIContext() 24 | let cgImage = context.createCGImage( 25 | ciImage, 26 | from: ciImage.extent, 27 | format: CIFormat.RGBA8, 28 | colorSpace: CGColorSpace(name: CGColorSpace.extendedSRGB)! 29 | )! 30 | 31 | let encoder = WebPEncoder() 32 | let data = try encoder.encode(RGBA: cgImage, config: .preset(.photo, quality: 90)) 33 | XCTAssertTrue(data.count > 0) 34 | 35 | let decoder = WebPDecoder() 36 | var options = WebPDecoderOptions() 37 | options.scaledWidth = Int(cgImage.width) 38 | options.scaledHeight = Int(cgImage.height) 39 | options.useScaling = true 40 | let decodedImage = try decoder.decode(data, options: options) 41 | XCTAssertEqual(decodedImage.width, options.scaledWidth) 42 | XCTAssertEqual(decodedImage.height, options.scaledHeight) 43 | } 44 | } 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /Tests/WebPTests/WebPEncoderIOSTests.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import XCTest 3 | import Foundation 4 | import UIKit 5 | @testable import WebP 6 | 7 | class WebPEncoderIOSTests: XCTestCase { 8 | 9 | override func setUp() { 10 | super.setUp() 11 | } 12 | 13 | override func tearDown() { 14 | super.tearDown() 15 | } 16 | 17 | func testExample() throws { 18 | let encoder = WebPEncoder() 19 | 20 | let path = Bundle.module.url(forResource: "jiro", withExtension: "jpg")! 21 | let uiimage = UIImage(contentsOfFile: path.path)! 22 | let data = try encoder.encode(uiimage, config: .preset(.photo, quality: 100)) 23 | XCTAssertTrue(data.count > 0) 24 | 25 | let decoder = WebPDecoder() 26 | var options = WebPDecoderOptions() 27 | options.useScaling = true 28 | options.scaledWidth = Int(uiimage.size.width) 29 | options.scaledHeight = Int(uiimage.size.height) 30 | let decodedImage = try decoder.decode(data, options: options) 31 | XCTAssertEqual(decodedImage.width, options.scaledWidth) 32 | XCTAssertEqual(decodedImage.height, options.scaledHeight) 33 | } 34 | 35 | } 36 | #endif 37 | -------------------------------------------------------------------------------- /Tests/WebPTests/WebPEncoderMacOSTests.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | import XCTest 3 | import Foundation 4 | import AppKit 5 | @testable import WebP 6 | 7 | class WebPEncoderMacOSTests: XCTestCase { 8 | override func setUp() { 9 | super.setUp() 10 | // Put setup code here. This method is called before the invocation of each test method in the class. 11 | } 12 | 13 | override func tearDown() { 14 | // Put teardown code here. This method is called after the invocation of each test method in the class. 15 | super.tearDown() 16 | } 17 | 18 | func testExample() throws { 19 | let imagePath = ResourceAccessHelper.getExamplImagePath() 20 | 21 | guard FileManager.default.fileExists(atPath: imagePath) else { 22 | XCTFail("Image couldn't be found at \(imagePath)") 23 | return 24 | } 25 | 26 | let nsImage = NSImage(contentsOfFile: imagePath)! 27 | let encoder = WebPEncoder() 28 | let data = try encoder.encode(nsImage, config: .preset(.photo, quality: 10)) 29 | XCTAssertTrue(data.count > 0) 30 | 31 | let decoder = WebPDecoder() 32 | var options = WebPDecoderOptions() 33 | options.scaledWidth = Int(nsImage.size.width) 34 | options.scaledHeight = Int(nsImage.size.height) 35 | options.useScaling = true 36 | let decodedImage = try decoder.decode(data, options: options) 37 | XCTAssertEqual(decodedImage.width, options.scaledWidth) 38 | XCTAssertEqual(decodedImage.height, options.scaledHeight) 39 | } 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /Tests/WebPTests/WebPImageInspectorTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebP 3 | import XCTest 4 | 5 | class WebPImageInspectorTests: XCTestCase { 6 | enum WebPImageInspectorTestError: Error { 7 | case cantReadTestData(String) 8 | } 9 | 10 | func testInspectWebPImageHeightAndWidth() throws { 11 | let path = ResourceAccessHelper.getExamplImagePath(of: "jiro.webp") 12 | guard let data = FileManager.default.contents(atPath: path) else { 13 | throw WebPImageInspectorTestError.cantReadTestData(path) 14 | } 15 | let feature = try WebPImageInspector.inspect(data) 16 | XCTAssert(feature.width > 0) 17 | XCTAssert(feature.height > 0) 18 | XCTAssertFalse(feature.hasAlpha) 19 | XCTAssertFalse(feature.hasAnimation) 20 | } 21 | 22 | func testInspectingJpegImageThrowsError() throws { 23 | let path = ResourceAccessHelper.getExamplImagePath(of: "jiro.jpg") 24 | guard let data = FileManager.default.contents(atPath: path) else { 25 | throw WebPImageInspectorTestError.cantReadTestData(path) 26 | } 27 | XCTAssertThrowsError(try WebPImageInspector.inspect(data)) { _error in 28 | if let error = _error as? WebPError, 29 | case .unexpectedError(let _) = error { 30 | XCTAssert(true) 31 | } else { 32 | XCTFail() 33 | } 34 | } 35 | } 36 | 37 | } 38 | --------------------------------------------------------------------------------