├── .github └── workflows │ ├── cocoapods-deploy.yaml │ └── unittest.yaml ├── .gitignore ├── .travis.yml ├── Cartfile.project ├── KingfisherWebP.podspec ├── KingfisherWebP.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── KingfisherWebP.xcscheme ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Image+WebP.swift ├── Info.plist ├── KingfisherWebP-ObjC │ ├── CGImage+WebP.m │ └── include │ │ └── CGImage+WebP.h ├── KingfisherWebP.h ├── WebPProcessor.swift └── WebPSerializer.swift ├── Tests ├── Info.plist ├── KingfisherWebPTests.swift └── Resources │ ├── animation.gif │ ├── animation.webp │ ├── cover.png │ ├── cover.webp │ ├── heart.png │ ├── heart.webp │ ├── kingfisher.jpg │ ├── kingfisher.webp │ ├── logo.png │ └── logo.webp └── spm_screenshot.png /.github/workflows/cocoapods-deploy.yaml: -------------------------------------------------------------------------------- 1 | name: deploy to cocoapods 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: macOS-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Deploy to cocoapods 18 | env: 19 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 20 | run: | 21 | export LIB_VERSION=$(git describe --tags $(git rev-list --tags --max-count=1)) 22 | gem install cocoapods 23 | pod setup 24 | perl -i -pe 'if (/^(\s*)def validated\?/) { $_ .= "${1} return true\n" }' "$(gem which cocoapods/validator)" 25 | pod lib lint --allow-warnings 26 | pod trunk push --allow-warnings 27 | shell: bash 28 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yaml: -------------------------------------------------------------------------------- 1 | name: unittest 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run-test: 7 | runs-on: macOS-latest 8 | strategy: 9 | matrix: 10 | destination: [ 11 | 'macOS', 12 | 'iOS Simulator,name=iPhone 14', 13 | 'tvOS Simulator,name=Apple TV 4K (3rd generation)', 14 | 'watchOS Simulator,name=Apple Watch Series 8 (45mm)' 15 | ] 16 | swift-version: [5.0] 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Run tests 20 | env: 21 | DESTINATION: platform=${{ matrix.destination }} 22 | SWIFT_VERSION: ${{ matrix.swift-version }} 23 | run: | 24 | xcodebuild clean test -project KingfisherWebP.xcodeproj -scheme KingfisherWebP -destination "${DESTINATION}" 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | Carthage 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 29 | # 30 | # Note: if you ignore the Pods directory, make sure to uncomment 31 | # `pod install` in .travis.yml 32 | # 33 | Pods/ 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | Package.pins 40 | Package.resolved 41 | 42 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 43 | # hence it is not needed unless you have added a package configuration file to your project 44 | .swiftpm 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | os: osx 3 | osx_image: xcode11.6 4 | cache: cocoapods 5 | podfile: Example/Podfile 6 | before_install: 7 | - gem install cocoapods # Since Travis is not always on latest version 8 | - pod repo update 9 | - pod install --project-directory=Example 10 | script: 11 | - set -o pipefail && xcodebuild clean test -workspace Example/KingfisherWebP.xcworkspace -scheme KingfisherWebP-Example -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -destination 'platform=iOS Simulator,name=iPhone 8,OS=13.6' | xcpretty 12 | - pod lib lint --allow-warnings 13 | -------------------------------------------------------------------------------- /Cartfile.project: -------------------------------------------------------------------------------- 1 | KingfisherWebP: 2 | project: KingfisherWebP.xcodeproj 3 | sdks: 4 | - watchsimulator 5 | - iphonesimulator 6 | - appletvsimulator 7 | - watchos 8 | - appletvos 9 | - iphoneos 10 | - macosx 11 | 12 | -------------------------------------------------------------------------------- /KingfisherWebP.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'KingfisherWebP' 3 | s.version = ENV['LIB_VERSION'] 4 | s.swift_version = '5.0' 5 | s.summary = 'A Kingfisher extension helping you process webp format' 6 | 7 | s.description = <<-DESC 8 | KingfisherWebP is an extension of the popular library [Kingfisher](https://github.com/onevcat/Kingfisher), providing a ImageProcessor and CacheSerializer for you to conveniently handle the WebP format. 9 | DESC 10 | 11 | s.homepage = 'https://github.com/yeatse/KingfisherWebP' 12 | s.license = { :type => 'MIT', :file => 'LICENSE' } 13 | s.author = { 'Yang Chao' => 'iyeatse@gmail.com' } 14 | s.source = { :git => 'https://github.com/yeatse/KingfisherWebP.git', :tag => s.version.to_s } 15 | s.social_media_url = 'https://twitter.com/yeatse' 16 | 17 | s.ios.deployment_target = "13.0" 18 | s.tvos.deployment_target = "13.0" 19 | s.osx.deployment_target = "10.15" 20 | s.watchos.deployment_target = "6.0" 21 | 22 | s.frameworks = "Accelerate" 23 | 24 | s.source_files = 'Sources/**/*.{h,m,swift}' 25 | s.pod_target_xcconfig = { 26 | 'USER_HEADER_SEARCH_PATHS' => '$(inherited) $(SRCROOT)/libwebp/src' 27 | } 28 | s.tvos.pod_target_xcconfig = { 29 | 'USER_HEADER_SEARCH_PATHS' => '$(inherited) $(SRCROOT)/libwebp/src' 30 | } 31 | s.osx.pod_target_xcconfig = { 32 | 'USER_HEADER_SEARCH_PATHS' => '$(inherited) $(SRCROOT)/libwebp/src' 33 | } 34 | s.watchos.pod_target_xcconfig = { 35 | 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) WEBP_USE_INTRINSICS=1', 36 | 'USER_HEADER_SEARCH_PATHS' => '$(inherited) $(SRCROOT)/libwebp/src' 37 | } 38 | 39 | s.dependency 'Kingfisher', '~> 8.0' 40 | s.dependency 'libwebp', '>= 1.1.0' 41 | end 42 | -------------------------------------------------------------------------------- /KingfisherWebP.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 388B9BD02A3DAA0700FEAD72 /* heart.webp in Resources */ = {isa = PBXBuildFile; fileRef = 388B9BCF2A3DAA0700FEAD72 /* heart.webp */; }; 11 | 38E23F292591B68900EBE21D /* KingfisherWebP.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38E23F1F2591B68900EBE21D /* KingfisherWebP.framework */; }; 12 | 38E23F2E2591B68900EBE21D /* KingfisherWebPTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E23F2D2591B68900EBE21D /* KingfisherWebPTests.swift */; }; 13 | 38E23F302591B68900EBE21D /* KingfisherWebP.h in Headers */ = {isa = PBXBuildFile; fileRef = 38E23F222591B68900EBE21D /* KingfisherWebP.h */; settings = {ATTRIBUTES = (Public, ); }; }; 14 | 38E23F3D2591B70300EBE21D /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 38E23F3C2591B70300EBE21D /* Kingfisher */; }; 15 | 38E23F432591B75200EBE21D /* WebPSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E23F402591B75200EBE21D /* WebPSerializer.swift */; }; 16 | 38E23F442591B75200EBE21D /* WebPProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E23F412591B75200EBE21D /* WebPProcessor.swift */; }; 17 | 38E23F452591B75200EBE21D /* Image+WebP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E23F422591B75200EBE21D /* Image+WebP.swift */; }; 18 | 38E23F752591BBB000EBE21D /* libwebp in Frameworks */ = {isa = PBXBuildFile; productRef = 38E23F742591BBB000EBE21D /* libwebp */; }; 19 | 38E23FB02591C39C00EBE21D /* cover.webp in Resources */ = {isa = PBXBuildFile; fileRef = 38E23FA82591C39C00EBE21D /* cover.webp */; }; 20 | 38E23FB12591C39C00EBE21D /* kingfisher.webp in Resources */ = {isa = PBXBuildFile; fileRef = 38E23FA92591C39C00EBE21D /* kingfisher.webp */; }; 21 | 38E23FB22591C39C00EBE21D /* logo.webp in Resources */ = {isa = PBXBuildFile; fileRef = 38E23FAA2591C39C00EBE21D /* logo.webp */; }; 22 | 38E23FB32591C39C00EBE21D /* kingfisher.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 38E23FAB2591C39C00EBE21D /* kingfisher.jpg */; }; 23 | 38E23FB42591C39C00EBE21D /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 38E23FAC2591C39C00EBE21D /* logo.png */; }; 24 | 38E23FB52591C39C00EBE21D /* animation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 38E23FAD2591C39C00EBE21D /* animation.webp */; }; 25 | 38E23FB62591C39C00EBE21D /* cover.png in Resources */ = {isa = PBXBuildFile; fileRef = 38E23FAE2591C39C00EBE21D /* cover.png */; }; 26 | 38E23FB72591C39C00EBE21D /* animation.gif in Resources */ = {isa = PBXBuildFile; fileRef = 38E23FAF2591C39C00EBE21D /* animation.gif */; }; 27 | 38FDDBF82A3DFD8F00553E3C /* heart.png in Resources */ = {isa = PBXBuildFile; fileRef = 38FDDBF72A3DFD8F00553E3C /* heart.png */; }; 28 | FEAA17F3264BF9CA00B1C2E7 /* CGImage+WebP.m in Sources */ = {isa = PBXBuildFile; fileRef = FEAA17F0264BF9CA00B1C2E7 /* CGImage+WebP.m */; }; 29 | FEAA17F4264BF9CA00B1C2E7 /* CGImage+WebP.h in Headers */ = {isa = PBXBuildFile; fileRef = FEAA17F2264BF9CA00B1C2E7 /* CGImage+WebP.h */; settings = {ATTRIBUTES = (Public, ); }; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXContainerItemProxy section */ 33 | 38E23F2A2591B68900EBE21D /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = 38E23F162591B68900EBE21D /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = 38E23F1E2591B68900EBE21D; 38 | remoteInfo = KingfisherWebP; 39 | }; 40 | /* End PBXContainerItemProxy section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | 388B9BCF2A3DAA0700FEAD72 /* heart.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = heart.webp; sourceTree = ""; }; 44 | 38E23F1F2591B68900EBE21D /* KingfisherWebP.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = KingfisherWebP.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 38E23F222591B68900EBE21D /* KingfisherWebP.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KingfisherWebP.h; sourceTree = ""; }; 46 | 38E23F232591B68900EBE21D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | 38E23F282591B68900EBE21D /* KingfisherWebPTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KingfisherWebPTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 38E23F2D2591B68900EBE21D /* KingfisherWebPTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KingfisherWebPTests.swift; sourceTree = ""; }; 49 | 38E23F2F2591B68900EBE21D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | 38E23F402591B75200EBE21D /* WebPSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebPSerializer.swift; sourceTree = ""; }; 51 | 38E23F412591B75200EBE21D /* WebPProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebPProcessor.swift; sourceTree = ""; }; 52 | 38E23F422591B75200EBE21D /* Image+WebP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Image+WebP.swift"; sourceTree = ""; }; 53 | 38E23FA82591C39C00EBE21D /* cover.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = cover.webp; sourceTree = ""; }; 54 | 38E23FA92591C39C00EBE21D /* kingfisher.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = kingfisher.webp; sourceTree = ""; }; 55 | 38E23FAA2591C39C00EBE21D /* logo.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = logo.webp; sourceTree = ""; }; 56 | 38E23FAB2591C39C00EBE21D /* kingfisher.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = kingfisher.jpg; sourceTree = ""; }; 57 | 38E23FAC2591C39C00EBE21D /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo.png; sourceTree = ""; }; 58 | 38E23FAD2591C39C00EBE21D /* animation.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = animation.webp; sourceTree = ""; }; 59 | 38E23FAE2591C39C00EBE21D /* cover.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = cover.png; sourceTree = ""; }; 60 | 38E23FAF2591C39C00EBE21D /* animation.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = animation.gif; sourceTree = ""; }; 61 | 38FDDBF72A3DFD8F00553E3C /* heart.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = heart.png; sourceTree = ""; }; 62 | FEAA17F0264BF9CA00B1C2E7 /* CGImage+WebP.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CGImage+WebP.m"; sourceTree = ""; }; 63 | FEAA17F2264BF9CA00B1C2E7 /* CGImage+WebP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CGImage+WebP.h"; sourceTree = ""; }; 64 | /* End PBXFileReference section */ 65 | 66 | /* Begin PBXFrameworksBuildPhase section */ 67 | 38E23F1C2591B68900EBE21D /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | 38E23F752591BBB000EBE21D /* libwebp in Frameworks */, 72 | 38E23F3D2591B70300EBE21D /* Kingfisher in Frameworks */, 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | 38E23F252591B68900EBE21D /* Frameworks */ = { 77 | isa = PBXFrameworksBuildPhase; 78 | buildActionMask = 2147483647; 79 | files = ( 80 | 38E23F292591B68900EBE21D /* KingfisherWebP.framework in Frameworks */, 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | /* End PBXFrameworksBuildPhase section */ 85 | 86 | /* Begin PBXGroup section */ 87 | 38E23F152591B68900EBE21D = { 88 | isa = PBXGroup; 89 | children = ( 90 | 38E23F212591B68900EBE21D /* Sources */, 91 | 38E23F2C2591B68900EBE21D /* Tests */, 92 | 38E23F202591B68900EBE21D /* Products */, 93 | ); 94 | sourceTree = ""; 95 | }; 96 | 38E23F202591B68900EBE21D /* Products */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 38E23F1F2591B68900EBE21D /* KingfisherWebP.framework */, 100 | 38E23F282591B68900EBE21D /* KingfisherWebPTests.xctest */, 101 | ); 102 | name = Products; 103 | sourceTree = ""; 104 | }; 105 | 38E23F212591B68900EBE21D /* Sources */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | FEAA17EF264BF9CA00B1C2E7 /* KingfisherWebP-ObjC */, 109 | 38E23F222591B68900EBE21D /* KingfisherWebP.h */, 110 | 38E23F422591B75200EBE21D /* Image+WebP.swift */, 111 | 38E23F412591B75200EBE21D /* WebPProcessor.swift */, 112 | 38E23F402591B75200EBE21D /* WebPSerializer.swift */, 113 | 38E23F232591B68900EBE21D /* Info.plist */, 114 | ); 115 | path = Sources; 116 | sourceTree = ""; 117 | }; 118 | 38E23F2C2591B68900EBE21D /* Tests */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 38E23F2D2591B68900EBE21D /* KingfisherWebPTests.swift */, 122 | 38E23FA72591C39C00EBE21D /* Resources */, 123 | 38E23F2F2591B68900EBE21D /* Info.plist */, 124 | ); 125 | path = Tests; 126 | sourceTree = ""; 127 | }; 128 | 38E23FA72591C39C00EBE21D /* Resources */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 38E23FA82591C39C00EBE21D /* cover.webp */, 132 | 38E23FA92591C39C00EBE21D /* kingfisher.webp */, 133 | 38E23FAA2591C39C00EBE21D /* logo.webp */, 134 | 38E23FAB2591C39C00EBE21D /* kingfisher.jpg */, 135 | 38E23FAC2591C39C00EBE21D /* logo.png */, 136 | 38E23FAD2591C39C00EBE21D /* animation.webp */, 137 | 38E23FAE2591C39C00EBE21D /* cover.png */, 138 | 38E23FAF2591C39C00EBE21D /* animation.gif */, 139 | 388B9BCF2A3DAA0700FEAD72 /* heart.webp */, 140 | 38FDDBF72A3DFD8F00553E3C /* heart.png */, 141 | ); 142 | path = Resources; 143 | sourceTree = ""; 144 | }; 145 | FEAA17EF264BF9CA00B1C2E7 /* KingfisherWebP-ObjC */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | FEAA17F0264BF9CA00B1C2E7 /* CGImage+WebP.m */, 149 | FEAA17F1264BF9CA00B1C2E7 /* include */, 150 | ); 151 | path = "KingfisherWebP-ObjC"; 152 | sourceTree = ""; 153 | }; 154 | FEAA17F1264BF9CA00B1C2E7 /* include */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | FEAA17F2264BF9CA00B1C2E7 /* CGImage+WebP.h */, 158 | ); 159 | path = include; 160 | sourceTree = ""; 161 | }; 162 | /* End PBXGroup section */ 163 | 164 | /* Begin PBXHeadersBuildPhase section */ 165 | 38E23F1A2591B68900EBE21D /* Headers */ = { 166 | isa = PBXHeadersBuildPhase; 167 | buildActionMask = 2147483647; 168 | files = ( 169 | FEAA17F4264BF9CA00B1C2E7 /* CGImage+WebP.h in Headers */, 170 | 38E23F302591B68900EBE21D /* KingfisherWebP.h in Headers */, 171 | ); 172 | runOnlyForDeploymentPostprocessing = 0; 173 | }; 174 | /* End PBXHeadersBuildPhase section */ 175 | 176 | /* Begin PBXNativeTarget section */ 177 | 38E23F1E2591B68900EBE21D /* KingfisherWebP */ = { 178 | isa = PBXNativeTarget; 179 | buildConfigurationList = 38E23F332591B68900EBE21D /* Build configuration list for PBXNativeTarget "KingfisherWebP" */; 180 | buildPhases = ( 181 | 38E23F1A2591B68900EBE21D /* Headers */, 182 | 38E23F1B2591B68900EBE21D /* Sources */, 183 | 38E23F1C2591B68900EBE21D /* Frameworks */, 184 | 38E23F1D2591B68900EBE21D /* Resources */, 185 | ); 186 | buildRules = ( 187 | ); 188 | dependencies = ( 189 | ); 190 | name = KingfisherWebP; 191 | packageProductDependencies = ( 192 | 38E23F3C2591B70300EBE21D /* Kingfisher */, 193 | 38E23F742591BBB000EBE21D /* libwebp */, 194 | ); 195 | productName = KingfisherWebP; 196 | productReference = 38E23F1F2591B68900EBE21D /* KingfisherWebP.framework */; 197 | productType = "com.apple.product-type.framework"; 198 | }; 199 | 38E23F272591B68900EBE21D /* KingfisherWebPTests */ = { 200 | isa = PBXNativeTarget; 201 | buildConfigurationList = 38E23F362591B68900EBE21D /* Build configuration list for PBXNativeTarget "KingfisherWebPTests" */; 202 | buildPhases = ( 203 | 38E23F242591B68900EBE21D /* Sources */, 204 | 38E23F252591B68900EBE21D /* Frameworks */, 205 | 38E23F262591B68900EBE21D /* Resources */, 206 | ); 207 | buildRules = ( 208 | ); 209 | dependencies = ( 210 | 38E23F2B2591B68900EBE21D /* PBXTargetDependency */, 211 | ); 212 | name = KingfisherWebPTests; 213 | productName = KingfisherWebPTests; 214 | productReference = 38E23F282591B68900EBE21D /* KingfisherWebPTests.xctest */; 215 | productType = "com.apple.product-type.bundle.unit-test"; 216 | }; 217 | /* End PBXNativeTarget section */ 218 | 219 | /* Begin PBXProject section */ 220 | 38E23F162591B68900EBE21D /* Project object */ = { 221 | isa = PBXProject; 222 | attributes = { 223 | BuildIndependentTargetsInParallel = YES; 224 | LastSwiftUpdateCheck = 1230; 225 | LastUpgradeCheck = 1430; 226 | TargetAttributes = { 227 | 38E23F1E2591B68900EBE21D = { 228 | CreatedOnToolsVersion = 12.3; 229 | LastSwiftMigration = 1230; 230 | }; 231 | 38E23F272591B68900EBE21D = { 232 | CreatedOnToolsVersion = 12.3; 233 | }; 234 | }; 235 | }; 236 | buildConfigurationList = 38E23F192591B68900EBE21D /* Build configuration list for PBXProject "KingfisherWebP" */; 237 | compatibilityVersion = "Xcode 9.3"; 238 | developmentRegion = en; 239 | hasScannedForEncodings = 0; 240 | knownRegions = ( 241 | en, 242 | Base, 243 | ); 244 | mainGroup = 38E23F152591B68900EBE21D; 245 | packageReferences = ( 246 | 38E23F3B2591B70300EBE21D /* XCRemoteSwiftPackageReference "Kingfisher" */, 247 | 38E23F732591BBB000EBE21D /* XCRemoteSwiftPackageReference "libwebp-Xcode" */, 248 | ); 249 | productRefGroup = 38E23F202591B68900EBE21D /* Products */; 250 | projectDirPath = ""; 251 | projectRoot = ""; 252 | targets = ( 253 | 38E23F1E2591B68900EBE21D /* KingfisherWebP */, 254 | 38E23F272591B68900EBE21D /* KingfisherWebPTests */, 255 | ); 256 | }; 257 | /* End PBXProject section */ 258 | 259 | /* Begin PBXResourcesBuildPhase section */ 260 | 38E23F1D2591B68900EBE21D /* Resources */ = { 261 | isa = PBXResourcesBuildPhase; 262 | buildActionMask = 2147483647; 263 | files = ( 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | }; 267 | 38E23F262591B68900EBE21D /* Resources */ = { 268 | isa = PBXResourcesBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | 38E23FB02591C39C00EBE21D /* cover.webp in Resources */, 272 | 38E23FB72591C39C00EBE21D /* animation.gif in Resources */, 273 | 38E23FB42591C39C00EBE21D /* logo.png in Resources */, 274 | 38E23FB22591C39C00EBE21D /* logo.webp in Resources */, 275 | 38E23FB12591C39C00EBE21D /* kingfisher.webp in Resources */, 276 | 38FDDBF82A3DFD8F00553E3C /* heart.png in Resources */, 277 | 38E23FB52591C39C00EBE21D /* animation.webp in Resources */, 278 | 38E23FB62591C39C00EBE21D /* cover.png in Resources */, 279 | 38E23FB32591C39C00EBE21D /* kingfisher.jpg in Resources */, 280 | 388B9BD02A3DAA0700FEAD72 /* heart.webp in Resources */, 281 | ); 282 | runOnlyForDeploymentPostprocessing = 0; 283 | }; 284 | /* End PBXResourcesBuildPhase section */ 285 | 286 | /* Begin PBXSourcesBuildPhase section */ 287 | 38E23F1B2591B68900EBE21D /* Sources */ = { 288 | isa = PBXSourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | 38E23F452591B75200EBE21D /* Image+WebP.swift in Sources */, 292 | 38E23F442591B75200EBE21D /* WebPProcessor.swift in Sources */, 293 | 38E23F432591B75200EBE21D /* WebPSerializer.swift in Sources */, 294 | FEAA17F3264BF9CA00B1C2E7 /* CGImage+WebP.m in Sources */, 295 | ); 296 | runOnlyForDeploymentPostprocessing = 0; 297 | }; 298 | 38E23F242591B68900EBE21D /* Sources */ = { 299 | isa = PBXSourcesBuildPhase; 300 | buildActionMask = 2147483647; 301 | files = ( 302 | 38E23F2E2591B68900EBE21D /* KingfisherWebPTests.swift in Sources */, 303 | ); 304 | runOnlyForDeploymentPostprocessing = 0; 305 | }; 306 | /* End PBXSourcesBuildPhase section */ 307 | 308 | /* Begin PBXTargetDependency section */ 309 | 38E23F2B2591B68900EBE21D /* PBXTargetDependency */ = { 310 | isa = PBXTargetDependency; 311 | target = 38E23F1E2591B68900EBE21D /* KingfisherWebP */; 312 | targetProxy = 38E23F2A2591B68900EBE21D /* PBXContainerItemProxy */; 313 | }; 314 | /* End PBXTargetDependency section */ 315 | 316 | /* Begin XCBuildConfiguration section */ 317 | 38E23F312591B68900EBE21D /* Debug */ = { 318 | isa = XCBuildConfiguration; 319 | buildSettings = { 320 | ALWAYS_SEARCH_USER_PATHS = NO; 321 | CLANG_ANALYZER_NONNULL = YES; 322 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 323 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 324 | CLANG_CXX_LIBRARY = "libc++"; 325 | CLANG_ENABLE_MODULES = YES; 326 | CLANG_ENABLE_OBJC_ARC = YES; 327 | CLANG_ENABLE_OBJC_WEAK = YES; 328 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 329 | CLANG_WARN_BOOL_CONVERSION = YES; 330 | CLANG_WARN_COMMA = YES; 331 | CLANG_WARN_CONSTANT_CONVERSION = YES; 332 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 333 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 334 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 335 | CLANG_WARN_EMPTY_BODY = YES; 336 | CLANG_WARN_ENUM_CONVERSION = YES; 337 | CLANG_WARN_INFINITE_RECURSION = YES; 338 | CLANG_WARN_INT_CONVERSION = YES; 339 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 340 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 341 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 342 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 343 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 344 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 345 | CLANG_WARN_STRICT_PROTOTYPES = YES; 346 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 347 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 348 | CLANG_WARN_UNREACHABLE_CODE = YES; 349 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 350 | COPY_PHASE_STRIP = NO; 351 | CURRENT_PROJECT_VERSION = 1; 352 | DEAD_CODE_STRIPPING = YES; 353 | DEBUG_INFORMATION_FORMAT = dwarf; 354 | ENABLE_STRICT_OBJC_MSGSEND = YES; 355 | ENABLE_TESTABILITY = YES; 356 | GCC_C_LANGUAGE_STANDARD = gnu11; 357 | GCC_DYNAMIC_NO_PIC = NO; 358 | GCC_NO_COMMON_BLOCKS = YES; 359 | GCC_OPTIMIZATION_LEVEL = 0; 360 | GCC_PREPROCESSOR_DEFINITIONS = ( 361 | "DEBUG=1", 362 | "$(inherited)", 363 | ); 364 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 365 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 366 | GCC_WARN_UNDECLARED_SELECTOR = YES; 367 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 368 | GCC_WARN_UNUSED_FUNCTION = YES; 369 | GCC_WARN_UNUSED_VARIABLE = YES; 370 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 371 | MACOSX_DEPLOYMENT_TARGET = 10.14; 372 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 373 | MTL_FAST_MATH = YES; 374 | ONLY_ACTIVE_ARCH = YES; 375 | SDKROOT = ""; 376 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos watchsimulator watchos appletvsimulator appletvos macosx"; 377 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 378 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 379 | TVOS_DEPLOYMENT_TARGET = 12.1; 380 | VERSIONING_SYSTEM = "apple-generic"; 381 | VERSION_INFO_PREFIX = ""; 382 | WATCHOS_DEPLOYMENT_TARGET = 5.0; 383 | }; 384 | name = Debug; 385 | }; 386 | 38E23F322591B68900EBE21D /* Release */ = { 387 | isa = XCBuildConfiguration; 388 | buildSettings = { 389 | ALWAYS_SEARCH_USER_PATHS = NO; 390 | CLANG_ANALYZER_NONNULL = YES; 391 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 392 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 393 | CLANG_CXX_LIBRARY = "libc++"; 394 | CLANG_ENABLE_MODULES = YES; 395 | CLANG_ENABLE_OBJC_ARC = YES; 396 | CLANG_ENABLE_OBJC_WEAK = YES; 397 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 398 | CLANG_WARN_BOOL_CONVERSION = YES; 399 | CLANG_WARN_COMMA = YES; 400 | CLANG_WARN_CONSTANT_CONVERSION = YES; 401 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 402 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 403 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 404 | CLANG_WARN_EMPTY_BODY = YES; 405 | CLANG_WARN_ENUM_CONVERSION = YES; 406 | CLANG_WARN_INFINITE_RECURSION = YES; 407 | CLANG_WARN_INT_CONVERSION = YES; 408 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 409 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 410 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 411 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 412 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 413 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 414 | CLANG_WARN_STRICT_PROTOTYPES = YES; 415 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 416 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 417 | CLANG_WARN_UNREACHABLE_CODE = YES; 418 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 419 | COPY_PHASE_STRIP = NO; 420 | CURRENT_PROJECT_VERSION = 1; 421 | DEAD_CODE_STRIPPING = YES; 422 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 423 | ENABLE_NS_ASSERTIONS = NO; 424 | ENABLE_STRICT_OBJC_MSGSEND = YES; 425 | GCC_C_LANGUAGE_STANDARD = gnu11; 426 | GCC_NO_COMMON_BLOCKS = YES; 427 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 428 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 429 | GCC_WARN_UNDECLARED_SELECTOR = YES; 430 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 431 | GCC_WARN_UNUSED_FUNCTION = YES; 432 | GCC_WARN_UNUSED_VARIABLE = YES; 433 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 434 | MACOSX_DEPLOYMENT_TARGET = 10.14; 435 | MTL_ENABLE_DEBUG_INFO = NO; 436 | MTL_FAST_MATH = YES; 437 | SDKROOT = ""; 438 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos watchsimulator watchos appletvsimulator appletvos macosx"; 439 | SWIFT_COMPILATION_MODE = wholemodule; 440 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 441 | TVOS_DEPLOYMENT_TARGET = 12.1; 442 | VALIDATE_PRODUCT = YES; 443 | VERSIONING_SYSTEM = "apple-generic"; 444 | VERSION_INFO_PREFIX = ""; 445 | WATCHOS_DEPLOYMENT_TARGET = 5.0; 446 | }; 447 | name = Release; 448 | }; 449 | 38E23F342591B68900EBE21D /* Debug */ = { 450 | isa = XCBuildConfiguration; 451 | buildSettings = { 452 | CLANG_ENABLE_MODULES = YES; 453 | CODE_SIGN_STYLE = Automatic; 454 | DEAD_CODE_STRIPPING = YES; 455 | DEFINES_MODULE = YES; 456 | DYLIB_COMPATIBILITY_VERSION = 1; 457 | DYLIB_CURRENT_VERSION = 1; 458 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 459 | ENABLE_MODULE_VERIFIER = YES; 460 | INFOPLIST_FILE = Sources/Info.plist; 461 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 462 | IPHONEOS_DEPLOYMENT_TARGET = 13; 463 | LD_RUNPATH_SEARCH_PATHS = ( 464 | "$(inherited)", 465 | "@executable_path/Frameworks", 466 | "@loader_path/Frameworks", 467 | ); 468 | MACOSX_DEPLOYMENT_TARGET = 10.15; 469 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 470 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; 471 | PRODUCT_BUNDLE_IDENTIFIER = com.yeatse.KingfisherWebP; 472 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 473 | SKIP_INSTALL = YES; 474 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 475 | SWIFT_VERSION = 5.0; 476 | TARGETED_DEVICE_FAMILY = "1,2,3,4"; 477 | TVOS_DEPLOYMENT_TARGET = 13; 478 | WATCHOS_DEPLOYMENT_TARGET = 6; 479 | }; 480 | name = Debug; 481 | }; 482 | 38E23F352591B68900EBE21D /* Release */ = { 483 | isa = XCBuildConfiguration; 484 | buildSettings = { 485 | CLANG_ENABLE_MODULES = YES; 486 | CODE_SIGN_STYLE = Automatic; 487 | DEAD_CODE_STRIPPING = YES; 488 | DEFINES_MODULE = YES; 489 | DYLIB_COMPATIBILITY_VERSION = 1; 490 | DYLIB_CURRENT_VERSION = 1; 491 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 492 | ENABLE_MODULE_VERIFIER = YES; 493 | INFOPLIST_FILE = Sources/Info.plist; 494 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 495 | IPHONEOS_DEPLOYMENT_TARGET = 13; 496 | LD_RUNPATH_SEARCH_PATHS = ( 497 | "$(inherited)", 498 | "@executable_path/Frameworks", 499 | "@loader_path/Frameworks", 500 | ); 501 | MACOSX_DEPLOYMENT_TARGET = 10.15; 502 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 503 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; 504 | PRODUCT_BUNDLE_IDENTIFIER = com.yeatse.KingfisherWebP; 505 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 506 | SKIP_INSTALL = YES; 507 | SWIFT_VERSION = 5.0; 508 | TARGETED_DEVICE_FAMILY = "1,2,3,4"; 509 | TVOS_DEPLOYMENT_TARGET = 13; 510 | WATCHOS_DEPLOYMENT_TARGET = 6; 511 | }; 512 | name = Release; 513 | }; 514 | 38E23F372591B68900EBE21D /* Debug */ = { 515 | isa = XCBuildConfiguration; 516 | buildSettings = { 517 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 518 | CODE_SIGN_STYLE = Automatic; 519 | DEAD_CODE_STRIPPING = YES; 520 | INFOPLIST_FILE = Tests/Info.plist; 521 | IPHONEOS_DEPLOYMENT_TARGET = 13; 522 | LD_RUNPATH_SEARCH_PATHS = ( 523 | "$(inherited)", 524 | "@executable_path/Frameworks", 525 | "@loader_path/Frameworks", 526 | ); 527 | MACOSX_DEPLOYMENT_TARGET = 10.15; 528 | PRODUCT_BUNDLE_IDENTIFIER = com.yeatse.KingfisherWebPTests; 529 | PRODUCT_NAME = "$(TARGET_NAME)"; 530 | SWIFT_VERSION = 5.0; 531 | TARGETED_DEVICE_FAMILY = "1,2"; 532 | TVOS_DEPLOYMENT_TARGET = 13; 533 | WATCHOS_DEPLOYMENT_TARGET = 6; 534 | }; 535 | name = Debug; 536 | }; 537 | 38E23F382591B68900EBE21D /* Release */ = { 538 | isa = XCBuildConfiguration; 539 | buildSettings = { 540 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 541 | CODE_SIGN_STYLE = Automatic; 542 | DEAD_CODE_STRIPPING = YES; 543 | INFOPLIST_FILE = Tests/Info.plist; 544 | IPHONEOS_DEPLOYMENT_TARGET = 13; 545 | LD_RUNPATH_SEARCH_PATHS = ( 546 | "$(inherited)", 547 | "@executable_path/Frameworks", 548 | "@loader_path/Frameworks", 549 | ); 550 | MACOSX_DEPLOYMENT_TARGET = 10.15; 551 | PRODUCT_BUNDLE_IDENTIFIER = com.yeatse.KingfisherWebPTests; 552 | PRODUCT_NAME = "$(TARGET_NAME)"; 553 | SWIFT_VERSION = 5.0; 554 | TARGETED_DEVICE_FAMILY = "1,2"; 555 | TVOS_DEPLOYMENT_TARGET = 13; 556 | WATCHOS_DEPLOYMENT_TARGET = 6; 557 | }; 558 | name = Release; 559 | }; 560 | /* End XCBuildConfiguration section */ 561 | 562 | /* Begin XCConfigurationList section */ 563 | 38E23F192591B68900EBE21D /* Build configuration list for PBXProject "KingfisherWebP" */ = { 564 | isa = XCConfigurationList; 565 | buildConfigurations = ( 566 | 38E23F312591B68900EBE21D /* Debug */, 567 | 38E23F322591B68900EBE21D /* Release */, 568 | ); 569 | defaultConfigurationIsVisible = 0; 570 | defaultConfigurationName = Release; 571 | }; 572 | 38E23F332591B68900EBE21D /* Build configuration list for PBXNativeTarget "KingfisherWebP" */ = { 573 | isa = XCConfigurationList; 574 | buildConfigurations = ( 575 | 38E23F342591B68900EBE21D /* Debug */, 576 | 38E23F352591B68900EBE21D /* Release */, 577 | ); 578 | defaultConfigurationIsVisible = 0; 579 | defaultConfigurationName = Release; 580 | }; 581 | 38E23F362591B68900EBE21D /* Build configuration list for PBXNativeTarget "KingfisherWebPTests" */ = { 582 | isa = XCConfigurationList; 583 | buildConfigurations = ( 584 | 38E23F372591B68900EBE21D /* Debug */, 585 | 38E23F382591B68900EBE21D /* Release */, 586 | ); 587 | defaultConfigurationIsVisible = 0; 588 | defaultConfigurationName = Release; 589 | }; 590 | /* End XCConfigurationList section */ 591 | 592 | /* Begin XCRemoteSwiftPackageReference section */ 593 | 38E23F3B2591B70300EBE21D /* XCRemoteSwiftPackageReference "Kingfisher" */ = { 594 | isa = XCRemoteSwiftPackageReference; 595 | repositoryURL = "https://github.com/onevcat/Kingfisher.git"; 596 | requirement = { 597 | kind = upToNextMajorVersion; 598 | minimumVersion = 8.0.0; 599 | }; 600 | }; 601 | 38E23F732591BBB000EBE21D /* XCRemoteSwiftPackageReference "libwebp-Xcode" */ = { 602 | isa = XCRemoteSwiftPackageReference; 603 | repositoryURL = "https://github.com/SDWebImage/libwebp-Xcode"; 604 | requirement = { 605 | kind = upToNextMajorVersion; 606 | minimumVersion = 1.0.0; 607 | }; 608 | }; 609 | /* End XCRemoteSwiftPackageReference section */ 610 | 611 | /* Begin XCSwiftPackageProductDependency section */ 612 | 38E23F3C2591B70300EBE21D /* Kingfisher */ = { 613 | isa = XCSwiftPackageProductDependency; 614 | package = 38E23F3B2591B70300EBE21D /* XCRemoteSwiftPackageReference "Kingfisher" */; 615 | productName = Kingfisher; 616 | }; 617 | 38E23F742591BBB000EBE21D /* libwebp */ = { 618 | isa = XCSwiftPackageProductDependency; 619 | package = 38E23F732591BBB000EBE21D /* XCRemoteSwiftPackageReference "libwebp-Xcode" */; 620 | productName = libwebp; 621 | }; 622 | /* End XCSwiftPackageProductDependency section */ 623 | }; 624 | rootObject = 38E23F162591B68900EBE21D /* Project object */; 625 | } 626 | -------------------------------------------------------------------------------- /KingfisherWebP.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /KingfisherWebP.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /KingfisherWebP.xcodeproj/xcshareddata/xcschemes/KingfisherWebP.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Yang Chao 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 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: "KingfisherWebP", 8 | platforms: [.iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macOS(.v10_15)], 9 | products: [ 10 | .library(name: "KingfisherWebP", targets: ["KingfisherWebP"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/onevcat/Kingfisher.git", from: "8.0.0"), 14 | .package(url: "https://github.com/SDWebImage/libwebp-Xcode.git", from: "1.1.0") 15 | ], 16 | targets: [ 17 | .target( 18 | name: "KingfisherWebP", 19 | dependencies: ["Kingfisher", "KingfisherWebP-ObjC"], 20 | path: "Sources", 21 | exclude: ["KingfisherWebP-ObjC"] 22 | ), 23 | .target( 24 | name: "KingfisherWebP-ObjC", 25 | dependencies: ["libwebp"] 26 | ) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KingfisherWebP 2 | 3 | [![CI Status](https://github.com/Yeatse/KingfisherWebP/workflows/unittest/badge.svg)](https://travis-ci.org/Yeatse/KingfisherWebP) 4 | [![Version](https://img.shields.io/cocoapods/v/KingfisherWebP.svg?style=flat)](http://cocoapods.org/pods/KingfisherWebP) 5 | [![License](https://img.shields.io/cocoapods/l/KingfisherWebP.svg?style=flat)](http://cocoapods.org/pods/KingfisherWebP) 6 | [![Platform](https://img.shields.io/cocoapods/p/KingfisherWebP.svg?style=flat)](http://cocoapods.org/pods/KingfisherWebP) 7 | 8 | # Description 9 | 10 | KingfisherWebP is an extension of the popular library [Kingfisher](https://github.com/onevcat/Kingfisher), providing an ImageProcessor and CacheSerializer for you to conveniently handle the [WebP format](https://developers.google.com/speed/webp/). Animated WebP is also supported! 11 | 12 | The library works seamlessly with `Kingfisher`. To display the webp images from network, simply add `WebPProcessor` and `WebPSerializer` to your `KingfisherOptionsInfo`: 13 | 14 | ```swift 15 | let url = URL(string: "url_of_your_webp_image") 16 | imageView.kf.setImage(with: url, options: [.processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default)]) 17 | ``` 18 | 19 | For convenience, you may set it as a global default option to all `KingfisherManager` related methods: 20 | 21 | ```swift 22 | // somewhere after your application launches... 23 | 24 | KingfisherManager.shared.defaultOptions += [ 25 | .processor(WebPProcessor.default), 26 | .cacheSerializer(WebPSerializer.default) 27 | ] 28 | 29 | // You can now use webp in Kingfisher like any other format 30 | imageView.kf.setImage(with: url) 31 | ``` 32 | 33 | Some image servers may expect the `"Accept"` header to include `"image/webp"`. 34 | You can include this header to all Kingfisher requests by doing: 35 | ```swift 36 | let modifier = AnyModifier { request in 37 | var req = request 38 | req.addValue("image/webp */*", forHTTPHeaderField: "Accept") 39 | return req 40 | } 41 | 42 | KingfisherManager.shared.defaultOptions += [ 43 | .requestModifier(modifier), 44 | // ... other options 45 | ] 46 | ``` 47 | 48 | If the image data is not in webp format, the default processor and serializer in `Kingfisher` will be used. 49 | 50 | 51 | ## Example 52 | 53 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 54 | 55 | ## Requirements 56 | 57 | iOS 12 or above 58 | 59 | ## Installation 60 | 61 | ### CocoaPods 62 | 63 | KingfisherWebP is available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your Podfile: 64 | 65 | ```ruby 66 | pod "KingfisherWebP" 67 | ``` 68 | 69 | ### Swift Package Manager 70 | 71 | From Xcode 11, you can use [Swift Package Manager](https://swift.org/package-manager/) to add KingfisherWebP to your project. The "package repository url" of KingfisherWebP is `https://github.com/yeatse/KingfisherWebP.git`. 72 | 73 | ![SPM screenshot](spm_screenshot.png) 74 | 75 | ### Carthage 76 | 77 | You can also add KingfisherWebP using [Carthage](https://github.com/Carthage/Carthage). Note that KingfisherWebP is built on top of [libwebp](https://chromium.googlesource.com/webm/libwebp) project, so in your `Cartfile` you should add `libwebp` dependency as well: 78 | 79 | ``` 80 | github "yeatse/KingfisherWebP" ~> 1.4.0 81 | github "onevcat/Kingfisher" ~> 7.0.0 82 | github "SDWebImage/libwebp-Xcode" ~> 1.1.0 83 | ``` 84 | 85 | 86 | ## Author 87 | 88 | Yang Chao, iyeatse@gmail.com 89 | 90 | ## License 91 | 92 | KingfisherWebP is available under the MIT license. See the LICENSE file for more info. 93 | -------------------------------------------------------------------------------- /Sources/Image+WebP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image+WebP.swift 3 | // Pods 4 | // 5 | // Created by yeatse on 2016/10/19. 6 | // 7 | // 8 | 9 | import Kingfisher 10 | import CoreGraphics 11 | import Foundation 12 | 13 | #if SWIFT_PACKAGE 14 | import KingfisherWebP_ObjC 15 | #endif 16 | 17 | #if canImport(AppKit) 18 | import AppKit 19 | #endif 20 | 21 | // MARK: - Image Representation 22 | extension KingfisherWrapper where Base: KFCrossPlatformImage { 23 | /// isLossy (0=lossy , 1=lossless (default)). 24 | /// Note that the default values are isLossy= false and quality=75.0f 25 | public func webpRepresentation(isLossy: Bool = false, quality: Float = 75.0) -> Data? { 26 | if let result = animatedWebPRepresentation(isLossy: isLossy, quality: quality) { 27 | return result 28 | } 29 | #if os(macOS) 30 | if let cgImage = base.cgImage(forProposedRect: nil, context: nil, hints: nil) { 31 | return WebPDataCreateWithImage(cgImage, isLossy, quality) as Data? 32 | } 33 | #else 34 | if let cgImage = base.cgImage { 35 | return WebPDataCreateWithImage(cgImage, isLossy, quality) as Data? 36 | } 37 | #endif 38 | return nil 39 | } 40 | 41 | /// isLossy (0=lossy , 1=lossless (default)). 42 | /// Note that the default values are isLossy= false and quality=75.0f 43 | private func animatedWebPRepresentation(isLossy: Bool = false, quality: Float = 75.0) -> Data? { 44 | let imageInfo: [CFString: Any] 45 | if let frameSource = frameSource { 46 | let frameCount = frameSource.frameCount 47 | imageInfo = [ 48 | kWebPAnimatedImageFrames: (0.. KFCrossPlatformImage? { 71 | let options = ImageCreatingOptions(scale: scale, preloadAll: true, onlyFirstFrame: onlyFirstFrame) 72 | return image(webpData: webpData, options: options) 73 | } 74 | 75 | public static func image(webpData: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? { 76 | let frameCount = WebPImageFrameCountGetFromData(webpData as CFData) 77 | if (frameCount == 0) { 78 | return nil 79 | } 80 | 81 | if (frameCount == 1 || options.onlyFirstFrame) { 82 | // MARK: Still image 83 | guard let cgImage = WebPImageCreateWithData(webpData as CFData) else { 84 | return nil 85 | } 86 | #if os(macOS) 87 | let image = KFCrossPlatformImage(cgImage: cgImage, size: .zero) 88 | #else 89 | let image = KFCrossPlatformImage(cgImage: cgImage, scale: options.scale, orientation: .up) 90 | #endif 91 | image.kf.imageFrameCount = Int(frameCount) 92 | return image 93 | } 94 | 95 | // MARK: Animated images 96 | guard let frameSource = WebPFrameSource(data: webpData) else { return nil } 97 | return KingfisherWrapper.animatedImage(source: frameSource, options: options) 98 | } 99 | } 100 | 101 | class WebPFrameSource: ImageFrameSource { 102 | init?(data: Data) { 103 | guard let decoder = WebPDecoderCreateWithData(data as CFData) else { 104 | return nil 105 | } 106 | self.data = data 107 | self.decoder = decoder 108 | // http://www.russbishop.net/the-law 109 | self.decoderLock = UnsafeMutablePointer.allocate(capacity: 1) 110 | self.decoderLock.initialize(to: os_unfair_lock()) 111 | } 112 | 113 | deinit { 114 | WebPDecoderDestroy(decoder) 115 | decoderLock.deallocate() 116 | } 117 | 118 | let data: Data? 119 | private let decoder: WebPDecoderRef 120 | private var decoderLock: UnsafeMutablePointer 121 | private var frameCache = NSCache() 122 | 123 | var frameCount: Int { 124 | get { 125 | return Int(WebPDecoderGetFrameCount(decoder)) 126 | } 127 | } 128 | 129 | func frame(at index: Int, maxSize: CGSize?) -> CGImage? { 130 | os_unfair_lock_lock(decoderLock) 131 | defer { 132 | os_unfair_lock_unlock(decoderLock) 133 | } 134 | var image = frameCache.object(forKey: index as NSNumber) 135 | if image == nil { 136 | image = WebPDecoderCopyImageAtIndex(decoder, Int32(index)) 137 | if image != nil { 138 | frameCache.setObject(image!, forKey: index as NSNumber) 139 | } 140 | } 141 | guard let image = image else { return nil } 142 | if let maxSize = maxSize, maxSize != .zero, CGFloat(image.width) > maxSize.width || CGFloat(image.height) > maxSize.height { 143 | let scale = min(maxSize.width / CGFloat(image.width), maxSize.height / CGFloat(image.height)) 144 | let destWidth = Int(CGFloat(image.width) * scale) 145 | let destHeight = Int(CGFloat(image.height) * scale) 146 | let context = CGContext(data: nil, 147 | width: destWidth, 148 | height: destHeight, 149 | bitsPerComponent: image.bitsPerComponent, 150 | bytesPerRow: 0, 151 | space: image.colorSpace ?? CGColorSpaceCreateDeviceRGB(), 152 | bitmapInfo: image.bitmapInfo.rawValue) 153 | context?.interpolationQuality = .high 154 | context?.draw(image, in: CGRect(x: 0, y: 0, width: destWidth, height: destHeight)) 155 | return context?.makeImage() ?? image 156 | } 157 | return image 158 | } 159 | 160 | func duration(at index: Int) -> TimeInterval { 161 | let duration = WebPDecoderGetDurationAtIndex(decoder, Int32(index)) 162 | // https://github.com/onevcat/Kingfisher/blob/3f6992b5cd3143e83b02300ea59c400d4cf0747a/Sources/Image/GIFAnimatedImage.swift#L106 163 | if duration > 0.011 { 164 | return duration 165 | } else { 166 | return 0.1 167 | } 168 | } 169 | } 170 | 171 | // MARK: - WebP Format Testing 172 | extension Data { 173 | public var isWebPFormat: Bool { 174 | if count < 12 { 175 | return false 176 | } 177 | 178 | let riffHeader = subdata(in: startIndex.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/KingfisherWebP-ObjC/CGImage+WebP.m: -------------------------------------------------------------------------------- 1 | // 2 | // CGImage+WebP.m 3 | // Pods 4 | // 5 | // Created by yeatse on 2016/10/20. 6 | // 7 | // 8 | 9 | #import "CGImage+WebP.h" 10 | 11 | #import 12 | #import 13 | 14 | #if __has_include("webp/decode.h") && __has_include("webp/encode.h") && __has_include("webp/demux.h") && __has_include("webp/mux.h") 15 | #import "webp/decode.h" 16 | #import "webp/encode.h" 17 | #import "webp/demux.h" 18 | #import "webp/mux.h" 19 | #elif __has_include() && __has_include() && __has_include() && __has_include() 20 | #import 21 | #import 22 | #import 23 | #import 24 | #else 25 | @import libwebp; 26 | #endif 27 | 28 | #pragma mark - Helper Functions 29 | 30 | static CGColorSpaceRef WebPColorSpaceForDeviceRGB(void) { 31 | static CGColorSpaceRef colorSpace; 32 | static dispatch_once_t onceToken; 33 | dispatch_once(&onceToken, ^{ 34 | colorSpace = CGColorSpaceCreateDeviceRGB(); 35 | }); 36 | return colorSpace; 37 | } 38 | 39 | /** 40 | Decode an image to bitmap buffer with the specified format. 41 | 42 | @param srcImage Source image. 43 | @param dest Destination buffer. It should be zero before call this method. 44 | If decode succeed, you should release the dest->data using free(). 45 | @param destFormat Destination bitmap format. 46 | 47 | @return Whether succeed. 48 | 49 | @warning This method support iOS7.0 and later. If call it on iOS6, it just returns NO. 50 | CG_AVAILABLE_STARTING(__MAC_10_9, __IPHONE_7_0) 51 | */ 52 | static BOOL WebPCGImageDecodeToBitmapBufferWithAnyFormat(CGImageRef srcImage, vImage_Buffer *dest, vImage_CGImageFormat *destFormat) { 53 | if (!srcImage || (((long)vImageConvert_AnyToAny) + 1 == 1) || !destFormat || !dest) return NO; 54 | size_t width = CGImageGetWidth(srcImage); 55 | size_t height = CGImageGetHeight(srcImage); 56 | if (width == 0 || height == 0) return NO; 57 | dest->data = NULL; 58 | 59 | vImage_Error error = kvImageNoError; 60 | CFDataRef srcData = NULL; 61 | vImageConverterRef convertor = NULL; 62 | vImage_CGImageFormat srcFormat = {0}; 63 | srcFormat.bitsPerComponent = (uint32_t)CGImageGetBitsPerComponent(srcImage); 64 | srcFormat.bitsPerPixel = (uint32_t)CGImageGetBitsPerPixel(srcImage); 65 | srcFormat.colorSpace = CGImageGetColorSpace(srcImage); 66 | srcFormat.bitmapInfo = CGImageGetBitmapInfo(srcImage) | CGImageGetAlphaInfo(srcImage); 67 | 68 | convertor = vImageConverter_CreateWithCGImageFormat(&srcFormat, destFormat, NULL, kvImageNoFlags, NULL); 69 | if (!convertor) goto fail; 70 | 71 | CGDataProviderRef srcProvider = CGImageGetDataProvider(srcImage); 72 | srcData = srcProvider ? CGDataProviderCopyData(srcProvider) : NULL; // decode 73 | size_t srcLength = srcData ? CFDataGetLength(srcData) : 0; 74 | const void *srcBytes = srcData ? CFDataGetBytePtr(srcData) : NULL; 75 | if (srcLength == 0 || !srcBytes) goto fail; 76 | 77 | vImage_Buffer src = {0}; 78 | src.data = (void *)srcBytes; 79 | src.width = width; 80 | src.height = height; 81 | src.rowBytes = CGImageGetBytesPerRow(srcImage); 82 | 83 | error = vImageBuffer_Init(dest, height, width, 32, kvImageNoFlags); 84 | if (error != kvImageNoError) goto fail; 85 | 86 | error = vImageConvert_AnyToAny(convertor, &src, dest, NULL, kvImageNoFlags); // convert 87 | if (error != kvImageNoError) goto fail; 88 | 89 | CFRelease(convertor); 90 | CFRelease(srcData); 91 | return YES; 92 | 93 | fail: 94 | if (convertor) CFRelease(convertor); 95 | if (srcData) CFRelease(srcData); 96 | if (dest->data) free(dest->data); 97 | dest->data = NULL; 98 | return NO; 99 | } 100 | 101 | /** 102 | Decode an image to bitmap buffer with the 32bit format (such as ARGB8888). 103 | 104 | @param srcImage Source image. 105 | @param dest Destination buffer. It should be zero before call this method. 106 | If decode succeed, you should release the dest->data using free(). 107 | @param bitmapInfo Destination bitmap format. 108 | 109 | @return Whether succeed. 110 | */ 111 | static BOOL WebPCGImageDecodeToBitmapBufferWith32BitFormat(CGImageRef srcImage, vImage_Buffer *dest, CGBitmapInfo bitmapInfo) { 112 | if (!srcImage || !dest) return NO; 113 | size_t width = CGImageGetWidth(srcImage); 114 | size_t height = CGImageGetHeight(srcImage); 115 | if (width == 0 || height == 0) return NO; 116 | 117 | BOOL hasAlpha = NO; 118 | BOOL alphaFirst = NO; 119 | BOOL alphaPremultiplied = NO; 120 | BOOL byteOrderNormal = NO; 121 | 122 | switch (bitmapInfo & kCGBitmapAlphaInfoMask) { 123 | case kCGImageAlphaPremultipliedLast: { 124 | hasAlpha = YES; 125 | alphaPremultiplied = YES; 126 | } break; 127 | case kCGImageAlphaPremultipliedFirst: { 128 | hasAlpha = YES; 129 | alphaPremultiplied = YES; 130 | alphaFirst = YES; 131 | } break; 132 | case kCGImageAlphaLast: { 133 | hasAlpha = YES; 134 | } break; 135 | case kCGImageAlphaFirst: { 136 | hasAlpha = YES; 137 | alphaFirst = YES; 138 | } break; 139 | case kCGImageAlphaNoneSkipLast: { 140 | } break; 141 | case kCGImageAlphaNoneSkipFirst: { 142 | alphaFirst = YES; 143 | } break; 144 | default: { 145 | return NO; 146 | } break; 147 | } 148 | 149 | switch (bitmapInfo & kCGBitmapByteOrderMask) { 150 | case kCGBitmapByteOrderDefault: { 151 | byteOrderNormal = YES; 152 | } break; 153 | case kCGBitmapByteOrder32Little: { 154 | } break; 155 | case kCGBitmapByteOrder32Big: { 156 | byteOrderNormal = YES; 157 | } break; 158 | default: { 159 | return NO; 160 | } break; 161 | } 162 | 163 | /* 164 | Try convert with vImageConvert_AnyToAny() (avaliable since iOS 7.0). 165 | If fail, try decode with CGContextDrawImage(). 166 | CGBitmapContext use a premultiplied alpha format, unpremultiply may lose precision. 167 | */ 168 | vImage_CGImageFormat destFormat = {0}; 169 | destFormat.bitsPerComponent = 8; 170 | destFormat.bitsPerPixel = 32; 171 | destFormat.colorSpace = WebPColorSpaceForDeviceRGB(); 172 | destFormat.bitmapInfo = bitmapInfo; 173 | dest->data = NULL; 174 | if (WebPCGImageDecodeToBitmapBufferWithAnyFormat(srcImage, dest, &destFormat)) return YES; 175 | 176 | CGBitmapInfo contextBitmapInfo = bitmapInfo & kCGBitmapByteOrderMask; 177 | if (!hasAlpha || alphaPremultiplied) { 178 | contextBitmapInfo |= (bitmapInfo & kCGBitmapAlphaInfoMask); 179 | } else { 180 | contextBitmapInfo |= alphaFirst ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaPremultipliedLast; 181 | } 182 | CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, WebPColorSpaceForDeviceRGB(), contextBitmapInfo); 183 | if (!context) goto fail; 184 | 185 | CGContextDrawImage(context, CGRectMake(0, 0, width, height), srcImage); // decode and convert 186 | size_t bytesPerRow = CGBitmapContextGetBytesPerRow(context); 187 | size_t length = height * bytesPerRow; 188 | void *data = CGBitmapContextGetData(context); 189 | if (length == 0 || !data) goto fail; 190 | 191 | dest->data = malloc(length); 192 | dest->width = width; 193 | dest->height = height; 194 | dest->rowBytes = bytesPerRow; 195 | if (!dest->data) goto fail; 196 | 197 | if (hasAlpha && !alphaPremultiplied) { 198 | vImage_Buffer tmpSrc = {0}; 199 | tmpSrc.data = data; 200 | tmpSrc.width = width; 201 | tmpSrc.height = height; 202 | tmpSrc.rowBytes = bytesPerRow; 203 | vImage_Error error; 204 | if (alphaFirst && byteOrderNormal) { 205 | error = vImageUnpremultiplyData_ARGB8888(&tmpSrc, dest, kvImageNoFlags); 206 | } else { 207 | error = vImageUnpremultiplyData_RGBA8888(&tmpSrc, dest, kvImageNoFlags); 208 | } 209 | if (error != kvImageNoError) goto fail; 210 | } else { 211 | memcpy(dest->data, data, length); 212 | } 213 | 214 | CFRelease(context); 215 | return YES; 216 | 217 | fail: 218 | if (context) CFRelease(context); 219 | if (dest->data) free(dest->data); 220 | dest->data = NULL; 221 | return NO; 222 | return NO; 223 | } 224 | 225 | static int WebPPictureImportCGImage(WebPPicture *picture, CGImageRef image) { 226 | vImage_Buffer buffer = {0}; 227 | int result = 0; 228 | if (WebPCGImageDecodeToBitmapBufferWith32BitFormat(image, &buffer, kCGImageAlphaLast | kCGBitmapByteOrderDefault)) { 229 | picture->width = (int)buffer.width; 230 | picture->height = (int)buffer.height; 231 | picture->use_argb = 1; 232 | result = WebPPictureImportRGBA(picture, buffer.data, (int)buffer.rowBytes); 233 | free(buffer.data); 234 | } 235 | return result; 236 | } 237 | 238 | #pragma mark - Still Images 239 | 240 | CGImageRef WebPImageCreateWithData(CFDataRef webpData) { 241 | WebPData webp_data; 242 | WebPDataInit(&webp_data); 243 | webp_data.bytes = CFDataGetBytePtr(webpData); 244 | webp_data.size = CFDataGetLength(webpData); 245 | 246 | WebPAnimDecoderOptions dec_options; 247 | WebPAnimDecoderOptionsInit(&dec_options); 248 | dec_options.use_threads = 1; 249 | dec_options.color_mode = MODE_rgbA; 250 | 251 | WebPAnimDecoder *dec = WebPAnimDecoderNew(&webp_data, &dec_options); 252 | if (!dec) { 253 | return NULL; 254 | } 255 | 256 | WebPAnimInfo anim_info; 257 | uint8_t *buf; 258 | int timestamp; 259 | if (!WebPAnimDecoderGetInfo(dec, &anim_info) || !WebPAnimDecoderGetNext(dec, &buf, ×tamp)) { 260 | WebPAnimDecoderDelete(dec); 261 | return NULL; 262 | } 263 | 264 | const size_t bufSize = anim_info.canvas_width * 4 * anim_info.canvas_height; 265 | CFDataRef imageData = CFDataCreate(kCFAllocatorDefault, buf, bufSize); 266 | WebPAnimDecoderDelete(dec); 267 | if (!imageData) { 268 | return NULL; 269 | } 270 | 271 | CGDataProviderRef provider = CGDataProviderCreateWithCFData(imageData); 272 | CGImageRef image = CGImageCreate(anim_info.canvas_width, anim_info.canvas_height, 8, 32, anim_info.canvas_width * 4, WebPColorSpaceForDeviceRGB(), kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault, provider, NULL, false, kCGRenderingIntentDefault); 273 | CGDataProviderRelease(provider); 274 | CFRelease(imageData); 275 | 276 | return image; 277 | } 278 | 279 | CFDataRef WebPDataCreateWithImage(CGImageRef image, bool isLossy, float quality) { 280 | WebPConfig config; 281 | WebPConfigInit(&config); 282 | if (isLossy) { 283 | WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, quality); 284 | } else { 285 | WebPConfigLosslessPreset(&config, 0); 286 | } 287 | 288 | WebPPicture picture; 289 | WebPPictureInit(&picture); 290 | 291 | WebPMemoryWriter writer; 292 | WebPMemoryWriterInit(&writer); 293 | picture.writer = WebPMemoryWrite; 294 | picture.custom_ptr = &writer; 295 | 296 | if (!(WebPPictureImportCGImage(&picture, image))) { 297 | goto fail; 298 | } 299 | 300 | if (!WebPEncode(&config, &picture)) { 301 | goto fail; 302 | } 303 | 304 | CFDataRef data = CFDataCreate(kCFAllocatorDefault, writer.mem, writer.size); 305 | WebPMemoryWriterClear(&writer); 306 | WebPPictureFree(&picture); 307 | return data; 308 | 309 | fail: 310 | WebPMemoryWriterClear(&writer); 311 | WebPPictureFree(&picture); 312 | return NULL; 313 | } 314 | 315 | #pragma mark - Animated Images 316 | 317 | const CFStringRef kWebPAnimatedImageDuration = CFSTR("kWebPAnimatedImageDuration"); 318 | const CFStringRef kWebPAnimatedImageLoopCount = CFSTR("kWebPAnimatedImageLoopCount"); 319 | const CFStringRef kWebPAnimatedImageFrames = CFSTR("kWebPAnimatedImageFrames"); 320 | const CFStringRef kWebPAnimatedImageFrameDurations = CFSTR("kWebPAnimatedImageFrameDurations"); 321 | 322 | uint32_t WebPImageFrameCountGetFromData(CFDataRef webpData) { 323 | WebPData webp_data; 324 | WebPDataInit(&webp_data); 325 | webp_data.bytes = CFDataGetBytePtr(webpData); 326 | webp_data.size = CFDataGetLength(webpData); 327 | 328 | WebPDemuxer *dmux = WebPDemux(&webp_data); 329 | if (!dmux) { 330 | return 0; 331 | } 332 | 333 | uint32_t frameCount = WebPDemuxGetI(dmux, WEBP_FF_FRAME_COUNT); 334 | WebPDemuxDelete(dmux); 335 | 336 | return frameCount; 337 | } 338 | 339 | CFDictionaryRef WebPAnimatedImageInfoCreateWithData(CFDataRef webpData) { 340 | WebPData webp_data; 341 | WebPDataInit(&webp_data); 342 | webp_data.bytes = CFDataGetBytePtr(webpData); 343 | webp_data.size = CFDataGetLength(webpData); 344 | 345 | WebPAnimDecoderOptions dec_options; 346 | WebPAnimDecoderOptionsInit(&dec_options); 347 | dec_options.use_threads = 1; 348 | dec_options.color_mode = MODE_rgbA; 349 | 350 | WebPAnimDecoder *dec = WebPAnimDecoderNew(&webp_data, &dec_options); 351 | if (!dec) { 352 | return NULL; 353 | } 354 | 355 | WebPAnimInfo anim_info; 356 | if (!WebPAnimDecoderGetInfo(dec, &anim_info)) { 357 | WebPAnimDecoderDelete(dec); 358 | return NULL; 359 | } 360 | 361 | CFMutableDictionaryRef imageInfo = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); 362 | 363 | CFMutableArrayRef imageFrames = CFArrayCreateMutable(kCFAllocatorDefault, anim_info.frame_count, &kCFTypeArrayCallBacks); 364 | 365 | int duration = 0; 366 | while (WebPAnimDecoderHasMoreFrames(dec)) { 367 | uint8_t *buf; 368 | 369 | if (!WebPAnimDecoderGetNext(dec, &buf, &duration)) { 370 | break; 371 | } 372 | 373 | const size_t bufSize = anim_info.canvas_width * 4 * anim_info.canvas_height; 374 | CFDataRef imageData = CFDataCreate(kCFAllocatorDefault, buf, bufSize); 375 | if (!imageData) { 376 | break; 377 | } 378 | CGDataProviderRef provider = CGDataProviderCreateWithCFData(imageData); 379 | CGImageRef image = CGImageCreate(anim_info.canvas_width, anim_info.canvas_height, 8, 32, anim_info.canvas_width * 4, WebPColorSpaceForDeviceRGB(), kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault, provider, NULL, false, kCGRenderingIntentDefault); 380 | CFArrayAppendValue(imageFrames, image); 381 | CGImageRelease(image); 382 | CGDataProviderRelease(provider); 383 | CFRelease(imageData); 384 | } 385 | 386 | // add last frame's duration 387 | const WebPDemuxer *dmux = WebPAnimDecoderGetDemuxer(dec); 388 | WebPIterator iter; 389 | if (WebPDemuxGetFrame(dmux, 0, &iter)) { 390 | duration += iter.duration; 391 | WebPDemuxReleaseIterator(&iter); 392 | } 393 | WebPAnimDecoderDelete(dec); 394 | 395 | CFDictionarySetValue(imageInfo, kWebPAnimatedImageFrames, imageFrames); 396 | CFRelease(imageFrames); 397 | 398 | CFNumberRef loopCount = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &anim_info.loop_count); 399 | CFDictionarySetValue(imageInfo, kWebPAnimatedImageLoopCount, loopCount); 400 | CFRelease(loopCount); 401 | 402 | double durationInSec = ((double)duration) / 1000; 403 | CFNumberRef durationRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &durationInSec); 404 | CFDictionarySetValue(imageInfo, kWebPAnimatedImageDuration, durationRef); 405 | CFRelease(durationRef); 406 | 407 | return imageInfo; 408 | } 409 | 410 | 411 | 412 | CFDataRef WebPDataCreateWithAnimatedImageInfo(CFDictionaryRef imageInfo, bool isLossy, float quality) { 413 | CFNumberRef loopCount = CFDictionaryGetValue(imageInfo, kWebPAnimatedImageLoopCount); 414 | CFNumberRef durationRef = CFDictionaryGetValue(imageInfo, kWebPAnimatedImageDuration); 415 | CFArrayRef imageFrames = CFDictionaryGetValue(imageInfo, kWebPAnimatedImageFrames); 416 | CFArrayRef frameDurations = CFDictionaryGetValue(imageInfo, kWebPAnimatedImageFrameDurations); 417 | 418 | if (!imageFrames || CFArrayGetCount(imageFrames) < 1) { 419 | return NULL; 420 | } 421 | 422 | if (frameDurations && CFArrayGetCount(frameDurations) != CFArrayGetCount(imageFrames)) { 423 | return NULL; 424 | } 425 | 426 | WebPAnimEncoderOptions enc_options; 427 | WebPAnimEncoderOptionsInit(&enc_options); 428 | if (loopCount) { 429 | CFNumberGetValue(loopCount, kCFNumberSInt32Type, &enc_options.anim_params.loop_count); 430 | } 431 | 432 | CGImageRef firstImage = (CGImageRef)CFArrayGetValueAtIndex(imageFrames, 0); 433 | WebPAnimEncoder *enc = WebPAnimEncoderNew((int)CGImageGetWidth(firstImage), (int)CGImageGetHeight(firstImage), &enc_options); 434 | if (!enc) { 435 | return NULL; 436 | } 437 | 438 | int defaultDurationInMilliSec = 100; 439 | if (durationRef && !frameDurations) { 440 | double totalDurationInSec; 441 | CFNumberGetValue(durationRef, kCFNumberDoubleType, &totalDurationInSec); 442 | defaultDurationInMilliSec = (int)(totalDurationInSec * 1000 / CFArrayGetCount(imageFrames)); 443 | } 444 | 445 | int timestamp = 0; 446 | for (CFIndex i = 0; i < CFArrayGetCount(imageFrames); i ++) { 447 | WebPPicture frame; 448 | WebPPictureInit(&frame); 449 | if (WebPPictureImportCGImage(&frame, (CGImageRef)CFArrayGetValueAtIndex(imageFrames, i))) { 450 | WebPConfig config; 451 | WebPConfigInit(&config); 452 | if (isLossy) { 453 | WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, quality); 454 | } else { 455 | WebPConfigLosslessPreset(&config, 0); 456 | } 457 | WebPAnimEncoderAdd(enc, &frame, timestamp, &config); 458 | if (frameDurations) { 459 | CFNumberRef frameDuration = CFArrayGetValueAtIndex(frameDurations, i); 460 | double durationInSec = 0.1; 461 | CFNumberGetValue(frameDuration, kCFNumberDoubleType, &durationInSec); 462 | timestamp += (int)(durationInSec * 1000); 463 | } else { 464 | timestamp += defaultDurationInMilliSec; 465 | } 466 | } 467 | WebPPictureFree(&frame); 468 | } 469 | WebPAnimEncoderAdd(enc, NULL, timestamp, NULL); 470 | 471 | WebPData webp_data; 472 | WebPDataInit(&webp_data); 473 | WebPAnimEncoderAssemble(enc, &webp_data); 474 | WebPAnimEncoderDelete(enc); 475 | 476 | CFDataRef data = CFDataCreate(kCFAllocatorDefault, webp_data.bytes, webp_data.size); 477 | WebPDataClear(&webp_data); 478 | 479 | return data; 480 | } 481 | 482 | struct WebPImageDecoder { 483 | WebPAnimDecoder *dec; 484 | WebPData webpData; 485 | CFDataRef cfData; 486 | int currentIndex; 487 | }; 488 | 489 | WebPDecoderRef WebPDecoderCreateWithData(CFDataRef webpData) { 490 | WebPDecoderRef decoder = calloc(1, sizeof(struct WebPImageDecoder)); 491 | if (!decoder) { 492 | return NULL; 493 | } 494 | 495 | WebPAnimDecoderOptions dec_options; 496 | WebPAnimDecoderOptionsInit(&dec_options); 497 | dec_options.use_threads = 1; 498 | dec_options.color_mode = MODE_rgbA; 499 | 500 | decoder->webpData.bytes = (uint8_t *)CFDataGetBytePtr(webpData); 501 | decoder->webpData.size = CFDataGetLength(webpData); 502 | decoder->dec = WebPAnimDecoderNew(&decoder->webpData, &dec_options); 503 | 504 | if (!decoder->dec) { 505 | free(decoder); 506 | return NULL; 507 | } 508 | 509 | decoder->cfData = CFRetain(webpData); 510 | return decoder; 511 | } 512 | 513 | void WebPDecoderDestroy(WebPDecoderRef decoder) { 514 | WebPAnimDecoderDelete(decoder->dec); 515 | CFRelease(decoder->cfData); 516 | free(decoder); 517 | } 518 | 519 | uint32_t WebPDecoderGetFrameCount(WebPDecoderRef decoder) { 520 | WebPAnimInfo info; 521 | if (!WebPAnimDecoderGetInfo(decoder->dec, &info)) { 522 | return 0; 523 | } 524 | return info.frame_count; 525 | } 526 | 527 | CFTimeInterval WebPDecoderGetDurationAtIndex(WebPDecoderRef decoder, int index) { 528 | WebPIterator iter; 529 | const WebPDemuxer *demux = WebPAnimDecoderGetDemuxer(decoder->dec); 530 | if (!WebPDemuxGetFrame(demux, index + 1, &iter)) { 531 | return 0; 532 | } 533 | CFTimeInterval duration = iter.duration / 1000.0; 534 | WebPDemuxReleaseIterator(&iter); 535 | return duration; 536 | } 537 | 538 | // Returns true if the frame covers the full canvas. 539 | static int IsFullFrame(int width, int height, int canvas_width, 540 | int canvas_height) { 541 | return (width == canvas_width && height == canvas_height); 542 | } 543 | 544 | // Returns true if the current frame is a key-frame. 545 | static int IsKeyFrame(const WebPIterator* const curr, 546 | const WebPIterator* const prev, 547 | int canvas_width, int canvas_height) { 548 | if (curr->frame_num == 1) { 549 | return 1; 550 | } else if ((!curr->has_alpha || curr->blend_method == WEBP_MUX_NO_BLEND) && 551 | IsFullFrame(curr->width, curr->height, canvas_width, canvas_height)) { 552 | return 1; 553 | } else { 554 | return prev->dispose_method == WEBP_MUX_DISPOSE_BACKGROUND && 555 | IsFullFrame(prev->width, prev->height, canvas_width, canvas_height); 556 | } 557 | } 558 | 559 | CGImageRef WebPDecoderCopyImageAtIndex(WebPDecoderRef decoder, int index) { 560 | WebPAnimInfo info; 561 | if (!WebPAnimDecoderGetInfo(decoder->dec, &info)) { 562 | return NULL; 563 | } 564 | 565 | const size_t bufSize = info.canvas_width * info.canvas_height * 4; 566 | CFDataRef imageData = NULL; 567 | 568 | // decode directly if target index is key frame 569 | if (index > 0 && index != decoder->currentIndex + 1) { 570 | const WebPDemuxer *demux = WebPAnimDecoderGetDemuxer(decoder->dec); 571 | WebPIterator prev; 572 | if (!WebPDemuxGetFrame(demux, index, &prev)) { 573 | goto anim_decoder; 574 | } 575 | WebPIterator curr; 576 | if (!WebPDemuxGetFrame(demux, index + 1, &curr)) { 577 | WebPDemuxReleaseIterator(&prev); 578 | goto anim_decoder; 579 | } 580 | int is_key_frame = IsKeyFrame(&curr, &prev, info.canvas_width, info.canvas_height); 581 | if (is_key_frame) { 582 | int width, height; 583 | uint8_t *buffer = WebPDecodeRGBA(curr.fragment.bytes, curr.fragment.size, &width, &height); 584 | if (width != info.canvas_width || height != info.canvas_height) { 585 | free(buffer); // fallback 586 | } else { 587 | imageData = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, buffer, bufSize, kCFAllocatorDefault); 588 | } 589 | } 590 | WebPDemuxReleaseIterator(&prev); 591 | WebPDemuxReleaseIterator(&curr); 592 | } 593 | 594 | anim_decoder: 595 | if (!imageData) { 596 | // In animated webp images, a single frame may blend with the previous one. To ensure that we get 597 | // the correct image, we decode not only the current frame but also all of its predecessors. While 598 | // this approach may be slow for random index access, it is performant in Kingfisher scenarios as it 599 | // is typical for frames to be accessed continuously. 600 | uint8_t *buf; 601 | int duration; 602 | if (index == 0 || decoder->currentIndex >= index) { 603 | WebPAnimDecoderReset(decoder->dec); 604 | decoder->currentIndex = 0; 605 | if (!WebPAnimDecoderGetNext(decoder->dec, &buf, &duration)) { 606 | return NULL; 607 | } 608 | } 609 | while (decoder->currentIndex < index) { 610 | if (!WebPAnimDecoderGetNext(decoder->dec, &buf, &duration)) { 611 | return NULL; 612 | } 613 | decoder->currentIndex ++; 614 | } 615 | imageData = CFDataCreate(kCFAllocatorDefault, buf, bufSize); 616 | } 617 | if (!imageData) { 618 | return NULL; 619 | } 620 | CGDataProviderRef provider = CGDataProviderCreateWithCFData(imageData); 621 | CGImageRef image = CGImageCreate(info.canvas_width, info.canvas_height, 8, 32, info.canvas_width * 4, CGColorSpaceCreateDeviceRGB(), kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault, provider, NULL, false, kCGRenderingIntentDefault); 622 | CGDataProviderRelease(provider); 623 | CFRelease(imageData); 624 | return image; 625 | } 626 | -------------------------------------------------------------------------------- /Sources/KingfisherWebP-ObjC/include/CGImage+WebP.h: -------------------------------------------------------------------------------- 1 | // 2 | // CGImage+WebP.h 3 | // Pods 4 | // 5 | // Created by yeatse on 2016/10/20. 6 | // 7 | // 8 | 9 | #import 10 | 11 | CF_IMPLICIT_BRIDGING_ENABLED 12 | CF_ASSUME_NONNULL_BEGIN 13 | 14 | // still image 15 | CGImageRef __nullable WebPImageCreateWithData(CFDataRef webpData); 16 | CFDataRef __nullable WebPDataCreateWithImage(CGImageRef image, bool isLossy, float quality); 17 | 18 | // animated image 19 | CG_EXTERN const CFStringRef kWebPAnimatedImageDuration; 20 | CG_EXTERN const CFStringRef kWebPAnimatedImageLoopCount; 21 | CG_EXTERN const CFStringRef kWebPAnimatedImageFrames; // CFArrayRef of CGImageRef 22 | CG_EXTERN const CFStringRef kWebPAnimatedImageFrameDurations; // CFArrayRef of CFNumberRef 23 | 24 | uint32_t WebPImageFrameCountGetFromData(CFDataRef webpData); 25 | CFDictionaryRef __nullable WebPAnimatedImageInfoCreateWithData(CFDataRef webpData); 26 | CFDataRef __nullable WebPDataCreateWithAnimatedImageInfo(CFDictionaryRef imageInfo, bool isLossy, float quality); 27 | 28 | // accumulative decoding 29 | typedef struct WebPImageDecoder *WebPDecoderRef; 30 | 31 | WebPDecoderRef __nullable WebPDecoderCreateWithData(CFDataRef webpData); 32 | void WebPDecoderDestroy(WebPDecoderRef decoder); 33 | 34 | uint32_t WebPDecoderGetFrameCount(WebPDecoderRef decoder); 35 | CFTimeInterval WebPDecoderGetDurationAtIndex(WebPDecoderRef decoder, int index); 36 | CGImageRef __nullable WebPDecoderCopyImageAtIndex(WebPDecoderRef decoder, int index); 37 | 38 | CF_ASSUME_NONNULL_END 39 | CF_IMPLICIT_BRIDGING_DISABLED 40 | -------------------------------------------------------------------------------- /Sources/KingfisherWebP.h: -------------------------------------------------------------------------------- 1 | // 2 | // KingfisherWebP.h 3 | // KingfisherWebP 4 | // 5 | // Created by yeatse on 2020/12/22. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for KingfisherWebP. 11 | FOUNDATION_EXPORT double KingfisherWebPVersionNumber; 12 | 13 | //! Project version string for KingfisherWebP. 14 | FOUNDATION_EXPORT const unsigned char KingfisherWebPVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | #import 18 | -------------------------------------------------------------------------------- /Sources/WebPProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebPProcessor.swift 3 | // Pods 4 | // 5 | // Created by yeatse on 2016/10/19. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Kingfisher 11 | 12 | public struct WebPProcessor: ImageProcessor { 13 | 14 | public static let `default` = WebPProcessor() 15 | 16 | public let identifier = "com.yeatse.WebPProcessor" 17 | 18 | public init() {} 19 | 20 | public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { 21 | switch item { 22 | case .image(let image): 23 | return image 24 | case .data(let data): 25 | if data.isWebPFormat { 26 | let creatingOptions = ImageCreatingOptions(scale: options.scaleFactor, preloadAll: options.preloadAllAnimationData, onlyFirstFrame: options.onlyLoadFirstFrame) 27 | return KingfisherWrapper.image(webpData: data, options: creatingOptions) 28 | } else { 29 | return DefaultImageProcessor.default.process(item: item, options: options) 30 | } 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Sources/WebPSerializer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebPSerializer.swift 3 | // Pods 4 | // 5 | // Created by yeatse on 2016/10/20. 6 | // 7 | // 8 | 9 | import CoreGraphics 10 | import Foundation 11 | import Kingfisher 12 | 13 | public struct WebPSerializer: CacheSerializer { 14 | public static let `default` = WebPSerializer() 15 | 16 | /// Whether the image should be serialized in a lossy format. Default is false. 17 | public var isLossy: Bool = false 18 | 19 | /// The compression quality when converting image to a lossy format data. Default is 1.0. 20 | public var compressionQuality: CGFloat = 1.0 21 | 22 | /// See ```CacheSerializer/originalDataUsed``` 23 | public var originalDataUsed: Bool = true 24 | 25 | private init() {} 26 | 27 | public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { 28 | if originalDataUsed { 29 | if let original = original { 30 | return original 31 | } 32 | if let frameData = image.kf.frameSource?.data { 33 | return frameData 34 | } 35 | } 36 | if let original = original, !original.isWebPFormat { 37 | return DefaultCacheSerializer.default.data(with: image, original: original) 38 | } 39 | let qualityInWebp = min(max(0, compressionQuality), 1) * 100 40 | return image.kf.normalized.kf.webpRepresentation(isLossy: isLossy, quality: Float(qualityInWebp)) 41 | } 42 | 43 | public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { 44 | return WebPProcessor.default.process(item: .data(data), options: options) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/KingfisherWebPTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KingfisherWebPTests.swift 3 | // KingfisherWebPTests 4 | // 5 | // Created by yeatse on 2020/12/22. 6 | // 7 | #if os(macOS) 8 | import AppKit 9 | #else 10 | import UIKit 11 | #endif 12 | import ImageIO 13 | 14 | import XCTest 15 | import Kingfisher 16 | @testable import KingfisherWebP 17 | 18 | class KingfisherWebPTests: XCTestCase { 19 | let fileNames = ["cover.png", "kingfisher.jpg", "logo.png", "animation.gif", "heart.png"] 20 | let animationFileNames = ["animation.gif", "heart.png"] 21 | 22 | override func setUp() { 23 | super.setUp() 24 | // Put setup code here. This method is called before the invocation of each test method in the class. 25 | } 26 | 27 | override func tearDown() { 28 | // Put teardown code here. This method is called after the invocation of each test method in the class. 29 | super.tearDown() 30 | } 31 | 32 | func testSingleFrameDecoding() { 33 | let p = WebPProcessor.default 34 | XCTAssertEqual(p.identifier, "com.yeatse.WebPProcessor") 35 | 36 | fileNames.forEach { fileName in 37 | let webpData = Data(fileName: (fileName as NSString).deletingPathExtension, extension: "webp") 38 | let decodedWebP = p.process(item: .data(webpData), options: .init([.onlyLoadFirstFrame])) 39 | XCTAssertNotNil(decodedWebP, fileName) 40 | 41 | let originalData = Data(fileName: fileName) 42 | let originalImage = KFCrossPlatformImage(data: originalData)! 43 | XCTAssertTrue(decodedWebP!.renderEqual(to: originalImage), fileName) 44 | } 45 | } 46 | 47 | #if os(macOS) 48 | func testMultipleFramesDecoding() { 49 | let p = WebPProcessor.default 50 | 51 | animationFileNames.forEach { fileName in 52 | let webpData = Data(fileName: (fileName as NSString).deletingPathExtension, extension: "webp") 53 | let decodedWebP = p.process(item: .data(webpData), options: .init([])) 54 | XCTAssertNotNil(decodedWebP, fileName) 55 | 56 | let originalData = Data(fileName: fileName) 57 | let originalImage = KingfisherWrapper.animatedImage(data: originalData, options: .init()) 58 | 59 | XCTAssertEqual(decodedWebP?.kf.imageFrameCount, originalImage?.kf.imageFrameCount) 60 | XCTAssertTrue(decodedWebP!.renderEqual(to: originalImage!), "The first frame should be equal") 61 | } 62 | } 63 | #else 64 | func testMultipleFramesDecoding() { 65 | let p = WebPProcessor.default 66 | 67 | animationFileNames.forEach { fileName in 68 | let webpData = Data(fileName: (fileName as NSString).deletingPathExtension, extension: "webp") 69 | let decodedWebP = p.process(item: .data(webpData), options: .init([.preloadAllAnimationData])) 70 | XCTAssertNotNil(decodedWebP, fileName) 71 | 72 | let originalData = Data(fileName: fileName) 73 | let originalImage = KingfisherWrapper.animatedImage(data: originalData, options: .init(preloadAll: true)) 74 | 75 | XCTAssertTrue(decodedWebP?.images?.count == originalImage?.images?.count, fileName) 76 | XCTAssertEqual(decodedWebP?.kf.imageFrameCount, originalImage?.kf.imageFrameCount) 77 | 78 | decodedWebP?.images?.enumerated().forEach { (index, frame) in 79 | let originalFrame = originalImage!.images![index] 80 | XCTAssertTrue(frame.renderEqual(to: originalFrame), "Frame \(index) of \(fileName) should be equal") 81 | } 82 | } 83 | } 84 | 85 | func testAccumulativeFrameDecoding() { 86 | let p = WebPProcessor.default 87 | 88 | animationFileNames.forEach { fileName in 89 | let webpData = Data(fileName: (fileName as NSString).deletingPathExtension, extension: "webp") 90 | let decodedWebP = p.process(item: .data(webpData), options: .init([]))! 91 | XCTAssertNil(decodedWebP.images, "The images array should be nil") 92 | let webpImageSource = decodedWebP.kf.frameSource! 93 | 94 | let originalData = Data(fileName: fileName) 95 | let originImageSource = CGImageSourceCreateWithData(originalData as CFData, nil)! 96 | 97 | XCTAssertEqual(webpImageSource.frameCount, CGImageSourceGetCount(originImageSource), "Frame count should be equal") 98 | for index in 0.. TimeInterval { return 0 } 223 | func frame(at index: Int, maxSize: CGSize?) -> CGImage? { 224 | KFCrossPlatformImage(data: .init(fileName: "cover.png"))?.kfCGImage 225 | } 226 | } 227 | 228 | let source = RandomFrameSource() 229 | let image = KingfisherWrapper.animatedImage(source: source, options: .init())! 230 | let encoded2 = s.data(with: image, original: nil) 231 | XCTAssertEqual(encoded2, source.data, "Original data should be used") 232 | } 233 | 234 | func testEncodingPerformance() { 235 | let s = WebPSerializer.default 236 | let images = fileNames.compactMap { fileName -> KFCrossPlatformImage? in 237 | let data = Data(fileName: fileName) 238 | return DefaultImageProcessor.default.process(item: .data(data), options: .init([])) 239 | } 240 | 241 | measure { 242 | images.forEach { 243 | let _ = s.data(with: $0, original: nil) 244 | } 245 | } 246 | } 247 | 248 | func testDecodingperformance() { 249 | let p = WebPProcessor.default 250 | let dataList = fileNames.compactMap { (fileName) -> Data? in 251 | return Data(fileName: (fileName as NSString).deletingPathExtension, extension: "webp") 252 | } 253 | measure { 254 | dataList.forEach { 255 | let _ = p.process(item: .data($0), options: .init([])) 256 | } 257 | } 258 | } 259 | } 260 | 261 | // MARK: - Helper 262 | extension Data { 263 | init(fileName: String, extension: String? = nil) { 264 | let url = Bundle(for: KingfisherWebPTests.self).url(forResource: fileName, withExtension: `extension`)! 265 | try! self.init(contentsOf: url) 266 | } 267 | } 268 | 269 | extension CGImageSource { 270 | func duration(at index: Int) -> TimeInterval { 271 | let properties = CGImageSourceCopyPropertiesAtIndex(self, index, nil) as! [CFString: Any] 272 | let result: TimeInterval 273 | if properties[kCGImagePropertyGIFDictionary] != nil { 274 | let subprop = properties[kCGImagePropertyGIFDictionary] as! [CFString: Any] 275 | result = subprop[kCGImagePropertyGIFUnclampedDelayTime] as! TimeInterval 276 | } else if properties[kCGImagePropertyPNGDictionary] != nil { 277 | let subprop = properties[kCGImagePropertyPNGDictionary] as! [CFString: Any] 278 | result = subprop[kCGImagePropertyAPNGUnclampedDelayTime] as! TimeInterval 279 | } else { 280 | result = 0.1 281 | } 282 | return round(result * 1000) / 1000 283 | } 284 | } 285 | 286 | // Copied from Kingfisher project 287 | extension KFCrossPlatformImage { 288 | var kfCGImage: CGImage? { 289 | #if os(macOS) 290 | return cgImage(forProposedRect: nil, context: nil, hints: nil) 291 | #else 292 | return cgImage 293 | #endif 294 | } 295 | 296 | func renderEqual(to image: KFCrossPlatformImage, withinTolerance tolerance: UInt8 = 3, tolerancePercent: Double = 0) -> Bool { 297 | guard size == image.size else { return false } 298 | #if os(macOS) 299 | let pngRep = { (image: KFCrossPlatformImage) -> Data? in 300 | let rep = self.kfCGImage.map { NSBitmapImageRep(cgImage: $0) } 301 | return rep?.representation(using: .png, properties: [:]) 302 | } 303 | guard let imageData1 = pngRep(self), let imageData2 = pngRep(image) else { return false } 304 | #else 305 | guard let imageData1 = pngData(), let imageData2 = image.pngData() else { return false } 306 | #endif 307 | guard let unifiedImage1 = KFCrossPlatformImage(data: imageData1), let unifiedImage2 = KFCrossPlatformImage(data: imageData2) else { return false } 308 | 309 | guard let rendered1 = unifiedImage1.rendered(), let rendered2 = unifiedImage2.rendered() else { return false } 310 | guard let data1 = rendered1.kfCGImage?.dataProvider?.data, let data2 = rendered2.kfCGImage?.dataProvider?.data else { return false } 311 | 312 | let length1 = CFDataGetLength(data1) 313 | let length2 = CFDataGetLength(data2) 314 | guard length1 == length2 else { return false } 315 | 316 | let dataPtr1: UnsafePointer = CFDataGetBytePtr(data1) 317 | let dataPtr2: UnsafePointer = CFDataGetBytePtr(data2) 318 | 319 | 320 | var dismatchedLength = 0; 321 | 322 | for index in 0.. tolerance { 328 | dismatchedLength += 1 329 | } 330 | } 331 | 332 | return dismatchedLength <= Int(tolerancePercent * Double(length1)) 333 | } 334 | 335 | func rendered() -> KFCrossPlatformImage? { 336 | // Ignore non CG images 337 | guard let cgImage = kfCGImage else { 338 | return nil 339 | } 340 | 341 | var bitmapInfo = cgImage.bitmapInfo 342 | let colorSpace = CGColorSpaceCreateDeviceRGB() 343 | let alpha = (bitmapInfo.rawValue & CGBitmapInfo.alphaInfoMask.rawValue) 344 | 345 | let w = cgImage.width 346 | let h = cgImage.height 347 | 348 | let size = CGSize(width: w, height: h) 349 | 350 | if alpha == CGImageAlphaInfo.none.rawValue { 351 | bitmapInfo.remove(.alphaInfoMask) 352 | bitmapInfo = CGBitmapInfo(rawValue: bitmapInfo.rawValue | CGImageAlphaInfo.noneSkipFirst.rawValue) 353 | } else if !(alpha == CGImageAlphaInfo.noneSkipFirst.rawValue) || !(alpha == CGImageAlphaInfo.noneSkipLast.rawValue) { 354 | bitmapInfo.remove(.alphaInfoMask) 355 | bitmapInfo = CGBitmapInfo(rawValue: bitmapInfo.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) 356 | } 357 | 358 | // Render the image 359 | guard let context = CGContext(data: nil, 360 | width: w, 361 | height: h, 362 | bitsPerComponent: cgImage.bitsPerComponent, 363 | bytesPerRow: 0, 364 | space: colorSpace, 365 | bitmapInfo: bitmapInfo.rawValue) else 366 | { 367 | return nil 368 | } 369 | 370 | context.draw(cgImage, in: CGRect(origin: CGPoint.zero, size: size)) 371 | 372 | #if os(macOS) 373 | return context.makeImage().flatMap { KFCrossPlatformImage(cgImage: $0, size: .zero) } 374 | #else 375 | return context.makeImage().flatMap { KFCrossPlatformImage(cgImage: $0) } 376 | #endif 377 | } 378 | } 379 | 380 | -------------------------------------------------------------------------------- /Tests/Resources/animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeatse/KingfisherWebP/c064b8ae5d2c85dc7d27b1049151f0e0b51091e3/Tests/Resources/animation.gif -------------------------------------------------------------------------------- /Tests/Resources/animation.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeatse/KingfisherWebP/c064b8ae5d2c85dc7d27b1049151f0e0b51091e3/Tests/Resources/animation.webp -------------------------------------------------------------------------------- /Tests/Resources/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeatse/KingfisherWebP/c064b8ae5d2c85dc7d27b1049151f0e0b51091e3/Tests/Resources/cover.png -------------------------------------------------------------------------------- /Tests/Resources/cover.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeatse/KingfisherWebP/c064b8ae5d2c85dc7d27b1049151f0e0b51091e3/Tests/Resources/cover.webp -------------------------------------------------------------------------------- /Tests/Resources/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeatse/KingfisherWebP/c064b8ae5d2c85dc7d27b1049151f0e0b51091e3/Tests/Resources/heart.png -------------------------------------------------------------------------------- /Tests/Resources/heart.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeatse/KingfisherWebP/c064b8ae5d2c85dc7d27b1049151f0e0b51091e3/Tests/Resources/heart.webp -------------------------------------------------------------------------------- /Tests/Resources/kingfisher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeatse/KingfisherWebP/c064b8ae5d2c85dc7d27b1049151f0e0b51091e3/Tests/Resources/kingfisher.jpg -------------------------------------------------------------------------------- /Tests/Resources/kingfisher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeatse/KingfisherWebP/c064b8ae5d2c85dc7d27b1049151f0e0b51091e3/Tests/Resources/kingfisher.webp -------------------------------------------------------------------------------- /Tests/Resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeatse/KingfisherWebP/c064b8ae5d2c85dc7d27b1049151f0e0b51091e3/Tests/Resources/logo.png -------------------------------------------------------------------------------- /Tests/Resources/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeatse/KingfisherWebP/c064b8ae5d2c85dc7d27b1049151f0e0b51091e3/Tests/Resources/logo.webp -------------------------------------------------------------------------------- /spm_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeatse/KingfisherWebP/c064b8ae5d2c85dc7d27b1049151f0e0b51091e3/spm_screenshot.png --------------------------------------------------------------------------------