├── .github └── workflows │ └── build.yml ├── .gitignore ├── FastImage.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── FastImage.xcscheme │ └── FastImageTests.xcscheme ├── FastImage ├── DataExtensions.swift ├── FastImage.h ├── FastImage.swift ├── Info.plist └── Size Decoders │ ├── BMPSizeDecoder.swift │ ├── CURSizeDecoder.swift │ ├── Exif.swift │ ├── GIFSizeDecoder.swift │ ├── ICOSizeDecoder.swift │ ├── ImageSizeDecoder.swift │ ├── JPGSizeDecoder.swift │ ├── PNGSizeDecoder.swift │ ├── PSDSizeDecoder.swift │ └── TIFFSizeDecoder.swift ├── FastImageTests ├── DataTests.swift ├── FastImageTests.swift └── Info.plist ├── LICENSE └── README.md /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: macOS-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Build and test 13 | run: | 14 | export DEVELOPER_DIR=/Applications/Xcode_10.3.app/Contents/Developer 15 | set -o pipefail && xcodebuild -project FastImage.xcodeproj -scheme FastImage -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 8,OS=12.4' build test | xcpretty 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Xcode 4 | # 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 | *.moved-aside 17 | DerivedData 18 | *.hmap 19 | *.ipa 20 | *.xcuserstate 21 | 22 | # CocoaPods 23 | # 24 | # We recommend against adding the Pods directory to your .gitignore. However 25 | # you should judge for yourself, the pros and cons are mentioned at: 26 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 27 | # 28 | Pods/ 29 | -------------------------------------------------------------------------------- /FastImage.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 27B63D652249AB5A004BCC5B /* DataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B63D642249AB5A004BCC5B /* DataTests.swift */; }; 11 | 27CF9392223352940093543E /* FastImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 27CF9390223352940093543E /* FastImage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | 27CF9397223352C20093543E /* FastImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CF9396223352C20093543E /* FastImage.swift */; }; 13 | 27CF939922335E490093543E /* FastImageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CF939822335E490093543E /* FastImageTests.swift */; }; 14 | 27CF939B22335ED90093543E /* FastImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CF938E223352940093543E /* FastImage.framework */; }; 15 | 27EDB84C2245EC84008406EB /* DataExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EDB84B2245EC84008406EB /* DataExtensions.swift */; }; 16 | 27EDB84F2245EEB7008406EB /* ImageSizeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EDB84E2245EEB7008406EB /* ImageSizeDecoder.swift */; }; 17 | 27EDB8512245EF4E008406EB /* PNGSizeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EDB8502245EF4E008406EB /* PNGSizeDecoder.swift */; }; 18 | 27EDB8532245EF9E008406EB /* GIFSizeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EDB8522245EF9E008406EB /* GIFSizeDecoder.swift */; }; 19 | 27EDB8552245EFD0008406EB /* JPGSizeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EDB8542245EFD0008406EB /* JPGSizeDecoder.swift */; }; 20 | 27EDB8572245EFE4008406EB /* BMPSizeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EDB8562245EFE4008406EB /* BMPSizeDecoder.swift */; }; 21 | 27EDB8592245EFFB008406EB /* TIFFSizeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EDB8582245EFFB008406EB /* TIFFSizeDecoder.swift */; }; 22 | 27EDB85B2245F05B008406EB /* Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EDB85A2245F05B008406EB /* Exif.swift */; }; 23 | 27EDB85D2245F8E0008406EB /* PSDSizeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EDB85C2245F8E0008406EB /* PSDSizeDecoder.swift */; }; 24 | 27EDB85F2245FC5C008406EB /* ICOSizeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EDB85E2245FC5C008406EB /* ICOSizeDecoder.swift */; }; 25 | 27EDB8612245FF55008406EB /* CURSizeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EDB8602245FF55008406EB /* CURSizeDecoder.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 27B63D642249AB5A004BCC5B /* DataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTests.swift; sourceTree = ""; }; 30 | 27CF938E223352940093543E /* FastImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FastImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | 27CF9390223352940093543E /* FastImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FastImage.h; sourceTree = ""; }; 32 | 27CF9391223352940093543E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 33 | 27CF9396223352C20093543E /* FastImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastImage.swift; sourceTree = ""; }; 34 | 27CF939822335E490093543E /* FastImageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastImageTests.swift; sourceTree = ""; }; 35 | 27EDB84B2245EC84008406EB /* DataExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtensions.swift; sourceTree = ""; }; 36 | 27EDB84E2245EEB7008406EB /* ImageSizeDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSizeDecoder.swift; sourceTree = ""; }; 37 | 27EDB8502245EF4E008406EB /* PNGSizeDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNGSizeDecoder.swift; sourceTree = ""; }; 38 | 27EDB8522245EF9E008406EB /* GIFSizeDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFSizeDecoder.swift; sourceTree = ""; }; 39 | 27EDB8542245EFD0008406EB /* JPGSizeDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JPGSizeDecoder.swift; sourceTree = ""; }; 40 | 27EDB8562245EFE4008406EB /* BMPSizeDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BMPSizeDecoder.swift; sourceTree = ""; }; 41 | 27EDB8582245EFFB008406EB /* TIFFSizeDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TIFFSizeDecoder.swift; sourceTree = ""; }; 42 | 27EDB85A2245F05B008406EB /* Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Exif.swift; sourceTree = ""; }; 43 | 27EDB85C2245F8E0008406EB /* PSDSizeDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PSDSizeDecoder.swift; sourceTree = ""; }; 44 | 27EDB85E2245FC5C008406EB /* ICOSizeDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICOSizeDecoder.swift; sourceTree = ""; }; 45 | 27EDB8602245FF55008406EB /* CURSizeDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CURSizeDecoder.swift; sourceTree = ""; }; 46 | A1B48FD51A0DD89F001BB364 /* FastImageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FastImageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | A1B48FDB1A0DD89F001BB364 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | /* End PBXFileReference section */ 49 | 50 | /* Begin PBXFrameworksBuildPhase section */ 51 | 27CF938B223352940093543E /* Frameworks */ = { 52 | isa = PBXFrameworksBuildPhase; 53 | buildActionMask = 2147483647; 54 | files = ( 55 | ); 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | A1B48FD21A0DD89F001BB364 /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | 27CF939B22335ED90093543E /* FastImage.framework in Frameworks */, 63 | ); 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | /* End PBXFrameworksBuildPhase section */ 67 | 68 | /* Begin PBXGroup section */ 69 | 27CF939A22335ED90093543E /* Frameworks */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | ); 73 | name = Frameworks; 74 | sourceTree = ""; 75 | }; 76 | 27EDB84D2245EDF9008406EB /* Size Decoders */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 27EDB84E2245EEB7008406EB /* ImageSizeDecoder.swift */, 80 | 27EDB8502245EF4E008406EB /* PNGSizeDecoder.swift */, 81 | 27EDB8522245EF9E008406EB /* GIFSizeDecoder.swift */, 82 | 27EDB8562245EFE4008406EB /* BMPSizeDecoder.swift */, 83 | 27EDB8542245EFD0008406EB /* JPGSizeDecoder.swift */, 84 | 27EDB8582245EFFB008406EB /* TIFFSizeDecoder.swift */, 85 | 27EDB85C2245F8E0008406EB /* PSDSizeDecoder.swift */, 86 | 27EDB85E2245FC5C008406EB /* ICOSizeDecoder.swift */, 87 | 27EDB8602245FF55008406EB /* CURSizeDecoder.swift */, 88 | 27EDB85A2245F05B008406EB /* Exif.swift */, 89 | ); 90 | path = "Size Decoders"; 91 | sourceTree = ""; 92 | }; 93 | A1B48FC11A0DD89F001BB364 = { 94 | isa = PBXGroup; 95 | children = ( 96 | A1B48FCC1A0DD89F001BB364 /* FastImage */, 97 | A1B48FD91A0DD89F001BB364 /* FastImageTests */, 98 | A1B48FCB1A0DD89F001BB364 /* Products */, 99 | 27CF939A22335ED90093543E /* Frameworks */, 100 | ); 101 | sourceTree = ""; 102 | }; 103 | A1B48FCB1A0DD89F001BB364 /* Products */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | A1B48FD51A0DD89F001BB364 /* FastImageTests.xctest */, 107 | 27CF938E223352940093543E /* FastImage.framework */, 108 | ); 109 | name = Products; 110 | sourceTree = ""; 111 | }; 112 | A1B48FCC1A0DD89F001BB364 /* FastImage */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 27EDB84D2245EDF9008406EB /* Size Decoders */, 116 | 27CF9390223352940093543E /* FastImage.h */, 117 | 27CF9396223352C20093543E /* FastImage.swift */, 118 | 27EDB84B2245EC84008406EB /* DataExtensions.swift */, 119 | 27CF9391223352940093543E /* Info.plist */, 120 | ); 121 | path = FastImage; 122 | sourceTree = ""; 123 | }; 124 | A1B48FD91A0DD89F001BB364 /* FastImageTests */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 27CF939822335E490093543E /* FastImageTests.swift */, 128 | 27B63D642249AB5A004BCC5B /* DataTests.swift */, 129 | A1B48FDA1A0DD89F001BB364 /* Supporting Files */, 130 | ); 131 | path = FastImageTests; 132 | sourceTree = ""; 133 | }; 134 | A1B48FDA1A0DD89F001BB364 /* Supporting Files */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | A1B48FDB1A0DD89F001BB364 /* Info.plist */, 138 | ); 139 | name = "Supporting Files"; 140 | sourceTree = ""; 141 | }; 142 | /* End PBXGroup section */ 143 | 144 | /* Begin PBXHeadersBuildPhase section */ 145 | 27CF9389223352940093543E /* Headers */ = { 146 | isa = PBXHeadersBuildPhase; 147 | buildActionMask = 2147483647; 148 | files = ( 149 | 27CF9392223352940093543E /* FastImage.h in Headers */, 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | /* End PBXHeadersBuildPhase section */ 154 | 155 | /* Begin PBXNativeTarget section */ 156 | 27CF938D223352940093543E /* FastImage */ = { 157 | isa = PBXNativeTarget; 158 | buildConfigurationList = 27CF9393223352940093543E /* Build configuration list for PBXNativeTarget "FastImage" */; 159 | buildPhases = ( 160 | 27CF9389223352940093543E /* Headers */, 161 | 27CF938A223352940093543E /* Sources */, 162 | 27CF938B223352940093543E /* Frameworks */, 163 | 27CF938C223352940093543E /* Resources */, 164 | ); 165 | buildRules = ( 166 | ); 167 | dependencies = ( 168 | ); 169 | name = FastImage; 170 | productName = FastImage; 171 | productReference = 27CF938E223352940093543E /* FastImage.framework */; 172 | productType = "com.apple.product-type.framework"; 173 | }; 174 | A1B48FD41A0DD89F001BB364 /* FastImageTests */ = { 175 | isa = PBXNativeTarget; 176 | buildConfigurationList = A1B48FE11A0DD89F001BB364 /* Build configuration list for PBXNativeTarget "FastImageTests" */; 177 | buildPhases = ( 178 | A1B48FD11A0DD89F001BB364 /* Sources */, 179 | A1B48FD21A0DD89F001BB364 /* Frameworks */, 180 | A1B48FD31A0DD89F001BB364 /* Resources */, 181 | ); 182 | buildRules = ( 183 | ); 184 | dependencies = ( 185 | ); 186 | name = FastImageTests; 187 | productName = FastImageTests; 188 | productReference = A1B48FD51A0DD89F001BB364 /* FastImageTests.xctest */; 189 | productType = "com.apple.product-type.bundle.unit-test"; 190 | }; 191 | /* End PBXNativeTarget section */ 192 | 193 | /* Begin PBXProject section */ 194 | A1B48FC21A0DD89F001BB364 /* Project object */ = { 195 | isa = PBXProject; 196 | attributes = { 197 | LastUpgradeCheck = 1020; 198 | ORGANIZATIONNAME = "Kyle Hickinson"; 199 | TargetAttributes = { 200 | 27CF938D223352940093543E = { 201 | CreatedOnToolsVersion = 10.1; 202 | LastSwiftMigration = 1020; 203 | ProvisioningStyle = Automatic; 204 | }; 205 | A1B48FD41A0DD89F001BB364 = { 206 | CreatedOnToolsVersion = 6.1; 207 | LastSwiftMigration = 1020; 208 | }; 209 | }; 210 | }; 211 | buildConfigurationList = A1B48FC51A0DD89F001BB364 /* Build configuration list for PBXProject "FastImage" */; 212 | compatibilityVersion = "Xcode 3.2"; 213 | developmentRegion = en; 214 | hasScannedForEncodings = 0; 215 | knownRegions = ( 216 | en, 217 | Base, 218 | ); 219 | mainGroup = A1B48FC11A0DD89F001BB364; 220 | productRefGroup = A1B48FCB1A0DD89F001BB364 /* Products */; 221 | projectDirPath = ""; 222 | projectRoot = ""; 223 | targets = ( 224 | 27CF938D223352940093543E /* FastImage */, 225 | A1B48FD41A0DD89F001BB364 /* FastImageTests */, 226 | ); 227 | }; 228 | /* End PBXProject section */ 229 | 230 | /* Begin PBXResourcesBuildPhase section */ 231 | 27CF938C223352940093543E /* Resources */ = { 232 | isa = PBXResourcesBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | ); 236 | runOnlyForDeploymentPostprocessing = 0; 237 | }; 238 | A1B48FD31A0DD89F001BB364 /* Resources */ = { 239 | isa = PBXResourcesBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | ); 243 | runOnlyForDeploymentPostprocessing = 0; 244 | }; 245 | /* End PBXResourcesBuildPhase section */ 246 | 247 | /* Begin PBXSourcesBuildPhase section */ 248 | 27CF938A223352940093543E /* Sources */ = { 249 | isa = PBXSourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | 27EDB84F2245EEB7008406EB /* ImageSizeDecoder.swift in Sources */, 253 | 27EDB8512245EF4E008406EB /* PNGSizeDecoder.swift in Sources */, 254 | 27EDB8592245EFFB008406EB /* TIFFSizeDecoder.swift in Sources */, 255 | 27EDB85F2245FC5C008406EB /* ICOSizeDecoder.swift in Sources */, 256 | 27CF9397223352C20093543E /* FastImage.swift in Sources */, 257 | 27EDB8572245EFE4008406EB /* BMPSizeDecoder.swift in Sources */, 258 | 27EDB8612245FF55008406EB /* CURSizeDecoder.swift in Sources */, 259 | 27EDB84C2245EC84008406EB /* DataExtensions.swift in Sources */, 260 | 27EDB8532245EF9E008406EB /* GIFSizeDecoder.swift in Sources */, 261 | 27EDB85B2245F05B008406EB /* Exif.swift in Sources */, 262 | 27EDB85D2245F8E0008406EB /* PSDSizeDecoder.swift in Sources */, 263 | 27EDB8552245EFD0008406EB /* JPGSizeDecoder.swift in Sources */, 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | }; 267 | A1B48FD11A0DD89F001BB364 /* Sources */ = { 268 | isa = PBXSourcesBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | 27CF939922335E490093543E /* FastImageTests.swift in Sources */, 272 | 27B63D652249AB5A004BCC5B /* DataTests.swift in Sources */, 273 | ); 274 | runOnlyForDeploymentPostprocessing = 0; 275 | }; 276 | /* End PBXSourcesBuildPhase section */ 277 | 278 | /* Begin XCBuildConfiguration section */ 279 | 27CF9394223352940093543E /* Debug */ = { 280 | isa = XCBuildConfiguration; 281 | buildSettings = { 282 | CLANG_ANALYZER_NONNULL = YES; 283 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 284 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 285 | CLANG_ENABLE_MODULES = YES; 286 | CLANG_ENABLE_OBJC_WEAK = YES; 287 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 288 | CLANG_WARN_COMMA = YES; 289 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 290 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 291 | CLANG_WARN_INFINITE_RECURSION = YES; 292 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 293 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 294 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 295 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 296 | CLANG_WARN_STRICT_PROTOTYPES = YES; 297 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 298 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 299 | CODE_SIGN_IDENTITY = ""; 300 | CODE_SIGN_STYLE = Automatic; 301 | CURRENT_PROJECT_VERSION = 1; 302 | DEBUG_INFORMATION_FORMAT = dwarf; 303 | DEFINES_MODULE = YES; 304 | DYLIB_COMPATIBILITY_VERSION = 1; 305 | DYLIB_CURRENT_VERSION = 1; 306 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 307 | ENABLE_TESTABILITY = YES; 308 | GCC_C_LANGUAGE_STANDARD = gnu11; 309 | GCC_NO_COMMON_BLOCKS = YES; 310 | INFOPLIST_FILE = FastImage/Info.plist; 311 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 312 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 313 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 314 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 315 | MTL_FAST_MATH = YES; 316 | PRODUCT_BUNDLE_IDENTIFIER = com.kylehickinson.FastImage; 317 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 318 | SKIP_INSTALL = YES; 319 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 320 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 321 | SWIFT_VERSION = 5.0; 322 | TARGETED_DEVICE_FAMILY = "1,2"; 323 | VERSIONING_SYSTEM = "apple-generic"; 324 | VERSION_INFO_PREFIX = ""; 325 | }; 326 | name = Debug; 327 | }; 328 | 27CF9395223352940093543E /* Release */ = { 329 | isa = XCBuildConfiguration; 330 | buildSettings = { 331 | CLANG_ANALYZER_NONNULL = YES; 332 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 333 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 334 | CLANG_ENABLE_MODULES = YES; 335 | CLANG_ENABLE_OBJC_WEAK = YES; 336 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 337 | CLANG_WARN_COMMA = YES; 338 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 339 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 340 | CLANG_WARN_INFINITE_RECURSION = YES; 341 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 342 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 343 | CLANG_WARN_OBJC_LITERAL_CONVERSION = 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 | CODE_SIGN_IDENTITY = ""; 349 | CODE_SIGN_STYLE = Automatic; 350 | COPY_PHASE_STRIP = NO; 351 | CURRENT_PROJECT_VERSION = 1; 352 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 353 | DEFINES_MODULE = YES; 354 | DYLIB_COMPATIBILITY_VERSION = 1; 355 | DYLIB_CURRENT_VERSION = 1; 356 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 357 | GCC_C_LANGUAGE_STANDARD = gnu11; 358 | GCC_NO_COMMON_BLOCKS = YES; 359 | INFOPLIST_FILE = FastImage/Info.plist; 360 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 361 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 362 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 363 | MTL_FAST_MATH = YES; 364 | PRODUCT_BUNDLE_IDENTIFIER = com.kylehickinson.FastImage; 365 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 366 | SKIP_INSTALL = YES; 367 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 368 | SWIFT_VERSION = 5.0; 369 | TARGETED_DEVICE_FAMILY = "1,2"; 370 | VERSIONING_SYSTEM = "apple-generic"; 371 | VERSION_INFO_PREFIX = ""; 372 | }; 373 | name = Release; 374 | }; 375 | A1B48FDC1A0DD89F001BB364 /* Debug */ = { 376 | isa = XCBuildConfiguration; 377 | buildSettings = { 378 | ALWAYS_SEARCH_USER_PATHS = NO; 379 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 380 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 381 | CLANG_CXX_LIBRARY = "libc++"; 382 | CLANG_ENABLE_MODULES = YES; 383 | CLANG_ENABLE_OBJC_ARC = YES; 384 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 385 | CLANG_WARN_BOOL_CONVERSION = YES; 386 | CLANG_WARN_COMMA = YES; 387 | CLANG_WARN_CONSTANT_CONVERSION = YES; 388 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 389 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 390 | CLANG_WARN_EMPTY_BODY = YES; 391 | CLANG_WARN_ENUM_CONVERSION = YES; 392 | CLANG_WARN_INFINITE_RECURSION = YES; 393 | CLANG_WARN_INT_CONVERSION = YES; 394 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 395 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 396 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 397 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 398 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 399 | CLANG_WARN_STRICT_PROTOTYPES = YES; 400 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 401 | CLANG_WARN_UNREACHABLE_CODE = YES; 402 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 403 | COPY_PHASE_STRIP = NO; 404 | ENABLE_STRICT_OBJC_MSGSEND = YES; 405 | ENABLE_TESTABILITY = YES; 406 | GCC_C_LANGUAGE_STANDARD = gnu99; 407 | GCC_DYNAMIC_NO_PIC = NO; 408 | GCC_NO_COMMON_BLOCKS = YES; 409 | GCC_OPTIMIZATION_LEVEL = 0; 410 | GCC_PREPROCESSOR_DEFINITIONS = ( 411 | "DEBUG=1", 412 | "$(inherited)", 413 | ); 414 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 415 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 416 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 417 | GCC_WARN_UNDECLARED_SELECTOR = YES; 418 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 419 | GCC_WARN_UNUSED_FUNCTION = YES; 420 | GCC_WARN_UNUSED_VARIABLE = YES; 421 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 422 | MTL_ENABLE_DEBUG_INFO = YES; 423 | ONLY_ACTIVE_ARCH = YES; 424 | SDKROOT = iphoneos; 425 | }; 426 | name = Debug; 427 | }; 428 | A1B48FDD1A0DD89F001BB364 /* Release */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | ALWAYS_SEARCH_USER_PATHS = NO; 432 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 433 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 434 | CLANG_CXX_LIBRARY = "libc++"; 435 | CLANG_ENABLE_MODULES = YES; 436 | CLANG_ENABLE_OBJC_ARC = YES; 437 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 438 | CLANG_WARN_BOOL_CONVERSION = YES; 439 | CLANG_WARN_COMMA = YES; 440 | CLANG_WARN_CONSTANT_CONVERSION = YES; 441 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 442 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 443 | CLANG_WARN_EMPTY_BODY = YES; 444 | CLANG_WARN_ENUM_CONVERSION = YES; 445 | CLANG_WARN_INFINITE_RECURSION = YES; 446 | CLANG_WARN_INT_CONVERSION = YES; 447 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 448 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 449 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 450 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 451 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 452 | CLANG_WARN_STRICT_PROTOTYPES = YES; 453 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 454 | CLANG_WARN_UNREACHABLE_CODE = YES; 455 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 456 | COPY_PHASE_STRIP = YES; 457 | ENABLE_NS_ASSERTIONS = NO; 458 | ENABLE_STRICT_OBJC_MSGSEND = YES; 459 | GCC_C_LANGUAGE_STANDARD = gnu99; 460 | GCC_NO_COMMON_BLOCKS = YES; 461 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 462 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 463 | GCC_WARN_UNDECLARED_SELECTOR = YES; 464 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 465 | GCC_WARN_UNUSED_FUNCTION = YES; 466 | GCC_WARN_UNUSED_VARIABLE = YES; 467 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 468 | MTL_ENABLE_DEBUG_INFO = NO; 469 | SDKROOT = iphoneos; 470 | SWIFT_COMPILATION_MODE = wholemodule; 471 | VALIDATE_PRODUCT = YES; 472 | }; 473 | name = Release; 474 | }; 475 | A1B48FE21A0DD89F001BB364 /* Debug */ = { 476 | isa = XCBuildConfiguration; 477 | buildSettings = { 478 | CLANG_ENABLE_MODULES = YES; 479 | GCC_PREPROCESSOR_DEFINITIONS = ( 480 | "DEBUG=1", 481 | "$(inherited)", 482 | ); 483 | INFOPLIST_FILE = FastImageTests/Info.plist; 484 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 485 | PRODUCT_BUNDLE_IDENTIFIER = "com.kylehickinson.$(PRODUCT_NAME:rfc1034identifier)"; 486 | PRODUCT_NAME = "$(TARGET_NAME)"; 487 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 488 | SWIFT_VERSION = 5.0; 489 | }; 490 | name = Debug; 491 | }; 492 | A1B48FE31A0DD89F001BB364 /* Release */ = { 493 | isa = XCBuildConfiguration; 494 | buildSettings = { 495 | CLANG_ENABLE_MODULES = YES; 496 | INFOPLIST_FILE = FastImageTests/Info.plist; 497 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 498 | PRODUCT_BUNDLE_IDENTIFIER = "com.kylehickinson.$(PRODUCT_NAME:rfc1034identifier)"; 499 | PRODUCT_NAME = "$(TARGET_NAME)"; 500 | SWIFT_VERSION = 5.0; 501 | }; 502 | name = Release; 503 | }; 504 | /* End XCBuildConfiguration section */ 505 | 506 | /* Begin XCConfigurationList section */ 507 | 27CF9393223352940093543E /* Build configuration list for PBXNativeTarget "FastImage" */ = { 508 | isa = XCConfigurationList; 509 | buildConfigurations = ( 510 | 27CF9394223352940093543E /* Debug */, 511 | 27CF9395223352940093543E /* Release */, 512 | ); 513 | defaultConfigurationIsVisible = 0; 514 | defaultConfigurationName = Release; 515 | }; 516 | A1B48FC51A0DD89F001BB364 /* Build configuration list for PBXProject "FastImage" */ = { 517 | isa = XCConfigurationList; 518 | buildConfigurations = ( 519 | A1B48FDC1A0DD89F001BB364 /* Debug */, 520 | A1B48FDD1A0DD89F001BB364 /* Release */, 521 | ); 522 | defaultConfigurationIsVisible = 0; 523 | defaultConfigurationName = Release; 524 | }; 525 | A1B48FE11A0DD89F001BB364 /* Build configuration list for PBXNativeTarget "FastImageTests" */ = { 526 | isa = XCConfigurationList; 527 | buildConfigurations = ( 528 | A1B48FE21A0DD89F001BB364 /* Debug */, 529 | A1B48FE31A0DD89F001BB364 /* Release */, 530 | ); 531 | defaultConfigurationIsVisible = 0; 532 | defaultConfigurationName = Release; 533 | }; 534 | /* End XCConfigurationList section */ 535 | }; 536 | rootObject = A1B48FC21A0DD89F001BB364 /* Project object */; 537 | } 538 | -------------------------------------------------------------------------------- /FastImage.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FastImage.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FastImage.xcodeproj/xcshareddata/xcschemes/FastImage.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | 90 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /FastImage.xcodeproj/xcshareddata/xcschemes/FastImageTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /FastImage/DataExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataExtensions.swift 3 | // FastImage 4 | // 5 | // Created by Kyle Hickinson on 2019-03-23. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Data { 12 | struct AccessError: Error { 13 | let range: Range 14 | let data: Data 15 | } 16 | func read(from index: Int, length: Int) throws -> ResultType { 17 | return try read(index.. UInt8 { 20 | return try read(index..(_ range: Range) throws -> ResultType { 23 | if range.upperBound <= count { 24 | return subdata(in: range).withUnsafeBytes { $0.load(as: ResultType.self) } 25 | } 26 | throw AccessError(range: range, data: self) 27 | } 28 | func readBytes(_ range: Range) throws -> [UInt8] { 29 | if range.upperBound <= count { 30 | return subdata(in: range).withUnsafeBytes { [UInt8]($0) } 31 | } 32 | throw AccessError(range: range, data: self) 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /FastImage/FastImage.h: -------------------------------------------------------------------------------- 1 | // 2 | // FastImage.h 3 | // FastImage 4 | // 5 | // Created by Kyle Hickinson on 2019-03-08. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for FastImage. 12 | FOUNDATION_EXPORT double FastImageVersionNumber; 13 | 14 | //! Project version string for FastImage. 15 | FOUNDATION_EXPORT const unsigned char FastImageVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /FastImage/FastImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FastImage.swift 3 | // FastImage 4 | // 5 | // Created by Kyle Hickinson on 2019-03-08. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Result { 12 | case success(T) 13 | case failure(Error) 14 | } 15 | 16 | /// An error thrown when there are no size decoders that can decode the given image 17 | public struct UnsupportedImageError: Error { 18 | public let data: Data 19 | } 20 | 21 | public struct InvalidStatusCodeError: Error { 22 | public let statusCode: Int 23 | } 24 | 25 | /// FastImage is an Swift port of the Ruby project by Stephen Sykes. It's directive is too 26 | /// request as little data as possible (usually just the first batch of bytes returned by a request), 27 | /// to determine the size and type of a remote image. 28 | /// 29 | /// See: https://github.com/sdsykes/fastimage 30 | public final class FastImage: NSObject { 31 | /// The list of image formats that may be returned by `imageSizeAndType(for:completion)` 32 | public enum ImageFormat { 33 | case gif 34 | case png 35 | case jpg 36 | case bmp 37 | case tiff 38 | case psd 39 | case ico 40 | case cur 41 | } 42 | /// Maximum amount of time a request has to complete its task. 43 | /// Defaults to 2 seconds 44 | public var requestTimeout: TimeInterval = 2.0 45 | 46 | private lazy var session = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil) 47 | 48 | public typealias CompletionBlock = (Result<(CGSize, ImageFormat)>) -> Void 49 | 50 | /// Start a fast image request to get size and type from a remote image 51 | /// 52 | /// - parameter url: The remote image URL to parse 53 | /// - parameter completion: Completion block indicating size and type of the remote image, or an error if it failed 54 | /// - returns: A data task which has been started. Use this returned value to cancel or suspend a task if needed 55 | @discardableResult 56 | public func imageSizeAndType( 57 | for url: URL, 58 | completion: @escaping CompletionBlock) -> URLSessionDataTask { 59 | 60 | if let request = requests[url.absoluteString] { 61 | return request.task 62 | } 63 | 64 | let urlRequest = URLRequest( 65 | url: url, 66 | cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, 67 | timeoutInterval: requestTimeout 68 | ) 69 | let task = session.dataTask(with: urlRequest) 70 | requests[url.absoluteString] = Request(task: task, completion: completion) 71 | task.resume() 72 | return task 73 | } 74 | 75 | private class Request { 76 | var task: URLSessionDataTask 77 | var data: Data 78 | var completion: CompletionBlock 79 | 80 | init(task: URLSessionDataTask, completion: @escaping CompletionBlock) { 81 | self.task = task 82 | self.data = Data() 83 | self.completion = completion 84 | } 85 | } 86 | 87 | private var requests: [String: Request] = [:] 88 | 89 | private let sizeDecoders: [ImageSizeDecoder.Type] = [ 90 | PNGSizeDecoder.self, 91 | GIFSizeDecoder.self, 92 | BMPSizeDecoder.self, 93 | JPGSizeDecoder.self, 94 | TIFFSizeDecoder.self, 95 | PSDSizeDecoder.self, 96 | ICOSizeDecoder.self, 97 | CURSizeDecoder.self 98 | ] 99 | 100 | private func decoder(for data: Data) -> ImageSizeDecoder.Type? { 101 | do { 102 | return try sizeDecoders.first(where: { try $0.isDecoder(for: data) }) 103 | } catch { 104 | print(error) 105 | } 106 | return nil 107 | } 108 | 109 | private func parse(request: Request) { 110 | let data = request.data 111 | if data.isEmpty { return } 112 | guard let decoder = decoder(for: data) else { 113 | request.completion(.failure(UnsupportedImageError(data: data))) 114 | request.task.cancel() 115 | if let urlString = request.task.originalRequest?.url?.absoluteString { 116 | requests.removeValue(forKey: urlString) 117 | } 118 | return 119 | } 120 | do { 121 | let size = try decoder.size(for: data) 122 | request.completion(.success((size, decoder.imageFormat))) 123 | request.task.cancel() 124 | if let urlString = request.task.originalRequest?.url?.absoluteString { 125 | requests.removeValue(forKey: urlString) 126 | } 127 | } catch { 128 | // Not enough data 129 | } 130 | } 131 | 132 | private func validate(request: Request) -> Bool { 133 | if let response = request.task.response as? HTTPURLResponse { 134 | if !(200..<300).contains(response.statusCode) { 135 | // Validate that the status code returned is a success code, otherwise fail 136 | request.completion(.failure(InvalidStatusCodeError(statusCode: response.statusCode))) 137 | if let urlString = request.task.originalRequest?.url?.absoluteString { 138 | requests.removeValue(forKey: urlString) 139 | } 140 | request.task.cancel() 141 | return false 142 | } 143 | } 144 | return true 145 | } 146 | } 147 | 148 | extension FastImage: URLSessionDataDelegate { 149 | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 150 | guard let urlString = dataTask.originalRequest?.url?.absoluteString, 151 | let request = requests[urlString] else { 152 | return 153 | } 154 | if !validate(request: request) { 155 | return 156 | } 157 | request.data.append(data) 158 | if (!request.data.isEmpty) { 159 | parse(request: request) 160 | } 161 | } 162 | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 163 | if let error = error as? URLError, error.code == .cancelled { 164 | return 165 | } 166 | guard let urlString = task.originalRequest?.url?.absoluteString, 167 | let request = requests[urlString] else { 168 | return 169 | } 170 | if !validate(request: request) { 171 | return 172 | } 173 | request.completion(.failure(error ?? SizeNotFoundError(data: request.data))) 174 | requests.removeValue(forKey: urlString) 175 | } 176 | public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 177 | completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /FastImage/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 | FMWK 17 | CFBundleShortVersionString 18 | 2.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /FastImage/Size Decoders/BMPSizeDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BMPSizeDecoder.swift 3 | // FastImage 4 | // 5 | // Created by Kyle Hickinson on 2019-03-23. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Bitmap size decoder 12 | /// 13 | /// File structure: http://www.fileformat.info/format/bmp/egff.htm 14 | final class BMPSizeDecoder: ImageSizeDecoder { 15 | static var imageFormat: FastImage.ImageFormat { 16 | return .bmp 17 | } 18 | 19 | static func isDecoder(for data: Data) throws -> Bool { 20 | return try String(bytes: data.readBytes(0..<2), encoding: .ascii) == "BM" 21 | } 22 | 23 | static func size(for data: Data) throws -> CGSize { 24 | // Read the BMP header size. Depending on the size, it may 25 | // be an older version which only contains 16 bit width/height 26 | if try data.read(14) == 12 { 27 | let width = try data.read(18..<20) as UInt16 28 | let height = try data.read(20..<22) as UInt16 29 | return CGSize(width: CGFloat(width), height: CGFloat(height)) 30 | } 31 | else { 32 | // Height can be negative (describing the origin of the image) 33 | // This doesn't really matter to us 34 | let width = try data.read(18..<22) as Int32 35 | let height = abs(try data.read(22..<26) as Int32) 36 | return CGSize(width: CGFloat(width), height: CGFloat(height)) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /FastImage/Size Decoders/CURSizeDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CURSizeDecoder.swift 3 | // FastImage 4 | // 5 | // Created by Kyle Hickinson on 2019-03-23. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// CUR size decoder 12 | /// 13 | /// File structure: https://en.wikipedia.org/wiki/ICO_(file_format)#Outline 14 | final class CURSizeDecoder: ImageSizeDecoder { 15 | static var imageFormat: FastImage.ImageFormat { 16 | return .cur 17 | } 18 | static func isDecoder(for data: Data) throws -> Bool { 19 | return try data.readBytes(0..<3) == [0x00, 0x00, 0x02] 20 | } 21 | static func size(for data: Data) throws -> CGSize { 22 | return try ICOSizeDecoder.size(for: data) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /FastImage/Size Decoders/Exif.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exif.swift 3 | // FastImage 4 | // 5 | // Created by Kyle Hickinson on 2019-03-23. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Obtains the image width, height and orientation from an EXIF data structure 12 | /// 13 | /// Can also be used for EXIF JPEG's, but will only find orientation data (among other Exif tags we dont care about) 14 | /// 15 | /// http://www.fileformat.info/format/tiff/egff.htm 16 | final class Exif { 17 | private(set) var orientation: UIImage.Orientation? 18 | private(set) var width: UInt16? 19 | private(set) var height: UInt16? 20 | 21 | init?(data: Data) throws { 22 | // Little endian defined as "II", big endian defined as "MM" 23 | let isLittleEndian = try String(bytes: data.readBytes(0..<2), encoding: .ascii) == "II" 24 | let swap32 = isLittleEndian ? CFSwapInt32LittleToHost : CFSwapInt32BigToHost 25 | let swap16 = isLittleEndian ? CFSwapInt16LittleToHost : CFSwapInt16BigToHost 26 | var offset = Int(swap32(try data.read(4..<8))) 27 | let numberOfTags = swap16(try data.read(from: offset, length: 2)) 28 | offset += 2 29 | 30 | for _ in 0.. Bool { 19 | return try String(bytes: data.readBytes(0..<3), encoding: .ascii) == "GIF" 20 | } 21 | static func size(for data: Data) throws -> CGSize { 22 | struct Size { 23 | var width: UInt16 24 | var height: UInt16 25 | } 26 | assert(MemoryLayout.size == 4) 27 | let size = try data.read(6..<10) as Size 28 | return CGSize(width: CGFloat(size.width), height: CGFloat(size.height)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /FastImage/Size Decoders/ICOSizeDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICOSizeDecoder.swift 3 | // FastImage 4 | // 5 | // Created by Kyle Hickinson on 2019-03-23. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// ICO size decoder 12 | /// 13 | /// File structure: https://en.wikipedia.org/wiki/ICO_(file_format)#Outline 14 | final class ICOSizeDecoder: ImageSizeDecoder { 15 | static var imageFormat: FastImage.ImageFormat { 16 | return .ico 17 | } 18 | static func isDecoder(for data: Data) throws -> Bool { 19 | return try data.readBytes(0..<3) == [0x00, 0x00, 0x01] 20 | } 21 | static func size(for data: Data) throws -> CGSize { 22 | let numberOfIcons = Int(CFSwapInt16LittleToHost(try data.read(4..<6))) 23 | var width: Int = 0 24 | var height: Int = 0 25 | for index in 0.. Bool 25 | /// Get the size of the image given the data available 26 | /// 27 | /// - throws: SizeNotFoundError, Data.AccessError 28 | static func size(for data: Data) throws -> CGSize 29 | } 30 | -------------------------------------------------------------------------------- /FastImage/Size Decoders/JPGSizeDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JPGSizeDecoder.swift 3 | // FastImage 4 | // 5 | // Created by Kyle Hickinson on 2019-03-23. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// JPEG Size decoder 12 | /// 13 | /// File structure: http://www.fileformat.info/format/jpeg/egff.htm 14 | /// JPEG Tags: https://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html 15 | final class JPGSizeDecoder: ImageSizeDecoder { 16 | static var imageFormat: FastImage.ImageFormat { 17 | return .jpg 18 | } 19 | 20 | static func isDecoder(for data: Data) throws -> Bool { 21 | let signature: [UInt8] = [0xFF, 0xD8] 22 | return try data.readBytes(0..<2) == signature 23 | } 24 | 25 | private enum SearchState { 26 | case findHeader 27 | case determineFrameType 28 | case skipFrame 29 | case foundSOF 30 | case foundEOI 31 | } 32 | 33 | static func size(for data: Data) throws -> CGSize { 34 | var searchState: SearchState = .findHeader 35 | var offset = 2 36 | var imageOrientation: UIImage.Orientation? 37 | 38 | while offset < data.count { 39 | switch searchState { 40 | case .findHeader: 41 | while try data.read(offset) != 0xFF { 42 | offset += 1 43 | } 44 | searchState = .determineFrameType 45 | case .determineFrameType: 46 | // We've found a data marker, now we determine what type of data we're looking at. 47 | // FF E0 -> FF EF are 'APPn', and include lots of metadata like EXIF, etc. 48 | // 49 | // What we want to find is one of the SOF (Start of Frame) header, cause' it includes 50 | // width and height (what we want!) 51 | // 52 | // JPEG Metadata Header Table 53 | // http://www.xbdev.net/image_formats/jpeg/tut_jpg/jpeg_file_layout.php 54 | // Each of these SOF data markers have the same data structure: 55 | // struct { 56 | // UInt16 header; // e.g. FFC0 57 | // UInt16 frameLength; 58 | // UInt8 samplePrecision; 59 | // UInt16 imageHeight; 60 | // UInt16 imageWidth; 61 | // ... // we only care about this part 62 | // } 63 | let sample = try data.read(offset) 64 | offset += 1 65 | 66 | // Technically we should check if this has EXIF data here (looking for FFE1 marker)… 67 | // Maybe TODO later 68 | switch sample { 69 | case 0xE1: 70 | let exifLength = CFSwapInt16BigToHost(try data.read(from: offset, length: 2)) 71 | let exifString = String(bytes: try data.readBytes(offset+2..=5.0) 76 | imageOrientation = exif.orientation 77 | #else 78 | imageOrientation = exif?.orientation 79 | #endif 80 | } 81 | } 82 | offset += Int(exifLength) 83 | searchState = .findHeader 84 | case 0xE0...0xEF: 85 | // Technically we should check if this has EXIF data here (looking for FFE1 marker)… 86 | searchState = .skipFrame 87 | case 0xC0...0xC3, 0xC5...0xC7, 0xC9...0xCB, 0xCD...0xCF: 88 | searchState = .foundSOF 89 | case 0xFF: 90 | searchState = .determineFrameType 91 | case 0xD9: 92 | // We made it to the end of the file somehow without finding the size? Likely a corrupt file 93 | searchState = .foundEOI 94 | default: 95 | // Since we don't handle every header case default to skipping an unknown data marker 96 | searchState = .skipFrame 97 | } 98 | case .skipFrame: 99 | let frameLength = Int(CFSwapInt16BigToHost(try data.read(offset..= 5 { 108 | // Rotated 109 | return CGSize( 110 | width: CGFloat(CFSwapInt16BigToHost(height)), 111 | height: CGFloat(CFSwapInt16BigToHost(width)) 112 | ) 113 | } 114 | return CGSize( 115 | width: CGFloat(CFSwapInt16BigToHost(width)), 116 | height: CGFloat(CFSwapInt16BigToHost(height)) 117 | ) 118 | case .foundEOI: 119 | throw SizeNotFoundError(data: data) 120 | } 121 | } 122 | 123 | throw SizeNotFoundError(data: data) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /FastImage/Size Decoders/PNGSizeDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PNGSizeDecoder.swift 3 | // FastImage 4 | // 5 | // Created by Kyle Hickinson on 2019-03-23. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A PNG size decoder 12 | /// 13 | /// PNG structure: 14 | /// - http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html 15 | /// - http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html 16 | final class PNGSizeDecoder: ImageSizeDecoder { 17 | static var imageFormat: FastImage.ImageFormat { 18 | return .png 19 | } 20 | static func isDecoder(for data: Data) throws -> Bool { 21 | let signature: [UInt8] = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] 22 | return try data.readBytes(0.. CGSize { 25 | // PNG is also simple: 8 byte header, next just read the first chunk (IHDR). 26 | // Chunks have the format: 27 | // 4 bytes - length 28 | // 4 bytes - type 29 | // X bytes - data 30 | // 4 bytes - CRC 31 | // meaning the first chunk is located at offset 8 and the data begins at 16. 32 | // the width and height are stored as 32 bit unsigned integers (big-endian) 33 | struct Size { 34 | var width: UInt32 = 0 // Big-endian 35 | var height: UInt32 = 0 // Big-endian 36 | } 37 | assert(MemoryLayout.size == 8) 38 | let size = try data.read(16..<24) as Size 39 | return CGSize( 40 | width: CGFloat(CFSwapInt32BigToHost(size.width)), 41 | height: CGFloat(CFSwapInt32BigToHost(size.height)) 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FastImage/Size Decoders/PSDSizeDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PSDSizeDecode.swift 3 | // FastImage 4 | // 5 | // Created by Kyle Hickinson on 2019-03-23. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// PSD size decoder 12 | final class PSDSizeDecoder: ImageSizeDecoder { 13 | static var imageFormat: FastImage.ImageFormat { 14 | return .psd 15 | } 16 | static func isDecoder(for data: Data) throws -> Bool { 17 | return try String(bytes: data.readBytes(0..<2), encoding: .ascii) == "8B" 18 | } 19 | static func size(for data: Data) throws -> CGSize { 20 | let height = CFSwapInt32BigToHost(try data.read(14..<18)) 21 | let width = CFSwapInt32BigToHost(try data.read(18..<22)) 22 | return CGSize(width: Int(width), height: Int(height)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /FastImage/Size Decoders/TIFFSizeDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TIFFSizeDecoder.swift 3 | // FastImage 4 | // 5 | // Created by Kyle Hickinson on 2019-03-23. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// TIFF size decoder 12 | /// 13 | /// File structure: http://www.fileformat.info/format/tiff/egff.htm 14 | final class TIFFSizeDecoder: ImageSizeDecoder { 15 | static var imageFormat: FastImage.ImageFormat { 16 | return .tiff 17 | } 18 | 19 | static func isDecoder(for data: Data) throws -> Bool { 20 | let order = try String(bytes: data.readBytes(0..<2), encoding: .ascii) 21 | switch order { 22 | case "II", "MM": 23 | // do not recognise CRW or CR2 as tiff 24 | let isCRW = try String(bytes: data.readBytes(8..<11), encoding: .ascii) == "APC" 25 | let isCR2 = try data.readBytes(8..<11) == [0x43, 0x52, 0x02] // "CR" 2 26 | return !(isCRW || isCR2) 27 | default: 28 | return false 29 | } 30 | } 31 | 32 | static func size(for data: Data) throws -> CGSize { 33 | guard let exif = try Exif(data: data), let width = exif.width, let height = exif.height else { 34 | throw SizeNotFoundError(data: data) 35 | } 36 | if let orientation = exif.orientation, orientation.rawValue >= 5 { 37 | // Rotated 38 | return CGSize(width: Int(height), height: Int(width)) 39 | } 40 | return CGSize(width: Int(width), height: Int(height)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /FastImageTests/DataTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataTests.swift 3 | // FastImageTests 4 | // 5 | // Created by Kyle Hickinson on 2019-03-25. 6 | // Copyright © 2019 Kyle Hickinson. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import FastImage 11 | 12 | class DataTests: XCTestCase { 13 | 14 | func testSingleByteRead() { 15 | let data = Data([0x01, 0x02]) 16 | do { 17 | XCTAssertEqual(try data.read(0), 0x01) 18 | XCTAssertEqual(try data.read(1), 0x02) 19 | } 20 | } 21 | 22 | func testDataAccessError() { 23 | let data = Data([0x01, 0x02]) 24 | do { 25 | _ = try data.read(3) 26 | XCTFail("Should have failed to read from 3") 27 | } catch { 28 | XCTAssertTrue(error is Data.AccessError) 29 | } 30 | } 31 | 32 | func testReadBytes() { 33 | let bytes: [UInt8] = [0xdd, 0xee, 0xff] 34 | let data = Data(bytes) 35 | XCTAssertEqual(try data.readBytes(0..<3), bytes) 36 | } 37 | 38 | func testReadToType() { 39 | let value: UInt16 = 4096 40 | let data = Data(withUnsafeBytes(of: value, { Array($0) })) 41 | XCTAssertEqual(data.count, 2) 42 | XCTAssertEqual(try data.read(0..<2) as UInt16, value) 43 | } 44 | 45 | func testReadToStruct() { 46 | struct TestType: Equatable { 47 | let a: UInt16 48 | let b: UInt16 49 | let c: UInt32 50 | } 51 | let value = TestType(a: 0x800, b: 0x1000, c: 0x10000) 52 | let data = Data(withUnsafeBytes(of: value, { Array($0) })) 53 | let structSize = MemoryLayout.size 54 | XCTAssertEqual(data.count, structSize) 55 | XCTAssertEqual(try data.read(0.. 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 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Kyle Hickinson 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastImage 2 | [![Build Status](https://travis-ci.org/kylehickinson/FastImage.svg)](https://travis-ci.org/kylehickinson/FastImage) 3 | 4 | FastImage 2.0 is an Swift port of the [Ruby project by Stephen Sykes](https://github.com/sdsykes/fastimage). It's directive is to request as little data as possible (usually just the first batch of bytes returned by a request) to determine the size and type of a remote image. 5 | 6 | This means you can get the size of a large image (say 2MB), by only downloading around 8KB - 32KB 7 | 8 | Fairly simple to use: 9 | 10 | ``` 11 | let fastImage = FastImage() 12 | let imageURL = URL("http://i.imgur.com/7GLI90s.jpg")! 13 | 14 | fastImage.imageSizeAndType(for: url, completion: { result in 15 | switch result { 16 | case .success(let (size, type)): 17 | // `size` would be 1600 x 1200 18 | // `type` would be .jpg 19 | ... 20 | case .failure(let error): 21 | // Get a URLError here (timed out, etc.) or a SizeNotFoundError (just couldn't get a size based on data) 22 | } 23 | }) 24 | ``` 25 | 26 | Currently supports **JPEG**, **PNG**, **BMP**, **GIF**, **TIFF**, **ICO**, **CUR**, and **PSD**. 27 | 28 | ## Compatability 29 | 30 | 2.0 was built with Xcode 10.1 with a deployment target of 11.0 but could likely be set lower 31 | 32 | ## License 33 | 34 | MIT, see LICENSE file 35 | --------------------------------------------------------------------------------