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 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).
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FaviconGem
4
+ VERSION = "0.1.0"
5
+ end
@@ -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: []