favicon_party 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +74 -0
- data/Rakefile +10 -0
- data/bin/console +10 -0
- data/bin/fetch_favicon +47 -0
- data/favicon_party.gemspec +30 -0
- data/lib/favicon_party.rb +34 -0
- data/lib/favicon_party/errors.rb +32 -0
- data/lib/favicon_party/fetcher.rb +153 -0
- data/lib/favicon_party/http_client.rb +50 -0
- data/lib/favicon_party/image.rb +178 -0
- data/lib/favicon_party/loader.rb +14 -0
- data/lib/favicon_party/utils.rb +47 -0
- data/lib/favicon_party/version.rb +3 -0
- data/test/fetcher_test.rb +31 -0
- data/test/fixtures/favicons/transparent-16x16.png +0 -0
- data/test/fixtures/favicons/transparent-1x1.gif +0 -0
- data/test/fixtures/favicons/transparent-1x1.png +0 -0
- data/test/fixtures/favicons/white-16x16.ico +0 -0
- data/test/fixtures/favicons/white-16x16.svg +9 -0
- data/test/fixtures/html/page1.html +7 -0
- data/test/image_test.rb +59 -0
- data/test/loader_test.rb +16 -0
- data/test/test_helper.rb +4 -0
- data/test/utils_test.rb +43 -0
- metadata +128 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 04c20f8f3afd2ba553493eaccc622a53e8475d52
|
|
4
|
+
data.tar.gz: 9236896799c20478321d54209c26c6bdd88bf9b7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 675b832b0921b0570a077627428da752f3272d35b500798d72e7539727da2c4153b6e474bdd2307776bcd1842b20252e0cfc3a4062574988dfe883b9fe6192e2
|
|
7
|
+
data.tar.gz: 764055135af45b2d82ae4cc1f30150942e2ec89c46ea6a65051bf51921370a3dd28b2806a53d6c0bab3716ff492fd1838ba54fe07023dc8861ed317ffa6b31c9
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015 Linmiao Xu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Favicon Party
|
|
2
|
+
|
|
3
|
+
Fetch favicons from websites and convert them into other image formats!
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Fetching favicon images
|
|
7
|
+
|
|
8
|
+
Favicon Party parses the html response of the query url to find favicon
|
|
9
|
+
links, and falls back to checking /favicon.ico for valid favicon images.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
image = FaviconParty.fetch! "https://github.com"
|
|
13
|
+
|
|
14
|
+
# => #<FaviconParty::Image mime_type: "image/x-icon", size: 6518>
|
|
15
|
+
|
|
16
|
+
image.valid? # true
|
|
17
|
+
image.source_data # binary favicon image data
|
|
18
|
+
image.to_png # binary 16x16 png data
|
|
19
|
+
image.base64_png # base64-encoded 16x16 png data
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
FaviconParty.fetch! "http://example.com" # raises FaviconNotFound
|
|
23
|
+
FaviconParty.fetch "http://example.com" # nil
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Loading favicon images
|
|
28
|
+
|
|
29
|
+
You can load favicon files directly as well.
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
image = FaviconParty.load "/path/to/favicon.ico"
|
|
33
|
+
|
|
34
|
+
# => #<FaviconParty::Image mime_type: "image/x-icon", size: 6518>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Same for loading favicon URLs.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
image = FaviconParty.load "https://github.com/favicon.ico"
|
|
41
|
+
|
|
42
|
+
# => #<FaviconParty::Image mime_type: "image/x-icon", size: 6518>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Command-line client
|
|
47
|
+
|
|
48
|
+
Favicon Party also provides a command-line script for fetching favicons. By
|
|
49
|
+
default, it attempts to fetch a favicon from the given URL and prints it out.
|
|
50
|
+
Run `fetch_favicon --help` to see the list of options.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
fetch_favicon github.com # prints favicon data to STDOUT
|
|
54
|
+
fetch_favicon --format png github.com # prints 16x16 favicon png to STDOUT
|
|
55
|
+
fetch_favicon example.com # prints an error to STDERR
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
The best way to install Favicon Party is through [RubyGems](https://rubygems.org/)
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
$ gem install favicon_party
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
* Ruby 2.0.0+
|
|
71
|
+
* [Nokogiri](https://github.com/sparklemotion/nokogiri) - parsing html
|
|
72
|
+
* [Imagemagick](http://www.imagemagick.org/) - validating and converting favicons
|
|
73
|
+
* [Curl](http://curl.haxx.se/) - http client
|
|
74
|
+
* [File](http://darwinsys.com/file/) - mime-type detection
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/fetch_favicon
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
$:.unshift "./lib"
|
|
4
|
+
|
|
5
|
+
require 'optparse'
|
|
6
|
+
require 'favicon_party'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
options = {}
|
|
10
|
+
OptionParser.new do |opts|
|
|
11
|
+
opts.banner = "Usage: ./fetch_favicon <url>"
|
|
12
|
+
|
|
13
|
+
opts.on("--png", "Outputs favicon as a 16x16 png") do
|
|
14
|
+
options[:format] = :png
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
opts.on("-o", "--output-file FILE", "Saves favicon to FILE") do |output|
|
|
18
|
+
options[:output] = output
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
opts.on_tail("-h", "--help", "Show this help text") do
|
|
22
|
+
puts opts
|
|
23
|
+
exit
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if ARGV.length == 0
|
|
27
|
+
puts opts
|
|
28
|
+
exit
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end.parse!
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
image = FaviconParty.fetch!(ARGV[0])
|
|
35
|
+
case options[:format]
|
|
36
|
+
when :png
|
|
37
|
+
favicon = image.to_png
|
|
38
|
+
else
|
|
39
|
+
favicon = image.source_data
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if options[:output]
|
|
44
|
+
open(options[:output], "wb").write {|f| f.write favicon }
|
|
45
|
+
else
|
|
46
|
+
puts favicon
|
|
47
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
4
|
+
$:.unshift(lib) unless $:.include?(lib)
|
|
5
|
+
|
|
6
|
+
require 'favicon_party/version'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Gem::Specification.new do |s|
|
|
10
|
+
s.name = "favicon_party"
|
|
11
|
+
s.version = FaviconParty::VERSION
|
|
12
|
+
s.authors = ["Linmiao Xu"]
|
|
13
|
+
s.email = ["linmiao.xu@gmail.com"]
|
|
14
|
+
|
|
15
|
+
s.summary = "Fetch favicons from websites and have a blast!"
|
|
16
|
+
s.description = "Fetch favicons from websites and have a blast!"
|
|
17
|
+
s.homepage = "https://github.com/linrock/favicon_party"
|
|
18
|
+
s.license = "MIT"
|
|
19
|
+
|
|
20
|
+
s.files = `git ls-files -z`.split("\0")
|
|
21
|
+
s.bindir = "bin"
|
|
22
|
+
s.executables = ["fetch_favicon"]
|
|
23
|
+
s.require_paths = ["lib"]
|
|
24
|
+
|
|
25
|
+
s.add_development_dependency "rake", "~> 10.0"
|
|
26
|
+
s.add_development_dependency "minitest", "~> 5.0"
|
|
27
|
+
s.add_development_dependency "pry", "~> 0.10"
|
|
28
|
+
|
|
29
|
+
s.add_dependency "nokogiri", "~> 1.6"
|
|
30
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'favicon_party/utils'
|
|
2
|
+
require 'favicon_party/errors'
|
|
3
|
+
require 'favicon_party/http_client'
|
|
4
|
+
require 'favicon_party/image'
|
|
5
|
+
require 'favicon_party/fetcher'
|
|
6
|
+
require 'favicon_party/loader'
|
|
7
|
+
require 'favicon_party/version'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
module FaviconParty
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
|
|
14
|
+
# @return [FaviconParty::Image]
|
|
15
|
+
#
|
|
16
|
+
def fetch!(url, options = {})
|
|
17
|
+
Fetcher.new(url, options).fetch
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [FaviconParty::Image]
|
|
21
|
+
#
|
|
22
|
+
def fetch(url)
|
|
23
|
+
fetch!(url) rescue nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [FaviconParty::Image]
|
|
27
|
+
#
|
|
28
|
+
def load(url_or_filename)
|
|
29
|
+
Loader.load url_or_filename
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module FaviconParty
|
|
2
|
+
|
|
3
|
+
class CurlError < StandardError; end
|
|
4
|
+
|
|
5
|
+
module Curl
|
|
6
|
+
class DNSError < CurlError; end
|
|
7
|
+
class SSLError < CurlError; end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class FaviconNotFound < StandardError; end
|
|
11
|
+
|
|
12
|
+
class Error < StandardError
|
|
13
|
+
attr_accessor :meta
|
|
14
|
+
|
|
15
|
+
def initialize(message)
|
|
16
|
+
@meta = {}
|
|
17
|
+
super(message)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_h
|
|
21
|
+
{
|
|
22
|
+
:error_class => self.class.to_s,
|
|
23
|
+
:error_message => self.message
|
|
24
|
+
}.merge(meta)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class InvalidData < StandardError; end
|
|
30
|
+
class ImageMagickError < Error; end
|
|
31
|
+
|
|
32
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'nokogiri'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
module FaviconParty
|
|
6
|
+
|
|
7
|
+
# Actually go and grab the favicon from the given url
|
|
8
|
+
#
|
|
9
|
+
class Fetcher
|
|
10
|
+
include FaviconParty::Utils
|
|
11
|
+
|
|
12
|
+
ICON_SELECTORS = [ 'link[rel="shortcut icon"]',
|
|
13
|
+
'link[rel="icon"]',
|
|
14
|
+
'link[type="image/x-icon"]',
|
|
15
|
+
'link[rel="fluid-icon"]',
|
|
16
|
+
'link[rel="apple-touch-icon"]'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
attr_accessor :query_url, :final_url, :favicon_url, :candidate_urls, :html, :data
|
|
20
|
+
|
|
21
|
+
def initialize(url, options = {})
|
|
22
|
+
@query_url = prefix_url(url)
|
|
23
|
+
@final_url = nil
|
|
24
|
+
@favicon_url = nil
|
|
25
|
+
@html = nil
|
|
26
|
+
@data = nil
|
|
27
|
+
@candidate_urls = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Encodes output as utf8 - Not for binary http responses
|
|
31
|
+
#
|
|
32
|
+
def http_get(url)
|
|
33
|
+
FaviconParty::HTTPClient.get(url)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fetch
|
|
37
|
+
@html = http_get final_url
|
|
38
|
+
set_candidate_favicon_urls
|
|
39
|
+
get_favicon_data
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def get_favicon_data
|
|
43
|
+
return @data if !@data.nil?
|
|
44
|
+
@data = get_favicon_data_from_candidate_urls
|
|
45
|
+
raise FaviconParty::FaviconNotFound.new(@query_url) unless @data
|
|
46
|
+
@data
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_favicon_data_from_candidate_urls
|
|
50
|
+
@candidate_urls.each do |url|
|
|
51
|
+
data = FaviconParty::Image.new(get_favicon_data_from_url(url))
|
|
52
|
+
begin
|
|
53
|
+
if data.valid?
|
|
54
|
+
@favicon_url = url
|
|
55
|
+
return data
|
|
56
|
+
end
|
|
57
|
+
rescue FaviconParty::ImageMagickError => error
|
|
58
|
+
error.meta = get_urls
|
|
59
|
+
error.meta[:favicon_url] ||= url
|
|
60
|
+
error.meta[:base64_favicon_data] = data.base64_source_data
|
|
61
|
+
raise error
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Tries to find favicon urls from the html content of query_url
|
|
68
|
+
#
|
|
69
|
+
def find_favicon_urls_in_html(html)
|
|
70
|
+
doc = Nokogiri.parse html
|
|
71
|
+
candidate_urls = doc.css(ICON_SELECTORS.join(",")).map {|e| e.attr('href') }.compact
|
|
72
|
+
candidate_urls.sort_by! {|href|
|
|
73
|
+
if href =~ /\.ico/
|
|
74
|
+
0
|
|
75
|
+
elsif href =~ /\.png/
|
|
76
|
+
1
|
|
77
|
+
else
|
|
78
|
+
2
|
|
79
|
+
end
|
|
80
|
+
}
|
|
81
|
+
uri = URI final_url
|
|
82
|
+
candidate_urls.map! do |href|
|
|
83
|
+
href = URI.encode(href.strip)
|
|
84
|
+
if href =~ /\A\/\//
|
|
85
|
+
href = "#{uri.scheme}:#{href}"
|
|
86
|
+
elsif href !~ /\Ahttp/
|
|
87
|
+
# Ignore invalid URLS - ex. {http://i50.tinypic.com/wbuzcn.png}
|
|
88
|
+
href = URI.join(url_root, href).to_s rescue nil
|
|
89
|
+
end
|
|
90
|
+
href
|
|
91
|
+
end.compact
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def set_candidate_favicon_urls
|
|
95
|
+
@candidate_urls = find_favicon_urls_in_html(@html)
|
|
96
|
+
@candidate_urls << URI.join(url_root, "favicon.ico").to_s
|
|
97
|
+
@candidate_urls << URI.join(url_root, "favicon.png").to_s
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Follow redirects from the query url to get to the last url
|
|
101
|
+
#
|
|
102
|
+
def final_url
|
|
103
|
+
return @final_url if !@final_url.nil?
|
|
104
|
+
output = FaviconParty::HTTPClient.head(@query_url)
|
|
105
|
+
final_location = output.scan(/\ALocation: (.*)/)[-1]
|
|
106
|
+
final = final_location && final_location[0].strip
|
|
107
|
+
if !final.nil?
|
|
108
|
+
if final =~ /\Ahttp/
|
|
109
|
+
@final_url = URI.encode final
|
|
110
|
+
else
|
|
111
|
+
uri = URI @query_url
|
|
112
|
+
root = "#{uri.scheme}://#{uri.host}"
|
|
113
|
+
@final_url = URI.encode URI.join(root, final).to_s
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
if !@final_url.nil?
|
|
117
|
+
if %w( 127.0.0.1 localhost ).any? {|host| @final_url.include? host }
|
|
118
|
+
# TODO Exception for invalid final urls
|
|
119
|
+
@final_url = @query_url
|
|
120
|
+
end
|
|
121
|
+
return @final_url
|
|
122
|
+
end
|
|
123
|
+
@final_url = @query_url
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def url_root
|
|
127
|
+
uri = URI final_url
|
|
128
|
+
"#{uri.scheme}://#{uri.host}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def get_favicon_data_from_url(url)
|
|
132
|
+
if url =~ /^data:/
|
|
133
|
+
data = url.split(',')[1]
|
|
134
|
+
return data && Base64.decode64(data)
|
|
135
|
+
end
|
|
136
|
+
FaviconParty::HTTPClient.bin_get url
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def get_urls
|
|
140
|
+
{
|
|
141
|
+
:query_url => @query_url,
|
|
142
|
+
:final_url => final_url,
|
|
143
|
+
:favicon_url => @favicon_url
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def has_data?
|
|
148
|
+
!@data.nil? && !@data.empty?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
module FaviconParty
|
|
5
|
+
|
|
6
|
+
module HTTPClient
|
|
7
|
+
include FaviconParty::Utils
|
|
8
|
+
|
|
9
|
+
TIMEOUT = 5
|
|
10
|
+
|
|
11
|
+
# For now, wrap command-line curl rather than using net/http or open-uri
|
|
12
|
+
# because of easier/more reliable SSL handling
|
|
13
|
+
#
|
|
14
|
+
def curl_cmd(url)
|
|
15
|
+
"curl -sL -k --compressed -m #{TIMEOUT} --ciphers 'RC4,3DES,ALL' --fail --show-error '#{url}'"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Encodes output as utf8 - Not for binary http responses
|
|
19
|
+
#
|
|
20
|
+
def get(url)
|
|
21
|
+
stdin, stdout, stderr, t = Open3.popen3(curl_cmd(url))
|
|
22
|
+
output = encode_utf8(stdout.read).strip
|
|
23
|
+
error = encode_utf8(stderr.read).strip
|
|
24
|
+
if !error.nil? && !error.empty?
|
|
25
|
+
if error.include? "SSL"
|
|
26
|
+
raise FaviconParty::Curl::SSLError.new(error)
|
|
27
|
+
elsif error.include? "Couldn't resolve host"
|
|
28
|
+
raise FaviconParty::Curl::DNSError.new(error)
|
|
29
|
+
else
|
|
30
|
+
raise FaviconParty::CurlError.new(error)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
output
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get binary data from url and ignore errors
|
|
37
|
+
#
|
|
38
|
+
def bin_get(url)
|
|
39
|
+
`#{curl_cmd(url)} 2>/dev/null`
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def head(url)
|
|
43
|
+
headers = `curl -sIL -1 --ciphers 'RC4,3DES,ALL' -m #{TIMEOUT} "#{url}"`
|
|
44
|
+
encode_utf8 headers
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
extend self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'open3'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
module FaviconParty
|
|
6
|
+
|
|
7
|
+
# For handling anything related to the image data of the favicon itself
|
|
8
|
+
#
|
|
9
|
+
class Image
|
|
10
|
+
include FaviconParty::Utils
|
|
11
|
+
|
|
12
|
+
# Threshold for stdev of color values under which the image data
|
|
13
|
+
# is considered one color
|
|
14
|
+
#
|
|
15
|
+
STDEV_THRESHOLD = 0.005
|
|
16
|
+
|
|
17
|
+
MAX_FILE_SIZE = 1024 * 1024
|
|
18
|
+
|
|
19
|
+
VALID_MIME_TYPES = %w(
|
|
20
|
+
image/x-ico
|
|
21
|
+
image/x-icon
|
|
22
|
+
image/png
|
|
23
|
+
image/gif
|
|
24
|
+
image/svg+xml
|
|
25
|
+
image/jpeg
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
attr_accessor :source_data, :png_data, :error
|
|
29
|
+
|
|
30
|
+
def initialize(source_data)
|
|
31
|
+
@source_data = source_data
|
|
32
|
+
@png_data = nil
|
|
33
|
+
@error = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def imagemagick_run(cmd, binmode = false)
|
|
37
|
+
stdin, stdout, stderr, t = Open3.popen3(cmd)
|
|
38
|
+
stdout.binmode if binmode
|
|
39
|
+
output = stdout.read.strip
|
|
40
|
+
error = stderr.read.strip
|
|
41
|
+
if output.empty? && !error.nil? && !error.empty?
|
|
42
|
+
raise FaviconParty::ImageMagickError.new(error)
|
|
43
|
+
end
|
|
44
|
+
output
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def mime_type
|
|
48
|
+
return @mime_type if defined? @mime_type
|
|
49
|
+
@mime_type = get_mime_type(@source_data)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def identify(verbose = false)
|
|
53
|
+
with_temp_data_file(@source_data) do |t|
|
|
54
|
+
imagemagick_run("identify #{"-verbose" if verbose} #{t.path.to_s}")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Does the data look like a valid favicon?
|
|
59
|
+
#
|
|
60
|
+
def valid?
|
|
61
|
+
@error =
|
|
62
|
+
if blank?
|
|
63
|
+
"source_data is blank"
|
|
64
|
+
elsif size_too_big?
|
|
65
|
+
"source_data file size too big"
|
|
66
|
+
elsif !valid_mime_type?
|
|
67
|
+
"source_data mime-type is invalid - #{mime_type}"
|
|
68
|
+
elsif transparent?
|
|
69
|
+
"source_data is a transparent image"
|
|
70
|
+
elsif one_pixel?
|
|
71
|
+
"source_data is a 1x1 image"
|
|
72
|
+
elsif one_color?
|
|
73
|
+
"png_data is one color (or close to it)"
|
|
74
|
+
end
|
|
75
|
+
@error.nil?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def blank?
|
|
79
|
+
@source_data.nil? || @source_data.length <= 1
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def size_too_big?
|
|
83
|
+
size >= MAX_FILE_SIZE
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# TODO set an option to decide how mime-type validity is handled
|
|
87
|
+
#
|
|
88
|
+
def invalid_mime_type?
|
|
89
|
+
mime_type =~ /(text|html|x-empty|octet-stream|ERROR|zip|jar)/
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def valid_mime_type?
|
|
93
|
+
VALID_MIME_TYPES.include? mime_type
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def transparent?
|
|
97
|
+
with_temp_data_file(@source_data) do |t|
|
|
98
|
+
cmd = "convert #{t.path.to_s} -channel a -negate -format '%[mean]' info:"
|
|
99
|
+
imagemagick_run(cmd).to_i == 0
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def one_pixel?
|
|
104
|
+
files = identify.split(/\n/)
|
|
105
|
+
files.length == 1 && files[0].include?(" 1x1 ")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def one_color?
|
|
109
|
+
colors_stdev < STDEV_THRESHOLD
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def colors_stdev
|
|
113
|
+
with_temp_data_file(to_png) do |t|
|
|
114
|
+
cmd = "identify -format '%[fx:image.standard_deviation]' #{t.path.to_s}"
|
|
115
|
+
imagemagick_run(cmd).to_f
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def n_colors
|
|
120
|
+
with_temp_data_file(@source_data) do |t|
|
|
121
|
+
cmd = "identify -format '%k' #{t.path.to_s}"
|
|
122
|
+
imagemagick_run(cmd).to_i
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def dimensions
|
|
127
|
+
with_temp_data_file(@source_data) do |t|
|
|
128
|
+
cmd = "convert #{t.path.to_s}[0] -format '%wx%h' info:"
|
|
129
|
+
imagemagick_run(cmd)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# number of bytes in the raw data
|
|
134
|
+
#
|
|
135
|
+
def size
|
|
136
|
+
@source_data && @source_data.size || 0
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def info_str
|
|
140
|
+
"#{mime_type}, #{dimensions}, #{size} bytes"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Export source_data as a 16x16 png
|
|
144
|
+
#
|
|
145
|
+
def to_png
|
|
146
|
+
return @png_data if !@png_data.nil?
|
|
147
|
+
with_temp_data_file(@source_data) do |t|
|
|
148
|
+
sizes = imagemagick_run("identify #{t.path.to_s}").split(/\n/)
|
|
149
|
+
images = []
|
|
150
|
+
%w(16x16 32x32 64x64).each do |dims|
|
|
151
|
+
%w(32-bit 24-bit 16-bit 8-bit).each do |bd|
|
|
152
|
+
images += sizes.select {|x| x.include?(dims) and x.include?(bd) }.
|
|
153
|
+
map {|x| x.split(' ')[0] }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
image_to_convert = images.uniq[0] || "#{t.path.to_s}[0]"
|
|
157
|
+
cmd = "convert -strip -resize 16x16! #{image_to_convert} png:fd:1"
|
|
158
|
+
@png_data = imagemagick_run(cmd, true)
|
|
159
|
+
raise FaviconParty::InvalidData.new("Empty png") if @png_data.empty?
|
|
160
|
+
@png_data
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def base64_source_data
|
|
165
|
+
Base64.encode64(@source_data).split(/\s+/).join
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def base64_png
|
|
169
|
+
Base64.encode64(to_png).split(/\s+/).join
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def inspect
|
|
173
|
+
%(#<FaviconParty::Image mime_type: "#{mime_type}", size: #{size}>)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'tempfile'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
module FaviconParty
|
|
6
|
+
|
|
7
|
+
module Utils
|
|
8
|
+
|
|
9
|
+
def prefix_url(url)
|
|
10
|
+
url = URI.encode url.strip.downcase
|
|
11
|
+
if url =~ /https?:\/\//
|
|
12
|
+
url
|
|
13
|
+
else
|
|
14
|
+
"http://#{url}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def encode_utf8(text)
|
|
19
|
+
return text if text.valid_encoding?
|
|
20
|
+
text.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => '')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def get_mime_type(data, use_file_cmd = true)
|
|
24
|
+
if use_file_cmd
|
|
25
|
+
with_temp_data_file(data) {|t| `file -b --mime-type #{t.path.to_s}`.strip }
|
|
26
|
+
else
|
|
27
|
+
FileMagic.new(:mime_type).buffer(data)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def with_temp_data_file(data, &block)
|
|
32
|
+
begin
|
|
33
|
+
t = Tempfile.new(["favicon", ".ico"])
|
|
34
|
+
t.binmode
|
|
35
|
+
t.write data
|
|
36
|
+
t.close
|
|
37
|
+
result = block.call(t)
|
|
38
|
+
ensure
|
|
39
|
+
t.unlink
|
|
40
|
+
end
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
extend self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FetcherTest < Minitest::Test
|
|
5
|
+
|
|
6
|
+
def setup
|
|
7
|
+
test_url = "http://localhost"
|
|
8
|
+
@fetcher = FaviconParty::Fetcher.new(test_url)
|
|
9
|
+
@fetcher.final_url = test_url
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_fetcher_raises_error_if_no_favicon_found
|
|
13
|
+
@fetcher.candidate_urls = []
|
|
14
|
+
assert_raises(FaviconParty::FaviconNotFound) do
|
|
15
|
+
@fetcher.get_favicon_data
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_fetcher_finds_favicon_urls_in_html
|
|
20
|
+
html = open("#{File.dirname(__FILE__)}/fixtures/html/page1.html").read
|
|
21
|
+
assert @fetcher.find_favicon_urls_in_html(html).length == 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_fetcher_decodes_base64_data_uri_links
|
|
25
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/transparent-16x16.png"
|
|
26
|
+
image = FaviconParty::Image.new(open(filename, "rb").read)
|
|
27
|
+
data_uri = "data:image/png;base64,#{image.base64_png}"
|
|
28
|
+
assert @fetcher.get_favicon_data_from_url(data_uri) == image.source_data
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
3
|
+
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve"> <image id="image0" width="16" height="16" x="0" y="0"
|
|
4
|
+
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQAAAAA3iMLMAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
|
|
5
|
+
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAd2KE6QAAAAHdElN
|
|
6
|
+
RQffCxwWHC8XrmKqAAAADklEQVQI12P4/5+BFAQA/U4f4XOLpqwAAAAldEVYdGRhdGU6Y3JlYXRl
|
|
7
|
+
ADIwMTUtMTEtMjhUMjI6Mjg6NDctMDg6MDC802/MAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTEx
|
|
8
|
+
LTI4VDIyOjI4OjQ3LTA4OjAwzY7XcAAAAABJRU5ErkJggg==" />
|
|
9
|
+
</svg>
|
data/test/image_test.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ImageTest < Minitest::Test
|
|
5
|
+
|
|
6
|
+
def setup
|
|
7
|
+
@klass = FaviconParty::Image
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_nil_data_is_invalid
|
|
11
|
+
@image = @klass.new nil
|
|
12
|
+
assert @image.valid? == false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_transparent_png_is_invalid
|
|
16
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/transparent-16x16.png"
|
|
17
|
+
@image = @klass.new open(filename, "rb").read
|
|
18
|
+
assert @image.mime_type == "image/png"
|
|
19
|
+
assert @image.transparent? == true
|
|
20
|
+
assert @image.one_color? == true
|
|
21
|
+
assert @image.valid? == false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_1x1_gif_is_invalid
|
|
25
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/transparent-1x1.gif"
|
|
26
|
+
@image = @klass.new open(filename, "rb").read
|
|
27
|
+
assert @image.mime_type == "image/gif"
|
|
28
|
+
assert @image.one_pixel? == true
|
|
29
|
+
assert @image.valid? == false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_all_white_ico_is_invalid
|
|
33
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/white-16x16.ico"
|
|
34
|
+
@image = @klass.new open(filename, "rb").read
|
|
35
|
+
assert %w( image/x-ico image/x-icon ).include? @image.mime_type
|
|
36
|
+
assert @image.one_color? == true
|
|
37
|
+
assert @image.valid? == false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_svg_mime_type_is_valid
|
|
41
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/white-16x16.svg"
|
|
42
|
+
@image = @klass.new open(filename, "rb").read
|
|
43
|
+
assert @image.one_color?
|
|
44
|
+
assert @image.valid_mime_type?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_base64_png_works
|
|
48
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/white-16x16.ico"
|
|
49
|
+
@image = @klass.new open(filename, "rb").read
|
|
50
|
+
assert !@image.base64_png.nil?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_dimensions_returns_correct_dimensions
|
|
54
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/white-16x16.ico"
|
|
55
|
+
@image = @klass.new open(filename, "rb").read
|
|
56
|
+
assert @image.dimensions == "16x16"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
data/test/loader_test.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LoaderTest < Minitest::Test
|
|
5
|
+
|
|
6
|
+
def setup
|
|
7
|
+
@klass = FaviconParty::Loader
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_loader_loads_favicon_images
|
|
11
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/transparent-16x16.png"
|
|
12
|
+
image = @klass.load filename
|
|
13
|
+
assert image.class == FaviconParty::Image
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
end
|
data/test/test_helper.rb
ADDED
data/test/utils_test.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UtilsTest < Minitest::Test
|
|
5
|
+
include FaviconParty::Utils
|
|
6
|
+
|
|
7
|
+
def test_get_mime_type_detects_png_correctly
|
|
8
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/transparent-16x16.png"
|
|
9
|
+
assert get_mime_type(open(filename, "rb").read) == "image/png"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_get_mime_type_detects_gif_correctly
|
|
13
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/transparent-1x1.gif"
|
|
14
|
+
assert get_mime_type(open(filename, "rb").read) == "image/gif"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_get_mime_type_detects_ico_correctly
|
|
18
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/white-16x16.ico"
|
|
19
|
+
assert %w( image/x-ico image/x-icon ).include? get_mime_type(open(filename, "rb").read)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_get_mime_type_detects_svg_correctly
|
|
23
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/favicons/white-16x16.svg"
|
|
24
|
+
assert get_mime_type(open(filename, "rb").read) == "image/svg+xml"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_get_mime_type_detects_html_correctly
|
|
28
|
+
filename = "#{File.dirname(__FILE__)}/fixtures/html/page1.html"
|
|
29
|
+
assert get_mime_type(open(filename, "rb").read) == "text/html"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_prefix_url_always_returns_prefixed_urls
|
|
33
|
+
assert prefix_url("localhost") =~ /\Ahttp:\/\//
|
|
34
|
+
assert prefix_url("http://localhost") =~ /\Ahttp:\/\//
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_encode_utf8_returns_valid_utf8_string
|
|
38
|
+
string = "\x12\x61\xA3"
|
|
39
|
+
assert !string.valid_encoding?
|
|
40
|
+
assert encode_utf8(string).valid_encoding?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: favicon_party
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.7.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Linmiao Xu
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-11-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rake
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '10.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '10.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: minitest
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '5.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '5.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: pry
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.10'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.10'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: nokogiri
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.6'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.6'
|
|
69
|
+
description: Fetch favicons from websites and have a blast!
|
|
70
|
+
email:
|
|
71
|
+
- linmiao.xu@gmail.com
|
|
72
|
+
executables:
|
|
73
|
+
- fetch_favicon
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- ".gitignore"
|
|
78
|
+
- Gemfile
|
|
79
|
+
- LICENSE
|
|
80
|
+
- README.md
|
|
81
|
+
- Rakefile
|
|
82
|
+
- bin/console
|
|
83
|
+
- bin/fetch_favicon
|
|
84
|
+
- favicon_party.gemspec
|
|
85
|
+
- lib/favicon_party.rb
|
|
86
|
+
- lib/favicon_party/errors.rb
|
|
87
|
+
- lib/favicon_party/fetcher.rb
|
|
88
|
+
- lib/favicon_party/http_client.rb
|
|
89
|
+
- lib/favicon_party/image.rb
|
|
90
|
+
- lib/favicon_party/loader.rb
|
|
91
|
+
- lib/favicon_party/utils.rb
|
|
92
|
+
- lib/favicon_party/version.rb
|
|
93
|
+
- test/fetcher_test.rb
|
|
94
|
+
- test/fixtures/favicons/transparent-16x16.png
|
|
95
|
+
- test/fixtures/favicons/transparent-1x1.gif
|
|
96
|
+
- test/fixtures/favicons/transparent-1x1.png
|
|
97
|
+
- test/fixtures/favicons/white-16x16.ico
|
|
98
|
+
- test/fixtures/favicons/white-16x16.svg
|
|
99
|
+
- test/fixtures/html/page1.html
|
|
100
|
+
- test/image_test.rb
|
|
101
|
+
- test/loader_test.rb
|
|
102
|
+
- test/test_helper.rb
|
|
103
|
+
- test/utils_test.rb
|
|
104
|
+
homepage: https://github.com/linrock/favicon_party
|
|
105
|
+
licenses:
|
|
106
|
+
- MIT
|
|
107
|
+
metadata: {}
|
|
108
|
+
post_install_message:
|
|
109
|
+
rdoc_options: []
|
|
110
|
+
require_paths:
|
|
111
|
+
- lib
|
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - ">="
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '0'
|
|
122
|
+
requirements: []
|
|
123
|
+
rubyforge_project:
|
|
124
|
+
rubygems_version: 2.4.8
|
|
125
|
+
signing_key:
|
|
126
|
+
specification_version: 4
|
|
127
|
+
summary: Fetch favicons from websites and have a blast!
|
|
128
|
+
test_files: []
|