images-convert 0.4.1

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
  4. data/.github/PULL_REQUEST_TEMPLATE.md +25 -0
  5. data/.github/workflows/ci.yml +79 -0
  6. data/.github/workflows/release.yml +122 -0
  7. data/.gitignore +14 -0
  8. data/CHANGELOG.md +146 -0
  9. data/Gemfile +9 -0
  10. data/LICENSE +41 -0
  11. data/README.md +378 -0
  12. data/RELEASE_NOTES_v0.2.12.md +66 -0
  13. data/RELEASE_NOTES_v0.2.3.md +19 -0
  14. data/RELEASE_NOTES_v0.3.0.md +66 -0
  15. data/RELEASE_NOTES_v0.4.0.md +14 -0
  16. data/RELEASE_NOTES_v0.4.1.md +13 -0
  17. data/Rakefile +13 -0
  18. data/bin/images-convert +8 -0
  19. data/bin/imgconv +8 -0
  20. data/images-convert.gemspec +39 -0
  21. data/lib/images_convert/cleanup.rb +31 -0
  22. data/lib/images_convert/configuration.rb +303 -0
  23. data/lib/images_convert/mini_magick_stub.rb +121 -0
  24. data/lib/images_convert/version.rb +5 -0
  25. data/lib/images_convert/waifu2x_test_stub.rb +93 -0
  26. data/lib/images_convert.rb +1557 -0
  27. data/lib/rubygems_plugin.rb +34 -0
  28. data/lib/waifu2x/downloader.rb +89 -0
  29. data/lib/waifu2x/pdf_builder.rb +105 -0
  30. data/lib/waifu2x/processor.rb +301 -0
  31. data/lib/waifu2x/setup.rb +127 -0
  32. data/lib/waifu2x/version.rb +5 -0
  33. data/lib/waifu2x.rb +221 -0
  34. data/test/images/autumn.jpg +0 -0
  35. data/test/images/spring.jpg +0 -0
  36. data/test/images/summer.jpg +0 -0
  37. data/test/images/winter.jpg +0 -0
  38. data/test/support/waifu2x_test_stub.rb +91 -0
  39. data/test/test_config.rb +143 -0
  40. data/test/test_fixtures.rb +144 -0
  41. data/test/test_formats.rb +213 -0
  42. data/test/test_help.rb +33 -0
  43. data/test/test_helper.rb +17 -0
  44. data/test/test_helper_mini_magick_stub.rb +4 -0
  45. data/test/test_selection.rb +142 -0
  46. data/test/test_version.rb +16 -0
  47. data/test/test_waifu2x.rb +81 -0
  48. metadata +179 -0
