#!/usr/bin/ruby require 'cli' require 'ip' options = CLI.new do description 'HTTP thumbnailing server' switch :no_bind, :description => "Do not bind to TCP socket - useful with -s fastcgi option" switch :no_logging, :description => "Disable logging" switch :debug, :description => "Enable debugging" switch :no_optimization, :description => "Disable size hinting and related optimization (loading, prescaling)" option :bind, :short => :b, :default => IP.new('127.0.0.1'), :cast => IP, :description => "HTTP server bind address - use 0.0.0.0 to bind to all interfaces" option :port, :short => :p, :default => 3100, :cast => Integer, :description => "HTTP server TCP port" option :server, :short => :s, :default => 'mongrel', :description => "Rack server handler like thin, mongrel, webrick, fastcgi etc." option :limit_memory, :default => 128*1024**2, :cast => Integer, :description => "Image cache heap memory size limit in bytes" option :limit_map, :default => 256*1024**2, :cast => Integer, :description => "Image cache memory mapped file size limit in bytes - used when heap memory limit is used up" option :limit_disk, :default => 0, :cast => Integer, :description => "Image cache temporary file size limit in bytes - used when memory mapped file limit is used up" end.parse! require 'sinatra/base' require 'haml' require 'RMagick' $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'httpthumbnailer/thumbnailer' require 'httpthumbnailer/thumbnail_specs' sinatra = Sinatra.new unless options.no_bind sinatra.set :port, options.port sinatra.set :bind, options.bind.to_s else sinatra.set :port, nil sinatra.set :bind, nil end sinatra.set :environment, 'production' sinatra.set :server, options.server sinatra.set :lock, true sinatra.set :boundary, "thumnail image data" sinatra.set :logging, (not options.no_logging) sinatra.set :debug, options.debug sinatra.set :optimization, (not options.no_optimization) sinatra.set :limit_memory, options.limit_memory sinatra.set :limit_map, options.limit_map sinatra.set :limit_disk, options.limit_disk sinatra.before do logger.level = Logger::DEBUG if settings.logging and settings.debug if $thumbnailer.nil? $thumbnailer = Thumbnailer.new(:logger => logger, :limit_memory => settings.limit_memory, :limit_map => settings.limit_map, :limit_disk => settings.limit_disk) $thumbnailer.method('crop') do |image, spec| image.resize_to_fill!(spec.width, spec.height) end $thumbnailer.method('fit') do |image, spec| image.resize_to_fit!(spec.width, spec.height) end $thumbnailer.method('pad') do |image, spec| image.resize_to_fit!(spec.width, spec.height) out = Magick::Image.new(spec.width, spec.height) { self.background_color = Magick::Pixel.new(Magick::MaxRGB, Magick::MaxRGB, Magick::MaxRGB, Magick::MaxRGB) # transparent }.composite!(image, Magick::CenterGravity, Magick::OverCompositeOp) image.destroy! out end end end sinatra.helpers do def plain_response(msg) headers "Content-Type" => "text/plain" body msg.gsub("\n", "\r\n") + "\r\n" end def plain_exception(exception) plain_response("Error: #{exception.class.name}: #{exception}") end end sinatra.get '/' do logger.info 'hello' end sinatra.get '/stats/images' do $thumbnailer.images.to_s end sinatra.put %r{^/thumbnail/(.*)} do |specs| thumbnail_specs = ThumbnailSpecs.from_uri(specs) opts = {} if settings.optimization opts.merge!({'max-width' => thumbnail_specs.max_width, 'max-height' => thumbnail_specs.max_height}) end input_image_handler = $thumbnailer.load(request.body, opts) input_image_handler.get do |input_image| status 200 headers "Content-Type" => "multipart/mixed; boundary=\"#{settings.boundary}\"" headers "X-Input-Image-Content-Type" => input_image.mime_type stream do |out| # this is non blocking input_image_handler.use do |input_image| thumbnail_specs.each do |spec| logger.info "Thumbnailing: #{spec}" out << "--#{settings.boundary}\r\n" begin input_image.thumbnail(spec) do |thumbnail| out << "Content-Type: #{thumbnail.mime_type}\r\n\r\n" out << thumbnail.data end rescue => e logger.error "Thumbnailing error: #{e.class.name}: #{e}: \n#{e.backtrace.join("\n")}" out << "Content-Type: text/plain\r\n\r\n" out << "Error: #{e.class.name}: #{e}\r\n" ensure out << "\r\n" end end out << "--#{settings.boundary}--" end end end end sinatra.error Thumbnailer::UnsupportedMediaTypeError do plain_exception(env['sinatra.error']) halt 415 end sinatra.error Thumbnailer::ImageTooLargeError do plain_exception(env['sinatra.error']) halt 413 end sinatra.error 404 do plain_response("Resource '#{request.path_info}' not found") end sinatra.error do plain_exception(env['sinatra.error']) end sinatra.run!