From c2e58b61c988318e07acba87a4a0b07887f8c2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Sat, 13 Apr 2013 16:31:20 +0200 Subject: [PATCH] automatically resizes images --- Gemfile | 3 + Gemfile.lock | 1 + lib/cooked_post_processor.rb | 75 ++++++------- lib/image_optimizer.rb | 100 +++++++++++++----- spec/components/cooked_post_processor_spec.rb | 4 +- spec/models/post_action_spec.rb | 4 + spec/models/post_alert_observer_spec.rb | 4 + spec/models/post_spec.rb | 4 + 8 files changed, 131 insertions(+), 64 deletions(-) diff --git a/Gemfile b/Gemfile index ef02bd1b5..b6e316909 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,9 @@ gem 'hiredis' # note: for image_optim to correctly work you need # sudo apt-get install -y advancecomp gifsicle jpegoptim libjpeg-progs optipng pngcrush gem 'image_optim' +# note: for image_sorcery to correctly work you need +# sudo apt-get install -y imagemagick +gem 'image_sorcery' gem 'jquery-rails' gem 'minitest' gem 'multi_json' diff --git a/Gemfile.lock b/Gemfile.lock index 340090ab0..1e38204c4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -219,6 +219,7 @@ GEM in_threads (~> 1.1.1) progress (~> 2.4.0) image_size (1.1.1) + image_sorcery (1.1.0) in_threads (1.1.1) ipaddress (0.8.0) jasminerice (0.0.10) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 48d458d20..b60e05a15 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -2,9 +2,9 @@ # example, inserting the onebox content, or image sizes. require_dependency 'oneboxer' +require_dependency 'image_optimizer' class CookedPostProcessor - require 'open-uri' def initialize(post, opts={}) @dirty = false @@ -34,58 +34,59 @@ class CookedPostProcessor images = @doc.search("img") return unless images.present? + images.each do |img| + src = img['src'] + src = Discourse.base_url_no_prefix + src if src[0] == "/" + + if src.present? + + if img['width'].blank? || img['height'].blank? + w, h = get_size_from_image_sizes(src, @opts[:image_sizes]) || image_dimensions(src) + + if w && h + img['width'] = w.to_s + img['height'] = h.to_s + @dirty = true + end + end + + if src != img['src'] + img['src'] = src + @dirty = true + end + + convert_to_link!(img) + img['src'] = optimize_image(img) + + end + end + # Extract the first image from the first post and use it as the 'topic image' if @post.post_number == 1 img = images.first @post.topic.update_column :image_url, img['src'] if img['src'].present? end - images.each do |img| - src = img['src'] - src = Discourse.base_url_no_prefix + src if src[0] == "/" - - if src.present? && (img['width'].blank? || img['height'].blank?) - - w,h = - get_size_from_image_sizes(src, @opts[:image_sizes]) || - image_dimensions(src) - - if w && h - img['width'] = w.to_s - img['height'] = h.to_s - @dirty = true - end - end - - if src.present? - if src != img['src'] - img['src'] = src - @dirty = true - end - convert_to_link!(img) - img.set_attribute('src', optimize_image(src)) - end - - end end - def optimize_image(src) - # uri = get_image_uri(src) - # uri.open(read_timeout: 20) do |f| - # - # end + def optimize_image(img) + src = img["src"] - src + # supports only local uploads + return src if SiteSetting.enable_imgur? || SiteSetting.enable_s3_uploads? + + width, height = img["width"].to_i, img["height"].to_i + + ImageOptimizer.new(src).optimized_image_url(width, height) end def convert_to_link!(img) src = img["src"] - width = img["width"].to_i - height = img["height"].to_i + width, height = img["width"].to_i, img["height"].to_i return unless src.present? && width > SiteSetting.auto_link_images_wider_than - original_width, original_height = get_size(src) + original_width, original_height = get_size(src) return unless original_width.to_i > width && original_height.to_i > height diff --git a/lib/image_optimizer.rb b/lib/image_optimizer.rb index 13bdbf764..0d8803f7f 100644 --- a/lib/image_optimizer.rb +++ b/lib/image_optimizer.rb @@ -1,35 +1,22 @@ +# # This class is used to download and optimize images. # -# I have not had a chance to implement me, and will not for about 3 weeks. -# If you are looking for a small project this simple API would be a good stint. -# -# Implement the following methods. With tests, the tests are a HUGE PITA cause -# network, disk and external dependencies are involved. + +require 'image_sorcery' +require 'digest/sha1' +require 'open-uri' class ImageOptimizer - attr_accessor :url, :root_dir + attr_accessor :url + # url is a url of an image ex: # 'http://site.com/image.png' # '/uploads/site/image.png' - # - # root_dir is the path where we - # store optimized images - def initialize(opts = {}) - @url = opts[:url] - @root_dir = opts[:root_dir] - end - - # attempt to refresh the original image, if refreshed - # remove old downsized copies - def refresh_local! - end - - # clear all local copies of the images - def clear_local! - end - - # yield a list of relative paths to local images cached - def each_local + def initialize(url) + @url = url + # make sure directories exists + FileUtils.mkdir_p downloads_dir + FileUtils.mkdir_p optimized_dir end # return the path of an optimized image, @@ -42,7 +29,68 @@ class ImageOptimizer # at the basic level it runs through image_optim https://github.com/toy/image_optim # it also has a failsafe that converts jpg to png or the opposite. if jpg size is 1.5* # as efficient as png it flips formats. - def optimized_image_path(width=nil, height=nil) + def optimized_image_url (width = nil, height = nil) + begin + unless has_been_uploaded? + return @url unless SiteSetting.crawl_images? + # download the file if it hasn't been cached yet + download! unless File.exists?(cached_path) + end + + # resize the image using Image Magick + result = ImageSorcery.new(cached_path).convert(optimized_path, resize: "#{width}x#{height}") + return optimized_url if result + @url + rescue + @url + end + end + +private + + def public_dir + @public_dir ||= "#{Rails.root}/public" + end + + def downloads_dir + @downloads_dir ||= "#{public_dir}/downloads/#{RailsMultisite::ConnectionManagement.current_db}" + end + + def optimized_dir + @optimized_dir ||= "#{public_dir}/images/#{RailsMultisite::ConnectionManagement.current_db}" + end + + def has_been_uploaded? + @url.start_with?(Discourse.base_url_no_prefix) + end + + def cached_path + @cached_path ||= if has_been_uploaded? + "#{public_dir}#{@url[Discourse.base_url_no_prefix.length..-1]}" + else + "#{downloads_dir}/#{file_name(@url)}" + end + end + + def optimized_path + @optimized_path ||= "#{optimized_dir}/#{file_name(cached_path)}" + end + + def file_name (uri) + image_info = FastImage.new(uri) + name = Digest::SHA1.hexdigest(uri)[0,16] + name << ".#{image_info.type}" + name + end + + def download! + File.open(cached_path, "wb") do |f| + f.write open(@url, "rb", read_timeout: 20).read + end + end + + def optimized_url + @optimized_url ||= Discourse::base_uri + "/images/#{RailsMultisite::ConnectionManagement.current_db}/#{file_name(cached_path)}" end end diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 250900537..88adf2292 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -41,6 +41,7 @@ describe CookedPostProcessor do before do @topic = Fabricate(:topic) @post = Fabricate.build(:post_with_image_url, topic: @topic, user: @topic.user) + ImageSorcery.any_instance.stubs(:convert).returns(false) @cpp = CookedPostProcessor.new(@post, image_sizes: {'http://www.forumwarz.com/images/header/logo.png' => {'width' => 111, 'height' => 222}}) @cpp.expects(:get_size).returns([111,222]) end @@ -63,6 +64,7 @@ describe CookedPostProcessor do before do FastImage.stubs(:size).returns([123, 456]) + ImageSorcery.any_instance.stubs(:convert).returns(false) CookedPostProcessor.any_instance.expects(:image_dimensions).returns([123, 456]) creator = PostCreator.new(user, raw: Fabricate.build(:post_with_images).raw, topic_id: topic.id) @post = creator.create @@ -70,7 +72,7 @@ describe CookedPostProcessor do it "adds a topic image if there's one in the post" do @post.topic.reload - @post.topic.image_url.should == "/path/to/img.jpg" + @post.topic.image_url.should == "http://test.localhost/path/to/img.jpg" end it "adds the height and width to images that don't have them" do diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb index 6d80f1db5..7cf669917 100644 --- a/spec/models/post_action_spec.rb +++ b/spec/models/post_action_spec.rb @@ -3,6 +3,10 @@ require_dependency 'post_destroyer' describe PostAction do + before do + ImageSorcery.any_instance.stubs(:convert).returns(false) + end + it { should belong_to :user } it { should belong_to :post } it { should belong_to :post_action_type } diff --git a/spec/models/post_alert_observer_spec.rb b/spec/models/post_alert_observer_spec.rb index b9366fea6..d33325166 100644 --- a/spec/models/post_alert_observer_spec.rb +++ b/spec/models/post_alert_observer_spec.rb @@ -3,6 +3,10 @@ require_dependency 'post_destroyer' describe PostAlertObserver do + before do + ImageSorcery.any_instance.stubs(:convert).returns(false) + end + let!(:evil_trout) { Fabricate(:evil_trout) } let(:post) { Fabricate(:post) } diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 805b74280..f46fed3f2 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -3,6 +3,10 @@ require_dependency 'post_destroyer' describe Post do + before do + ImageSorcery.any_instance.stubs(:convert).returns(false) + end + it { should belong_to :user } it { should belong_to :topic } it { should validate_presence_of :raw }