# encoding: UTF-8
require 'tempfile'
require 'mini_magick'
require 'delegate'
module Spontaneous
module FieldTypes
class ImageOptimizer
def self.run(source_image)
self.new(source_image).run
end
def self.jpegtran_binary
@jpegtran ||= find_binary("jpegtran")
end
def self.jpegoptim_binary
@jpegoptim ||= find_binary("jpegoptim")
end
def self.find_binary(name)
binary = `which #{name}`.chomp
return nil if binary.length == 0
binary
end
# def self.find_binary(name)
# binaries = ["/usr/bin/env #{name}"]
# puts "testing name #{name}"
# binaries.detect { |bin| status = Spontaneous.system("#{bin} -h"); p status; status == 1 }.tap do |b|
# puts "found #{b.inspect}"
# end
# end
def initialize(source_image)
@source_image = source_image
end
def run
jpegoptim!(@source_image)
jpegtran!(@source_image)
end
def jpegoptim!(input)
run_optimization(self.class.jpegoptim_binary, "-o -q --strip-all --preserve --force #{input} 2>&1 1>/dev/null")
end
def jpegtran!(input)
run_optimization(self.class.jpegtran_binary, "-optimize -progressive -copy none -outfile #{input} #{input}")
end
def run_optimization(binary, args)
return unless binary
command = [binary, args].join(" ")
Spontaneous.system(command)
end
end
module ImageFieldUtilities
attr_accessor :template_params
def render(format=:html, *args)
case format
when :html
to_html(*args)
else
value
end
end
def to_html(attr={})
default_attr = {
:src => src,
:width => width,
:height => height,
:alt => ""
}
default_attr.delete(:width) if width.nil?
default_attr.delete(:height) if height.nil?
if template_params && template_params.length > 0 && template_params[0].is_a?(Hash)
attr = template_params[0].merge(attr)
end
if attr.key?(:width) || attr.key?(:height)
default_attr.delete(:width)
default_attr.delete(:height)
if (attr.key?(:width) && !attr[:width]) || (attr.key?(:height) && !attr[:height])
attr.delete(:width)
attr.delete(:height)
end
end
attr = default_attr.merge(attr)
params = []
attr.each do |name, value|
params << %(#{name}="#{value.to_s.escape_html}")
end
%()
end
def to_s
src
end
def /(value)
return value if self.blank?
self
end
end
class ImageField < Field
include Spontaneous::Plugins::Field::EditorClass
include ImageFieldUtilities
def self.accepts
%w{image/(png|jpeg|gif)}
end
def self.size(name, &process)
self.sizes[name.to_sym] = process
unless method_defined?(name)
class_eval <<-IMAGE
def #{name}
sizes[:#{name}]
end
IMAGE
end
end
def self.sizes
size_definitions
end
def self.validate_sizes(sizes)
sizes
end
def self.size_definitions
@size_definitions ||= superclass.respond_to?(:size_definitions) ? superclass.size_definitions.dup : {}
end
def image?
true
end
def sizes
@sizes ||= Hash.new { |hash, key| hash[key] = ImageAttributes.new(processed_values[key]) }
end
# value used to show conflicts between the current value and the value they're attempting to enter
def conflicted_value
value
end
# original is special and should always be defined
def original
@original ||= sizes[:original]
end
def width
original.width
end
def height
original.height
end
def filesize
original.filesize
end
def src
original.src
end
def filepath
unprocessed_value
end
# formats are irrelevant to image/file fields
def outputs
[:original].concat(self.class.size_definitions.map { |name, process| name })
end
def value(format=:html, *args)
sizes[:original].src
end
def generate(output, media_file)
return { :src => media_file } if media_file.is_a?(String)#File.exist?(image_path)
image = ImageProcessor.new(media_file)
# Create a tempfile here that will be kept open for the duration of the block
# this is used in #apply to hold a copy of the processed image data rather than
# rely on the minimagick generated tempfiles which can get closed
result = Tempfile.open("image_#{output}") do |tempfile|
case output
when :original
image
else
process = self.class.size_definitions[output]
image.apply(process, output, tempfile)
end
end
result.serialize
end
def preprocess(image_path)
filename = mimetype = nil
case image_path
when Hash
mimetype = image_path[:type]
filename = image_path[:filename]
image_path = image_path[:tempfile].path
when String
# return image_path unless File.exist?(image_path)
filename = ::File.basename(image_path)
end
return image_path unless File.exist?(image_path)
# media_path = owner.make_media_file(image_path, filename)
media_file = Spontaneous::Media::File.new(owner, filename, mimetype)
media_file.copy(image_path)
set_unprocessed_value(File.expand_path(media_file.filepath))
# media_path
# image_path
media_file
end
def export(user = nil)
super(user).merge({
:processed_value => processed_values
})
end
end
class ImageAttributes
include ImageFieldUtilities
attr_reader :src, :width, :height, :filesize, :filepath
def initialize(params={})
params ||= {}
@src, @width, @height, @filesize, @filepath = params[:src], params[:width], params[:height], params[:filesize], params[:path]
end
def serialize
{
:src => src,
:width => width,
:height => height,
:filesize => filesize,
:path => filepath
}
end
def inspect
%(<#{self.class.name}: src=#{src.inspect} width="#{width}" height="#{height}">)
end
def blank?
src.blank?
end
alias_method :empty?, :blank?
end
class ImageProcessor
include ImageFieldUtilities
class ImageDelegator < SimpleDelegator
def initialize(image)
super(image)
end
alias_method :image, :__getobj__
def format(*args, &block)
image.format(*args, &block)
end
def fit(width, height)
image.combine_options do |c|
c.add_command(:geometry, "#{width}x#{height}>")
end
end
def crop(width, height)
image.combine_options do |c|
dimensions = "#{width}x#{height}"
c.add_command(:geometry, "#{dimensions}^")
c.add_command(:gravity, "center")
c.add_command(:crop, "#{dimensions}+0+0!")
end
end
def width(width)
image.combine_options do |c|
c.add_command(:geometry, "#{width}x>")
end
end
def height(height)
image.combine_options do |c|
c.add_command(:geometry, "x#{height}>")
end
end
def greyscale
image.combine_options do |c|
c.add_command(:type, "Grayscale")
end
end
def composite(other_image_path, output_extension = "jpg", &block)
File.open(other_image_path) do |other_image|
new_image = image.composite(other_image, output_extension, &block)
image.path = new_image.path
end
end
def smush_it!
::Spontaneous::Utils::SmushIt.smush!(image.path, current_image_format)
end
def optimize!
ImageOptimizer.run(image.path) if current_image_format == "jpg"
end
def border_radius(radius, bg_color = nil)
@image.format('png') if bg_color.nil? or bg_color == 'transparent'
puts @image.path
c = MiniMagick::CommandBuilder.new('convert')
c << @image.path
c.add_command(:format, "roundrectangle 0,0 %[fx:w-1],%[fx:h-1], 10,10")
c.add_command(:write, "info:tmp.mvg")
c << @image.path
puts c.command
# @image.run(c)
sub = Subexec.run(c.command, :timeout => MiniMagick.timeout)
c = MiniMagick::CommandBuilder.new('convert')
c << @image.path
# c.add_command(:write, "info:tmp.mvg")
c.add_command(:matte)
c.add_command(:bordercolor, "none")
c.add_command(:border, "0")
c.push('\\(')
c.push("+clone")
c.add_command(:alpha, 'transparent')
c.add_command(:background, 'white')
c.add_command(:fill, 'white')
c.add_command(:stroke, 'none')
c.add_command(:strokewidth, '0')
c.add_command(:draw, "@tmp.mvg")
c.push('\\)')
c.add_command(:compose, 'DstIn')
c.add_command(:composite)
c << @image.path
puts c.command
@image.run(c)
end
def __run__(process)
instance_eval(&process)
end
def method_missing(method, *args, &block)
params = args.map(&:to_s)
if image.respond_to?(method)
image.__send__(method, *params, &block)
else
image.method_missing(method, *params, &block)
end
end
def current_image_format
image[:format].downcase.gsub(/^jpeg$/, "jpg")
end
end
MAX_DIM = 2 ** ([42].pack('i').size * 8) - 1 unless defined?(MAX_DIM)
attr_reader :path
def initialize(media_file)
@media_file = media_file
@path = media_file.source
end
def src
@media_file.url
end
def filesize
File.size(path)
end
def width
dimensions[0]
end
def height
dimensions[1]
end
def dimensions
@dimensions ||= Spontaneous::ImageSize.read(path)
end
def apply(process, name, tempfile)
image = ::MiniMagick::Image.open(path)
processor = ImageProcessor::ImageDelegator.new(image)
processor.__run__(process)
file = @media_file.rename(filename_for_size(name, image))
# copy the image into the tempfile provided by the parent #generate call
# this stops us from using a tempfile that is out of our control and can
# be closed before we're done
FileUtils.cp(image.path, tempfile.path)
file.copy(tempfile)
ImageProcessor.new(file)
end
def filename_for_size(name, image)
original_filename = @media_file.filename
parts = original_filename.split('.')
# use the image format for the extension because tempfiles generated by
# mini_magick don't have extensions
# I hate the "jpeg" extension though
ext = image[:format].downcase.gsub(/^jpeg$/, "jpg")
base = parts[0..-2].join('.')
filename = [base, name, ext].join('.')
filename
end
def serialize
{
:src => src,
:width => width,
:height => height,
:filesize => filesize,
:path => path
}
end
def inspect
%(#)
end
end
ImageField.register(:image, :photo)
end
end