| OLD | NEW | 
|---|
| 1 // | 1 // | 
| 2 // FavIcon | 2 // FavIcon | 
| 3 // Copyright © 2016 Leon Breedt | 3 // Copyright © 2016 Leon Breedt | 
| 4 // | 4 // | 
| 5 // Licensed under the Apache License, Version 2.0 (the "License"); | 5 // Licensed under the Apache License, Version 2.0 (the "License"); | 
| 6 // you may not use this file except in compliance with the License. | 6 // you may not use this file except in compliance with the License. | 
| 7 // You may obtain a copy of the License at | 7 // You may obtain a copy of the License at | 
| 8 // | 8 // | 
| 9 // http://www.apache.org/licenses/LICENSE-2.0 | 9 // http://www.apache.org/licenses/LICENSE-2.0 | 
| 10 // | 10 // | 
| 11 // Unless required by applicable law or agreed to in writing, software | 11 // Unless required by applicable law or agreed to in writing, software | 
| 12 // distributed under the License is distributed on an "AS IS" BASIS, | 12 // distributed under the License is distributed on an "AS IS" BASIS, | 
| 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
| 14 // See the License for the specific language governing permissions and | 14 // See the License for the specific language governing permissions and | 
| 15 // limitations under the License. | 15 // limitations under the License. | 
| 16 // | 16 // | 
| 17 | 17 | 
|  | 18 // swiftlint:disable file_length | 
|  | 19 | 
| 18 import Foundation | 20 import Foundation | 
| 19 |  | 
| 20 #if os(iOS) | 21 #if os(iOS) | 
| 21     import UIKit | 22     import UIKit | 
| 22     /// Alias for the iOS image type (`UIImage`). | 23     /// Alias for the iOS image type (`UIImage`). | 
| 23     public typealias ImageType = UIImage | 24     public typealias ImageType = UIImage | 
| 24 #elseif os(OSX) | 25 #elseif os(OSX) | 
| 25     import Cocoa | 26     import Cocoa | 
| 26     /// Alias for the OS X image type (`NSImage`). | 27     /// Alias for the OS X image type (`NSImage`). | 
| 27     public typealias ImageType = NSImage | 28     public typealias ImageType = NSImage | 
| 28 #endif | 29 #endif | 
| 29 | 30 | 
| 30 /// Represents the result of attempting to download an icon. | 31 /// Represents the result of attempting to download an icon. | 
| 31 public enum IconDownloadResult { | 32 public enum IconDownloadResult { | 
| 32 | 33 | 
| 33     /// Download successful. | 34     /// Download successful. | 
| 34     /// | 35     /// | 
| 35     /// - parameter image: The `ImageType` for the downloaded icon. | 36     /// - parameter image: The `ImageType` for the downloaded icon. | 
| 36     case success(image: ImageType) | 37     case success(image: ImageType) | 
| 37 | 38 | 
| 38     /// Download failed for some reason. | 39     /// Download failed for some reason. | 
| 39     /// | 40     /// | 
| 40     /// - parameter error: The error which can be consulted to determine the roo
     t cause. | 41     /// - parameter error: The error which can be consulted to determine the roo
     t cause. | 
| 41     case failure(error: Error) | 42     case failure(error: Error) | 
| 42 | 43 | 
| 43 } | 44 } | 
| 44 | 45 | 
| 45 /// Responsible for detecting all of the different icons supported by a given si
     te. | 46 /// Responsible for detecting all of the different icons supported by a given si
     te. | 
