fastlane-plugin-wpmreleasetoolkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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