Skip to content

Commit

Permalink
Merge pull request #41 from will-lumley/feature/web-application-manif…
Browse files Browse the repository at this point in the history
…est-file

Web Application Manifest File
  • Loading branch information
will-lumley authored Sep 1, 2021
2 parents 7162634 + bc076d2 commit f62aeb2
Show file tree
Hide file tree
Showing 11 changed files with 537 additions and 52 deletions.
6 changes: 6 additions & 0 deletions Example/Pods/Pods.xcodeproj/project.pbxproj

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 82 additions & 7 deletions Example/Tests/FaviconFinderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import XCTest
@testable import FaviconFinder

class FaviconFinderTests: XCTestCase {

let googleUrl = URL(string: "https://google.com")!
let appleUrl = URL(string: "https://apple.com")!
let realFaviconGeneratorUrl = URL(string: "https://realfavicongenerator.net/blog/apple-touch-icon-the-good-the-bad-the-ugly/")!
let webApplicationManifestUrl = URL(string: "https://googlechrome.github.io/samples/web-application-manifest/")!

override func setUp() {

Expand All @@ -26,30 +28,103 @@ class FaviconFinderTests: XCTestCase {
func testFaviconIcoFind() {
let expectation = self.expectation(description: "Favicon.ico FaviconFind")

FaviconFinder(url: self.googleUrl).downloadFavicon { result in
FaviconFinder(
url: self.googleUrl,
preferredType: .ico,
preferences: [:],
logEnabled: true
).downloadFavicon { result in
switch result {
case .success:
case .success(let favicon):
// Ensure that our favicon is actually valid
XCTAssertTrue(favicon.image.isValidImage)

// Ensure that our favicon was retrieved from the desired source
XCTAssertTrue(favicon.downloadType == .ico)

// Let the test know that we got our favicon back
expectation.fulfill()

case .failure(let error):
XCTAssert(false, "Failed to download favicon.ico file: \(error)")
}
}
waitForExpectations(timeout: 20.0, handler: nil)

waitForExpectations(timeout: 5.0, handler: nil)
}

func testFaviconHtmlFind() {
let expectation = self.expectation(description: "HTML FaviconFind")

FaviconFinder(url: self.realFaviconGeneratorUrl).downloadFavicon { result in
FaviconFinder(
url: self.realFaviconGeneratorUrl,
preferredType: .html,
preferences: [:],
logEnabled: true
).downloadFavicon { result in
switch result {
case .success:
case .success(let favicon):

// Ensure that our favicon is actually valid
XCTAssertTrue(favicon.image.isValidImage)

// Ensure that our favicon was retrieved from the desired source
XCTAssertTrue(favicon.downloadType == .html)

// Let the test know that we got our favicon back
expectation.fulfill()

case .failure(let error):
XCTAssert(false, "Failed to download favicon from HTML header: \(error.localizedDescription)")
}
}

waitForExpectations(timeout: 20.0, handler: nil)
waitForExpectations(timeout: 5.0, handler: nil)
}

func testFaviconWebApplicationManifestFileFind() {
let expectation = self.expectation(description: "WebApplicationManifestFile FaviconFind")

FaviconFinder(
url: self.webApplicationManifestUrl,
preferredType: .webApplicationManifestFile,
preferences: [:],
logEnabled: true
).downloadFavicon { result in
switch result {
case .success(let favicon):

// Ensure that our favicon is actually valid
XCTAssertTrue(favicon.image.isValidImage)

// Ensure that our favicon was retrieved from the desired source
XCTAssertTrue(favicon.downloadType == .webApplicationManifestFile)

// Let the test know that we got our favicon back
expectation.fulfill()

case .failure(let error):
XCTAssert(false, "Failed to download favicon from WebApplicationManifestFile header: \(error.localizedDescription)")
}
}

waitForExpectations(timeout: 5.0, handler: nil)
}
}

private extension FaviconImage {

var isValidImage: Bool {
#if targetEnvironment(macCatalyst)
return self.isValid

#elseif canImport(AppKit)
return self.isValid

#elseif canImport(UIKit)
return self.isValid

#endif
}

}
2 changes: 1 addition & 1 deletion FaviconFinder.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Pod::Spec.new do |s|

s.name = "FaviconFinder"
s.version = "3.1.0"
s.version = "3.2.0"
s.summary = "A pure Swift library to detect favicons use by a website."
s.homepage = "https://github.com/will-lumley/FaviconFinder.git"
s.license = { :type => 'MIT', :file => 'LICENSE.txt' }
Expand Down
72 changes: 50 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@

FaviconFinder is a small, pure Swift library designed for iOS and macOS applications that allows you to detect favicons used by a website.

Why not just download the file that exists at `https://site.com/favicon.ico`? There are multiple places that a developer can place there favicon, not just at the root directory with the specific filename of `fav.ico`. FaviconFinder handles the dirty work for you and iterates through the numerous locations that the favicon could be located at, and simply delivers the image to you in a closure, once the image is found.
Why not just download the file that exists at `https://site.com/favicon.ico`? There are multiple places that a developer can place there favicon, not just at the root directory with the specific filename of `favicon.ico`. The favicons address may be linked within the HTML header tags, or it may be within a web application manifest JSON file, or it could even be a file with a custom filename.

FaviconFinder handles the dirty work for you and iterates through the numerous locations that the favicon could be located at, and simply delivers the image to you in a closure, once the image is found.


FaviconFinder will:
- [x] Detect the favicon in the root directory of the URL provided
- [x] Will automatically check if the favicon is located within the root URL if the subdomain failed (Will check `https://site.com/favicon.ico` if `https://subdomain.site.com/favicon.ico` fails)
- [x] Automatically check if the favicon is located within the root URL if the subdomain failed (Will check `https://site.com/favicon.ico` if `https://subdomain.site.com/favicon.ico` fails)
- [x] Detect and parse the HTML at the URL for the declaration of the favicon
- [x] Is able to read the favicon URL, even if it's a relative URL to the subdomain that you're querying
- [x] Resolve the favicon URL for you, even if it's a relative URL to the subdomain that you're querying
- [x] Allow you to prioritise which format of favicon you would like served
- [x] Detect and parse web application manifest JSON files for favicon locations

To do:
- [ ] Detect and parse web application manifest JSON files
- [ ] Detect and parse web application Microsoft browser configuration XML

## Usage
Expand All @@ -46,28 +47,55 @@ FaviconFinder(url: url).downloadFavicon { result in
}
```

However if you're the type to want to have some fine-tuned control over what sort of favicon's we're after, you can do so. Just insert this code into your project:
## Advanced Usage

However if you're the type to want to have some fine-tuned control over what sort of favicon's we're after, you can do so.

FaviconFinder allows you to specify which download type you'd prefer (HTML, actual file, or web application manifest file), and then allows you to specify which favicon type you'd prefer for each download type.

For example, you can specify that you'd prefer a HTML tag favicon, with the type of `appleTouchIcon`. FaviconFinder will then search through the HTML favicon tags for the `appleTouchIcon` type. If it cannot find the `appleTouchIcon` type, it will search for the other HTML favicon tag types.

If the URL does not have a HTML tag that specifies the favicon, FaviconFinder will default to other download types, and will search the URL for each favicon download type until it finds one, or it'll return an error.

Just like how you can specify which HTML favicon tag you'd prefer, you can set which filename you'd prefer when search for actual files.

Similarly, you can specify which JSON key you'd prefer when iterating through the web application manifest file.


For the `.ico` download type, you can request FaviconFinder searchs for a filename of your choosing.


Here's how you'd make that request:

```swift
FaviconFinder(url: url, preferredType: .html, preferences: [
.html: FaviconType.appleTouchIcon.rawValue,
.ico: "favicon.ico"
]).downloadFavicon { result in
switch result {
case .success(let favicon):
print("URL of Favicon: \(favicon.url)")
DispatchQueue.main.async {
self.imageView.image = favicon.image
FaviconFinder(
url: url,
preferredType: .html,
preferences: [
.html: FaviconType.appleTouchIcon.rawValue,
.ico: "favicon.ico",
.webApplicationManifestFile: FaviconType.launcherIcon4x.rawValue
],
logEnabled: true
).downloadFavicon { result in
switch result {
case .success(let favicon):
print("URL of Favicon: \(favicon.url)")
DispatchQueue.main.async {
self.imageView.image = favicon.image
}

case .failure(let error):
print("Error: \(error)")
}

case .failure(let error):
print("Error: \(error)")
}
}
```

This allows you to control:
- What type of download type FaviconFinder will use first
- When iterating through each download type, what sub-type to look for. For the HTML download type, this allows you to prioritise different "rel" types. For the file .ico type, this allows you to choose the filename.
- When iterating through each download type, what sub-type to look for. For the HTML download type, this allows you to prioritise different "rel" types. For the file.ico type, this allows you to choose the filename.

If your desired download type doesn't exist for your URL (ie. you requested the favicon that exists as a file but there's no file), FaviconFinder will automatically try all other methods of favicon storage for you.

## Example Project

Expand All @@ -84,15 +112,15 @@ FaviconFinder is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:

```ruby
pod 'FaviconFinder', '3.1.0'
pod 'FaviconFinder', '3.2.0'
```

### Carthage
FaviconFinder is also available through [Carthage](https://github.com/Carthage/Carthage). To install
it, simply add the following line to your Cartfile:

```ruby
github "will-lumley/FaviconFinder" == 3.1.0
github "will-lumley/FaviconFinder" == 3.2.0
```

### Swift Package Manager
Expand All @@ -102,7 +130,7 @@ To install it, simply add the dependency to your Package.Swift file:
```swift
...
dependencies: [
.package(url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.1.0"),
.package(url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.2.0"),
],
targets: [
.target( name: "YourTarget", dependencies: ["FaviconFinder"]),
Expand Down
4 changes: 3 additions & 1 deletion Sources/FaviconFinder/Classes/FaviconFinder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ private extension FaviconFinder {
print("Successfully extracted favicon from url: \(self.url)")
}

let favicon = Favicon(image: image, url: url, type: type)
let downloadType = FaviconDownloadType(type: type)

let favicon = Favicon(image: image, url: url, type: type, downloadType: downloadType)
onDownload(.success(favicon))

}).resume()
Expand Down
48 changes: 33 additions & 15 deletions Sources/FaviconFinder/Classes/Finders/HTMLFaviconFinder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,29 +157,34 @@ private extension HTMLFaviconFinder {

let href = mostPreferrableIcon.icon.href

//If we don't have a http or https prepended to our href, prepend our base domain
if !Regex.testForHttpsOrHttp(input: href) {
let baseRef = { () -> URL in
if let baseRef = try? html.head()?.getElementsByTag("base").attr("href"),
let baseRefUrl = URL(string: baseRef, relativeTo: self.url) {
var hrefUrl: URL?

// If we don't have a http or https prepended to our href, prepend our base domain
if Regex.testForHttpsOrHttp(input: href) == false {
let baseRef = {() -> URL in
// Try and get the base URL from a HTML tag if we can
if let baseRef = try? html.head()?.getElementsByTag("base").attr("href"), let baseRefUrl = URL(string: baseRef, relativeTo: self.url) {
return baseRefUrl
} else {
}

// We couldn't get the base URL from a HTML tag, so we'll use the base URL that we have on hand
else {
return self.url
}
}

guard let url = URL(string: href, relativeTo: baseRef()) else {
return nil
}

let faviconURL = FaviconURL(url: url, type: mostPreferrableIcon.type)
return faviconURL
hrefUrl = URL(string: href, relativeTo: baseRef())
}

guard let url = URL(string: href) else {
return nil
// Our href is a proper URL, nevermind
else {
hrefUrl = URL(string: href)
}

guard let url = hrefUrl else {
return nil
}

let faviconURL = FaviconURL(url: url, type: mostPreferrableIcon.type)
return faviconURL
}
Expand All @@ -190,15 +195,28 @@ private extension HTMLFaviconFinder {
- returns: The most preferred image link from our aray of icons
*/
func mostPreferrableIcon(icons: [HTMLFaviconReference]) -> (icon: HTMLFaviconReference, type: FaviconType)? {
if let icon = icons.first(where: { FaviconType(rawValue: $0.rel) == .appleTouchIcon }) {

// Check for the users preferred type
if let icon = icons.first(where: { FaviconType(rawValue: $0.rel)?.rawValue == preferredType }) {
return (icon: icon, type: FaviconType(rawValue: icon.rel)!)
}

// Check for appleTouchIcon type
else if let icon = icons.first(where: { FaviconType(rawValue: $0.rel) == .appleTouchIcon }) {
return (icon: icon, type: FaviconType(rawValue: icon.rel)!)
}

// Check for appleTouchIconPrecomposed type
else if let icon = icons.first(where: { FaviconType(rawValue: $0.rel) == .appleTouchIconPrecomposed }) {
return (icon: icon, type: FaviconType(rawValue: icon.rel)!)
}

// Check for shortcutIcon type
else if let icon = icons.first(where: { FaviconType(rawValue: $0.rel) == .shortcutIcon }) {
return (icon: icon, type: FaviconType(rawValue: icon.rel)!)
}

// Check for icon type
else if let icon = icons.first(where: { FaviconType(rawValue: $0.rel) == .icon }) {
return (icon: icon, type: FaviconType(rawValue: icon.rel)!)
}
Expand Down
Loading

0 comments on commit f62aeb2

Please sign in to comment.