1use crate::{Dependencies, Error, Result};
2use clap::Parser;
3use std::path::{Path, PathBuf};
4
5#[derive(Clone, Debug, Parser)]
11pub struct CreateMse {
12 #[arg(value_name = "out-base")]
15 out_base: String,
16
17 #[arg(value_name = "prefix", long)]
20 prefix: Option<String>,
21
22 #[arg(value_name = "metal-rough", long)]
24 metal_rough: Option<PathBuf>,
25
26 #[arg(value_name = "emissive", long)]
28 emissive: Option<PathBuf>,
29
30 #[arg(value_name = "albedo", long)]
32 albedo: Option<PathBuf>,
33
34 #[arg(
36 value_name = "ignore-metal-rough",
37 long,
38 conflicts_with = "metal_rough"
39 )]
40 ignore_metal_rough: bool,
41
42 #[arg(value_name = "ignore-emissive", long, conflicts_with = "emissive")]
44 ignore_emissive: bool,
45
46 #[arg(value_name = "ignore-albedo", long, conflicts_with = "albedo")]
48 ignore_albedo: bool,
49}
50
51impl CreateMse {
52 pub fn execute(self, dependencies: impl Dependencies) -> Result<()> {
53 let CreateMse {
54 out_base,
55 prefix,
56 metal_rough,
57 emissive,
58 albedo,
59 ignore_metal_rough,
60 ignore_emissive,
61 ignore_albedo,
62 } = self;
63
64 let metal_rough_path = if ignore_metal_rough {
68 None
69 } else {
70 Some(match metal_rough {
71 Some(p) => coerce_png(p),
72 None => match &prefix {
73 Some(pfx) => dependencies.glob_single_match(&format!("{pfx}-metalness.png"))?,
74 None => dependencies.glob_single_match("*metalness.png")?,
75 },
76 })
77 };
78
79 let emissive_path = if ignore_emissive {
80 None
81 } else {
82 Some(match emissive {
83 Some(p) => coerce_png(p),
84 None => match &prefix {
85 Some(pfx) => dependencies.glob_single_match(&format!("{pfx}-emission.png"))?,
86 None => dependencies.glob_single_match("*emission.png")?,
87 },
88 })
89 };
90
91 let albedo_path = if ignore_albedo {
92 None
93 } else {
94 Some(match albedo {
95 Some(p) => coerce_png(p),
96 None => match &prefix {
97 Some(pfx) => dependencies.glob_single_match(&format!("{pfx}-albedo.png"))?,
98 None => dependencies.glob_single_match("*albedo.png")?,
99 },
100 })
101 };
102
103 let base_img = metal_rough_path
107 .as_ref()
108 .or(emissive_path.as_ref())
109 .or(albedo_path.as_ref())
110 .ok_or_else(|| Error::Glob("all channels are ignored; nothing to do".into()))?;
111
112 let size_output = dependencies.exec_magick([
113 "identify".into(),
114 "-ping".into(),
115 "-format".into(),
116 "%wx%h".into(),
117 base_img.to_string_lossy().into_owned(),
118 ])?;
119 let size = String::from_utf8_lossy(&size_output).trim().to_string();
120 if size.is_empty() {
121 return Err(Error::Glob(format!(
122 "couldn't determine image size for: {}",
123 base_img.display()
124 )));
125 }
126
127 let tmpdir = dependencies.create_temp_dir()?;
131 let result = self::create_mse_inner(
132 &dependencies,
133 &metal_rough_path,
134 &emissive_path,
135 &albedo_path,
136 &out_base,
137 &size,
138 &tmpdir,
139 );
140 dependencies.remove_dir_all(&tmpdir)?;
141 result
142 }
143}
144
145fn create_mse_inner(
146 dependencies: &impl Dependencies,
147 metal_rough_path: &Option<PathBuf>,
148 emissive_path: &Option<PathBuf>,
149 albedo_path: &Option<PathBuf>,
150 out_base: &str,
151 size: &str,
152 tmpdir: &Path,
153) -> Result<()> {
154 let r_img = tmpdir.join("r.png");
155 let g_img = tmpdir.join("g.png");
156 let b_img = tmpdir.join("b.png");
157
158 let r_str = r_img.to_string_lossy().into_owned();
159 let g_str = g_img.to_string_lossy().into_owned();
160 let b_str = b_img.to_string_lossy().into_owned();
161
162 match metal_rough_path {
164 None => {
165 dependencies.exec_magick(["-size", size, "xc:black", &r_str])?;
166 }
167 Some(mr) => {
168 let mr_str = mr.to_string_lossy().into_owned();
169 dependencies.exec_magick([
170 &mr_str,
171 "-colorspace",
172 "sRGB",
173 "-alpha",
174 "on",
175 "-channel",
176 "R",
177 "-separate",
178 "+channel",
179 "-resize",
180 &format!("{size}!"),
181 &r_str,
182 ])?;
183 }
184 }
185
186 match metal_rough_path {
188 None => {
189 dependencies.exec_magick(["-size", size, "xc:black", &g_str])?;
190 }
191 Some(mr) => {
192 let mr_str = mr.to_string_lossy().into_owned();
193 dependencies.exec_magick([
194 &mr_str,
195 "-colorspace",
196 "sRGB",
197 "-alpha",
198 "on",
199 "-alpha",
200 "extract",
201 "-fx",
202 "u==0 ? 0 : 1-u",
203 "-resize",
204 &format!("{size}!"),
205 &g_str,
206 ])?;
207 }
208 }
209
210 match emissive_path {
212 None => {
213 dependencies.exec_magick(["-size", size, "xc:black", &b_str])?;
214 }
215 Some(em) => {
216 let em_str = em.to_string_lossy().into_owned();
217 dependencies.exec_magick([
218 &em_str,
219 "-colorspace",
220 "sRGB",
221 "-alpha",
222 "on",
223 "-alpha",
224 "extract",
225 "-resize",
226 &format!("{size}!"),
227 &b_str,
228 ])?;
229 }
230 }
231
232 let mse_out = format!("{out_base}-mse.png");
234 dependencies.exec_magick([
235 &r_str,
236 &g_str,
237 &b_str,
238 "-combine",
239 "-colorspace",
240 "sRGB",
241 &mse_out,
242 ])?;
243
244 dependencies.write_stdout(format!("Wrote: {mse_out}\n").as_bytes())?;
245
246 if let Some(albedo) = albedo_path {
248 let albedo_out = format!("{out_base}-albedo.png");
249 dependencies.copy_file(albedo, &albedo_out)?;
250 dependencies.write_stdout(format!("Wrote: {albedo_out}\n").as_bytes())?;
251 }
252
253 Ok(())
254}
255
256fn coerce_png(p: PathBuf) -> PathBuf {
258 if p.extension().is_none() {
259 p.with_extension("png")
260 } else {
261 p
262 }
263}