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