require "FlickrCollage/version" require "methadone" require "flickraw" require "mini_magick" require "open-uri" require "fileutils" # Creating collage from top-rated Flickr images for your keywords # @author Chromium module FlickrCollage # Class for Dictionary. Provides random words IMG_COUNT = 10 IMG_PATH = "./" COLLAGE_FILE = "flickrCollage" RAISE_ERROR = true class Dictionary DICT_FILE = "/usr/share/dict/words" # @return [Array] All words at dictionary file attr_accessor :words def initialize(opts = {}) opts[:file] ||= DICT_FILE @words = File.read(opts[:file]).lines.select do |line| (4..9).cover?(line.strip.size) end end # Get random word from Dictionary file # # @return [String] random word def get_word @words.sample.strip end end # Class for Flickr. Get and download top-rated images for keywords class Flickr include Methadone::CLILogging API_KEY = "27ad23a09746546c02c79f39879ee408" SHARED_SECRET = "707eaf92eece0744" SEARCH_KEYWORD = "Giant Robots Smashing into Other Giant Robots" SEARCH_COUNT = 1 # @return [String] Path for saving images attr_accessor :img_path def initialize(opts = {}) opts[:api_key] ||= API_KEY opts[:shared_secret] ||= SHARED_SECRET opts[:img_path] ||= IMG_PATH FlickRaw.api_key = opts[:api_key] FlickRaw.shared_secret = opts[:shared_secret] @img_path = opts[:img_path] end # Search top-rated images for keyword # # @param [Hash] opts # @option opts [String] :keyword ('Giant Robots Smashing into Other Giant Robots') keyword for search # @option opts [Number] :count (1) how many images you need # # @return [Array] urls with top-rated images for keyword # @return [String] url with top-rated image for keyword def search(opts = {}) opts[:keyword] ||= SEARCH_KEYWORD opts[:count] ||= SEARCH_COUNT photos = [] begin list = flickr.photos.search(text: opts[:keyword], sort: "interestingness-desc", per_page: opts[:count]) debug "flickr.search: #{opts[:keyword]} (img: #{list.size})" list.each do |photo| info = flickr.photos.getInfo(photo_id: photo.id) debug "flickr.search: get image url for #{photo.id}" # debug "flickr.search: #{FlickRaw.url_photopage(info)}" photos << FlickRaw.url_c(info) end rescue StandardError => e warn "flickr.search: caught exception #{e}" raise e if RAISE_ERROR end photos.size > 1 ? photos : photos.first end # Download images from urls # # @param [Array] opts # @option opts [String] :keyword keyword for file name # @option opts [String] :url image url # # @return [Array] files paths for downloaded images # @return [String] file path for downloaded image def download(opts = {}) opts ||= [] photos = [] return photos unless opts.any? opts = [keyword: opts[:keyword], url: opts[:url]] unless opts.is_a?(Array) FileUtils.mkdir_p("#{@img_path}tmp") begin opts.each do |img| debug "flickr.download: #{img[:keyword]} #{img[:url]}" next unless defined?(img[:url]) && !img[:url].nil? open(img[:url]) do |url| File.open("#{@img_path}tmp/#{img[:keyword]}.jpg", "wb") do |file| file.puts url.read end file = "#{@img_path}tmp/#{img[:keyword]}.jpg" debug "flickr.download: file #{file}" photos << file if Image.valid?(file) end end rescue StandardError => e warn "flickr.download: caught exception: #{e.inspect}" raise e if RAISE_ERROR end photos.size > 1 ? photos : photos.first end # Scrape top-rated images for keyword # # @param [Hash] opts # @option opts [String] :keyword ('Giant Robots Smashing into Other Giant Robots') keyword for scrape # # @return [String] file path for downloaded image def scrape(opts = {}) opts[:keyword] ||= SEARCH_KEYWORD url = search(keyword: opts[:keyword]) debug "flickr.scrape: #{opts[:keyword]} #{url}" download(keyword: opts[:keyword], url: url) end end # Class for Image. Create collage and crop images # @attr_reader [String] img_path Path for saving images # @attr_reader [Number] img_width Width for images crop # @attr_reader [Number] img_height Height for images crop # @attr_reader [String] collage_file Collage file name # @attr_reader [Boolean] clear_tmp Delete all downloaded images class Image include Methadone::CLILogging IMG_WIDTH = 800 IMG_HEIGHT = 600 attr_accessor :img_path, :img_width, :img_height, :collage_file, :clear_tmp def initialize(opts = {}) opts[:img_path] ||= IMG_PATH opts[:img_width] ||= IMG_WIDTH opts[:img_height] ||= IMG_HEIGHT opts[:collage_file] ||= COLLAGE_FILE @img_path = opts[:img_path] @img_width = opts[:img_width] @img_height = opts[:img_height] @collage_file = opts[:collage_file] @clear_tmp = opts[:clear_tmp] end # Validates downloaded image # # @param [String] file file path for downloaded image # # @return [true] image is valid # @return [false] image is not valid def self.valid?(file) return false if file.nil? begin image = MiniMagick::Image.open(file) rescue StandardError => e warn "Image.valid: caught exception #{e}" raise e if RAISE_ERROR end image.valid? end # Resize image # # @param [Hash] opts # @option opts [String] :file file path for image # @option opts [Number] :width (@img_width) image new width # @option opts [Number] :height (@img_height) image new height # # @return [String] file path for resized image def resize(opts = {}) return false if opts[:file].nil? opts[:width] ||= @img_width opts[:height] ||= @img_height begin image = MiniMagick::Image.open(opts[:file]) resize_to_fill(opts[:width], opts[:height], image) file_resize = "#{@img_path}tmp/#{File.basename(opts[:file], ".jpg")}_resize.jpg" image.write file_resize debug "Resize #{opts[:file]} to #{opts[:width]}x#{opts[:height]}" rescue StandardError => e warn "Image.resize: caught exception #{e}" raise e if RAISE_ERROR end file_resize end # Creates collage from 10 images # # @param [Hash] opts # @option opts [Array] :files file paths for collage images # # @return [true] collage created successfully # @return [false] something went wrong def collage(opts = {}) unless opts[:files].size == IMG_COUNT info "Not enough images for collage" return false end begin (1..4).each do |row| montage = MiniMagick::Tool::Montage.new files = montage_resize(files: opts[:files], row: row) files.each do |file| montage << file end montage << "-mode" montage << "Concatenate" montage << "-background" montage << "none" montage << "-geometry" montage << montage_geometry(row: row) montage << "-tile" montage << montage_tile(row: row) montage << montage_img_path(row: row) montage.call end rescue StandardError => e warn "Image.collage: caught exception #{e}" raise e if RAISE_ERROR end FileUtils.rm_rf("#{@img_path}tmp") if @clear_tmp true end # Smart image crop # # @param [Number] width image new width # @param [Number] height image new height # @param [MiniMagick::Image] img image # @param [String] gravity crop around # # @return [MiniMagick::Image] cropped image def resize_to_fill(width, height, img, gravity = "Center") cols, rows = img[:dimensions] img.combine_options do |cmd| if width != cols || height != rows scale_x = width / cols.to_f scale_y = height / rows.to_f if scale_x >= scale_y cols = (scale_x * (cols + 0.5)).round rows = (scale_x * (rows + 0.5)).round cmd.resize cols.to_s else cols = (scale_y * (cols + 0.5)).round rows = (scale_y * (rows + 0.5)).round cmd.resize "x#{rows}" end end cmd.gravity gravity cmd.background "rgba(255,255,255,0.0)" cmd.extent "#{width}x#{height}" if cols != width || rows != height end end # Creates simple collage 5x2 from 10 images # # @param [Hash] opts # @option opts [Array] :files file paths for collage images # # @return [true] collage created successfully # @return [false] something went wrong # @deprecated First collage implementation def collage_simple(opts = {}) unless opts[:files].size == IMG_COUNT info "No images for collage" return false end begin montage = MiniMagick::Tool::Montage.new opts[:files].each do |file| montage << resize(file: file) end montage << "-mode" montage << "Concatenate" montage << "-background" montage << "none" montage << "-geometry" montage << "#{@img_width}x#{@img_height}+0+0" montage << "-tile" montage << "5x2" montage << "#{@img_path}#{@collage_file}.jpg" puts montage.inspect.to_s montage.call rescue StandardError => e warn "Image.collage: caught exception #{e}" raise e if RAISE_ERROR end true end private # Montage Collage - resizing images for rows # 1 row - 3 images # 2 row - 4 images # 3 row - 3 images # # @param [Hash] opts # @option opts [Number] :row collage row 1..4 (4 row - final collage from first 3 rows) # # @return [Array] Array of file paths for resized images def montage_resize(opts = {}) files = [] case opts[:row] when 1 (0..2).each do |i| files << resize(file: opts[:files][i]) end when 2 (3..6).each do |i| files << resize(file: opts[:files][i], width: @img_width * 0.75) end when 3 (7..9).each do |i| files << resize(file: opts[:files][i]) end when 4 (1..3).each do |i| files << "#{@img_path}tmp/#{@collage_file}-#{i}.jpg" end end files end # Montage Collage - setting images geometry for rows # 1 row - 3 images # 2 row - 4 images # 3 row - 3 images # # @param [Hash] opts # @option opts [Number] :row collage row 1..4 (4 row - final collage from first 3 rows) # # @return [String] images geometry def montage_geometry(opts = {}) case opts[:row] when 1, 3 then "#{@img_width}x#{@img_height}+0+0" when 2 then "#{@img_width * 0.75}x#{@img_height}+0+0" when 4 then "#{@img_width * 3}x#{@img_height}+0+0" end end # Montage Collage - setting images tile for rows # 1 row - 3 images # 2 row - 4 images # 3 row - 3 images # # @param [Hash] opts # @option opts [Number] :row collage row 1..4 (4 row - final collage from first 3 rows) # # @return [String] images tile def montage_tile(opts = {}) case opts[:row] when 1, 3 then "3x1" when 2 then "4x1" when 4 then "1x3" end end # Montage Collage - setting images paths for rows # 1 row - 3 images # 2 row - 4 images # 3 row - 3 images # # @param [Hash] opts # @option opts [Number] :row collage row 1..4 (4 row - final collage from first 3 rows) # # @return [String] images path def montage_img_path(opts = {}) case opts[:row] when 1..3 then "#{@img_path}tmp/#{@collage_file}-#{opts[:row]}.jpg" when 4 then "#{@img_path}#{@collage_file}.jpg" else "" end end end end