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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +25 -0
- data/.github/workflows/ci.yml +79 -0
- data/.github/workflows/release.yml +122 -0
- data/.gitignore +14 -0
- data/CHANGELOG.md +146 -0
- data/Gemfile +9 -0
- data/LICENSE +41 -0
- data/README.md +378 -0
- data/RELEASE_NOTES_v0.2.12.md +66 -0
- data/RELEASE_NOTES_v0.2.3.md +19 -0
- data/RELEASE_NOTES_v0.3.0.md +66 -0
- data/RELEASE_NOTES_v0.4.0.md +14 -0
- data/RELEASE_NOTES_v0.4.1.md +13 -0
- data/Rakefile +13 -0
- data/bin/images-convert +8 -0
- data/bin/imgconv +8 -0
- data/images-convert.gemspec +39 -0
- data/lib/images_convert/cleanup.rb +31 -0
- data/lib/images_convert/configuration.rb +303 -0
- data/lib/images_convert/mini_magick_stub.rb +121 -0
- data/lib/images_convert/version.rb +5 -0
- data/lib/images_convert/waifu2x_test_stub.rb +93 -0
- data/lib/images_convert.rb +1557 -0
- data/lib/rubygems_plugin.rb +34 -0
- data/lib/waifu2x/downloader.rb +89 -0
- data/lib/waifu2x/pdf_builder.rb +105 -0
- data/lib/waifu2x/processor.rb +301 -0
- data/lib/waifu2x/setup.rb +127 -0
- data/lib/waifu2x/version.rb +5 -0
- data/lib/waifu2x.rb +221 -0
- data/test/images/autumn.jpg +0 -0
- data/test/images/spring.jpg +0 -0
- data/test/images/summer.jpg +0 -0
- data/test/images/winter.jpg +0 -0
- data/test/support/waifu2x_test_stub.rb +91 -0
- data/test/test_config.rb +143 -0
- data/test/test_fixtures.rb +144 -0
- data/test/test_formats.rb +213 -0
- data/test/test_help.rb +33 -0
- data/test/test_helper.rb +17 -0
- data/test/test_helper_mini_magick_stub.rb +4 -0
- data/test/test_selection.rb +142 -0
- data/test/test_version.rb +16 -0
- data/test/test_waifu2x.rb +81 -0
- metadata +179 -0
@@ -0,0 +1,1557 @@
|
|
1
|
+
def clamp_pdf_density(value)
|
2
|
+
return ImagesConvert::Configuration.default_output_pdf_density if value.nil?
|
3
|
+
v = value.to_i
|
4
|
+
v = 1200 if v > 1200
|
5
|
+
v = 72 if v < 72
|
6
|
+
v
|
7
|
+
end
|
8
|
+
#!/usr/bin/env ruby
|
9
|
+
# -*- coding: utf-8 -*-
|
10
|
+
|
11
|
+
require 'thor'
|
12
|
+
require 'fileutils'
|
13
|
+
require 'time'
|
14
|
+
if ENV['IMGCONV_TEST_FAKE_MINIMAGICK'] == '1'
|
15
|
+
require_relative 'images_convert/mini_magick_stub'
|
16
|
+
else
|
17
|
+
require 'mini_magick'
|
18
|
+
end
|
19
|
+
require 'rbconfig'
|
20
|
+
require_relative 'images_convert/version'
|
21
|
+
require_relative 'images_convert/configuration'
|
22
|
+
require_relative 'images_convert/cleanup'
|
23
|
+
require_relative 'waifu2x'
|
24
|
+
|
25
|
+
# ImageMagickがインストールされているかチェック(未導入なら最小限の対話で導入を試みる)
|
26
|
+
def imagemagick_installed?
|
27
|
+
system('which magick > /dev/null 2>&1') || system('which convert > /dev/null 2>&1')
|
28
|
+
end
|
29
|
+
|
30
|
+
unless imagemagick_installed?
|
31
|
+
puts "ImageMagick が見つかりません。今すぐインストールしますか? [Y/n]"
|
32
|
+
print "> "
|
33
|
+
answer = (STDIN.gets || "").strip
|
34
|
+
answer = 'y' if answer.empty?
|
35
|
+
|
36
|
+
if answer.downcase.start_with?('y')
|
37
|
+
ostype = RbConfig::CONFIG['host_os']
|
38
|
+
success = false
|
39
|
+
|
40
|
+
if ostype =~ /darwin/
|
41
|
+
if system('which brew > /dev/null 2>&1')
|
42
|
+
puts "Homebrew で ImageMagick をインストールします..."
|
43
|
+
success = system('brew install imagemagick')
|
44
|
+
else
|
45
|
+
puts "Homebrew が見つかりません。https://brew.sh/ からインストールしてください。"
|
46
|
+
end
|
47
|
+
elsif ostype =~ /linux/
|
48
|
+
if system('which apt-get > /dev/null 2>&1')
|
49
|
+
puts "apt-get で ImageMagick をインストールします(sudo が必要な場合があります)..."
|
50
|
+
success = system('sudo apt-get update && sudo apt-get install -y imagemagick')
|
51
|
+
elsif system('which yum > /dev/null 2>&1')
|
52
|
+
puts "yum で ImageMagick をインストールします(sudo が必要な場合があります)..."
|
53
|
+
success = system('sudo yum install -y ImageMagick')
|
54
|
+
elsif system('which dnf > /dev/null 2>&1')
|
55
|
+
puts "dnf で ImageMagick をインストールします(sudo が必要な場合があります)..."
|
56
|
+
success = system('sudo dnf install -y ImageMagick')
|
57
|
+
else
|
58
|
+
puts "対応するパッケージマネージャが見つかりませんでした。"
|
59
|
+
end
|
60
|
+
else
|
61
|
+
puts "このOSでは自動インストールに対応していません。"
|
62
|
+
puts "Windows はインストーラから導入してください: https://imagemagick.org/script/download.php#windows"
|
63
|
+
end
|
64
|
+
|
65
|
+
if success && imagemagick_installed?
|
66
|
+
puts "ImageMagick のインストールが完了しました。"
|
67
|
+
else
|
68
|
+
puts "ImageMagick の自動インストールに失敗しました。手動でのインストールをお願いします。"
|
69
|
+
puts " macOS: brew install imagemagick"
|
70
|
+
puts " Ubuntu/Debian: sudo apt-get install -y imagemagick"
|
71
|
+
puts " CentOS/RHEL/Fedora: sudo yum/dnf install -y ImageMagick"
|
72
|
+
exit 1
|
73
|
+
end
|
74
|
+
else
|
75
|
+
puts "ImageMagick が必要です。後で以下のいずれかでインストールしてください:"
|
76
|
+
puts " macOS: brew install imagemagick"
|
77
|
+
puts " Ubuntu/Debian: sudo apt-get install -y imagemagick"
|
78
|
+
puts " CentOS/RHEL/Fedora: sudo yum/dnf install -y ImageMagick"
|
79
|
+
puts " Windows: https://imagemagick.org/script/download.php#windows"
|
80
|
+
exit 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# images_convert.rb: Thorベースの画像変換スクリプト
|
85
|
+
# 形式やリサイズ、入出力ディレクトリは設定値を使用(config コマンドで変更可能)
|
86
|
+
|
87
|
+
class ImagesConvertCLI < Thor
|
88
|
+
include Thor::Actions
|
89
|
+
|
90
|
+
DOWNLOADS_DIR = "#{ENV['HOME']}/Downloads"
|
91
|
+
|
92
|
+
# コマンド名を imgconv に設定(短縮形として imgconv を使用)
|
93
|
+
def self.command_name
|
94
|
+
'imgconv'
|
95
|
+
end
|
96
|
+
|
97
|
+
# `images-convert --version` / `images-convert -v` に対応
|
98
|
+
map %w[-v --version] => :version
|
99
|
+
# `images-convert --help` / `images-convert -h` に対応(Thor の help にマップ)
|
100
|
+
map %w[-h --help] => :help
|
101
|
+
# 引数なし実行時は help を表示
|
102
|
+
default_task :help
|
103
|
+
desc 'version', 'バージョン情報を表示'
|
104
|
+
def version
|
105
|
+
say "images-convert #{ImagesConvert::VERSION}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def help(command = nil, subcommand = false)
|
109
|
+
if command
|
110
|
+
if command.to_s == 'convert'
|
111
|
+
say <<~HELP
|
112
|
+
Usage:
|
113
|
+
imgconv convert TARGET [DEST_DIR]
|
114
|
+
|
115
|
+
Options:
|
116
|
+
-i, --input-dir=INPUT_DIR 入力ディレクトリ(デフォルト: 設定値または ~/Downloads)
|
117
|
+
-o, --output=OUTPUT 出力ファイルパス(単一ファイル変換時、ディレクトリ指定時は出力ディレクトリ)
|
118
|
+
--output-dir=OUTPUT_DIR 出力ディレクトリ(単一ファイル時)
|
119
|
+
-d, --delete 変換後に元のファイルを削除
|
120
|
+
-f, --overwrite 既存の出力ファイルがある場合に上書き
|
121
|
+
-s, --suffix=SUFFIX 出力ファイル名の末尾に付与するサフィックス(例: _converted)
|
122
|
+
--recursive ディレクトリを再帰的に処理(一括変換時)
|
123
|
+
--resize=RESIZE リサイズ値(例: 1920x, 50%)
|
124
|
+
--latest=N 最新の画像ファイル N 件を変換(入力ディレクトリ内から)
|
125
|
+
--latest-exif 最新 N 件の選定を EXIF 撮影日時に基づいて行う(--latest と併用)
|
126
|
+
--all 入力ディレクトリ内の対象画像をすべて変換
|
127
|
+
--from=FROM 入力形式(設定を一時的に上書き)
|
128
|
+
--to=TO 出力形式(設定を一時的に上書き)
|
129
|
+
|
130
|
+
Description:
|
131
|
+
使い方:
|
132
|
+
|
133
|
+
images-convert [convert] TARGET [DEST_DIR] [オプション]
|
134
|
+
imgconv [convert] TARGET [DEST_DIR] [オプション]
|
135
|
+
|
136
|
+
動作:
|
137
|
+
- TARGET がファイル名/番号の場合は単一ファイル変換(-i/-o で入出力ディレクトリ指定可)
|
138
|
+
- TARGET がディレクトリの場合は一括変換(DEST_DIR 未指定時は in-place 変換)
|
139
|
+
|
140
|
+
主なオプション:
|
141
|
+
-i, --input-dir=DIR 入力ディレクトリ(単一ファイル時)
|
142
|
+
-o, --output-dir=DIR 出力ディレクトリ(単一ファイル時)
|
143
|
+
-d, --delete 変換後に元ファイルを削除
|
144
|
+
-f, --overwrite 既存の出力ファイルがある場合に上書き
|
145
|
+
-s, --suffix=SUFFIX 出力ファイル名の末尾に付与(例: _web)
|
146
|
+
--recursive ディレクトリを再帰的に処理(一括変換時)
|
147
|
+
|
148
|
+
例:
|
149
|
+
# 1枚だけ変換(番号指定、入出力ディレクトリや変換形式は既定値)
|
150
|
+
images-convert [convert] 1234
|
151
|
+
# または短縮コマンド
|
152
|
+
imgconv [convert] 1234
|
153
|
+
|
154
|
+
# 1枚だけ変換(ファイル名指定、入出力ディレクトリを明示)
|
155
|
+
images-convert [convert] IMG_0001 ~/Downloads ~/Downloads
|
156
|
+
|
157
|
+
# 一括変換(in-place)
|
158
|
+
images-convert [convert] ./photos
|
159
|
+
|
160
|
+
# 一括変換(出力先を別ディレクトリ、再帰処理、サフィックス付与)
|
161
|
+
images-convert [convert] ./photos ./export --recursive --suffix _resized
|
162
|
+
HELP
|
163
|
+
else
|
164
|
+
super
|
165
|
+
end
|
166
|
+
else
|
167
|
+
say <<~HELP
|
168
|
+
Commands:
|
169
|
+
imgconv [convert] TARGET [INPUT_DIR] [OUTPUT_DIR] # 単一ファイル変換(TARGET=ファイル名/番号)
|
170
|
+
imgconv [convert] INPUT_DIR [OUTPUT_DIR] # 一括変換
|
171
|
+
imgconv config # 現在の設定を表示
|
172
|
+
imgconv set FROM TO [RESIZE] # 変換形式、リサイズ値の既定値を設定
|
173
|
+
imgconv set-dir INPUT_DIR OUTPUT_DIR # 入出力ディレクトリの既定値を設定
|
174
|
+
imgconv --help # 利用可能なコマンド一覧を表示
|
175
|
+
imgconv [COMMAND] --help # 各コマンドの詳細を表示
|
176
|
+
imgconv --version # バージョン情報を表示
|
177
|
+
HELP
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
method_option :scale, type: :numeric, desc: 'waifu2x の既定倍率を設定(set-waifu2x サブコマンドで使用)'
|
182
|
+
method_option :noise, type: :numeric, desc: 'waifu2x のノイズ除去レベル(set-waifu2x サブコマンドで使用)'
|
183
|
+
method_option :model, type: :string, desc: 'waifu2x のモデルエイリアス(set-waifu2x サブコマンドで使用)'
|
184
|
+
method_option :auto_scale_a4, type: :boolean, desc: 'A4自動スケールの有効/無効(set-waifu2x サブコマンド)'
|
185
|
+
method_option :a4_print, type: :boolean, desc: 'A4レイアウトPDFの有効/無効(set-waifu2x サブコマンド)'
|
186
|
+
method_option :image_format, type: :string, desc: 'waifu2x 出力フォーマット(set-waifu2x サブコマンドで使用)'
|
187
|
+
method_option :on_error, type: :string, enum: %w[skip stop], desc: 'waifu2x エラー時の動作(set-waifu2x サブコマンド)'
|
188
|
+
method_option :quality, type: :numeric, desc: 'waifu2x 出力品質(set-waifu2x サブコマンド)'
|
189
|
+
desc 'config [SUBCOMMAND]', '現在の設定を表示、または設定を変更(subcommand: set/set-dir/set-waifu2x/set-output/reset)'
|
190
|
+
method_option :force, type: :boolean, default: false, desc: '確認なしでリセットを実行'
|
191
|
+
def config(*args)
|
192
|
+
if args.first.to_s == 'reset'
|
193
|
+
unless options[:force]
|
194
|
+
say '設定ファイルを既定値に戻します。よろしいですか? [y/N]'
|
195
|
+
print '> '
|
196
|
+
answer = (STDIN.gets || '').strip.downcase
|
197
|
+
unless %w[y yes].include?(answer)
|
198
|
+
say 'リセットをキャンセルしました。'
|
199
|
+
return
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
ImagesConvert::Configuration.reset_to_defaults
|
204
|
+
say '設定を既定値にリセットしました。'
|
205
|
+
say ''
|
206
|
+
say '現在の設定:'
|
207
|
+
show_config_summary
|
208
|
+
return
|
209
|
+
end
|
210
|
+
|
211
|
+
# サブコマンド: set FROM TO RESIZE
|
212
|
+
if args.first.to_s == 'set'
|
213
|
+
_, from, to, resize = args
|
214
|
+
unless from && to && resize
|
215
|
+
say "使い方: images-convert config set FROM TO RESIZE", :red
|
216
|
+
exit 1
|
217
|
+
end
|
218
|
+
ImagesConvert::Configuration.save_defaults(from: from, to: to, resize: resize)
|
219
|
+
say "設定を変更しました:"
|
220
|
+
say " デフォルト入力形式: #{from}"
|
221
|
+
say " デフォルト出力形式: #{to}"
|
222
|
+
say " デフォルトリサイズ: #{resize}"
|
223
|
+
return
|
224
|
+
end
|
225
|
+
|
226
|
+
# サブコマンド: set-dir INPUT_DIR OUTPUT_DIR
|
227
|
+
if args.first.to_s == 'set-dir'
|
228
|
+
_, input_dir, output_dir = args
|
229
|
+
unless input_dir && output_dir
|
230
|
+
say "使い方: images-convert config set-dir INPUT_DIR OUTPUT_DIR", :red
|
231
|
+
exit 1
|
232
|
+
end
|
233
|
+
ImagesConvert::Configuration.save_directories(input_dir: input_dir, output_dir: output_dir)
|
234
|
+
say "ディレクトリ設定を変更しました:"
|
235
|
+
say " デフォルト入力ディレクトリ: #{input_dir}"
|
236
|
+
say " デフォルト出力ディレクトリ: #{output_dir}"
|
237
|
+
return
|
238
|
+
end
|
239
|
+
|
240
|
+
if args.first.to_s == 'set-latest-mode'
|
241
|
+
_, mode = args
|
242
|
+
unless mode
|
243
|
+
say "使い方: images-convert config set-latest-mode MODE (mtime/exif)", :red
|
244
|
+
exit 1
|
245
|
+
end
|
246
|
+
|
247
|
+
normalized = mode.to_s.downcase
|
248
|
+
begin
|
249
|
+
ImagesConvert::Configuration.save_latest_mode(normalized)
|
250
|
+
rescue ArgumentError => e
|
251
|
+
say "エラー: #{e.message}", :red
|
252
|
+
exit 1
|
253
|
+
end
|
254
|
+
|
255
|
+
say "最新判定モードを #{normalized} に設定しました。"
|
256
|
+
return
|
257
|
+
end
|
258
|
+
|
259
|
+
if args.first.to_s == 'set-waifu2x'
|
260
|
+
new_scale = if options[:scale]
|
261
|
+
clamp_waifu2x_scale(options[:scale])
|
262
|
+
else
|
263
|
+
ImagesConvert::Configuration.default_waifu2x_scale
|
264
|
+
end
|
265
|
+
|
266
|
+
new_noise = if options[:noise]
|
267
|
+
clamp_waifu2x_noise(options[:noise])
|
268
|
+
else
|
269
|
+
ImagesConvert::Configuration.default_waifu2x_noise
|
270
|
+
end
|
271
|
+
|
272
|
+
new_model = options[:model] || ImagesConvert::Configuration.default_waifu2x_model
|
273
|
+
|
274
|
+
auto_scale_default = ImagesConvert::Configuration.default_waifu2x_auto_scale_a4
|
275
|
+
new_auto_scale = options[:auto_scale_a4].nil? ? auto_scale_default : options[:auto_scale_a4]
|
276
|
+
|
277
|
+
a4_print_default = ImagesConvert::Configuration.default_waifu2x_a4_print
|
278
|
+
new_a4_print = options[:a4_print].nil? ? a4_print_default : options[:a4_print]
|
279
|
+
|
280
|
+
if options[:image_format]
|
281
|
+
normalized_format = normalize_waifu2x_output_format(options[:image_format])
|
282
|
+
unless %w[png jpg jpeg webp].include?(normalized_format)
|
283
|
+
say "エラー: waifu2x 出力フォーマットは png/jpg/jpeg/webp のいずれかを指定してください。", :red
|
284
|
+
exit 1
|
285
|
+
end
|
286
|
+
end
|
287
|
+
new_image_format = if options[:image_format]
|
288
|
+
normalize_waifu2x_output_format(options[:image_format])
|
289
|
+
else
|
290
|
+
normalize_waifu2x_output_format(ImagesConvert::Configuration.default_waifu2x_image_format)
|
291
|
+
end
|
292
|
+
|
293
|
+
new_on_error = options[:on_error] || ImagesConvert::Configuration.default_waifu2x_on_error
|
294
|
+
|
295
|
+
ImagesConvert::Configuration.save_waifu2x_settings(
|
296
|
+
scale: new_scale,
|
297
|
+
noise: new_noise,
|
298
|
+
model: new_model,
|
299
|
+
auto_scale_a4: new_auto_scale,
|
300
|
+
a4_print: new_a4_print,
|
301
|
+
image_format: new_image_format,
|
302
|
+
on_error: new_on_error
|
303
|
+
)
|
304
|
+
|
305
|
+
say "waifu2x 設定を更新しました:"
|
306
|
+
say " 拡大率: #{new_scale}"
|
307
|
+
say " ノイズ除去レベル: #{new_noise}"
|
308
|
+
say " モデル: #{new_model}"
|
309
|
+
say " A4自動スケール: #{new_auto_scale}"
|
310
|
+
say " A4レイアウトPDF: #{new_a4_print}"
|
311
|
+
say " 出力フォーマット: #{new_image_format}"
|
312
|
+
say " エラー時の動作: #{new_on_error}"
|
313
|
+
return
|
314
|
+
end
|
315
|
+
|
316
|
+
if args.first.to_s == 'set-output'
|
317
|
+
new_image_quality = if options[:image_quality]
|
318
|
+
clamp_output_quality(options[:image_quality])
|
319
|
+
else
|
320
|
+
ImagesConvert::Configuration.default_output_image_quality
|
321
|
+
end
|
322
|
+
|
323
|
+
new_pdf_quality = if options[:pdf_quality]
|
324
|
+
clamp_output_quality(options[:pdf_quality])
|
325
|
+
else
|
326
|
+
ImagesConvert::Configuration.default_output_pdf_quality
|
327
|
+
end
|
328
|
+
|
329
|
+
new_pdf_compression = if options[:pdf_compression]
|
330
|
+
normalized = options[:pdf_compression].to_s.strip.downcase
|
331
|
+
%w[jpeg jpeg2000 zip].include?(normalized) ? normalized : ImagesConvert::Configuration.default_output_pdf_compression
|
332
|
+
else
|
333
|
+
ImagesConvert::Configuration.default_output_pdf_compression
|
334
|
+
end
|
335
|
+
|
336
|
+
new_pdf_density = if options[:pdf_density]
|
337
|
+
clamp_pdf_density(options[:pdf_density])
|
338
|
+
else
|
339
|
+
ImagesConvert::Configuration.default_output_pdf_density
|
340
|
+
end
|
341
|
+
|
342
|
+
ImagesConvert::Configuration.save_output_settings(
|
343
|
+
image_quality: new_image_quality,
|
344
|
+
pdf_quality: new_pdf_quality,
|
345
|
+
pdf_compression: new_pdf_compression,
|
346
|
+
pdf_density: new_pdf_density
|
347
|
+
)
|
348
|
+
|
349
|
+
say "出力設定を更新しました:"
|
350
|
+
say " 画像品質: #{new_image_quality}"
|
351
|
+
say " PDF 品質: #{new_pdf_quality}"
|
352
|
+
say " PDF 圧縮方式: #{new_pdf_compression}"
|
353
|
+
say " PDF DPI: #{new_pdf_density}"
|
354
|
+
return
|
355
|
+
end
|
356
|
+
|
357
|
+
# それ以外(引数なし)は現在の設定を表示
|
358
|
+
settings = ImagesConvert::Configuration.show_all_settings
|
359
|
+
|
360
|
+
# 初回実行時はメッセージが既に表示されているので、改行だけ
|
361
|
+
puts "" unless ImagesConvert::Configuration.first_run_message_displayed?
|
362
|
+
|
363
|
+
show_config_summary(settings)
|
364
|
+
|
365
|
+
say "設定を変更するには:"
|
366
|
+
say " images-convert set FROM TO RESIZE"
|
367
|
+
say " images-convert set-dir INPUT_DIR OUTPUT_DIR"
|
368
|
+
say " images-convert set-latest-mode MODE (mtime/exif)"
|
369
|
+
say " images-convert set-waifu2x [--scale N --noise N --model TYPE --no-auto-scale-a4 --no-a4-print --image-format FORMAT --on-error skip]"
|
370
|
+
say " images-convert set-output [--image-quality N --pdf-quality N --pdf-compression TYPE --pdf-density DPI]"
|
371
|
+
end
|
372
|
+
|
373
|
+
desc 'setup', '対話形式で設定を更新'
|
374
|
+
def setup
|
375
|
+
say '=== images-convert 設定ウィザード ===', :green
|
376
|
+
say 'Enter キーのみで現在の設定値を維持できます。'
|
377
|
+
say ''
|
378
|
+
|
379
|
+
ImagesConvert::Configuration.load
|
380
|
+
|
381
|
+
updated_sections = []
|
382
|
+
changes = []
|
383
|
+
|
384
|
+
if prompt_yes_no('1) 入出力形式とリサイズを変更しますか?', default: true)
|
385
|
+
current_from = ImagesConvert::Configuration.default_from
|
386
|
+
current_to = ImagesConvert::Configuration.default_to
|
387
|
+
current_resize = ImagesConvert::Configuration.default_resize
|
388
|
+
|
389
|
+
new_from = prompt_string(' 入力形式', current_from)
|
390
|
+
new_to = prompt_string(' 出力形式', current_to)
|
391
|
+
new_resize = prompt_string(' リサイズ値', current_resize)
|
392
|
+
|
393
|
+
if [new_from, new_to, new_resize] != [current_from, current_to, current_resize]
|
394
|
+
record_change(changes, 'デフォルト入力形式', current_from, new_from) if new_from != current_from
|
395
|
+
record_change(changes, 'デフォルト出力形式', current_to, new_to) if new_to != current_to
|
396
|
+
record_change(changes, 'デフォルトリサイズ', current_resize, new_resize) if new_resize != current_resize
|
397
|
+
ImagesConvert::Configuration.save_defaults(from: new_from, to: new_to, resize: new_resize)
|
398
|
+
updated_sections << '基本設定'
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
if prompt_yes_no('2) 入出力ディレクトリを変更しますか?', default: false)
|
403
|
+
current_input = ImagesConvert::Configuration.default_input_dir
|
404
|
+
current_output = ImagesConvert::Configuration.default_output_dir
|
405
|
+
|
406
|
+
new_input = prompt_path(' 入力ディレクトリ', current_input)
|
407
|
+
new_output = prompt_path(' 出力ディレクトリ', current_output)
|
408
|
+
|
409
|
+
if [new_input, new_output] != [current_input, current_output]
|
410
|
+
record_change(changes, 'デフォルト入力ディレクトリ', current_input, new_input) if new_input != current_input
|
411
|
+
record_change(changes, 'デフォルト出力ディレクトリ', current_output, new_output) if new_output != current_output
|
412
|
+
ImagesConvert::Configuration.save_directories(input_dir: new_input, output_dir: new_output)
|
413
|
+
updated_sections << 'ディレクトリ'
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
if prompt_yes_no('3) 最新判定モードを変更しますか?', default: false)
|
418
|
+
current_latest = ImagesConvert::Configuration.default_latest_mode
|
419
|
+
new_latest = prompt_choice(' 最新判定モード (mtime/exif)', current_latest, %w[mtime exif])
|
420
|
+
|
421
|
+
if new_latest != current_latest
|
422
|
+
record_change(changes, '最新判定モード', current_latest, new_latest)
|
423
|
+
ImagesConvert::Configuration.save_latest_mode(new_latest)
|
424
|
+
updated_sections << '最新判定モード'
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
if prompt_yes_no('4) 出力品質を変更しますか?', default: false)
|
429
|
+
current_image_quality = ImagesConvert::Configuration.default_output_image_quality
|
430
|
+
current_pdf_quality = ImagesConvert::Configuration.default_output_pdf_quality
|
431
|
+
current_pdf_compression = ImagesConvert::Configuration.default_output_pdf_compression
|
432
|
+
current_pdf_density = ImagesConvert::Configuration.default_output_pdf_density
|
433
|
+
|
434
|
+
new_image_quality = prompt_integer(' 出力画像品質 (1-100)', current_image_quality, min: 1, max: 100)
|
435
|
+
new_pdf_quality = prompt_integer(' PDF 品質 (1-100)', current_pdf_quality, min: 1, max: 100)
|
436
|
+
new_pdf_compression = prompt_choice(' PDF 圧縮方式 (jpeg/jpeg2000/zip)', current_pdf_compression, %w[jpeg jpeg2000 zip])
|
437
|
+
new_pdf_density = prompt_integer(' PDF DPI (72-1200)', current_pdf_density, min: 72, max: 1200)
|
438
|
+
|
439
|
+
new_image_quality = clamp_output_quality(new_image_quality)
|
440
|
+
new_pdf_quality = clamp_output_quality(new_pdf_quality)
|
441
|
+
new_pdf_density = clamp_pdf_density(new_pdf_density)
|
442
|
+
|
443
|
+
if [new_image_quality, new_pdf_quality, new_pdf_compression, new_pdf_density] != [current_image_quality, current_pdf_quality, current_pdf_compression, current_pdf_density]
|
444
|
+
record_change(changes, '出力画像品質', current_image_quality, new_image_quality) if new_image_quality != current_image_quality
|
445
|
+
record_change(changes, 'PDF 品質', current_pdf_quality, new_pdf_quality) if new_pdf_quality != current_pdf_quality
|
446
|
+
record_change(changes, 'PDF 圧縮方式', current_pdf_compression, new_pdf_compression) if new_pdf_compression != current_pdf_compression
|
447
|
+
record_change(changes, 'PDF DPI', current_pdf_density, new_pdf_density) if new_pdf_density != current_pdf_density
|
448
|
+
ImagesConvert::Configuration.save_output_settings(
|
449
|
+
image_quality: new_image_quality,
|
450
|
+
pdf_quality: new_pdf_quality,
|
451
|
+
pdf_compression: new_pdf_compression,
|
452
|
+
pdf_density: new_pdf_density
|
453
|
+
)
|
454
|
+
updated_sections << '出力品質'
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
if prompt_yes_no('5) waifu2x の設定を変更しますか?', default: false)
|
459
|
+
current_scale = ImagesConvert::Configuration.default_waifu2x_scale
|
460
|
+
current_noise = ImagesConvert::Configuration.default_waifu2x_noise
|
461
|
+
current_model = ImagesConvert::Configuration.default_waifu2x_model
|
462
|
+
current_auto_scale = ImagesConvert::Configuration.default_waifu2x_auto_scale_a4
|
463
|
+
current_a4_print = ImagesConvert::Configuration.default_waifu2x_a4_print
|
464
|
+
current_image_format = ImagesConvert::Configuration.default_waifu2x_image_format
|
465
|
+
current_on_error = ImagesConvert::Configuration.default_waifu2x_on_error
|
466
|
+
|
467
|
+
new_scale = prompt_integer(' waifu2x 拡大率 (1-4)', current_scale, min: 1, max: 4)
|
468
|
+
new_noise = prompt_integer(' waifu2x ノイズ除去レベル (0-4)', current_noise, min: 0, max: 4)
|
469
|
+
new_model = prompt_string(' waifu2x モデル (anime/photo/anime_rgb/cunet/upresnet10)', current_model)
|
470
|
+
new_auto_scale = prompt_yes_no(' A4自動スケールを有効にしますか?', default: current_auto_scale, current_value: current_auto_scale)
|
471
|
+
new_a4_print = prompt_yes_no(' A4レイアウトPDFを適用しますか?', default: current_a4_print, current_value: current_a4_print)
|
472
|
+
new_image_format_raw = prompt_choice(' waifu2x 出力フォーマット (png/jpg/webp)', current_image_format, %w[png jpg jpeg webp])
|
473
|
+
new_on_error = prompt_choice(' waifu2x エラー時の動作 (stop/skip)', current_on_error, %w[stop skip])
|
474
|
+
|
475
|
+
new_scale = clamp_waifu2x_scale(new_scale)
|
476
|
+
new_noise = clamp_waifu2x_noise(new_noise)
|
477
|
+
new_image_format = normalize_waifu2x_output_format(new_image_format_raw)
|
478
|
+
current_image_format_normalized = normalize_waifu2x_output_format(current_image_format)
|
479
|
+
|
480
|
+
if [new_scale, new_noise, new_model, new_auto_scale, new_a4_print, new_image_format, new_on_error] != [current_scale, current_noise, current_model, current_auto_scale, current_a4_print, current_image_format_normalized, current_on_error]
|
481
|
+
record_change(changes, 'waifu2x 既定倍率', current_scale, new_scale) if new_scale != current_scale
|
482
|
+
record_change(changes, 'waifu2x ノイズ除去', current_noise, new_noise) if new_noise != current_noise
|
483
|
+
record_change(changes, 'waifu2x モデル', current_model, new_model) if new_model != current_model
|
484
|
+
record_change(changes, 'waifu2x A4自動スケール', current_auto_scale, new_auto_scale) if new_auto_scale != current_auto_scale
|
485
|
+
record_change(changes, 'waifu2x A4レイアウトPDF', current_a4_print, new_a4_print) if new_a4_print != current_a4_print
|
486
|
+
record_change(changes, 'waifu2x 出力フォーマット', current_image_format_normalized, new_image_format) if new_image_format != current_image_format_normalized
|
487
|
+
record_change(changes, 'waifu2x エラー時動作', current_on_error, new_on_error) if new_on_error != current_on_error
|
488
|
+
ImagesConvert::Configuration.save_waifu2x_settings(
|
489
|
+
scale: new_scale,
|
490
|
+
noise: new_noise,
|
491
|
+
model: new_model,
|
492
|
+
auto_scale_a4: new_auto_scale,
|
493
|
+
a4_print: new_a4_print,
|
494
|
+
image_format: new_image_format,
|
495
|
+
on_error: new_on_error
|
496
|
+
)
|
497
|
+
updated_sections << 'waifu2x'
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
if updated_sections.empty?
|
502
|
+
say ''
|
503
|
+
say '設定は変更されませんでした。', :yellow
|
504
|
+
else
|
505
|
+
say ''
|
506
|
+
say '更新された設定:'
|
507
|
+
updated_sections.each do |label|
|
508
|
+
say " - #{label}"
|
509
|
+
end
|
510
|
+
if changes.any?
|
511
|
+
say ''
|
512
|
+
say '変更点の詳細:'
|
513
|
+
changes.each do |change|
|
514
|
+
say " - #{change[:label]}: #{change[:before]} => #{set_color(change[:after], :green)}"
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
say ''
|
520
|
+
invoke :config
|
521
|
+
end
|
522
|
+
|
523
|
+
desc 'set FROM TO [RESIZE]', '変換形式やリサイズ値の既定値を設定'
|
524
|
+
def set(from = nil, to = nil, resize = nil)
|
525
|
+
unless from && to
|
526
|
+
say "使い方: imgconv set FROM TO [RESIZE]", :red
|
527
|
+
exit 1
|
528
|
+
end
|
529
|
+
|
530
|
+
resize ||= ImagesConvert::Configuration.default_resize
|
531
|
+
config('set', from, to, resize)
|
532
|
+
end
|
533
|
+
|
534
|
+
desc 'set-dir INPUT_DIR OUTPUT_DIR', '入出力ディレクトリの既定値を設定'
|
535
|
+
def set_dir(input_dir = nil, output_dir = nil)
|
536
|
+
unless input_dir && output_dir
|
537
|
+
say "使い方: imgconv set-dir INPUT_DIR OUTPUT_DIR", :red
|
538
|
+
exit 1
|
539
|
+
end
|
540
|
+
|
541
|
+
config('set-dir', input_dir, output_dir)
|
542
|
+
end
|
543
|
+
|
544
|
+
desc 'set-latest-mode MODE', '最新件数選定時の判定基準を設定 (mtime/exif)'
|
545
|
+
def set_latest_mode(mode = nil)
|
546
|
+
unless mode
|
547
|
+
say "使い方: imgconv set-latest-mode MODE (mtime/exif)", :red
|
548
|
+
exit 1
|
549
|
+
end
|
550
|
+
|
551
|
+
normalized = mode.to_s.downcase
|
552
|
+
unless %w[mtime exif].include?(normalized)
|
553
|
+
say "エラー: MODE には mtime または exif を指定してください。", :red
|
554
|
+
exit 1
|
555
|
+
end
|
556
|
+
|
557
|
+
ImagesConvert::Configuration.save_latest_mode(normalized)
|
558
|
+
say "最新判定モードを #{normalized} に設定しました。"
|
559
|
+
end
|
560
|
+
|
561
|
+
desc 'cleanup', '設定ファイルとディレクトリを削除'
|
562
|
+
def cleanup
|
563
|
+
ImagesConvert::Cleanup.run
|
564
|
+
end
|
565
|
+
|
566
|
+
# 自己アンインストール: 設定削除 → gem アンインストール
|
567
|
+
map 'self-uninstall' => :self_uninstall
|
568
|
+
desc 'self-uninstall', '設定を削除して gem もアンインストール'
|
569
|
+
def self_uninstall
|
570
|
+
ImagesConvert::Cleanup.run
|
571
|
+
say ''
|
572
|
+
say 'images-convert の gem をアンインストールしますか? [Y/n]'
|
573
|
+
print '> '
|
574
|
+
answer = (STDIN.gets || '').strip
|
575
|
+
answer = 'y' if answer.empty?
|
576
|
+
if answer.downcase.start_with?('y')
|
577
|
+
system('gem uninstall images-convert')
|
578
|
+
else
|
579
|
+
say 'gem のアンインストールをキャンセルしました。'
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
desc 'convert TARGET [DEST_DIR]', '単一ファイル変換(TARGET=ファイル名/番号)または一括変換(TARGET=入力ディレクトリ, DEST_DIR=出力ディレクトリ)'
|
584
|
+
long_desc <<~DESC
|
585
|
+
使い方:
|
586
|
+
|
587
|
+
images-convert [convert] TARGET [DEST_DIR] [オプション]
|
588
|
+
imgconv [convert] TARGET [DEST_DIR] [オプション]
|
589
|
+
|
590
|
+
動作:
|
591
|
+
- TARGET がファイル名/番号の場合は単一ファイル変換(-i/-o で入出力ディレクトリ指定可)
|
592
|
+
- TARGET がディレクトリの場合は一括変換(DEST_DIR 未指定時は in-place 変換)
|
593
|
+
|
594
|
+
主なオプション:
|
595
|
+
-i, --input-dir=DIR 入力ディレクトリ(単一ファイル時)
|
596
|
+
-o, --output-dir=DIR 出力ディレクトリ(単一ファイル時)
|
597
|
+
-d, --delete 変換後に元ファイルを削除
|
598
|
+
-f, --overwrite 既存の出力ファイルがある場合に上書き
|
599
|
+
-s, --suffix=SUFFIX 出力ファイル名の末尾に付与(例: _web)
|
600
|
+
--recursive ディレクトリを再帰的に処理(一括変換時)
|
601
|
+
|
602
|
+
例:
|
603
|
+
# 1枚だけ変換(番号指定、入出力ディレクトリや変換形式は既定値)
|
604
|
+
images-convert [convert] 1234
|
605
|
+
# または短縮コマンド
|
606
|
+
imgconv [convert] 1234
|
607
|
+
|
608
|
+
# 1枚だけ変換(ファイル名指定、入出力ディレクトリを明示)
|
609
|
+
images-convert [convert] IMG_0001 ~/Downloads ~/Downloads
|
610
|
+
|
611
|
+
# 一括変換(in-place)
|
612
|
+
images-convert [convert] ./photos
|
613
|
+
|
614
|
+
# 一括変換(出力先を別ディレクトリ、再帰処理、サフィックス付与)
|
615
|
+
images-convert [convert] ./photos ./export --recursive --suffix _resized
|
616
|
+
DESC
|
617
|
+
method_option :input_dir, aliases: '-i', desc: '入力ディレクトリ(デフォルト: 設定値または ~/Downloads)'
|
618
|
+
method_option :output, aliases: '-o', desc: '出力ファイルパス(単一ファイル変換時、ディレクトリ指定時は出力ディレクトリ)'
|
619
|
+
method_option :output_dir, type: :string, desc: '出力ディレクトリ(単一ファイル時)'
|
620
|
+
method_option :delete, aliases: '-d', type: :boolean, desc: '変換後に元のファイルを削除'
|
621
|
+
method_option :overwrite, aliases: '-f', type: :boolean, desc: '既存の出力ファイルがある場合に上書き'
|
622
|
+
method_option :suffix, aliases: '-s', type: :string, desc: '出力ファイル名の末尾に付与するサフィックス(例: _converted)'
|
623
|
+
method_option :recursive, type: :boolean, desc: 'ディレクトリを再帰的に処理(一括変換時)'
|
624
|
+
method_option :resize, type: :string, desc: 'リサイズ値(例: 1920x, 50%)'
|
625
|
+
method_option :output_mode, type: :string, enum: %w[images pdf both], default: 'images', desc: '出力モード(images/pdf/both)'
|
626
|
+
method_option :waifu2x_scale, type: :numeric, desc: 'waifu2x の拡大率 (1-4)'
|
627
|
+
method_option :waifu2x_noise, type: :numeric, desc: 'waifu2x のノイズ除去レベル (0-4)'
|
628
|
+
method_option :waifu2x_model, type: :string, desc: 'waifu2x モデルエイリアス(photo/anime/anime_rgb/cunet/upresnet10)'
|
629
|
+
method_option :waifu2x_processor, type: :string, default: 'waifu2x_ncnn_vulkan', desc: 'waifu2x プロセッサ'
|
630
|
+
method_option :waifu2x_bin, type: :string, desc: 'waifu2x バイナリへのパス'
|
631
|
+
method_option :waifu2x_models_path, type: :string, desc: 'waifu2x モデルディレクトリへのパス'
|
632
|
+
method_option :waifu2x_download_url, type: :string, desc: 'waifu2x 自動ダウンロード用URL'
|
633
|
+
method_option :waifu2x_auto_scale_a4, type: :boolean, desc: 'A4印刷を目安に拡大率を自動調整'
|
634
|
+
method_option :waifu2x_a4_print, type: :boolean, desc: 'A4レイアウトPDFを適用(--output-mode pdf/both)'
|
635
|
+
method_option :waifu2x_image_format, type: :string, desc: 'waifu2x 出力画像フォーマット(png/jpg/webp)'
|
636
|
+
method_option :waifu2x_verbose, type: :boolean, desc: 'waifu2x の詳細ログを表示'
|
637
|
+
method_option :waifu2x_on_error, type: :string, enum: %w[skip stop], desc: 'waifu2x 処理失敗時の動作'
|
638
|
+
method_option :quality, type: :numeric, desc: '出力画像の品質 (0-100, JPEG/WebP など)'
|
639
|
+
method_option :pdf_compression, type: :string, enum: %w[jpeg jpeg2000 zip], desc: 'PDF 埋め込み画像の圧縮方式'
|
640
|
+
method_option :pdf_quality, type: :numeric, desc: 'PDF 埋め込み画像の品質(0-100)'
|
641
|
+
method_option :pdf_density, type: :numeric, desc: 'PDF ページの DPI'
|
642
|
+
# 追加機能: 最新N件/全件変換をサポート(-N, -0/--all)
|
643
|
+
method_option :latest, type: :numeric, desc: '最新の画像ファイル N 件を変換(入力ディレクトリ内から)'
|
644
|
+
method_option :latest_exif, type: :boolean, default: false, negatable: false, desc: '最新 N 件の選定を EXIF 撮影日時に基づいて行う(--latest と併用)'
|
645
|
+
method_option :all, type: :boolean, default: false, negatable: false, desc: '入力ディレクトリ内の対象画像をすべて変換'
|
646
|
+
# オプションで from/to を一時的に上書き(ショートハンドや内部用途)
|
647
|
+
method_option :from, type: :string, desc: '入力形式(設定を一時的に上書き)'
|
648
|
+
method_option :to, type: :string, desc: '出力形式(設定を一時的に上書き)'
|
649
|
+
def convert(target = nil, dest_dir = nil)
|
650
|
+
from = options[:from] || ImagesConvert::Configuration.default_from
|
651
|
+
to = options[:to] || ImagesConvert::Configuration.default_to
|
652
|
+
resize = options[:resize] || ImagesConvert::Configuration.default_resize
|
653
|
+
latest_mode = latest_mode_from_options
|
654
|
+
pdf_density = clamp_pdf_density(options[:pdf_density])
|
655
|
+
|
656
|
+
# 最新N件/全件変換(入力ディレクトリを走査)
|
657
|
+
if options[:all] || (options.key?(:latest) && options[:latest].to_s.strip != '')
|
658
|
+
# コマンドライン引数でディレクトリが指定されている場合は優先
|
659
|
+
input_dir = if target && File.directory?(target)
|
660
|
+
File.expand_path(target)
|
661
|
+
else
|
662
|
+
options[:input_dir] || ImagesConvert::Configuration.default_input_dir
|
663
|
+
end
|
664
|
+
output_dir = if dest_dir
|
665
|
+
File.expand_path(dest_dir)
|
666
|
+
else
|
667
|
+
options[:output_dir] || ImagesConvert::Configuration.default_output_dir
|
668
|
+
end
|
669
|
+
|
670
|
+
unless Dir.exist?(input_dir)
|
671
|
+
say "エラー: 入力ディレクトリ '#{input_dir}' が存在しません。", :red
|
672
|
+
exit 1
|
673
|
+
end
|
674
|
+
|
675
|
+
# 入力候補の列挙
|
676
|
+
exts = candidate_exts(from)
|
677
|
+
candidates = list_input_candidates(input_dir, exts)
|
678
|
+
|
679
|
+
if candidates.empty?
|
680
|
+
say "対象ファイルが見つかりませんでした (#{input_dir}, 拡張子: #{exts.uniq.join('/')})."
|
681
|
+
return
|
682
|
+
end
|
683
|
+
|
684
|
+
# 最新順の並べ替え(EXIF 対応)
|
685
|
+
sorted, exif_fallback_used = sort_candidates_by_mode(candidates, latest_mode)
|
686
|
+
sorted = sorted.uniq
|
687
|
+
mode, count = resolve_selection(sorted)
|
688
|
+
case mode
|
689
|
+
when :latest
|
690
|
+
say "最新の #{count} 件を変換します (#{input_dir}, 基準: #{latest_mode})"
|
691
|
+
if latest_mode == 'exif' && exif_fallback_used
|
692
|
+
say ' ※ EXIF 情報がないファイルはファイル更新日時で判定しました。'
|
693
|
+
end
|
694
|
+
when :all
|
695
|
+
say "すべての画像ファイルを変換します (#{input_dir})"
|
696
|
+
else
|
697
|
+
say "エラー: --latest N (N>=1) または --all / -0 を指定してください(--latest 0 は全件として扱います)", :red
|
698
|
+
exit 1
|
699
|
+
end
|
700
|
+
|
701
|
+
targets = sorted.first(count)
|
702
|
+
|
703
|
+
targets.each do |filepath|
|
704
|
+
base = File.basename(filepath, '.*')
|
705
|
+
perform_conversion(base, from, to, resize, input_dir, output_dir, suffix: options[:suffix], pdf_density: pdf_density)
|
706
|
+
end
|
707
|
+
return
|
708
|
+
end
|
709
|
+
|
710
|
+
# 一括変換(ディレクトリ指定)
|
711
|
+
if target && File.directory?(target)
|
712
|
+
src_dir = File.expand_path(target)
|
713
|
+
out_dir = dest_dir ? File.expand_path(dest_dir) : src_dir # 単一引数時は in-place
|
714
|
+
unless Dir.exist?(src_dir)
|
715
|
+
say "エラー: 入力ディレクトリ '#{src_dir}' が存在しません。", :red
|
716
|
+
exit 1
|
717
|
+
end
|
718
|
+
FileUtils.mkdir_p(out_dir)
|
719
|
+
perform_bulk_conversion(
|
720
|
+
src_dir,
|
721
|
+
out_dir,
|
722
|
+
from,
|
723
|
+
to,
|
724
|
+
resize,
|
725
|
+
overwrite: options[:overwrite],
|
726
|
+
suffix: options[:suffix],
|
727
|
+
recursive: options[:recursive],
|
728
|
+
delete: options[:delete],
|
729
|
+
pdf_density: pdf_density
|
730
|
+
)
|
731
|
+
return
|
732
|
+
end
|
733
|
+
|
734
|
+
# 単一ファイル変換: target はファイル名または番号(IMG_1234 を想定)
|
735
|
+
input_dir = options[:input_dir] || ImagesConvert::Configuration.default_input_dir
|
736
|
+
output_dir = if dest_dir
|
737
|
+
File.expand_path(dest_dir)
|
738
|
+
else
|
739
|
+
options[:output_dir] || ImagesConvert::Configuration.default_output_dir
|
740
|
+
end
|
741
|
+
|
742
|
+
# --output オプションが指定されている場合は出力ファイルを直接指定
|
743
|
+
if options[:output] && target
|
744
|
+
# --output で指定されたパスをフルパスに変換
|
745
|
+
output_file_path = File.expand_path(options[:output])
|
746
|
+
output_dir = File.dirname(output_file_path)
|
747
|
+
output_basename = File.basename(output_file_path, '.*')
|
748
|
+
# 出力ファイル名を指定されたものに変更
|
749
|
+
def self.custom_output_filename(input_file, to, output_dir, custom_basename, suffix: nil, create_dir: true)
|
750
|
+
basename = suffix && !suffix.empty? ? "#{custom_basename}#{suffix}" : custom_basename
|
751
|
+
FileUtils.mkdir_p(output_dir) if create_dir && !Dir.exist?(output_dir)
|
752
|
+
"#{output_dir}/#{basename}.#{to.downcase}"
|
753
|
+
end
|
754
|
+
|
755
|
+
# 入力がフルパス/実在パスなら input_dir 検証をスキップしてそのまま使う
|
756
|
+
input_path_like = target.to_s.include?('/') || File.exist?(target.to_s)
|
757
|
+
input_file = nil
|
758
|
+
if input_path_like
|
759
|
+
candidate = File.expand_path(target.to_s)
|
760
|
+
if File.exist?(candidate)
|
761
|
+
input_file = candidate
|
762
|
+
end
|
763
|
+
end
|
764
|
+
|
765
|
+
# まだ見つからない場合は従来どおり input_dir + base 名から探索
|
766
|
+
unless input_file
|
767
|
+
validate_arguments(target, input_dir)
|
768
|
+
input_file = find_input_file(target, from, input_dir)
|
769
|
+
unless input_file
|
770
|
+
say "エラー: 指定されたファイル '#{target}' (#{from} 形式) は #{input_dir} に存在しません。", :red
|
771
|
+
exit 1
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
normalized_to = normalized_to_format(to)
|
776
|
+
output_file = custom_output_filename(input_file, normalized_to, output_dir, output_basename)
|
777
|
+
|
778
|
+
if attempt_waifu2x_single(
|
779
|
+
input_file: input_file,
|
780
|
+
normalized_to: normalized_to,
|
781
|
+
resize: resize,
|
782
|
+
output_path: output_file,
|
783
|
+
overwrite: options[:overwrite],
|
784
|
+
delete_original: options[:delete],
|
785
|
+
settings: waifu2x_runtime_settings(target_format: normalized_to),
|
786
|
+
explicit_scale: options[:waifu2x_scale]
|
787
|
+
)
|
788
|
+
return
|
789
|
+
end
|
790
|
+
|
791
|
+
if File.exist?(output_file) && !options[:overwrite]
|
792
|
+
say "- 既存のためスキップ: #{output_file}(--overwrite で上書き可能)", :yellow
|
793
|
+
return
|
794
|
+
end
|
795
|
+
|
796
|
+
say "#{input_file} を #{output_file} に変換しています..."
|
797
|
+
|
798
|
+
begin
|
799
|
+
image = MiniMagick::Image.open(input_file)
|
800
|
+
image.resize resize unless resize.empty?
|
801
|
+
image.format normalized_to
|
802
|
+
apply_image_quality(image, effective_output_quality)
|
803
|
+
image.write output_file
|
804
|
+
|
805
|
+
say "変換成功! 新しいファイルは #{output_file} です。", :green
|
806
|
+
|
807
|
+
if options[:delete]
|
808
|
+
FileUtils.rm(input_file)
|
809
|
+
say "元のファイル #{input_file} は削除されました。", :yellow
|
810
|
+
else
|
811
|
+
say '元のファイルは削除されませんでした。削除するには --delete オプションを使用してください。', :blue
|
812
|
+
end
|
813
|
+
rescue => e
|
814
|
+
say "エラー: 変換に失敗しました。#{e.message}", :red
|
815
|
+
say 'ImageMagick のインストール状況を確認してください。', :red
|
816
|
+
return
|
817
|
+
end
|
818
|
+
return
|
819
|
+
end
|
820
|
+
|
821
|
+
perform_conversion(target, from, to, resize, input_dir, output_dir, suffix: options[:suffix], pdf_density: pdf_density)
|
822
|
+
end
|
823
|
+
|
824
|
+
private
|
825
|
+
|
826
|
+
# 最新N件/全件の選定ロジック
|
827
|
+
# @return [Symbol,Integer] mode (:latest|:all|:none), count
|
828
|
+
def resolve_selection(sorted)
|
829
|
+
# --latest の明示(-N は preprocess で --latest N に展開済み)
|
830
|
+
latest_val = options[:latest]
|
831
|
+
if !(latest_val.nil? || latest_val.to_s.strip.empty?)
|
832
|
+
n = latest_val.to_i
|
833
|
+
return [:all, sorted.size] if n <= 0
|
834
|
+
return [:latest, n]
|
835
|
+
end
|
836
|
+
|
837
|
+
# --all
|
838
|
+
return [:all, sorted.size] if options[:all]
|
839
|
+
|
840
|
+
[:none, 0]
|
841
|
+
end
|
842
|
+
|
843
|
+
# 入力拡張子の候補(jpeg の場合は jpg と jpeg の両方、大文字拡張子も含めて検出)
|
844
|
+
def candidate_exts(from)
|
845
|
+
from.to_s.match?(/\Ajpe?g\z/i) ? %w[jpg jpeg JPG JPEG] : [from.downcase, from.upcase]
|
846
|
+
end
|
847
|
+
|
848
|
+
# 出力拡張子の正規化(jpeg 系は jpg に統一)
|
849
|
+
def normalized_to_format(to)
|
850
|
+
to.to_s.match?(/\Ajpe?g\z/i) ? 'jpg' : to.downcase
|
851
|
+
end
|
852
|
+
|
853
|
+
def validate_arguments(base_name, input_dir)
|
854
|
+
unless base_name
|
855
|
+
say 'エラー: ファイル名または番号を指定してください。', :red
|
856
|
+
exit 1
|
857
|
+
end
|
858
|
+
|
859
|
+
unless Dir.exist?(input_dir)
|
860
|
+
say "エラー: 入力ディレクトリ '#{input_dir}' が存在しません。", :red
|
861
|
+
exit 1
|
862
|
+
end
|
863
|
+
end
|
864
|
+
|
865
|
+
def perform_conversion(base_name, from, to, resize, input_dir, output_dir, suffix: nil, pdf_density: ImagesConvert::Configuration.default_output_pdf_density)
|
866
|
+
input_file = find_input_file(base_name, from, input_dir)
|
867
|
+
|
868
|
+
unless input_file
|
869
|
+
say "エラー: 指定されたファイル '#{base_name}' (#{from} 形式) は #{input_dir} に存在しません。", :red
|
870
|
+
exit 1
|
871
|
+
end
|
872
|
+
|
873
|
+
# 拡張子とフォーマット名の正規化(jpeg は jpg に統一)
|
874
|
+
normalized_to = normalized_to_format(to)
|
875
|
+
output_file = output_filename(input_file, normalized_to, output_dir, suffix: suffix)
|
876
|
+
|
877
|
+
if attempt_waifu2x_single(
|
878
|
+
input_file: input_file,
|
879
|
+
normalized_to: normalized_to,
|
880
|
+
resize: resize,
|
881
|
+
output_path: output_file,
|
882
|
+
overwrite: options[:overwrite],
|
883
|
+
delete_original: options[:delete],
|
884
|
+
settings: waifu2x_runtime_settings(target_format: normalized_to),
|
885
|
+
explicit_scale: options[:waifu2x_scale]
|
886
|
+
)
|
887
|
+
return
|
888
|
+
end
|
889
|
+
|
890
|
+
if File.exist?(output_file) && !options[:overwrite]
|
891
|
+
say "- 既存のためスキップ: #{output_file}(--overwrite で上書き可能)", :yellow
|
892
|
+
return
|
893
|
+
end
|
894
|
+
|
895
|
+
say "#{input_file} を #{output_file} に変換しています..."
|
896
|
+
|
897
|
+
# MiniMagick で変換(リサイズを追加)
|
898
|
+
begin
|
899
|
+
image = MiniMagick::Image.open(input_file)
|
900
|
+
image.resize resize unless resize.empty?
|
901
|
+
image.format normalized_to
|
902
|
+
apply_image_quality(image, effective_output_quality)
|
903
|
+
image.write output_file
|
904
|
+
|
905
|
+
say "変換成功! 新しいファイルは #{output_file} です。", :green
|
906
|
+
|
907
|
+
if options[:delete]
|
908
|
+
FileUtils.rm(input_file)
|
909
|
+
say "元のファイル #{input_file} は削除されました。", :yellow
|
910
|
+
else
|
911
|
+
say '元のファイルは削除されませんでした。削除するには --delete オプションを使用してください。', :blue
|
912
|
+
end
|
913
|
+
rescue => e
|
914
|
+
say "エラー: 変換に失敗しました。#{e.message}", :red
|
915
|
+
say 'ImageMagick のインストール状況を確認してください。', :red
|
916
|
+
# バッチ処理継続のため、ここでは終了せず次へ
|
917
|
+
return
|
918
|
+
end
|
919
|
+
end
|
920
|
+
|
921
|
+
def find_input_file(base_name, from, input_dir)
|
922
|
+
input_file = nil
|
923
|
+
|
924
|
+
normalized_exts = candidate_exts(from).flat_map do |ext|
|
925
|
+
ext = ext.delete_prefix('.')
|
926
|
+
[ext.downcase, ext.upcase]
|
927
|
+
end.uniq
|
928
|
+
|
929
|
+
if base_name.match?(/\A\d+\z/)
|
930
|
+
# 数字のみの場合、IMG_プレフィックスを試す
|
931
|
+
candidates = normalized_exts.map { |ext| "#{input_dir}/IMG_#{base_name}.#{ext}" }
|
932
|
+
else
|
933
|
+
# 数字以外の場合、そのままファイル名を試す
|
934
|
+
candidates = normalized_exts.map { |ext| "#{input_dir}/#{base_name}.#{ext}" } + [
|
935
|
+
"#{input_dir}/#{base_name}"
|
936
|
+
]
|
937
|
+
end
|
938
|
+
|
939
|
+
candidates.each do |candidate|
|
940
|
+
if File.exist?(candidate)
|
941
|
+
input_file = candidate
|
942
|
+
break
|
943
|
+
end
|
944
|
+
end
|
945
|
+
|
946
|
+
input_file
|
947
|
+
end
|
948
|
+
|
949
|
+
def output_filename(input_file, to, output_dir, suffix: nil, create_dir: true)
|
950
|
+
basename = File.basename(input_file, ".*")
|
951
|
+
basename = suffix && !suffix.empty? ? "#{basename}#{suffix}" : basename
|
952
|
+
FileUtils.mkdir_p(output_dir) if create_dir && !Dir.exist?(output_dir)
|
953
|
+
"#{output_dir}/#{basename}.#{to.downcase}"
|
954
|
+
end
|
955
|
+
|
956
|
+
# オプションから最新判定モードを決定する
|
957
|
+
def latest_mode_from_options
|
958
|
+
flag = options[:latest_exif]
|
959
|
+
flag = options['latest_exif'] if flag.nil?
|
960
|
+
flag = options['latest-exif'] if flag.nil?
|
961
|
+
flag ? 'exif' : ImagesConvert::Configuration.default_latest_mode
|
962
|
+
end
|
963
|
+
|
964
|
+
def waifu2x_runtime_settings(target_format: nil)
|
965
|
+
explicit_scale = options[:waifu2x_scale]
|
966
|
+
noise = options[:waifu2x_noise]
|
967
|
+
model = options[:waifu2x_model]
|
968
|
+
auto_scale_a4 = options.key?(:waifu2x_auto_scale_a4) ? options[:waifu2x_auto_scale_a4] : nil
|
969
|
+
a4_print = options.key?(:waifu2x_a4_print) ? options[:waifu2x_a4_print] : nil
|
970
|
+
image_format = options[:waifu2x_image_format]
|
971
|
+
on_error = options[:waifu2x_on_error]
|
972
|
+
|
973
|
+
{
|
974
|
+
scale: clamp_waifu2x_scale(explicit_scale || ImagesConvert::Configuration.default_waifu2x_scale),
|
975
|
+
noise: clamp_waifu2x_noise(noise.nil? ? ImagesConvert::Configuration.default_waifu2x_noise : noise.to_i),
|
976
|
+
model: model || ImagesConvert::Configuration.default_waifu2x_model,
|
977
|
+
auto_scale_a4: auto_scale_a4.nil? ? ImagesConvert::Configuration.default_waifu2x_auto_scale_a4 : auto_scale_a4,
|
978
|
+
a4_print: a4_print.nil? ? ImagesConvert::Configuration.default_waifu2x_a4_print : a4_print,
|
979
|
+
image_format: normalize_waifu2x_output_format(image_format || target_format || ImagesConvert::Configuration.default_waifu2x_image_format),
|
980
|
+
on_error: on_error || ImagesConvert::Configuration.default_waifu2x_on_error,
|
981
|
+
processor: options[:waifu2x_processor] || 'waifu2x_ncnn_vulkan',
|
982
|
+
download_url: options[:waifu2x_download_url],
|
983
|
+
bin: options[:waifu2x_bin],
|
984
|
+
models_path: options[:waifu2x_models_path],
|
985
|
+
quality: clamp_output_quality(options[:quality] || ImagesConvert::Configuration.default_output_image_quality)
|
986
|
+
}
|
987
|
+
end
|
988
|
+
|
989
|
+
def normalize_waifu2x_output_format(fmt)
|
990
|
+
return ImagesConvert::Configuration.default_waifu2x_image_format unless fmt
|
991
|
+
fmt.to_s.strip.downcase.then { |v| v == 'jpeg' ? 'jpg' : v }
|
992
|
+
end
|
993
|
+
|
994
|
+
def attempt_waifu2x_single(input_file:, normalized_to:, resize:, output_path:, overwrite:, delete_original:, settings:, explicit_scale: nil)
|
995
|
+
return false unless (options[:output_mode] || 'images').to_s == 'images'
|
996
|
+
return false unless waifu2x_supported_format?(normalized_to)
|
997
|
+
|
998
|
+
ratio = resize_ratio_for(input_file, resize)
|
999
|
+
return false if ratio <= 1.0 && explicit_scale.nil?
|
1000
|
+
|
1001
|
+
configured_scale = settings[:scale] || 1
|
1002
|
+
auto_scale = waifu2x_scale_from_ratio(ratio, nil)
|
1003
|
+
|
1004
|
+
scale = if explicit_scale
|
1005
|
+
clamp_waifu2x_scale(explicit_scale)
|
1006
|
+
else
|
1007
|
+
clamp_waifu2x_scale([auto_scale, configured_scale].max)
|
1008
|
+
end
|
1009
|
+
return false if scale <= 1
|
1010
|
+
|
1011
|
+
if File.exist?(output_path)
|
1012
|
+
unless overwrite
|
1013
|
+
say "- 既存のためスキップ: #{output_path}(--overwrite で上書き可能)", :yellow
|
1014
|
+
return true
|
1015
|
+
end
|
1016
|
+
FileUtils.rm_f(output_path)
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
resolved = resolve_waifu2x_paths(settings: settings)
|
1020
|
+
return false unless resolved
|
1021
|
+
|
1022
|
+
begin
|
1023
|
+
Dir.mktmpdir('imgconv-waifu2x-in') do |tmp_in|
|
1024
|
+
Dir.mktmpdir('imgconv-waifu2x-out') do |tmp_out|
|
1025
|
+
temp_input = File.join(tmp_in, File.basename(input_file))
|
1026
|
+
FileUtils.cp(input_file, temp_input)
|
1027
|
+
|
1028
|
+
Waifu2x::Processor.process_images(
|
1029
|
+
tmp_in,
|
1030
|
+
tmp_out,
|
1031
|
+
scale: scale,
|
1032
|
+
noise: settings[:noise],
|
1033
|
+
model: settings[:model],
|
1034
|
+
processor: settings[:processor].to_sym,
|
1035
|
+
waifu2x_bin: resolved[:bin],
|
1036
|
+
models_path: resolved[:models],
|
1037
|
+
recursive: false,
|
1038
|
+
auto_scale_a4: settings[:auto_scale_a4],
|
1039
|
+
pdf_density: clamp_pdf_density(options[:pdf_density]),
|
1040
|
+
verbose: options[:waifu2x_verbose],
|
1041
|
+
image_format: settings[:image_format],
|
1042
|
+
on_error: settings[:on_error]
|
1043
|
+
)
|
1044
|
+
|
1045
|
+
produced = Dir.glob(File.join(tmp_out, "*.#{settings[:image_format]}"))
|
1046
|
+
produced = produced.first || Dir.glob(File.join(tmp_out, '*')).first
|
1047
|
+
|
1048
|
+
unless produced && File.exist?(produced)
|
1049
|
+
say 'waifu2x の出力が見つかりませんでした。MiniMagick で再試行します。', :yellow
|
1050
|
+
return false
|
1051
|
+
end
|
1052
|
+
|
1053
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
1054
|
+
FileUtils.mv(produced, output_path, force: true)
|
1055
|
+
say "waifu2x を使用して #{output_path} を生成しました。", :green
|
1056
|
+
|
1057
|
+
enforce_resize_after_waifu2x(output_path, resize, settings[:quality])
|
1058
|
+
|
1059
|
+
if delete_original
|
1060
|
+
FileUtils.rm_f(input_file)
|
1061
|
+
say "元のファイル #{input_file} は削除されました。", :yellow
|
1062
|
+
end
|
1063
|
+
return true
|
1064
|
+
end
|
1065
|
+
end
|
1066
|
+
rescue StandardError => e
|
1067
|
+
safe_msg = begin
|
1068
|
+
e.message.to_s.encode('UTF-8', invalid: :replace, undef: :replace)
|
1069
|
+
rescue StandardError
|
1070
|
+
e.message.to_s
|
1071
|
+
end
|
1072
|
+
say "waifu2x の実行に失敗しました: #{safe_msg}。MiniMagick で再試行します。", :yellow
|
1073
|
+
false
|
1074
|
+
end
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
def waifu2x_supported_format?(normalized_to)
|
1078
|
+
%w[png jpg jpeg webp].include?(normalized_to.to_s.downcase)
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
def resolve_waifu2x_paths(settings:)
|
1082
|
+
Waifu2x::Setup.resolve_paths(
|
1083
|
+
waifu2x_bin: settings[:bin],
|
1084
|
+
models_path: settings[:models_path],
|
1085
|
+
auto_download: true,
|
1086
|
+
download_url: settings[:download_url],
|
1087
|
+
model: settings[:model]
|
1088
|
+
)
|
1089
|
+
rescue StandardError => e
|
1090
|
+
safe_msg = begin
|
1091
|
+
e.message.to_s.encode('UTF-8', invalid: :replace, undef: :replace)
|
1092
|
+
rescue StandardError
|
1093
|
+
e.message.to_s
|
1094
|
+
end
|
1095
|
+
say "waifu2x の準備に失敗しました: #{safe_msg}。MiniMagick でフォールバックします。", :yellow
|
1096
|
+
nil
|
1097
|
+
end
|
1098
|
+
|
1099
|
+
def resize_ratio_for(path, resize)
|
1100
|
+
resize = resize.to_s.strip
|
1101
|
+
return 1.0 if resize.empty?
|
1102
|
+
|
1103
|
+
if (percent = resize.match(/\A(\d+(?:\.\d+)?)%\z/))
|
1104
|
+
return percent[1].to_f / 100.0
|
1105
|
+
end
|
1106
|
+
|
1107
|
+
width, height = image_dimensions(path)
|
1108
|
+
return 1.0 if width.to_i <= 0 || height.to_i <= 0
|
1109
|
+
|
1110
|
+
cleaned = resize.tr('^0-9x', '')
|
1111
|
+
parts = cleaned.split('x', 2)
|
1112
|
+
target_w = parts[0].to_s.empty? ? nil : parts[0].to_i
|
1113
|
+
target_h = parts[1].to_s.empty? ? nil : parts[1].to_i
|
1114
|
+
|
1115
|
+
ratios = []
|
1116
|
+
ratios << (target_w.to_f / width) if target_w && width.positive?
|
1117
|
+
ratios << (target_h.to_f / height) if target_h && height.positive?
|
1118
|
+
ratio = ratios.compact.max
|
1119
|
+
ratio && ratio > 0 ? ratio : 1.0
|
1120
|
+
rescue StandardError
|
1121
|
+
1.0
|
1122
|
+
end
|
1123
|
+
|
1124
|
+
def image_dimensions(path)
|
1125
|
+
@_waifu2x_dimension_cache ||= {}
|
1126
|
+
return @_waifu2x_dimension_cache[path] if @_waifu2x_dimension_cache.key?(path)
|
1127
|
+
|
1128
|
+
image = MiniMagick::Image.ping(path)
|
1129
|
+
dims = [image.width.to_i, image.height.to_i]
|
1130
|
+
@_waifu2x_dimension_cache[path] = dims
|
1131
|
+
rescue StandardError
|
1132
|
+
@_waifu2x_dimension_cache[path] = [0, 0]
|
1133
|
+
end
|
1134
|
+
|
1135
|
+
def waifu2x_scale_from_ratio(ratio, explicit_scale)
|
1136
|
+
return clamp_waifu2x_scale(explicit_scale.to_i) if explicit_scale
|
1137
|
+
|
1138
|
+
return 1 if ratio <= 1.0
|
1139
|
+
return 2 if ratio <= 2.0
|
1140
|
+
4
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
def clamp_waifu2x_scale(value)
|
1144
|
+
v = value.to_i
|
1145
|
+
v = 4 if v > 4
|
1146
|
+
v = 1 if v < 1
|
1147
|
+
v == 3 ? 4 : v
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
def clamp_waifu2x_noise(value)
|
1151
|
+
v = value.to_i
|
1152
|
+
v = 4 if v > 4
|
1153
|
+
v = 0 if v < 0
|
1154
|
+
v
|
1155
|
+
end
|
1156
|
+
|
1157
|
+
def clamp_output_quality(value)
|
1158
|
+
return nil if value.nil?
|
1159
|
+
v = value.to_i
|
1160
|
+
v = 100 if v > 100
|
1161
|
+
v = 1 if v < 1
|
1162
|
+
v
|
1163
|
+
end
|
1164
|
+
|
1165
|
+
def effective_output_quality
|
1166
|
+
clamp_output_quality(options[:quality] || ImagesConvert::Configuration.default_output_image_quality)
|
1167
|
+
end
|
1168
|
+
|
1169
|
+
def apply_image_quality(image, quality)
|
1170
|
+
return if quality.nil?
|
1171
|
+
image.quality(quality)
|
1172
|
+
rescue StandardError
|
1173
|
+
# ignore
|
1174
|
+
end
|
1175
|
+
|
1176
|
+
def enforce_resize_after_waifu2x(path, resize, quality)
|
1177
|
+
resize = resize.to_s.strip
|
1178
|
+
return if resize.empty?
|
1179
|
+
|
1180
|
+
begin
|
1181
|
+
image = MiniMagick::Image.open(path)
|
1182
|
+
image.resize(resize)
|
1183
|
+
apply_image_quality(image, quality)
|
1184
|
+
image.write(path)
|
1185
|
+
rescue StandardError => e
|
1186
|
+
say "警告: waifu2x 出力の最終リサイズに失敗しました: #{e.message}", :yellow
|
1187
|
+
end
|
1188
|
+
end
|
1189
|
+
|
1190
|
+
# 入力ディレクトリから対象拡張子の候補を列挙
|
1191
|
+
def list_input_candidates(input_dir, exts)
|
1192
|
+
pattern = "*.{#{exts.join(',')}}"
|
1193
|
+
Dir.chdir(input_dir) { Dir.glob(pattern) }
|
1194
|
+
.map { |p| File.join(input_dir, p) }
|
1195
|
+
.select { |f| File.file?(f) }
|
1196
|
+
end
|
1197
|
+
|
1198
|
+
def perform_bulk_conversion(src_dir, out_dir, from, to, resize, overwrite:, suffix:, recursive:, delete:, pdf_density: ImagesConvert::Configuration.default_output_pdf_density)
|
1199
|
+
start_time = Time.now
|
1200
|
+
# 入力拡張子の候補(jpeg の場合は jpg と jpeg の両方)
|
1201
|
+
exts = candidate_exts(from)
|
1202
|
+
pattern = recursive ? "**/*.{#{exts.join(',')}}" : "*.{#{exts.join(',')}}"
|
1203
|
+
|
1204
|
+
candidates = Dir.chdir(src_dir) { Dir.glob(pattern) }.uniq
|
1205
|
+
.select { |p| File.file?(File.join(src_dir, p)) }
|
1206
|
+
|
1207
|
+
total = candidates.size
|
1208
|
+
converted = 0
|
1209
|
+
skipped = 0
|
1210
|
+
failed = 0
|
1211
|
+
deleted = 0
|
1212
|
+
|
1213
|
+
normalized_to = to.to_s.match?(/\Ajpe?g\z/i) ? 'jpg' : to.downcase
|
1214
|
+
|
1215
|
+
if total.zero?
|
1216
|
+
say "対象ファイルが見つかりませんでした (#{src_dir}, 拡張子: #{exts.uniq.join('/')})."
|
1217
|
+
end
|
1218
|
+
|
1219
|
+
candidates.each do |rel_path|
|
1220
|
+
begin
|
1221
|
+
in_path = File.join(src_dir, rel_path)
|
1222
|
+
# 出力は out_dir 直下に相対パスを維持
|
1223
|
+
out_path_dir = File.join(out_dir, File.dirname(rel_path))
|
1224
|
+
FileUtils.mkdir_p(out_path_dir) unless Dir.exist?(out_path_dir)
|
1225
|
+
out_basename = File.basename(rel_path, ".*")
|
1226
|
+
out_basename = suffix && !suffix.empty? ? "#{out_basename}#{suffix}" : out_basename
|
1227
|
+
out_path = File.join(out_path_dir, "#{out_basename}.#{normalized_to}")
|
1228
|
+
|
1229
|
+
if File.exist?(out_path) && !overwrite
|
1230
|
+
skipped += 1
|
1231
|
+
say "- 既存のためスキップ: #{out_path}(--overwrite で上書き可能)", :yellow
|
1232
|
+
next
|
1233
|
+
end
|
1234
|
+
|
1235
|
+
say "#{in_path} を #{out_path} に変換しています..."
|
1236
|
+
image = MiniMagick::Image.open(in_path)
|
1237
|
+
image.resize resize unless resize.to_s.empty?
|
1238
|
+
image.format normalized_to
|
1239
|
+
apply_image_quality(image, effective_output_quality)
|
1240
|
+
image.write out_path
|
1241
|
+
converted += 1
|
1242
|
+
say "変換成功! 新しいファイルは #{out_path} です。", :green
|
1243
|
+
|
1244
|
+
if delete
|
1245
|
+
FileUtils.rm(in_path)
|
1246
|
+
deleted += 1
|
1247
|
+
say "元のファイル #{in_path} は削除されました。", :yellow
|
1248
|
+
end
|
1249
|
+
rescue => e
|
1250
|
+
failed += 1
|
1251
|
+
say "エラー: 変換に失敗しました: #{rel_path} — #{e.message}", :red
|
1252
|
+
end
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
elapsed = Time.now - start_time
|
1256
|
+
say ""
|
1257
|
+
say "処理結果サマリ:", :bold if respond_to?(:say)
|
1258
|
+
say " 対象ファイル数: #{total}"
|
1259
|
+
say " 変換成功: #{converted}"
|
1260
|
+
say " スキップ: #{skipped}"
|
1261
|
+
say " 失敗: #{failed}"
|
1262
|
+
say " 削除: #{deleted}"
|
1263
|
+
say " 所要時間: #{format('%.2f', elapsed)} 秒"
|
1264
|
+
end
|
1265
|
+
|
1266
|
+
def prompt_yes_no(question, default: true, current_value: nil)
|
1267
|
+
suffix = ' [y/n]'
|
1268
|
+
meta = []
|
1269
|
+
unless current_value.nil?
|
1270
|
+
meta << "現在: #{current_value ? 'はい' : 'いいえ'}"
|
1271
|
+
end
|
1272
|
+
meta << "既定: #{default ? 'はい' : 'いいえ'}"
|
1273
|
+
meta_text = meta.empty? ? '' : " (#{meta.join(' / ')})"
|
1274
|
+
loop do
|
1275
|
+
answer = ask("#{question}#{suffix}#{meta_text}:").to_s.strip.downcase
|
1276
|
+
return default if answer.empty?
|
1277
|
+
return true if %w[y yes].include?(answer)
|
1278
|
+
return false if %w[n no].include?(answer)
|
1279
|
+
say 'y または n を入力してください。', :yellow
|
1280
|
+
end
|
1281
|
+
end
|
1282
|
+
|
1283
|
+
def prompt_string(label, current_value, hint: nil)
|
1284
|
+
hint_text = hint ? " #{hint}" : ''
|
1285
|
+
response = ask("#{label} [現在: #{current_value}]#{hint_text} (Enterで維持):").to_s
|
1286
|
+
trimmed = response.strip
|
1287
|
+
trimmed.empty? ? current_value : trimmed
|
1288
|
+
end
|
1289
|
+
|
1290
|
+
def prompt_integer(label, current_value, min:, max:)
|
1291
|
+
loop do
|
1292
|
+
response = ask("#{label} [現在: #{current_value}] (Enterで維持):").to_s.strip
|
1293
|
+
return current_value if response.empty?
|
1294
|
+
unless response.match?(/\A-?\d+\z/)
|
1295
|
+
say '数値を入力してください。', :yellow
|
1296
|
+
next
|
1297
|
+
end
|
1298
|
+
value = response.to_i
|
1299
|
+
value = max if value > max
|
1300
|
+
value = min if value < min
|
1301
|
+
return value
|
1302
|
+
end
|
1303
|
+
end
|
1304
|
+
|
1305
|
+
def prompt_choice(label, current_value, choices)
|
1306
|
+
normalized_choices = choices.map(&:downcase)
|
1307
|
+
loop do
|
1308
|
+
response = ask("#{label} [現在: #{current_value}] (Enterで維持):").to_s.strip
|
1309
|
+
return current_value if response.empty?
|
1310
|
+
idx = normalized_choices.index(response.downcase)
|
1311
|
+
if idx
|
1312
|
+
return choices[idx]
|
1313
|
+
end
|
1314
|
+
say "#{choices.join('/')} のいずれかを入力してください。", :yellow
|
1315
|
+
end
|
1316
|
+
end
|
1317
|
+
|
1318
|
+
def prompt_path(label, current_value)
|
1319
|
+
loop do
|
1320
|
+
response = ask("#{label} [現在: #{current_value}] (Enterで維持):").to_s.strip
|
1321
|
+
return current_value if response.empty?
|
1322
|
+
begin
|
1323
|
+
expanded = File.expand_path(response)
|
1324
|
+
return expanded
|
1325
|
+
rescue StandardError => e
|
1326
|
+
say "パスを解釈できませんでした: #{e.message}", :yellow
|
1327
|
+
end
|
1328
|
+
end
|
1329
|
+
end
|
1330
|
+
|
1331
|
+
def show_config_summary(settings = nil)
|
1332
|
+
settings ||= ImagesConvert::Configuration.show_all_settings
|
1333
|
+
|
1334
|
+
say "現在の設定:"
|
1335
|
+
say " デフォルト入力形式: #{settings[:defaults][:from]}"
|
1336
|
+
say " デフォルト出力形式: #{settings[:defaults][:to]}"
|
1337
|
+
say " デフォルトリサイズ: #{settings[:defaults][:resize]}"
|
1338
|
+
say " デフォルト入力ディレクトリ: #{settings[:directories][:input]}"
|
1339
|
+
say " デフォルト出力ディレクトリ: #{settings[:directories][:output]}"
|
1340
|
+
say " 最新判定モード: #{settings[:selection][:latest_mode]}"
|
1341
|
+
say " waifu2x 既定倍率: #{settings[:waifu2x][:scale]}"
|
1342
|
+
say " waifu2x ノイズ除去: #{settings[:waifu2x][:noise]}"
|
1343
|
+
say " waifu2x モデル: #{settings[:waifu2x][:model]}"
|
1344
|
+
say " waifu2x A4自動スケール: #{settings[:waifu2x][:auto_scale_a4]}"
|
1345
|
+
say " waifu2x A4レイアウトPDF: #{settings[:waifu2x][:a4_print]}"
|
1346
|
+
say " waifu2x 出力フォーマット: #{settings[:waifu2x][:image_format]}"
|
1347
|
+
say " waifu2x エラー時動作: #{settings[:waifu2x][:on_error]}"
|
1348
|
+
say " 出力画像品質: #{settings[:output][:image_quality]}"
|
1349
|
+
say " PDF 品質: #{settings[:output][:pdf_quality]}"
|
1350
|
+
say " PDF 圧縮方式: #{settings[:output][:pdf_compression]}"
|
1351
|
+
say " PDF DPI: #{settings[:output][:pdf_density]}"
|
1352
|
+
say ""
|
1353
|
+
end
|
1354
|
+
|
1355
|
+
def record_change(collection, label, before, after)
|
1356
|
+
collection << {
|
1357
|
+
label: label,
|
1358
|
+
before: format_value(before),
|
1359
|
+
after: format_value(after)
|
1360
|
+
}
|
1361
|
+
end
|
1362
|
+
|
1363
|
+
def format_value(value)
|
1364
|
+
case value
|
1365
|
+
when TrueClass
|
1366
|
+
'はい'
|
1367
|
+
when FalseClass
|
1368
|
+
'いいえ'
|
1369
|
+
when NilClass
|
1370
|
+
'なし'
|
1371
|
+
else
|
1372
|
+
value.to_s
|
1373
|
+
end
|
1374
|
+
end
|
1375
|
+
|
1376
|
+
def sort_candidates_by_mode(candidates, mode)
|
1377
|
+
if mode == 'exif'
|
1378
|
+
fallback_used = false
|
1379
|
+
sorted = candidates.sort_by do |path|
|
1380
|
+
ts = exif_timestamp_for(path)
|
1381
|
+
if ts.nil?
|
1382
|
+
fallback_used = true
|
1383
|
+
File.mtime(path)
|
1384
|
+
else
|
1385
|
+
ts
|
1386
|
+
end
|
1387
|
+
end.reverse
|
1388
|
+
[sorted, fallback_used]
|
1389
|
+
else
|
1390
|
+
[candidates.sort_by { |f| File.mtime(f) }.reverse, false]
|
1391
|
+
end
|
1392
|
+
end
|
1393
|
+
|
1394
|
+
def exif_timestamp_for(path)
|
1395
|
+
@_exif_timestamp_cache ||= {}
|
1396
|
+
return @_exif_timestamp_cache[path] if @_exif_timestamp_cache.key?(path)
|
1397
|
+
|
1398
|
+
@_exif_timestamp_cache[path] = extract_exif_timestamp(path)
|
1399
|
+
end
|
1400
|
+
|
1401
|
+
def extract_exif_timestamp(path)
|
1402
|
+
image = MiniMagick::Image.open(path)
|
1403
|
+
candidates = [
|
1404
|
+
image['EXIF:DateTimeOriginal'],
|
1405
|
+
image['EXIF:CreateDate'],
|
1406
|
+
image['EXIF:DateTimeDigitized'],
|
1407
|
+
image['EXIF:DateTime']
|
1408
|
+
]
|
1409
|
+
|
1410
|
+
candidates.each do |value|
|
1411
|
+
parsed = parse_exif_datetime(value)
|
1412
|
+
return parsed if parsed
|
1413
|
+
end
|
1414
|
+
|
1415
|
+
nil
|
1416
|
+
rescue StandardError
|
1417
|
+
nil
|
1418
|
+
end
|
1419
|
+
|
1420
|
+
def parse_exif_datetime(value)
|
1421
|
+
return nil unless value
|
1422
|
+
str = value.to_s.strip
|
1423
|
+
return nil if str.empty?
|
1424
|
+
|
1425
|
+
str = str.delete("\u0000")
|
1426
|
+
|
1427
|
+
Time.strptime(str, '%Y:%m:%d %H:%M:%S')
|
1428
|
+
rescue ArgumentError
|
1429
|
+
Time.parse(str)
|
1430
|
+
rescue ArgumentError
|
1431
|
+
nil
|
1432
|
+
end
|
1433
|
+
end
|
1434
|
+
|
1435
|
+
# Thor 起動前に ARGV を前処理して、数値短縮フラグ(-1, -5, -0)を正式オプションへ変換
|
1436
|
+
def preprocess_argv!(argv)
|
1437
|
+
if argv.empty?
|
1438
|
+
argv.replace(['help'])
|
1439
|
+
return
|
1440
|
+
end
|
1441
|
+
|
1442
|
+
converted = []
|
1443
|
+
argv.each do |arg|
|
1444
|
+
if arg.is_a?(String) && (m = arg.match(/^(?:-)(\d+)$/))
|
1445
|
+
n = m[1].to_i
|
1446
|
+
if n == 0
|
1447
|
+
converted << '--all'
|
1448
|
+
else
|
1449
|
+
converted << '--latest' << n.to_s
|
1450
|
+
end
|
1451
|
+
else
|
1452
|
+
converted << arg
|
1453
|
+
end
|
1454
|
+
end
|
1455
|
+
|
1456
|
+
# 先頭にサブコマンドが無い場合(例: `images-convert -1`)は convert を補う
|
1457
|
+
known_tasks = %w[convert config cleanup self-uninstall version help set-latest-mode setup]
|
1458
|
+
first_non_option = converted.find { |a| !(a.to_s.start_with?('-')) }
|
1459
|
+
global_flags = %w[-v --version -h --help]
|
1460
|
+
has_global_flag = converted.any? { |a| global_flags.include?(a) }
|
1461
|
+
|
1462
|
+
if !has_global_flag && (first_non_option.nil? || !known_tasks.include?(first_non_option))
|
1463
|
+
# FROM TO BASENAME のショートハンドをサポート(既知フォーマットに限る)
|
1464
|
+
# 例: heic jpeg 1000 -> config set HEIC JPG <resize>; convert 1000
|
1465
|
+
# 許可フォーマットとエイリアス
|
1466
|
+
aliases = {
|
1467
|
+
'jpeg' => 'JPG',
|
1468
|
+
'jpg' => 'JPG',
|
1469
|
+
'png' => 'PNG',
|
1470
|
+
'gif' => 'GIF',
|
1471
|
+
'webp' => 'WEBP',
|
1472
|
+
'heic' => 'HEIC',
|
1473
|
+
'tiff' => 'TIFF',
|
1474
|
+
'bmp' => 'BMP',
|
1475
|
+
'ico' => 'ICO',
|
1476
|
+
'pdf' => 'PDF',
|
1477
|
+
}
|
1478
|
+
known_formats = aliases.values.uniq
|
1479
|
+
|
1480
|
+
non_opts = converted.select { |a| !(a.to_s.start_with?('-')) }
|
1481
|
+
if non_opts.size >= 3
|
1482
|
+
first_arg, second_arg, third_arg = non_opts[0,3]
|
1483
|
+
# リサイズ値のパターン(数字+x、x数字、数字+%)
|
1484
|
+
is_resize_value = third_arg.to_s.match?(/^(?:\d+[x%]|[x%]\d+)$/) || third_arg.to_s.match?(/^\d+%$/)
|
1485
|
+
|
1486
|
+
if is_resize_value
|
1487
|
+
# INPUT_FILE OUTPUT_FILE RESIZE パターン
|
1488
|
+
# 例: flower.jpg flower.webp 1024x -> convert flower.jpg --output flower.webp --resize 1024x
|
1489
|
+
input_file = first_arg
|
1490
|
+
output_file = second_arg
|
1491
|
+
resize_value = third_arg
|
1492
|
+
|
1493
|
+
# 残りのオプション(元の順序を維持しつつ、最初の3つの非オプショントークンのみ取り除く)
|
1494
|
+
rest = []
|
1495
|
+
non_opt_seen = 0
|
1496
|
+
converted.each do |tok|
|
1497
|
+
if !(tok.to_s.start_with?('-')) && non_opt_seen < 3
|
1498
|
+
non_opt_seen += 1
|
1499
|
+
next
|
1500
|
+
end
|
1501
|
+
rest << tok
|
1502
|
+
end
|
1503
|
+
converted = ['convert', input_file, '--output', output_file, '--resize', resize_value] + rest
|
1504
|
+
elsif (from_raw, to_raw, base_raw = non_opts[0,3])
|
1505
|
+
from_nm = aliases[from_raw.to_s.downcase]
|
1506
|
+
to_nm = aliases[to_raw.to_s.downcase]
|
1507
|
+
is_digits = base_raw.to_s.match?(/^\d+$/)
|
1508
|
+
if from_nm && !to_nm && is_digits
|
1509
|
+
# FROM は既知だが TO が未知(例: heic tif 1000)の場合は明示的にエラー
|
1510
|
+
puts "エラー: 未知の出力形式 '#{to_raw}'. 利用可能な形式: #{known_formats.join('/')}"
|
1511
|
+
exit 1
|
1512
|
+
elsif from_nm && to_nm && is_digits && known_formats.include?(from_nm) && known_formats.include?(to_nm)
|
1513
|
+
# 現在のリサイズ設定値(未作成ならここで作成される)
|
1514
|
+
resize = ImagesConvert::Configuration.default_resize
|
1515
|
+
# 構築: convert BASENAME --from FROM --to TO [残りのオプション]
|
1516
|
+
# 残りのオプション(元の順序を維持しつつ、最初の3つの非オプショントークンのみ取り除く)
|
1517
|
+
rest = []
|
1518
|
+
non_opt_seen = 0
|
1519
|
+
converted.each do |tok|
|
1520
|
+
if !(tok.to_s.start_with?('-')) && non_opt_seen < 3
|
1521
|
+
non_opt_seen += 1
|
1522
|
+
next
|
1523
|
+
end
|
1524
|
+
rest << tok
|
1525
|
+
end
|
1526
|
+
converted = ['convert', base_raw, '--from', from_nm, '--to', to_nm] + rest
|
1527
|
+
else
|
1528
|
+
# 通常の convert 補完にフォールバック
|
1529
|
+
converted.unshift('convert')
|
1530
|
+
end
|
1531
|
+
else
|
1532
|
+
converted.unshift('convert')
|
1533
|
+
end
|
1534
|
+
else
|
1535
|
+
converted.unshift('convert')
|
1536
|
+
end
|
1537
|
+
end
|
1538
|
+
|
1539
|
+
help_flags = (Thor::HELP_MAPPINGS + ['--help']).uniq
|
1540
|
+
if converted.any? { |tok| help_flags.include?(tok) }
|
1541
|
+
first_command = converted.find { |tok| !(tok.to_s.start_with?('-')) && !help_flags.include?(tok) }
|
1542
|
+
if first_command
|
1543
|
+
argv.replace(['help', first_command])
|
1544
|
+
else
|
1545
|
+
argv.replace(['help'])
|
1546
|
+
end
|
1547
|
+
return
|
1548
|
+
end
|
1549
|
+
|
1550
|
+
argv.replace(converted)
|
1551
|
+
end
|
1552
|
+
|
1553
|
+
# Thor コマンドを実行
|
1554
|
+
if __FILE__ == $0
|
1555
|
+
preprocess_argv!(ARGV)
|
1556
|
+
ImagesConvertCLI.start(ARGV)
|
1557
|
+
end
|