fastlane-plugin-wpmreleasetoolkit 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +339 -0
  3. data/README.md +38 -0
  4. data/bin/drawText +19 -0
  5. data/ext/drawText/extconf.rb +36 -0
  6. data/lib/fastlane/plugin/wpmreleasetoolkit.rb +16 -0
  7. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/README.md +20 -0
  8. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb +53 -0
  9. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_update_metadata_source_action.rb +171 -0
  10. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_validate_lib_strings_action.rb +63 -0
  11. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_betabuild_prechecks.rb +103 -0
  12. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_prechecks.rb +83 -0
  13. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_preflight.rb +54 -0
  14. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_beta.rb +69 -0
  15. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_final_release.rb +58 -0
  16. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_hotfix.rb +77 -0
  17. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_release.rb +89 -0
  18. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_codefreeze_prechecks.rb +79 -0
  19. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_completecodefreeze_prechecks.rb +68 -0
  20. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_xml_release_notes.rb +63 -0
  21. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_current_branch_is_hotfix.rb +44 -0
  22. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb +79 -0
  23. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_translations_action.rb +115 -0
  24. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_finalize_prechecks.rb +71 -0
  25. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_alpha_version.rb +44 -0
  26. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_app_version.rb +44 -0
  27. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_release_version.rb +44 -0
  28. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_hotifx_prechecks.rb +78 -0
  29. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_merge_translators_strings.rb +106 -0
  30. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_tag_build.rb +51 -0
  31. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_metadata.rb +52 -0
  32. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_release_notes.rb +56 -0
  33. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/circleci_trigger_job_action.rb +63 -0
  34. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/close_milestone_action.rb +56 -0
  35. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb +59 -0
  36. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_action.rb +91 -0
  37. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/extract_release_notes_for_version_action.rb +89 -0
  38. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_list_action.rb +64 -0
  39. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_downloadmetadata_action.rb +90 -0
  40. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_update_metadata_source.rb +170 -0
  41. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/promo_screenshots_action.rb +247 -0
  42. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/removebranchprotection_action.rb +57 -0
  43. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setbranchprotection_action.rb +56 -0
  44. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setfrozentag_action.rb +81 -0
  45. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_add_files_to_copy_action.rb +96 -0
  46. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_apply_action.rb +139 -0
  47. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_download_action.rb +57 -0
  48. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_setup_action.rb +86 -0
  49. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_update_action.rb +139 -0
  50. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_validate_action.rb +134 -0
  51. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_development_certificates_to_provisioning_profiles.rb +77 -0
  52. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_devices_to_provisioning_profiles.rb +79 -0
  53. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_betabuild_prechecks.rb +92 -0
  54. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_prechecks.rb +74 -0
  55. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_preflight.rb +78 -0
  56. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_beta.rb +68 -0
  57. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_hotfix.rb +87 -0
  58. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_release.rb +114 -0
  59. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_check_beta_deps.rb +62 -0
  60. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_clear_intermediate_tags.rb +60 -0
  61. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_codefreeze_prechecks.rb +70 -0
  62. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_completecodefreeze_prechecks.rb +63 -0
  63. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_current_branch_is_hotfix.rb +40 -0
  64. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_final_tag.rb +52 -0
  65. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_finalize_prechecks.rb +64 -0
  66. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_app_version.rb +47 -0
  67. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_build_version.rb +60 -0
  68. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_store_app_sizes.rb +121 -0
  69. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_hotifx_prechecks.rb +78 -0
  70. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +167 -0
  71. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_localize_project.rb +44 -0
  72. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_translators_strings.rb +93 -0
  73. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_tag_build.rb +44 -0
  74. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata.rb +40 -0
  75. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata_source.rb +81 -0
  76. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_release_notes.rb +56 -0
  77. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_validate_ci_build.rb +46 -0
  78. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/an_metadata_update_helper.rb +152 -0
  79. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_git_helper.rb +44 -0
  80. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +359 -0
  81. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_version_helper.rb +475 -0
  82. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ci_helper.rb +91 -0
  83. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/configure_helper.rb +282 -0
  84. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/encryption_helper.rb +51 -0
  85. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/filesystem_helper.rb +93 -0
  86. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/git_helper.rb +224 -0
  87. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb +135 -0
  88. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_adc_app_sizes_helper.rb +74 -0
  89. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_git_helper.rb +80 -0
  90. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +208 -0
  91. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_version_helper.rb +348 -0
  92. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_download_helper.rb +107 -0
  93. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_update_helper.rb +182 -0
  94. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb +399 -0
  95. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/release_notes_helper.rb +21 -0
  96. data/lib/fastlane/plugin/wpmreleasetoolkit/models/configuration.rb +40 -0
  97. data/lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb +86 -0
  98. data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +5 -0
  99. metadata +449 -0
