chico 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.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +36 -0
- data/LICENSE.txt +13 -0
- data/README.rdoc +13 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/chico.gemspec +94 -0
- data/lib/chico.rb +36 -0
- data/lib/chico/bitmapper.rb +96 -0
- data/lib/chico/extractor.rb +102 -0
- data/lib/chico/fetcher.rb +81 -0
- data/spec/chico/bitmapper_spec.rb +35 -0
- data/spec/chico/extractor_spec.rb +31 -0
- data/spec/chico/fetcher_spec.rb +26 -0
- data/spec/chico_spec.rb +29 -0
- data/spec/res/foxnews.ico +0 -0
- data/spec/res/google.ico +0 -0
- data/spec/res/microsoft.ico +0 -0
- data/spec/res/msn.ico +0 -0
- data/spec/res/nhl.ico +0 -0
- data/spec/res/sports.ico +0 -0
- data/spec/res/wikipedia.ico +0 -0
- data/spec/res/yahoo.ico +0 -0
- data/spec/spec_helper.rb +22 -0
- metadata +198 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem "gogetter"
|
4
|
+
gem "nokogiri", "~> 1.5.0"
|
5
|
+
gem "chunky_png"
|
6
|
+
|
7
|
+
group :development, :test do
|
8
|
+
gem "rspec", "~> 2.6.0"
|
9
|
+
gem "yard", "~> 0.7.2"
|
10
|
+
gem "bundler", "~> 1.0.0"
|
11
|
+
gem "jeweler", "~> 1.6.4"
|
12
|
+
gem "fakeweb", "~> 1.3.0"
|
13
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
chunky_png (1.2.1)
|
5
|
+
diff-lcs (1.1.2)
|
6
|
+
fakeweb (1.3.0)
|
7
|
+
git (1.2.5)
|
8
|
+
gogetter (0.1.0)
|
9
|
+
jeweler (1.6.4)
|
10
|
+
bundler (~> 1.0)
|
11
|
+
git (>= 1.2.5)
|
12
|
+
rake
|
13
|
+
nokogiri (1.5.0)
|
14
|
+
rake (0.9.2)
|
15
|
+
rspec (2.6.0)
|
16
|
+
rspec-core (~> 2.6.0)
|
17
|
+
rspec-expectations (~> 2.6.0)
|
18
|
+
rspec-mocks (~> 2.6.0)
|
19
|
+
rspec-core (2.6.4)
|
20
|
+
rspec-expectations (2.6.0)
|
21
|
+
diff-lcs (~> 1.1.2)
|
22
|
+
rspec-mocks (2.6.0)
|
23
|
+
yard (0.7.2)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
bundler (~> 1.0.0)
|
30
|
+
chunky_png
|
31
|
+
fakeweb (~> 1.3.0)
|
32
|
+
gogetter
|
33
|
+
jeweler (~> 1.6.4)
|
34
|
+
nokogiri (~> 1.5.0)
|
35
|
+
rspec (~> 2.6.0)
|
36
|
+
yard (~> 0.7.2)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright [2011] [Conduit Ltd.]
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.rdoc
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
= Chico
|
2
|
+
|
3
|
+
Chico fetches favicons from web pages and extracts PNG images from the ICO
|
4
|
+
format, all in native Ruby.
|
5
|
+
|
6
|
+
== Usage
|
7
|
+
icon_dir = Chico::Extractor.new(IO.read('google.ico'))
|
8
|
+
|
9
|
+
icons = image.entries
|
10
|
+
== Copyright
|
11
|
+
|
12
|
+
Copyright (c) 2011 Conduit Ltd.
|
13
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
gem.name = "chico"
|
17
|
+
gem.homepage = "http://github.com/ConduitTeam/chico"
|
18
|
+
gem.license = "Apache2"
|
19
|
+
gem.summary = %Q{Fetch favicons}
|
20
|
+
gem.email = "elad.kehat@conduit.com"
|
21
|
+
gem.authors = ["Elad Kehat", 'Ben Aviram']
|
22
|
+
["gogetter",["nokogiri", "~> 1.5.0"], "chunky_png"].each do |dep|
|
23
|
+
gem.add_dependency *dep
|
24
|
+
end
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
31
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
task :default => :spec
|
35
|
+
|
36
|
+
require 'yard'
|
37
|
+
YARD::Rake::YardocTask.new
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/chico.gemspec
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "chico"
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Elad Kehat", "Ben Aviram"]
|
12
|
+
s.date = "2011-08-31"
|
13
|
+
s.email = "elad.kehat@conduit.com"
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE.txt",
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".document",
|
20
|
+
".rspec",
|
21
|
+
"Gemfile",
|
22
|
+
"Gemfile.lock",
|
23
|
+
"LICENSE.txt",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"chico.gemspec",
|
28
|
+
"lib/chico.rb",
|
29
|
+
"lib/chico/bitmapper.rb",
|
30
|
+
"lib/chico/extractor.rb",
|
31
|
+
"lib/chico/fetcher.rb",
|
32
|
+
"spec/chico/bitmapper_spec.rb",
|
33
|
+
"spec/chico/extractor_spec.rb",
|
34
|
+
"spec/chico/fetcher_spec.rb",
|
35
|
+
"spec/chico_spec.rb",
|
36
|
+
"spec/res/foxnews.ico",
|
37
|
+
"spec/res/google.ico",
|
38
|
+
"spec/res/microsoft.ico",
|
39
|
+
"spec/res/msn.ico",
|
40
|
+
"spec/res/nhl.ico",
|
41
|
+
"spec/res/sports.ico",
|
42
|
+
"spec/res/wikipedia.ico",
|
43
|
+
"spec/res/yahoo.ico",
|
44
|
+
"spec/spec_helper.rb"
|
45
|
+
]
|
46
|
+
s.homepage = "http://github.com/ConduitTeam/chico"
|
47
|
+
s.licenses = ["Apache2"]
|
48
|
+
s.require_paths = ["lib"]
|
49
|
+
s.rubygems_version = "1.8.10"
|
50
|
+
s.summary = "Fetch favicons"
|
51
|
+
|
52
|
+
if s.respond_to? :specification_version then
|
53
|
+
s.specification_version = 3
|
54
|
+
|
55
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
56
|
+
s.add_runtime_dependency(%q<gogetter>, [">= 0"])
|
57
|
+
s.add_runtime_dependency(%q<nokogiri>, ["~> 1.5.0"])
|
58
|
+
s.add_runtime_dependency(%q<chunky_png>, [">= 0"])
|
59
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.6.0"])
|
60
|
+
s.add_development_dependency(%q<yard>, ["~> 0.7.2"])
|
61
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
62
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
|
63
|
+
s.add_development_dependency(%q<fakeweb>, ["~> 1.3.0"])
|
64
|
+
s.add_runtime_dependency(%q<gogetter>, [">= 0"])
|
65
|
+
s.add_runtime_dependency(%q<nokogiri>, ["~> 1.5.0"])
|
66
|
+
s.add_runtime_dependency(%q<chunky_png>, [">= 0"])
|
67
|
+
else
|
68
|
+
s.add_dependency(%q<gogetter>, [">= 0"])
|
69
|
+
s.add_dependency(%q<nokogiri>, ["~> 1.5.0"])
|
70
|
+
s.add_dependency(%q<chunky_png>, [">= 0"])
|
71
|
+
s.add_dependency(%q<rspec>, ["~> 2.6.0"])
|
72
|
+
s.add_dependency(%q<yard>, ["~> 0.7.2"])
|
73
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
74
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
75
|
+
s.add_dependency(%q<fakeweb>, ["~> 1.3.0"])
|
76
|
+
s.add_dependency(%q<gogetter>, [">= 0"])
|
77
|
+
s.add_dependency(%q<nokogiri>, ["~> 1.5.0"])
|
78
|
+
s.add_dependency(%q<chunky_png>, [">= 0"])
|
79
|
+
end
|
80
|
+
else
|
81
|
+
s.add_dependency(%q<gogetter>, [">= 0"])
|
82
|
+
s.add_dependency(%q<nokogiri>, ["~> 1.5.0"])
|
83
|
+
s.add_dependency(%q<chunky_png>, [">= 0"])
|
84
|
+
s.add_dependency(%q<rspec>, ["~> 2.6.0"])
|
85
|
+
s.add_dependency(%q<yard>, ["~> 0.7.2"])
|
86
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
87
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
88
|
+
s.add_dependency(%q<fakeweb>, ["~> 1.3.0"])
|
89
|
+
s.add_dependency(%q<gogetter>, [">= 0"])
|
90
|
+
s.add_dependency(%q<nokogiri>, ["~> 1.5.0"])
|
91
|
+
s.add_dependency(%q<chunky_png>, [">= 0"])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
data/lib/chico.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'chico/extractor'
|
2
|
+
require 'chico/bitmapper'
|
3
|
+
require 'chico/fetcher'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Chico
|
7
|
+
|
8
|
+
def self.extract_from_file(filepath, dest_dir, options={})
|
9
|
+
ex = Extractor.new(IO.read(filepath))
|
10
|
+
basename = File.basename(filepath, File.extname(filepath))
|
11
|
+
ex.entries.each do |entry|
|
12
|
+
ext = "#{entry[:width]}x#{entry[:height]}"
|
13
|
+
content = ex.image_for(entry)
|
14
|
+
File.open(File.join(dest_dir, "#{basename}.#{ext}.png"), 'wb') do |f|
|
15
|
+
f.write content
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.extract_from_url(url, dest_dir, options={})
|
21
|
+
fetcher = Fetecher.new(url)
|
22
|
+
ex = Extractor.new(fetcher.fetch)
|
23
|
+
write_to_files(ex, dest_dir, URI.parse(url).host)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def self.write_to_files(extractor, dest_dir, basename)
|
28
|
+
extractor.entries.each do |entry|
|
29
|
+
ext = "#{entry[:width]}x#{entry[:height]}"
|
30
|
+
content = extractor.image_for(entry)
|
31
|
+
File.open(File.join(dest_dir, "#{basename}.#{ext}.png"), 'wb') do |f|
|
32
|
+
f.write content
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# Unpack an ico bitmap
|
2
|
+
module Chico
|
3
|
+
class Bitmapper
|
4
|
+
attr_reader :pixels
|
5
|
+
|
6
|
+
def initialize(data, ico_direntry=nil)
|
7
|
+
@data = data
|
8
|
+
@ico_direntry = ico_direntry
|
9
|
+
end
|
10
|
+
|
11
|
+
def bitmap
|
12
|
+
parse_dib
|
13
|
+
@pixels = case @bits_per_pixel
|
14
|
+
when 0..8 then bitmap8
|
15
|
+
when 24 then bitmap24
|
16
|
+
when 32 then bitmap32
|
17
|
+
else raise "Unable to handle #{@bits_per_pixel} bits per pixel"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a String with the pixels,
|
22
|
+
# rgba, one byte per channel
|
23
|
+
def to_stream
|
24
|
+
sio = StringIO.new
|
25
|
+
@pixels.each {|p| sio.putc(p) }
|
26
|
+
sio.pos = 0
|
27
|
+
sio
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_dib
|
31
|
+
@offset = @data[0, 4].unpack('V').first
|
32
|
+
unless @offset == 40
|
33
|
+
raise "Unknown BMP format" # can't handle an ICO where the dib isn't 40 bytes
|
34
|
+
end
|
35
|
+
|
36
|
+
@width = @ico_direntry[:width] || @data[4, 4].unpack('V').first
|
37
|
+
@height = @ico_direntry[:height] || @data[8, 4].unpack('V').first
|
38
|
+
@bits_per_pixel = @data[14, 2].unpack('v').first
|
39
|
+
@image_size = @data[24, 4].unpack('V').first
|
40
|
+
if @image_size == 0
|
41
|
+
@image_size = @width * @height * @bits_per_pixel / 8
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def bitmap8
|
46
|
+
# assemble the color palette
|
47
|
+
color_count = 2 ** @bits_per_pixel
|
48
|
+
raw_colors = (0...color_count).map {|i| @data[@offset + 4 * i, 4].unpack('CCCC') }
|
49
|
+
#puts "raw colors: " + raw_colors.inspect
|
50
|
+
# the RGBQUAD for each palette color is (blue, green, red, reserved)
|
51
|
+
palette = raw_colors.map {|raw| raw.values_at(2, 1, 0) }
|
52
|
+
# get the XorMap bits
|
53
|
+
xor_data_bits = (0...@image_size).map {|i|
|
54
|
+
@data[@offset + 4 * color_count + i].unpack('B8').first.split('').map(&:to_i)
|
55
|
+
}.flatten
|
56
|
+
# get the AndMap bits
|
57
|
+
and_row_size = ((@width + 31) >> 5) << 2
|
58
|
+
and_data_bits = (0...(and_row_size * @height)).map {|i|
|
59
|
+
@data[@offset + 4 * color_count + @image_size].unpack('B8').first.split('').map(&:to_i)
|
60
|
+
}.flatten
|
61
|
+
get_pixel = ->(x, y) {
|
62
|
+
if and_data_bits[(@height - y - 1) * and_row_size * 8 + x] == '1'
|
63
|
+
[0, 0, 0, 0] # transparent
|
64
|
+
else
|
65
|
+
# use the xor value, made solid
|
66
|
+
bits = xor_data_bits[@bits_per_pixel * ((@height - y - 1) * @width + x), @bits_per_pixel]
|
67
|
+
palette[bits.reduce(0) {|acc, bit| 2 * acc + bit }] + [255]
|
68
|
+
end
|
69
|
+
}
|
70
|
+
(0...@height).map { |y|
|
71
|
+
(0...@width).map { |x|
|
72
|
+
get_pixel.call(x, y)
|
73
|
+
}
|
74
|
+
}.flatten
|
75
|
+
end
|
76
|
+
|
77
|
+
def bitmap32
|
78
|
+
(0...@height).to_a.reverse.map { |y|
|
79
|
+
(0...@width).map { |x|
|
80
|
+
pos = @offset + 4 * (y * @width + x)
|
81
|
+
#puts "data[#{pos}] = #{@data[pos]}"
|
82
|
+
@data[@offset + 4 * (y * @width + x), 4].unpack('CCCC').values_at(2, 1, 0, 3)
|
83
|
+
}
|
84
|
+
}.flatten
|
85
|
+
end
|
86
|
+
|
87
|
+
def bitmap24
|
88
|
+
(0...@height-1).to_a.reverse.map { |y|
|
89
|
+
(0...@width).map { |x|
|
90
|
+
@data[@offset + 3 * (y * @width + x), 3].unpack('CCC').values_at(2, 1, 0) + [255]
|
91
|
+
}
|
92
|
+
}.flatten
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'chunky_png'
|
2
|
+
module Chico
|
3
|
+
# Extracts icons images (bmp / png) from an .ico file
|
4
|
+
class Extractor
|
5
|
+
attr_reader :entries
|
6
|
+
|
7
|
+
def initialize(bin)
|
8
|
+
@sio = StringIO.new(bin, 'rb')
|
9
|
+
@entries = []
|
10
|
+
parse_icondir
|
11
|
+
end
|
12
|
+
|
13
|
+
def num_images
|
14
|
+
@entries.size
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the sizes of the images packed in this icon, as width, height pairs,
|
18
|
+
# sorted from smallest to largest width
|
19
|
+
def image_sizes
|
20
|
+
@entries.map {|e| [e[:width], e[:height]] }.sort
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add support for retrieving images by their size, e.g. 32x32, 16x16, etc.
|
24
|
+
def method_missing(sym, *args, &block)
|
25
|
+
if sym.to_s =~ /^(\d+)x(\d+)$/
|
26
|
+
image_by_size $1, $2
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def largest
|
33
|
+
w, h = image_sizes.last
|
34
|
+
self.send "#{w}x#{h}".to_sym
|
35
|
+
end
|
36
|
+
|
37
|
+
def smallest
|
38
|
+
w, h = image_sizes.first
|
39
|
+
self.send "#{w}x#{h}".to_sym
|
40
|
+
end
|
41
|
+
|
42
|
+
def image_for(entry)
|
43
|
+
extract_image(entry)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def parse_icondir
|
49
|
+
unless [read(2), read(2)] == [0, 1]
|
50
|
+
raise "Not a valid icon"
|
51
|
+
end
|
52
|
+
count = read 2 # number of icons in this file
|
53
|
+
count.times { parse_icondirentry }
|
54
|
+
end
|
55
|
+
|
56
|
+
ICONDIRENTRY_KEYS = [:width, 1, :height, 1, :colors, 1, :reserved, 1, :planes, 2, :bit_count, 2, :size, 4, :offset, 4].freeze
|
57
|
+
|
58
|
+
def parse_icondirentry
|
59
|
+
entry = {}
|
60
|
+
ICONDIRENTRY_KEYS.each_slice(2) {|pair| entry[pair[0]] = read(pair[1]) }
|
61
|
+
[:width, :height, :colors].each {|key| entry[key] = 256 if entry[key] == 0 }
|
62
|
+
@entries << entry
|
63
|
+
end
|
64
|
+
|
65
|
+
# Extract the first image of the given size, or returns nil if no such image is found
|
66
|
+
def image_by_size(width, height)
|
67
|
+
if entry = @entries.find {|e| e[:width] == width && e[:height] == height }
|
68
|
+
extract_image entry
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Extract the image file that corresponds to the given icondirentry
|
73
|
+
def extract_image(entry)
|
74
|
+
@sio.pos = entry[:offset]
|
75
|
+
raw = @sio.read(entry[:size])
|
76
|
+
if png?(raw)
|
77
|
+
raw
|
78
|
+
else
|
79
|
+
bmp = Bitmapper.new(raw, entry)
|
80
|
+
bmp.bitmap
|
81
|
+
ChunkyPNG::Image.from_rgba_stream(entry[:width], entry[:height], bmp.to_stream)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# detect a png header
|
86
|
+
def png?(raw)
|
87
|
+
raw.slice(0, 8).unpack('H*').first == "89504e470d0a1a0a"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Read len bytes from @sio and unpack them
|
91
|
+
def read(len)
|
92
|
+
format = case len
|
93
|
+
when 1 then 'C'
|
94
|
+
when 2 then 'S'
|
95
|
+
when 4 then 'L'
|
96
|
+
end
|
97
|
+
|
98
|
+
@sio.read(len).unpack(format).first
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'gogetter'
|
2
|
+
require 'nokogiri'
|
3
|
+
module Chico
|
4
|
+
|
5
|
+
# How it works:
|
6
|
+
# When given a domain w/o a path, try to find /favicon.ico
|
7
|
+
# If not found (or given a path), load the home page and search for one of:
|
8
|
+
# * <link rel="shortcut icon" href="http://example.com/myicon.ico" />
|
9
|
+
# * <link rel="icon" type="image/vnd.microsoft.icon" href="http://example.com/image.ico" />
|
10
|
+
# * <link rel="icon" type="image/png" href="http://example.com/image.png" />
|
11
|
+
# * <link rel="icon" type="image/gif" href="http://example.com/image.gif" />
|
12
|
+
# If given a path, and the above didn't work, retry on the domain's index page
|
13
|
+
class Fetcher
|
14
|
+
attr_reader :url, :searched_icon
|
15
|
+
|
16
|
+
def initialize(url)
|
17
|
+
@url = url
|
18
|
+
@uri = GoGetter.parse_url @url
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch(uri=@uri)
|
22
|
+
if @uri.path == '/'
|
23
|
+
fetch_default || fetch_link_on_page(@url)
|
24
|
+
else
|
25
|
+
# try to find a favicon for the specific path first
|
26
|
+
if path = fetch_link_on_page(@url)
|
27
|
+
fetch_icon path
|
28
|
+
else
|
29
|
+
# if failed, try again with the root path
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def fetch_default
|
38
|
+
fetch_icon '/favicon.ico'
|
39
|
+
end
|
40
|
+
|
41
|
+
def fetch_icon(path)
|
42
|
+
uri = @uri.clone
|
43
|
+
uri.path = path
|
44
|
+
res = GoGetter.get uri
|
45
|
+
if res.is_a? Net::HTTPOK
|
46
|
+
@searched_icon = uri.path
|
47
|
+
@raw = res.body
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
ICON_XPATHS = [
|
52
|
+
"link[@rel='shortcut icon']",
|
53
|
+
"link[@rel='icon']"
|
54
|
+
]
|
55
|
+
def fetch_link_on_page(url)
|
56
|
+
icon_url = find_link_on_page url
|
57
|
+
icon_url = Fetcher.make_relative(icon_url)
|
58
|
+
if !icon_url.to_s.empty?
|
59
|
+
fetch_icon icon_url
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def find_link_on_page(url)
|
64
|
+
res = GoGetter.get url
|
65
|
+
if res.is_a? Net::HTTPOK
|
66
|
+
doc = Nokogiri res.body
|
67
|
+
ICON_XPATHS.each do |xpath|
|
68
|
+
if link = doc.at(xpath)
|
69
|
+
return link[:href]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
return ""
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.make_relative(url)
|
77
|
+
URI.parse(url).path
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe "Chico" do
|
4
|
+
describe "Bitmapper" do
|
5
|
+
|
6
|
+
describe "#parse_dib" do
|
7
|
+
before(:all) do
|
8
|
+
class Chico::Bitmapper
|
9
|
+
attr_reader :offset, :width, :height, :bits_per_pixel, :image_size
|
10
|
+
end
|
11
|
+
raw16 = load_res_file('microsoft.16x16.raw')
|
12
|
+
@bitmapper16 = Chico::Bitmapper.new(raw16)
|
13
|
+
@bitmapper16.parse_dib
|
14
|
+
raw32 = load_res_file('microsoft.32x32.raw')
|
15
|
+
@bitmapper32 = Chico::Bitmapper.new(raw32)
|
16
|
+
@bitmapper32.parse_dib
|
17
|
+
end
|
18
|
+
|
19
|
+
it "reads the correct dib offset" do
|
20
|
+
@bitmapper16.offset.should == 40
|
21
|
+
@bitmapper32.offset.should == 40
|
22
|
+
end
|
23
|
+
|
24
|
+
it "extracts the correct width" do
|
25
|
+
@bitmapper16.width.should == 16
|
26
|
+
@bitmapper32.width.should == 32
|
27
|
+
end
|
28
|
+
|
29
|
+
it "extracts the correct height" do
|
30
|
+
@bitmapper16.height.should == 16
|
31
|
+
@bitmapper32.height.should == 32
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe "Chico" do
|
4
|
+
describe "Extractor" do
|
5
|
+
|
6
|
+
describe "#initialize" do
|
7
|
+
it "parses the icondir entries" do
|
8
|
+
ex = Chico::Extractor.new(load_icon_file('microsoft'))
|
9
|
+
ex.should have_exactly(2).entries
|
10
|
+
ex.entries[0][:width].should == 32
|
11
|
+
ex.entries[1][:width].should == 16
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#num_images" do
|
16
|
+
it "returns the number of images contained in the ico file" do
|
17
|
+
ex = Chico::Extractor.new(load_icon_file('microsoft'))
|
18
|
+
ex.num_images.should == 2
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#image_sizes" do
|
23
|
+
it "returns a sorted list of contained image width,height pairs" do
|
24
|
+
ex = Chico::Extractor.new(load_icon_file('microsoft'))
|
25
|
+
ex.image_sizes.should eql([[16,16],[32,32]])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe "Chico" do
|
4
|
+
describe "Fetcher" do
|
5
|
+
describe "#fetch" do
|
6
|
+
it "founds website with default favicon" do
|
7
|
+
fetcher = Chico::Fetcher.new('http://microsoft.com/')
|
8
|
+
fetcher.fetch().length.should > 0
|
9
|
+
fetcher.searched_icon.should == "/favicon.ico"
|
10
|
+
end
|
11
|
+
it "founds favicon for website without default favicon" do
|
12
|
+
fetcher = Chico::Fetcher.new('http://video.foxnews.com')
|
13
|
+
fetcher.fetch().length.should > 0
|
14
|
+
fetcher.searched_icon.should == '/images/foxnews/favicon.ico'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
describe "#make_relative" do
|
18
|
+
it "returns the relative part from full url" do
|
19
|
+
Chico::Fetcher.make_relative('http://video.foxnews.com/icon.ico').should == '/icon.ico'
|
20
|
+
end
|
21
|
+
it "returns the param from relative url" do
|
22
|
+
Chico::Fetcher.make_relative('/icon.ico').should == '/icon.ico'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/spec/chico_spec.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "Chico" do
|
4
|
+
path = File.expand_path("res/temp", File.dirname(__FILE__))
|
5
|
+
it "Extracts ico bmps as pngs" do
|
6
|
+
filepath = File.join(File.dirname(__FILE__), 'res', "msn.ico")
|
7
|
+
ex = Chico::Extractor.new(IO.read(filepath))
|
8
|
+
basename = File.basename(filepath, File.extname(filepath))
|
9
|
+
ex.entries.each do |entry|
|
10
|
+
ext = "#{entry[:width]}x#{entry[:height]}"
|
11
|
+
content = ex.image_for(entry)
|
12
|
+
if !File.directory? path
|
13
|
+
Dir.mkdir(path)
|
14
|
+
end
|
15
|
+
File.open(File.join(File.dirname(__FILE__), 'res/temp', "#{basename}.#{ext}.png"), 'wb') do |f|
|
16
|
+
f.write content
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it "Extracts icon from website markup" do
|
22
|
+
imageDir = Chico::extract_from_url('http://video.foxnews.com', path)
|
23
|
+
imageDir.length.should == 1
|
24
|
+
end
|
25
|
+
it "Extracts icon from website default favicon" do
|
26
|
+
imageDir = Chico::extract_from_url('http://microsoft.com', path)
|
27
|
+
imageDir.length.should == 2
|
28
|
+
end
|
29
|
+
end
|
Binary file
|
data/spec/res/google.ico
ADDED
Binary file
|
Binary file
|
data/spec/res/msn.ico
ADDED
Binary file
|
data/spec/res/nhl.ico
ADDED
Binary file
|
data/spec/res/sports.ico
ADDED
Binary file
|
Binary file
|
data/spec/res/yahoo.ico
ADDED
Binary file
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.require(:default, :test)
|
3
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
4
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
5
|
+
require 'rspec'
|
6
|
+
require 'chico'
|
7
|
+
|
8
|
+
# Requires supporting files with custom matchers and macros, etc,
|
9
|
+
# in ./support/ and its subdirectories.
|
10
|
+
#Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
11
|
+
|
12
|
+
def load_icon_file(name)
|
13
|
+
IO.read(File.join(File.dirname(__FILE__), 'res', "#{name}.ico"))
|
14
|
+
end
|
15
|
+
|
16
|
+
def load_res_file(name)
|
17
|
+
IO.read(File.join(File.dirname(__FILE__), 'res', name))
|
18
|
+
end
|
19
|
+
|
20
|
+
RSpec.configure do |config|
|
21
|
+
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: chico
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Elad Kehat
|
9
|
+
- Ben Aviram
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2011-08-31 00:00:00.000000000Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: gogetter
|
17
|
+
requirement: &75086960 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *75086960
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: nokogiri
|
28
|
+
requirement: &75086720 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.5.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *75086720
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: chunky_png
|
39
|
+
requirement: &75086480 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :runtime
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *75086480
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rspec
|
50
|
+
requirement: &75086240 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ~>
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 2.6.0
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *75086240
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: yard
|
61
|
+
requirement: &75086000 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ~>
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 0.7.2
|
67
|
+
type: :development
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: *75086000
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: bundler
|
72
|
+
requirement: &75085760 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.0.0
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: *75085760
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: jeweler
|
83
|
+
requirement: &75085520 !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ~>
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 1.6.4
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: *75085520
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: fakeweb
|
94
|
+
requirement: &75085280 !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ~>
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 1.3.0
|
100
|
+
type: :development
|
101
|
+
prerelease: false
|
102
|
+
version_requirements: *75085280
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: gogetter
|
105
|
+
requirement: &75105850 !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
type: :runtime
|
112
|
+
prerelease: false
|
113
|
+
version_requirements: *75105850
|
114
|
+
- !ruby/object:Gem::Dependency
|
115
|
+
name: nokogiri
|
116
|
+
requirement: &75105610 !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ~>
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: 1.5.0
|
122
|
+
type: :runtime
|
123
|
+
prerelease: false
|
124
|
+
version_requirements: *75105610
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: chunky_png
|
127
|
+
requirement: &75105370 !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ! '>='
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
type: :runtime
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: *75105370
|
136
|
+
description:
|
137
|
+
email: elad.kehat@conduit.com
|
138
|
+
executables: []
|
139
|
+
extensions: []
|
140
|
+
extra_rdoc_files:
|
141
|
+
- LICENSE.txt
|
142
|
+
- README.rdoc
|
143
|
+
files:
|
144
|
+
- .document
|
145
|
+
- .rspec
|
146
|
+
- Gemfile
|
147
|
+
- Gemfile.lock
|
148
|
+
- LICENSE.txt
|
149
|
+
- README.rdoc
|
150
|
+
- Rakefile
|
151
|
+
- VERSION
|
152
|
+
- chico.gemspec
|
153
|
+
- lib/chico.rb
|
154
|
+
- lib/chico/bitmapper.rb
|
155
|
+
- lib/chico/extractor.rb
|
156
|
+
- lib/chico/fetcher.rb
|
157
|
+
- spec/chico/bitmapper_spec.rb
|
158
|
+
- spec/chico/extractor_spec.rb
|
159
|
+
- spec/chico/fetcher_spec.rb
|
160
|
+
- spec/chico_spec.rb
|
161
|
+
- spec/res/foxnews.ico
|
162
|
+
- spec/res/google.ico
|
163
|
+
- spec/res/microsoft.ico
|
164
|
+
- spec/res/msn.ico
|
165
|
+
- spec/res/nhl.ico
|
166
|
+
- spec/res/sports.ico
|
167
|
+
- spec/res/wikipedia.ico
|
168
|
+
- spec/res/yahoo.ico
|
169
|
+
- spec/spec_helper.rb
|
170
|
+
homepage: http://github.com/ConduitTeam/chico
|
171
|
+
licenses:
|
172
|
+
- Apache2
|
173
|
+
post_install_message:
|
174
|
+
rdoc_options: []
|
175
|
+
require_paths:
|
176
|
+
- lib
|
177
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
178
|
+
none: false
|
179
|
+
requirements:
|
180
|
+
- - ! '>='
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: '0'
|
183
|
+
segments:
|
184
|
+
- 0
|
185
|
+
hash: -726080447
|
186
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
187
|
+
none: false
|
188
|
+
requirements:
|
189
|
+
- - ! '>='
|
190
|
+
- !ruby/object:Gem::Version
|
191
|
+
version: '0'
|
192
|
+
requirements: []
|
193
|
+
rubyforge_project:
|
194
|
+
rubygems_version: 1.8.10
|
195
|
+
signing_key:
|
196
|
+
specification_version: 3
|
197
|
+
summary: Fetch favicons
|
198
|
+
test_files: []
|