2013-06-17 02:46:42 +02:00
require " digest/sha1 "
2015-06-12 12:02:36 +02:00
require_dependency " file_helper "
require_dependency " url_helper "
require_dependency " db_helper "
require_dependency " file_store/local_store "
2013-06-17 02:46:42 +02:00
2013-06-16 10:39:48 +02:00
class OptimizedImage < ActiveRecord :: Base
belongs_to :upload
2015-05-28 01:03:24 +02:00
# BUMP UP if optimized image algorithm changes
VERSION = 1
2014-05-22 17:34:33 +10:00
def self . create_for ( upload , width , height , opts = { } )
2013-11-05 19:04:47 +01:00
return unless width > 0 && height > 0
2015-06-10 18:15:10 +02:00
return if upload . try ( :sha1 ) . blank?
2013-07-08 01:39:08 +02:00
2015-05-12 16:45:33 +02:00
DistributedMutex . synchronize ( " optimized_image_ #{ upload . id } _ #{ width } _ #{ height } " ) do
# do we already have that thumbnail?
thumbnail = find_by ( upload_id : upload . id , width : width , height : height )
2013-06-17 02:46:42 +02:00
2015-06-01 17:49:58 +02:00
# make sure we have an url
2015-05-12 16:45:33 +02:00
if thumbnail && thumbnail . url . blank?
thumbnail . destroy
thumbnail = nil
end
2013-11-05 19:04:47 +01:00
2015-05-12 16:45:33 +02:00
# return the previous thumbnail if any
return thumbnail unless thumbnail . nil?
2013-06-17 02:46:42 +02:00
2015-05-12 16:45:33 +02:00
# create the thumbnail otherwise
2015-06-01 11:17:42 +10:00
original_path = Discourse . store . path_for ( upload )
if original_path . blank?
2015-06-10 18:18:20 +02:00
external_copy = Discourse . store . download ( upload ) rescue nil
2015-06-01 11:17:42 +10:00
original_path = external_copy . try ( :path )
2015-02-03 18:44:18 +01:00
end
2014-11-03 19:54:10 +01:00
2015-05-12 16:45:33 +02:00
if original_path . blank?
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 = File . extname ( original_path )
temp_file = Tempfile . new ( [ " discourse-thumbnail " , extension ] )
temp_path = temp_file . path
if extension =~ / \ .svg$ /i
FileUtils . cp ( original_path , temp_path )
resized = true
2013-11-05 19:04:47 +01:00
else
2015-05-12 16:45:33 +02:00
resized = resize ( original_path , temp_path , width , height , opts )
2013-11-05 19:04:47 +01:00
end
2015-05-12 16:45:33 +02:00
if resized
thumbnail = OptimizedImage . create! (
upload_id : upload . id ,
sha1 : Digest :: SHA1 . file ( temp_path ) . hexdigest ,
extension : extension ,
width : width ,
height : height ,
url : " " ,
)
# store the optimized image and update its url
2015-05-29 13:02:05 +02:00
File . open ( temp_path ) do | file |
url = Discourse . store . store_optimized_image ( file , thumbnail )
if url . present?
thumbnail . url = url
thumbnail . save
else
Rails . logger . error ( " Failed to store optimized image #{ width } x #{ height } for #{ upload . url } " )
end
2015-05-12 16:45:33 +02:00
end
else
Rails . logger . error ( " Failed to create optimized image #{ width } x #{ height } for #{ upload . url } " )
end
# close && remove temp file
temp_file . close!
2013-11-05 19:04:47 +01:00
end
2015-05-12 16:45:33 +02:00
# make sure we remove the cached copy from external stores
if Discourse . store . external?
external_copy . try ( :close! ) rescue nil
end
2013-06-17 02:46:42 +02:00
2015-05-12 16:45:33 +02:00
thumbnail
2015-02-09 17:00:58 +01:00
end
2013-06-17 01:00:25 +02:00
end
2013-06-21 09:34:02 +02:00
def destroy
OptimizedImage . transaction do
2013-08-13 22:09:27 +02:00
Discourse . store . remove_optimized_image ( self )
2013-06-21 09:34:02 +02:00
super
end
end
2015-05-26 12:32:52 +10:00
def local?
! ( url =~ / ^(https?:)? \/ \/ / )
end
2015-02-25 15:08:33 +01:00
def self . resize_instructions ( from , to , dimensions , opts = { } )
2014-06-11 16:01:01 +02:00
# NOTE: ORDER is important!
2015-02-25 15:08:33 +01:00
%W{
#{from}[0]
- gravity center
- background transparent
- thumbnail #{dimensions}^
- extent #{dimensions}
- interpolate bicubic
- unsharp 2 x0 . 5 + 0 . 7 + 0
- quality 98
#{to}
}
2015-02-20 17:24:37 +01:00
end
2014-05-22 17:34:33 +10:00
2015-02-25 15:08:33 +01:00
def self . resize_instructions_animated ( from , to , dimensions , opts = { } )
%W{
#{from}
- coalesce
- gravity center
- thumbnail #{dimensions}^
- extent #{dimensions}
#{to}
}
end
def self . downsize_instructions ( from , to , dimensions , opts = { } )
%W{
#{from}[0]
- gravity center
- background transparent
2015-03-26 18:16:15 +01:00
- resize #{dimensions}#{!!opts[:force_aspect_ratio] ? "\\!" : "\\>"}
2015-02-25 15:08:33 +01:00
#{to}
}
end
def self . downsize_instructions_animated ( from , to , dimensions , opts = { } )
%W{
#{from}
- coalesce
- gravity center
- background transparent
2015-03-26 18:16:15 +01:00
- resize #{dimensions}#{!!opts[:force_aspect_ratio] ? "\\!" : "\\>"}
2015-02-25 15:08:33 +01:00
#{to}
}
2014-05-22 17:34:33 +10:00
end
2015-02-21 18:37:37 +01:00
def self . resize ( from , to , width , height , opts = { } )
2015-02-25 15:08:33 +01:00
optimize ( " resize " , from , to , width , height , opts )
2015-02-20 17:24:37 +01:00
end
2015-02-21 18:37:37 +01:00
def self . downsize ( from , to , max_width , max_height , opts = { } )
2015-02-25 15:08:33 +01:00
optimize ( " downsize " , from , to , max_width , max_height , opts )
end
def self . optimize ( operation , from , to , width , height , opts = { } )
dim = dimensions ( width , height )
method_name = " #{ operation } _instructions "
method_name += " _animated " if ! ! opts [ :allow_animation ] && from =~ / \ .GIF$ /i
instructions = self . send ( method_name . to_sym , from , to , dim , opts )
2015-05-29 10:58:27 +02:00
convert_with ( instructions , to )
2015-02-25 15:08:33 +01:00
end
def self . dimensions ( width , height )
" #{ width } x #{ height } "
2015-02-20 17:24:37 +01:00
end
2015-05-29 10:58:27 +02:00
def self . convert_with ( instructions , to )
2015-06-04 19:16:52 +02:00
` convert #{ instructions . join ( " " ) } &> /dev/null `
2015-02-20 17:24:37 +01:00
return false if $? . exitstatus != 0
2015-05-29 13:02:05 +02:00
ImageOptim . new . optimize_image! ( to )
2015-02-20 17:24:37 +01:00
true
2015-05-29 13:02:05 +02:00
rescue
Rails . logger . error ( " Could not optimize image: #{ to } " )
false
2015-02-20 17:24:37 +01:00
end
2015-06-12 12:02:36 +02:00
def self . migrate_to_new_scheme ( limit = 50 )
problems = [ ]
if SiteSetting . migrate_to_new_scheme
max_file_size_kb = SiteSetting . max_image_size_kb . kilobytes
local_store = FileStore :: LocalStore . new
OptimizedImage . includes ( :upload )
. where ( " url NOT LIKE '%/optimized/_X/%' " )
. limit ( limit )
. order ( id : :desc )
. each do | optimized_image |
begin
# keep track of the url
previous_url = optimized_image . url . dup
# where is the file currently stored?
external = previous_url =~ / ^ \/ \/ /
# download if external
if external
url = SiteSetting . scheme + " : " + previous_url
file = FileHelper . download ( url , max_file_size_kb , " discourse " , true ) rescue nil
path = file . path
else
path = local_store . path_for ( optimized_image )
file = File . open ( path )
end
# compute SHA if missing
if optimized_image . sha1 . blank?
optimized_image . sha1 = Digest :: SHA1 . file ( path ) . hexdigest
end
# optimize if image
ImageOptim . new . optimize_image! ( path )
# store to new location & update the filesize
File . open ( path ) do | f |
optimized_image . url = Discourse . store . store_optimized_image ( f , optimized_image )
optimized_image . save
end
# remap the URLs
DbHelper . remap ( UrlHelper . absolute ( previous_url ) , optimized_image . url ) unless external
DbHelper . remap ( previous_url , optimized_image . url )
# remove the old file (when local)
unless external
FileUtils . rm ( path , force : true ) rescue nil
end
rescue = > e
problems << { optimized_image : optimized_image , ex : e }
2015-06-15 18:30:11 +02:00
# just ditch the optimized image if there was any errors
optimized_image . destroy
2015-06-12 12:02:36 +02:00
ensure
file . try ( :unlink ) rescue nil
file . try ( :close ) rescue nil
end
end
end
problems
end
2013-06-16 10:39:48 +02:00
end
2013-06-17 02:48:58 +02:00
# == Schema Information
#
# Table name: optimized_images
#
# id :integer not null, primary key
2013-06-17 04:02:17 +02:00
# sha1 :string(40) not null
# extension :string(10) not null
2013-06-17 02:48:58 +02:00
# width :integer not null
# height :integer not null
# upload_id :integer not null
2013-08-13 22:09:27 +02:00
# url :string(255) not null
2013-06-17 02:48:58 +02:00
#
# Indexes
#
# index_optimized_images_on_upload_id (upload_id)
# index_optimized_images_on_upload_id_and_width_and_height (upload_id,width,height) UNIQUE
#