Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: FavIcon/IconExtraction.swift

Issue 29664569: Favicon: Issue 6245 - SwiftLinted project files (Closed)
Patch Set: Favicon: Issue 6245 - Swiftlinted project files Created Jan. 16, 2018, 12:55 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
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 import FavIcon.XMLDocument
18 import Foundation 19 import Foundation
d108 2018/02/13 21:48:44 This import is duplicated by the one above it and
19 import FavIcon.XMLDocument
20 20
21 /// Represents an icon size. 21 /// Represents an icon size.
22 struct IconSize : Hashable, Equatable { 22 struct IconSize: Hashable, Equatable {
23 var hashValue: Int { 23 var hashValue: Int {
24 return width.hashValue ^ height.hashValue 24 return width.hashValue ^ height.hashValue
25 } 25 }
26 26
27 static func ==(lhs: IconSize, rhs: IconSize) -> Bool { 27 static func == (lhs: IconSize, rhs: IconSize) -> Bool {
28 return lhs.width == rhs.width && lhs.height == rhs.height 28 return lhs.width == rhs.width && lhs.height == rhs.height
29 } 29 }
30 30
31 /// The width of the icon. 31 /// The width of the icon.
32 let width: Int 32 let width: Int
33 /// The height of the icon. 33 /// The height of the icon.
34 let height: Int 34 let height: Int
35 } 35 }
36 36
37 private let kRelIconTypeMap: [IconSize: DetectedIconType] = [ 37 private let kRelIconTypeMap: [IconSize: DetectedIconType] = [
38 IconSize(width: 16, height: 16): .classic, 38 IconSize(width: 16, height: 16): .classic,
39 IconSize(width: 32, height: 32): .appleOSXSafariTab, 39 IconSize(width: 32, height: 32): .appleOSXSafariTab,
40 IconSize(width: 96, height: 96): .googleTV, 40 IconSize(width: 96, height: 96): .googleTV,
41 IconSize(width: 192, height: 192): .googleAndroidChrome, 41 IconSize(width: 192, height: 192): .googleAndroidChrome,
42 IconSize(width: 196, height: 196): .googleAndroidChrome 42 IconSize(width: 196, height: 196): .googleAndroidChrome
43 ] 43 ]
44 44
45 private let kMicrosoftSizeMap: [String: IconSize] = [ 45 private let kMicrosoftSizeMap: [String: IconSize] = [
46 "msapplication-tileimage": IconSize(width: 144, height: 144), 46 "msapplication-tileimage": IconSize(width: 144, height: 144),
47 "msapplication-square70x70logo": IconSize(width: 70, height: 70), 47 "msapplication-square70x70logo": IconSize(width: 70, height: 70),
48 "msapplication-square150x150logo": IconSize(width: 150, height: 150), 48 "msapplication-square150x150logo": IconSize(width: 150, height: 150),
49 "msapplication-wide310x150logo": IconSize(width: 310, height: 150), 49 "msapplication-wide310x150logo": IconSize(width: 310, height: 150),
50 "msapplication-square310x310logo": IconSize(width: 310, height: 310), 50 "msapplication-square310x310logo": IconSize(width: 310, height: 310)
51 ] 51 ]
52 52
53 private let siteImage: [String: IconSize] = [ 53 private let siteImage: [String: IconSize] = [
54 "og:image": IconSize(width: 1024, height: 512), 54 "og:image": IconSize(width: 1024, height: 512),
55 "twitter:image": IconSize(width: 1024, height: 512) 55 "twitter:image": IconSize(width: 1024, height: 512)
56 ] 56 ]
57 57
58 /// Extracts a list of icons from the `<head>` section of an HTML document. 58 /// Extracts a list of icons from the `<head>` section of an HTML document.
59 /// 59 ///
60 /// - parameter document: An HTML document to process. 60 /// - parameter document: An HTML document to process.
61 /// - parameter baseURL: A base URL to combine with any relative image paths. 61 /// - parameter baseURL: A base URL to combine with any relative image paths.
62 /// - parameter returns: An array of `DetectedIcon` structures. 62 /// - parameter returns: An array of `DetectedIcon` structures.
63 // swiftlint:disable function_body_length 63 // swiftlint:disable function_body_length
64 // swiftlint:disable cyclomatic_complexity 64 // swiftlint:disable cyclomatic_complexity
65 func examineHTMLMeta(_ document: HTMLDocument, baseURL: URL) -> [String:String] { 65 func examineHTMLMeta(_ document: HTMLDocument, baseURL: URL) -> [String: String] {
66 var resp: [String:String] = [:] 66 var resp: [String: String] = [:]
67 for meta in document.query("/html/head/meta") { 67 for meta in document.query("/html/head/meta") {
68 if let property = meta.attributes["property"]?.lowercased(), 68 if let property = meta.attributes["property"]?.lowercased(),
69 let content = meta.attributes["content"]{ 69 let content = meta.attributes["content"] {
70 switch property { 70 switch property {
71 case "og:url": 71 case "og:url":
72 resp["og:url"] = content; 72 resp["og:url"] = content
73 break 73 case "og:description":
74 case "og:description": 74 resp["description"] = content
75 resp["description"] = content; 75 case "og:image":
76 break 76 resp["image"] = content
77 case "og:image": 77 case "og:title":
78 resp["image"] = content; 78 resp["title"] = content
79 break 79 case "og:site_name":
80 case "og:title": 80 resp["site_name"] = content
81 resp["title"] = content; 81 default:
82 break 82 break
83 case "og:site_name": 83 }
84 resp["site_name"] = content; 84 }
85 break 85 if let name = meta.attributes["name"]?.lowercased(),
86 default: 86 let content = meta.attributes["content"],
87 break 87 name == "description" {
88 } 88 resp["description"] = resp["description"] ?? content
89 }
89 } 90 }
90 if let name = meta.attributes["name"]?.lowercased(), 91
91 let content = meta.attributes["content"], 92 for title in document.query("/html/head/title") {
92 name == "description" { 93 if let titleString = title.contents {
93 resp["description"] = resp["description"] ?? content; 94 resp["title"] = resp["title"] ?? titleString
95 }
94 } 96 }
95 } 97
96 98 for link in document.query("/html/head/link") {
97 for title in document.query("/html/head/title") { 99 if let rel = link.attributes["rel"],
98 if let titleString = title.contents { 100 let href = link.attributes["href"],
99 resp["title"] = resp["title"] ?? titleString; 101 let url = URL(string: href, relativeTo: baseURL) {
102 switch rel.lowercased() {
103 case "canonical":
104 resp["canonical"] = url.absoluteString
105 case "amphtml":
106 resp["amphtml"] = url.absoluteString
107 case "search":
108 resp["search"] = url.absoluteString
109 case "fluid-icon":
110 resp["fluid-icon"] = url.absoluteString
111 case "alternate":
112 let application = link.attributes["application"]
113 if application == "application/atom+xml" {
114 resp["atom"] = url.absoluteString
115 }
116 default:
117 break
118 }
119 }
100 } 120 }
101 } 121 return resp
102
103 for link in document.query("/html/head/link") {
104 if let rel = link.attributes["rel"],
105 let href = link.attributes["href"],
106 let url = URL(string: href, relativeTo: baseURL)
107 {
108 switch rel.lowercased() {
109 case "canonical":
110 resp["canonical"] = url.absoluteString;
111 break
112 case "amphtml":
113 resp["amphtml"] = url.absoluteString;
114 break
115 case "search":
116 resp["search"] = url.absoluteString;
117 break
118 case "fluid-icon":
119 resp["fluid-icon"] = url.absoluteString;
120 break
121 case "alternate":
122 let application = link.attributes["application"]
123 if application == "application/atom+xml" {
124 resp["atom"] = url.absoluteString;
125 }
126 break
127 default:
128 break
129 }
130 }
131 }
132
133 return resp;
134 } 122 }
135 123
136 func extractHTMLHeadIcons(_ document: HTMLDocument, baseURL: URL) -> [DetectedIc on] { 124 func extractHTMLHeadIcons(_ document: HTMLDocument, baseURL: URL) -> [DetectedIc on] {
137 var icons: [DetectedIcon] = [] 125 var icons: [DetectedIcon] = []
138 126
139 for link in document.query("/html/head/link") { 127 for link in document.query("/html/head/link") {
140 if let rel = link.attributes["rel"], 128 if let rel = link.attributes["rel"],
141 let href = link.attributes["href"], 129 let href = link.attributes["href"],
142 let url = URL(string: href, relativeTo: baseURL) { 130 let url = URL(string: href, relativeTo: baseURL) {
143 switch rel.lowercased() { 131 switch rel.lowercased() {
144 case "shortcut icon": 132 case "shortcut icon":
145 icons.append(DetectedIcon(url: url.absoluteURL, type:.shortcut)) 133 icons.append(DetectedIcon(url: url.absoluteURL, type: .shortcut) )
146 break 134 case "icon":
147 case "icon": 135 if let type = link.attributes["type"], type.lowercased() == "ima ge/png" {
148 if let type = link.attributes["type"], type.lowercased() == "image/png" { 136 let sizes = parseHTMLIconSizes(link.attributes["sizes"])
149 let sizes = parseHTMLIconSizes(link.attributes["sizes"]) 137 if sizes.count > 0 {
150 if sizes.count > 0 { 138 for size in sizes {
151 for size in sizes { 139 if let type = kRelIconTypeMap[size] {
152 if let type = kRelIconTypeMap[size] { 140 icons.append(DetectedIcon(url: url,
141 type: type,
142 width: size.width,
143 height: size.height))
144 }
145 }
146 } else {
147 icons.append(DetectedIcon(url: url.absoluteURL, type: .c lassic))
148 }
149 } else {
150 icons.append(DetectedIcon(url: url.absoluteURL, type: .class ic))
151 }
152 case "apple-touch-icon":
153 let sizes = parseHTMLIconSizes(link.attributes["sizes"])
154 if sizes.count > 0 {
155 for size in sizes {
156 icons.append(DetectedIcon(url: url.absoluteURL,
157 type: .appleIOSWebClip,
158 width: size.width,
159 height: size.height))
160 }
161 } else {
162 icons.append(DetectedIcon(url: url.absoluteURL,
163 type: .appleIOSWebClip,
164 width: 60,
165 height: 60))
166 }
167 default:
168 break
169 }
170 }
171 }
172
173 for meta in document.query("/html/head/meta") {
174 if let name = meta.attributes["name"]?.lowercased(),
175 let content = meta.attributes["content"],
176 let url = URL(string: content, relativeTo: baseURL),
177 let size = kMicrosoftSizeMap[name] {
178 icons.append(DetectedIcon(url: url,
179 type: .microsoftPinnedSite,
180 width: size.width,
181 height: size.height))
182 } else if
183 let property = meta.attributes["property"]?.lowercased(),
184 let content = meta.attributes["content"],
185 let url = URL(string: content, relativeTo: baseURL),
186 let size = siteImage[property] {
153 icons.append(DetectedIcon(url: url, 187 icons.append(DetectedIcon(url: url,
154 type: type, 188 type: .FBImage,
155 width: size.width, 189 width: size.width,
156 height: size.height)) 190 height: size.height))
157 }
158 }
159 } else {
160 icons.append(DetectedIcon(url: url.absoluteURL, type: .classic))
161 }
162 } else {
163 icons.append(DetectedIcon(url: url.absoluteURL, type: .classic))
164 } 191 }
165 case "apple-touch-icon":
166 let sizes = parseHTMLIconSizes(link.attributes["sizes"])
167 if sizes.count > 0 {
168 for size in sizes {
169 icons.append(DetectedIcon(url: url.absoluteURL,
170 type: .appleIOSWebClip,
171 width: size.width,
172 height: size.height))
173 }
174 } else {
175 icons.append(DetectedIcon(url: url.absoluteURL,
176 type: .appleIOSWebClip,
177 width: 60,
178 height: 60))
179 }
180 default:
181 break
182 }
183 } 192 }
184 } 193
185 194 return icons
186 for meta in document.query("/html/head/meta") {
187 if let name = meta.attributes["name"]?.lowercased(),
188 let content = meta.attributes["content"],
189 let url = URL(string: content, relativeTo: baseURL),
190 let size = kMicrosoftSizeMap[name] {
191 icons.append(DetectedIcon(url: url,
192 type: .microsoftPinnedSite,
193 width: size.width,
194 height: size.height))
195 } else if
196 let property = meta.attributes["property"]?.lowercased(),
197 let content = meta.attributes["content"],
198 let url = URL(string: content, relativeTo: baseURL),
199 let size = siteImage[property] {
200 icons.append(DetectedIcon(url: url,
201 type: .FBImage,
202 width: size.width,
203 height: size.height))
204 }
205 }
206
207 return icons
208 } 195 }
209 // swiftlint:enable cyclomatic_complexity 196 // swiftlint:enable cyclomatic_complexity
210 // swiftlint:enable function_body_length 197 // swiftlint:enable function_body_length
211 198
212 /// Extracts a list of icons from a Web Application Manifest file 199 /// Extracts a list of icons from a Web Application Manifest file
213 /// 200 ///
214 /// - parameter jsonString: A JSON string containing the contents of the manifes t file. 201 /// - parameter jsonString: A JSON string containing the contents of the manifes t file.
215 /// - parameter baseURL: A base URL to combine with any relative image paths. 202 /// - parameter baseURL: A base URL to combine with any relative image paths.
216 /// - returns: An array of `DetectedIcon` structures. 203 /// - returns: An array of `DetectedIcon` structures.
217 func extractManifestJSONIcons(_ jsonString: String, baseURL: URL) -> [DetectedIc on] { 204 func extractManifestJSONIcons(_ jsonString: String, baseURL: URL) -> [DetectedIc on] {
218 var icons: [DetectedIcon] = [] 205 var icons: [DetectedIcon] = []
219 206
220 if let data = jsonString.data(using: String.Encoding.utf8), 207 if let data = jsonString.data(using: String.Encoding.utf8),
221 let object = try? JSONSerialization.jsonObject(with: data, options: JSONSeri alization.ReadingOptions()), 208 let object = try? JSONSerialization.jsonObject(with: data, options: JSON Serialization.ReadingOptions()),
222 let manifest = object as? NSDictionary, 209 let manifest = object as? NSDictionary,
223 let manifestIcons = manifest["icons"] as? [NSDictionary] { 210 let manifestIcons = manifest["icons"] as? [NSDictionary] {
224 for icon in manifestIcons { 211 for icon in manifestIcons {
225 if let type = icon["type"] as? String, type.lowercased() == "image/png", 212 if let type = icon["type"] as? String, type.lowercased() == "image/p ng",
226 let src = icon["src"] as? String, 213 let src = icon["src"] as? String,
227 let url = URL(string: src, relativeTo: baseURL)?.absoluteURL { 214 let url = URL(string: src, relativeTo: baseURL)?.absoluteURL {
228 let sizes = parseHTMLIconSizes(icon["sizes"] as? String) 215 let sizes = parseHTMLIconSizes(icon["sizes"] as? String)
229 if sizes.count > 0 { 216 if sizes.count > 0 {
230 for size in sizes { 217 for size in sizes {
231 icons.append(DetectedIcon(url: url, 218 icons.append(DetectedIcon(url: url,
232 type: .webAppManifest, 219 type: .webAppManifest,
233 width: size.width, 220 width: size.width,
234 height: size.height)) 221 height: size.height))
235 } 222 }
236 } else { 223 } else {
237 icons.append(DetectedIcon(url: url, type: .webAppManifest)) 224 icons.append(DetectedIcon(url: url, type: .webAppManifest))
225 }
226 }
238 } 227 }
239 }
240 } 228 }
241 } 229
242 230 return icons
243 return icons
244 } 231 }
245 232
246 /// Extracts a list of icons from a Microsoft browser configuration XML document . 233 /// Extracts a list of icons from a Microsoft browser configuration XML document .
247 /// 234 ///
248 /// - parameter document: An `XMLDocument` for the Microsoft browser configurati on file. 235 /// - parameter document: An `XMLDocument` for the Microsoft browser configurati on file.
249 /// - parameter baseURL: A base URL to combine with any relative image paths. 236 /// - parameter baseURL: A base URL to combine with any relative image paths.
250 /// - returns: An array of `DetectedIcon` structures. 237 /// - returns: An array of `DetectedIcon` structures.
251 func extractBrowserConfigXMLIcons(_ document: LBXMLDocument, baseURL: URL) -> [D etectedIcon] { 238 func extractBrowserConfigXMLIcons(_ document: LBXMLDocument, baseURL: URL) -> [D etectedIcon] {
252 var icons: [DetectedIcon] = [] 239 var icons: [DetectedIcon] = []
253 240
254 for tile in document.query("/browserconfig/msapplication/tile/*") { 241 for tile in document.query("/browserconfig/msapplication/tile/*") {
255 if let src = tile.attributes["src"], 242 if let src = tile.attributes["src"],
256 let url = URL(string: src, relativeTo: baseURL)?.absoluteURL { 243 let url = URL(string: src, relativeTo: baseURL)?.absoluteURL {
257 switch tile.name.lowercased() { 244 switch tile.name.lowercased() {
258 case "tileimage": 245 case "tileimage":
259 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 1 44, height: 144)) 246 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 144, height: 144))
260 break 247 case "square70x70logo":
261 case "square70x70logo": 248 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 70, height: 70))
262 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 7 0, height: 70)) 249 case "square150x150logo":
263 break 250 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 150, height: 150))
264 case "square150x150logo": 251 case "wide310x150logo":
265 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 1 50, height: 150)) 252 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 310, height: 150))
266 break 253 case "square310x310logo":
267 case "wide310x150logo": 254 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 310, height: 310))
268 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 3 10, height: 150)) 255 default:
269 break 256 break
270 case "square310x310logo": 257 }
271 icons.append(DetectedIcon(url: url, type: .microsoftPinnedSite, width: 3 10, height: 310)) 258 }
272 break
273 default:
274 break
275 }
276 } 259 }
277 } 260
278 261 return icons
279 return icons
280 } 262 }
281 263
282 /// Extracts the Web App Manifest URLs from an HTML document, if any. 264 /// Extracts the Web App Manifest URLs from an HTML document, if any.
283 /// 265 ///
284 /// - parameter document: The HTML document to scan for Web App Manifest URLs 266 /// - parameter document: The HTML document to scan for Web App Manifest URLs
285 /// - parameter baseURL: The base URL that any 'href' attributes are relative to . 267 /// - parameter baseURL: The base URL that any 'href' attributes are relative to .
286 /// - returns: An array of Web App Manifest `URL`s. 268 /// - returns: An array of Web App Manifest `URL`s.
287 func extractWebAppManifestURLs(_ document: HTMLDocument, baseURL: URL) -> [URL] { 269 func extractWebAppManifestURLs(_ document: HTMLDocument, baseURL: URL) -> [URL] {
288 var urls: [URL] = [] 270 var urls: [URL] = []
289 for link in document.query("/html/head/link") { 271 for link in document.query("/html/head/link") {
290 if let rel = link.attributes["rel"]?.lowercased(), rel == "manifest", 272 if let rel = link.attributes["rel"]?.lowercased(), rel == "manifest",
291 let href = link.attributes["href"], let manifestURL = URL(string: href, re lativeTo: baseURL) { 273 let href = link.attributes["href"], let manifestURL = URL(string: hr ef, relativeTo: baseURL) {
292 urls.append(manifestURL) 274 urls.append(manifestURL)
275 }
293 } 276 }
294 } 277 return urls
295 return urls
296 } 278 }
297 279
298 /// Extracts the first browser config XML file URL from an HTML document, if any . 280 /// Extracts the first browser config XML file URL from an HTML document, if any .
299 /// 281 ///
300 /// - parameter document: The HTML document to scan for browser config XML file URLs. 282 /// - parameter document: The HTML document to scan for browser config XML file URLs.
301 /// - parameter baseURL: The base URL that any 'href' attributes are relative to . 283 /// - parameter baseURL: The base URL that any 'href' attributes are relative to .
302 /// - returns: A named tuple describing the file URL or a flag indicating that t he server 284 /// - returns: A named tuple describing the file URL or a flag indicating that t he server
303 /// explicitly requested that the file not be downloaded. 285 /// explicitly requested that the file not be downloaded.
304 func extractBrowserConfigURL(_ document: HTMLDocument, baseURL: URL) -> (url: UR L?, disabled: Bool) { 286 func extractBrowserConfigURL(_ document: HTMLDocument, baseURL: URL) -> (url: UR L?, disabled: Bool) {
305 for meta in document.query("/html/head/meta") { 287 for meta in document.query("/html/head/meta") {
306 if let name = meta.attributes["name"]?.lowercased(), name == "msapplication- config", 288 if let name = meta.attributes["name"]?.lowercased(), name == "msapplicat ion-config",
307 let content = meta.attributes["content"] { 289 let content = meta.attributes["content"] {
308 if content.lowercased() == "none" { 290 if content.lowercased() == "none" {
309 // Explicitly asked us not to download the file. 291 // Explicitly asked us not to download the file.
310 return (url: nil, disabled: true) 292 return (url: nil, disabled: true)
311 } else { 293 } else {
312 return (url: URL(string: content, relativeTo: baseURL)?.absoluteURL, dis abled: false) 294 return (url: URL(string: content, relativeTo: baseURL)?.absolute URL, disabled: false)
313 } 295 }
296 }
314 } 297 }
315 } 298 return (url: nil, disabled: false)
316 return (url: nil, disabled: false)
317 } 299 }
318 300
319 /// Helper function for parsing a W3 `sizes` attribute value. 301 /// Helper function for parsing a W3 `sizes` attribute value.
320 /// 302 ///
321 /// - parameter string: If not `nil`, the value of the attribute to parse (e.g. `50x50 144x144`). 303 /// - parameter string: If not `nil`, the value of the attribute to parse (e.g. `50x50 144x144`).
322 /// - returns: An array of `IconSize` structs for each size found. 304 /// - returns: An array of `IconSize` structs for each size found.
323 func parseHTMLIconSizes(_ string: String?) -> [IconSize] { 305 func parseHTMLIconSizes(_ string: String?) -> [IconSize] {
324 var sizes: [IconSize] = [] 306 var sizes: [IconSize] = []
325 if let string = string?.lowercased(), string != "any" { 307 if let string = string?.lowercased(), string != "any" {
326 for size in string.components(separatedBy: .whitespaces) { 308 for size in string.components(separatedBy: .whitespaces) {
327 let parts = size.components(separatedBy: "x") 309 let parts = size.components(separatedBy: "x")
328 if parts.count != 2 { continue } 310 if parts.count != 2 { continue }
329 if let width = Int(parts[0]), let height = Int(parts[1]) { 311 if let width = Int(parts[0]), let height = Int(parts[1]) {
330 sizes.append(IconSize(width: width, height: height)) 312 sizes.append(IconSize(width: width, height: height))
331 } 313 }
314 }
332 } 315 }
333 } 316 return sizes
334 return sizes
335 } 317 }
336
OLDNEW

Powered by Google App Engine
This is Rietveld