| 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 |