| 46 @objc public final class FavIcon : NSObject { | 47 @objc | 
|  | 48 public final class FavIcon: NSObject { | 
| 47 | 49 | 
| 48     // swiftlint:disable function_body_length | 50     // swiftlint:disable function_body_length | 
| 49 | 51 | 
| 50     /// Scans a base URL, attempting to determine all of the supported icons tha
     t can | 52     /// Scans a base URL, attempting to determine all of the supported icons tha
     t can | 
| 51     /// be used for favicon purposes. | 53     /// be used for favicon purposes. | 
| 52     /// | 54     /// | 
| 53     /// It will do the following to determine possible icons that can be used: | 55     /// It will do the following to determine possible icons that can be used: | 
| 54     /// | 56     /// | 
| 55     /// - Check whether or not `/favicon.ico` exists. | 57     /// - Check whether or not `/favicon.ico` exists. | 
| 56     /// - If the base URL returns an HTML page, parse the `<head>` section and c
     heck for `<link>` | 58     /// - If the base URL returns an HTML page, parse the `<head>` section and c
     heck for `<link>` | 
| 57     ///   and `<meta>` tags that reference icons using Apple, Microsoft and Goog
     le | 59     ///   and `<meta>` tags that reference icons using Apple, Microsoft and Goog
     le | 
| 58     ///   conventions. | 60     ///   conventions. | 
| 59     /// - If _Web Application Manifest JSON_ (`manifest.json`) files are referen
     ced, or | 61     /// - If _Web Application Manifest JSON_ (`manifest.json`) files are referen
     ced, or | 
| 60     ///   _Microsoft browser configuration XML_ (`browserconfig.xml`) files | 62     ///   _Microsoft browser configuration XML_ (`browserconfig.xml`) files | 
| 61     ///   are referenced, download and parse them to check if they reference ico
     ns. | 63     ///   are referenced, download and parse them to check if they reference ico
     ns. | 
| 62     /// | 64     /// | 
| 63     ///  All of this work is performed in a background queue. | 65     ///  All of this work is performed in a background queue. | 
| 64     /// | 66     /// | 
| 65     /// - parameter url: The base URL to scan. | 67     /// - parameter url: The base URL to scan. | 
| 66     /// - parameter completion: A closure to call when the scan has completed. T
     he closure will be call | 68     /// - parameter completion: A closure to call when the scan has completed. T
     he closure will be call | 
| 67     ///                         on the main queue. | 69     ///                         on the main queue. | 
| 68     @objc public static func scan(_ url: URL, completion: @escaping ([DetectedIc
     on], [String:String]) -> Void) { | 70     @objc | 
|  | 71     public static func scan(_ url: URL, completion: @escaping ([DetectedIcon], [
     String: String]) -> Void) { | 
| 69         let queue = DispatchQueue(label: "org.bitserf.FavIcon", attributes: []) | 72         let queue = DispatchQueue(label: "org.bitserf.FavIcon", attributes: []) | 
| 70         var icons: [DetectedIcon] = [] | 73         var icons: [DetectedIcon] = [] | 
| 71         var additionalDownloads: [URLRequestWithCallback] = [] | 74         var additionalDownloads: [URLRequestWithCallback] = [] | 
| 72         let urlSession = urlSessionProvider() | 75         let urlSession = urlSessionProvider() | 
| 73         var meta: [String:String] = [:] | 76         var meta: [String: String] = [:] | 
| 74 | 77 | 
| 75         let downloadHTMLOperation = DownloadTextOperation(url: url, session: url
     Session) | 78         let downloadHTMLOperation = DownloadTextOperation(url: url, session: url
     Session) | 
| 76         let downloadHTML = urlRequestOperation(downloadHTMLOperation) { result i
     n | 79         let downloadHTML = urlRequestOperation(downloadHTMLOperation) { result i
     n | 
| 77             if case let .textDownloaded(actualURL, text, contentType) = result { | 80             if case let .textDownloaded(actualURL, text, contentType) = result { | 
| 78                 if contentType == "text/html" { | 81                 if contentType == "text/html" { | 
| 79                     let document = HTMLDocument(string: text) | 82                     let document = HTMLDocument(string: text) | 
| 80 | 83 | 
| 81                     let htmlIcons = extractHTMLHeadIcons(document, baseURL: actu
     alURL) | 84                     let htmlIcons = extractHTMLHeadIcons(document, baseURL: actu
     alURL) | 
| 82                     let htmlMeta = examineHTMLMeta(document, baseURL: actualURL) | 85                     let htmlMeta = examineHTMLMeta(document, baseURL: actualURL) | 
| 83                     queue.sync { | 86                     queue.sync { | 
| 84                         icons.append(contentsOf: htmlIcons) | 87                         icons.append(contentsOf: htmlIcons) | 
| 85                         meta = htmlMeta | 88                         meta = htmlMeta | 
| 86                     } | 89                     } | 
| 87 | 90 | 
| 88                     for manifestURL in extractWebAppManifestURLs(document, baseU
     RL: url) { | 91                     for manifestURL in extractWebAppManifestURLs(document, baseU
     RL: url) { | 
| 89                         let downloadOperation = DownloadTextOperation(url: manif
     estURL, | 92                         let downloadOperation = DownloadTextOperation(url: manif
     estURL, | 
| 90                                                                               se
     ssion: urlSession) | 93                                                                       session: u
     rlSession) | 
| 91                         let download = urlRequestOperation(downloadOperation) { 
     result in | 94                         let download = urlRequestOperation(downloadOperation) { 
     result in | 
| 92                             if case .textDownloaded(_, let manifestJSON, _) = re
     sult { | 95                             if case .textDownloaded(_, let manifestJSON, _) = re
     sult { | 
| 93                                 let jsonIcons = extractManifestJSONIcons( | 96                                 let jsonIcons = extractManifestJSONIcons( | 
| 94                                     manifestJSON, | 97                                     manifestJSON, | 
| 95                                     baseURL: actualURL | 98                                     baseURL: actualURL | 
| 96                                 ) | 99                                 ) | 
| 97                                 queue.sync { | 100                                 queue.sync { | 
| 98                                     icons.append(contentsOf: jsonIcons) | 101                                     icons.append(contentsOf: jsonIcons) | 
| 99                                 } | 102                                 } | 
| 100                             } | 103                             } | 
| (...skipping 16 matching lines...) Expand all  Loading... | 
| 117                                     icons.append(contentsOf: xmlIcons) | 120                                     icons.append(contentsOf: xmlIcons) | 
| 118                                 } | 121                                 } | 
| 119                             } | 122                             } | 
| 120                         } | 123                         } | 
| 121                         additionalDownloads.append(download) | 124                         additionalDownloads.append(download) | 
| 122                     } | 125                     } | 
| 123                 } | 126                 } | 
| 124             } | 127             } | 
| 125         } | 128         } | 
| 126 | 129 | 
| 127 |  | 
| 128         let favIconURL = URL(string: "/favicon.ico", relativeTo: url as URL)!.ab
     soluteURL | 130         let favIconURL = URL(string: "/favicon.ico", relativeTo: url as URL)!.ab
     soluteURL | 
