mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
* Batch: Include All Subfolders (AUTO-219-5, #3753) File > Automate > Batch and File > Scripts > Image Processor take an optional "includeSubfolders" (default false): a folder input also yields the images in its subfolders, walked depth first in name order, bounded at 32 levels and 100000 files, with a seen-set of canonical folders against symlink loops and the output folder left out. Each result keeps its relative subfolder under the output folder, so same-named files in different subfolders no longer collide. A non-boolean value is BadParams. Both dialogs gain an "Include All Subfolders" checkbox (translated in every complete catalog). Scorecard AUTO-219-5 stays partial (naming template, progress/cancel, error log file); gaps.md G11 updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Batch: translate Include All Subfolders for Hungarian and Turkish Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
7833a6cad4
commit
aad56285ad
@@ -945,13 +945,106 @@ pub(crate) fn batch_inputs(p: &Value, cmd: &str) -> Result<Vec<String>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// One input of a batch run: the file, and the folder it sits in relative to the input folder
|
||||
/// (`""` at the top level, `"a/b"` two levels down), which its result keeps under `output`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct BatchInput {
|
||||
pub path: String,
|
||||
pub rel_dir: String,
|
||||
}
|
||||
|
||||
/// Subfolders below a batch input folder are walked at most this deep ("Include All Subfolders").
|
||||
pub(crate) const MAX_SUBFOLDER_DEPTH: usize = 32;
|
||||
/// A batch over subfolders lists at most this many files; a larger tree is an error, not a run
|
||||
/// that silently stops partway.
|
||||
pub(crate) const MAX_SUBFOLDER_FILES: usize = 100_000;
|
||||
|
||||
/// [`batch_inputs`] for the commands whose Photoshop dialog has "Include All Subfolders" (Batch,
|
||||
/// Image Processor): with `"includeSubfolders": true` a folder input also yields the images in its
|
||||
/// subfolders, each tagged with its relative folder. `output` (when inside the input folder, as
|
||||
/// the dialog's default `<input>/batch` is) is not walked, so a rerun doesn't process its results.
|
||||
pub(crate) fn batch_inputs_tree(p: &Value, cmd: &str, output: &str) -> Result<Vec<BatchInput>> {
|
||||
let recurse = match p.get("includeSubfolders") {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::Bool(b)) => *b,
|
||||
Some(_) => return Err(EngineError::BadParams { cmd: cmd.into(), msg: "\"includeSubfolders\" must be true or false".into() }),
|
||||
};
|
||||
match p.get("input").or_else(|| p.get("files")) {
|
||||
Some(Value::String(dir)) if recurse => list_images_tree(dir, output, cmd),
|
||||
_ => Ok(batch_inputs(p, cmd)?.into_iter().map(|path| BatchInput { path, rel_dir: String::new() }).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The images under `dir` and its subfolders: each folder's files sorted by name, then its
|
||||
/// subfolders in name order (depth first). Bounded by [`MAX_SUBFOLDER_DEPTH`] and
|
||||
/// [`MAX_SUBFOLDER_FILES`]; a folder reached twice (a symlink loop) is walked once, and `skip`
|
||||
/// (the output folder) is left out.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn list_images_tree(dir: &str, skip: &str, cmd: &str) -> Result<Vec<BatchInput>> {
|
||||
use std::path::{Path, PathBuf};
|
||||
let canon = |p: &Path| std::fs::canonicalize(p).ok();
|
||||
let root = Path::new(dir);
|
||||
let root_canon = canon(root).ok_or_else(|| EngineError::Other(format!("{dir}: cannot open the folder")))?;
|
||||
let skip = if skip.is_empty() { None } else { canon(Path::new(skip)) };
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
seen.insert(root_canon);
|
||||
let mut out = Vec::new();
|
||||
// Depth first with an explicit stack (no recursion): (folder, relative folder, depth).
|
||||
let mut stack: Vec<(PathBuf, String, usize)> = vec![(root.to_path_buf(), String::new(), 0)];
|
||||
while let Some((folder, rel, depth)) = stack.pop() {
|
||||
let shown = folder.to_string_lossy().into_owned();
|
||||
// The top folder must be readable; a subfolder that isn't is skipped.
|
||||
let files = match list_images(&shown) {
|
||||
Ok(f) => f,
|
||||
Err(e) if depth == 0 => return Err(e),
|
||||
Err(_) => continue,
|
||||
};
|
||||
if out.len().saturating_add(files.len()) > MAX_SUBFOLDER_FILES {
|
||||
return Err(EngineError::BadParams { cmd: cmd.into(), msg: format!("{dir} holds more than {MAX_SUBFOLDER_FILES} images with its subfolders") });
|
||||
}
|
||||
out.extend(files.into_iter().map(|path| BatchInput { path, rel_dir: rel.clone() }));
|
||||
if depth >= MAX_SUBFOLDER_DEPTH {
|
||||
continue;
|
||||
}
|
||||
let Ok(rd) = std::fs::read_dir(&folder) else { continue };
|
||||
let mut subs: Vec<(String, PathBuf)> = rd
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.is_dir())
|
||||
.filter_map(|p| Some((p.file_name()?.to_string_lossy().into_owned(), p)))
|
||||
.collect();
|
||||
subs.sort();
|
||||
// Pushed in reverse so the stack pops them in name order.
|
||||
for (name, sub) in subs.into_iter().rev() {
|
||||
let Some(c) = canon(&sub) else { continue };
|
||||
if skip.as_ref() == Some(&c) || !seen.insert(c) {
|
||||
continue;
|
||||
}
|
||||
let rel = if rel.is_empty() { name } else { format!("{rel}/{name}") };
|
||||
stack.push((sub, rel, depth + 1));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) fn list_images_tree(dir: &str, _skip: &str, _cmd: &str) -> Result<Vec<BatchInput>> {
|
||||
Err(EngineError::Other(format!("cannot list {dir}: no file system on the web")))
|
||||
}
|
||||
|
||||
/// Opens each input in a scratch session, runs `f` on it and saves it to `output` as `format`
|
||||
/// (`"same"` keeps the input's extension). Errors per file are collected, not fatal.
|
||||
pub(crate) fn process_files(inputs: &[String], output: &str, format: &str, save: SaveOpts, suffix: &str, f: &dyn Fn(&mut Session) -> Result<()>) -> Value {
|
||||
let outputs = inputs.iter().map(|path| {
|
||||
let inputs: Vec<BatchInput> = inputs.iter().map(|path| BatchInput { path: path.clone(), rel_dir: String::new() }).collect();
|
||||
process_tree(&inputs, output, format, save, suffix, f)
|
||||
}
|
||||
|
||||
/// [`process_files`] for inputs from subfolders: each result goes to its input's relative folder
|
||||
/// under `output` (created as needed), so `a/x.png` and `b/x.png` don't collide.
|
||||
pub(crate) fn process_tree(inputs: &[BatchInput], output: &str, format: &str, save: SaveOpts, suffix: &str, f: &dyn Fn(&mut Session) -> Result<()>) -> Value {
|
||||
let outputs = inputs.iter().map(|BatchInput { path, rel_dir }| {
|
||||
let ext =
|
||||
if format == "same" { path.rsplit('.').next().unwrap_or("png").to_ascii_lowercase() } else { format.trim_start_matches('.').to_ascii_lowercase() };
|
||||
(path.as_str(), join(output, &format!("{}{suffix}.{ext}", stem(path))))
|
||||
(path.as_str(), join(&join(output, rel_dir), &format!("{}{suffix}.{ext}", stem(path))))
|
||||
});
|
||||
process_outputs(outputs, save, f)
|
||||
}
|
||||
@@ -1015,8 +1108,8 @@ fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some((id, _)) = steps.iter().find(|(id, _)| crate::commands::find(id).is_none()) {
|
||||
return Err(EngineError::BadParams { cmd: cmd.into(), msg: format!("unknown command `{id}` in the action") });
|
||||
}
|
||||
let inputs = batch_inputs(p, cmd)?;
|
||||
let output = str_param(p, "output", cmd)?.to_string();
|
||||
let inputs = batch_inputs_tree(p, cmd, &output)?;
|
||||
let format = p.get("format").and_then(Value::as_str).unwrap_or("same").to_string();
|
||||
// Steps that call another action play it from the caller's actions; the playback stack comes
|
||||
// along so an action that batches itself is caught as a recursive call.
|
||||
@@ -1047,23 +1140,23 @@ fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(())
|
||||
};
|
||||
let r = match names {
|
||||
Some(names) => process_outputs(inputs.iter().map(String::as_str).zip(names), save, &run),
|
||||
None => process_files(&inputs, &output, &format, save, "", &run),
|
||||
Some(names) => process_outputs(inputs.iter().map(|i| i.path.as_str()).zip(names), save, &run),
|
||||
None => process_tree(&inputs, &output, &format, save, "", &run),
|
||||
};
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
fn image_processor(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "file.scripts.imageProcessor";
|
||||
let inputs = batch_inputs(p, cmd)?;
|
||||
let output = str_param(p, "output", cmd)?.to_string();
|
||||
let inputs = batch_inputs_tree(p, cmd, &output)?;
|
||||
let format = p.get("format").and_then(Value::as_str).unwrap_or("jpg").to_string();
|
||||
let fit = match (size_param(p, "width"), size_param(p, "height")) {
|
||||
(None, None) => None,
|
||||
(w, h) => Some(json!({"width": w.unwrap_or(1e9), "height": h.unwrap_or(1e9), "dontEnlarge": true})),
|
||||
};
|
||||
let to_srgb = p.get("convertToSrgb").and_then(Value::as_bool).unwrap_or(false);
|
||||
let r = process_files(&inputs, &output, &format, SaveOpts::from_params(s, p, cmd)?.or_quality(8.0), "", &|scratch| {
|
||||
let r = process_tree(&inputs, &output, &format, SaveOpts::from_params(s, p, cmd)?.or_quality(8.0), "", &|scratch| {
|
||||
if to_srgb && scratch.active().is_some_and(|d| d.doc.mode != ColorMode::Rgb) {
|
||||
scratch.execute("image.mode.rgb", json!({}))?;
|
||||
}
|
||||
@@ -1564,7 +1657,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Batch…",
|
||||
&["File", "Automate"],
|
||||
None,
|
||||
r##"{"steps":[[commandId,params]|{"command":id,"params":{}}…] (a recorded action),"input":folder|[paths],"output":folder,"format":"same|png|jpg|psd|tiff|…"="same","quality":0..12?,"naming":string? (blank keeps source names; {name}, {serial} padded to 3 digits, {date} as YYYY-MM-DD UTC; extension added automatically),"startingSerial":1..4294967295=1 (input position, including failed inputs),"tiffLayers":bool=false,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"calledActions":[{"name","steps"}…]? (the actions Play Action steps call; default: the session's)} → {files, errors} (an input whose output name was already written in the run goes to errors)"##,
|
||||
r##"{"steps":[[commandId,params]|{"command":id,"params":{}}…] (a recorded action),"input":folder|[paths],"includeSubfolders":bool=false (a folder input also takes the images in its subfolders, up to 32 levels and 100000 files; each result keeps its subfolder under output),"output":folder,"format":"same|png|jpg|psd|tiff|…"="same","quality":0..12?,"naming":string? (blank keeps source names; {name}, {serial} padded to 3 digits, {date} as YYYY-MM-DD UTC; extension added automatically),"startingSerial":1..4294967295=1 (input position, including failed inputs),"tiffLayers":bool=false,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"calledActions":[{"name","steps"}…]? (the actions Play Action steps call; default: the session's)} → {files, errors} (an input whose output name was already written in the run goes to errors)"##,
|
||||
native,
|
||||
batch
|
||||
),
|
||||
@@ -1573,7 +1666,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Image Processor…",
|
||||
&["File", "Scripts"],
|
||||
None,
|
||||
r##"{"input":folder|[paths],"output":folder,"format":"jpg|png|psd|tiff|…"="jpg","quality":0..12=8,"tiffLayers":bool=false,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"width":px?,"height":px? (fit, never enlarge),"convertToSrgb":bool=false} → {files, errors} (an input whose output name was already written in the run goes to errors)"##,
|
||||
r##"{"input":folder|[paths],"includeSubfolders":bool=false (a folder input also takes the images in its subfolders, up to 32 levels and 100000 files; each result keeps its subfolder under output),"output":folder,"format":"jpg|png|psd|tiff|…"="jpg","quality":0..12=8,"tiffLayers":bool=false,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"width":px?,"height":px? (fit, never enlarge),"convertToSrgb":bool=false} → {files, errors} (an input whose output name was already written in the run goes to errors)"##,
|
||||
native,
|
||||
image_processor
|
||||
),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Batch output names are planned before opening or writing files. Names substituted from an
|
||||
//! input are literal: braces in a source filename must never be interpreted as template tokens.
|
||||
|
||||
use super::{EngineError, Result, Value, join, stem};
|
||||
use super::{BatchInput, EngineError, Result, Value, join, stem};
|
||||
|
||||
fn bad(message: impl Into<String>) -> EngineError {
|
||||
EngineError::BadParams { cmd: "file.automate.batch".into(), msg: message.into() }
|
||||
@@ -51,7 +51,7 @@ fn expand(parts: &[Part<'_>], name: &str, serial: u32, date: &str) -> String {
|
||||
|
||||
/// An absent or empty template preserves the old filename path exactly. An explicit template
|
||||
/// generates one component per input; the export format supplies the extension separately.
|
||||
pub(super) fn plan(inputs: &[String], output: &str, format: &str, params: &Value) -> Result<Option<Vec<String>>> {
|
||||
pub(super) fn plan(inputs: &[BatchInput], output: &str, format: &str, params: &Value) -> Result<Option<Vec<String>>> {
|
||||
let start = match params.get("startingSerial") {
|
||||
None => 1,
|
||||
Some(n) => {
|
||||
@@ -77,7 +77,7 @@ pub(super) fn plan(inputs: &[String], output: &str, format: &str, params: &Value
|
||||
return Err(bad("naming: the current UTC date is unavailable"));
|
||||
}
|
||||
let mut outputs = Vec::with_capacity(inputs.len());
|
||||
for (index, path) in inputs.iter().enumerate() {
|
||||
for (index, BatchInput { path, rel_dir }) in inputs.iter().enumerate() {
|
||||
let serial = if has_serial {
|
||||
u32::try_from(index).ok().and_then(|index| start.checked_add(index)).ok_or_else(|| bad("startingSerial plus the input count exceeds 4294967295"))?
|
||||
} else {
|
||||
@@ -93,14 +93,15 @@ pub(super) fn plan(inputs: &[String], output: &str, format: &str, params: &Value
|
||||
if name.len() > 255 || name.chars().any(|c| c.is_control() || "/\\<>:\"|?*".contains(c)) {
|
||||
return Err(bad("naming must produce a filename of at most 255 bytes without path separators or reserved characters"));
|
||||
}
|
||||
outputs.push(join(output, &name));
|
||||
// From subfolders ("Include All Subfolders"), each result keeps its subfolder.
|
||||
outputs.push(join(&join(output, rel_dir), &name));
|
||||
}
|
||||
// Renaming within the source folder could replace a later input before it is read. Resolve
|
||||
// existing paths so aliases such as `source/./001.png` cannot bypass the check. The default
|
||||
// (no template) path keeps its existing in-place behavior.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let sources: std::collections::HashSet<_> = inputs.iter().filter_map(|p| std::fs::canonicalize(p).ok()).collect();
|
||||
let sources: std::collections::HashSet<_> = inputs.iter().filter_map(|i| std::fs::canonicalize(&i.path).ok()).collect();
|
||||
for output in &outputs {
|
||||
if std::fs::canonicalize(output).ok().is_some_and(|p| sources.contains(&p)) {
|
||||
return Err(bad(format!("naming would replace an input file: {output}; choose a different destination or template")));
|
||||
|
||||
@@ -1044,6 +1044,110 @@ fn reveal_in_finder_reveals_the_documents_saved_path() {
|
||||
assert_eq!(r["program"], json!(program));
|
||||
assert_eq!(r["args"], json!(args));
|
||||
}
|
||||
|
||||
/// "Include All Subfolders" (AUTO-219-5, #3753): Batch and Image Processor take the images in
|
||||
/// subfolders too, each result keeping its subfolder under the output folder (so same-named files
|
||||
/// don't collide), and skip the output folder when it sits inside the input folder.
|
||||
#[test]
|
||||
fn batch_include_all_subfolders() {
|
||||
let dir = tmp("batch-subfolders");
|
||||
let input = join(&dir, "in");
|
||||
for sub in ["sub/deeper", "z"] {
|
||||
std::fs::create_dir_all(join(&input, sub)).unwrap();
|
||||
}
|
||||
png(&input, "a.png", 8, 8, "#ff0000");
|
||||
png(&join(&input, "sub"), "b.png", 8, 8, "#00ff00");
|
||||
png(&join(&input, "sub/deeper"), "a.png", 8, 8, "#0000ff");
|
||||
png(&join(&input, "z"), "a.png", 8, 8, "#ffffff");
|
||||
let mut s = Session::new();
|
||||
// Default: the top level only, as before.
|
||||
let out = join(&input, "batch");
|
||||
let r = s.execute("file.automate.batch", json!({"steps": [], "input": input, "output": out, "format": "png"})).unwrap();
|
||||
assert_eq!((r["files"].as_array().unwrap().len(), r["errors"].as_array().unwrap().len()), (1, 0), "{r}");
|
||||
// With subfolders: every image, in folder order, results kept apart by subfolder; the output
|
||||
// folder (inside the input folder, holding last run's a.png) is not an input.
|
||||
let r = s
|
||||
.execute(
|
||||
"file.automate.batch",
|
||||
json!({"steps": [["image.adjustments.invert", {}]], "input": input, "includeSubfolders": true, "output": out, "format": "png"}),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(r["errors"].as_array().unwrap().is_empty(), "{r}");
|
||||
let files: Vec<String> = r["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap().replace('\\', "/")).collect();
|
||||
let o = out.replace('\\', "/");
|
||||
assert_eq!(files, vec![format!("{o}/a.png"), format!("{o}/sub/b.png"), format!("{o}/sub/deeper/a.png"), format!("{o}/z/a.png")]);
|
||||
let deep = photocraft_io::import("a.png", &std::fs::read(join(&out, "sub/deeper/a.png")).unwrap()).unwrap().document;
|
||||
let px = photocraft_compose::render(&deep, Rect::new(1, 1, 2, 2)).px[0];
|
||||
assert!(px[0] > 0.99 && px[1] > 0.99 && px[2] < 0.01, "blue inverted to yellow: {px:?}");
|
||||
std::fs::remove_dir_all(&out).unwrap();
|
||||
// A naming template applies within each kept subfolder (output outside the input folder).
|
||||
let named = join(&dir, "named");
|
||||
let r = s
|
||||
.execute("file.automate.batch", json!({"steps": [], "input": input, "includeSubfolders": true, "output": named, "format": "png", "naming": "{serial}"}))
|
||||
.unwrap();
|
||||
assert!(r["errors"].as_array().unwrap().is_empty(), "{r}");
|
||||
let files: Vec<String> = r["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap().replace('\\', "/")).collect();
|
||||
let n = named.replace('\\', "/");
|
||||
assert_eq!(files, vec![format!("{n}/001.png"), format!("{n}/sub/002.png"), format!("{n}/sub/deeper/003.png"), format!("{n}/z/004.png")]);
|
||||
// Image Processor shares the option (with its results out of the input folder).
|
||||
let out = join(&dir, "jpg");
|
||||
let r = s.execute("file.scripts.imageProcessor", json!({"input": input, "includeSubfolders": true, "output": out})).unwrap();
|
||||
assert_eq!((r["files"].as_array().unwrap().len(), r["errors"].as_array().unwrap().len()), (4, 0), "{r}");
|
||||
assert!(std::path::Path::new(&join(&out, "sub/deeper/a.jpg")).is_file());
|
||||
// A wrong type is a parameter error, for both.
|
||||
for bad in [json!("yes"), json!(1), json!([true])] {
|
||||
for cmd in ["file.automate.batch", "file.scripts.imageProcessor"] {
|
||||
let e = s.execute(cmd, json!({"steps": [], "input": input, "includeSubfolders": bad, "output": out})).unwrap_err();
|
||||
assert!(matches!(e, EngineError::BadParams { .. }), "{cmd} {bad}: {e}");
|
||||
}
|
||||
}
|
||||
// A missing input folder is an error, not a panic.
|
||||
assert!(s.execute("file.automate.batch", json!({"steps": [], "input": join(&dir, "nope"), "includeSubfolders": true, "output": out})).is_err());
|
||||
}
|
||||
|
||||
/// The subfolder walk stops at `MAX_SUBFOLDER_DEPTH` levels.
|
||||
#[test]
|
||||
fn batch_subfolder_walk_is_depth_limited() {
|
||||
let dir = tmp("batch-subfolder-depth");
|
||||
let mut deep = dir.clone();
|
||||
for level in 1..=MAX_SUBFOLDER_DEPTH + 1 {
|
||||
deep = join(&deep, "d");
|
||||
std::fs::create_dir_all(&deep).unwrap();
|
||||
if level >= MAX_SUBFOLDER_DEPTH {
|
||||
std::fs::write(join(&deep, &format!("l{level}.png")), b"x").unwrap();
|
||||
}
|
||||
}
|
||||
let found = list_images_tree(&dir, "", "test").unwrap();
|
||||
let names: Vec<_> = found.iter().map(|i| file_name(&i.path)).collect();
|
||||
assert_eq!(names, vec![format!("l{MAX_SUBFOLDER_DEPTH}.png")], "{found:?}");
|
||||
assert_eq!(found[0].rel_dir.split('/').count(), MAX_SUBFOLDER_DEPTH);
|
||||
}
|
||||
|
||||
/// A symlink back up the tree is walked once, not forever. Skipped where the platform doesn't
|
||||
/// let the test create directory symlinks (Windows without developer mode).
|
||||
#[test]
|
||||
fn batch_subfolder_walk_survives_symlink_loops() {
|
||||
let dir = tmp("batch-subfolder-loop");
|
||||
let sub = join(&dir, "sub");
|
||||
std::fs::create_dir_all(&sub).unwrap();
|
||||
std::fs::write(join(&dir, "a.png"), b"x").unwrap();
|
||||
std::fs::write(join(&sub, "b.png"), b"x").unwrap();
|
||||
let link = std::path::Path::new(&sub).join("loop");
|
||||
#[cfg(unix)]
|
||||
let made = std::os::unix::fs::symlink(&dir, &link);
|
||||
#[cfg(windows)]
|
||||
let made = std::os::windows::fs::symlink_dir(&dir, &link);
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
let made: std::io::Result<()> = Err(std::io::Error::other("no symlinks"));
|
||||
if made.is_err() {
|
||||
eprintln!("skipped: cannot create a directory symlink here");
|
||||
return;
|
||||
}
|
||||
let found = list_images_tree(&dir, "", "test").unwrap();
|
||||
let names: Vec<_> = found.iter().map(|i| (i.rel_dir.as_str(), file_name(&i.path))).collect();
|
||||
assert_eq!(names, vec![("", "a.png".to_string()), ("sub", "b.png".to_string())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_naming_validates_the_whole_plan_before_writing() {
|
||||
let dir = tmp("naming-invalid");
|
||||
|
||||
@@ -3741,6 +3741,8 @@ navigator Zoom Zvětšení
|
||||
Action playback stopped Přehrávání akce zastaveno
|
||||
Step {step} is waiting for its dialog Krok {step} čeká na svůj dialog
|
||||
@plural {n} action|{n} actions {n} akce|{n} akce|{n} akcí
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Zahrnout všechny podsložky
|
||||
Auto Add/Delete Automaticky přidat/odstranit
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3131,6 +3131,8 @@ curves Smooth Glätten
|
||||
Action playback stopped Aktionswiedergabe angehalten
|
||||
Step {step} is waiting for its dialog Schritt {step} wartet auf seinen Dialog
|
||||
@plural {n} action|{n} actions {n} Aktion|{n} Aktionen
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Alle Unterordner einschließen
|
||||
Auto Add/Delete Autom. hinzuf./löschen
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3831,6 +3831,8 @@ curves Smooth Εξομάλυνση
|
||||
Action playback stopped Η αναπαραγωγή της ενέργειας σταμάτησε
|
||||
Step {step} is waiting for its dialog Το βήμα {step} περιμένει το παράθυρο διαλόγου του
|
||||
@plural {n} action|{n} actions {n} ενέργεια|{n} ενέργειες
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Συμπερίληψη όλων των υποφακέλων
|
||||
Auto Add/Delete Αυτόματη προσθήκη/διαγραφή
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1029 and column 18.
|
@@ -2818,6 +2818,8 @@ curves Smooth Suavizar
|
||||
Action playback stopped Reproducción de la acción detenida
|
||||
Step {step} is waiting for its dialog El paso {step} está esperando su cuadro de diálogo
|
||||
@plural {n} action|{n} actions {n} acción|{n} acciones
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Incluir todas las subcarpetas
|
||||
Auto Add/Delete Añadir/eliminar automáticamente
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -3135,6 +3135,8 @@ curves Smooth Lisser
|
||||
Action playback stopped Exécution de l’action arrêtée
|
||||
Step {step} is waiting for its dialog L’étape {step} attend sa boîte de dialogue
|
||||
@plural {n} action|{n} actions {n} action|{n} actions
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Inclure tous les sous-dossiers
|
||||
Auto Add/Delete Ajout/suppr. auto
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -606,6 +606,7 @@
|
||||
HSB/HSL HSB/HSL
|
||||
Huge Óriási
|
||||
Image Processor Képfeldolgozó
|
||||
Include All Subfolders Az összes almappa belefoglalása
|
||||
Indexed Color… Indexelt szín…
|
||||
Info Információ
|
||||
Install Plug-in Bővítmény telepítése
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3122,6 +3122,8 @@ curves Smooth Haluskan
|
||||
Action playback stopped Pemutaran tindakan dihentikan
|
||||
Step {step} is waiting for its dialog Langkah {step} menunggu dialognya
|
||||
@plural {n} action|{n} actions {n} tindakan
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Sertakan semua subfolder
|
||||
Auto Add/Delete Tambah/Hapus Otomatis
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -4096,6 +4096,8 @@ navigator Zoom Zoom
|
||||
Action playback stopped Esecuzione dell’azione interrotta
|
||||
Step {step} is waiting for its dialog Il passaggio {step} attende la sua finestra di dialogo
|
||||
@plural {n} action|{n} actions {n} azione|{n} azioni
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Includi tutte le sottocartelle
|
||||
Auto Add/Delete Aggiungi/elimina automaticamente
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2644 and column 53.
|
@@ -3740,6 +3740,8 @@ curves Smooth スムーズ
|
||||
Action playback stopped アクションの再生を停止しました
|
||||
Step {step} is waiting for its dialog ステップ {step} はダイアログを待っています
|
||||
@plural {n} action|{n} actions アクション {n} 個
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders すべてのサブフォルダーを含める
|
||||
|
||||
# Liquify tool names (shown via tl!(tool.label()) in the UI, so they are not found by the static scan)
|
||||
Forward Warp 前方ワープ
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3308,6 +3308,8 @@ curves Smooth 매끄럽게
|
||||
Action playback stopped 액션 재생이 중지되었습니다
|
||||
Step {step} is waiting for its dialog {step}단계가 대화 상자를 기다리고 있습니다
|
||||
@plural {n} action|{n} actions 액션 {n}개
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders 모든 하위 폴더 포함
|
||||
Auto Add/Delete 자동 추가/삭제
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3157 and column 53.
|
@@ -3893,6 +3893,8 @@ curves Smooth Vloeiend maken
|
||||
Action playback stopped Afspelen van de handeling gestopt
|
||||
Step {step} is waiting for its dialog Stap {step} wacht op zijn dialoogvenster
|
||||
@plural {n} action|{n} actions {n} handeling|{n} handelingen
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Alle submappen opnemen
|
||||
Auto Add/Delete Automatisch toevoegen/verwijderen
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -3731,6 +3731,8 @@ curves Smooth Wygładź
|
||||
Action playback stopped Odtwarzanie operacji zatrzymane
|
||||
Step {step} is waiting for its dialog Krok {step} czeka na swoje okno dialogowe
|
||||
@plural {n} action|{n} actions {n} operacja|{n} operacje|{n} operacji
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Uwzględnij wszystkie podfoldery
|
||||
Auto Add/Delete Automatyczne dodawanie/usuwanie
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3923,6 +3923,8 @@ layerLabel No Color Sem cor
|
||||
Action playback stopped Reprodução da ação interrompida
|
||||
Step {step} is waiting for its dialog A etapa {step} está aguardando sua caixa de diálogo
|
||||
@plural {n} action|{n} actions {n} ação|{n} ações
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Incluir todas as subpastas
|
||||
Auto Add/Delete Adicionar/excluir automaticamente
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3079,6 +3079,8 @@ curves Smooth Сгладить
|
||||
Action playback stopped Воспроизведение операции остановлено
|
||||
Step {step} is waiting for its dialog Шаг {step} ожидает своего диалога
|
||||
@plural {n} action|{n} actions {n} операция|{n} операции|{n} операций
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Включить все вложенные папки
|
||||
Auto Add/Delete Автодобавление/удаление
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2997,6 +2997,7 @@ cameraData Lens Objektiv
|
||||
Create a new rectangular frame Napravi novi pravougaoni okvir
|
||||
Create a new elliptical frame Napravi novi eliptični okvir
|
||||
Include in playback Uključi u reprodukciju
|
||||
Include All Subfolders Uključi sve podfascikle
|
||||
Perspective Crop Tool Alatka za perspektivno opsecanje
|
||||
Show Grid Prikaži mrežu
|
||||
Drag a box or click four corners · drag the corners onto the edges of a skewed shape · {commit} commits · Esc cancels Prevucite okvir ili kliknite četiri ugla · prevucite uglove na ivice iskošenog oblika · {commit} potvrđuje · Esc otkazuje
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3355,6 +3355,7 @@ cameraRaw Color Renk
|
||||
How often fastening points are placed by themselves Sabitleme noktalarının kendi başlarına ne sıklıkla yerleştirildiği
|
||||
ISO Speed ISO Hızı
|
||||
Image Processor Görüntü İşlemcisi
|
||||
Include All Subfolders Tüm Alt Klasörleri Dahil Et
|
||||
In içinde
|
||||
Install Plug-in Eklentiyi Yükle
|
||||
Interpret Footage Görüntüyü Yorumlama
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -3832,6 +3832,8 @@ curves Smooth Згладити
|
||||
Action playback stopped Відтворення операції зупинено
|
||||
Step {step} is waiting for its dialog Крок {step} очікує на свій діалог
|
||||
@plural {n} action|{n} actions {n} операція|{n} операції|{n} операцій
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Включити всі вкладені папки
|
||||
Auto Add/Delete Автододавання/видалення
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1385 and column 18.
|
@@ -3617,6 +3617,8 @@ curves Smooth Làm mượt
|
||||
Step {step} is waiting for its dialog Bước {step} đang chờ hộp thoại của nó
|
||||
@plural {n} action|{n} actions {n} hành động
|
||||
Presentation Trình bày
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders Bao gồm tất cả thư mục con
|
||||
Gradient Preset Options Tùy chọn thiết lập sẵn chuyển sắc
|
||||
Import .GRD Nhập .GRD
|
||||
Choose a gradient file (.grd). Chọn tệp chuyển sắc (.grd).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2743,6 +2743,8 @@ curves Smooth 平滑
|
||||
Action playback stopped 动作播放已停止
|
||||
Step {step} is waiting for its dialog 步骤 {step} 正在等待其对话框
|
||||
@plural {n} action|{n} actions {n} 个动作
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders 包含所有子文件夹
|
||||
Auto Add/Delete 自动添加/删除
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2590 and column 53.
|
@@ -2728,6 +2728,8 @@ curves Smooth 平滑
|
||||
Action playback stopped 動作播放已停止
|
||||
Step {step} is waiting for its dialog 步驟 {step} 正在等待其對話框
|
||||
@plural {n} action|{n} actions {n} 個動作
|
||||
# Batch / Image Processor
|
||||
Include All Subfolders 包含所有子資料夾
|
||||
Auto Add/Delete 自動增加/刪除
|
||||
|
||||
# Native Windows printing qualification
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -985,6 +985,11 @@ fn form(app: &mut PhotocraftApp, command: &str, label: &str, fields: Value, choi
|
||||
app.ui.open_dialog(DialogKind::Command, f)
|
||||
}
|
||||
|
||||
/// The Batch and Image Processor dialogs' "Include All Subfolders" checkbox reads as Photoshop's.
|
||||
fn subfolders_label() -> Value {
|
||||
json!({"includeSubfolders": tl!("Include All Subfolders")})
|
||||
}
|
||||
|
||||
/// Alt+Cancel (Reset): restore the `__form` params captured at open.
|
||||
pub fn reset_form(f: &mut Map<String, Value>) {
|
||||
if !f.contains_key("__form") {
|
||||
@@ -1322,7 +1327,7 @@ fn front(app: &mut PhotocraftApp, ctx: &egui::Context, id: &str, params: &Value)
|
||||
),
|
||||
"file.scripts.imageProcessor" => dialog(
|
||||
app,
|
||||
json!({"input": dir, "output": format!("{dir}/processed"), "format": "jpg", "quality": 8, "width": 0, "height": 0, "convertToSrgb": true}),
|
||||
json!({"input": dir, "includeSubfolders": false, "__labels": subfolders_label(), "output": format!("{dir}/processed"), "format": "jpg", "quality": 8, "width": 0, "height": 0, "convertToSrgb": true}),
|
||||
json!({"format": ["jpg", "png", "psd", "tiff", "webp"]}),
|
||||
),
|
||||
"file.automate.batch" => {
|
||||
@@ -1333,7 +1338,7 @@ fn front(app: &mut PhotocraftApp, ctx: &egui::Context, id: &str, params: &Value)
|
||||
let name = action.name.clone();
|
||||
dialog(
|
||||
app,
|
||||
json!({"action": name, "steps": steps, "input": dir, "output": format!("{dir}/batch"), "format": "same", "quality": null, "__batchQuality": 8, "naming": "", "startingSerial": 1}),
|
||||
json!({"action": name, "steps": steps, "input": dir, "includeSubfolders": false, "__labels": subfolders_label(), "output": format!("{dir}/batch"), "format": "same", "quality": null, "__batchQuality": 8, "naming": "", "startingSerial": 1}),
|
||||
json!({"format": ["same", "png", "jpg", "psd", "tiff", "webp"]}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -359,6 +359,32 @@ fn lookup_export_dialog_uses_the_current_adjustment_selection() {
|
||||
assert!(direct["cube"].as_str().unwrap().contains("LUT_3D_SIZE 3"));
|
||||
}
|
||||
|
||||
/// Batch and Image Processor carry Photoshop's "Include All Subfolders" checkbox (AUTO-219-5,
|
||||
/// #3753): off by default, and confirming it on processes a subfolder's images too.
|
||||
#[test]
|
||||
fn batch_dialogs_include_all_subfolders() {
|
||||
let (mut app, ctx) = app_with(1);
|
||||
let dir = std::env::temp_dir().join(format!("photocraft-ui-batch-subfolders-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(dir.join("in/sub")).unwrap();
|
||||
let input = dir.join("in").to_string_lossy().into_owned();
|
||||
app.run("file.saveACopy", json!({"path": dir.join("in/sub/a.png").to_string_lossy()})).unwrap();
|
||||
app.session.actions.list.push(photocraft_engine::actions_cmds::Action::new("Invert", vec![("image.adjustments.invert".into(), json!({}))]));
|
||||
for (id, out) in [("file.automate.batch", "batch"), ("file.scripts.imageProcessor", "processed")] {
|
||||
let d = menu(&mut app, &ctx, id, json!({})).unwrap()["dialog"].as_u64().unwrap();
|
||||
let fields = &mut app.ui.dialog_mut(d).unwrap().fields;
|
||||
assert_eq!(fields["includeSubfolders"], json!(false), "{id}");
|
||||
assert_eq!(fields["__labels"]["includeSubfolders"], json!("Include All Subfolders"), "{id}");
|
||||
fields.insert("input".into(), json!(input));
|
||||
fields.insert("output".into(), json!(dir.join(out).to_string_lossy()));
|
||||
fields.insert("includeSubfolders".into(), json!(true));
|
||||
let r = crate::dialogs::confirm(&mut app, d).unwrap();
|
||||
assert_eq!(r["files"].as_array().map(Vec::len), Some(1), "{id}: {r}");
|
||||
assert!(dir.join(out).join("sub").is_dir(), "{id} keeps the subfolder: {r}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Float in Window takes the document and the screen position a dragged-off tab was dropped at
|
||||
/// (#3733); bad params are errors, not panics.
|
||||
#[test]
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
# Where PhotoCraft falls short of Photoshop
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (G1: 32-bit Brightness/Contrast legacy, Channel Mixer, Photo Filter and Black & White tint unclipped as in Photoshop, Photoshop-oracle floor 149/258; G1: mask feathers blur with their exact variance, psd-tools oracle floor 240/309; G1: Threshold splits on Photoshop's luminosity, psd-tools oracle floor 241/309; G1: pass-through clipping base with an adjustment renders isolated, psd-tools oracle floor 240/309; G1: 16-bit Posterize as Photoshop bins it, psd-tools oracle floor 241/309; G1: Gradient Map driven by the W3C luminosity, Photoshop-oracle floor 147/258; G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed) · **Target:** Adobe Photoshop 2026
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (G11: Batch Include All Subfolders landed; G1: 32-bit Brightness/Contrast legacy, Channel Mixer, Photo Filter and Black & White tint unclipped as in Photoshop, Photoshop-oracle floor 149/258; G1: mask feathers blur with their exact variance, psd-tools oracle floor 240/309; G1: Threshold splits on Photoshop's luminosity, psd-tools oracle floor 241/309; G1: pass-through clipping base with an adjustment renders isolated, psd-tools oracle floor 240/309; G1: 16-bit Posterize as Photoshop bins it, psd-tools oracle floor 241/309; G1: Gradient Map driven by the W3C luminosity, Photoshop-oracle floor 147/258; G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed) · **Target:** Adobe Photoshop 2026
|
||||
|
||||
Every known shortfall against Photoshop 2026 (27.11.0), one entry each, ranked by user impact
|
||||
on the way to beta. This is the work list agents pick from: take the highest-ranked entry that
|
||||
@@ -92,8 +92,8 @@ what an agent can't finish alone.
|
||||
- **2026-10-13 evidence:** Batch supports [output naming templates](batch-naming.md) using
|
||||
`{name}`, `{serial}` and a captured UTC `{date}`, with a starting serial number. Colliding
|
||||
names remain errors; custom output names cannot replace inputs. AUTO-219-5 stays partial.
|
||||
- **Missing:** document-independent action recording (AUTO-219-1), Batch recursion, open-document inputs, progress/cancel and an error-log file
|
||||
(AUTO-219-5), ExtendScript/UXP compatibility. `.atn` steps outside the
|
||||
- **Missing:** document-independent action recording (AUTO-219-1), Batch open-document inputs, progress/cancel and an error-log file
|
||||
(AUTO-219-5; Include All Subfolders landed 2026-10-11, #3753), ExtendScript/UXP compatibility. `.atn` steps outside the
|
||||
mapped events (paths, scripts, Generative Fill, Filter Gallery effects other than Spatter,
|
||||
Duplicate Layers into a new document, Bitmap halftone screens…) load as kept Photoshop steps
|
||||
that fail at playback by name.
|
||||
@@ -286,6 +286,7 @@ what an agent can't finish alone.
|
||||
| 2026-10-11 | minor | G13: Czech dynamic UI text localized; engine messages and native review remain open |
|
||||
| 2026-10-11 | minor | G8: triple/quadruple/five-click type selection done (TYPE-218-8, #3493); Type evidence recounted |
|
||||
| 2026-10-11 | minor | G8: Character panel OpenType buttons landed (TYPE-218-5, #3752); Numerals and Stylistic Sets submenus still missing |
|
||||
| 2026-10-11 | minor | G11: Batch and Image Processor gained Include All Subfolders (AUTO-219-5 stays partial; #3753) |
|
||||
| 2026-10-10 | minor | G2: recorded Tilt-Shift computation/filtered-proxy measurements and limits; gap and estimate remain open |
|
||||
| 2026-10-10 | minor | Checked against the alpha gate (no alpha blockers); #2374 marked closed |
|
||||
| 2026-10-10 | major | First version, from the full re-measure against Photoshop 2026 27.11.0 and the open issues |
|
||||
|
||||
+1
-1
@@ -231,7 +231,7 @@ Menu wiring (`docs/parity-checklist.md`): 631 of 631 menu items live (100.0%). T
|
||||
| AUTO-219-2 | Actions panel: step and dialog toggles, reorder, delete a step, Stop, sets, .atn import | done | [#219](https://github.com/storytold/photocraft/issues/219) | 2026-10-12: set folders (collapsible, drag between sets, actions.assignSet), dialog toggle column (playback opens the step's dialog seeded with its values and resumes on OK; actions.dialog), Insert Stop… (actions.insert), step toggles, reorder, delete a step; Load/Save Actions (.atn) in the panel menu, CLI batch and actions.load/save (atn_cmds.rs): 90 Photoshop events map to commands both ways, the rest are kept verbatim and fail at playback by name; step toggles, reorder and delete a step done; Stop steps play (actions.stopMessage) but there is no Insert Stop menu item; sets and dialog toggles are stored and round-trip but the panel neither groups by set nor edits the dialog toggle |
|
||||
| AUTO-219-3 | actions.record/stop/play/list commands for agents and the CLI | done | [#219](https://github.com/storytold/photocraft/issues/219) | actions.list/get/record/stop/play/delete in actions_cmds.rs; the panel, CLI and MCP share them. Nested steps pass the session authorize hook. |
|
||||
| AUTO-219-4 | Replay tests for crop, filters, selections and styled text on a second document | done | [#219](https://github.com/storytold/photocraft/issues/219) | actions_cmds.rs: a_recorded_edit_run_replays_on_a_second_document records a crop, a Gaussian Blur, a selection fill and faux-bold type on one document and replays all of them onto a second (plus the original layer/select/fill replay) |
|
||||
| AUTO-219-5 | Batch: subfolders, open documents, naming template, progress/cancel, error log, stop | partial | [#219](https://github.com/storytold/photocraft/issues/219) | file_cmds.rs and file_cmds/batch_naming.rs: folder/array input, per-file errors, {name}/{serial}/{date} output templates and starting serial; no recursion, open-documents source, cancel or error-log file |
|
||||
| AUTO-219-5 | Batch: subfolders, open documents, naming template, progress/cancel, error log, stop | partial | [#219](https://github.com/storytold/photocraft/issues/219) | file_cmds.rs and file_cmds/batch_naming.rs: folder/array input, per-file errors, {name}/{serial}/{date} output templates and starting serial, Include All Subfolders (includeSubfolders: depth/loop/file-count bounded, results keep their subfolder; Batch and Image Processor dialogs, #3753); no open-documents source, cancel or error-log file |
|
||||
| AUTO-219-6 | Plug-in ABI v2: panel kind, progress/cancel calls, read other layers, Last Filter repeats | missing | [#219](https://github.com/storytold/photocraft/issues/219) | runtime.rs:15 ABI_VERSION = 1; manifest.rs:18 Kind::Filter only |
|
||||
| AUTO-219-7 | Local ONNX models behind a flag, managed in Preferences; subject select, background removal | missing | [#219](https://github.com/storytold/photocraft/issues/219) | No ONNX runtime dependency |
|
||||
| AUTO-219-8 | xtask check that every panel button routes through a command id | missing | [#219](https://github.com/storytold/photocraft/issues/219) | No such xtask check |
|
||||
|
||||
@@ -44,7 +44,7 @@ id = "AUTO-219-5"
|
||||
target = "Batch: subfolders, open documents, naming template, progress/cancel, error log, stop"
|
||||
status = "partial"
|
||||
issue = 219
|
||||
note = "file_cmds.rs and file_cmds/batch_naming.rs: folder/array input, per-file errors, {name}/{serial}/{date} output templates and starting serial; no recursion, open-documents source, cancel or error-log file"
|
||||
note = "file_cmds.rs and file_cmds/batch_naming.rs: folder/array input, per-file errors, {name}/{serial}/{date} output templates and starting serial, Include All Subfolders (includeSubfolders: depth/loop/file-count bounded, results keep their subfolder; Batch and Image Processor dialogs, #3753); no open-documents source, cancel or error-log file"
|
||||
|
||||
[[item]]
|
||||
id = "AUTO-219-6"
|
||||
|
||||
Reference in New Issue
Block a user