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 |