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