Prefer Minimagick for social image generation (#19826)

* Prefer Minimagick for social image generation

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Reformat main file

* Fix user tests

* Finish up tests

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Change asset path syntax

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Refactor magickally file

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Fix style issues

* Remove pure black

* Test color swap

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Add more test coverage to service

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Fix doubled spec name

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Add upload spec

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/images/generate_social_image_magickally_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Swap out magickally in social image worker

* Add test stub and rescue

* Update spec/rails_helper.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Fix regex

* Add cache busting and error logging

* Update app/services/images/generate_social_image_magickally.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Fix style issues

* Add more test coverage

* Add feature flag

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Ben Halpern 2023-08-11 08:22:06 -04:00 committed by GitHub
parent 991472e4d6
commit c9af090af0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 648 additions and 1 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

View file

@ -205,6 +205,7 @@ class Article < ApplicationRecord
after_save :create_conditional_autovomits
after_save :bust_cache
after_save :collection_cleanup
after_save :generate_social_image
after_update_commit :update_notifications, if: proc { |article|
article.notifications.any? && !article.saved_changes.empty?
@ -984,6 +985,17 @@ class Article < ApplicationRecord
touch_actor_latest_article_updated_at(destroying: destroying)
end
def generate_social_image
return unless FeatureFlag.enabled?(:minimagick_social_images)
return if main_image.present?
change = saved_change_to_attribute?(:title) || saved_change_to_attribute?(:published_at)
return unless (change || social_image.blank?) && published
Images::SocialImageWorker.perform_async(id, self.class.name)
end
def calculate_base_scores
self.hotness_score = 1000 if hotness_score.blank?
end

View file

@ -18,6 +18,7 @@ class Organization < ApplicationRecord
before_save :generate_secret
after_save :bust_cache
after_save :generate_social_images
after_update_commit :conditionally_update_articles
after_destroy_commit :bust_cache
@ -132,6 +133,15 @@ class Organization < ApplicationRecord
private
def generate_social_images
return unless FeatureFlag.enabled?(:minimagick_social_images)
change = saved_change_to_attribute?(:name) || saved_change_to_attribute?(:profile_image)
return unless change && articles.published.size.positive?
Images::SocialImageWorker.perform_async(id, self.class.name)
end
def evaluate_markdown
self.cta_processed_html = MarkdownProcessor::Parser.new(cta_body_markdown).evaluate_limited_markdown
end

View file

@ -233,6 +233,7 @@ class User < ApplicationRecord
after_create_commit :send_welcome_notification
after_save :create_conditional_autovomits
after_save :generate_social_images
after_commit :subscribe_to_mailchimp_newsletter
after_commit :bust_cache
@ -582,6 +583,15 @@ class User < ApplicationRecord
private
def generate_social_images
return unless FeatureFlag.enabled?(:minimagick_social_images)
change = saved_change_to_attribute?(:name) || saved_change_to_attribute?(:profile_image)
return unless change && articles.published.size.positive?
Images::SocialImageWorker.perform_async(id, self.class.name)
end
def create_users_settings_and_notification_settings_records
self.setting = Users::Setting.create
self.notification_setting = Users::NotificationSetting.create

View file

@ -0,0 +1,196 @@
module Images
MEDIUM_FONT_PATH = Rails.root.join("app/assets/fonts/Roboto-Medium.ttf").freeze
BOLD_FONT_PATH = Rails.root.join("app/assets/fonts/Roboto-Bold.ttf").freeze
TEMPLATE_PATH = Rails.root.join("app/assets/images/social_template.png").freeze
ROUNDED_MASK_PATH = Rails.root.join("app/assets/images/rounded_mask.png").freeze
class GenerateSocialImageMagickally
def self.call(resource = nil)
new(resource).call
end
def initialize(resource)
@resource = resource
@logo_url = Settings::General.logo_png
end
def call
if @resource.is_a?(Article)
@user = @resource.user
read_files
url = generate_magickally(title: @resource.title,
date: @resource.readable_publish_date,
author_name: @user.name,
color: @user.setting.brand_color1)
@resource.update_column(:social_image, url)
## We only need to bust article. All else can fade naturally
EdgeCache::BustArticle.call(@resource)
elsif @resource.is_a?(User)
@user = @resource
read_files
@resource.articles.published.where(organization_id: nil, main_image: nil).find_each do |article|
url = generate_magickally(title: article.title,
date: article.readable_publish_date,
author_name: @user.name,
color: @user.setting.brand_color1)
article.update_column(:social_image, url)
end
else # Organization
@user = @resource
read_files
@resource.articles.published.where(main_image: nil).find_each do |article|
url = generate_magickally(title: article.title,
date: article.readable_publish_date,
author_name: @user.name,
color: @user.bg_color_hex)
article.update_column(:social_image, url)
end
end
rescue StandardError => e
Rails.logger.error(e)
Honeybadger.notify(e)
end
private
def generate_magickally(title: nil, date: nil, author_name: nil, color: nil)
result = draw_stripe(color)
result = add_logo(result)
result = add_text(result, title, date, author_name)
result = add_profile_image(result)
upload_result(result)
end
def draw_stripe(color)
color = "#111212" if color == "#000000" # pure black has minimagick side effects
@background_image.combine_options do |c|
c.fill color
c.draw "rectangle 0,0 1000,24" # adjust width according to your image width
end
end
def add_logo(result)
if @logo_image
# Add white stroke to the overlay image
@logo_image.combine_options do |c|
c.stroke "white"
c.strokewidth "4"
c.fill "none"
c.draw "rectangle 0,0 1000,1000" # adjust as needed based on image size
end
# Resize the overlay image
@logo_image.resize "64x64"
result = @background_image.composite(@logo_image) do |c|
c.compose "Over" # OverCompositeOp
c.geometry "+850+372" # move the overlay to the top left
end
end
result
end
def add_text(result, title, date, author_name)
title = wrap_text(title)
font_size = calculate_font_size(title)
result.combine_options do |c|
c.gravity "West" # Set the origin for the text at the top left corner
c.pointsize font_size.to_s
c.draw "text 80,-39 '#{title}'" # Start drawing text 90 from the left and slightly north
c.fill "black"
c.font BOLD_FONT_PATH.to_s
end
result.combine_options do |c|
c.gravity "Southwest"
c.pointsize "32"
c.draw "text 156,88 '#{author_name}'" # adjust coordinates as needed
c.fill "black"
c.font MEDIUM_FONT_PATH.to_s
end
result.combine_options do |c|
c.gravity "Southwest"
c.pointsize "26"
c.draw "text 156,60 '#{date}'" # adjust coordinates as needed
c.fill "#525252"
end
end
def add_profile_image(result)
profile_image_size = "64x64"
profile_image_location = "+80+63"
# Add subtext and author image
@author_image.resize profile_image_size
result = result.composite(@author_image) do |c|
c.compose "Over"
c.gravity "Southwest"
c.geometry profile_image_location
end
@rounded_mask.resize profile_image_size
result.composite(@rounded_mask) do |c|
c.compose "Over"
c.gravity "Southwest"
c.geometry profile_image_location
end
end
def upload_result(result)
tempfile = Tempfile.new(["output", ".png"])
result.write tempfile.path
image_uploader = ArticleImageUploader.new.tap do |uploader|
uploader.store!(tempfile)
end
# Don't forget to close and unlink (delete) the tempfile after you're done with it.
tempfile.close
tempfile.unlink
# Return the uploaded url
image_uploader.url
end
attr_reader :resource
def calculate_font_size(text)
text_length = text.length
if text_length < 18
88
elsif text_length < 40
77
elsif text_length < 55
65
elsif text_length < 70
60
else
50
end
end
def wrap_text(text)
line_width = if text.length < 40
20
elsif text.length < 70
27
else
35
end
text.split("\n").map do |line|
line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
end * "\n"
end
def read_files
# These are files we can open once for all the images we are generating within the loop.
@background_image = MiniMagick::Image.open(ActionController::Base.helpers.asset_path(TEMPLATE_PATH))
@logo_image = MiniMagick::Image.open(@logo_url) if @logo_url.present?
image = @user.profile_image_90
author_image_url = image.start_with?("http") ? image : Images::Profile::BACKUP_LINK
@author_image = MiniMagick::Image.open(author_image_url)
@rounded_mask = MiniMagick::Image.open(ActionController::Base.helpers.asset_path(ROUNDED_MASK_PATH))
end
end
end

View file

@ -41,8 +41,8 @@ module Articles
end
def user_defined_image
return article.social_image if article.social_image.present?
return article.main_image if article.main_image.present?
return article.social_image if article.social_image.present?
return article.video_thumbnail_url if article.video_thumbnail_url.present?
end
end

View file

@ -0,0 +1,12 @@
module Images
class SocialImageWorker
include Sidekiq::Job
sidekiq_options queue: :medium_priority, retry: 5
def perform(id, class_name)
object = class_name.constantize.find(id)
Images::GenerateSocialImageMagickally.call(object)
end
end
end

View file

@ -0,0 +1,7 @@
module DataUpdateScripts
class AddMinimagickSocialImagesFeatureFlag
def run
FeatureFlag.add :minimagick_social_images
end
end
end

View file

@ -1443,4 +1443,42 @@ RSpec.describe Article do
expect(Languages::Detection).not_to have_received(:call)
end
end
describe "#generate_social_image" do
before do
allow(Images::SocialImageWorker).to receive(:perform_async)
end
context "when title or published_at attribute changes and published is true" do
it "triggers the Images::SocialImageWorker" do
article.body_markdown = "---\ntitle: New Title #{rand(1_000)}\npublished: true\n---\n\n# Hello World"
article.main_image = nil
article.save
expect(Images::SocialImageWorker).to have_received(:perform_async)
end
end
context "when attributes have changed, but main image is present" do
it "does not trigger the Images::SocialImageWorker" do
article.body_markdown = "---\ntitle: New Title #{rand(1_000)}\ncover_image: https://example.com/i.jpg\npublished: true\n---\n\n# Hello World"
article.save
expect(Images::SocialImageWorker).not_to have_received(:perform_async)
end
end
context "when neither title nor published_at attribute changes" do
it "does not trigger the Images::SocialImageWorker" do
article.save
expect(Images::SocialImageWorker).not_to have_received(:perform_async)
end
end
context "when title or published_at attribute changes but published is false" do
it "does not trigger the Images::SocialImageWorker" do
article.body_markdown = "---\ntitle: New Title #{rand(1_000)}\npublished: false\n---\n\n# Hello World"
article.save
expect(Images::SocialImageWorker).not_to have_received(:perform_async)
end
end
end
end

View file

@ -391,6 +391,37 @@ RSpec.describe Organization do
end
end
describe "#generate_social_images" do
before do
allow(Images::SocialImageWorker).to receive(:perform_async)
create(:article, organization: organization)
end
context "when the name or profile_image has changed and the organization has articles" do
it "calls SocialImageWorker.perform_async" do
organization.name = "New name for this org!"
organization.save
expect(Images::SocialImageWorker).to have_received(:perform_async)
end
end
context "when the name or profile_image has not changed or the organization has no articles" do
it "does not call SocialImageWorker.perform_async" do
expect(Images::SocialImageWorker).not_to receive(:perform_async)
organization.save
end
end
context "when the name or profile_image has changed and the organization has no articles" do
it "does not call SocialImageWorker.perform_async" do
organization.articles.destroy_all
organization.name = "New name for this org!!"
organization.save
expect(Images::SocialImageWorker).not_to receive(:perform_async)
end
end
end
describe "#bust_cache" do
context "when a new user is added to the organization" do
let(:user) { create(:user) }

View file

@ -926,4 +926,26 @@ RSpec.describe User do
end
end
end
describe "#generate_social_images" do
before do
create(:article, user: user)
allow(Images::SocialImageWorker).to receive(:perform_async)
end
context "when the name or profile_image has changed and the user has articles" do
it "calls SocialImageWorker.perform_async" do
user.name = "New name for this user!"
user.save
expect(Images::SocialImageWorker).to have_received(:perform_async)
end
end
context "when the name or profile_image has not changed or the user has no articles" do
it "does not call SocialImageWorker.perform_async" do
user.save
expect(Images::SocialImageWorker).not_to have_received(:perform_async)
end
end
end
end

View file

@ -146,6 +146,7 @@ RSpec.configure do |config|
# "Please stub a default value first if message might be received with other args as well."
allow(FeatureFlag).to receive(:enabled?).and_call_original
allow(FeatureFlag).to receive(:enabled?).with(:connect).and_return(true)
allow(FeatureFlag).to receive(:enabled?).with(:minimagick_social_images, any_args).and_return(true)
end
config.around(:each, :flaky) do |ex|
@ -217,6 +218,13 @@ RSpec.configure do |config|
"Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
"User-Agent" => "Ruby"
}).to_return(status: 200, body: "", headers: {})
stub_request(:get, /assets\/icon/)
.with(headers:
{
"Accept" => "*/*",
"Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
"User-Agent" => "Ruby"
}).to_return(status: 200, body: "", headers: {})
allow(Settings::Community).to receive(:community_description).and_return("Some description")
allow(Settings::UserExperience).to receive(:public).and_return(true)