| 129         let checkFavIconOperation = CheckURLExistsOperation(url: favIconURL, ses
     sion: urlSession) | 131         let checkFavIconOperation = CheckURLExistsOperation(url: favIconURL, ses
     sion: urlSession) | 
| 130         let checkFavIcon = urlRequestOperation(checkFavIconOperation) { result i
     n | 132         let checkFavIcon = urlRequestOperation(checkFavIconOperation) { result i
     n | 
| 131             if case let .success(actualURL) = result { | 133             if case let .success(actualURL) = result { | 
| 132                 queue.sync { | 134                 queue.sync { | 
| 133                     icons.append(DetectedIcon(url: actualURL, type: .classic)) | 135                     icons.append(DetectedIcon(url: actualURL, type: .classic)) | 
| 134                 } | 136                 } | 
| 135             } | 137             } | 
| 136         } | 138         } | 
| 137 | 139 | 
| 138         let touchIconURL = URL(string: "/apple-touch-icon.png", relativeTo: url 
     as URL)!.absoluteURL | 140         let touchIconURL = URL(string: "/apple-touch-icon.png", relativeTo: url 
     as URL)!.absoluteURL | 
| 139         let checkTouchIconOperation = CheckURLExistsOperation(url: touchIconURL,
      session: urlSession) | 141         let checkTouchIconOperation = CheckURLExistsOperation(url: touchIconURL,
      session: urlSession) | 
| 140         let checkTouchIcon = urlRequestOperation(checkTouchIconOperation) { resu
     lt in | 142         let checkTouchIcon = urlRequestOperation(checkTouchIconOperation) { resu
     lt in | 
| 141           if case let .success(actualURL) = result { | 143           if case let .success(actualURL) = result { | 
| 142             queue.sync { | 144             queue.sync { | 
| 143               icons.append(DetectedIcon(url: actualURL, type: .appleIOSWebClip, 
     width: 60, height: 60)) | 145               icons.append(DetectedIcon(url: actualURL, type: .appleIOSWebClip, 
     width: 60, height: 60)) | 
| 144             } | 146             } | 
| 145           } | 147           } | 
| 146         } | 148         } | 
| 147 | 149 | 
| (...skipping 12 matching lines...) Expand all  Loading... | 
| 160         } | 162         } | 
| 161     } | 163     } | 
| 162     // swiftlint:enable function_body_length | 164     // swiftlint:enable function_body_length | 
| 163 | 165 | 
| 164     /// Downloads an array of detected icons in the background. | 166     /// Downloads an array of detected icons in the background. | 
| 165     /// | 167     /// | 
| 166     /// - parameter icons: The icons to download. | 168     /// - parameter icons: The icons to download. | 
| 167     /// - parameter completion: A closure to call when all download tasks have | 169     /// - parameter completion: A closure to call when all download tasks have | 
| 168     ///                         results available (successful or otherwise). The
      closure | 170     ///                         results available (successful or otherwise). The
      closure | 
