lib/frameit/editor.rb in frameit-1.0.1 vs lib/frameit/editor.rb in frameit-2.0.0
- old
+ new
@@ -1,172 +1,239 @@
-require 'deliver'
-require 'fastimage'
-
module Frameit
class Editor
- module Color
- BLACK = "SpaceGray"
- SILVER = "Slvr"
- end
+ attr_accessor :screenshot # reference to the screenshot object to fetch the path, title, etc.
+ attr_accessor :frame # the frame of the device
+ attr_accessor :image # the current image used for editing
+ attr_accessor :top_space_above_device
- module Orientation
- PORTRAIT = "Vert"
- LANDSCAPE = "Horz"
- end
+ def frame!(screenshot)
+ self.screenshot = screenshot
+ prepare_image
+ if load_frame # e.g. Mac doesn't need a frame
+ self.frame = MiniMagick::Image.open(load_frame)
+ end
- def initialize
- converter = FrameConverter.new
- unless converter.frames_exist?
- # First run
- converter.run
+ if should_add_title?
+ @image = complex_framing
else
- # Just make sure, the PSD files are converted to PNG
- converter.convert_frames
+ # easy mode from 1.0 - no title or background
+ width = offset['width']
+ image.resize width # resize the image to fit the frame
+ put_into_frame # put it in the frame
end
+
+ store_result # write to file system
end
- def run(path, color = Color::BLACK)
- @color = color
+ def load_frame
+ TemplateFinder.get_template(screenshot)
+ end
- screenshots = Dir.glob("#{path}/**/*.{png,PNG}")
+ def prepare_image
+ @image = MiniMagick::Image.open(screenshot.path)
+ end
- if screenshots.count > 0
- screenshots.each do |screenshot|
- next if screenshot.include?"_framed.png"
- next if screenshot.include?".itmsp/" # a package file, we don't want to modify that
-
- begin
- template_path = get_template(screenshot)
- if template_path
- template = MiniMagick::Image.open(template_path)
- image = MiniMagick::Image.open(screenshot)
- offset_information = image_offset(screenshot)
- raise "Could not find offset_information for '#{screenshot}'" unless (offset_information and offset_information[:width])
- width = offset_information[:width]
- image.resize width
+ private
+ def store_result
+ output_path = screenshot.path.gsub('.png', '_framed.png').gsub('.PNG', '_framed.png')
+ image.format "png"
+ image.write output_path
+ Helper.log.info "Added frame: '#{File.expand_path(output_path)}'".green
+ end
- result = template.composite(image, "png") do |c|
- c.compose "Over"
- c.geometry offset_information[:offset]
- end
+ # puts the screenshot into the frame
+ def put_into_frame
+ @image = frame.composite(image, "png") do |c|
+ c.compose "Over"
+ c.geometry offset['offset']
+ end
+ end
- output_path = screenshot.gsub('.png', '_framed.png').gsub('.PNG', '_framed.png')
- result.format "png"
- result.write output_path
- Helper.log.info "Added frame: '#{File.expand_path(output_path)}'".green
- end
- rescue => ex
- Helper.log.error ex
- end
+ def offset
+ return @offset_information if @offset_information
+
+ @offset_information = fetch_config['offset'] || Offsets.image_offset(screenshot)
+
+ if @offset_information and (@offset_information['offset'] or @offset_information['offset'])
+ return @offset_information
end
- else
- Helper.log.error "Could not find screenshots"
+ raise "Could not find offset_information for '#{screenshot}'"
end
- end
- # This will detect the screen size and choose the correct template
- def get_template(path)
- parts = [
- device_name(screen_size(path)),
- orientation_name(path),
- @color
- ]
+ #########################################################################################
+ # Everything below is related to title, background, etc. and is not used in the easy mode
+ #########################################################################################
- templates_path = [ENV['HOME'], FrameConverter::FRAME_PATH].join('/')
- templates = Dir["#{templates_path}/**/#{parts.join('_')}*.png"]
+ # this is used to correct the 1:1 offset information
+ # the offset information is stored to work for the template images
+ # since we resize the template images to have higher quality screenshots
+ # we need to modify the offset information by a certain factor
+ def modify_offset(multiplicator)
+ # Format: "+133+50"
+ hash = offset['offset']
+ x = hash.split("+")[1].to_f * multiplicator
+ y = hash.split("+")[2].to_f * multiplicator
+ new_offset = "+#{x.round}+#{y.round}"
+ @offset_information['offset'] = new_offset
+ end
- if templates.count == 0
- if screen_size(path) == Deliver::AppScreenshot::ScreenSize::IOS_35
- Helper.log.warn "Unfortunately 3.5\" device frames were discontinued. Skipping screen '#{path}'".yellow
- else
- Helper.log.error "Could not find a valid template for screenshot '#{path}'".red
- Helper.log.error "You can download new templates from '#{FrameConverter::DOWNLOAD_URL}'"
- Helper.log.error "and store them in '#{templates_path}'"
- Helper.log.error "Missing file: '#{parts.join('_')}.psd'".red
+ # Do we add a background and title as well?
+ def should_add_title?
+ return (fetch_config['background'] and (fetch_config['title'] or fetch_config['keyword']))
+ end
+
+ # more complex mode: background, frame and title
+ def complex_framing
+ background = generate_background
+
+ if self.frame # we have no frame on le mac
+ resize_frame!
+ @image = put_into_frame
+
+ # Decrease the size of the framed screenshot to fit into the defined padding + background
+ frame_width = background.width - fetch_config['padding'] * 2
+ image.resize "#{frame_width}x"
end
- return nil
- else
- # Helper.log.debug "Found template '#{templates.first}' for screenshot '#{path}'"
- return templates.first.gsub(" ", "\ ")
+
+ @@image = put_device_into_background(background)
+
+ if fetch_config['title']
+ @image = add_title
+ end
+
+ image
end
- end
- private
- def screen_size(path)
- Deliver::AppScreenshot.calculate_screen_size(path)
+ # Returns a correctly sized background image
+ def generate_background
+ background = MiniMagick::Image.open(fetch_config['background'])
+
+ if background.height != screenshot.size[1]
+ background.resize "#{screenshot.size[0]}x#{screenshot.size[1]}!" # `!` says it should ignore the ratio
+ end
+ background
end
- def device_name(screen_size)
- size = Deliver::AppScreenshot::ScreenSize
- case screen_size
- when size::IOS_55
- return 'iPhone_6_Plus'
- when size::IOS_47
- return 'iPhone_6'
- when size::IOS_40
- return 'iPhone_5s'
- when size::IOS_IPAD
- return 'iPad_mini'
+ def put_device_into_background(background)
+ left_space = (background.width / 2.0 - image.width / 2.0).round
+ bottom_space = -(image.height / 10).round # to be just a bit below the image bottom
+ bottom_space -= 40 if screenshot.is_portrait? # even more for portrait mode
+
+ self.top_space_above_device = background.height - image.height - bottom_space
+
+ @image = background.composite(image, "png") do |c|
+ c.compose "Over"
+ c.geometry "+#{left_space}+#{top_space_above_device}"
end
+
+ return image
end
- def orientation_name(path)
- size = FastImage.size(path)
- return Orientation::PORTRAIT if size[0] < size[1]
- return Orientation::LANDSCAPE
+ # Resize the frame as it's too low quality by default
+ def resize_frame!
+ multiplicator = (screenshot.size[0].to_f / offset['width'].to_f) # by how much do we have to change this?
+ new_frame_width = multiplicator * frame.width # the new width for the frame
+ frame.resize "#{new_frame_width.round}x" # resize it to the calculated witdth
+ modify_offset(multiplicator) # modify the offset to properly insert the screenshot into the frame later
end
- def image_offset(path)
- size = Deliver::AppScreenshot::ScreenSize
- case orientation_name(path)
- when Orientation::PORTRAIT
- case screen_size(path)
- when size::IOS_55
- return {
- offset: '+42+147',
- width: 539
- }
- when size::IOS_47
- return {
- offset: '+41+154',
- width: 530
- }
- when size::IOS_40
- return {
- offset: "+54+197",
- width: 543
- }
- when size::IOS_IPAD
- return {
- offset: '+50+134',
- width: 792
- }
- end
- when Orientation::LANDSCAPE
- case screen_size(path)
- when size::IOS_55
- return {
- offset: "+146+41",
- width: 960
- }
- when size::IOS_47
- return {
- offset: "+153+41",
- width: 946
- }
- when size::IOS_40
- return {
- offset: "+201+48",
- width: 970
- }
- when size::IOS_IPAD
- return {
- offset: '+133+50',
- width: 1058
- }
- end
+ # Add the title above the device
+ def add_title
+ title_images = build_title_images(image.width)
+ keyword = title_images[:keyword]
+ title = title_images[:title]
+
+ sum_width = (keyword.width rescue 0) + title.width + keyword_padding
+ top_space = (top_space_above_device / 2.0 - actual_font_size / 2.0).round # centered
+
+ left_space = (image.width / 2.0 - sum_width / 2.0).round
+ if keyword
+ @image = image.composite(keyword, "png") do |c|
+ c.compose "Over"
+ c.geometry "+#{left_space}+#{top_space}"
+ end
end
+
+ left_space += (keyword.width rescue 0) + keyword_padding
+ @image = image.composite(title, "png") do |c|
+ c.compose "Over"
+ c.geometry "+#{left_space}+#{top_space}"
+ end
+ image
end
+
+ def actual_font_size
+ (screenshot.size[0] / 20.0).round # depends on the width of the screenshot
+ end
+
+ def keyword_padding
+ (actual_font_size / 2.0).round
+ end
+
+ # This will build 2 individual images with the title, which will then be added to the real image
+ def build_title_images(max_width)
+ words = [:keyword, :title].keep_if{ |a| fetch_text(a) } # optional keyword/title
+ results = {}
+ words.each do |key|
+ # Create empty background
+ empty_path = File.join(Helper.gem_path('frameit'), "lib/assets/empty.png")
+ title_image = MiniMagick::Image.open(empty_path)
+ image_height = actual_font_size * 2 # gets trimmed afterwards anyway, and on the iPad the `y` would get cut
+ title_image.combine_options do |i|
+ i.resize "#{max_width}x#{image_height}!" # `!` says it should ignore the ratio
+ end
+
+ # Add the actual title
+ font = fetch_config[key.to_s]['font']
+ title_image.combine_options do |i|
+ i.font font if font
+ i.gravity "Center"
+ i.pointsize actual_font_size
+ i.draw "text 0,0 '#{fetch_text(key)}'"
+ i.fill fetch_config[key.to_s]['color']
+ end
+ title_image.trim # remove white space
+
+ results[key] = title_image
+ end
+ results
+ end
+
+ # Loads the config (colors, background, texts, etc.)
+ # Don't use this method to access the actual text and use `fetch_texts` instead
+ def fetch_config
+ return @config if @config
+
+ config_path = File.join(File.expand_path("..", screenshot.path), "Framefile.json")
+ config_path = File.join(File.expand_path("../..", screenshot.path), "Framefile.json") unless File.exists?config_path
+ file = ConfigParser.new.load(config_path)
+ return {} unless file # no config file at all
+ @config = file.fetch_value(screenshot.path)
+ end
+
+ # Fetches the title + keyword for this particular screenshot
+ def fetch_text(type)
+ raise "Valid parameters :keyword, :title" unless [:keyword, :title].include?type
+
+ # Try to get it from a keyword.strings or title.strings file
+ strings_path = File.join(File.expand_path("..", screenshot.path), "#{type.to_s}.strings")
+ if File.exists?strings_path
+ parsed = StringsParser.parse(strings_path)
+ result = parsed.find { |k, v| screenshot.path.include?k }
+ return result.last if result
+ end
+
+ # No string files, fallback to Framefile config
+ result = fetch_config[type.to_s]['text']
+
+ if !result and type == :title
+ # title is mandatory
+ raise "Could not get title for screenshot #{screenshot.path}. Please provide one in your Framefile.json".red
+ end
+
+ return result
+ end
+
end
-end
+end
\ No newline at end of file