@@ -0,0 +1,107 @@
1
+ require 'net/http'
2
+ require 'json'
3
+
4
+ module Fastlane
5
+ module Helper
6
+ class MetadataDownloader
7
+ attr_reader :target_folder, :target_files
8
+
9
+ def initialize(target_folder, target_files)
10
+ @target_folder = target_folder
11
+ @target_files = target_files
12
+ @alternates = {}
13
+ end
14
+
15
+ # Downloads data from GlotPress,
16
+ # in JSON format
17
+ def download(target_locale, glotpress_url, is_source)
18
+ uri = URI(glotpress_url)
19
+ response = Net::HTTP.get_response(uri)
20
+ response = Net::HTTP.get_response(URI.parse(response.header['location'])) if response.code == '301'
21
+
22
+ @alternates.clear
23
+ loc_data = JSON.parse(response.body) rescue loc_data = nil
24
+ parse_data(target_locale, loc_data, is_source)
25
+ reparse_alternates(target_locale, loc_data, is_source) unless @alternates.length == 0
26
+ end
27
+
28
+ # Parse JSON data and update the local files
29
+ def parse_data(target_locale, loc_data, is_source)
30
+ delete_existing_metadata(target_locale)
31
+
32
+ if loc_data.nil?
33
+ UI.message "No translation available for #{target_locale}"
34
+ return
35
+ end
36
+
37
+ loc_data.each do |d|
38
+ key = d[0].split(/\u0004/).first
39
+ source = d[0].split(/\u0004/).last
40
+
41
+ target_files.each do |file|
42
+ if file[0].to_s == key
43
+ data = file[1]
44
+ msg = is_source ? source : d[1]
45
+ update_key(target_locale, key, file, data, msg)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ # Parse JSON data and update the local files
52
+ def reparse_alternates(target_locale, loc_data, is_source)
53
+ loc_data.each do |d|
54
+ key = d[0].split(/\u0004/).first
55
+ source = d[0].split(/\u0004/).last
56
+
57
+ @alternates.each do |file|
58
+ puts "Data: #{file[0]} - key: #{key}"
59
+ if file[0].to_s == key
60
+ puts "Alternate: #{key}"
61
+ data = file[1]
62
+ msg = is_source ? source : d[1]
63
+ update_key(target_locale, key, file, data, msg)
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ def update_key(target_locale, key, file, data, msg)
70
+ message_len = msg.to_s.length - 4 # Don't count JSON delimiters.
71
+ if (data.key?(:max_size)) && (data[:max_size] != 0) && ((message_len) > data[:max_size])
72
+ if data.key?(:alternate_key)
73
+ UI.message("#{target_locale} translation for #{key} exceeds maximum length (#{message_len}). Switching to the alternate translation.")
74
+ @alternates[data[:alternate_key]] = { desc: data[:desc], max_size: 0 }
75
+ else
76
+ UI.message("Rejecting #{target_locale} traslation for #{key}: translation length: #{message_len} - max allowed length: #{data[:max_size]}")
77
+ end
78
+ else
79
+ save_metadata(target_locale, file[1][:desc], msg)
80
+ end
81
+ end
82
+
83
+ # Writes the downloaded content
84
+ # to the target file
85
+ def save_metadata(locale, file_name, content)
86
+ file_path = get_target_file_path(locale, file_name)
87
+
88
+ dir_path = File.dirname(file_path)
89
+ FileUtils.mkdir_p(dir_path) unless File.exist?(dir_path)
90
+
91
+ File.open(file_path, 'w') { |file| file.puts(content) }
92
+ end
93
+
94
+ # Some small helpers
95
+ def delete_existing_metadata(target_locale)
96
+ @target_files.each do |file|
97
+ file_path = get_target_file_path(target_locale, file[1][:desc])
98
+ File.delete(file_path) if File.exist? file_path
99
+ end
100
+ end
101
+
102
+ def get_target_file_path(locale, file_name)
103
+ "#{@target_folder}/#{locale}/#{file_name}"
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,182 @@
1
+ module Fastlane
2
+ module Helper
3
+ # Basic line handler
4
+ class MetadataBlock
5
+ attr_reader :block_key
6
+
7
+ def initialize(block_key)
8
+ @block_key = block_key
9
+ end
10
+
11
+ def handle_line(fw, line)
12
+ fw.puts(line) # Standard line handling: just copy
13
+ end
14
+
15
+ def is_handler_for(key)
16
+ true
17
+ end
18
+ end
19
+
20
+ class UnknownMetadataBlock < MetadataBlock
21
+ attr_reader :content_file_path
22
+
23
+ def initialize
24
+ super(nil)
25
+ end
26
+ end
27
+
28
+ class StandardMetadataBlock < MetadataBlock
29
+ attr_reader :content_file_path
30
+
31
+ def initialize(block_key, content_file_path)
32
+ super(block_key)
33
+ @content_file_path = content_file_path
34
+ end
35
+
36
+ def is_handler_for(key)
37
+ key == @block_key.to_s
38
+ end
39
+
40
+ def handle_line(fw, line)
41
+ # put the new content on block start
42
+ # and skip all the other content
43
+ generate_block(fw) if line.start_with?('msgctxt')
44
+ end
45
+
46
+ def generate_block(fw)
47
+ # init
48
+ fw.puts("msgctxt \"#{@block_key}\"")
49
+ line_count = File.foreach(@content_file_path).inject(0) { |c, _line| c + 1 }
50
+
51
+ if line_count <= 1
52
+ # Single line output
53
+ fw.puts("msgid \"#{File.open(@content_file_path, 'r').read}\"")
54
+ else
55
+ # Multiple line output
56
+ fw.puts('msgid ""')
57
+
58
+ # insert content
59
+ sf = File.open(@content_file_path, 'r').to_a
60
+ sf.each do |line|
61
+ l = "\"#{line.strip}"
62
+ l << '\\n' unless line == sf.last
63
+ l << '"'
64
+ fw.puts(l)
65
+ end
66
+ end
67
+
68
+ # close
69
+ fw.puts('msgstr ""')
70
+ fw.puts('')
71
+ end
72
+ end
73
+
74
+ class ReleaseNoteMetadataBlock < StandardMetadataBlock
75
+ attr_reader :new_key, :keep_key, :rel_note_key, :release_version
76
+
77
+ def initialize(block_key, content_file_path, release_version)
78
+ super(block_key, content_file_path)
79
+ @rel_note_key = 'release_note'
80
+ @release_version = release_version
81
+ generate_keys(release_version)
82
+ end
83
+
84
+ def generate_keys(release_version)
85
+ values = release_version.split('.')
86
+ version_major = Integer(values[0])
87
+ version_minor = Integer(values[1])
88
+ @new_key = "#{@rel_note_key}_#{version_major}#{version_minor}"
89
+
90
+ version_major = version_major - 1 if version_minor == 0
91
+ version_minor = version_minor == 0 ? 9 : version_minor - 1
92
+
93
+ @keep_key = "#{@rel_note_key}_#{version_major}#{version_minor}"
94
+ end
95
+
96
+ def is_handler_for(key)
97
+ values = key.split('_')
98
+ key.start_with?(@rel_note_key) && values.length == 3 && (!Integer(values[2]).nil? rescue false)
99
+ end
100
+
101
+ def handle_line(fw, line)
102
+ # put content on block start or if copying the latest one
103
+ # and skip all the other content
104
+ if line.start_with?('msgctxt')
105
+ key = extract_key(line)
106
+ @is_copying = (key == @keep_key)
107
+ generate_block(fw) if @is_copying
108
+ end
109
+
110
+ fw.puts(line) if @is_copying
111
+ end
112
+
113
+ def generate_block(fw)
114
+ # init
115
+ fw.puts("msgctxt \"#{@new_key}\"")
116
+ fw.puts('msgid ""')
117
+ fw.puts("\"#{@release_version}:\\n\"")
118
+
119
+ # insert content
120
+ File.open(@content_file_path, 'r').each do |line|
121
+ fw.puts("\"#{line.strip}\\n\"")
122
+ end
123
+
124
+ # close
125
+ fw.puts('msgstr ""')
126
+ fw.puts('')
127
+ end
128
+
129
+ def extract_key(line)
130
+ line.split(' ')[1].tr('\"', '')
131
+ end
132
+ end
133
+
134
+ class WhatsNewMetadataBlock < StandardMetadataBlock
135
+ attr_reader :new_key, :old_key, :rel_note_key, :release_version
136
+
137
+ def initialize(block_key, content_file_path, release_version)
138
+ super(block_key, content_file_path)
139
+ @rel_note_key = 'whats_new'
140
+ @release_version = release_version
141
+ generate_keys(release_version)
142
+ end
143
+
144
+ def generate_keys(release_version)
145
+ values = release_version.split('.')
146
+ version_major = Integer(values[0])
147
+ version_minor = Integer(values[1])
148
+ @new_key = "v#{release_version}-whats-new"
149
+
150
+ version_major = version_major - 1 if version_minor == 0
151
+ version_minor = version_minor == 0 ? 9 : version_minor - 1
152
+
153
+ @old_key = "v#{version_major}.#{version_minor}-whats-new"
154
+ end
155
+
156
+ def is_handler_for(key)
157
+ key.start_with?('v') && key.end_with?('-whats-new')
158
+ end
159
+
160
+ def handle_line(fw, line)
161
+ # put content on block start or if copying the latest one
162
+ # and skip all the other content
163
+ generate_block(fw) if line.start_with?('msgctxt')
164
+ end
165
+
166
+ def generate_block(fw)
167
+ # init
168
+ fw.puts("msgctxt \"#{@new_key}\"")
169
+ fw.puts('msgid ""')
170
+
171
+ # insert content
172
+ File.open(@content_file_path, 'r').each do |line|
173
+ fw.puts("\"#{line.strip}\\n\"")
174
+ end
175
+
176
+ # close
177
+ fw.puts('msgstr ""')
178
+ fw.puts('')
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,399 @@
1
+ require 'tmpdir'
2
+ begin
3
+ $skip_magick = false
4
+ require 'RMagick'
5
+ rescue LoadError
6
+ $skip_magick = true
7
+ end
8
+ require 'json'
9
+ require 'tempfile'
10
+ require 'optparse'
11
+ require 'pathname'
12
+ require 'progress_bar'
13
+ require 'parallel'
14
+ require 'jsonlint'
15
+ require 'chroma'
16
+ require 'securerandom'
17
+
18
+ include Magick unless $skip_magick
19
+
20
+ module Fastlane
21
+ module Helper
22
+ class PromoScreenshots
23
+ def initialize
24
+ if $skip_magick
25
+ message = "PromoScreenshots feature is currently disabled.\n"
26
+ message << "Please, install RMagick if you aim to generate the PromoScreenshots.\n"
27
+ message << "\'bundle install --with screenshots\' should do it if your project is configured for PromoScreenshots.\n"
28
+ message << 'Aborting.'
29
+ UI.user_error!(message)
30
+ end
31
+ end
32
+
33
+ def read_json(configFilePath)
34
+ configFilePath = resolve_path(configFilePath)
35
+
36
+ begin
37
+ return JSON.parse(open(configFilePath).read)
38
+ rescue
39
+ linter = JsonLint::Linter.new
40
+ linter.check(configFilePath)
41
+ linter.display_errors
42
+
43
+ UI.user_error!('Invalid JSON configuration. See errors in log.')
44
+ end
45
+ end
46
+
47
+ def draw_caption_to_canvas(entry, canvas, device, stylesheet_path = '')
48
+ # If no caption is provided, it's ok to skip the body of this method
49
+ return canvas if entry['text'].nil?
50
+
51
+ text = entry['text']
52
+ text_size = device['text_size']
53
+ font_size = device['font_size']
54
+ locale = entry['locale']
55
+
56
+ text = resolve_text_into_path(text, locale)
57
+
58
+ stylesheet_path = resolve_path(stylesheet_path) if can_resolve_path(stylesheet_path)
59
+
60
+ width = text_size[0]
61
+ height = text_size[1]
62
+
63
+ x_position = 0
64
+ y_position = 0
65
+
66
+ unless device['text_offset'].nil?
67
+ x_position = device['text_offset'][0]
68
+ y_position = device['text_offset'][1]
69
+ end
70
+
71
+ draw_text_to_canvas(canvas,
72
+ text,
73
+ width,
74
+ height,
75
+ x_position,
76
+ y_position,
77
+ font_size,
78
+ stylesheet_path)
79
+ end
80
+
81
+ def draw_background_to_canvas(canvas, entry)
82
+ unless entry['background'].nil?
83
+
84
+ # If we're passed an image path, let's open it and paint it to the canvas
85
+ if can_resolve_path(entry['background'])
86
+ background_image = open_image(entry['background'])
87
+ return composite_image(canvas, background_image, 0, 0)
88
+ else # Otherwise, let's assume this is a colour code
89
+ background_image = create_image(canvas.columns, canvas.rows, entry['background'])
90
+ canvas = composite_image(canvas, background_image, 0, 0)
91
+ end
92
+ end
93
+
94
+ canvas
95
+ end
96
+
97
+ def draw_device_frame_to_canvas(device, canvas)
98
+ # Apply the device frame to the canvas, but only if one is provided
99
+ return canvas if device['device_frame_size'].nil?
100
+
101
+ w = device['device_frame_size'][0]
102
+ h = device['device_frame_size'][1]
103
+
104
+ x = 0
105
+ y = 0
106
+
107
+ unless device['device_frame_size'].nil?
108
+ x = device['device_frame_offset'][0]
109
+ y = device['device_frame_offset'][1]
110
+ end
111
+
112
+ device_frame = open_image(device['device_frame'])
113
+ device_frame = resize_image(device_frame, w, h)
114
+ composite_image(canvas, device_frame, x, y)
115
+ end
116
+
117
+ def draw_screenshot_to_canvas(entry, canvas, device)
118
+ # Don't require a screenshot to be present – we can just skip
119
+ # this function if one doesn't exist.
120
+ return canvas if entry['screenshot'].nil?
121
+
122
+ device_mask = device['screenshot_mask']
123
+ screenshot_size = device['screenshot_size']
124
+ screenshot_offset = device['screenshot_offset']
125
+
126
+ screenshot = entry['screenshot']
127
+
128
+ screenshot = open_image(screenshot)
129
+
130
+ screenshot = mask_image(screenshot, open_image(device_mask)) unless device_mask.nil?
131
+
132
+ screenshot = resize_image(screenshot, screenshot_size[0], screenshot_size[1])
133
+ composite_image(canvas, screenshot, screenshot_offset[0], screenshot_offset[1])
134
+ end
135
+
136
+ def draw_attachments_to_canvas(entry, canvas)
137
+ entry['attachments'].each do |attachment|
138
+ if !attachment['file'].nil?
139
+ canvas = draw_file_attachment_to_canvas(attachment, canvas, entry)
140
+ elsif !attachment['text'].nil?
141
+ canvas = draw_text_attachment_to_canvas(attachment, canvas, entry['locale'])
142
+ end
143
+ end
144
+
145
+ return canvas
146
+ end
147
+
148
+ def draw_file_attachment_to_canvas(attachment, canvas, entry)
149
+ file = resolve_path(attachment['file'])
150
+
151
+ image = open_image(file)
152
+
153
+ if attachment.member?('operations')
154
+
155
+ attachment['operations'].each do |operation|
156
+ image = apply_operation(image, operation, canvas)
157
+ end
158
+
159
+ end
160
+
161
+ size = attachment['size']
162
+
163
+ x_pos = attachment['position'][0]
164
+ y_pos = attachment['position'][1]
165
+
166
+ unless attachment['offset'].nil?
167
+ x_pos += attachment['offset'][0]
168
+ y_pos += attachment['offset'][1]
169
+ end
170
+
171
+ image = resize_image(image, size[0], size[1])
172
+ canvas = composite_image(canvas, image, x_pos, y_pos)
173
+ end
174
+
175
+ def draw_text_attachment_to_canvas(attachment, canvas, locale)
176
+ text = resolve_text_into_path(attachment['text'], locale)
177
+ font_size = attachment['font-size'] ||= 12
178
+
179
+ width = attachment['size'][0]
180
+ height = attachment['size'][1]
181
+
182
+ x_position = attachment['position'][0] ||= 0
183
+ y_position = attachment['position'][1] ||= 0
184
+
185
+ stylesheet_path = attachment['stylesheet']
186
+ stylesheet_path = resolve_path(stylesheet_path) if can_resolve_path(stylesheet_path)
187
+
188
+ alignment = attachment['alignment'] ||= 'center'
189
+
190
+ draw_text_to_canvas(canvas,
191
+ text,
192
+ width,
193
+ height,
194
+ x_position,
195
+ y_position,
196
+ font_size,
197
+ stylesheet_path,
198
+ alignment)
199
+ end
200
+
201
+ def apply_operation(image, operation, canvas)
202
+ case operation['type']
203
+ when 'crop'
204
+ x_pos = operation['at'][0]
205
+ y_pos = operation['at'][1]
206
+
207
+ width = operation['to'][0]
208
+ height = operation['to'][1]
209
+
210
+ crop_image(image, x_pos, y_pos, width, height)
211
+
212
+ when 'resize'
213
+ width = operation['to'][0]
214
+ height = operation['to'][1]
215
+
216
+ resize_image(image, width, height)
217
+
218
+ when 'composite'
219
+
220
+ x_pos = operation['at'][0]
221
+ y_pos = operation['at'][1]
222
+
223
+ if operation.member?('offset')
224
+ x_pos += operation['offset'][0]
225
+ y_pos += operation['offset'][1]
226
+ end
227
+
228
+ composite_image(canvas, image, x_pos, y_pos)
229
+ end
230
+ end
231
+
232
+ def draw_text_to_canvas(canvas, text, width, height, x_position, y_position, font_size, stylesheet_path, position = 'center')
233
+ begin
234
+ tempTextFile = Tempfile.new()
235
+
236
+ command = "bundle exec drawText html=\"#{text}\" maxWidth=#{width} maxHeight=#{height} output=#{tempTextFile.path} fontSize=#{font_size} stylesheet=\"#{stylesheet_path}\" alignment=\"#{position}\""
237
+
238
+ UI.crash!('Unable to draw text') unless system(command)
239
+
240
+ text_content = open_image(tempTextFile.path).trim
241
+ text_frame = create_image(width, height)
242
+ text_frame = case position
243
+ when 'left' then composite_image_left(text_frame, text_content, 0, 0)
244
+ when 'center' then composite_image_center(text_frame, text_content, 0, 0)
245
+ when 'top' then composite_image_top(text_frame, text_content, 0, 0)
246
+ end
247
+ ensure
248
+ tempTextFile.close
249
+ tempTextFile.unlink
250
+ end
251
+
252
+ composite_image(canvas, text_frame, x_position, y_position)
253
+ end
254
+
255
+ # mask_image
256
+ #
257
+ # @example
258
+ #
259
+ # image = open_image("image-path")
260
+ # mask = open_image("mask-path")
261
+ #
262
+ # mask_image(image, mask)
263
+ #
264
+ # @param [Magick::Image] image An ImageMagick object containing the image to be masked.
265
+ # @param [Magick::Image] mask An ImageMagick object containing the mask to be be applied.
266
+ #
267
+ # @return [Magick::Image] The masked image
268
+ def mask_image(image, mask, offset_x = 0, offset_y = 0)
269
+ image.composite(mask, offset_x, offset_y, CopyAlphaCompositeOp)
270
+ end
271
+
272
+ # resize_image
273
+ #
274
+ # @example
275
+ #
276
+ # image = open_image("image-path")
277
+ # resize_image(image, 640, 480)
278
+ #
279
+ # @param [Magick::Image] original An ImageMagick object containing the image to be masked.
280
+ # @param [Integer] width The new width for the image.
281
+ # @param [Integer] height The new height for the image.
282
+ #
283
+ # @return [Magick::Image] The resized image
284
+ def resize_image(original, width, height)
285
+ UI.user_error!('You must pass an image object to `resize_image`.') unless original.is_a?(Magick::Image)
286
+
287
+ original.adaptive_resize(width, height)
288
+ end
289
+
290
+ # composite_image
291
+ #
292
+ # @example
293
+ #
294
+ # image = open_image("image-path")
295
+ # other = open_image("other-path")
296
+ # composite_image(image, other, 0, 0)
297
+ #
298
+ # @param [Magick::Image] original The original image.
299
+ # @param [Magick::Image] child The image that will be placed onto the original image.
300
+ # @param [Integer] x_position The horizontal position for the image to be placed.
301
+ # @param [Integer] y_position The vertical position for the image to be placed.
302
+ #
303
+ # @return [Magick::Image] The resized image
304
+ def composite_image(original, child, x_position, y_position, starting_position = NorthWestGravity)
305
+ UI.user_error!('You must pass an image object as the first argument to `composite_image`.') unless original.is_a?(Magick::Image)
306
+
307
+ UI.user_error!('You must pass an image object as the second argument to `composite_image`.') unless child.is_a?(Magick::Image)
308
+
309
+ original.composite(child, starting_position, x_position, y_position, Magick::OverCompositeOp)
310
+ end
311
+
312
+ def composite_image_top(original, child, x_position, y_position)
313
+ composite_image(original, child, x_position, y_position, NorthGravity)
314
+ end
315
+
316
+ def composite_image_left(original, child, x_position, y_position)
317
+ composite_image(original, child, x_position, y_position, WestGravity)
318
+ end
319
+
320
+ def composite_image_center(original, child, x_position, y_position)
321
+ composite_image(original, child, x_position, y_position, CenterGravity)
322
+ end
323
+
324
+ # crop_image
325
+ #
326
+ # @example
327
+ #
328
+ # image = open_image("image-path")
329
+ # crop_image(image, other, 0, 0)
330
+ #
331
+ # @param [Magick::Image] original The original image.
332
+ # @param [Integer] x_position The horizontal position to start cropping from.
333
+ # @param [Integer] y_position The vertical position to start cropping from.
334
+ # @param [Integer] width The width of the final image.
335
+ # @param [Integer] height The height of the final image.
336
+ #
337
+ # @return [Magick::Image] The resized image
338
+ def crop_image(original, x_position, y_position, width, height)
339
+ UI.user_error!('You must pass an image object to `crop_image`.') unless original.is_a?(Magick::Image)
340
+
341
+ original.crop(x_position, y_position, width, height)
342
+ end
343
+
344
+ def open_image(path)
345
+ path = resolve_path(path)
346
+
347
+ Magick::Image.read(path) do
348
+ self.background_color = 'transparent'
349
+ end.first
350
+ end
351
+
352
+ def create_image(width, height, background = 'transparent')
353
+ background_color = background.paint.to_hex
354
+
355
+ Image.new(width, height) do
356
+ self.background_color = background
357
+ end
358
+ end
359
+
360
+ def can_resolve_path(path)
361
+ begin
362
+ resolve_path(path)
363
+ return true
364
+ rescue
365
+ return false
366
+ end
367
+ end
368
+
369
+ def resolve_path(path)
370
+ UI.crash!('Path not provided – you must provide one to continue') if path.nil?
371
+
372
+ [
373
+ Pathname.new(path), # Absolute Path
374
+ Pathname.new(FastlaneCore::FastlaneFolder.fastfile_path).dirname + path, # Path Relative to the fastfile
375
+ Fastlane::Helper::FilesystemHelper.plugin_root + path, # Path Relative to the plugin
376
+ Fastlane::Helper::FilesystemHelper.plugin_root + 'spec/test-data/' + path, # Path Relative to the test data
377
+ ]
378
+ .each do |resolved_path|
379
+ return resolved_path if !resolved_path.nil? && resolved_path.exist?
380
+ end
381
+
382
+ message = "Unable to locate #{path}"
383
+ UI.crash!(message)
384
+ end
385
+
386
+ def resolve_text_into_path(text, locale)
387
+ localizedFile = format(text, locale)
388
+
389
+ text = if File.exist?(localizedFile)
390
+ localizedFile
391
+ elsif can_resolve_path(localizedFile)
392
+ resolve_path(localizedFile).realpath.to_s
393
+ else
394
+ format(text, 'source')
395
+ end
396
+ end
397
+ end
398
+ end
399
+ end