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
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RubyGems plugin hooks for automatic cleanup on gem uninstall
4
+ # This file is loaded by RubyGems when any gem command runs.
5
+
6
+ require 'fileutils'
7
+
8
+ Gem.post_uninstall do |uninstaller|
9
+ begin
10
+ spec = uninstaller.respond_to?(:spec) ? uninstaller.spec : nil
11
+ next unless spec && spec.name == 'images-convert'
12
+
13
+ config_dir = File.expand_path('~/.images-convert')
14
+ next unless Dir.exist?(config_dir)
15
+
16
+ # Ask for confirmation interactively in TTY environments
17
+ confirm = 'y'
18
+ if $stdout.isatty && $stdin.isatty
19
+ puts "images-convert: 設定ディレクトリ (#{config_dir}) を削除しますか? [Y/n]"
20
+ print '> '
21
+ ans = ($stdin.gets || '').strip
22
+ confirm = ans.empty? ? 'y' : ans
23
+ end
24
+
25
+ if confirm.to_s.downcase.start_with?('y')
26
+ FileUtils.rm_rf(config_dir)
27
+ puts "images-convert: 設定ディレクトリを削除しました: #{config_dir}"
28
+ else
29
+ puts "images-convert: 設定ディレクトリの削除をスキップしました"
30
+ end
31
+ rescue => e
32
+ warn "images-convert: アンインストール後のクリーンアップに失敗しました: #{e.class}: #{e.message}"
33
+ end
34
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+ require 'open3'
6
+
7
+ module Waifu2x
8
+ module Downloader
9
+ module_function
10
+
11
+ def download_and_install(download_url)
12
+ raise 'download_url が空です' if download_url.to_s.strip.empty?
13
+
14
+ cache_root = File.expand_path('~/.cache/waifu2x-pdf')
15
+ FileUtils.mkdir_p(cache_root)
16
+
17
+ Dir.mktmpdir('waifu2x-dl') do |tmp|
18
+ archive_path = File.join(tmp, File.basename(download_url))
19
+
20
+ run!(%W[curl -L -o #{archive_path} #{download_url}])
21
+
22
+ extract_dir = File.join(tmp, 'extract')
23
+ FileUtils.mkdir_p(extract_dir)
24
+
25
+ if archive_path =~ /\.zip\z/i
26
+ run!(%W[unzip -q #{archive_path} -d #{extract_dir}])
27
+ else
28
+ run!(%W[tar -xf #{archive_path} -C #{extract_dir}])
29
+ end
30
+
31
+ bin_path = nil
32
+ models_path = nil
33
+
34
+ Dir.glob(File.join(extract_dir, '**', 'waifu2x-ncnn-vulkan')).each do |p|
35
+ next unless File.file?(p)
36
+ bin_path = p
37
+ break
38
+ end
39
+
40
+ if bin_path
41
+ candidate = File.join(File.dirname(bin_path), 'models')
42
+ models_path = candidate if File.directory?(candidate)
43
+ end
44
+
45
+ if models_path.nil?
46
+ Dir.glob(File.join(extract_dir, '**', 'models')).each do |p|
47
+ next unless File.directory?(p)
48
+ models_path = p
49
+ break
50
+ end
51
+ end
52
+
53
+ raise 'ダウンロードしたアーカイブから waifu2x-ncnn-vulkan が見つかりません' unless bin_path
54
+
55
+ FileUtils.chmod('+x', bin_path)
56
+
57
+ install_dir = File.join(cache_root, 'waifu2x-ncnn-vulkan')
58
+ FileUtils.mkdir_p(install_dir)
59
+
60
+ unique = File.basename(archive_path).gsub(/[^a-zA-Z0-9_.-]/, '_')
61
+ target_dir = File.join(install_dir, unique)
62
+ FileUtils.mkdir_p(target_dir)
63
+
64
+ src_root = File.dirname(bin_path)
65
+ FileUtils.cp_r(Dir.glob(File.join(src_root, '*')), target_dir)
66
+
67
+ resolved_bin = File.join(target_dir, 'waifu2x-ncnn-vulkan')
68
+ resolved_models = File.directory?(File.join(target_dir, 'models')) ? File.join(target_dir, 'models') : models_path
69
+
70
+ current_link = File.join(install_dir, 'current')
71
+ begin
72
+ FileUtils.rm_f(current_link)
73
+ FileUtils.ln_s(target_dir, current_link)
74
+ rescue StandardError
75
+ end
76
+
77
+ { bin: resolved_bin, models: resolved_models }
78
+ end
79
+ end
80
+
81
+ def run!(argv)
82
+ stdout_str, stderr_str, status = Open3.capture3(*argv)
83
+ unless status.success?
84
+ raise "Command failed (#{argv.join(' ')}): #{stderr_str}\n#{stdout_str}"
85
+ end
86
+ stdout_str
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'combine_pdf'
4
+ require 'fileutils'
5
+ require 'tmpdir'
6
+ require 'shellwords'
7
+
8
+ module Waifu2x
9
+ class PDFBuilder
10
+ class << self
11
+ def create_pdf(image_paths, output_path, options = {})
12
+ return if image_paths.nil? || image_paths.empty?
13
+
14
+ Dir.mktmpdir('waifu2x-pdf-pages') do |pages_dir|
15
+ pdf_paths = images_to_single_pdfs(image_paths, pages_dir, options)
16
+ merge_pdfs(pdf_paths, output_path)
17
+ end
18
+ end
19
+
20
+ def images_to_single_pdfs(image_paths, output_dir, options = {})
21
+ FileUtils.mkdir_p(output_dir)
22
+ pdfs = []
23
+
24
+ compression = (options[:pdf_compression] || '').to_s.downcase
25
+ quality = options[:pdf_quality]
26
+ density = options[:pdf_density]
27
+ a4_print = options.key?(:a4_print) ? options[:a4_print] : true
28
+
29
+ tool = find_imagemagick_tool
30
+ raise 'ImageMagick CLI (magick or convert) が見つかりません' unless tool
31
+
32
+ image_paths.each_with_index do |path, idx|
33
+ base = format('%04d', idx + 1)
34
+ pdf_path = File.join(output_dir, "page_#{base}.pdf")
35
+ d = (density && density.to_i > 0) ? density.to_i : 300
36
+ compress_args = imagemagick_compress_args(compression: compression, quality: quality)
37
+
38
+ if a4_print
39
+ a4_width_in = 8.26771653543307
40
+ a4_height_in = 11.6929133858268
41
+ canvas_w = (a4_width_in * d).round
42
+ canvas_h = (a4_height_in * d).round
43
+ base_cmd = (tool == 'magick') ? ['magick', path] : ['convert', path]
44
+ cmd = base_cmd + [
45
+ '-define', 'png:ignore-crc=true',
46
+ '-auto-orient', '-strip', '-units', 'PixelsPerInch', '-density', d.to_s,
47
+ '-resize', "#{canvas_w}x#{canvas_h}>",
48
+ '-background', 'white', '-gravity', 'center', '-extent', "#{canvas_w}x#{canvas_h}"
49
+ ]
50
+ cmd += compress_args
51
+ cmd += [pdf_path]
52
+ run_cmd!(cmd)
53
+ else
54
+ base_cmd = (tool == 'magick') ? ['magick', path] : ['convert', path]
55
+ cmd = base_cmd + ['-define', 'png:ignore-crc=true', '-auto-orient', '-strip', '-units', 'PixelsPerInch', '-density', d.to_s]
56
+ cmd += compress_args
57
+ cmd += [pdf_path]
58
+ run_cmd!(cmd)
59
+ end
60
+
61
+ pdfs << pdf_path
62
+ end
63
+ pdfs
64
+ end
65
+
66
+ def merge_pdfs(pdf_paths, output_path)
67
+ combined = CombinePDF.new
68
+ pdf_paths.each do |pdf|
69
+ combined << CombinePDF.load(pdf)
70
+ end
71
+ FileUtils.mkdir_p(File.dirname(output_path)) unless Dir.exist?(File.dirname(output_path))
72
+ combined.save(output_path)
73
+ end
74
+
75
+ def imagemagick_compress_args(compression:, quality: nil)
76
+ args = []
77
+ case compression
78
+ when 'jpeg'
79
+ args += ['-compress', 'JPEG']
80
+ when 'jpeg2000'
81
+ args += ['-compress', 'JPEG2000']
82
+ when 'zip'
83
+ args += ['-compress', 'Zip']
84
+ end
85
+ if quality && quality.to_i > 0
86
+ args += ['-quality', quality.to_i.to_s]
87
+ end
88
+ args
89
+ end
90
+
91
+ def run_cmd!(cmd)
92
+ success = system(*cmd, out: File::NULL, err: File::NULL)
93
+ return true if success
94
+ code = $?.respond_to?(:exitstatus) ? $?.exitstatus : nil
95
+ raise "ImageMagick command failed (exit #{code}): #{cmd.join(' ')}"
96
+ end
97
+
98
+ def find_imagemagick_tool
99
+ return 'magick' if system('which magick > /dev/null 2>&1')
100
+ return 'convert' if system('which convert > /dev/null 2>&1')
101
+ nil
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+ require 'parallel'
6
+ require 'shellwords'
7
+
8
+ module Waifu2x
9
+ class Processor
10
+ class Waifu2xNotFoundError < StandardError; end
11
+
12
+ class << self
13
+ def process_images(input_dir, output_dir, scale: 2, noise: 1, model: 'anime_style_art_rgb', processor: :waifu2x_ncnn_vulkan, waifu2x_bin: nil, models_path: nil, recursive: false, auto_scale_a4: false, pdf_density: nil, verbose: false, image_format: 'png', on_error: 'stop')
14
+ FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
15
+
16
+ image_paths = find_images(input_dir, recursive: recursive)
17
+ return if image_paths.empty?
18
+
19
+ Parallel.each(image_paths, in_threads: [Parallel.processor_count, 1].max) do |image_path|
20
+ local_scale = if auto_scale_a4
21
+ choose_scale_for_a4(image_path, target_dpi: (pdf_density || 300))
22
+ else
23
+ scale
24
+ end
25
+ begin
26
+ process_single_image(image_path, output_dir, local_scale, noise, model, processor, waifu2x_bin, models_path, verbose, image_format)
27
+ rescue StandardError => e
28
+ safe_path = begin
29
+ image_path.to_s.encode('UTF-8', invalid: :replace, undef: :replace)
30
+ rescue StandardError
31
+ image_path.to_s
32
+ end
33
+ safe_err = begin
34
+ e.message.to_s.encode('UTF-8', invalid: :replace, undef: :replace)
35
+ rescue StandardError
36
+ e.message.to_s
37
+ end
38
+ warn "[waifu2x] failed: #{safe_path} (#{e.class}: #{safe_err})"
39
+ raise e if on_error != 'skip'
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def choose_scale_for_a4(image_path, target_dpi: 300)
47
+ a4_width_in = 8.26771653543307
48
+ a4_height_in = 11.6929133858268
49
+ target_w = (a4_width_in * target_dpi).round
50
+ target_h = (a4_height_in * target_dpi).round
51
+
52
+ w, h = safe_image_dimensions(image_path)
53
+
54
+ return 2 if w.to_i <= 0 || h.to_i <= 0
55
+ scale_w = target_w.to_f / w
56
+ scale_h = target_h.to_f / h
57
+ needed = [scale_w, scale_h].max
58
+
59
+ return 1 if needed <= 1.0
60
+ return 2 if needed <= 2.0
61
+ 4
62
+ rescue StandardError
63
+ 2
64
+ end
65
+
66
+ def safe_image_dimensions(path)
67
+ cmd = identify_command
68
+ if cmd
69
+ out = `#{cmd} -format "%w %h" #{Shellwords.escape(path)} 2>/dev/null`
70
+ if $?.success?
71
+ parts = out.strip.split
72
+ if parts.size == 2
73
+ w = Integer(parts[0]) rescue nil
74
+ h = Integer(parts[1]) rescue nil
75
+ return [w, h] if w && h
76
+ end
77
+ end
78
+ end
79
+ if system('which sips > /dev/null 2>&1')
80
+ out = `sips -g pixelWidth -g pixelHeight #{Shellwords.escape(path)} 2>/dev/null`
81
+ if $?.success?
82
+ w = out[/pixelWidth:\s*(\d+)/, 1]
83
+ h = out[/pixelHeight:\s*(\d+)/, 1]
84
+ return [w.to_i, h.to_i] if w && h
85
+ end
86
+ end
87
+ [0, 0]
88
+ end
89
+
90
+ def identify_command
91
+ if system('which magick > /dev/null 2>&1')
92
+ 'magick identify'
93
+ elsif system('which identify > /dev/null 2>&1')
94
+ 'identify'
95
+ else
96
+ nil
97
+ end
98
+ end
99
+
100
+ def find_images(directory, recursive: false)
101
+ exts = %w[jpg JPG jpeg JPEG png PNG gif GIF webp WEBP]
102
+ brace = "{#{exts.join(',')}}"
103
+ pattern = recursive ? File.join(directory, '**', "*.#{brace}") : File.join(directory, "*.#{brace}")
104
+ paths = Dir.glob(pattern)
105
+ paths.select! { |p| File.file?(p) }
106
+ paths.reject! do |p|
107
+ base = File.basename(p)
108
+ base.start_with?('._') || base.casecmp('thumbs.db').zero?
109
+ end
110
+ paths.sort
111
+ end
112
+
113
+ def process_single_image(input_path, output_dir, scale, noise, model, processor, waifu2x_bin, models_path, verbose, image_format)
114
+ base_name = File.basename(input_path, '.*')
115
+ requested_ext = image_format.to_s.downcase
116
+ requested_ext = 'png' unless %w[png jpg jpeg webp].include?(requested_ext)
117
+ requested_ext = 'jpg' if requested_ext == 'jpeg'
118
+ output_path = File.join(output_dir, base_name + ".#{requested_ext}")
119
+ png_path = File.join(output_dir, base_name + '.png')
120
+
121
+ direct_format = (requested_ext == 'png' ? nil : requested_ext)
122
+ direct_quality = (requested_ext == 'webp' || requested_ext == 'jpg') ? 95 : nil
123
+ waifu_out_path = direct_format ? output_path : png_path
124
+
125
+ case processor
126
+ when :waifu2x_ncnn_vulkan
127
+ ok, status = process_with_waifu2x_ncnn_vulkan(
128
+ input_path,
129
+ waifu_out_path,
130
+ scale,
131
+ noise,
132
+ model,
133
+ waifu2x_bin,
134
+ models_path,
135
+ verbose,
136
+ format: direct_format,
137
+ quality: direct_quality
138
+ )
139
+ unless ok
140
+ cleaned = clean_input_image(input_path, verbose: verbose)
141
+ if cleaned && File.exist?(cleaned)
142
+ ok2, status2 = process_with_waifu2x_ncnn_vulkan(
143
+ cleaned,
144
+ waifu_out_path,
145
+ scale,
146
+ noise,
147
+ model,
148
+ waifu2x_bin,
149
+ models_path,
150
+ verbose,
151
+ format: direct_format,
152
+ quality: direct_quality
153
+ )
154
+ ok = ok2
155
+ status = status2
156
+ end
157
+ unless ok
158
+ raise RuntimeError, "waifu2x exited with status #{status || 'unknown'}"
159
+ end
160
+ end
161
+ else
162
+ raise "Unsupported processor: #{processor}"
163
+ end
164
+
165
+ if File.exist?(png_path) && (File.extname(output_path).downcase != '.png') && direct_format.nil?
166
+ begin
167
+ convert_with_imagemagick_cli!(png_path, output_path, verbose: verbose)
168
+ File.delete(png_path) if File.exist?(output_path) && File.exist?(png_path)
169
+ rescue StandardError => e
170
+ safe_path = begin
171
+ output_path.to_s.encode('UTF-8', invalid: :replace, undef: :replace)
172
+ rescue StandardError
173
+ output_path.to_s
174
+ end
175
+ safe_err = begin
176
+ e.message.to_s.encode('UTF-8', invalid: :replace, undef: :replace)
177
+ rescue StandardError
178
+ e.message.to_s
179
+ end
180
+ warn "[imagemagick] convert skipped, keep PNG: #{safe_path} (#{e.class}: #{safe_err})"
181
+ end
182
+ end
183
+
184
+ unless File.exist?(output_path) || File.exist?(png_path)
185
+ raise "出力画像の作成に失敗しました: #{output_path}"
186
+ end
187
+ end
188
+
189
+ def process_with_waifu2x_ncnn_vulkan(input_path, output_path, scale, noise, model, waifu2x_bin, models_path, verbose, format: nil, quality: nil)
190
+ bin = waifu2x_bin || 'waifu2x-ncnn-vulkan'
191
+ cmd = [bin, '-i', input_path, '-o', output_path, '-s', scale.to_s, '-n', noise.to_s]
192
+ cmd += ['-f', format] if format
193
+ cmd += ['-q', quality.to_i.to_s] if quality
194
+ cmd += ['-m', models_path] if models_path && !models_path.empty?
195
+ cmd += ['-g', '0', '-j', '1:2:2']
196
+
197
+ success = if verbose
198
+ system(*cmd)
199
+ else
200
+ system(*cmd, out: File::NULL, err: File::NULL)
201
+ end
202
+ status = $?.respond_to?(:exitstatus) ? $?.exitstatus : (success ? 0 : nil)
203
+ [success, status]
204
+ end
205
+
206
+ def convert_with_imagemagick_cli!(src_path, dst_path, verbose: false)
207
+ tool = find_imagemagick_tool
208
+ raise 'ImageMagick CLI (magick or convert) が見つかりません' unless tool
209
+
210
+ dst_ext = File.extname(dst_path).downcase
211
+ quality = 90
212
+ args = []
213
+ args += ['-define', 'png:ignore-crc=true', '-auto-orient', '-strip']
214
+
215
+ case dst_ext
216
+ when '.jpg', '.jpeg'
217
+ args += ['-quality', quality.to_s]
218
+ when '.webp'
219
+ args += ['-quality', quality.to_s]
220
+ end
221
+ cmd = if tool == 'magick'
222
+ ['magick', src_path] + args + [dst_path]
223
+ else
224
+ ['convert', src_path] + args + [dst_path]
225
+ end
226
+ success = run_im_cmd(cmd, verbose: verbose)
227
+ return true if success
228
+
229
+ Dir.mktmpdir('w2x-fix') do |tmp|
230
+ cleaned = File.join(tmp, 'clean.png')
231
+ fix_cmd = if tool == 'magick'
232
+ ['magick', src_path, '-define', 'png:ignore-crc=true', '-auto-orient', '-strip', '-colorspace', 'sRGB', 'PNG32:' + cleaned]
233
+ else
234
+ ['convert', src_path, '-define', 'png:ignore-crc=true', '-auto-orient', '-strip', '-colorspace', 'sRGB', 'PNG32:' + cleaned]
235
+ end
236
+ run_im_cmd(fix_cmd, verbose: verbose)
237
+ retry_cmd = if tool == 'magick'
238
+ ['magick', cleaned] + args + [dst_path]
239
+ else
240
+ ['convert', cleaned] + args + [dst_path]
241
+ end
242
+ success2 = run_im_cmd(retry_cmd, verbose: verbose)
243
+ return true if success2
244
+
245
+ if system('which sips > /dev/null 2>&1')
246
+ sips_png = File.join(tmp, 'sips.png')
247
+ sips_ok = system('sips', '-s', 'format', 'png', src_path, '--out', sips_png, out: File::NULL, err: File::NULL)
248
+ if sips_ok && File.exist?(sips_png)
249
+ retry_cmd2 = if tool == 'magick'
250
+ ['magick', sips_png] + args + [dst_path]
251
+ else
252
+ ['convert', sips_png] + args + [dst_path]
253
+ end
254
+ success3 = run_im_cmd(retry_cmd2, verbose: verbose)
255
+ return true if success3
256
+ end
257
+ end
258
+ end
259
+
260
+ code = $?.respond_to?(:exitstatus) ? $?.exitstatus : nil
261
+ raise "ImageMagick convert failed (exit #{code})"
262
+ end
263
+
264
+ def find_imagemagick_tool
265
+ return 'magick' if system('which magick > /dev/null 2>&1')
266
+ return 'convert' if system('which convert > /dev/null 2>&1')
267
+ nil
268
+ end
269
+
270
+ def run_im_cmd(cmd, verbose: false)
271
+ if verbose
272
+ system(*cmd)
273
+ else
274
+ system(*cmd, out: File::NULL, err: File::NULL)
275
+ end
276
+ end
277
+
278
+ def clean_input_image(src_path, verbose: false)
279
+ tool = find_imagemagick_tool
280
+ Dir.mktmpdir('w2x-clean') do |tmp|
281
+ cleaned = File.join(tmp, 'clean.png')
282
+ if tool
283
+ cmd = if tool == 'magick'
284
+ ['magick', src_path, '-define', 'png:ignore-crc=true', '-auto-orient', '-strip', '-colorspace', 'sRGB', 'PNG32:' + cleaned]
285
+ else
286
+ ['convert', src_path, '-define', 'png:ignore-crc=true', '-auto-orient', '-strip', '-colorspace', 'sRGB', 'PNG32:' + cleaned]
287
+ end
288
+ return cleaned if run_im_cmd(cmd, verbose: verbose) && File.exist?(cleaned)
289
+ end
290
+ if system('which sips > /dev/null 2>&1')
291
+ sips_ok = system('sips', '-s', 'format', 'png', src_path, '--out', cleaned, out: File::NULL, err: File::NULL)
292
+ return cleaned if sips_ok && File.exist?(cleaned)
293
+ end
294
+ nil
295
+ end
296
+ rescue StandardError
297
+ nil
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'fileutils'
5
+ require_relative 'downloader'
6
+
7
+ module Waifu2x
8
+ module Setup
9
+ DEFAULT_MACOS_URL = 'https://github.com/nihui/waifu2x-ncnn-vulkan/releases/download/20250915/waifu2x-ncnn-vulkan-20250915-macos.zip'
10
+
11
+ module_function
12
+
13
+ def resolve_paths(options = {})
14
+ bin = options[:waifu2x_bin] || ENV['WAIFU2X_BIN'] || which('waifu2x-ncnn-vulkan')
15
+ models = options[:models_path] || ENV['WAIFU2X_MODELS']
16
+ source = nil
17
+
18
+ if options[:waifu2x_bin]
19
+ source = :option
20
+ elsif ENV['WAIFU2X_BIN']
21
+ source = :env
22
+ elsif bin && !bin.empty?
23
+ source = :path
24
+ end
25
+
26
+ if bin.nil? || bin.empty?
27
+ cached = find_cached_install
28
+ if cached
29
+ bin = cached[:bin]
30
+ models ||= cached[:models]
31
+ source = :cached
32
+ end
33
+ end
34
+
35
+ if bin.nil? || bin.empty?
36
+ if options[:auto_download]
37
+ download_url = options[:download_url]
38
+ download_url = DEFAULT_MACOS_URL if download_url.to_s.strip.empty?
39
+
40
+ installed = Downloader.download_and_install(download_url)
41
+ bin = installed[:bin]
42
+ models ||= installed[:models]
43
+ source = :download
44
+ else
45
+ raise 'waifu2x-ncnn-vulkan が見つかりませんでした。--waifu2x-bin でパスを指定するか、--auto-download と --download-url を指定してください。'
46
+ end
47
+ end
48
+
49
+ models ||= begin
50
+ candidate = File.join(File.dirname(bin), 'models')
51
+ File.directory?(candidate) ? candidate : nil
52
+ end
53
+
54
+ if models && options[:model]
55
+ selected = select_model_dir(models, options[:model].to_s)
56
+ models = selected if selected
57
+ end
58
+
59
+ case source
60
+ when :option
61
+ puts "Using waifu2x binary (option): #{bin}"
62
+ when :env
63
+ puts "Using waifu2x binary (env WAIFU2X_BIN): #{bin}"
64
+ when :path
65
+ puts "Using waifu2x binary (PATH): #{bin}"
66
+ when :cached
67
+ puts "Using waifu2x binary (cached): #{bin}"
68
+ when :download
69
+ puts "Downloaded waifu2x binary: #{bin}"
70
+ end
71
+
72
+ { bin: bin, models: models }
73
+ end
74
+
75
+ def select_model_dir(models_root, alias_name)
76
+ return nil unless File.directory?(models_root)
77
+
78
+ key = alias_name.to_s.downcase
79
+ patterns = case key
80
+ when 'photo'
81
+ [/upconv_7_photo\z/, /photo\z/]
82
+ when 'anime_rgb'
83
+ [/upconv_7_anime_style_art_rgb\z/]
84
+ when 'anime'
85
+ [/upconv_7_anime_style_art\z/, /anime_style_art\z/]
86
+ when 'cunet'
87
+ [/cunet\z/]
88
+ when 'upresnet10'
89
+ [/upresnet10\z/]
90
+ else
91
+ []
92
+ end
93
+
94
+ return nil if patterns.empty?
95
+
96
+ entries = Dir.children(models_root)
97
+ dirnames = entries.select { |e| File.directory?(File.join(models_root, e)) }
98
+
99
+ patterns.each do |pat|
100
+ match = dirnames.find { |name| name.match?(pat) || name.match?(/models-.*#{pat.source}/) }
101
+ return File.join(models_root, match) if match
102
+ end
103
+
104
+ nil
105
+ end
106
+
107
+ def which(cmd)
108
+ stdout, _stderr, status = Open3.capture3('bash', '-lc', "command -v #{cmd}")
109
+ status.success? ? stdout.strip : nil
110
+ end
111
+
112
+ def find_cached_install
113
+ cache_root = File.expand_path('~/.cache/waifu2x-pdf/waifu2x-ncnn-vulkan')
114
+ return nil unless Dir.exist?(cache_root)
115
+
116
+ candidates = Dir.children(cache_root).map { |d| File.join(cache_root, d) }.select { |p| File.directory?(p) }
117
+ candidates.sort_by! { |p| File.mtime(p) }.reverse!
118
+ candidates.each do |dir|
119
+ bin = File.join(dir, 'waifu2x-ncnn-vulkan')
120
+ next unless File.exist?(bin)
121
+ models = File.join(dir, 'models')
122
+ return { bin: bin, models: File.directory?(models) ? models : nil }
123
+ end
124
+ nil
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Waifu2x
4
+ VERSION = '0.1.0'
5
+ end