frameit 1.0.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +3 -0
- data/README.md +10 -8
- data/bin/frameit +3 -2
- data/lib/assets/empty.png +0 -0
- data/lib/frameit.rb +16 -2
- data/lib/frameit/config_parser.rb +83 -0
- data/lib/frameit/dependency_checker.rb +2 -0
- data/lib/frameit/device_types.rb +11 -0
- data/lib/frameit/editor.rb +218 -151
- data/lib/frameit/frame_converter.rb +1 -1
- data/lib/frameit/mac_editor.rb +33 -0
- data/lib/frameit/offsets.rb +62 -0
- data/lib/frameit/runner.rb +38 -0
- data/lib/frameit/screenshot.rb +66 -0
- data/lib/frameit/strings_parser.rb +31 -0
- data/lib/frameit/template_finder.rb +37 -0
- data/lib/frameit/version.rb +1 -1
- metadata +14 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39874df0b50f3d2b85da9a8e15fd1a63639cc090
|
4
|
+
data.tar.gz: e0ef4228c227cadd7557cab83bc3a17c09361e9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ffb3757b70c4a004f0a69462526ac341e676bc26c011d23164733e9217b30a4c80714419babd61bce104b8d47985a76f0e50924cda6fbc9b7f8d4e8ddc7dd362
|
7
|
+
data.tar.gz: ad69ccef5a466dbe23ba6a1533372c351a191961df49084ade21683ecff7024b0ff9bcc08dafa4ec198222366a5d0f8b7da11c83ab9c5b74b06edadfc2696998
|
data/LICENSE
CHANGED
@@ -19,3 +19,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
21
|
SOFTWARE.
|
22
|
+
|
23
|
+
All screenshots located inside the 'assets' folder of this directory are excluded
|
24
|
+
from this license.
|
data/README.md
CHANGED
@@ -27,11 +27,10 @@ frameit
|
|
27
27
|
[![Twitter: @KauseFx](https://img.shields.io/badge/contact-@KrauseFx-blue.svg?style=flat)](https://twitter.com/KrauseFx)
|
28
28
|
[![License](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/KrauseFx/frameit/blob/master/LICENSE)
|
29
29
|
[![Gem](https://img.shields.io/gem/v/frameit.svg?style=flat)](http://rubygems.org/gems/frameit)
|
30
|
+
[![Build Status](https://img.shields.io/travis/fastlane/frameit/master.svg?style=flat)](https://travis-ci.org/fastlane/frameit)
|
30
31
|
|
31
32
|
###### Quickly put your screenshots into the right device frames
|
32
33
|
|
33
|
-
This tool is not supposed to be used for App Store screenshots. Instead it should be used to prepare screenshots for websites, emails and prints.
|
34
|
-
|
35
34
|
Get in contact with the developer on Twitter: [@KrauseFx](https://twitter.com/KrauseFx)
|
36
35
|
|
37
36
|
|
@@ -60,10 +59,7 @@ Put a gorgeous device frame around your iOS screenshots just by running one simp
|
|
60
59
|
|
61
60
|
Here is a nice gif, that shows ```frameit``` in action:
|
62
61
|
|
63
|
-
![assets/FrameitGit.gif](assets/FrameitGit.gif)
|
64
|
-
|
65
|
-
Here is how the result can look like
|
66
|
-
![assets/Overview.png](assets/Overview.png)
|
62
|
+
![assets/FrameitGit.gif](assets/FrameitGit.gif?raw=1)
|
67
63
|
|
68
64
|
# Installation
|
69
65
|
|
@@ -103,6 +99,8 @@ To run the setup process again to add new frames use:
|
|
103
99
|
|
104
100
|
frameit setup
|
105
101
|
|
102
|
+
When using `frameit` without titles on top, the screenshots will have the full resolution, which means they can't be uploaded to the App Store directly. They are supposed to be used for websites, print media and emails. Check out the section below to use the screenshots for the App Store.
|
103
|
+
|
106
104
|
# Tips
|
107
105
|
|
108
106
|
## [`fastlane`](https://fastlane.tools) Toolchain
|
@@ -118,8 +116,12 @@ To run the setup process again to add new frames use:
|
|
118
116
|
|
119
117
|
##### [Like this tool? Be the first to know about updates and new fastlane tools](https://tinyletter.com/krausefx)
|
120
118
|
|
121
|
-
## Generate screenshots
|
122
|
-
Check out [
|
119
|
+
## Generate localized screenshots
|
120
|
+
Check out [`snapshot`](https://github.com/KrauseFx/snapshot) to automatically generate screenshots using ```UI Automation```.
|
121
|
+
|
122
|
+
## White background of frames
|
123
|
+
|
124
|
+
Some stock images provided by Apple still have a white background instead of a transparent one. You'll have to edit the Photoshop file to remove the white background, delete the generated `.png` file and run `frameit` again.
|
123
125
|
|
124
126
|
## Use a clean status bar
|
125
127
|
You can use [SimulatorStatusMagic](https://github.com/shinydevelopment/SimulatorStatusMagic) to clean up the status bar.
|
data/bin/frameit
CHANGED
@@ -29,7 +29,7 @@ class FrameItApplication
|
|
29
29
|
c.description = "Adds a black frame around all screenshots."
|
30
30
|
|
31
31
|
c.action do |args, options|
|
32
|
-
Frameit::
|
32
|
+
Frameit::Runner.new.run('.', Frameit::Color::BLACK)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -38,7 +38,7 @@ class FrameItApplication
|
|
38
38
|
c.description = "Adds a silver frame around all screenshots."
|
39
39
|
|
40
40
|
c.action do |args, options|
|
41
|
-
Frameit::
|
41
|
+
Frameit::Runner.new.run('.', Frameit::Color::SILVER)
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
@@ -60,6 +60,7 @@ end
|
|
60
60
|
|
61
61
|
begin
|
62
62
|
FastlaneCore::UpdateChecker.start_looking_for_update('frameit')
|
63
|
+
Frameit::DependencyChecker.check_dependencies
|
63
64
|
FrameItApplication.new.run
|
64
65
|
ensure
|
65
66
|
FastlaneCore::UpdateChecker.show_update_status('frameit', Frameit::VERSION)
|
Binary file
|
data/lib/frameit.rb
CHANGED
@@ -1,13 +1,27 @@
|
|
1
1
|
require 'mini_magick'
|
2
2
|
require 'frameit/version'
|
3
3
|
require 'frameit/frame_converter'
|
4
|
+
require 'frameit/device_types'
|
5
|
+
require 'frameit/runner'
|
6
|
+
require 'frameit/screenshot'
|
7
|
+
require 'frameit/config_parser'
|
8
|
+
require 'frameit/offsets'
|
4
9
|
require 'frameit/editor'
|
10
|
+
require 'frameit/template_finder'
|
11
|
+
require 'frameit/strings_parser'
|
12
|
+
require 'frameit/mac_editor'
|
5
13
|
require 'frameit/dependency_checker'
|
6
14
|
|
7
15
|
require 'fastlane_core'
|
8
16
|
|
9
17
|
module Frameit
|
10
18
|
Helper = FastlaneCore::Helper # you gotta love Ruby: Helper.* should use the Helper class contained in FastlaneCore
|
11
|
-
|
12
|
-
Frameit::DependencyChecker.check_dependencies
|
13
19
|
end
|
20
|
+
|
21
|
+
|
22
|
+
class ::Hash
|
23
|
+
def fastlane_deep_merge(second)
|
24
|
+
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
25
|
+
self.merge(second, &merger)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Frameit
|
2
|
+
class ConfigParser
|
3
|
+
def load(path)
|
4
|
+
return nil unless File.exists?(path) # we are okay with no config at all
|
5
|
+
Helper.log.info "Parsing config file '#{path}'".yellow if $verbose
|
6
|
+
@path = path
|
7
|
+
self.parse(File.read(path))
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
# @param data (String) the JSON data to be parsed
|
13
|
+
def parse(data)
|
14
|
+
begin
|
15
|
+
@data = JSON.parse(data)
|
16
|
+
rescue => ex
|
17
|
+
Helper.log.fatal ex
|
18
|
+
raise "Invalid JSON file at path '#{@path}'. Make sure it's a valid JSON file".red
|
19
|
+
end
|
20
|
+
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
# Fetches the finished configuration for a given path. This will try to look for a specific value
|
25
|
+
# and fallback to a default value if nothing was found
|
26
|
+
def fetch_value(path)
|
27
|
+
specific = @data['data'].find { |a| path.include?a['filter'] }
|
28
|
+
|
29
|
+
default = @data['default']
|
30
|
+
|
31
|
+
values = default.fastlane_deep_merge(specific || {})
|
32
|
+
|
33
|
+
change_paths_to_absolutes!(values)
|
34
|
+
validate_values(values)
|
35
|
+
|
36
|
+
values
|
37
|
+
end
|
38
|
+
|
39
|
+
# Use absolute paths instead of relative
|
40
|
+
def change_paths_to_absolutes!(values)
|
41
|
+
values.each do |key, value|
|
42
|
+
if value.kind_of?Hash
|
43
|
+
change_paths_to_absolutes!(value) # recursive call
|
44
|
+
else
|
45
|
+
if ['font', 'background'].include?key
|
46
|
+
# Change the paths to relative ones
|
47
|
+
# `replace`: to change the content of the string, so it's actually stored
|
48
|
+
|
49
|
+
if @path # where is the config file. We don't have a config file in tests
|
50
|
+
containing_folder = File.expand_path('..', @path)
|
51
|
+
value.replace File.join(containing_folder, value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Make sure the paths/colors are valid
|
59
|
+
def validate_values(values)
|
60
|
+
values.each do |key, value|
|
61
|
+
if value.kind_of?Hash
|
62
|
+
validate_values(value) # recursive call
|
63
|
+
else
|
64
|
+
if key == 'font'
|
65
|
+
raise "Could not find font at path '#{File.expand_path(value)}'" unless File.exists?value
|
66
|
+
end
|
67
|
+
|
68
|
+
if key == 'background'
|
69
|
+
raise "Could not find background image at path '#{File.expand_path(value)}'" unless File.exists?value
|
70
|
+
end
|
71
|
+
|
72
|
+
if key == 'color'
|
73
|
+
raise "Invalid color '#{value}'. Must be valid Hex #123123" unless value.include?"#"
|
74
|
+
end
|
75
|
+
|
76
|
+
if key == 'padding'
|
77
|
+
raise "padding must be type integer" unless value.kind_of?Integer
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/frameit/editor.rb
CHANGED
@@ -1,172 +1,239 @@
|
|
1
|
-
require 'deliver'
|
2
|
-
require 'fastimage'
|
3
|
-
|
4
1
|
module Frameit
|
5
2
|
class Editor
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
3
|
+
attr_accessor :screenshot # reference to the screenshot object to fetch the path, title, etc.
|
4
|
+
attr_accessor :frame # the frame of the device
|
5
|
+
attr_accessor :image # the current image used for editing
|
6
|
+
attr_accessor :top_space_above_device
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
8
|
+
def frame!(screenshot)
|
9
|
+
self.screenshot = screenshot
|
10
|
+
prepare_image
|
15
11
|
|
12
|
+
if load_frame # e.g. Mac doesn't need a frame
|
13
|
+
self.frame = MiniMagick::Image.open(load_frame)
|
14
|
+
end
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
unless converter.frames_exist?
|
20
|
-
# First run
|
21
|
-
converter.run
|
16
|
+
if should_add_title?
|
17
|
+
@image = complex_framing
|
22
18
|
else
|
23
|
-
#
|
24
|
-
|
19
|
+
# easy mode from 1.0 - no title or background
|
20
|
+
width = offset['width']
|
21
|
+
image.resize width # resize the image to fit the frame
|
22
|
+
put_into_frame # put it in the frame
|
25
23
|
end
|
24
|
+
|
25
|
+
store_result # write to file system
|
26
26
|
end
|
27
27
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
c.compose "Over"
|
51
|
-
c.geometry offset_information[:offset]
|
52
|
-
end
|
53
|
-
|
54
|
-
output_path = screenshot.gsub('.png', '_framed.png').gsub('.PNG', '_framed.png')
|
55
|
-
result.format "png"
|
56
|
-
result.write output_path
|
57
|
-
Helper.log.info "Added frame: '#{File.expand_path(output_path)}'".green
|
58
|
-
end
|
59
|
-
rescue => ex
|
60
|
-
Helper.log.error ex
|
61
|
-
end
|
28
|
+
def load_frame
|
29
|
+
TemplateFinder.get_template(screenshot)
|
30
|
+
end
|
31
|
+
|
32
|
+
def prepare_image
|
33
|
+
@image = MiniMagick::Image.open(screenshot.path)
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
private
|
38
|
+
def store_result
|
39
|
+
output_path = screenshot.path.gsub('.png', '_framed.png').gsub('.PNG', '_framed.png')
|
40
|
+
image.format "png"
|
41
|
+
image.write output_path
|
42
|
+
Helper.log.info "Added frame: '#{File.expand_path(output_path)}'".green
|
43
|
+
end
|
44
|
+
|
45
|
+
# puts the screenshot into the frame
|
46
|
+
def put_into_frame
|
47
|
+
@image = frame.composite(image, "png") do |c|
|
48
|
+
c.compose "Over"
|
49
|
+
c.geometry offset['offset']
|
62
50
|
end
|
63
|
-
else
|
64
|
-
Helper.log.error "Could not find screenshots"
|
65
51
|
end
|
66
|
-
end
|
67
52
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
@
|
74
|
-
|
75
|
-
|
76
|
-
templates_path = [ENV['HOME'], FrameConverter::FRAME_PATH].join('/')
|
77
|
-
templates = Dir["#{templates_path}/**/#{parts.join('_')}*.png"]
|
78
|
-
|
79
|
-
if templates.count == 0
|
80
|
-
if screen_size(path) == Deliver::AppScreenshot::ScreenSize::IOS_35
|
81
|
-
Helper.log.warn "Unfortunately 3.5\" device frames were discontinued. Skipping screen '#{path}'".yellow
|
82
|
-
else
|
83
|
-
Helper.log.error "Could not find a valid template for screenshot '#{path}'".red
|
84
|
-
Helper.log.error "You can download new templates from '#{FrameConverter::DOWNLOAD_URL}'"
|
85
|
-
Helper.log.error "and store them in '#{templates_path}'"
|
86
|
-
Helper.log.error "Missing file: '#{parts.join('_')}.psd'".red
|
53
|
+
def offset
|
54
|
+
return @offset_information if @offset_information
|
55
|
+
|
56
|
+
@offset_information = fetch_config['offset'] || Offsets.image_offset(screenshot)
|
57
|
+
|
58
|
+
if @offset_information and (@offset_information['offset'] or @offset_information['offset'])
|
59
|
+
return @offset_information
|
87
60
|
end
|
88
|
-
|
89
|
-
else
|
90
|
-
# Helper.log.debug "Found template '#{templates.first}' for screenshot '#{path}'"
|
91
|
-
return templates.first.gsub(" ", "\ ")
|
61
|
+
raise "Could not find offset_information for '#{screenshot}'"
|
92
62
|
end
|
93
|
-
end
|
94
63
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
64
|
+
#########################################################################################
|
65
|
+
# Everything below is related to title, background, etc. and is not used in the easy mode
|
66
|
+
#########################################################################################
|
67
|
+
|
68
|
+
# this is used to correct the 1:1 offset information
|
69
|
+
# the offset information is stored to work for the template images
|
70
|
+
# since we resize the template images to have higher quality screenshots
|
71
|
+
# we need to modify the offset information by a certain factor
|
72
|
+
def modify_offset(multiplicator)
|
73
|
+
# Format: "+133+50"
|
74
|
+
hash = offset['offset']
|
75
|
+
x = hash.split("+")[1].to_f * multiplicator
|
76
|
+
y = hash.split("+")[2].to_f * multiplicator
|
77
|
+
new_offset = "+#{x.round}+#{y.round}"
|
78
|
+
@offset_information['offset'] = new_offset
|
79
|
+
end
|
80
|
+
|
81
|
+
# Do we add a background and title as well?
|
82
|
+
def should_add_title?
|
83
|
+
return (fetch_config['background'] and (fetch_config['title'] or fetch_config['keyword']))
|
84
|
+
end
|
85
|
+
|
86
|
+
# more complex mode: background, frame and title
|
87
|
+
def complex_framing
|
88
|
+
background = generate_background
|
89
|
+
|
90
|
+
if self.frame # we have no frame on le mac
|
91
|
+
resize_frame!
|
92
|
+
@image = put_into_frame
|
93
|
+
|
94
|
+
# Decrease the size of the framed screenshot to fit into the defined padding + background
|
95
|
+
frame_width = background.width - fetch_config['padding'] * 2
|
96
|
+
image.resize "#{frame_width}x"
|
97
|
+
end
|
98
|
+
|
99
|
+
@@image = put_device_into_background(background)
|
100
|
+
|
101
|
+
if fetch_config['title']
|
102
|
+
@image = add_title
|
103
|
+
end
|
104
|
+
|
105
|
+
image
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns a correctly sized background image
|
109
|
+
def generate_background
|
110
|
+
background = MiniMagick::Image.open(fetch_config['background'])
|
111
|
+
|
112
|
+
if background.height != screenshot.size[1]
|
113
|
+
background.resize "#{screenshot.size[0]}x#{screenshot.size[1]}!" # `!` says it should ignore the ratio
|
114
|
+
end
|
115
|
+
background
|
116
|
+
end
|
117
|
+
|
118
|
+
def put_device_into_background(background)
|
119
|
+
left_space = (background.width / 2.0 - image.width / 2.0).round
|
120
|
+
bottom_space = -(image.height / 10).round # to be just a bit below the image bottom
|
121
|
+
bottom_space -= 40 if screenshot.is_portrait? # even more for portrait mode
|
122
|
+
|
123
|
+
self.top_space_above_device = background.height - image.height - bottom_space
|
124
|
+
|
125
|
+
@image = background.composite(image, "png") do |c|
|
126
|
+
c.compose "Over"
|
127
|
+
c.geometry "+#{left_space}+#{top_space_above_device}"
|
111
128
|
end
|
129
|
+
|
130
|
+
return image
|
112
131
|
end
|
113
132
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
offset: "+54+197",
|
138
|
-
width: 543
|
139
|
-
}
|
140
|
-
when size::IOS_IPAD
|
141
|
-
return {
|
142
|
-
offset: '+50+134',
|
143
|
-
width: 792
|
144
|
-
}
|
145
|
-
end
|
146
|
-
when Orientation::LANDSCAPE
|
147
|
-
case screen_size(path)
|
148
|
-
when size::IOS_55
|
149
|
-
return {
|
150
|
-
offset: "+146+41",
|
151
|
-
width: 960
|
152
|
-
}
|
153
|
-
when size::IOS_47
|
154
|
-
return {
|
155
|
-
offset: "+153+41",
|
156
|
-
width: 946
|
157
|
-
}
|
158
|
-
when size::IOS_40
|
159
|
-
return {
|
160
|
-
offset: "+201+48",
|
161
|
-
width: 970
|
162
|
-
}
|
163
|
-
when size::IOS_IPAD
|
164
|
-
return {
|
165
|
-
offset: '+133+50',
|
166
|
-
width: 1058
|
167
|
-
}
|
168
|
-
end
|
133
|
+
# Resize the frame as it's too low quality by default
|
134
|
+
def resize_frame!
|
135
|
+
multiplicator = (screenshot.size[0].to_f / offset['width'].to_f) # by how much do we have to change this?
|
136
|
+
new_frame_width = multiplicator * frame.width # the new width for the frame
|
137
|
+
frame.resize "#{new_frame_width.round}x" # resize it to the calculated witdth
|
138
|
+
modify_offset(multiplicator) # modify the offset to properly insert the screenshot into the frame later
|
139
|
+
end
|
140
|
+
|
141
|
+
# Add the title above the device
|
142
|
+
def add_title
|
143
|
+
title_images = build_title_images(image.width)
|
144
|
+
keyword = title_images[:keyword]
|
145
|
+
title = title_images[:title]
|
146
|
+
|
147
|
+
sum_width = (keyword.width rescue 0) + title.width + keyword_padding
|
148
|
+
top_space = (top_space_above_device / 2.0 - actual_font_size / 2.0).round # centered
|
149
|
+
|
150
|
+
left_space = (image.width / 2.0 - sum_width / 2.0).round
|
151
|
+
if keyword
|
152
|
+
@image = image.composite(keyword, "png") do |c|
|
153
|
+
c.compose "Over"
|
154
|
+
c.geometry "+#{left_space}+#{top_space}"
|
155
|
+
end
|
169
156
|
end
|
157
|
+
|
158
|
+
left_space += (keyword.width rescue 0) + keyword_padding
|
159
|
+
@image = image.composite(title, "png") do |c|
|
160
|
+
c.compose "Over"
|
161
|
+
c.geometry "+#{left_space}+#{top_space}"
|
162
|
+
end
|
163
|
+
image
|
164
|
+
end
|
165
|
+
|
166
|
+
def actual_font_size
|
167
|
+
(screenshot.size[0] / 20.0).round # depends on the width of the screenshot
|
168
|
+
end
|
169
|
+
|
170
|
+
def keyword_padding
|
171
|
+
(actual_font_size / 2.0).round
|
170
172
|
end
|
173
|
+
|
174
|
+
# This will build 2 individual images with the title, which will then be added to the real image
|
175
|
+
def build_title_images(max_width)
|
176
|
+
words = [:keyword, :title].keep_if{ |a| fetch_text(a) } # optional keyword/title
|
177
|
+
results = {}
|
178
|
+
words.each do |key|
|
179
|
+
# Create empty background
|
180
|
+
empty_path = File.join(Helper.gem_path('frameit'), "lib/assets/empty.png")
|
181
|
+
title_image = MiniMagick::Image.open(empty_path)
|
182
|
+
image_height = actual_font_size * 2 # gets trimmed afterwards anyway, and on the iPad the `y` would get cut
|
183
|
+
title_image.combine_options do |i|
|
184
|
+
i.resize "#{max_width}x#{image_height}!" # `!` says it should ignore the ratio
|
185
|
+
end
|
186
|
+
|
187
|
+
# Add the actual title
|
188
|
+
font = fetch_config[key.to_s]['font']
|
189
|
+
title_image.combine_options do |i|
|
190
|
+
i.font font if font
|
191
|
+
i.gravity "Center"
|
192
|
+
i.pointsize actual_font_size
|
193
|
+
i.draw "text 0,0 '#{fetch_text(key)}'"
|
194
|
+
i.fill fetch_config[key.to_s]['color']
|
195
|
+
end
|
196
|
+
title_image.trim # remove white space
|
197
|
+
|
198
|
+
results[key] = title_image
|
199
|
+
end
|
200
|
+
results
|
201
|
+
end
|
202
|
+
|
203
|
+
# Loads the config (colors, background, texts, etc.)
|
204
|
+
# Don't use this method to access the actual text and use `fetch_texts` instead
|
205
|
+
def fetch_config
|
206
|
+
return @config if @config
|
207
|
+
|
208
|
+
config_path = File.join(File.expand_path("..", screenshot.path), "Framefile.json")
|
209
|
+
config_path = File.join(File.expand_path("../..", screenshot.path), "Framefile.json") unless File.exists?config_path
|
210
|
+
file = ConfigParser.new.load(config_path)
|
211
|
+
return {} unless file # no config file at all
|
212
|
+
@config = file.fetch_value(screenshot.path)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Fetches the title + keyword for this particular screenshot
|
216
|
+
def fetch_text(type)
|
217
|
+
raise "Valid parameters :keyword, :title" unless [:keyword, :title].include?type
|
218
|
+
|
219
|
+
# Try to get it from a keyword.strings or title.strings file
|
220
|
+
strings_path = File.join(File.expand_path("..", screenshot.path), "#{type.to_s}.strings")
|
221
|
+
if File.exists?strings_path
|
222
|
+
parsed = StringsParser.parse(strings_path)
|
223
|
+
result = parsed.find { |k, v| screenshot.path.include?k }
|
224
|
+
return result.last if result
|
225
|
+
end
|
226
|
+
|
227
|
+
# No string files, fallback to Framefile config
|
228
|
+
result = fetch_config[type.to_s]['text']
|
229
|
+
|
230
|
+
if !result and type == :title
|
231
|
+
# title is mandatory
|
232
|
+
raise "Could not get title for screenshot #{screenshot.path}. Please provide one in your Framefile.json".red
|
233
|
+
end
|
234
|
+
|
235
|
+
return result
|
236
|
+
end
|
237
|
+
|
171
238
|
end
|
172
|
-
end
|
239
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Frameit
|
2
|
+
# Responsible for framing Mac Screenshots
|
3
|
+
class MacEditor < Editor
|
4
|
+
|
5
|
+
def prepare_image
|
6
|
+
image = super
|
7
|
+
image.resize("#{offset['width']}x") if offset['width']
|
8
|
+
end
|
9
|
+
|
10
|
+
def put_device_into_background(background)
|
11
|
+
self.top_space_above_device = offset['titleHeight'] # needed for centering the title
|
12
|
+
|
13
|
+
@image = background.composite(image, "png") do |c|
|
14
|
+
c.compose "Over"
|
15
|
+
c.geometry offset['offset']
|
16
|
+
end
|
17
|
+
|
18
|
+
return image
|
19
|
+
end
|
20
|
+
|
21
|
+
def load_frame
|
22
|
+
nil # Macs don't need frames - backgrounds only
|
23
|
+
end
|
24
|
+
|
25
|
+
def should_add_title?
|
26
|
+
true # Mac screenshots always need a background
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_background
|
30
|
+
MiniMagick::Image.open(fetch_config['background']) # no resizing on the Mac
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Frameit
|
2
|
+
class Offsets
|
3
|
+
# Returns the image offset needed for a certain device type for a given orientation
|
4
|
+
# uses deliver to detect the screen size
|
5
|
+
def self.image_offset(screenshot)
|
6
|
+
size = Deliver::AppScreenshot::ScreenSize
|
7
|
+
case screenshot.orientation_name
|
8
|
+
when Orientation::PORTRAIT
|
9
|
+
case screenshot.screen_size
|
10
|
+
when size::IOS_55
|
11
|
+
return {
|
12
|
+
'offset' => '+42+147',
|
13
|
+
'width' => 539
|
14
|
+
}
|
15
|
+
when size::IOS_47
|
16
|
+
return {
|
17
|
+
'offset' => '+41+154',
|
18
|
+
'width' => 530
|
19
|
+
}
|
20
|
+
when size::IOS_40
|
21
|
+
return {
|
22
|
+
'offset' => "+54+197",
|
23
|
+
'width' => 543
|
24
|
+
}
|
25
|
+
when size::IOS_IPAD
|
26
|
+
return {
|
27
|
+
'offset' => '+50+134',
|
28
|
+
'width' => 792
|
29
|
+
}
|
30
|
+
end
|
31
|
+
when Orientation::LANDSCAPE
|
32
|
+
case screenshot.screen_size
|
33
|
+
when size::IOS_55
|
34
|
+
return {
|
35
|
+
'offset' => "+146+41",
|
36
|
+
'width' => 960
|
37
|
+
}
|
38
|
+
when size::IOS_47
|
39
|
+
return {
|
40
|
+
'offset' => "+153+41",
|
41
|
+
'width' => 946
|
42
|
+
}
|
43
|
+
when size::IOS_40
|
44
|
+
return {
|
45
|
+
'offset' => "+201+48",
|
46
|
+
'width' => 970
|
47
|
+
}
|
48
|
+
when size::IOS_35
|
49
|
+
return {
|
50
|
+
'offset' => "+258+52",
|
51
|
+
'width' => 966
|
52
|
+
}
|
53
|
+
when size::IOS_IPAD
|
54
|
+
return {
|
55
|
+
'offset' => '+133+50',
|
56
|
+
'width' => 1058
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'deliver'
|
2
|
+
require 'fastimage'
|
3
|
+
|
4
|
+
module Frameit
|
5
|
+
class Runner
|
6
|
+
def initialize
|
7
|
+
converter = FrameConverter.new
|
8
|
+
unless converter.frames_exist?
|
9
|
+
# First run
|
10
|
+
converter.run
|
11
|
+
else
|
12
|
+
# Just make sure, the PSD files are converted to PNG
|
13
|
+
converter.convert_frames
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def run(path, color = Color::BLACK)
|
18
|
+
screenshots = Dir.glob("#{path}/**/*.{png,PNG}")
|
19
|
+
|
20
|
+
if screenshots.count > 0
|
21
|
+
screenshots.each do |full_path|
|
22
|
+
next if full_path.include?"_framed.png"
|
23
|
+
next if full_path.include?".itmsp/" # a package file, we don't want to modify that
|
24
|
+
next if full_path.include?"device_frames/" # these are the device frames the user is using
|
25
|
+
|
26
|
+
begin
|
27
|
+
screenshot = Screenshot.new(full_path, color)
|
28
|
+
screenshot.frame!
|
29
|
+
rescue => ex
|
30
|
+
Helper.log.error ex
|
31
|
+
end
|
32
|
+
end
|
33
|
+
else
|
34
|
+
Helper.log.error "Could not find screenshots"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Frameit
|
2
|
+
# Represents one screenshot
|
3
|
+
class Screenshot
|
4
|
+
attr_accessor :path # path to the screenshot
|
5
|
+
attr_accessor :size # size in px array of 2 elements: height and width
|
6
|
+
attr_accessor :screen_size # deliver screen size type, is unique per device type, used in device_name
|
7
|
+
attr_accessor :color # the color to use for the frame
|
8
|
+
|
9
|
+
# path: Path to screenshot
|
10
|
+
# color: Color to use for the frame
|
11
|
+
def initialize(path, color)
|
12
|
+
raise "Couldn't find file at path '#{path}'".red unless File.exists?path
|
13
|
+
@color = color
|
14
|
+
@path = path
|
15
|
+
@size = FastImage.size(path)
|
16
|
+
|
17
|
+
@screen_size = ENV["FRAMEIT_FORCE_DEVICE_TYPE"] || Deliver::AppScreenshot.calculate_screen_size(path)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Device name for a given screen size. Used to use the correct template
|
21
|
+
def device_name
|
22
|
+
sizes = Deliver::AppScreenshot::ScreenSize
|
23
|
+
case @screen_size
|
24
|
+
when sizes::IOS_55
|
25
|
+
return 'iPhone_6_Plus'
|
26
|
+
when sizes::IOS_47
|
27
|
+
return 'iPhone_6'
|
28
|
+
when sizes::IOS_40
|
29
|
+
return 'iPhone_5s'
|
30
|
+
when sizes::IOS_35
|
31
|
+
return 'iPhone_4'
|
32
|
+
when sizes::IOS_IPAD
|
33
|
+
return 'iPad_mini'
|
34
|
+
when sizes::MAC
|
35
|
+
return 'Mac'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def is_mac?
|
40
|
+
return device_name == 'Mac'
|
41
|
+
end
|
42
|
+
|
43
|
+
# The name of the orientation of a screenshot. Used to find the correct template
|
44
|
+
def orientation_name
|
45
|
+
return Orientation::PORTRAIT if size[0] < size[1]
|
46
|
+
return Orientation::LANDSCAPE
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_portrait?
|
50
|
+
return (orientation_name == Orientation::PORTRAIT)
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_s
|
54
|
+
self.path
|
55
|
+
end
|
56
|
+
|
57
|
+
# Add the device frame, this will also call the method that adds the background + title
|
58
|
+
def frame!
|
59
|
+
if self.is_mac?
|
60
|
+
MacEditor.new.frame!(self)
|
61
|
+
else
|
62
|
+
Editor.new.frame!(self)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Frameit
|
2
|
+
# This class will parse the .string files
|
3
|
+
class StringsParser
|
4
|
+
def self.parse(path)
|
5
|
+
raise "Couldn't find strings file at path '#{path}'".red unless File.exists?path
|
6
|
+
raise "Must be .strings file, only got '#{path}'".red unless path.end_with?".strings"
|
7
|
+
|
8
|
+
result = {}
|
9
|
+
|
10
|
+
# A .strings file is UTF-16 encoded. We only want to deal with UTF-8
|
11
|
+
content = `iconv -f UTF-16 -t UTF-8 '#{path}'`
|
12
|
+
|
13
|
+
content.split("\n").each do |line|
|
14
|
+
begin
|
15
|
+
# We don't care about comments and empty lines
|
16
|
+
if line.start_with?'"'
|
17
|
+
key = line.match(/"(.*)" \= /)[1]
|
18
|
+
value = line.match(/ \= "(.*)"/)[1]
|
19
|
+
|
20
|
+
result[key] = value
|
21
|
+
end
|
22
|
+
rescue => ex
|
23
|
+
Helper.log.error ex
|
24
|
+
Helper.log.error line
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
result
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Frameit
|
2
|
+
# Responsible for finding the correct device
|
3
|
+
class TemplateFinder
|
4
|
+
|
5
|
+
# This will detect the screen size and choose the correct template
|
6
|
+
def self.get_template(screenshot)
|
7
|
+
return nil if screenshot.is_mac?
|
8
|
+
parts = [
|
9
|
+
screenshot.device_name,
|
10
|
+
screenshot.orientation_name,
|
11
|
+
screenshot.color
|
12
|
+
]
|
13
|
+
parts << "sRGB" if screenshot.device_name == 'iPad_mini'
|
14
|
+
|
15
|
+
|
16
|
+
templates_path = [ENV['HOME'], FrameConverter::FRAME_PATH].join('/')
|
17
|
+
templates = Dir["../**/#{parts.join('_')}*.{png,jpg}"] # local directory
|
18
|
+
templates = templates + Dir["#{templates_path}/**/#{parts.join('_')}*.{png,jpg}"] # ~/.frameit folder
|
19
|
+
|
20
|
+
|
21
|
+
if templates.count == 0
|
22
|
+
if screenshot.screen_size == Deliver::AppScreenshot::ScreenSize::IOS_35
|
23
|
+
Helper.log.warn "Unfortunately 3.5\" device frames were discontinued. Skipping screen '#{screenshot.path}'".yellow
|
24
|
+
Helper.log.debug "Looked for: '#{parts.join('_')}.png'".red
|
25
|
+
else
|
26
|
+
Helper.log.error "Could not find a valid template for screenshot '#{screenshot.path}'".red
|
27
|
+
Helper.log.error "You can download new templates from '#{FrameConverter::DOWNLOAD_URL}'"
|
28
|
+
Helper.log.error "and store them in '#{templates_path}'"
|
29
|
+
Helper.log.error "Missing file: '#{parts.join('_')}.png'".red
|
30
|
+
end
|
31
|
+
return nil
|
32
|
+
else
|
33
|
+
return templates.first.gsub(" ", "\ ")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/frameit/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: frameit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Felix Krause
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fastlane_core
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '>='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.7.2
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '>='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.7.2
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: fastimage
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -137,7 +137,7 @@ dependencies:
|
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: 0.8.7.4
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
140
|
+
name: coveralls
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
142
142
|
requirements:
|
143
143
|
- - '>='
|
@@ -161,10 +161,19 @@ files:
|
|
161
161
|
- LICENSE
|
162
162
|
- README.md
|
163
163
|
- bin/frameit
|
164
|
+
- lib/assets/empty.png
|
164
165
|
- lib/frameit.rb
|
166
|
+
- lib/frameit/config_parser.rb
|
165
167
|
- lib/frameit/dependency_checker.rb
|
168
|
+
- lib/frameit/device_types.rb
|
166
169
|
- lib/frameit/editor.rb
|
167
170
|
- lib/frameit/frame_converter.rb
|
171
|
+
- lib/frameit/mac_editor.rb
|
172
|
+
- lib/frameit/offsets.rb
|
173
|
+
- lib/frameit/runner.rb
|
174
|
+
- lib/frameit/screenshot.rb
|
175
|
+
- lib/frameit/strings_parser.rb
|
176
|
+
- lib/frameit/template_finder.rb
|
168
177
|
- lib/frameit/version.rb
|
169
178
|
homepage: https://fastlane.tools
|
170
179
|
licenses:
|