| 169     ///                         will be called on the main queue. | 171     ///                         will be called on the main queue. | 
| 170     @objc public static func download(_ icons: [DetectedIcon], completion: @esca
     ping ([ImageType]) -> Void) { | 172     @objc | 
|  | 173     public static func download(_ icons: [DetectedIcon], completion: @escaping (
     [ImageType]) -> Void) { | 
| 171         let urlSession = urlSessionProvider() | 174         let urlSession = urlSessionProvider() | 
| 172         let operations: [DownloadImageOperation] = | 175         let operations: [DownloadImageOperation] = | 
| 173             icons.map { DownloadImageOperation(url: $0.url, session: urlSession)
      } | 176             icons.map { DownloadImageOperation(url: $0.url, session: urlSession)
      } | 
| 174 | 177 | 
| 175         executeURLOperations(operations) { results in | 178         executeURLOperations(operations) { results in | 
| 176             let downloadResults: [ImageType] = results.flatMap { result in | 179             let downloadResults: [ImageType] = results.flatMap { result in | 
| 177                 switch result { | 180                 switch result { | 
| 178                 case .imageDownloaded(_, let image): | 181                 case .imageDownloaded(_, let image): | 
| 179                   return image; | 182                   return image | 
| 180                 case .failed(_): | 183                 case .failed: | 
| 181                   return nil; | 184                   return nil | 
| 182                 default: | 185                 default: | 
| 183                   return nil; | 186                   return nil | 
| 184                 } | 187                 } | 
| 185             } | 188             } | 
| 186 | 189 | 
| 187             DispatchQueue.main.async { | 190             DispatchQueue.main.async { | 
| 188                 completion(downloadResults) | 191                 completion(downloadResults) | 
| 189             } | 192             } | 
| 190         } | 193         } | 
| 191     } | 194     } | 
| 192 | 195 | 
| 193     /// Downloads all available icons by calling `scan(url:)` to discover the av
     ailable icons, and then | 196     /// Downloads all available icons by calling `scan(url:)` to discover the av
     ailable icons, and then | 
| 194     /// performing background downloads of each icon. | 197     /// performing background downloads of each icon. | 
| 195     /// | 198     /// | 
| 196     /// - parameter url: The URL to scan for icons. | 199     /// - parameter url: The URL to scan for icons. | 
| 197     /// - parameter completion: A closure to call when all download tasks have r
     esults available | 200     /// - parameter completion: A closure to call when all download tasks have r
     esults available | 
| 198     ///                         (successful or otherwise). The closure will be c
     alled on the main queue. | 201     ///                         (successful or otherwise). The closure will be c
     alled on the main queue. | 
| 199     @objc public static func downloadAll(_ url: URL, completion: @escaping ([Ima
     geType]) -> Void) { | 202     @objc | 
| 200         scan(url) { icons, meta in | 203     public static func downloadAll(_ url: URL, completion: @escaping ([ImageType
     ]) -> Void) { | 
|  | 204         scan(url) { icons, _ in | 
| 201             download(icons, completion: completion) | 205             download(icons, completion: completion) | 
| 202         } | 206         } | 
| 203     } | 207     } | 
| 204 | 208 | 
| 205     /// Downloads the most preferred icon, by calling `scan(url:)` to discover a
     vailable icons, and then choosing | 209     /// Downloads the most preferred icon, by calling `scan(url:)` to discover a
     vailable icons, and then choosing | 
| 206     /// the most preferable available icon. If both `width` and `height` are sup
     plied, the icon closest to the | 210     /// the most preferable available icon. If both `width` and `height` are sup
     plied, the icon closest to the | 
| 207     /// preferred size is chosen. Otherwise, the largest icon is chosen, if dime
     nsions are known. If no icon | 211     /// preferred size is chosen. Otherwise, the largest icon is chosen, if dime
     nsions are known. If no icon | 