data/lib/waifu2x.rb ADDED
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+ require_relative 'waifu2x/version'
6
+ require_relative 'waifu2x/processor'
7
+ require_relative 'waifu2x/pdf_builder'
8
+ require_relative 'waifu2x/setup'
9
+ require_relative 'images_convert/waifu2x_test_stub'
10
+
11
+ module Waifu2x
12
+ class Error < StandardError; end
13
+
14
+ def self.process(input_dir, output_pdf, **options)
15
+ temp_dir = Dir.mktmpdir('waifu2x-pdf')
16
+
17
+ begin
18
+ output_mode = (options[:output_mode] || 'pdf').to_s
19
+ output_images_root = options[:output_dir]
20
+ pdf_override = options[:output_pdf]
21
+
22
+ recursive = options.key?(:recursive) ? options[:recursive] : false
23
+ pattern = recursive ? File.join(input_dir, '**', '*.{jpg,jpeg,png,gif,webp}') : File.join(input_dir, '*.{jpg,jpeg,png,gif,webp}')
24
+ input_images = Dir.glob(pattern)
25
+ if input_images.empty?
26
+ raise "入力ディレクトリに対象画像が見つかりませんでした: #{input_dir} (対応拡張子: jpg, jpeg, png, gif, webp)"
27
+ end
28
+
29
+ resolved = Setup.resolve_paths(
30
+ waifu2x_bin: options[:waifu2x_bin],
31
+ models_path: options[:models_path],
32
+ auto_download: options[:auto_download],
33
+ download_url: options[:download_url],
34
+ model: options[:model]
35
+ )
36
+
37
+ total_images_sec = 0.0
38
+ total_pdf_sec = 0.0
39
+
40
+ if recursive && output_pdf.nil?
41
+ dirs = input_images.map { |p| File.dirname(p) }.uniq.sort
42
+ dirs.each do |dir|
43
+ if output_mode == 'images' || output_mode == 'both'
44
+ out_dir = if output_images_root
45
+ File.join(output_images_root, File.basename(dir))
46
+ else
47
+ File.join(File.dirname(dir), "#{File.basename(dir)} 高解像度")
48
+ end
49
+ FileUtils.mkdir_p(out_dir)
50
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
51
+ Processor.process_images(
52
+ dir,
53
+ out_dir,
54
+ scale: options[:scale] || 2,
55
+ noise: options[:noise] || 1,
56
+ model: options[:model] || 'anime_style_art_rgb',
57
+ processor: options[:processor] || :waifu2x_ncnn_vulkan,
58
+ waifu2x_bin: resolved[:bin],
59
+ models_path: resolved[:models],
60
+ recursive: false,
61
+ auto_scale_a4: options[:auto_scale_a4],
62
+ pdf_density: options[:pdf_density],
63
+ verbose: options[:verbose],
64
+ image_format: options[:image_format],
65
+ on_error: options[:on_error]
66
+ )
67
+ images_sec = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
68
+ total_images_sec += images_sec
69
+ puts format('%s done in %.2fs: %s', 'Images'.ljust(6), images_sec, out_dir)
70
+ end
71
+
72
+ if %w[images both].include?(output_mode) && (options[:image_format].to_s.downcase == 'webp')
73
+ Dir.glob(File.join(out_dir, '*.png')).each do |png|
74
+ begin
75
+ File.delete(png)
76
+ rescue StandardError
77
+ end
78
+ end
79
+ end
80
+
81
+ if output_mode == 'pdf' || output_mode == 'both'
82
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
83
+ page_images = nil
84
+ if output_mode == 'both'
85
+ if options[:image_format].to_s.downcase == 'webp'
86
+ page_images = Dir.glob(File.join(out_dir, '*.webp')).sort
87
+ else
88
+ exts = %w[png jpg jpeg webp]
89
+ page_images = exts.flat_map { |e| Dir.glob(File.join(out_dir, "*.#{e}")) }.sort
90
+ end
91
+ else
92
+ Dir.mktmpdir('waifu2x-pdf-dir') do |dir_temp|
93
+ Processor.process_images(
94
+ dir,
95
+ dir_temp,
96
+ scale: options[:scale] || 2,
97
+ noise: options[:noise] || 1,
98
+ model: options[:model] || 'anime_style_art_rgb',
99
+ processor: options[:processor] || :waifu2x_ncnn_vulkan,
100
+ waifu2x_bin: resolved[:bin],
101
+ models_path: resolved[:models],
102
+ recursive: false,
103
+ auto_scale_a4: options[:auto_scale_a4],
104
+ pdf_density: options[:pdf_density],
105
+ verbose: options[:verbose],
106
+ image_format: 'png',
107
+ on_error: options[:on_error]
108
+ )
109
+ page_images = Dir.glob(File.join(dir_temp, '*.png')).sort
110
+ end
111
+ end
112
+
113
+ next if page_images.nil? || page_images.empty?
114
+
115
+ out_path = pdf_override || output_pdf || File.join(File.dirname(dir), "#{File.basename(dir)}.pdf")
116
+ PDFBuilder.create_pdf(
117
+ page_images,
118
+ out_path,
119
+ pdf_compression: options[:pdf_compression],
120
+ pdf_quality: options[:pdf_quality],
121
+ pdf_density: options[:pdf_density],
122
+ a4_print: options[:a4_print]
123
+ )
124
+ pdf_sec = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
125
+ total_pdf_sec += pdf_sec
126
+ puts format('%s done in %.2fs: %s', 'PDF'.ljust(6), pdf_sec, out_path)
127
+ end
128
+ end
129
+ else
130
+ if output_mode == 'images' || output_mode == 'both'
131
+ out_dir = output_images_root || File.join(File.dirname(File.expand_path(input_dir)), "#{File.basename(File.expand_path(input_dir))} 高解像度")
132
+ FileUtils.mkdir_p(out_dir)
133
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
134
+ Processor.process_images(
135
+ input_dir,
136
+ out_dir,
137
+ scale: options[:scale] || 2,
138
+ noise: options[:noise] || 1,
139
+ model: options[:model] || 'anime_style_art_rgb',
140
+ processor: options[:processor] || :waifu2x_ncnn_vulkan,
141
+ waifu2x_bin: resolved[:bin],
142
+ models_path: resolved[:models],
143
+ recursive: recursive,
144
+ auto_scale_a4: options[:auto_scale_a4],
145
+ pdf_density: options[:pdf_density],
146
+ verbose: options[:verbose],
147
+ image_format: options[:image_format],
148
+ on_error: options[:on_error]
149
+ )
150
+ images_sec = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
151
+ total_images_sec += images_sec
152
+ puts format('%s done in %.2fs: %s', 'Images'.ljust(6), images_sec, out_dir)
153
+ end
154
+
155
+ if %w[images both].include?(output_mode) && (options[:image_format].to_s.downcase == 'webp')
156
+ Dir.glob(File.join(out_dir, '*.png')).each do |png|
157
+ begin
158
+ File.delete(png)
159
+ rescue StandardError
160
+ end
161
+ end
162
+ end
163
+
164
+ if output_mode == 'pdf' || output_mode == 'both'
165
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
166
+ if output_mode == 'both'
167
+ if options[:image_format].to_s.downcase == 'webp'
168
+ image_paths = Dir.glob(File.join(out_dir, '*.webp')).sort
169
+ else
170
+ exts = %w[png jpg jpeg webp]
171
+ image_paths = exts.flat_map { |e| Dir.glob(File.join(out_dir, "*.#{e}")) }.sort
172
+ end
173
+ else
174
+ Processor.process_images(
175
+ input_dir,
176
+ temp_dir,
177
+ scale: options[:scale] || 2,
178
+ noise: options[:noise] || 1,
179
+ model: options[:model] || 'anime_style_art_rgb',
180
+ processor: options[:processor] || :waifu2x_ncnn_vulkan,
181
+ waifu2x_bin: resolved[:bin],
182
+ models_path: resolved[:models],
183
+ recursive: recursive,
184
+ auto_scale_a4: options[:auto_scale_a4],
185
+ pdf_density: options[:pdf_density],
186
+ verbose: options[:verbose],
187
+ image_format: 'png',
188
+ on_error: options[:on_error]
189
+ )
190
+ image_paths = Dir.glob(File.join(temp_dir, '*.png')).sort
191
+ end
192
+
193
+ if image_paths.empty?
194
+ raise '画像処理の結果が得られませんでした(出力画像が0件)。入力画像やwaifu2xの動作をご確認ください。'
195
+ end
196
+ pdf_destination = pdf_override || output_pdf || File.join(File.dirname(File.expand_path(input_dir)), "#{File.basename(File.expand_path(input_dir))}.pdf")
197
+ PDFBuilder.create_pdf(
198
+ image_paths,
199
+ pdf_destination,
200
+ pdf_compression: options[:pdf_compression],
201
+ pdf_quality: options[:pdf_quality],
202
+ pdf_density: options[:pdf_density],
203
+ a4_print: options[:a4_print]
204
+ )
205
+
206
+ if File.exist?(pdf_destination)
207
+ pdf_sec = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
208
+ total_pdf_sec += pdf_sec
209
+ puts format('%s done in %.2fs: %s', 'PDF'.ljust(6), pdf_sec, pdf_destination)
210
+ else
211
+ raise "PDFの作成に失敗しました: #{pdf_destination}"
212
+ end
213
+ end
214
+ end
215
+ ensure
216
+ puts format('Total %s time: %.2fs', 'images', total_images_sec) if total_images_sec > 0.0
217
+ puts format('Total %s time: %.2fs', 'PDF'.ljust(6), total_pdf_sec) if total_pdf_sec > 0.0
218
+ FileUtils.remove_entry(temp_dir) if Dir.exist?(temp_dir)
219
+ end
220
+ end
221
+ end
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module Waifu2x
7
+ module TestInstrumentation
8
+ class << self
9
+ def enabled?
10
+ path = ENV['IMGCONV_TEST_WAIFU2X_CALL_LOG']
11
+ path && !path.empty?
12
+ end
13
+
14
+ def log(payload)
15
+ path = ENV['IMGCONV_TEST_WAIFU2X_CALL_LOG']
16
+ return unless path && !path.empty?
17
+
18
+ FileUtils.mkdir_p(File.dirname(path))
19
+ File.open(path, 'a') do |f|
20
+ f.write(JSON.dump(payload))
21
+ f.write("\n")
22
+ end
23
+ end
24
+
25
+ def fake_paths
26
+ {
27
+ bin: File.expand_path('waifu2x-ncnn-vulkan', Dir.tmpdir),
28
+ models: File.expand_path('waifu2x-models', Dir.tmpdir)
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ if defined?(Waifu2x::Processor)
36
+ class << Waifu2x::Processor
37
+ unless method_defined?(:process_images_without_test_stub)
38
+ alias_method :process_images_without_test_stub, :process_images
39
+ end
40
+
41
+ def process_images(input_dir, output_dir, **options)
42
+ if Waifu2x::TestInstrumentation.enabled?
43
+ FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
44
+ format = options[:image_format].to_s
45
+ format = 'png' if format.empty?
46
+ stub_path = File.join(output_dir, "waifu2x_stub_output.#{format}")
47
+ File.write(stub_path, 'waifu2x-stub')
48
+ Waifu2x::TestInstrumentation.log({ input_dir: input_dir, output_dir: output_dir, options: options })
49
+ return
50
+ end
51
+
52
+ process_images_without_test_stub(input_dir, output_dir, **options)
53
+ end
54
+ end
55
+ end
56
+
57
+ if defined?(Waifu2x::Setup)
58
+ class << Waifu2x::Setup
59
+ unless method_defined?(:resolve_paths_without_test_stub)
60
+ alias_method :resolve_paths_without_test_stub, :resolve_paths
61
+ end
62
+
63
+ def resolve_paths(**options)
64
+ if Waifu2x::TestInstrumentation.enabled?
65
+ fake = Waifu2x::TestInstrumentation.fake_paths
66
+ return { bin: fake[:bin], models: fake[:models] }
67
+ end
68
+
69
+ resolve_paths_without_test_stub(**options)
70
+ end
71
+ end
72
+ end
73
+
74
+ if defined?(Waifu2x::PDFBuilder)
75
+ class << Waifu2x::PDFBuilder
76
+ unless method_defined?(:create_pdf_without_test_stub)
77
+ alias_method :create_pdf_without_test_stub, :create_pdf
78
+ end
79
+
80
+ def create_pdf(image_paths, output_path, options = {})
81
+ if Waifu2x::TestInstrumentation.enabled?
82
+ FileUtils.mkdir_p(File.dirname(output_path))
83
+ File.write(output_path, "waifu2x-pdf-stub\n")
84
+ Waifu2x::TestInstrumentation.log({ pdf_output: output_path, images: image_paths, options: options })
85
+ return
86
+ end
87
+
88
+ create_pdf_without_test_stub(image_paths, output_path, options)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ # 概要: config 動作のスモークテスト
4
+ # 目的:
5
+ # - 初回実行で ~/.images-convert/config.yml が作成されること
6
+ # - `config set JPG WebP 1024x` の反映(from/to/resize)
7
+ # - `config set ... 50%` のような百分率リサイズも受け入れること
8
+ # - `cleanup` 実行で設定ディレクトリが削除されること
9
+
10
+ require_relative './test_helper'
11
+ require 'open3'
12
+ require 'fileutils'
13
+
14
+ class CLIConfigTest < Minitest::Test
15
+ ROOT = File.expand_path('..', __dir__)
16
+ BIN = File.join(ROOT, 'bin', 'imgconv')
17
+ LIB = File.join(ROOT, 'lib')
18
+
19
+ def run_cmd_env(env, *args)
20
+ cmd = ['ruby', '-I', LIB, BIN] + args
21
+ stdout, stderr, status = Open3.capture3(env, *cmd)
22
+ [stdout, stderr, status]
23
+ end
24
+
25
+ def with_isolated_home
26
+ Dir.mktmpdir do |home|
27
+ env = { 'HOME' => home }
28
+ yield env
29
+ end
30
+ end
31
+
32
+ # 1) config.yml が存在すること(config 実行で作成)
33
+ def test_config_creates_config_yaml
34
+ with_isolated_home do |env|
35
+ stdout, stderr, status = run_cmd_env(env, 'config')
36
+ assert status.success?, "config failed: #{status.exitstatus}, stderr=\n#{stderr}\nstdout=\n#{stdout}"
37
+ cfg = File.join(env['HOME'], '.images-convert', 'config.yml')
38
+ assert File.exist?(cfg), "expected config file to exist: #{cfg}"
39
+ end
40
+ end
41
+
42
+ # 2) set JPG WebP 1024x の反映
43
+ # 3) 1024x がサイズとして表示されること
44
+ def test_config_set_jpg_webp_1024x_reflected
45
+ with_isolated_home do |env|
46
+ _o1, e1, s1 = run_cmd_env(env, 'config', 'set', 'JPG', 'WebP', '1024x')
47
+ assert s1.success?, "config set failed: #{e1}"
48
+ stdout, stderr, status = run_cmd_env(env, 'config')
49
+ assert status.success?, "config failed: #{status.exitstatus}, stderr=\n#{stderr}"
50
+ assert_includes stdout, 'デフォルト入力形式: JPG', stdout
51
+ assert_includes stdout, 'デフォルト出力形式: WebP', stdout
52
+ assert_includes stdout, 'デフォルトリサイズ: 1024x', stdout
53
+ end
54
+ end
55
+
56
+ # 4) 50% の指定も受け入れること
57
+ def test_config_set_resize_percent_accepted
58
+ with_isolated_home do |env|
59
+ _o1, e1, s1 = run_cmd_env(env, 'config', 'set', 'JPG', 'JPG', '50%')
60
+ assert s1.success?, "config set failed: #{e1}"
61
+ stdout, stderr, status = run_cmd_env(env, 'config')
62
+ assert status.success?, "config failed: #{status.exitstatus}, stderr=\n#{stderr}"
63
+ assert_includes stdout, 'デフォルトリサイズ: 50%', stdout
64
+ end
65
+ end
66
+
67
+ def test_config_set_latest_mode_exif
68
+ with_isolated_home do |env|
69
+ stdout, stderr, status = run_cmd_env(env, 'config', 'set-latest-mode', 'exif')
70
+ assert status.success?, "set-latest-mode failed: #{status.exitstatus}, stderr=\n#{stderr}\nstdout=\n#{stdout}"
71
+
72
+ stdout_config, stderr_config, status_config = run_cmd_env(env, 'config')
73
+ assert status_config.success?, "config failed: #{status_config.exitstatus}, stderr=\n#{stderr_config}"
74
+ assert_includes stdout_config, '最新判定モード: exif', stdout_config
75
+ end
76
+ end
77
+
78
+ def test_config_set_latest_mode_invalid
79
+ with_isolated_home do |env|
80
+ stdout, stderr, status = run_cmd_env(env, 'config', 'set-latest-mode', 'invalid')
81
+ refute status.success?, 'expected set-latest-mode invalid to fail'
82
+ combined = stdout + stderr
83
+ assert_includes combined, 'MODE には mtime または exif', combined
84
+ end
85
+ end
86
+
87
+ def test_config_reset_restores_defaults
88
+ with_isolated_home do |env|
89
+ _, _, status = run_cmd_env(env, 'config', 'set', 'JPG', 'WebP', '1024x')
90
+ assert status.success?, 'config set should succeed before reset'
91
+
92
+ stdout_reset, stderr_reset, status_reset = run_cmd_env(env, 'config', 'reset', '--force')
93
+ assert status_reset.success?, "config reset failed: #{stderr_reset}\nstdout=\n#{stdout_reset}"
94
+ assert_includes stdout_reset, '設定を既定値にリセットしました。', stdout_reset
95
+ assert_includes stdout_reset, 'デフォルト出力形式: JPG', stdout_reset
96
+ assert_includes stdout_reset, 'デフォルトリサイズ: 1920x', stdout_reset
97
+ end
98
+ end
99
+
100
+ def test_config_set_waifu2x_updates_settings
101
+ with_isolated_home do |env|
102
+ stdout, stderr, status = run_cmd_env(
103
+ env,
104
+ 'config',
105
+ 'set-waifu2x',
106
+ '--scale', '4',
107
+ '--noise', '3',
108
+ '--model', 'anime',
109
+ '--no-auto-scale-a4',
110
+ '--no-a4-print',
111
+ '--image_format', 'png',
112
+ '--on_error', 'skip'
113
+ )
114
+ assert status.success?, "set-waifu2x failed: #{status.exitstatus}, stderr=\n#{stderr}\nstdout=\n#{stdout}"
115
+
116
+ stdout_config, stderr_config, status_config = run_cmd_env(env, 'config')
117
+ assert status_config.success?, "config failed: #{status_config.exitstatus}, stderr=\n#{stderr_config}"
118
+ assert_includes stdout_config, 'waifu2x 既定倍率: 4', stdout_config
119
+ assert_includes stdout_config, 'waifu2x ノイズ除去: 3', stdout_config
120
+ assert_includes stdout_config, 'waifu2x モデル: anime', stdout_config
121
+ assert_includes stdout_config, 'waifu2x A4自動スケール: false', stdout_config
122
+ assert_includes stdout_config, 'waifu2x A4レイアウトPDF: false', stdout_config
123
+ assert_includes stdout_config, 'waifu2x 出力フォーマット: png', stdout_config
124
+ assert_includes stdout_config, 'waifu2x エラー時動作: skip', stdout_config
125
+ end
126
+ end
127
+
128
+ # 5) cleanup 時には、config.yml が消されること
129
+ def test_cleanup_removes_config_dir
130
+ with_isolated_home do |env|
131
+ # まず config で作成
132
+ _o1, e1, s1 = run_cmd_env(env, 'config')
133
+ assert s1.success?, "config failed: #{e1}"
134
+ cfg_dir = File.join(env['HOME'], '.images-convert')
135
+ assert Dir.exist?(cfg_dir), 'config dir expected to exist before cleanup'
136
+
137
+ # cleanup 実行
138
+ stdout, stderr, status = run_cmd_env(env, 'cleanup')
139
+ assert status.success?, "cleanup failed: #{status.exitstatus}, stderr=\n#{stderr}\nstdout=\n#{stdout}"
140
+ refute Dir.exist?(cfg_dir), 'config dir should be removed after cleanup'
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './test_helper_mini_magick_stub'
4
+ require 'open3'
5
+ require 'tmpdir'
6
+ require 'fileutils'
7
+
8
+ class CLIFixturesTest < Minitest::Test
9
+ ROOT = File.expand_path('..', __dir__)
10
+ BIN = File.join(ROOT, 'bin', 'imgconv')
11
+ LIB = File.join(ROOT, 'lib')
12
+ FIX = File.join(ROOT, 'test', 'images')
13
+
14
+ def run_cmd_env(env, *args)
15
+ base_env = { 'IMGCONV_TEST_FAKE_MINIMAGICK' => '1' }.merge(env)
16
+ cmd = ['ruby', '-I', LIB, BIN] + args
17
+ Open3.capture3(base_env, *cmd)
18
+ end
19
+
20
+ def with_isolated_home
21
+ Dir.mktmpdir do |home|
22
+ env = { 'HOME' => home }
23
+ yield env
24
+ end
25
+ end
26
+
27
+ def copy_fixtures(src_dir, names)
28
+ names.each do |name|
29
+ FileUtils.cp(File.join(FIX, "#{name}.jpg"), File.join(src_dir, "#{name}.jpg"))
30
+ end
31
+ end
32
+
33
+ def configure_defaults(env, from: 'JPG', to: 'PNG', resize: '1920x')
34
+ _o, e1, s1 = run_cmd_env(env, 'config', 'set', from, to, resize)
35
+ assert s1.success?, "config set #{from} #{to} failed: #{e1}"
36
+ end
37
+
38
+ def configure_directories(env, input:, output:)
39
+ _o, e, s = run_cmd_env(env, 'config', 'set-dir', input, output)
40
+ assert s.success?, "config set-dir failed: #{e}"
41
+ end
42
+
43
+ def test_bulk_convert_jpg_to_png_with_fixtures
44
+ with_isolated_home do |env|
45
+ configure_defaults(env)
46
+ Dir.mktmpdir do |src|
47
+ Dir.mktmpdir do |dst|
48
+ copy_fixtures(src, %w[spring summer autumn winter])
49
+
50
+ stdout, stderr, status = run_cmd_env(env, 'convert', src, dst, '--from', 'JPG', '--to', 'PNG', '--overwrite')
51
+ assert status.success?, "bulk convert fixtures failed: #{status.exitstatus}\nstderr=\n#{stderr}\nstdout=\n#{stdout}"
52
+
53
+ %w[spring summer autumn winter].each do |name|
54
+ assert File.exist?(File.join(dst, "#{name}.png")), "expected output #{name}.png to exist"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def test_latest_two_selection_converts_two_newest_files
62
+ with_isolated_home do |env|
63
+ configure_defaults(env)
64
+ Dir.mktmpdir do |src|
65
+ Dir.mktmpdir do |dst|
66
+ copy_fixtures(src, %w[spring summer autumn winter])
67
+ base_time = Time.now - 3600
68
+ {
69
+ 'spring' => base_time + 100,
70
+ 'summer' => base_time + 2000,
71
+ 'autumn' => base_time + 300,
72
+ 'winter' => base_time + 4000,
73
+ }.each do |name, t|
74
+ File.utime(t, t, File.join(src, "#{name}.jpg"))
75
+ end
76
+
77
+ configure_directories(env, input: src, output: dst)
78
+
79
+ stdout, stderr, status = run_cmd_env(env, '--latest', '2')
80
+ assert status.success?, "--latest 2 failed: #{status.exitstatus}\nstderr=\n#{stderr}\nstdout=\n#{stdout}"
81
+
82
+ converted = Dir.glob(File.join(dst, '*.png')).map { |p| File.basename(p, '.png') }
83
+ assert_equal %w[summer winter].sort, converted.sort, "expected latest two images to be converted"
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def test_bulk_with_suffix_writes_suffixed_files
90
+ with_isolated_home do |env|
91
+ configure_defaults(env)
92
+ Dir.mktmpdir do |src|
93
+ Dir.mktmpdir do |dst|
94
+ copy_fixtures(src, %w[spring summer])
95
+
96
+ stdout, stderr, status = run_cmd_env(env, 'convert', src, dst, '--suffix', '_web')
97
+ assert status.success?, "bulk with suffix failed: #{status.exitstatus}\nstderr=\n#{stderr}\nstdout=\n#{stdout}"
98
+
99
+ %w[spring summer].each do |name|
100
+ assert File.exist?(File.join(dst, "#{name}_web.png")), "expected suffixed output for #{name}"
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ def test_recursive_bulk_keeps_subdirs_and_suffix
108
+ with_isolated_home do |env|
109
+ configure_defaults(env)
110
+ Dir.mktmpdir do |src|
111
+ Dir.mktmpdir do |dst|
112
+ FileUtils.cp(File.join(FIX, 'spring.jpg'), File.join(src, 'spring.jpg'))
113
+ sub = File.join(src, 'seasons')
114
+ FileUtils.mkdir_p(sub)
115
+ FileUtils.cp(File.join(FIX, 'summer.jpg'), File.join(sub, 'summer.jpg'))
116
+
117
+ stdout, stderr, status = run_cmd_env(env, 'convert', src, dst, '--from', 'JPG', '--to', 'PNG', '--recursive', '--suffix', '_web')
118
+ assert status.success?, "recursive bulk failed: #{status.exitstatus}\nstderr=\n#{stderr}\nstdout=\n#{stdout}"
119
+
120
+ assert File.exist?(File.join(dst, 'spring_web.png')), 'expected root image to be converted with suffix'
121
+ assert File.exist?(File.join(dst, 'seasons', 'summer_web.png')), 'expected nested image to be converted with suffix'
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ def test_single_file_delete_removes_source
128
+ with_isolated_home do |env|
129
+ configure_defaults(env)
130
+ Dir.mktmpdir do |dir|
131
+ src = File.join(dir, 'src')
132
+ dst = File.join(dir, 'dst')
133
+ FileUtils.mkdir_p(src)
134
+ FileUtils.mkdir_p(dst)
135
+ FileUtils.cp(File.join(FIX, 'autumn.jpg'), File.join(src, 'autumn.jpg'))
136
+
137
+ stdout, stderr, status = run_cmd_env(env, 'convert', 'autumn', '--input_dir', src, '--output_dir', dst, '--delete', '--overwrite')
138
+ assert status.success?, "single convert with --delete failed: #{status.exitstatus}\nstderr=\n#{stderr}\nstdout=\n#{stdout}"
139
+ assert File.exist?(File.join(dst, 'autumn.png')), 'expected output file to be created'
140
+ refute File.exist?(File.join(src, 'autumn.jpg')), 'expected source file to be deleted'
141
+ end
142
+ end
143
+ end
144
+ end