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