photomosaic 0.0.1

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.
@@ -0,0 +1,39 @@
1
+ require "fileutils"
2
+ require "open-uri"
3
+ require "tmpdir"
4
+
5
+ module Photomosaic
6
+ class ImageDownloader
7
+ def initialize(save_dir = tmp_dir)
8
+ @save_dir = save_dir
9
+ end
10
+
11
+ def download_images(image_url_list)
12
+ image_url_list.inject([]) do |path_list, image_url|
13
+ image_path = File.join(@save_dir, File.basename(image_url))
14
+
15
+ begin
16
+ download_image(image_url, image_path)
17
+ path_list << image_path
18
+ rescue
19
+ end
20
+
21
+ path_list
22
+ end
23
+ end
24
+
25
+ def remove_save_dir
26
+ FileUtils.remove_entry_secure(@save_dir)
27
+ end
28
+
29
+ private
30
+
31
+ def download_image(image_url, image_path)
32
+ open(image_path, "wb+") { |f| f.puts open(image_url).read }
33
+ end
34
+
35
+ def tmp_dir
36
+ Dir.mktmpdir("photomosaic")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,80 @@
1
+ require "optparse"
2
+
3
+ module Photomosaic
4
+ class Options
5
+ KEYS = %w(
6
+ api_key
7
+ base_image
8
+ color_model
9
+ colors
10
+ height
11
+ keyword
12
+ level
13
+ output_path
14
+ results
15
+ search_engine
16
+ width
17
+ )
18
+
19
+ REQUIRED_KEYS = %w(
20
+ api_key
21
+ base_image
22
+ output_path
23
+ )
24
+
25
+ def self.parse(argv)
26
+ options = default_options
27
+ parser(options).parse(argv)
28
+ options[:api_key] = api_key
29
+ check_options(options)
30
+
31
+ self.new(options)
32
+ end
33
+
34
+ def initialize(options)
35
+ @options = options
36
+ end
37
+
38
+ KEYS.each do |key|
39
+ define_method(key) { @options[key.to_sym] }
40
+ end
41
+
42
+ private
43
+
44
+ def self.api_key
45
+ ENV["PHOTOMOSAIC_API_KEY"]
46
+ end
47
+
48
+ def self.check_options(options)
49
+ REQUIRED_KEYS.each do |key|
50
+ raise OptionParser::MissingArgument, key unless options[key.to_sym]
51
+ end
52
+ end
53
+
54
+ def self.default_options
55
+ {
56
+ color_model: :rgb,
57
+ colors: 16,
58
+ height: 200,
59
+ level: 4,
60
+ results: 50,
61
+ search_engine: Photomosaic::SearchEngine::Bing,
62
+ width: 200,
63
+ }
64
+ end
65
+
66
+ def self.parser(options)
67
+ OptionParser.new do |opt|
68
+ opt.on("-b", "--base_image=VAL", "Path of base image") { |val| options[:base_image] = File.expand_path(val) }
69
+ opt.on("-c", "--colors=VAL", "Number of colors") { |val| options[:colors] = val.to_i }
70
+ opt.on("-h", "--height=VAL", "Height of output image") { |val| options[:height] = val.to_i }
71
+ opt.on("-k", "--keyword=VAL", "Keyword") { |val| options[:keyword] = val }
72
+ opt.on("-l", "--level=VAL", "Color level") { |val| options[:level] = val.to_i }
73
+ opt.on("-o", "--output_path=VAL", "Path of mosaic image") { |val| options[:output_path] = File.expand_path(val) }
74
+ opt.on("-r", "--results=VAL", "Number of results") { |val| options[:results] = val.to_i }
75
+ opt.on("-w", "--width=VAL", "Width of output image") { |val| options[:width] = val.to_i }
76
+ opt.on("--hsv", "Use HSV") { |val| options[:color_model] = :hsv }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ require "searchbing"
2
+
3
+ module Photomosaic
4
+ module SearchEngine
5
+ class Bing
6
+ def initialize(api_key, results)
7
+ @client = ::Bing.new(api_key, results, "Image")
8
+ end
9
+
10
+ def get_image_list(keyword)
11
+ @client.search(keyword)[0][:Image].map { |image| image[:MediaUrl] }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Photomosaic
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'photomosaic/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "photomosaic"
8
+ spec.version = Photomosaic::VERSION
9
+ spec.authors = ["Daisuke Fujita"]
10
+ spec.email = ["dtanshi45@gmail.com"]
11
+ spec.summary = %q{Photomosaic Generator}
12
+ spec.description = %q{Photomosaic Generator}
13
+ spec.homepage = "https://github.com/dtan4/photomosaic"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "searchbing"
22
+ spec.add_dependency "rmagick"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.5"
25
+ spec.add_development_dependency "coveralls"
26
+ spec.add_development_dependency "guard-rspec"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "rspec", "~> 3.0.0"
29
+ spec.add_development_dependency "terminal-notifier-guard"
30
+ spec.add_development_dependency "webmock"
31
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,91 @@
1
+ require "spec_helper"
2
+ require "ostruct"
3
+
4
+ module Photomosaic
5
+ describe Client do
6
+ let(:api_key) { "api_key" }
7
+ let(:base_image) { fixture_path("lena.png") }
8
+ let(:color_model) { :rgb }
9
+ let(:colors) { 16 }
10
+ let(:height) { 200 }
11
+ let(:keyword) { "keyword" }
12
+ let(:level) { 4 }
13
+ let(:output_path) { tmp_path("output.png") }
14
+ let(:results) { 50 }
15
+ let(:search_engine) { SearchEngine::Bing }
16
+ let(:width) { 200 }
17
+
18
+ let(:options) do
19
+ {
20
+ api_key: api_key,
21
+ base_image: base_image,
22
+ color_model: color_model,
23
+ colors: colors,
24
+ height: height,
25
+ keyword: keyword,
26
+ level: level,
27
+ output_path: output_path,
28
+ results: results,
29
+ search_engine: search_engine,
30
+ width: width
31
+ }
32
+ end
33
+
34
+ let(:client) do
35
+ described_class.new("argv")
36
+ end
37
+
38
+ let(:dispatched_images) do
39
+ [
40
+ [image],
41
+ [image],
42
+ [image]
43
+ ]
44
+ end
45
+
46
+ let(:image) do
47
+ double(Photomosaic::Image)
48
+ end
49
+
50
+ let(:image_name_list) do
51
+ [
52
+ "lena_0.png",
53
+ "lena_1.png",
54
+ "lena_2.png",
55
+ "notfound.png"
56
+ ]
57
+ end
58
+
59
+ let(:image_path_list) do
60
+ image_name_list.map { |name| fixture_path(name) }
61
+ end
62
+
63
+ let(:image_url_list) do
64
+ image_name_list.map { |name| "http://example.com/#{name}" }
65
+ end
66
+
67
+ before do
68
+ allow(Photomosaic::Options).to receive(:parse).and_return(OpenStruct.new(options))
69
+ end
70
+
71
+ describe "#execute" do
72
+ before do
73
+ allow_any_instance_of(Photomosaic::SearchEngine::Bing).to receive(:get_image_list).and_return(image_url_list)
74
+ allow_any_instance_of(Photomosaic::ImageDownloader).to receive(:download_images).and_return(image_path_list)
75
+ allow(Photomosaic::Image).to receive(:create_mosaic_image)
76
+ allow(Photomosaic::Image).to receive(:new).with(/lena(?:_\d)?\.png/).and_return(image)
77
+ allow(Photomosaic::Image).to receive(:new).with(/notfound.png/).and_raise Magick::ImageMagickError
78
+ allow(Photomosaic::Image).to receive(:resize_to_pixel_size)
79
+ allow(image).to receive(:dispatch_images).and_return(dispatched_images)
80
+ allow(image).to receive(:posterize!)
81
+ allow(image).to receive(:reduce_colors!)
82
+ allow(image).to receive(:resize!)
83
+ end
84
+
85
+ it "should execute the program" do
86
+ expect(Photomosaic::Image).to receive(:create_mosaic_image)
87
+ client.execute
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,16 @@
1
+ require "spec_helper"
2
+
3
+ module Photomosaic
4
+ module Color
5
+ describe HSV do
6
+ describe "#calculate_distance" do
7
+ it "should calculate color distance" do
8
+ color_a = described_class.new(100, 50.0, 70.0)
9
+ color_b = described_class.new(50, 30.0, 50.0)
10
+
11
+ expect(color_a.calculate_distance(color_b)).to be_within(0.1).of(57.4)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ require "spec_helper"
2
+
3
+ module Photomosaic
4
+ module Color
5
+ describe RGB do
6
+ describe "#calculate_distance" do
7
+ it "should calculate color distance" do
8
+ color_a = described_class.new(50, 100, 200)
9
+ color_b = described_class.new(10, 20, 30)
10
+
11
+ expect(color_a.calculate_distance(color_b)).to be_within(0.5).of(192.0)
12
+ end
13
+ end
14
+
15
+ describe "#to_hsv" do
16
+ # cf. http://tech-unlimited.com/color.html
17
+
18
+ it "should convert (50, 50, 50) to (0, 0, 20)" do
19
+ rgb = described_class.new(50, 50, 50)
20
+ hsv = rgb.to_hsv
21
+ expect(hsv.hue).to eq 0
22
+ expect(hsv.saturation).to be_within(1).of(0)
23
+ expect(hsv.value).to be_within(1).of(20)
24
+ end
25
+
26
+ it "should convert (50, 100, 200) to (220, 75, 78)" do
27
+ rgb = described_class.new(50, 100, 200)
28
+ hsv = rgb.to_hsv
29
+ expect(hsv.hue).to eq 220
30
+ expect(hsv.saturation).to be_within(1).of(75)
31
+ expect(hsv.value).to be_within(1).of(78)
32
+ end
33
+
34
+ it "should convert (100, 50, 200) to (260, 75, 78)" do
35
+ rgb = described_class.new(100, 50, 200)
36
+ hsv = rgb.to_hsv
37
+ expect(hsv.hue).to eq 260
38
+ expect(hsv.saturation).to be_within(1).of(75)
39
+ expect(hsv.value).to be_within(1).of(78)
40
+ end
41
+
42
+ it "should convert (100, 200, 50) to (100, 75, 78)" do
43
+ rgb = described_class.new(100, 200, 50)
44
+ hsv = rgb.to_hsv
45
+ expect(hsv.hue).to eq 100
46
+ expect(hsv.saturation).to be_within(1).of(75)
47
+ expect(hsv.value).to be_within(1).of(78)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,74 @@
1
+ require "spec_helper"
2
+ require "fileutils"
3
+ require "webmock/rspec"
4
+
5
+ module Photomosaic
6
+ describe ImageDownloader do
7
+ let(:downloader) do
8
+ described_class.new(tmp_dir)
9
+ end
10
+
11
+ describe "#initialize" do
12
+ context "with save directory" do
13
+ it "should set the specified directory to @save_dir" do
14
+ downloader = described_class.new(tmp_dir)
15
+ expect(downloader.instance_variable_get(:@save_dir)).to eq tmp_dir
16
+ end
17
+ end
18
+
19
+ context "without save directory" do
20
+ it "should set temporary directory to @save_dir" do
21
+ downloader = described_class.new
22
+ expect(downloader.instance_variable_get(:@save_dir)).to match(/photomosaic/)
23
+ end
24
+ end
25
+ end
26
+
27
+ describe "#download_images" do
28
+ let(:image_url_list) do
29
+ [
30
+ "http://example.com/image01.jpg",
31
+ "http://example.com/image02.jpg",
32
+ "http://example.com/notfound.jpg"
33
+ ]
34
+ end
35
+
36
+ before do
37
+ stub_request(:get, %r(http://example\.com/image0\d\.jpg))
38
+ .to_return(status: 200, body: "hoge")
39
+ stub_request(:get, "http://example.com/notfound.jpg")
40
+ .to_return(status: 404)
41
+ FileUtils.rm_rf(tmp_dir) if Dir.exist?(tmp_dir)
42
+ Dir.mkdir(tmp_dir)
43
+ end
44
+
45
+ it "should download listed images to temporary directory" do
46
+ downloader.download_images(image_url_list)
47
+
48
+ %w(image01.jpg image02.jpg).each do |image|
49
+ expect(File.exist?(tmp_path(image))).to be_truthy
50
+ end
51
+ end
52
+
53
+ it "should return the path list" do
54
+ result = downloader.download_images(image_url_list)
55
+ expect(result).to match_array %w(image01.jpg image02.jpg).map { |image| tmp_path(image) }
56
+ end
57
+
58
+ after do
59
+ FileUtils.rm_rf(tmp_dir)
60
+ end
61
+ end
62
+
63
+ describe "#remove_save_dir" do
64
+ before do
65
+ Dir.mkdir(tmp_dir)
66
+ end
67
+
68
+ it "should remove save directory" do
69
+ downloader.remove_save_dir
70
+ expect(Dir.exist?(tmp_dir)).to be_falsy
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,151 @@
1
+ require "spec_helper"
2
+ require "fileutils"
3
+ require "tempfile"
4
+
5
+ module Photomosaic
6
+ describe Image do
7
+ let(:image_path) do
8
+ fixture_path("lena.png")
9
+ end
10
+
11
+ let(:image_path_list) do
12
+ [
13
+ fixture_path("lena_0.png"),
14
+ fixture_path("lena_1.png"),
15
+ fixture_path("lena_2.png"),
16
+ fixture_path("lena_3.png"),
17
+ fixture_path("lena_4.png"),
18
+ fixture_path("lena_5.png")
19
+ ]
20
+ end
21
+
22
+ context "class methods" do
23
+ describe "#create_mosaic_image" do
24
+ let(:image_list) do
25
+ 5.times.inject([]) do |image_list, _|
26
+ image_list << image_path_list.map { |path| described_class.new(path) }
27
+ image_list
28
+ end
29
+ end
30
+
31
+ let(:output_path) do
32
+ tmp_path("tiled_image.jpg")
33
+ end
34
+
35
+ before do
36
+ FileUtils.rm_rf(tmp_dir) if Dir.exist?(tmp_dir)
37
+ Dir.mkdir(tmp_dir)
38
+ end
39
+
40
+ it "should create the mosaic image" do
41
+ described_class.create_mosaic_image(image_list, output_path)
42
+ expect(File.exist?(output_path)).to be_truthy
43
+ end
44
+
45
+ after do
46
+ FileUtils.rm_rf(tmp_dir)
47
+ end
48
+ end
49
+
50
+ describe "#preprocess_image" do
51
+ it "should preprocess image" do
52
+ expect_any_instance_of(described_class).to receive(:resize!).with(100, 100, true).once
53
+ expect_any_instance_of(described_class).to receive(:posterize!).with(4).once
54
+ expect_any_instance_of(described_class).to receive(:reduce_colors!).with(8).once
55
+ described_class.preprocess_image(image_path, 100, 100, 4, 8)
56
+ end
57
+ end
58
+
59
+ describe "#resize_images" do
60
+ let(:image) do
61
+ double(:resize!)
62
+ end
63
+
64
+ let(:image_list) do
65
+ 5.times.inject([]) do |image_list, _|
66
+ image_list << [image]
67
+ image_list
68
+ end
69
+ end
70
+
71
+ it "should resize images" do
72
+ expect(image).to receive(:resize!).exactly(5).times
73
+ described_class.resize_images(image_list, 40, 20)
74
+ end
75
+ end
76
+ end
77
+
78
+ context "instance methods" do
79
+ let(:image) do
80
+ described_class.new(image_path)
81
+ end
82
+
83
+ describe "#characteristic_color" do
84
+ context "by RGB" do
85
+ it "should return characteristic color in RGB" do
86
+ characteristic_color = image.characteristic_color(:rgb)
87
+ expect(characteristic_color.red).to be_within(1).of(180)
88
+ expect(characteristic_color.green).to be_within(1).of(100)
89
+ expect(characteristic_color.blue).to be_within(1).of(107)
90
+ end
91
+ end
92
+
93
+ context "by HSV" do
94
+ it "should return characteristic color in HSV" do
95
+ characteristic_color = image.characteristic_color(:hsv)
96
+ expect(characteristic_color.hue).to eq 354
97
+ expect(characteristic_color.saturation).to be_within(0.5).of(44.7)
98
+ expect(characteristic_color.value).to be_within(0.5).of(70.0)
99
+ end
100
+ end
101
+ end
102
+
103
+ describe "#dispatch_images" do
104
+ let(:image_list) do
105
+ image_path_list.map { |path| described_class.new(path) }
106
+ end
107
+
108
+ it "should return the map of dispatched images as a 2-dimentional array" do
109
+ images = image.dispatch_images(image_list, 8, 8)
110
+ expect(images.length).to eq 64
111
+ expect(images[0].length).to eq 64
112
+ expect(images[0][0]).to be_a described_class
113
+ end
114
+ end
115
+
116
+ describe "#posterize!" do
117
+ it "should posterize itself" do
118
+ expect_any_instance_of(Magick::Image).to receive(:posterize).with(4)
119
+ expect_any_instance_of(described_class).to receive(:reload_image)
120
+ image.posterize!(4)
121
+ end
122
+ end
123
+
124
+ describe "#reduce_colors!" do
125
+ it "should reduce its colors" do
126
+ expect_any_instance_of(Magick::Image).to receive(:quantize).with(8)
127
+ expect_any_instance_of(described_class).to receive(:reload_image)
128
+ image.reduce_colors!(8)
129
+ end
130
+ end
131
+
132
+ describe "#resize!" do
133
+ context "to keep aspect ratio" do
134
+ it "should resize itself" do
135
+ expect_any_instance_of(Magick::Image).to receive(:resize_to_fit!).with(25, 50)
136
+ expect_any_instance_of(described_class).to receive(:reload_image)
137
+ image.resize!(25, 50, true)
138
+ end
139
+ end
140
+
141
+ context "not to keep aspect ratio" do
142
+ it "should resize itself" do
143
+ expect_any_instance_of(Magick::Image).to receive(:resize!).with(25, 50)
144
+ expect_any_instance_of(described_class).to receive(:reload_image)
145
+ image.resize!(25, 50, false)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end