FIX: Perform crop using user-specified image sizes (#9224)

* FIX: Perform crop using user-specified image sizes

It used to resize the images to max width and height first and then
perform the crop operation. This is wrong because it ignored the user
specified image sizes from the Markdown.

* DEV: Use real images in test
This commit is contained in:
Bianca Nenciu
2020-03-26 16:40:00 +02:00
committed by GitHub
parent ba1a08510e
commit 7952cbb9a2
4 changed files with 63 additions and 42 deletions

View File

@ -187,6 +187,10 @@
&.site-icon { &.site-icon {
padding-bottom: 0; padding-bottom: 0;
} }
&.resizable {
object-fit: cover;
object-position: top;
}
} }
.d-editor-preview .image-wrapper { .d-editor-preview .image-wrapper {

View File

@ -308,29 +308,34 @@ class CookedPostProcessor
end end
def convert_to_link!(img) def convert_to_link!(img)
w, h = img["width"].to_i, img["height"].to_i
user_width, user_height = (w > 0 && h > 0 && [w, h]) ||
get_size_from_attributes(img) ||
get_size_from_image_sizes(img["src"], @opts[:image_sizes])
limit_size!(img)
src = img["src"] src = img["src"]
return if src.blank? || is_a_hyperlink?(img) || is_svg?(img) return if src.blank? || is_a_hyperlink?(img) || is_svg?(img)
width, height = img["width"].to_i, img["height"].to_i
# TODO: store original dimentions in db
original_width, original_height = (get_size(src) || [0, 0]).map(&:to_i) original_width, original_height = (get_size(src) || [0, 0]).map(&:to_i)
# can't reach the image...
if original_width == 0 || original_height == 0 if original_width == 0 || original_height == 0
Rails.logger.info "Can't reach '#{src}' to get its dimension." Rails.logger.info "Can't reach '#{src}' to get its dimension."
return return
end end
return if original_width <= width && original_height <= height
return if original_width <= SiteSetting.max_image_width && original_height <= SiteSetting.max_image_height return if original_width <= SiteSetting.max_image_width && original_height <= SiteSetting.max_image_height
crop = SiteSetting.min_ratio_to_crop > 0 user_width, user_height = [original_width, original_height] if user_width.to_i <= 0 && user_height.to_i <= 0
crop &&= original_width.to_f / original_height.to_f < SiteSetting.min_ratio_to_crop width, height = user_width, user_height
crop = SiteSetting.min_ratio_to_crop > 0 && width.to_f / height.to_f < SiteSetting.min_ratio_to_crop
if crop if crop
width, height = ImageSizer.crop(original_width, original_height) width, height = ImageSizer.crop(width, height)
img["width"] = width img["width"], img["height"] = width, height
img["height"] = height else
width, height = ImageSizer.resize(width, height)
end end
# if the upload already exists and is attached to a different post, # if the upload already exists and is attached to a different post,
@ -700,10 +705,7 @@ class CookedPostProcessor
def post_process_images def post_process_images
extract_images.each do |img| extract_images.each do |img|
unless add_image_placeholder!(img) convert_to_link!(img) unless add_image_placeholder!(img)
limit_size!(img)
convert_to_link!(img)
end
end end
end end

View File

@ -416,12 +416,17 @@ describe CookedPostProcessor do
end end
context "with unsized images" do context "with unsized images" do
fab!(:upload) { Fabricate(:image_upload, width: 123, height: 456) }
fab!(:post) do
Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}">
HTML
end
fab!(:post) { Fabricate(:post_with_unsized_images) }
let(:cpp) { CookedPostProcessor.new(post) } let(:cpp) { CookedPostProcessor.new(post) }
it "adds the width and height to images that don't have them" do it "adds the width and height to images that don't have them" do
FastImage.expects(:size).returns([123, 456])
cpp.post_process cpp.post_process
expect(cpp.html).to match(/width="123" height="456"/) expect(cpp.html).to match(/width="123" height="456"/)
expect(cpp).to be_dirty expect(cpp).to be_dirty
@ -430,6 +435,8 @@ describe CookedPostProcessor do
end end
context "with large images" do context "with large images" do
fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) }
fab!(:post) do fab!(:post) do
Fabricate(:post, raw: <<~HTML) Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}"> <img src="#{upload.url}">
@ -441,13 +448,9 @@ describe CookedPostProcessor do
before do before do
SiteSetting.max_image_height = 2000 SiteSetting.max_image_height = 2000
SiteSetting.create_thumbnails = true SiteSetting.create_thumbnails = true
FastImage.expects(:size).returns([1750, 2000])
end end
it "generates overlay information" do it "generates overlay information" do
OptimizedImage.expects(:resize).returns(true)
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
cpp.post_process cpp.post_process
expect(cpp.html).to match_html <<~HTML expect(cpp.html).to match_html <<~HTML
@ -468,6 +471,8 @@ describe CookedPostProcessor do
end end
it 'should not add lightbox' do it 'should not add lightbox' do
FastImage.expects(:size).returns([1750, 2000])
cpp.post_process cpp.post_process
expect(cpp.html).to match_html <<~HTML expect(cpp.html).to match_html <<~HTML
@ -482,6 +487,8 @@ describe CookedPostProcessor do
end end
it 'should not add lightbox' do it 'should not add lightbox' do
FastImage.expects(:size).returns([1750, 2000])
cpp.post_process cpp.post_process
expect(cpp.html).to match_html <<~HTML expect(cpp.html).to match_html <<~HTML
@ -495,6 +502,8 @@ describe CookedPostProcessor do
end end
it 'should not add lightbox' do it 'should not add lightbox' do
FastImage.expects(:size).returns([1750, 2000])
SiteSetting.crawl_images = true SiteSetting.crawl_images = true
cpp.post_process cpp.post_process
@ -537,8 +546,8 @@ describe CookedPostProcessor do
context "when the upload is attached to the correct post" do context "when the upload is attached to the correct post" do
before do before do
FastImage.expects(:size).returns([1750, 2000])
OptimizedImage.expects(:resize).returns(true) OptimizedImage.expects(:resize).returns(true)
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
Discourse.store.class.any_instance.expects(:has_been_uploaded?).at_least_once.returns(true) Discourse.store.class.any_instance.expects(:has_been_uploaded?).at_least_once.returns(true)
upload.update(secure: true, access_control_post: post) upload.update(secure: true, access_control_post: post)
end end
@ -568,6 +577,8 @@ describe CookedPostProcessor do
end end
context "with tall images" do context "with tall images" do
fab!(:upload) { Fabricate(:image_upload, width: 860, height: 2000) }
fab!(:post) do fab!(:post) do
Fabricate(:post, raw: <<~HTML) Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}"> <img src="#{upload.url}">
@ -578,10 +589,6 @@ describe CookedPostProcessor do
before do before do
SiteSetting.create_thumbnails = true SiteSetting.create_thumbnails = true
FastImage.expects(:size).returns([860, 2000])
OptimizedImage.expects(:resize).never
OptimizedImage.expects(:crop).returns(true)
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
end end
it "crops the image" do it "crops the image" do
@ -594,6 +601,8 @@ describe CookedPostProcessor do
end end
context "with iPhone X screenshots" do context "with iPhone X screenshots" do
fab!(:upload) { Fabricate(:image_upload, width: 1125, height: 2436) }
fab!(:post) do fab!(:post) do
Fabricate(:post, raw: <<~HTML) Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}"> <img src="#{upload.url}">
@ -604,10 +613,6 @@ describe CookedPostProcessor do
before do before do
SiteSetting.create_thumbnails = true SiteSetting.create_thumbnails = true
FastImage.expects(:size).returns([1125, 2436])
OptimizedImage.expects(:resize).returns(true)
OptimizedImage.expects(:crop).never
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
end end
it "crops the image" do it "crops the image" do
@ -625,6 +630,8 @@ describe CookedPostProcessor do
end end
context "with large images when using subfolders" do context "with large images when using subfolders" do
fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) }
fab!(:post) do fab!(:post) do
Fabricate(:post, raw: <<~HTML) Fabricate(:post, raw: <<~HTML)
<img src="/subfolder#{upload.url}"> <img src="/subfolder#{upload.url}">
@ -635,13 +642,10 @@ describe CookedPostProcessor do
before do before do
set_subfolder "/subfolder" set_subfolder "/subfolder"
stub_request(:get, "http://#{Discourse.current_hostname}/subfolder#{upload.url}").to_return(status: 200, body: File.new(Discourse.store.path_for(upload)))
SiteSetting.max_image_height = 2000 SiteSetting.max_image_height = 2000
SiteSetting.create_thumbnails = true SiteSetting.create_thumbnails = true
FastImage.expects(:size).returns([1750, 2000])
OptimizedImage.expects(:resize).returns(true)
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
end end
it "generates overlay information" do it "generates overlay information" do
@ -670,6 +674,8 @@ describe CookedPostProcessor do
end end
context "with title and alt" do context "with title and alt" do
fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) }
fab!(:post) do fab!(:post) do
Fabricate(:post, raw: <<~HTML) Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}" title="WAT" alt="RED"> <img src="#{upload.url}" title="WAT" alt="RED">
@ -681,9 +687,6 @@ describe CookedPostProcessor do
before do before do
SiteSetting.max_image_height = 2000 SiteSetting.max_image_height = 2000
SiteSetting.create_thumbnails = true SiteSetting.create_thumbnails = true
FastImage.expects(:size).returns([1750, 2000])
OptimizedImage.expects(:resize).returns(true)
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
end end
it "generates overlay information using image title and ignores alt" do it "generates overlay information using image title and ignores alt" do
@ -701,6 +704,8 @@ describe CookedPostProcessor do
end end
context "with title only" do context "with title only" do
fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) }
fab!(:post) do fab!(:post) do
Fabricate(:post, raw: <<~HTML) Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}" title="WAT"> <img src="#{upload.url}" title="WAT">
@ -712,9 +717,6 @@ describe CookedPostProcessor do
before do before do
SiteSetting.max_image_height = 2000 SiteSetting.max_image_height = 2000
SiteSetting.create_thumbnails = true SiteSetting.create_thumbnails = true
FastImage.expects(:size).returns([1750, 2000])
OptimizedImage.expects(:resize).returns(true)
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
end end
it "generates overlay information using image title" do it "generates overlay information using image title" do
@ -732,6 +734,8 @@ describe CookedPostProcessor do
end end
context "with alt only" do context "with alt only" do
fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) }
fab!(:post) do fab!(:post) do
Fabricate(:post, raw: <<~HTML) Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}" alt="RED"> <img src="#{upload.url}" alt="RED">
@ -743,9 +747,6 @@ describe CookedPostProcessor do
before do before do
SiteSetting.max_image_height = 2000 SiteSetting.max_image_height = 2000
SiteSetting.create_thumbnails = true SiteSetting.create_thumbnails = true
FastImage.expects(:size).returns([1750, 2000])
OptimizedImage.expects(:resize).returns(true)
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
end end
it "generates overlay information using image alt" do it "generates overlay information using image alt" do

View File

@ -21,6 +21,20 @@ Fabricator(:upload) do
extension "png" extension "png"
end end
Fabricator(:image_upload, from: :upload) do
after_create do |upload|
file = Tempfile.new(['fabricated', '.png'])
`convert -size #{upload.width}x#{upload.height} xc:white "#{file.path}"`
upload.url = Discourse.store.store_upload(file, upload)
upload.sha1 = Upload.generate_digest(file.path)
WebMock
.stub_request(:get, "http://#{Discourse.current_hostname}#{upload.url}")
.to_return(status: 200, body: File.new(file.path))
end
end
Fabricator(:video_upload, from: :upload) do Fabricator(:video_upload, from: :upload) do
original_filename "video.mp4" original_filename "video.mp4"
width nil width nil