diff --git a/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6 b/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6 index 4f2b7729b96..8835b0cacce 100644 --- a/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6 +++ b/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6 @@ -2,24 +2,39 @@ const OBSERVER_OPTIONS = { rootMargin: "50%" // load images slightly before they're visible }; +const imageSources = new WeakMap(); + +const LOADING_DATA = + "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + // We hide an image by replacing it with a transparent gif function hide(image) { image.classList.add("d-lazyload"); image.classList.add("d-lazyload-hidden"); - image.setAttribute("data-src", image.getAttribute("src")); + + imageSources.set(image, { + src: image.getAttribute("src"), + srcSet: image.getAttribute("srcset") + }); + image.removeAttribute("srcset"); + image.setAttribute( "src", - "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" + image.getAttribute("data-small-upload") || LOADING_DATA ); + image.removeAttribute("data-small-upload"); } -// Restore an image from the `data-src` attribute +// Restore an image when onscreen function show(image) { - let dataSrc = image.getAttribute("data-src"); - if (dataSrc) { - image.setAttribute("src", dataSrc); - image.classList.remove("d-lazyload-hidden"); + let sources = imageSources.get(image); + if (sources) { + image.setAttribute("src", sources.src); + if (sources.srcSet) { + image.setAttribute("srcset", sources.srcSet); + } } + image.classList.remove("d-lazyload-hidden"); } export function setupLazyLoading(api) { diff --git a/app/assets/stylesheets/common/base/lightbox.scss b/app/assets/stylesheets/common/base/lightbox.scss index 7fc53af5192..9ddc6e919e7 100644 --- a/app/assets/stylesheets/common/base/lightbox.scss +++ b/app/assets/stylesheets/common/base/lightbox.scss @@ -1,6 +1,7 @@ .lightbox-wrapper .lightbox { position: relative; display: inline-block; + overflow: hidden; background: $primary-low; &:hover .meta { opacity: 0.9; @@ -9,7 +10,6 @@ } .d-lazyload-hidden { - opacity: 0; box-sizing: border-box; } diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index c9cdbd94ef5..a80103c7b44 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -68,7 +68,7 @@ class OptimizedImage < ActiveRecord::Base Rails.logger.error("Could not find file in the store located at url: #{upload.url}") else # create a temp file with the same extension as the original - extension = ".#{upload.extension}" + extension = ".#{opts[:format] || upload.extension}" if extension.length == 1 return nil @@ -96,6 +96,7 @@ class OptimizedImage < ActiveRecord::Base url: "", filesize: File.size(temp_path) ) + # store the optimized image and update its url File.open(temp_path) do |file| url = Discourse.store.store_optimized_image(file, thumbnail) @@ -173,7 +174,7 @@ class OptimizedImage < ActiveRecord::Base IM_DECODERS ||= /\A(jpe?g|png|tiff?|bmp|ico|gif)\z/i def self.prepend_decoder!(path, ext_path = nil, opts = nil) - extension = File.extname((opts && opts[:filename]) || ext_path || path)[1..-1] + extension = File.extname((opts && opts[:filename]) || path || ext_path)[1..-1] raise Discourse::InvalidAccess if !extension || !extension.match?(IM_DECODERS) "#{extension}:#{path}" end @@ -189,10 +190,14 @@ class OptimizedImage < ActiveRecord::Base from = prepend_decoder!(from, to, opts) to = prepend_decoder!(to, to, opts) + instructions = ['convert', "#{from}[0]"] + + if opts[:colors] + instructions << "-colors" << opts[:colors].to_s + end + # NOTE: ORDER is important! - %W{ - convert - #{from}[0] + instructions.concat(%W{ -auto-orient -gravity center -background transparent @@ -204,7 +209,7 @@ class OptimizedImage < ActiveRecord::Base -quality 98 -profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')} #{to} - } + }) end def self.resize_instructions_animated(from, to, dimensions, opts = {}) @@ -212,7 +217,7 @@ class OptimizedImage < ActiveRecord::Base %W{ gifsicle - --colors=256 + --colors=#{opts[:colors] || 256} --resize-fit #{dimensions} --optimize=3 --output #{to} @@ -248,7 +253,7 @@ class OptimizedImage < ActiveRecord::Base %W{ gifsicle --crop 0,0+#{dimensions} - --colors=256 + --colors=#{opts[:colors] || 256} --optimize=3 --output #{to} #{from} diff --git a/app/models/upload.rb b/app/models/upload.rb index 3f1f27960c4..a14b71b6fea 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -65,7 +65,8 @@ class Upload < ActiveRecord::Base opts = opts.merge(raise_on_error: true) begin OptimizedImage.create_for(self, width, height, opts) - rescue + rescue => ex + Rails.logger.info ex if Rails.env.development? opts = opts.merge(raise_on_error: false) if fix_image_extension OptimizedImage.create_for(self, width, height, opts) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 7458622c34c..eefac2fdaa2 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -10,6 +10,8 @@ class CookedPostProcessor INLINE_ONEBOX_LOADING_CSS_CLASS = "inline-onebox-loading" INLINE_ONEBOX_CSS_CLASS = "inline-onebox" + LOADING_SIZE = 10 + LOADING_COLORS = 32 attr_reader :cooking_options, :doc @@ -27,6 +29,8 @@ class CookedPostProcessor @doc = Nokogiri::HTML::fragment(post.cook(post.raw, @cooking_options)) @has_oneboxes = post.post_analyzer.found_oneboxes? @size_cache = {} + + @disable_loading_image = !!opts[:disable_loading_image] end def post_process(bypass_bump = false) @@ -332,11 +336,19 @@ class CookedPostProcessor upload.create_thumbnail!(resized_w, resized_h, crop: crop) end end + + unless @disable_loading_image + upload.create_thumbnail!(LOADING_SIZE, LOADING_SIZE, format: 'png', colors: LOADING_COLORS) + end end add_lightbox!(img, original_width, original_height, upload, cropped: crop) end + def loading_image(upload) + upload.thumbnail(LOADING_SIZE, LOADING_SIZE) + end + def is_a_hyperlink?(img) parent = img.parent while parent @@ -398,6 +410,10 @@ class CookedPostProcessor else img["src"] = upload.url end + + if small_upload = loading_image(upload) + img["data-small-upload"] = small_upload.url + end end # then, some overlay informations diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 0c5d05c20a3..21d360d0b60 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -15,7 +15,7 @@ describe CookedPostProcessor do RAW end - let(:cpp) { CookedPostProcessor.new(post) } + let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) } let(:post_process) { sequence("post_process") } it "post process in sequence" do @@ -288,10 +288,22 @@ describe CookedPostProcessor do filesize: 800 ) + # Fake a loading image + OptimizedImage.create!( + url: 'http://a.b.c/10x10.png', + width: CookedPostProcessor::LOADING_SIZE, + height: CookedPostProcessor::LOADING_SIZE, + upload_id: upload.id, + sha1: SecureRandom.hex, + extension: '.png', + filesize: 123 + ) + cpp = CookedPostProcessor.new(post) cpp.add_to_size_cache(upload.url, 2000, 1500) cpp.post_process_images + expect(cpp.loading_image(upload)).to be_present # 1.5x is skipped cause we have a missing thumb expect(cpp.html).to include('srcset="http://a.b.c/666x500.jpg, http://a.b.c/1998x1500.jpg 3x"') @@ -379,7 +391,7 @@ describe CookedPostProcessor do let(:upload) { Fabricate(:upload) } let(:post) { Fabricate(:post_with_large_image) } - let(:cpp) { CookedPostProcessor.new(post) } + let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) } before do SiteSetting.max_image_height = 2000 @@ -447,7 +459,7 @@ describe CookedPostProcessor do let(:upload) { Fabricate(:upload) } let(:post) { Fabricate(:post_with_large_image) } - let(:cpp) { CookedPostProcessor.new(post) } + let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) } before do SiteSetting.create_thumbnails = true @@ -462,7 +474,7 @@ describe CookedPostProcessor do it "crops the image" do cpp.post_process_images - expect(cpp.html).to match /width="690" height="500">/ + expect(cpp.html).to match(/width="690" height="500">/) expect(cpp).to be_dirty end @@ -472,7 +484,7 @@ describe CookedPostProcessor do let(:upload) { Fabricate(:upload) } let(:post) { Fabricate(:post_with_large_image) } - let(:cpp) { CookedPostProcessor.new(post) } + let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) } before do SiteSetting.create_thumbnails = true @@ -499,7 +511,7 @@ describe CookedPostProcessor do let(:upload) { Fabricate(:upload) } let(:post) { Fabricate(:post_with_large_image_on_subfolder) } - let(:cpp) { CookedPostProcessor.new(post) } + let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) } let(:base_url) { "http://test.localhost/subfolder" } let(:base_uri) { "/subfolder" } @@ -538,7 +550,7 @@ describe CookedPostProcessor do let(:upload) { Fabricate(:upload) } let(:post) { Fabricate(:post_with_large_image_and_title) } - let(:cpp) { CookedPostProcessor.new(post) } + let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) } before do SiteSetting.max_image_height = 2000 diff --git a/spec/models/optimized_image_spec.rb b/spec/models/optimized_image_spec.rb index 1eefbb01c35..5483a433e1a 100644 --- a/spec/models/optimized_image_spec.rb +++ b/spec/models/optimized_image_spec.rb @@ -29,6 +29,21 @@ describe OptimizedImage do end end + describe ".resize_instructions" do + let(:image) { "#{Rails.root}/spec/fixtures/images/logo.png" } + + it "doesn't return any color options by default" do + instructions = described_class.resize_instructions(image, image, "50x50") + expect(instructions).to_not include('-colors') + end + + it "supports an optional color option" do + instructions = described_class.resize_instructions(image, image, "50x50", colors: 12) + expect(instructions).to include('-colors') + end + + end + describe '.resize' do it 'should work correctly when extension is bad' do @@ -186,7 +201,6 @@ describe OptimizedImage do describe ".create_for" do it "is able to 'optimize' an svg" do - # we don't really optimize anything, we simply copy # but at least this confirms this actually works @@ -239,6 +253,11 @@ describe OptimizedImage do expect(oi.url).to eq("/internally/stored/optimized/image.png") end + it "is able to change the format" do + oi = OptimizedImage.create_for(upload, 100, 200, format: 'gif') + expect(oi.url).to eq("/internally/stored/optimized/image.gif") + end + end end