| 208     /// has dimensions, the icons are chosen by order of their `DetectedIconType
     ` enumeration raw value. | 212     /// has dimensions, the icons are chosen by order of their `DetectedIconType
     ` enumeration raw value. | 
| 209     /// | 213     /// | 
| 210     /// - parameter url: The URL to scan for icons. | 214     /// - parameter url: The URL to scan for icons. | 
| 211     /// - parameter width: The preferred icon width, in pixels, or `nil`. | 215     /// - parameter width: The preferred icon width, in pixels, or `nil`. | 
| 212     /// - parameter height: The preferred icon height, in pixels, or `nil`. | 216     /// - parameter height: The preferred icon height, in pixels, or `nil`. | 
| 213     /// - parameter completion: A closure to call when the download task has pro
     duced results. The closure will | 217     /// - parameter completion: A closure to call when the download task has pro
     duced results. The closure will | 
| 214     ///                         be called on the main queue. | 218     ///                         be called on the main queue. | 
| 215     /// - throws: An appropriate `IconError` if downloading was not successful. | 219     /// - throws: An appropriate `IconError` if downloading was not successful. | 
| 216     @objc public static func downloadPreferred(_ url: URL, | 220     @objc | 
|  | 221     public static func downloadPreferred(_ url: URL, | 
| 217                                          width: Int, | 222                                          width: Int, | 
| 218                                          height: Int, | 223                                          height: Int, | 
| 219                                          completion: @escaping (ImageType?) -> V
     oid) { | 224                                          completion: @escaping (ImageType?) -> V
     oid) { | 
| 220         scan(url) { icons, meta in | 225         scan(url) { icons, _ in | 
| 221             guard let icon = chooseIcon(icons, width: width, height: height) els
     e { | 226             guard let icon = chooseIcon(icons, width: width, height: height) els
     e { | 
| 222                 DispatchQueue.main.async { | 227                 DispatchQueue.main.async { | 
| 223                   completion(ImageType()); | 228                   completion(ImageType()) | 
| 224                 } | 229                 } | 
| 225                 return | 230                 return | 
| 226             } | 231             } | 
| 227 | 232 | 
| 228             let urlSession = urlSessionProvider() | 233             let urlSession = urlSessionProvider() | 
| 229 | 234 | 
| 230             let operations = [DownloadImageOperation(url: icon.url, session: url
     Session)] | 235             let operations = [DownloadImageOperation(url: icon.url, session: url
     Session)] | 
| 231             executeURLOperations(operations) { results in | 236             executeURLOperations(operations) { results in | 
| 232                 let downloadResults: [ImageType] = results.flatMap { result in | 237                 let downloadResults: [ImageType] = results.flatMap { result in | 
| 233                     switch result { | 238                     switch result { | 
| 234                     case let .imageDownloaded(_, image): | 239                     case let .imageDownloaded(_, image): | 
| 235                       return image; | 240                       return image | 
| 236                     case .failed(_): | 241                     case .failed: | 
| 237                       return nil; | 242                       return nil | 
| 238                     default: | 243                     default: | 
| 239                       return nil; | 244                       return nil | 
| 240                     } | 245                     } | 
| 241                 } | 246                 } | 
| 242 | 247 | 
| 243                 DispatchQueue.main.async { | 248                 DispatchQueue.main.async { | 
| 244                     completion(downloadResults.first) | 249                     completion(downloadResults.first) | 
| 245                 } | 250                 } | 
| 246             } | 251             } | 
| 247         } | 252         } | 
| 248     } | 253     } | 
| 249 | 254 | 
| 250   @objc public static func chooseLargestIconSmallerThan(_ icons: [DetectedIcon],
      width: Int, height: Int) -> DetectedIcon? { | 255     @objc | 
| 251     var filteredIcons = icons; | 256     public static func chooseLargestIconSmallerThan(_ icons: [DetectedIcon], wid
     th: Int, height: Int) -> DetectedIcon? { | 
| 252     if (width > 0 && height > 0) { | 257     var filteredIcons = icons | 
| 253       filteredIcons = icons.filter { (icon) -> Bool in | 258     if width > 0 && height > 0 { | 
|  | 259       filteredIcons = icons.filter { icon -> Bool in | 
| 254         if let iconWidth = icon.width, | 260         if let iconWidth = icon.width, | 
| 255            let iconHeight = icon.height { | 261            let iconHeight = icon.height { | 
| 256           return iconWidth <= width  && iconHeight <= height; | 262           return iconWidth <= width && iconHeight <= height | 
| 257         } else { return true; } | 263         } else { return true; } | 
| 258       } | 264       } | 
| 259     } | 265     } | 
| 260     return chooseIcon(filteredIcons, width: 0, height: 0); | 266     return chooseIcon(filteredIcons, width: 0, height: 0) | 
| 261 | 267 | 
| 262   } | 268   } | 
| 263 | 269 | 
| 264   @objc public static func choseIconLargerThan(_ icons: [DetectedIcon], width: I
     nt, height: Int) -> DetectedIcon? { | 270     @objc | 
| 265     var filteredIcons = icons; | 271     public static func choseIconLargerThan(_ icons: [DetectedIcon], width: Int, 
     height: Int) -> DetectedIcon? { | 
| 266     if (width > 0 && height > 0) { | 272     var filteredIcons = icons | 
| 267       filteredIcons = icons.filter { (icon) -> Bool in | 273     if width > 0 && height > 0 { | 
|  | 274       filteredIcons = icons.filter { icon -> Bool in | 
| 268         if let iconWidth = icon.width, | 275         if let iconWidth = icon.width, | 
| 269           let iconHeight = icon.height { | 276           let iconHeight = icon.height { | 
| 270           return iconWidth >= width  && iconHeight >= height; | 277           return iconWidth >= width && iconHeight >= height | 
| 271         } else { return true; } | 278         } else { return true; } | 
| 272       } | 279       } | 
| 273     } | 280     } | 
| 274     return chooseIcon(filteredIcons, width: 0, height: 0); | 281     return chooseIcon(filteredIcons, width: 0, height: 0) | 
| 275 |  | 
| 276   } | 282   } | 
| 277     // MARK: Test hooks | 283     // MARK: Test hooks | 
| 278 | 284 | 
| 279     typealias URLSessionProvider = () -> URLSession | 285     typealias URLSessionProvider = () -> URLSession | 
|  | 286 | 
| 280     @objc static var urlSessionProvider: URLSessionProvider = FavIcon.createDefa
     ultURLSession | 287     @objc static var urlSessionProvider: URLSessionProvider = FavIcon.createDefa
     ultURLSession | 
| 281 | 288 | 
| 282     // MARK: Internal | 289     // MARK: Internal | 
| 283 | 290 | 
| 284     @objc static func createDefaultURLSession() -> URLSession { | 291     @objc | 
|  | 292     static func createDefaultURLSession() -> URLSession { | 
| 285         return URLSession.shared | 293         return URLSession.shared | 
| 286     } | 294     } | 
| 287 | 295 | 
| 288     /// Helper function to choose an icon to use out of a set of available icons
     . If preferred | 296     /// Helper function to choose an icon to use out of a set of available icons
     . If preferred | 
| 289     /// width or height is supplied, the icon closest to the preferred size is c
     hosen. If no | 297     /// width or height is supplied, the icon closest to the preferred size is c
     hosen. If no | 
| 290     /// preferred width or height is supplied, the largest icon (if known) is ch
     osen. | 298     /// preferred width or height is supplied, the largest icon (if known) is ch
     osen. | 
| 291     /// | 299     /// | 
| 292     /// - parameter icons: The icons to choose from. | 300     /// - parameter icons: The icons to choose from. | 
| 293     /// - parameter width: The preferred icon width. | 301     /// - parameter width: The preferred icon width. | 
| 294     /// - parameter height: The preferred icon height. | 302     /// - parameter height: The preferred icon height. | 
| 295     /// - returns: The chosen icon, or `nil`, if `icons` is empty. | 303     /// - returns: The chosen icon, or `nil`, if `icons` is empty. | 
| 296     static func chooseIcon(_ icons: [DetectedIcon], width: Int? = nil, height: I
     nt? = nil) -> DetectedIcon? { | 304     static func chooseIcon(_ icons: [DetectedIcon], width: Int? = nil, height: I
     nt? = nil) -> DetectedIcon? { | 
| 297         guard icons.count > 0 else { return nil } | 305         guard icons.count > 0 else { return nil } | 
| 298 | 306 | 
| 299         let iconsInPreferredOrder = icons.sorted { left, right in | 307         let iconsInPreferredOrder = icons.sorted { left, right in | 
| 300             if width! > 0 || height! > 0  { | 308             if width! > 0 || height! > 0 { | 
| 301                 let preferredWidth = width, preferredHeight = height, | 309                 let preferredWidth = width, preferredHeight = height, | 
| 302                 widthLeft = left.width, heightLeft = left.height, | 310                 widthLeft = left.width, heightLeft = left.height, | 
| 303                 widthRight = right.width, heightRight = right.height; | 311                 widthRight = right.width, heightRight = right.height | 
| 304                 // Which is closest to preferred size? | 312                 // Which is closest to preferred size? | 
| 305                 let deltaA = abs(widthLeft! - preferredWidth!) * abs(heightLeft!
      - preferredHeight!) | 313                 let deltaA = abs(widthLeft! - preferredWidth!) * abs(heightLeft!
      - preferredHeight!) | 
| 306                 let deltaB = abs(widthRight! - preferredWidth!) * abs(heightRigh
     t! - preferredHeight!) | 314                 let deltaB = abs(widthRight! - preferredWidth!) * abs(heightRigh
     t! - preferredHeight!) | 
| 307                 return deltaA < deltaB | 315                 return deltaA < deltaB | 
| 308             } else { | 316             } else { | 
| 309                 if let areaLeft = left.area, let areaRight = right.area { | 317                 if let areaLeft = left.area, let areaRight = right.area { | 
| 310                     // Which is larger? | 318                     // Which is larger? | 
| 311                     return areaRight < areaLeft | 319                     return areaRight < areaLeft | 
| 312                 } | 320                 } | 
| 313             } | 321             } | 
| 314 | 322 | 
| 315             if left.area != nil { | 323             if left.area != nil { | 
| 316                 // Only A has dimensions, prefer it. | 324                 // Only A has dimensions, prefer it. | 
| 317                 return true | 325                 return true | 
| 318             } | 326             } | 
| 319             if right.area != nil { | 327             if right.area != nil { | 
| 320                 // Only B has dimensions, prefer it. | 328                 // Only B has dimensions, prefer it. | 
| 321                 return false | 329                 return false | 
| 322             } | 330             } | 
| 323 | 331 | 
| 324             // Neither has dimensions, order by enum value | 332             // Neither has dimensions, order by enum value | 
| 325             return left.type.rawValue < right.type.rawValue | 333             return left.type.rawValue < right.type.rawValue | 
| 326         } | 334         } | 
| 327 | 335 | 
| 328         return iconsInPreferredOrder.first! | 336         return iconsInPreferredOrder.first! | 
| 329     } | 337     } | 
| 330 | 338 | 
| 331     fileprivate override init () { | 339     private override init () { | 
| 332     } | 340     } | 
| 333 } | 341 } | 
| 334 | 342 | 
| 335 /// Enumerates errors that can be thrown while detecting or downloading icons. | 343 /// Enumerates errors that can be thrown while detecting or downloading icons. | 
| 336 enum IconError: Error { | 344 enum IconError: Error { | 
| 337     /// The base URL specified is not a valid URL. | 345     /// The base URL specified is not a valid URL. | 
| 338     case invalidBaseURL | 346     case invalidBaseURL | 
| 339     /// At least one icon to must be specified for downloading. | 347     /// At least one icon to must be specified for downloading. | 
| 340     case atLeastOneOneIconRequired | 348     case atLeastOneOneIconRequired | 
| 341     /// Unexpected response when downloading | 349     /// Unexpected response when downloading | 
| 342     case invalidDownloadResponse | 350     case invalidDownloadResponse | 
| 343     /// No icons were detected, so nothing could be downloaded. | 351     /// No icons were detected, so nothing could be downloaded. | 
| 344     case noIconsDetected | 352     case noIconsDetected | 
| 345 } | 353 } | 
| 346 | 354 | 
| 347 extension FavIcon { | 355 extension FavIcon { | 
| 348     /// Convenience overload for `scan(url:completion:)` that takes a `String` | 356     /// Convenience overload for `scan(url:completion:)` that takes a `String` | 
| 349     /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
     ot a valid URL. | 357     /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
     ot a valid URL. | 
| 350     /// | 358     /// | 
| 351     /// - parameter url: The base URL to scan. | 359     /// - parameter url: The base URL to scan. | 
| 352     /// - parameter completion: A closure to call when the scan has completed. T
     he closure will be called | 360     /// - parameter completion: A closure to call when the scan has completed. T
     he closure will be called | 
| 353     ///                         on the main queue. | 361     ///                         on the main queue. | 
| 354     /// - throws: An `IconError` if the scan failed for some reason. | 362     /// - throws: An `IconError` if the scan failed for some reason. | 
| 355     @objc public static func scan(_ url: String, completion: @escaping ([Detecte
     dIcon], [String:String]) -> Void) throws { | 363     @objc | 
|  | 364     public static func scan(_ url: String, completion: @escaping ([DetectedIcon]
     , [String: String]) -> Void) throws { | 
| 356         guard let url = URL(string: url) else { throw IconError.invalidBaseURL } | 365         guard let url = URL(string: url) else { throw IconError.invalidBaseURL } | 
| 357         scan(url, completion: completion) | 366         scan(url, completion: completion) | 
| 358     } | 367     } | 
| 359 | 368 | 
| 360     /// Convenience overload for `downloadAll(url:completion:)` that takes a `St
     ring` | 369     /// Convenience overload for `downloadAll(url:completion:)` that takes a `St
     ring` | 
| 361     /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
     ot a valid URL. | 370     /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
     ot a valid URL. | 
| 362     /// | 371     /// | 
| 363     /// - parameter url: The URL to scan for icons. | 372     /// - parameter url: The URL to scan for icons. | 
| 364     /// - parameter completion: A closure to call when all download tasks have r
     esults available | 373     /// - parameter completion: A closure to call when all download tasks have r
     esults available | 
| 365     ///                         (successful or otherwise). The closure will be c
     alled on the main queue. | 374     ///                         (successful or otherwise). The closure will be c
     alled on the main queue. | 
| 366     /// - throws: An `IconError` if the scan or download failed for some reason. | 375     /// - throws: An `IconError` if the scan or download failed for some reason. | 
| 367     @objc public static func downloadAll(_ url: String, completion: @escaping ([
     ImageType]) -> Void) throws { | 376     @objc | 
|  | 377     public static func downloadAll(_ url: String, completion: @escaping ([ImageT
     ype]) -> Void) throws { | 
| 368         guard let url = URL(string: url) else { throw IconError.invalidBaseURL } | 378         guard let url = URL(string: url) else { throw IconError.invalidBaseURL } | 
| 369         downloadAll(url, completion: completion) | 379         downloadAll(url, completion: completion) | 
| 370     } | 380     } | 
| 371 | 381 | 
| 372     /// Convenience overload for `downloadPreferred(url:width:height:completion:
     )` that takes a `String` | 382     /// Convenience overload for `downloadPreferred(url:width:height:completion:
     )` that takes a `String` | 
| 373     /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
     ot a valid URL. | 383     /// instead of a `URL` as the URL parameter. Throws an error if the URL is n
     ot a valid URL. | 
| 374     /// | 384     /// | 
| 375     /// - parameter url: The URL to scan for icons. | 385     /// - parameter url: The URL to scan for icons. | 
| 376     /// - parameter width: The preferred icon width, in pixels, or `nil`. | 386     /// - parameter width: The preferred icon width, in pixels, or `nil`. | 
| 377     /// - parameter height: The preferred icon height, in pixels, or `nil`. | 387     /// - parameter height: The preferred icon height, in pixels, or `nil`. | 
| 378     /// - parameter completion: A closure to call when the download task has pro
     duced a result. The closure will | 388     /// - parameter completion: A closure to call when the download task has pro
     duced a result. The closure will | 
| 379     ///                         be called on the main queue. | 389     ///                         be called on the main queue. | 
| 380     /// - throws: An appropriate `IconError` if downloading failed for some reas
     on. | 390     /// - throws: An appropriate `IconError` if downloading failed for some reas
     on. | 
| 381     @objc public static func downloadPreferred(_ url: String, | 391     @objc | 
|  | 392     public static func downloadPreferred(_ url: String, | 
| 382                                          width: Int, | 393                                          width: Int, | 
| 383                                          height: Int, | 394                                          height: Int, | 
| 384                                          completion: @escaping (ImageType?) -> V
     oid) throws { | 395                                          completion: @escaping (ImageType?) -> V
     oid) throws { | 
| 385         guard let url = URL(string: url) else { throw IconError.invalidBaseURL } | 396         guard let url = URL(string: url) else { throw IconError.invalidBaseURL } | 
| 386         downloadPreferred(url, width: width, height: height, completion: complet
     ion) | 397         downloadPreferred(url, width: width, height: height, completion: complet
     ion) | 
| 387     } | 398     } | 
| 388 } | 399 } | 
| 389 | 400 | 
| 390 extension DetectedIcon { | 401 extension DetectedIcon { | 
| 391     /// The area of a detected icon, if known. | 402     /// The area of a detected icon, if known. | 
| 392     var area: Int? { | 403     var area: Int? { | 
| 393         if let width = width, let height = height { | 404         if let width = width, let height = height { | 
| 394             return width * height | 405             return width * height | 
| 395         } | 406         } | 
| 396         return nil | 407         return nil | 
| 397     } | 408     } | 
| 398 } | 409 } | 
| 399 |  | 
| 400 |  | 
| OLD | NEW | 
|---|