favicon_gem 0.1.0
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/README.md +83 -0
- data/lib/favicon_gem/version.rb +5 -0
- data/lib/favicon_gem.rb +207 -0
- metadata +148 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 968a02874daebda7085218d3f92dc00e4363e173c55f19548f4c92ce5d726264
|
4
|
+
data.tar.gz: 78f7ecfd4326b6104a1f69b8df06ba7faaf0e6901b06be0e0f455e4343080f29
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d9ed6ec4017bcf457324229777636410dfbc12ee8ece545e306c5f5eda61e770878e0b137ad6d2385a25a0a9e3587d3417e92ae2ab1e6fe513572509364a677c
|
7
|
+
data.tar.gz: de2d8bfe54132424873ff4718c1a283ac4f918ae545376c256ba6b82f520101095719e5cd14d05503d223741d576ff6f026ea6e0ff59395540fa3b4887290005
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# FaviconGem
|
2
|
+
|
3
|
+
FaviconGem is a Ruby gem for finding and retrieving website favicons (icons). It's a port of the popular [favicon](https://github.com/scottwernervt/favicon) Python library.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'favicon_gem'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it manually:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ gem install favicon_gem
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
### Get all icons
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'favicon_gem'
|
31
|
+
|
32
|
+
icons = FaviconGem.get('https://www.ruby-lang.org/')
|
33
|
+
# => [Icon, Icon, Icon, ...]
|
34
|
+
|
35
|
+
# The first icon is usually the largest
|
36
|
+
icon = icons.first
|
37
|
+
puts "URL: #{icon.url}"
|
38
|
+
puts "Size: #{icon.width}x#{icon.height}"
|
39
|
+
puts "Format: #{icon.format}"
|
40
|
+
```
|
41
|
+
|
42
|
+
### Download an icon
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require 'favicon_gem'
|
46
|
+
require 'open-uri'
|
47
|
+
|
48
|
+
icons = FaviconGem.get('https://www.ruby-lang.org/')
|
49
|
+
icon = icons.first
|
50
|
+
|
51
|
+
URI.open(icon.url) do |image|
|
52
|
+
File.open("/tmp/ruby-favicon.#{icon.format}", "wb") do |file|
|
53
|
+
file.write(image.read)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# => /tmp/ruby-favicon.png
|
58
|
+
```
|
59
|
+
|
60
|
+
### Additional parameters
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
require 'favicon_gem'
|
64
|
+
|
65
|
+
# Custom headers
|
66
|
+
headers = {
|
67
|
+
'User-Agent' => 'My custom User-Agent'
|
68
|
+
}
|
69
|
+
|
70
|
+
# Timeout and other parameters
|
71
|
+
FaviconGem.get('https://www.ruby-lang.org/',
|
72
|
+
headers: headers,
|
73
|
+
timeout: 5)
|
74
|
+
```
|
75
|
+
|
76
|
+
## Requirements
|
77
|
+
|
78
|
+
* [nokogiri](https://github.com/sparklemotion/nokogiri) - for HTML parsing
|
79
|
+
* [faraday](https://github.com/lostisland/faraday) - for HTTP requests
|
80
|
+
|
81
|
+
## License
|
82
|
+
|
83
|
+
This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/favicon_gem.rb
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "favicon_gem/version"
|
4
|
+
require "faraday"
|
5
|
+
require "faraday/follow_redirects"
|
6
|
+
require "nokogiri"
|
7
|
+
require "uri"
|
8
|
+
require "set"
|
9
|
+
|
10
|
+
module FaviconGem
|
11
|
+
class Error < StandardError; end
|
12
|
+
|
13
|
+
# Website icon representation
|
14
|
+
Icon = Struct.new(:url, :width, :height, :format)
|
15
|
+
|
16
|
+
# Gem metadata
|
17
|
+
TITLE = "favicon_gem"
|
18
|
+
AUTHOR = "Ported from Python favicon by Scott Werner"
|
19
|
+
LICENSE = "MIT"
|
20
|
+
|
21
|
+
HEADERS = {
|
22
|
+
"User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) " \
|
23
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) " \
|
24
|
+
"Chrome/33.0.1750.152 Safari/537.36"
|
25
|
+
}
|
26
|
+
|
27
|
+
LINK_RELS = [
|
28
|
+
"icon",
|
29
|
+
"shortcut icon",
|
30
|
+
"apple-touch-icon",
|
31
|
+
"apple-touch-icon-precomposed"
|
32
|
+
]
|
33
|
+
|
34
|
+
META_NAMES = ["msapplication-TileImage"] # Removed og:image from metatags as it's usually not a favicon
|
35
|
+
|
36
|
+
# Format priorities (higher = better)
|
37
|
+
FORMAT_PRIORITY = {
|
38
|
+
"ico" => 10,
|
39
|
+
"png" => 9,
|
40
|
+
"jpg" => 8,
|
41
|
+
"jpeg" => 7,
|
42
|
+
"svg" => 6,
|
43
|
+
"gif" => 5,
|
44
|
+
"" => 0 # Unknown format has the lowest priority
|
45
|
+
}
|
46
|
+
|
47
|
+
SIZE_RE = /(?<width>\d{2,4})x(?<height>\d{2,4})/i
|
48
|
+
|
49
|
+
class << self
|
50
|
+
# Get all icons for a URL
|
51
|
+
#
|
52
|
+
# @param url [String] Page URL
|
53
|
+
# @param headers [Hash] Request headers
|
54
|
+
# @return [Array<Icon>] List of found icons, sorted by size
|
55
|
+
def get(url, headers: HEADERS, **request_options)
|
56
|
+
request_options[:headers] ||= headers
|
57
|
+
|
58
|
+
conn = Faraday.new(url: url) do |faraday|
|
59
|
+
faraday.request :url_encoded
|
60
|
+
faraday.headers = request_options[:headers]
|
61
|
+
faraday.options.timeout = request_options[:timeout] if request_options[:timeout]
|
62
|
+
faraday.use Faraday::FollowRedirects::Middleware
|
63
|
+
faraday.adapter Faraday.default_adapter
|
64
|
+
end
|
65
|
+
|
66
|
+
response = conn.get
|
67
|
+
raise Error, "Failed to fetch URL: #{response.status}" unless response.success?
|
68
|
+
|
69
|
+
icons = Set.new
|
70
|
+
|
71
|
+
default_icon = default(response.env.url.to_s, **request_options)
|
72
|
+
icons.add(default_icon) if default_icon
|
73
|
+
|
74
|
+
link_icons = tags(response.env.url.to_s, response.body)
|
75
|
+
icons.merge(link_icons) if link_icons.any?
|
76
|
+
|
77
|
+
# Improve sorting:
|
78
|
+
# 1. By size (larger first)
|
79
|
+
# 2. If sizes are equal, sort by format (ico/png have higher priority)
|
80
|
+
# 3. All icons with zero sizes go to the end
|
81
|
+
icons.to_a.sort_by do |icon|
|
82
|
+
format_priority = FORMAT_PRIORITY[icon.format] || 0
|
83
|
+
size = icon.width + icon.height
|
84
|
+
|
85
|
+
if size > 0
|
86
|
+
[1, size, format_priority] # Icons with non-zero size first
|
87
|
+
else
|
88
|
+
[0, format_priority] # Zero sizes - second priority
|
89
|
+
end
|
90
|
+
end.reverse
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Get icon using default filename favicon.ico
|
96
|
+
#
|
97
|
+
# @param url [String] Site URL
|
98
|
+
# @param request_options [Hash] Request parameters
|
99
|
+
# @return [Icon, nil] Icon or nil
|
100
|
+
def default(url, **request_options)
|
101
|
+
uri = URI.parse(url)
|
102
|
+
favicon_url = "#{uri.scheme}://#{uri.host}/favicon.ico"
|
103
|
+
|
104
|
+
conn = Faraday.new(url: favicon_url) do |faraday|
|
105
|
+
faraday.headers = request_options[:headers] if request_options[:headers]
|
106
|
+
faraday.options.timeout = request_options[:timeout] if request_options[:timeout]
|
107
|
+
faraday.use Faraday::FollowRedirects::Middleware
|
108
|
+
faraday.adapter Faraday.default_adapter
|
109
|
+
end
|
110
|
+
|
111
|
+
response = conn.head
|
112
|
+
return Icon.new(response.env.url.to_s, 0, 0, "ico") if response.success?
|
113
|
+
nil
|
114
|
+
rescue Faraday::Error
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get icons from link and meta tags
|
119
|
+
#
|
120
|
+
# @param url [String] Site URL
|
121
|
+
# @param html [String] Page HTML code
|
122
|
+
# @return [Set<Icon>] Found icons
|
123
|
+
def tags(url, html)
|
124
|
+
doc = Nokogiri::HTML(html)
|
125
|
+
icons = Set.new
|
126
|
+
|
127
|
+
# Search in <link> tags
|
128
|
+
link_tags = Set.new
|
129
|
+
LINK_RELS.each do |rel|
|
130
|
+
doc.css("link[rel='#{rel}'][href]").each do |link_tag|
|
131
|
+
link_tags.add(link_tag)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Search in <meta> tags
|
136
|
+
meta_tags = Set.new
|
137
|
+
META_NAMES.each do |name|
|
138
|
+
doc.css("meta[name='#{name}'][content], meta[property='#{name}'][content]").each do |meta_tag|
|
139
|
+
meta_tags.add(meta_tag)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
(link_tags | meta_tags).each do |tag|
|
144
|
+
href = tag["href"] || tag["content"] || ""
|
145
|
+
href = href.strip
|
146
|
+
|
147
|
+
next if href.empty? || href.start_with?("data:image/")
|
148
|
+
|
149
|
+
url_parsed = if is_absolute(href)
|
150
|
+
href
|
151
|
+
else
|
152
|
+
URI.join(url, href).to_s
|
153
|
+
end
|
154
|
+
|
155
|
+
# Fix URLs like '//cdn.network.com/favicon.png'
|
156
|
+
uri = URI.parse(url)
|
157
|
+
parsed_uri = URI.parse(url_parsed)
|
158
|
+
if parsed_uri.scheme.nil? && parsed_uri.host.nil? && parsed_uri.path.start_with?("//")
|
159
|
+
url_parsed = "#{uri.scheme}:#{parsed_uri.path}"
|
160
|
+
end
|
161
|
+
|
162
|
+
width, height = dimensions(tag)
|
163
|
+
ext = File.extname(URI.parse(url_parsed).path)[1..]&.downcase || ""
|
164
|
+
|
165
|
+
icons.add(Icon.new(url_parsed, width, height, ext))
|
166
|
+
end
|
167
|
+
|
168
|
+
icons
|
169
|
+
end
|
170
|
+
|
171
|
+
# Check if URL is absolute
|
172
|
+
#
|
173
|
+
# @param url [String] URL
|
174
|
+
# @return [Boolean] true if absolute
|
175
|
+
def is_absolute(url)
|
176
|
+
uri = URI.parse(url)
|
177
|
+
!uri.host.nil?
|
178
|
+
rescue URI::InvalidURIError
|
179
|
+
false
|
180
|
+
end
|
181
|
+
|
182
|
+
# Get icon dimensions from size attribute or filename
|
183
|
+
#
|
184
|
+
# @param tag [Nokogiri::XML::Element] Link or meta tag
|
185
|
+
# @return [Array<Integer>] Width and height, or [0,0]
|
186
|
+
def dimensions(tag)
|
187
|
+
sizes = tag["sizes"]
|
188
|
+
if sizes && sizes != "any"
|
189
|
+
size = sizes.split(" ").max_by { |s| s.scan(/\d+/).map(&:to_i).sum }
|
190
|
+
width, height = size.split(/[x×]/)
|
191
|
+
else
|
192
|
+
filename = tag["href"] || tag["content"] || ""
|
193
|
+
size = SIZE_RE.match(filename)
|
194
|
+
if size
|
195
|
+
width, height = size[:width], size[:height]
|
196
|
+
else
|
197
|
+
width, height = "0", "0"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Clean non-digit characters
|
202
|
+
width = width.to_s.scan(/\d+/).join
|
203
|
+
height = height.to_s.scan(/\d+/).join
|
204
|
+
[width.to_i, height.to_i]
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
metadata
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: favicon_gem
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ivan Nemytchenko
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-04-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.14'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.14'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.7'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: faraday-follow_redirects
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.3'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '13.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '13.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '5.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: webmock
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.18'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.18'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.50'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.50'
|
111
|
+
description: favicon_gem is a Ruby library to find a website's favicon, ported from
|
112
|
+
Python's favicon library
|
113
|
+
email:
|
114
|
+
- nemytchenko@gmail.com
|
115
|
+
executables: []
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- README.md
|
120
|
+
- lib/favicon_gem.rb
|
121
|
+
- lib/favicon_gem/version.rb
|
122
|
+
homepage: https://github.com/inem/favicon
|
123
|
+
licenses:
|
124
|
+
- MIT
|
125
|
+
metadata:
|
126
|
+
homepage_uri: https://github.com/inem/favicon
|
127
|
+
source_code_uri: https://github.com/inem/favicon
|
128
|
+
changelog_uri: https://github.com/inem/favicon/blob/main/CHANGELOG.md
|
129
|
+
post_install_message:
|
130
|
+
rdoc_options: []
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: 2.6.0
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
requirements: []
|
144
|
+
rubygems_version: 3.2.32
|
145
|
+
signing_key:
|
146
|
+
specification_version: 4
|
147
|
+
summary: Ruby gem to find a website's favicon
|
148
|
+
test_files: []
|