diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index f3f07aaadc5..5f3f9fe963c 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -103,6 +103,9 @@ def notice_message(article) end def fit_to_size_for_ogp - Ogp::ImageProcessor.fit_to_size(@article.thumbnail) if @article.thumbnail.attached? + return unless @article.thumbnail.attached? + + image_resizer = ImageResizer.new(@article.thumbnail) + image_resizer.fit_to!(*ImageResizer::OGP_SIZE) end end diff --git a/app/models/image_resizer.rb b/app/models/image_resizer.rb new file mode 100644 index 00000000000..73dca38fa84 --- /dev/null +++ b/app/models/image_resizer.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class ImageResizer + OGP_SIZE = [1200, 630].freeze + + def initialize(attached_one) + @attached_one = attached_one + @transformation = ImageResizerTransformation.new + end + + def fit_to!(width, height) + return nil if just_fit_to_size?(width, height) + + original_width, original_height = original_size + + fitted = @attached_one.variant(**@transformation.fit_to(original_width, original_height, width, height)) + .processed + .image.blob + + # 変形前の画像関連データの削除にpurge、purge_laterは不要。 + # データの削除はattachで担える + @attached_one.attach(fitted) + end + + def just_fit_to_size?(width, height) + original_width, original_height = original_size + width == original_width && height == original_height + end + + private + + def original_size + blob = @attached_one.blob + blob.analyze unless blob.analyzed? + [blob.metadata[:width], blob.metadata[:height]] + end +end diff --git a/app/models/image_resizer_transformation.rb b/app/models/image_resizer_transformation.rb new file mode 100644 index 00000000000..2a304f85f56 --- /dev/null +++ b/app/models/image_resizer_transformation.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class ImageResizerTransformation + def fit_to(original_width, original_height, width, height) + # ハッシュを返すが処理順(リサイズ->切り抜き)にオプションを記述 + {}.merge(resize(original_width, original_height, width, height), + center_crop(width, height)) + end + + # 縦横比を維持しながら、リサイズ後の縦をx、横をyとしたときに + # いずれかの条件を満たすようにリサイズする + # x = widht かつ y >= height + # x >= width かつ y = height + def resize(original_width, original_height, width, height) + ratio_w = width.to_f / original_width + ratio_h = height.to_f / original_height + + if ratio_w > ratio_h + { resize: "#{width}x" } + else + { resize: "x#{height}" } + end + end + + def center_crop(width, height) + { gravity: :center, + # 仮想キャンバスのメタデータをリセットするために!を末尾につける + # +repageは指定できないため、+repage相当の動作をする!を採用 + crop: "#{width}x#{height}+0+0!" } + end +end diff --git a/app/models/ogp/image_processor.rb b/app/models/ogp/image_processor.rb deleted file mode 100644 index 270abc1f3e6..00000000000 --- a/app/models/ogp/image_processor.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -module Ogp - class ImageProcessor - WIDTH = 1200 - HEIGHT = 630 - - class << self - def fit_to_size(attached_one) - blob = attached_one.blob - blob.analyze unless blob.analyzed? - width = blob.metadata[:width] - height = blob.metadata[:height] - - return nil if just_fit_to_size?(width: width, height: height) - - opts = fit_to_size_option(width: width, height: height) - - fitted = attached_one.variant(**opts).processed.image.blob - - # 変形前の画像関連データの削除にpurge、purge_laterは不要。 - # データの削除はattachで担える - attached_one.attach(fitted) - end - - def just_fit_to_size?(width:, height:) - width == WIDTH && height == HEIGHT - end - - def fit_to_size_option(width:, height:) - # ハッシュを返すが処理順(リサイズ->切り抜き)にオプションを記述 - {}.merge(resize_option(width: width, height: height), clip_option) - end - - def resize_option(width:, height:) - ratio_w = WIDTH.to_f / width - ratio_h = HEIGHT.to_f / height - - if ratio_w > ratio_h - { resize: "#{WIDTH}x" } - else - { resize: "x#{HEIGHT}" } - end - end - - def clip_option - { gravity: :center, - # 仮想キャンバスのメタデータをリセットするために!を末尾につける - # +repageは指定できないため、+repage相当の動作をする!を採用 - crop: "#{WIDTH}x#{HEIGHT}+0+0!" } - end - end - end -end diff --git a/test/models/image_resizer_test.rb b/test/models/image_resizer_test.rb new file mode 100644 index 00000000000..6e8f104edfe --- /dev/null +++ b/test/models/image_resizer_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ImageResizerTest < ActiveSupport::TestCase + test '.fit_to!' do + article = articles(:article4) + + ImageResizer.new(article.thumbnail).fit_to!(*ImageResizer::OGP_SIZE) + + article.thumbnail.blob.analyze unless article.thumbnail.blob.analyzed? + assert_equal({ width: 1200, height: 630 }, + article.thumbnail.blob.metadata.slice(:width, :height).symbolize_keys) + end + + test '.just_fit_to_size?' do + article = articles(:article4) + + image_resizer = ImageResizer.new(article.thumbnail) + width, height = ImageResizer::OGP_SIZE + image_resizer.fit_to!(width, height) + + assert image_resizer.just_fit_to_size?(width, height) + assert_not image_resizer.just_fit_to_size?(width + 1, height) + assert_not image_resizer.just_fit_to_size?(width, height + 1) + end +end diff --git a/test/models/image_resizer_transformation_test.rb b/test/models/image_resizer_transformation_test.rb new file mode 100644 index 00000000000..dacecbe313c --- /dev/null +++ b/test/models/image_resizer_transformation_test.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ImageResizerTransformationTest < ActiveSupport::TestCase + setup do + @image_resizer_transformation = ImageResizerTransformation.new + @width = 1200 + @height = 630 + end + + test '.fit_to' do + assert_equal({ resize: "#{@width}x", gravity: :center, crop: "#{@width}x#{@height}+0+0!" }, + @image_resizer_transformation.fit_to(500, 500, @width, @height)) + end + + test '.resize return 1200x when width magnification is grater than height magnification' do + assert_equal({ resize: "#{@width}x" }, @image_resizer_transformation.resize(500, 500, @width, @height)) + assert_equal({ resize: "#{@width}x" }, @image_resizer_transformation.resize(500, 630, @width, @height)) + assert_equal({ resize: "#{@width}x" }, @image_resizer_transformation.resize(1500, 1500, @width, @height)) + end + + test '.resize return x630 when height magnification is grater than width magnification' do + assert_equal({ resize: "x#{@height}" }, @image_resizer_transformation.resize(1000, 500, @width, @height)) + assert_equal({ resize: "x#{@height}" }, @image_resizer_transformation.resize(1200, 500, @width, @height)) + assert_equal({ resize: "x#{@height}" }, @image_resizer_transformation.resize(1500, 700, @width, @height)) + end + + test '.center_crop' do + assert_equal({ gravity: :center, crop: "#{@width}x#{@height}+0+0!" }, + @image_resizer_transformation.center_crop(@width, @height)) + end +end diff --git a/test/models/ogp/image_processor_test.rb b/test/models/ogp/image_processor_test.rb deleted file mode 100644 index e2ff7673ecf..00000000000 --- a/test/models/ogp/image_processor_test.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -module Ogp - class ImageProcessorTest < ActiveSupport::TestCase - test '.fit_to_size' do - article = articles(:article4) - - ImageProcessor.fit_to_size(article.thumbnail) - - article.thumbnail.blob.analyze unless article.thumbnail.blob.analyzed? - assert_equal({ width: 1200, height: 630 }, - article.thumbnail.blob.metadata.slice(:width, :height).symbolize_keys) - end - - test '.just_fit_to_size?' do - assert ImageProcessor.just_fit_to_size?(width: 1200, height: 630) - assert_not ImageProcessor.just_fit_to_size?(width: 1201, height: 630) - assert_not ImageProcessor.just_fit_to_size?(width: 1200, height: 631) - end - - test '.resize_option return 1200x when width magnification is grater than height magnification' do - assert_equal({ resize: '1200x' }, ImageProcessor.resize_option(width: 500, height: 500)) - assert_equal({ resize: '1200x' }, ImageProcessor.resize_option(width: 500, height: 630)) - assert_equal({ resize: '1200x' }, ImageProcessor.resize_option(width: 1500, height: 1500)) - end - - test '.resize_option return x630 when height magnification is grater than width magnification' do - assert_equal({ resize: 'x630' }, ImageProcessor.resize_option(width: 1000, height: 500)) - assert_equal({ resize: 'x630' }, ImageProcessor.resize_option(width: 1200, height: 500)) - assert_equal({ resize: 'x630' }, ImageProcessor.resize_option(width: 1500, height: 700)) - end - end -end