View file

@ -215,6 +215,18 @@ RSpec.describe "StoriesShow" do
get article.path
expect(response.body).not_to include("noindex")
end
it "renders og:image with main image if present ahead of social" do
article = create(:article, with_main_image: true, social_image: "https://example.com/image.jpg")
get article.path
expect(response.body).to include(%(property="og:image" content="#{article.main_image}"))
end
it "renders og:image with social if present and main image not so much" do
article = create(:article, with_main_image: false, social_image: "https://example.com/image.jpg")
get article.path
expect(response.body).to include(%(property="og:image" content="#{article.social_image}"))
end
end
describe "GET /:username (org)" do

View file

@ -0,0 +1,289 @@
require "rails_helper"
RSpec.describe Images::GenerateSocialImageMagickally, type: :model do
let(:user) { create(:user) }
let!(:article) { create(:article, user_id: user.id, with_main_image: false) }
let(:organization) { create(:organization) }
let!(:second_article) { create(:article, user_id: user.id, organization_id: organization.id, with_main_image: false) }
let(:background_image) { instance_double(MiniMagick::Image) }
# rubocop:disable Style/Send
# rubocop:disable Layout/LineLength
# rubocop:disable RSpec/VerifiedDoubles
# rubocop:disable RSpec/ContextWording
# rubocop:disable Style/Semicolon
# rubocop:disable RSpec/NestedGroups
describe ".call" do
context "when resource is an Article" do
let(:generator) { described_class.new(article) }
before do
allow(described_class).to receive(:new).and_return(generator)
allow(generator).to receive(:read_files)
allow(generator).to receive(:add_logo)
allow(generator).to receive(:add_text)
allow(generator).to receive(:add_profile_image)
allow(generator).to receive(:upload_result).and_return("image_url")
allow(generator).to receive(:generate_magickally)
end
it "calls the class methods" do
described_class.call(article)
expect(generator).to have_received(:generate_magickally).once
end
it "updates article to have social image" do
allow(generator).to receive(:generate_magickally).and_return("https://www.example.com")
described_class.call(article)
expect(article.reload.social_image).to eq("https://www.example.com")
end
it "converts black color to off-black" do
allow(background_image).to receive(:combine_options).with(any_args) do |&block|
command = MiniMagick::CommandBuilder.new(:mogrify)
block.call(command)
expect(command.args.join(" ")).to include("fill '#111212'")
end
described_class.call(article)
end
it "busts article cache" do
allow(EdgeCache::BustArticle).to receive(:call).with(article)
described_class.call(article)
end
end
context "when resource is a User" do
let(:generator) { described_class.new(user) }
before do
allow(described_class).to receive(:new).and_return(generator)
allow(generator).to receive(:read_files)
allow(generator).to receive(:generate_magickally)
allow(user).to receive(:profile_image_90).and_return("https://www.example.com/image.png")
end
it "calls the class methods for each published article" do
described_class.call(user)
expect(generator).to have_received(:generate_magickally).once
.with(
title: article.title,
date: article.readable_publish_date,
author_name: user.name,
color: user.setting.brand_color1,
)
end
it "updates article to have social image" do
allow(generator).to receive(:generate_magickally).and_return("https://www.example.com")
described_class.call(user)
expect(article.reload.social_image).to eq("https://www.example.com")
end
end
context "when resource is an Organization" do
let(:generator) { described_class.new(organization) }
before do
allow(described_class).to receive(:new).and_return(generator)
allow(generator).to receive(:read_files)
end
it "calls the class methods for each published article" do
allow(generator).to receive(:generate_magickally)
described_class.call(organization)
expect(generator).to have_received(:generate_magickally).once
end
it "updates article to have social image" do
allow(generator).to receive(:generate_magickally).and_return("https://www.example.com")
described_class.call(organization)
expect(generator).to have_received(:generate_magickally).once
expect(second_article.reload.social_image).to eq("https://www.example.com")
end
end
context "when calculate_font_size" do
let(:generator) { described_class.new(article) }
it "returns the correct font size for short text" do
expect(generator.send(:calculate_font_size, "short text")).to eq(88)
end
it "returns the correct font size for medium text" do
medium_text = "This is a slightly longer text."
expect(generator.send(:calculate_font_size, medium_text)).to eq(77)
end
it "returns the correct font size for medium-to-long text" do
medium_long_text = "This is a slightly longer text. Slightly longer."
expect(generator.send(:calculate_font_size, medium_long_text)).to eq(65)
end
it "returns the correct font size for medium-to-long text" do
almost_long_text = "This is a slightly longer text. Slightly longer than the last."
expect(generator.send(:calculate_font_size, almost_long_text)).to eq(60)
end
it "returns the correct font size for long text" do
long_text = "This is a very long text that is definitely more than 70 characters long
and should return a smaller font size"
expect(generator.send(:calculate_font_size, long_text)).to eq(50)
end
end
context "wrap_text" do
let(:generator) { described_class.new(article) }
it "returns the same text when the text length is less than 40" do
text = "This is a short text"
expect(generator.send(:wrap_text, text)).to eq(text)
end
it "returns wrapped text when the text length is between 40 and 70" do
text = "This is a slightly longer text that is about 50 characters long and should be wrapped"
wrapped_text = "This is a slightly longer text that\nis about 50 characters long and\nshould be wrapped"
expect(generator.send(:wrap_text, text)).to eq(wrapped_text)
end
it "returns wrapped text when the text length is more than 70" do
text = "This is a very long text that is definitely more than 70 characters long and should be wrapped over multiple lines"
wrapped_text = "This is a very long text that is\ndefinitely more than 70 characters\nlong and should be wrapped over\nmultiple lines"
expect(generator.send(:wrap_text, text)).to eq(wrapped_text)
end
end
context "add_text" do
let(:generator) { described_class.new(article) }
let(:result_image) { double(MiniMagick::Tool::Convert) }
before do
allow(generator).to receive(:calculate_font_size).and_return(40)
allow(generator).to receive(:wrap_text).and_return("Wrapped Text")
allow(result_image).to receive(:combine_options) { |&block| block.call(result_image); result_image }
allow(result_image).to receive(:gravity)
allow(result_image).to receive(:pointsize)
allow(result_image).to receive(:draw)
allow(result_image).to receive(:fill)
allow(result_image).to receive(:font)
generator.instance_variable_set(:@background_image, result_image)
end
it "adds title, date, and author_name text to the image" do
generator.send(:add_text, result_image, "title", "date", "author_name")
expect(result_image).to have_received(:combine_options).exactly(3).times
end
end
context "add_profile_image" do
let(:user) { create(:user) }
let(:article) { create(:article, user_id: user.id, with_main_image: false) }
let(:generator) { described_class.new(article) }
let(:result_image) { double(MiniMagick::Tool::Convert) }
let(:author_image) { double(MiniMagick::Tool::Convert) }
let(:rounded_mask) { double(MiniMagick::Tool::Convert) }
before do
allow(result_image).to receive(:composite).and_return(result_image)
allow(author_image).to receive(:resize)
allow(rounded_mask).to receive(:resize)
generator.instance_variable_set(:@background_image, result_image)
generator.instance_variable_set(:@author_image, author_image)
generator.instance_variable_set(:@rounded_mask, rounded_mask)
end
it "adds the profile image and rounded mask to the image" do
generator.send(:add_profile_image, result_image)
expect(author_image).to have_received(:resize).with("64x64")
expect(rounded_mask).to have_received(:resize).with("64x64")
expect(result_image).to have_received(:composite).twice
end
end
context "add_logo" do
let(:user) { create(:user) }
let(:article) { create(:article, user_id: user.id, with_main_image: false) }
let(:generator) { described_class.new(article) }
let(:result_image) { double(MiniMagick::Tool::Convert) }
let(:logo_image) { double(MiniMagick::Tool::Convert) }
before do
allow(result_image).to receive(:composite).and_return(result_image)
allow(logo_image).to receive(:combine_options).and_yield(logo_image)
allow(logo_image).to receive(:resize)
allow(logo_image).to receive(:stroke)
allow(logo_image).to receive(:strokewidth)
allow(logo_image).to receive(:fill)
allow(logo_image).to receive(:draw)
generator.instance_variable_set(:@background_image, result_image)
generator.instance_variable_set(:@logo_image, logo_image)
end
context "when logo image is present" do
it "adds the logo to the image" do
generator.send(:add_logo, result_image)
expect(logo_image).to have_received(:combine_options)
expect(logo_image).to have_received(:stroke).with("white")
expect(logo_image).to have_received(:strokewidth).with("4")
expect(logo_image).to have_received(:fill).with("none")
expect(logo_image).to have_received(:draw).with("rectangle 0,0 1000,1000")
expect(logo_image).to have_received(:resize).with("64x64")
expect(result_image).to have_received(:composite).with(logo_image)
end
end
context "when logo image is not present" do
before do
generator.instance_variable_set(:@logo_image, nil)
end
it "does not attempt to add the logo to the image" do
generator.send(:add_logo, result_image)
expect(result_image).not_to have_received(:composite)
end
end
context "upload_result" do
let(:generator) { described_class.new(article) }
let(:result_image) { double(MiniMagick::Tool::Convert) }
let(:tempfile) { instance_double(Tempfile, path: "/tmp/output.png") }
let(:uploader) { instance_double(ArticleImageUploader) }
before do
allow(Tempfile).to receive(:new).and_return(tempfile)
allow(tempfile).to receive(:close)
allow(tempfile).to receive(:unlink)
allow(result_image).to receive(:write)
allow(ArticleImageUploader).to receive(:new).and_return(uploader)
allow(uploader).to receive(:store!)
allow(uploader).to receive(:url).and_return("http://example.com/social_image.png")
generator.instance_variable_set(:@background_image, result_image)
end
it "creates a tempfile, writes the image, uploads the image, then cleans up the tempfile" do
expect(generator.send(:upload_result, result_image)).to eq "http://example.com/social_image.png"
expect(Tempfile).to have_received(:new).with(["output", ".png"])
expect(result_image).to have_received(:write).with(tempfile.path)
expect(ArticleImageUploader).to have_received(:new)
expect(uploader).to have_received(:store!).with(tempfile)
expect(tempfile).to have_received(:close)
expect(tempfile).to have_received(:unlink)
expect(uploader).to have_received(:url)
end
end
end
end
# rubocop:enable Style/Send
# rubocop:enable RSpec/VerifiedDoubles
# rubocop:enable Layout/LineLength
# rubocop:enable RSpec/ContextWording
# rubocop:enable Style/Semicolon
# rubocop:enable RSpec/NestedGroups
end