require "juicer/chainable" require "juicer/cache_buster" require "juicer/asset/path_resolver" module Juicer # # The ImageEmbed is a tool that can parse a CSS file and substitute all # referenced URLs by a data uri # # - data uri (http://en.wikipedia.org/wiki/Data_URI_scheme) # # Only local resources will be processed this way, external resources referenced # by absolute urls will be left alone # class ImageEmbed include Juicer::Chainable # The maximum supported limit for modern browsers, See the Readme.rdoc for details SIZE_LIMIT = 32768 # # Returns the size limit # def size_limit SIZE_LIMIT end def initialize(options = {}) @document_root = options[:document_root] @document_root.sub!(%r{/?$}, "") if @document_root # Remove trailing slash @type = options[:type] || :none @contents = nil @hosts = options[:hosts] @path_resolver = Juicer::Asset::PathResolver.new(:document_root => options[:document_root], :hosts => options[:hosts]) @force = options[:force] end # # Update file. If no +output+ is provided, the input file is overwritten # def save(file, output = nil) return unless @type == :data_uri output_file = output || file @contents = File.read(file) used = [] @path_resolver = Juicer::Asset::PathResolver.new(:document_root => @document_root, :hosts => @hosts, :base => File.dirname(file)) assets = urls(file) # TODO: Remove "?embed=true" from duplicate urls duplicates = duplicate_urls(assets) if duplicates.length > 0 Juicer::LOGGER.warn("Duplicate image urls detected, these images will not be embedded: #{duplicates.collect { |v| v.gsub('?embed=true', '') }.inspect}") end assets.each do |asset| begin next if used.include?(asset) || duplicates.include?(asset.path) used << asset # make sure we do not exceed SIZE_LIMIT new_path = embed_data_uri(asset.filename) if new_path.length < SIZE_LIMIT # replace the url in the css file with the data uri @contents.gsub!(asset.path, embed_data_uri(asset.filename)) if @force || asset.filename.match( /\?embed=true$/ ) else Juicer::LOGGER.warn("The final data uri for the image located at #{asset.path.gsub('?embed=true', '')} exceeds #{SIZE_LIMIT} and will not be embedded to maintain compatability.") end rescue Errno::ENOENT puts "Unable to locate file #{asset.path}, skipping image embedding" end end File.open(output || file, "w") { |f| f.puts @contents } @contents = nil end chain_method :save def embed_data_uri( path ) new_path = path if @force supported_file_matches = path.match( /(?:\.)(png|gif|jpg|jpeg)$/i ) else supported_file_matches = path.match( /(?:\.)(png|gif|jpg|jpeg)(?:\?embed=true)$/i ) end filetype = supported_file_matches[1] if supported_file_matches if ( filetype ) filename = path.gsub('?embed=true','') # check if file exists, throw an error if it doesn't exist if File.exist?( filename ) # read contents of file into memory content = File.open(filename, "rb") { |f| f.read } content_type = "image/#{filetype}" # encode the url new_path = Datafy::make_data_uri( content, content_type ) else puts "Unable to locate file #{filename} on local file system, skipping image embedding" end end return new_path end # # Returns all referenced URLs in +file+. # def urls(file) @contents = File.read(file) unless @contents @contents.scan(/url\([\s"']*([^\)"'\s]*)[\s"']*\)/m).collect do |match| @path_resolver.resolve(match.first) end end private def duplicate_urls(urls) urls.inject({}) { |h,v| h[v.path] = h[v.path].to_i+1; h }.reject{ |k,v| v == 1 }.keys end end end