mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-14 20:34:03 +00:00
Merge pull request #10001 from indy2kro/feat/8064-psd-maximize-compatibility-param
Saves take maximizeCompatibility for a PSD/PSB's merged image (#8064)
This commit is contained in:
Generated
+1
@@ -5247,6 +5247,7 @@ dependencies = [
|
||||
"photocraft-engine",
|
||||
"photocraft-format",
|
||||
"photocraft-io",
|
||||
"photocraft-psd",
|
||||
"photocraft-raster",
|
||||
"photocraft-raw",
|
||||
"serde",
|
||||
|
||||
@@ -45,6 +45,8 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
[dev-dependencies]
|
||||
serde = { workspace = true }
|
||||
tempfile = "3"
|
||||
# Reading back a saved PSD's merged-image flag (#8064).
|
||||
photocraft-psd = { workspace = true }
|
||||
# Synthetic camera raws (#7789).
|
||||
photocraft-raw = { workspace = true, features = ["testgen"] }
|
||||
# Shell completions and man pages (examples/man-and-completions.rs, for the packages); not in
|
||||
|
||||
@@ -87,6 +87,12 @@ pub(crate) struct ExportArgs {
|
||||
/// composite/full.png; it may be omitted. Check the manifest's full_composite member.
|
||||
#[arg(long)]
|
||||
pub(crate) pcraft_full_composite: bool,
|
||||
/// A layered PSD/PSB stores Photoshop's white placeholder instead of the merged image
|
||||
/// (Maximize Compatibility off): a smaller file and a faster save, but readers that show only
|
||||
/// the merged image see white. Bitmap, Indexed Color and Duotone documents still write the
|
||||
/// real image
|
||||
#[arg(long)]
|
||||
pub(crate) no_maximize_compatibility: bool,
|
||||
}
|
||||
|
||||
/// Convert between formats (.pcraft, .psd, .png, .jpg, .tif, .webp, .exr, …)
|
||||
|
||||
@@ -33,6 +33,7 @@ impl From<&ExportArgs> for ExportOptions {
|
||||
o.encode.dds.compression = c;
|
||||
}
|
||||
o.encode.dds.mipmaps = a.dds_mipmaps;
|
||||
o.psd_maximize_compatibility = !a.no_maximize_compatibility;
|
||||
o
|
||||
}
|
||||
}
|
||||
|
||||
@@ -867,6 +867,24 @@ fn dds_converts_both_ways_with_the_compression_asked_for() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #8064: `--no-maximize-compatibility` writes a layered PSD/PSB with Photoshop's placeholder
|
||||
/// instead of the merged image, flagged in resource 1057; without it, the real image.
|
||||
#[test]
|
||||
fn no_maximize_compatibility_writes_the_psd_placeholder() {
|
||||
let d = tmp("maximize-compat");
|
||||
let a = d.join("a.png");
|
||||
write_png(&a, 32, 16, 3);
|
||||
for (args, ext, real) in
|
||||
[(&[][..], "psd", true), (&["--no-maximize-compatibility"][..], "psd", false), (&["--no-maximize-compatibility"][..], "psb", false)]
|
||||
{
|
||||
let out = d.join(format!("a.{ext}"));
|
||||
ok(bin().arg("run").arg(&a).args(["--cmd", "layer.new.layer"]).arg("--out").arg(&out).args(args));
|
||||
let psd = photocraft_psd::PsdFile::from_bytes(&std::fs::read(&out).unwrap()).unwrap();
|
||||
assert_eq!(psd.has_real_merged_data(), Some(real), "{args:?} {ext}");
|
||||
}
|
||||
std::fs::remove_dir_all(d).unwrap();
|
||||
}
|
||||
|
||||
/// `--tga-bits` picks a TGA's bits per pixel (byte 16 of its header); anything but 24 or 32 is a
|
||||
/// usage error and nothing is written.
|
||||
#[test]
|
||||
|
||||
@@ -184,6 +184,9 @@ impl Headless {
|
||||
let tiff_bool = |key| photocraft_engine::file_cmds::tiff_bool_param(&p, key, "doc.save").map_err(|e| bad(e.to_string()));
|
||||
opts.tiff_transparency = tiff_bool("tiffTransparency")?;
|
||||
opts.encode.tiff_pyramid = tiff_bool("tiffPyramid")?.unwrap_or(false);
|
||||
if let Some(maximize) = photocraft_engine::file_cmds::maximize_compatibility_param(&p, "doc.save").map_err(|e| bad(e.to_string()))? {
|
||||
opts.psd_maximize_compatibility = maximize;
|
||||
}
|
||||
match p.get("quality") {
|
||||
None | Some(Value::Null) => {}
|
||||
Some(v) => {
|
||||
|
||||
@@ -303,6 +303,28 @@ fn doc_save_rejects_a_bad_quality_instead_of_ignoring_or_clamping_it() {
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// #8064: `doc.save` takes `maximizeCompatibility` for a layered PSD's merged image (false: the
|
||||
/// placeholder, flagged in resource 1057); anything but a boolean writes nothing.
|
||||
#[test]
|
||||
fn doc_save_takes_maximize_compatibility() {
|
||||
let dir = std::env::temp_dir().join(format!("pc-rpc-save-maximize-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut h = Headless::trusted_local();
|
||||
h.handle("doc.new", json!({"width": 64, "height": 32})).unwrap();
|
||||
h.handle("engine.execute", json!({"command": "layer.new.layer"})).unwrap();
|
||||
let real_merged = |p: &std::path::Path| photocraft_psd::PsdFile::from_bytes(&std::fs::read(p).unwrap()).unwrap().has_real_merged_data();
|
||||
for (name, maximize, real) in [("default.psd", json!(null), true), ("off.psd", json!(false), false), ("on.psd", json!(true), true)] {
|
||||
let path = dir.join(name);
|
||||
h.handle("doc.save", json!({"path": path.to_string_lossy(), "maximizeCompatibility": maximize})).unwrap();
|
||||
assert_eq!(real_merged(&path), Some(real), "{name}");
|
||||
}
|
||||
let bad = dir.join("bad.psd");
|
||||
let err = h.handle("doc.save", json!({"path": bad.to_string_lossy(), "maximizeCompatibility": 0})).unwrap_err().to_string();
|
||||
assert!(err.contains("maximizeCompatibility"), "{err}");
|
||||
assert!(!bad.exists(), "a bad value writes nothing");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automation_preview_rejects_oversized_full_size_and_numeric_wraparound() {
|
||||
let mut h = Headless::new();
|
||||
|
||||
@@ -113,6 +113,11 @@ pub struct ExportLayersParams {
|
||||
/// Encoder quality, 0..12, for the formats that take one.
|
||||
#[serde(default)]
|
||||
pub quality: Option<f64>,
|
||||
/// PSD/PSB: write the real merged image (Photoshop's Maximize Compatibility); false writes
|
||||
/// Photoshop's white placeholder (Bitmap, Indexed Color and Duotone documents still write the
|
||||
/// real image). Unset: the engine's Preferences › File Handling decides.
|
||||
#[serde(default, rename = "maximizeCompatibility")]
|
||||
pub maximize_compatibility: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, JsonSchema)]
|
||||
@@ -214,6 +219,13 @@ pub struct SaveParams {
|
||||
/// The save may omit it; check the manifest's `full_composite` member. Off by default.
|
||||
#[serde(default, rename = "pcraftFullComposite")]
|
||||
pub pcraft_full_composite: Option<bool>,
|
||||
/// PSD/PSB: write the real merged image (Photoshop's Maximize Compatibility). false writes
|
||||
/// Photoshop's white placeholder instead: a smaller file and a faster save, but readers that
|
||||
/// show only the merged image see white. Bitmap, Indexed Color and Duotone documents still
|
||||
/// write the real image. Unset: the real image (in bridge mode, the app's
|
||||
/// Preferences › File Handling decides).
|
||||
#[serde(default, rename = "maximizeCompatibility")]
|
||||
pub maximize_compatibility: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub index: Option<usize>,
|
||||
}
|
||||
@@ -798,6 +810,9 @@ impl PhotocraftMcp {
|
||||
if let Some(v) = p.quality {
|
||||
opts.insert("quality".into(), json!(v));
|
||||
}
|
||||
if let Some(v) = p.maximize_compatibility {
|
||||
opts.insert("maximizeCompatibility".into(), json!(v));
|
||||
}
|
||||
let opts = Value::Object(opts);
|
||||
// Bridge mode runs the app on this machine, which writes its own paths like any command.
|
||||
let mut bridge_params = opts.clone();
|
||||
@@ -1186,9 +1201,10 @@ impl PhotocraftMcp {
|
||||
if let Some(b) = self.bridge_client() {
|
||||
// The bridge forwards to the running app's `app.save`, which takes only an optional
|
||||
// path (without one it writes back to the document's own layered file, as headless
|
||||
// does), the Targa bits, the DDS options and the TIFF compression, transparency and
|
||||
// pyramid, so the other options are headless-only. Saying so beats saving with
|
||||
// defaults while the caller believes their quality or format was applied.
|
||||
// does), the Targa bits, the DDS options, the TIFF compression, transparency and
|
||||
// pyramid, and Maximize Compatibility, so the other options are headless-only. Saying
|
||||
// so beats saving with defaults while the caller believes their quality or format was
|
||||
// applied.
|
||||
let unsupported: Vec<&str> = [
|
||||
p.format.is_some().then_some("format"),
|
||||
p.quality.is_some().then_some("quality"),
|
||||
@@ -1226,6 +1242,9 @@ impl PhotocraftMcp {
|
||||
if p.tiff_pyramid {
|
||||
params["tiffPyramid"] = json!(true);
|
||||
}
|
||||
if let Some(maximize) = p.maximize_compatibility {
|
||||
params["maximizeCompatibility"] = json!(maximize);
|
||||
}
|
||||
return to_result(b.call("app.save", params).await);
|
||||
}
|
||||
let Some(r) = self
|
||||
@@ -1243,6 +1262,9 @@ impl PhotocraftMcp {
|
||||
}
|
||||
opts.encode.tiff_pyramid = p.tiff_pyramid;
|
||||
opts.encode.dds = photocraft_io::DdsOptions { compression: dds_compression.unwrap_or_default(), mipmaps: p.dds_mipmaps };
|
||||
if let Some(maximize) = p.maximize_compatibility {
|
||||
opts.psd_maximize_compatibility = maximize;
|
||||
}
|
||||
if let Some(q) = p.quality {
|
||||
opts.encode.jpeg_quality = q;
|
||||
opts.encode.webp_quality = q;
|
||||
|
||||
@@ -513,6 +513,70 @@ async fn headless_doc_save_writes_tiff_options() {
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
/// #8064: headless `doc_save`/`doc_export` take `maximizeCompatibility` for a layered PSD/PSB's
|
||||
/// merged image: false writes Photoshop's placeholder and says so in resource 1057; unset, the
|
||||
/// real image. A value that isn't a boolean is refused before anything is written.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn headless_doc_save_takes_maximize_compatibility() {
|
||||
let dir = tmp("maximize-compat");
|
||||
let client = connect(headless_in(&dir)).await;
|
||||
call(&client, "doc_new", json!({"width": 64, "height": 32, "background": "white"})).await;
|
||||
let r = call(&client, "command_run", json!({"id": "layer.new.layer", "params": {}})).await;
|
||||
assert_ne!(r.is_error, Some(true), "{}", text(&r));
|
||||
for (tool, params, real) in [
|
||||
("doc_export", json!({"path": "default.psd"}), true),
|
||||
("doc_export", json!({"path": "off.psd", "maximizeCompatibility": false}), false),
|
||||
("doc_save", json!({"path": "off.psb", "maximizeCompatibility": false}), false),
|
||||
("doc_save", json!({"path": "on.psd", "maximizeCompatibility": true}), true),
|
||||
] {
|
||||
let r = call(&client, tool, params.clone()).await;
|
||||
assert_ne!(r.is_error, Some(true), "{params}: {}", text(&r));
|
||||
let bytes = std::fs::read(dir.join(params["path"].as_str().unwrap())).unwrap();
|
||||
assert_eq!(photocraft_psd::PsdFile::from_bytes(&bytes).unwrap().has_real_merged_data(), Some(real), "{params}");
|
||||
}
|
||||
let r = call(&client, "doc_export", json!({"path": "bad.psd", "maximizeCompatibility": "no"})).await;
|
||||
assert_eq!(r.is_error, Some(true), "{}", text(&r));
|
||||
assert!(!dir.join("bad.psd").exists());
|
||||
client.cancel().await.unwrap();
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
/// #8064: `doc_export_layers` takes `maximizeCompatibility` too, for PSD/PSB layer files.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn headless_doc_export_layers_takes_maximize_compatibility() {
|
||||
let dir = tmp("export-layers-maximize-compat");
|
||||
let client = connect(headless_in(&dir)).await;
|
||||
call(&client, "doc_new", json!({"width": 16, "height": 16, "background": "white", "name": "card"})).await;
|
||||
for (sub, params, real) in
|
||||
[("on", json!({"dir": "on", "format": "psd"}), true), ("off", json!({"dir": "off", "format": "psd", "maximizeCompatibility": false}), false)]
|
||||
{
|
||||
let result = json_of(&call(&client, "doc_export_layers", params.clone()).await);
|
||||
assert_eq!(result["count"], 1, "{params}: {result}");
|
||||
let file = result["files"][0].as_str().unwrap();
|
||||
assert!(file.starts_with(sub), "{file}");
|
||||
let bytes = std::fs::read(dir.join(file)).unwrap();
|
||||
assert_eq!(photocraft_psd::PsdFile::from_bytes(&bytes).unwrap().has_real_merged_data(), Some(real), "{params}");
|
||||
}
|
||||
let r = call(&client, "doc_export_layers", json!({"dir": "bad", "format": "psd", "maximizeCompatibility": "no"})).await;
|
||||
assert_eq!(r.is_error, Some(true), "{}", text(&r));
|
||||
client.cancel().await.unwrap();
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
/// #8064: `maximizeCompatibility` reaches the app's `app.save` through the bridge; unset, nothing
|
||||
/// is sent and the app's preference decides.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn bridge_doc_save_forwards_maximize_compatibility() {
|
||||
let (addr, app) = fake_app().await;
|
||||
let client = connect(PhotocraftMcp::bridge(&addr, CONTROL_TOKEN).unwrap()).await;
|
||||
let saved = json_of(&call(&client, "doc_save", json!({"path": "out.psd", "maximizeCompatibility": false})).await);
|
||||
assert_eq!(saved, json!({"saved": {"path": "out.psd", "maximizeCompatibility": false}}));
|
||||
let saved = json_of(&call(&client, "doc_save", json!({"path": "out.psd"})).await);
|
||||
assert_eq!(saved, json!({"saved": {"path": "out.psd"}}));
|
||||
client.cancel().await.unwrap();
|
||||
app.abort();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn bridge_response_budget_drops_connection_without_retrying_the_operation() {
|
||||
use photocraft_automation::{BridgeClient, budgets::MAX_RESPONSE_BYTES};
|
||||
|
||||
@@ -63,8 +63,8 @@ use processing::{conditional_mode_change, fit_image, flatten_all_effects, flatte
|
||||
use save::save_a_copy;
|
||||
pub(crate) use save::{SaveOpts, encode, save_doc};
|
||||
pub use save::{
|
||||
dds_param, dpx_bits_param, jpeg_format_param, jpeg_quality, pcraft_full_composite_param, tga_bits_param, tiff_bool_param, tiff_compression_param,
|
||||
tiff_would_write_layers,
|
||||
dds_param, dpx_bits_param, jpeg_format_param, jpeg_quality, maximize_compatibility_param, pcraft_full_composite_param, tga_bits_param, tiff_bool_param,
|
||||
tiff_compression_param, tiff_would_write_layers,
|
||||
};
|
||||
|
||||
mod batch_naming;
|
||||
@@ -272,7 +272,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Save a Copy…",
|
||||
&["File"],
|
||||
Some("Cmd+Alt+S"),
|
||||
r##"{"path":str (format from the extension),"quality":0..12? (JPEG),"jpegFormat":"baseline|optimized|progressive"? (JPEG; default baseline),"layers":bool=true,"overwrite":bool=false (replace the document's own file),"tiffLayers":bool=false (TIFF: keep the layers; flat by default),"tiffCompression":"none|lzw|zip"? (TIFF: image compression; unset, none),"tiffTransparency":bool? (TIFF: Save Transparency; off composites over white; unset, kept unless the document has alpha or spot channels),"tiffPyramid":bool=false (TIFF: Save Image Pyramid, half-size copies after the image),"tgaBits":24|32? (TGA: 32 writes the alpha channel, or else the transparency, as the alpha; unset, 32 with an alpha channel or transparency, else 24),"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"ddsCompression":"none|bc1|bc2|bc3"="none" (DDS: uncompressed, or block compression; BC1 keeps alpha on or off),"ddsMipmaps":bool=false (DDS: also write the full mip chain),"pcraftFullComposite":bool? (.pcraft: request the flattened document at its canvas size as composite/full.png; it may be omitted; check the manifest's full_composite member; unset, Preferences › File Handling)}"##,
|
||||
r##"{"path":str (format from the extension),"quality":0..12? (JPEG),"jpegFormat":"baseline|optimized|progressive"? (JPEG; default baseline),"layers":bool=true,"overwrite":bool=false (replace the document's own file),"tiffLayers":bool=false (TIFF: keep the layers; flat by default),"tiffCompression":"none|lzw|zip"? (TIFF: image compression; unset, none),"tiffTransparency":bool? (TIFF: Save Transparency; off composites over white; unset, kept unless the document has alpha or spot channels),"tiffPyramid":bool=false (TIFF: Save Image Pyramid, half-size copies after the image),"tgaBits":24|32? (TGA: 32 writes the alpha channel, or else the transparency, as the alpha; unset, 32 with an alpha channel or transparency, else 24),"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"ddsCompression":"none|bc1|bc2|bc3"="none" (DDS: uncompressed, or block compression; BC1 keeps alpha on or off),"ddsMipmaps":bool=false (DDS: also write the full mip chain),"maximizeCompatibility":bool? (PSD/PSB: write the real merged image; unset, Preferences > File Handling > Maximize PSD and PSB File Compatibility),"pcraftFullComposite":bool? (.pcraft: request the flattened document at its canvas size as composite/full.png; it may be omitted; check the manifest's full_composite member; unset, Preferences › File Handling)}"##,
|
||||
native_doc,
|
||||
save_a_copy
|
||||
),
|
||||
@@ -349,7 +349,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Batch…",
|
||||
&["File", "Automate"],
|
||||
None,
|
||||
r##"{"action":name|index|[steps]? (used only when steps is absent; named actions exclude disabled steps),"set":str? (for a named action),"steps":[[commandId,params]|{"command":id,"params":{}}…] (a recorded action; a Save, Save As or Save a Copy step marks the document written to output, in this command's format and options: as the save found it if it was closed or another document is active at the end, else as the action leaves it; with no such step, the active document at the end),"source":"folder|open"="folder" (open runs on live documents),"input":folder|[paths] (folder source),"errorLog":path? (blank disables it),"suppressOpenOptions":bool=false (use caller RAW defaults and fully supported prior XMP without a dialog; unsupported prior settings or unanswerable profile questions fail the input),"suppressWarnings":bool=false (apply caller opening policy without profile questions; import diagnostics remain in notices),"overrideOpen":bool=false (execute action Open steps with the current input path, retaining other Open options; without an executed Open the input fails),"includeNonImages":bool=false (folder inputs report unsupported files as per-file errors instead of skipping them; arrays are unchanged),"includeSubfolders":bool=false (a folder input also takes the images in its subfolders, up to 32 levels and 100000 files; ordinary outputs are flat; namingComponents can retain subfolders when flatOutput is false),"destination":"folder|saveAndClose"="folder" (Save and Close writes the same source path/format; rejects already-open sources and changed result document identity),"output":folder? (required only for folder; omit for Save and Close),"format":"same|png|jpg|psd|tiff|…"="same" (Save and Close requires same),"quality":0..12?,"jpegFormat":"baseline|optimized|progressive"?,"namingComponents":[{"component":choice}|{"text":str}]? (up to six; the exact filename, with no automatic extension),"compatibility":{"macintosh":bool,"windows":bool,"unix":bool}?,"flatOutput":bool=false (structured naming only; put every result directly under output, dropping its subfolder path; duplicate output names make later results error entries),"overrideSaveAs":bool? (recorded routing; requires a directly represented Save configuration),"stopOnError":bool=false (report the first failed input and stop; earlier outputs remain saved),"naming":string? (blank keeps source names; {name}/{NAME}/{Name}, {serial}/{serial:1..4}, {letter}/{LETTER}, {date} and date formats, {ext}/{EXT}; template determines the whole filename),"startingSerial":1..4294967295=1 (input position, including failed inputs),"tiffLayers":bool? (Folder default false; Save and Close default true, with false refused when TIFF layer data would be lost),"tiffCompression":"none|lzw|zip"?,"tiffTransparency":bool?,"tiffPyramid":bool=false,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"ddsCompression":"none|bc1|bc2|bc3"="none" (DDS: uncompressed, or block compression; BC1 keeps alpha on or off),"ddsMipmaps":bool=false (DDS: also write the full mip chain),"calledActions":[{"name","steps"}…]? (internal action references; default: the session's; a named source is selected from the caller)} → {files, errors, stopped?, skipped?, errorLog?, documents?} (structured naming, includeNonImages and Save and Close refuse duplicate results; ordinary native templates keep the later result)"##,
|
||||
r##"{"action":name|index|[steps]? (used only when steps is absent; named actions exclude disabled steps),"set":str? (for a named action),"steps":[[commandId,params]|{"command":id,"params":{}}…] (a recorded action; a Save, Save As or Save a Copy step marks the document written to output, in this command's format and options: as the save found it if it was closed or another document is active at the end, else as the action leaves it; with no such step, the active document at the end),"source":"folder|open"="folder" (open runs on live documents),"input":folder|[paths] (folder source),"errorLog":path? (blank disables it),"suppressOpenOptions":bool=false (use caller RAW defaults and fully supported prior XMP without a dialog; unsupported prior settings or unanswerable profile questions fail the input),"suppressWarnings":bool=false (apply caller opening policy without profile questions; import diagnostics remain in notices),"overrideOpen":bool=false (execute action Open steps with the current input path, retaining other Open options; without an executed Open the input fails),"includeNonImages":bool=false (folder inputs report unsupported files as per-file errors instead of skipping them; arrays are unchanged),"includeSubfolders":bool=false (a folder input also takes the images in its subfolders, up to 32 levels and 100000 files; ordinary outputs are flat; namingComponents can retain subfolders when flatOutput is false),"destination":"folder|saveAndClose"="folder" (Save and Close writes the same source path/format; rejects already-open sources and changed result document identity),"output":folder? (required only for folder; omit for Save and Close),"format":"same|png|jpg|psd|tiff|…"="same" (Save and Close requires same),"quality":0..12?,"jpegFormat":"baseline|optimized|progressive"?,"namingComponents":[{"component":choice}|{"text":str}]? (up to six; the exact filename, with no automatic extension),"compatibility":{"macintosh":bool,"windows":bool,"unix":bool}?,"flatOutput":bool=false (structured naming only; put every result directly under output, dropping its subfolder path; duplicate output names make later results error entries),"overrideSaveAs":bool? (recorded routing; requires a directly represented Save configuration),"stopOnError":bool=false (report the first failed input and stop; earlier outputs remain saved),"naming":string? (blank keeps source names; {name}/{NAME}/{Name}, {serial}/{serial:1..4}, {letter}/{LETTER}, {date} and date formats, {ext}/{EXT}; template determines the whole filename),"startingSerial":1..4294967295=1 (input position, including failed inputs),"tiffLayers":bool? (Folder default false; Save and Close default true, with false refused when TIFF layer data would be lost),"tiffCompression":"none|lzw|zip"?,"tiffTransparency":bool?,"tiffPyramid":bool=false,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"ddsCompression":"none|bc1|bc2|bc3"="none" (DDS: uncompressed, or block compression; BC1 keeps alpha on or off),"ddsMipmaps":bool=false (DDS: also write the full mip chain),"maximizeCompatibility":bool? (PSD/PSB: write the real merged image; unset, Preferences > File Handling > Maximize PSD and PSB File Compatibility),"calledActions":[{"name","steps"}…]? (internal action references; default: the session's; a named source is selected from the caller)} → {files, errors, stopped?, skipped?, errorLog?, documents?} (structured naming, includeNonImages and Save and Close refuse duplicate results; ordinary native templates keep the later result)"##,
|
||||
native,
|
||||
batch
|
||||
),
|
||||
@@ -358,7 +358,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Image Processor…",
|
||||
&["File", "Scripts"],
|
||||
None,
|
||||
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,"jpegFormat":"baseline|optimized|progressive"?,"tiffLayers":bool=false,"tiffCompression":"none|lzw|zip"?,"tiffTransparency":bool?,"tiffPyramid":bool=false,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"ddsCompression":"none|bc1|bc2|bc3"="none" (DDS: uncompressed, or block compression; BC1 keeps alpha on or off),"ddsMipmaps":bool=false (DDS: also write the full mip chain),"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,"jpegFormat":"baseline|optimized|progressive"?,"tiffLayers":bool=false,"tiffCompression":"none|lzw|zip"?,"tiffTransparency":bool?,"tiffPyramid":bool=false,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"ddsCompression":"none|bc1|bc2|bc3"="none" (DDS: uncompressed, or block compression; BC1 keeps alpha on or off),"ddsMipmaps":bool=false (DDS: also write the full mip chain),"maximizeCompatibility":bool? (PSD/PSB: write the real merged image; unset, Preferences > File Handling > Maximize PSD and PSB File Compatibility),"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
|
||||
),
|
||||
@@ -386,7 +386,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Layers to Files…",
|
||||
&["File", "Export"],
|
||||
None,
|
||||
r##"{"dir":folder,"format":"png|jpg|psd|tiff|…"="png","prefix":str=document name,"visibleOnly":bool=true,"trim":bool=false,"quality":0..12?,"jpegFormat":"baseline|optimized|progressive"?,"tiffLayers":bool=false,"tiffCompression":"none|lzw|zip"?,"tiffTransparency":bool?,"tiffPyramid":bool=false,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"ddsCompression":"none|bc1|bc2|bc3"="none" (DDS: uncompressed, or block compression; BC1 keeps alpha on or off),"ddsMipmaps":bool=false (DDS: also write the full mip chain)} → {files}"##,
|
||||
r##"{"dir":folder,"format":"png|jpg|psd|tiff|…"="png","prefix":str=document name,"visibleOnly":bool=true,"trim":bool=false,"quality":0..12?,"jpegFormat":"baseline|optimized|progressive"?,"tiffLayers":bool=false,"tiffCompression":"none|lzw|zip"?,"tiffTransparency":bool?,"tiffPyramid":bool=false,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"ddsCompression":"none|bc1|bc2|bc3"="none" (DDS: uncompressed, or block compression; BC1 keeps alpha on or off),"ddsMipmaps":bool=false (DDS: also write the full mip chain),"maximizeCompatibility":bool? (PSD/PSB: write the real merged image; unset, Preferences > File Handling > Maximize PSD and PSB File Compatibility)} → {files}"##,
|
||||
native_doc,
|
||||
layers_to_files
|
||||
),
|
||||
|
||||
@@ -72,7 +72,8 @@ impl SaveOpts {
|
||||
/// `jpegFormat` it doesn't name or a `tgaBits` other than 24 or 32 is an error); the image
|
||||
/// previews and the PSD merged image from the session's File Handling preferences (Ask When
|
||||
/// Saving has no dialog here, so it saves them, as Photoshop's Save As checkbox and its
|
||||
/// Maximize Compatibility checkbox both start on). The `.pcraft` full-size composite comes from
|
||||
/// Maximize Compatibility checkbox both start on); `maximizeCompatibility` decides the merged
|
||||
/// image for this save instead. The `.pcraft` full-size composite comes from
|
||||
/// `pcraftFullComposite`, or without it from the File Handling preference.
|
||||
pub(crate) fn from_params(s: &Session, p: &Value, cmd: &str) -> Result<Self> {
|
||||
let file_handling = &s.prefs().file_handling;
|
||||
@@ -88,7 +89,7 @@ impl SaveOpts {
|
||||
tiff_pyramid: tiff_bool_param(p, "tiffPyramid", cmd)?.unwrap_or(false),
|
||||
dds: dds_param(p, cmd)?.unwrap_or_default(),
|
||||
image_previews: file_handling.image_previews != crate::prefs::Ask::Never,
|
||||
psd_maximize_compatibility: file_handling.maximize_psd_compatibility != crate::prefs::Ask::Never,
|
||||
psd_maximize_compatibility: maximize_compatibility_param(p, cmd)?.unwrap_or(file_handling.maximize_psd_compatibility != crate::prefs::Ask::Never),
|
||||
pcraft_full_composite: pcraft_full_composite_param(p, cmd)?.unwrap_or(file_handling.pcraft_full_composite),
|
||||
})
|
||||
}
|
||||
@@ -131,6 +132,19 @@ pub fn pcraft_full_composite_param(p: &Value, cmd: &str) -> Result<Option<bool>>
|
||||
}
|
||||
}
|
||||
|
||||
/// One save's Maximize Compatibility in `p` (`"maximizeCompatibility": bool`, Photoshop's checkbox
|
||||
/// when saving a PSD/PSB; absent or null: the File Handling preference decides). Anything else is
|
||||
/// a bad parameter of command `cmd` (#8064).
|
||||
pub fn maximize_compatibility_param(p: &Value, cmd: &str) -> Result<Option<bool>> {
|
||||
match p.get("maximizeCompatibility") {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(v) => v
|
||||
.as_bool()
|
||||
.map(Some)
|
||||
.ok_or_else(|| EngineError::BadParams { cmd: cmd.into(), msg: format!("`maximizeCompatibility` must be true or false, got {v}") }),
|
||||
}
|
||||
}
|
||||
|
||||
/// The TIFF compression in `p` (`"tiffCompression": "none" | "lzw" | "zip"`, Photoshop's TIFF
|
||||
/// Options; absent or null: none). Anything else is a bad parameter of command `cmd` (#5301).
|
||||
pub fn tiff_compression_param(p: &Value, cmd: &str) -> Result<Option<photocraft_codecs::TiffCompression>> {
|
||||
|
||||
@@ -1875,6 +1875,41 @@ fn full_composite_preference_controls_the_pcraft_full_size_composite() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #8064: `maximizeCompatibility` decides one save's PSD/PSB merged image over the preference
|
||||
/// (Photoshop's Maximize Compatibility checkbox); unset, the preference decides. Anything but a
|
||||
/// boolean is refused before anything is written.
|
||||
#[test]
|
||||
fn maximize_compatibility_param_overrides_the_preference_for_one_save() {
|
||||
use crate::prefs::Ask;
|
||||
let dir = tmp("maximize-param");
|
||||
let mut s = session(300, 200, 16);
|
||||
s.execute("layer.new.layer", json!({})).unwrap();
|
||||
let real_merged = |path: &str| photocraft_psd::PsdFile::from_bytes(&std::fs::read(path).unwrap()).unwrap().has_real_merged_data();
|
||||
for (ask, param, expect) in [
|
||||
(Ask::Always, json!(false), false),
|
||||
(Ask::Never, json!(true), true),
|
||||
(Ask::Ask, json!(false), false),
|
||||
(Ask::Never, json!(null), false),
|
||||
(Ask::Always, json!(null), true),
|
||||
] {
|
||||
s.edit_prefs(|p| p.file_handling.maximize_psd_compatibility = ask);
|
||||
for ext in ["psd", "psb"] {
|
||||
let out = join(&dir, &format!("{ask:?}-{param}.{ext}"));
|
||||
s.execute("file.saveACopy", json!({"path": out, "maximizeCompatibility": param})).unwrap();
|
||||
assert_eq!(real_merged(&out), Some(expect), "{ask:?} {param} {ext}");
|
||||
}
|
||||
}
|
||||
let state = |s: &Session| s.active().map(|st| (st.revision, st.path.clone(), st.is_dirty()));
|
||||
let before = state(&s);
|
||||
for bad in [json!("no"), json!(0), json!(1), json!([true])] {
|
||||
let out = join(&dir, "bad.psd");
|
||||
let r = s.execute("file.saveACopy", json!({"path": out, "maximizeCompatibility": bad}));
|
||||
assert!(matches!(r, Err(EngineError::BadParams { .. })), "{bad}: {r:?}");
|
||||
assert!(!std::path::Path::new(&out).exists(), "{bad}: nothing written");
|
||||
assert_eq!(state(&s), before, "{bad}: the document is as it was");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_as_forces_the_decoder() {
|
||||
let dir = tmp("openas");
|
||||
|
||||
@@ -1422,6 +1422,50 @@ fn automation_save_passes_targa_bits_without_asking() {
|
||||
assert_eq!(written.borrow().len(), 3, "a bad tgaBits writes nothing");
|
||||
}
|
||||
|
||||
/// #8064: `app.save` takes `maximizeCompatibility` for a PSD/PSB's merged image over Preferences ›
|
||||
/// File Handling; unset, the preference decides. Anything but a boolean writes nothing.
|
||||
#[test]
|
||||
fn app_save_takes_maximize_compatibility() {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
let asked: Rc<RefCell<Vec<bool>>> = Rc::default();
|
||||
let written: Rc<RefCell<Vec<String>>> = Rc::default();
|
||||
let (a, w) = (asked.clone(), written.clone());
|
||||
let services = crate::Services {
|
||||
export: Some(Box::new(move |_d: &photocraft_doc::Document, _p: &str, s: &crate::ExportSettings| {
|
||||
a.borrow_mut().push(s.psd_maximize_compatibility);
|
||||
Ok((b"out".to_vec(), Vec::new()))
|
||||
})),
|
||||
automation_write: Some(Box::new(move |path: &str, _b: &[u8]| {
|
||||
w.borrow_mut().push(path.to_string());
|
||||
Ok(())
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), services);
|
||||
let ctx = egui::Context::default();
|
||||
app.run("file.new", json!({"width": 8, "height": 8})).unwrap();
|
||||
let saves = [
|
||||
json!({"path": "a.psd"}),
|
||||
json!({"path": "b.psd", "maximizeCompatibility": false}),
|
||||
json!({"path": "c.psd", "maximizeCompatibility": true}),
|
||||
json!({"path": "d.psd", "maximizeCompatibility": null}),
|
||||
];
|
||||
for params in &saves {
|
||||
let r = call(&mut app, &ctx, "app.save", params.clone());
|
||||
assert_eq!(r["result"]["path"], params["path"], "{r}");
|
||||
}
|
||||
app.session.edit_prefs(|p| p.file_handling.maximize_psd_compatibility = photocraft_engine::prefs::Ask::Never);
|
||||
for params in [json!({"path": "e.psd"}), json!({"path": "f.psd", "maximizeCompatibility": true})] {
|
||||
let r = call(&mut app, &ctx, "app.save", params.clone());
|
||||
assert_eq!(r["result"]["path"], params["path"], "{r}");
|
||||
}
|
||||
assert_eq!(*asked.borrow(), [true, false, true, true, false, true]);
|
||||
let r = call(&mut app, &ctx, "app.save", json!({"path": "g.psd", "maximizeCompatibility": "no"}));
|
||||
assert!(r["error"].as_str().unwrap().contains("maximizeCompatibility"), "{r}");
|
||||
assert_eq!(written.borrow().len(), 6, "a bad maximizeCompatibility writes nothing");
|
||||
}
|
||||
|
||||
/// `app.save` takes JPEG Options (`quality` 0–12, `jpegFormat`) and never stops at the prompt;
|
||||
/// a value it can't read is an error that writes nothing.
|
||||
#[test]
|
||||
|
||||
@@ -8,15 +8,17 @@ pub(crate) const SAVE_OPTIONS_OPEN: &str = "Answer the TIFF, Targa, DDS or JPEG
|
||||
|
||||
/// The format answers a save takes instead of asking: the Targa bits per pixel (`"tgaBits"`),
|
||||
/// the DDS options (`"ddsCompression"`, `"ddsMipmaps"`), JPEG Options (`"quality"` 0–12,
|
||||
/// `"jpegFormat"`) and TIFF Options (`"tiffCompression"`, `"tiffTransparency"`, `"tiffPyramid"`,
|
||||
/// `"tiffLayers"`). `None` asks in Save As and takes the default in automation saves, which never
|
||||
/// prompt.
|
||||
/// `"jpegFormat"`), TIFF Options (`"tiffCompression"`, `"tiffTransparency"`, `"tiffPyramid"`,
|
||||
/// `"tiffLayers"`) and a PSD/PSB's Maximize Compatibility (`"maximizeCompatibility"`). `None`
|
||||
/// asks in Save As and takes the default in automation saves, which never prompt (Maximize
|
||||
/// Compatibility: Preferences › File Handling decides).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct FormatChoices {
|
||||
pub tga_bits: Option<photocraft_io::TgaBits>,
|
||||
pub dds: Option<photocraft_io::DdsOptions>,
|
||||
pub jpeg: Option<jpeg_options_ui::JpegChoice>,
|
||||
pub tiff: Option<tiff_options_ui::TiffChoice>,
|
||||
pub maximize_compatibility: Option<bool>,
|
||||
}
|
||||
|
||||
impl FormatChoices {
|
||||
@@ -27,6 +29,7 @@ impl FormatChoices {
|
||||
dds: dds_options_ui::options_param(params, cmd)?,
|
||||
jpeg: jpeg_options_ui::choice_param(params, cmd)?,
|
||||
tiff: tiff_options_ui::choice_param(params, cmd)?,
|
||||
maximize_compatibility: maximize_param(params, cmd)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,12 +41,18 @@ impl FormatChoices {
|
||||
dds: dds_options_ui::options_param(params, cmd)?,
|
||||
jpeg: jpeg_options_ui::recorded_choice(params, cmd)?,
|
||||
tiff: tiff_options_ui::choice_param(params, cmd)?,
|
||||
maximize_compatibility: maximize_param(params, cmd)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// The export settings with these answers applied.
|
||||
pub fn apply(self, settings: ExportSettings) -> ExportSettings {
|
||||
let settings = ExportSettings { tga_bits: self.tga_bits.or(settings.tga_bits), dds: self.dds.unwrap_or(settings.dds), ..settings };
|
||||
let settings = ExportSettings {
|
||||
tga_bits: self.tga_bits.or(settings.tga_bits),
|
||||
dds: self.dds.unwrap_or(settings.dds),
|
||||
psd_maximize_compatibility: self.maximize_compatibility.unwrap_or(settings.psd_maximize_compatibility),
|
||||
..settings
|
||||
};
|
||||
let settings = match self.tiff {
|
||||
Some(t) => t.apply(settings),
|
||||
None => settings,
|
||||
@@ -68,9 +77,17 @@ impl FormatChoices {
|
||||
if let Some(t) = self.tiff {
|
||||
t.record(step);
|
||||
}
|
||||
if let Some(maximize) = self.maximize_compatibility {
|
||||
step["maximizeCompatibility"] = maximize.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `"maximizeCompatibility"` in command or control call `cmd`'s params, read as the engine reads it.
|
||||
fn maximize_param(params: &Value, cmd: &str) -> Result<Option<bool>, String> {
|
||||
photocraft_engine::file_cmds::maximize_compatibility_param(params, cmd).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Decode a file: (document, warnings about anything approximated or dropped).
|
||||
pub type ImportFn = Box<dyn Fn(&str, &[u8], &photocraft_io::ImportOptions) -> Result<(Document, Vec<String>), String>>;
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ The transport is `apps/photocraft/src/control_server.rs`, and the handlers are i
|
||||
another document becomes active before the capture, the request fails with an error rather than
|
||||
saving the wrong document
|
||||
- `ui.focus`: bring the main window to the front
|
||||
- `app.open {path}` / `app.save {path?, tgaBits?, ddsCompression?, ddsMipmaps?, quality?, jpegFormat?, tiffCompression?, tiffTransparency?, tiffPyramid?, tiffLayers?}` (JPEG: `quality` on the 0–12 scale, `jpegFormat` `baseline`, `optimized` or `progressive`; TIFF: `tiffCompression` `none`, `lzw` or `zip`, `tiffTransparency` and `tiffPyramid` bools; automation never shows JPEG or TIFF Options): relative file I/O through the configured automation roots (`app.open` reads under the read root, `app.save` writes under the write root; absolute paths beneath the canonical or configured root are accepted, while `..` and paths escaping the root are refused, and both fail closed when no root was granted). Both reply with `warnings` (import/export notes such as "adjustment layer flattened"; `[]` when none), also shown to the user in the status bar and as a notice (`notices` in `ui.inspect`); `app.open` also returns the `path` and document `name` (directory `.pcraft` — the native on-disk form — loads through the read root's capability; the single-file (zip) form takes the bytes path), `app.save` the `path` written. `app.save` without `path` writes back only to the document's own PSD, PSB or `.pcraft` file, like File › Save. `tgaBits` (24 or 32) sets a TGA's bits per pixel: 32 writes the alpha channel (or else the transparency) as its alpha, 24 none; without it, 32 with an alpha channel or transparency, else 24. `ddsCompression` (`none`, `bc1`, `bc2` or `bc3`) and `ddsMipmaps` (bool) set a DDS texture's compression and mip chain; without them, uncompressed without mipmaps. Automation saves never show TGA, DDS, JPEG or TIFF options. Automation opens and saves never fire script events. Use these two rather than `file.open`, `file.save`, `file.saveAs` or `file.saveACopy`, which the control channel refuses (see [Engine commands](#engine-commands))
|
||||
- `app.open {path}` / `app.save {path?, tgaBits?, ddsCompression?, ddsMipmaps?, quality?, jpegFormat?, tiffCompression?, tiffTransparency?, tiffPyramid?, tiffLayers?, maximizeCompatibility?}` (JPEG: `quality` on the 0–12 scale, `jpegFormat` `baseline`, `optimized` or `progressive`; TIFF: `tiffCompression` `none`, `lzw` or `zip`, `tiffTransparency` and `tiffPyramid` bools; automation never shows JPEG or TIFF Options; PSD/PSB: `maximizeCompatibility` false writes Photoshop's placeholder instead of the merged image (Bitmap, Indexed Color and Duotone documents still write the real image); unset or null follows Preferences › File Handling: Always and Ask When Saving write the real merged image, Never the placeholder, and `app.save` never opens a compatibility dialog): relative file I/O through the configured automation roots (`app.open` reads under the read root, `app.save` writes under the write root; absolute paths beneath the canonical or configured root are accepted, while `..` and paths escaping the root are refused, and both fail closed when no root was granted). Both reply with `warnings` (import/export notes such as "adjustment layer flattened"; `[]` when none), also shown to the user in the status bar and as a notice (`notices` in `ui.inspect`); `app.open` also returns the `path` and document `name` (directory `.pcraft` — the native on-disk form — loads through the read root's capability; the single-file (zip) form takes the bytes path), `app.save` the `path` written. `app.save` without `path` writes back only to the document's own PSD, PSB or `.pcraft` file, like File › Save. `tgaBits` (24 or 32) sets a TGA's bits per pixel: 32 writes the alpha channel (or else the transparency) as its alpha, 24 none; without it, 32 with an alpha channel or transparency, else 24. `ddsCompression` (`none`, `bc1`, `bc2` or `bc3`) and `ddsMipmaps` (bool) set a DDS texture's compression and mip chain; without them, uncompressed without mipmaps. Automation saves never show TGA, DDS, JPEG or TIFF options. Automation opens and saves never fire script events. Use these two rather than `file.open`, `file.save`, `file.saveAs` or `file.saveACopy`, which the control channel refuses (see [Engine commands](#engine-commands))
|
||||
- `app.quit`
|
||||
|
||||
## Engine commands
|
||||
@@ -386,7 +386,7 @@ How each MCP tool maps onto control methods in bridge mode:
|
||||
| `doc_export_layers {dir, …}` | `engine.execute {command: "file.export.layersToFiles", params}` |
|
||||
| `doc_export_data_sets {dir, …}` | `engine.execute {command: "file.export.dataSetsAsFiles", params}` |
|
||||
| `doc_import_data_sets {path, …}` | `engine.execute {command: "file.import.variableDataSets", params}` |
|
||||
| `doc_save {path?, tgaBits?, ddsCompression?, ddsMipmaps?}` / `doc_export {path, tgaBits?, ddsCompression?, ddsMipmaps?}` | `app.save {path?, tgaBits?, ddsCompression?, ddsMipmaps?}` |
|
||||
| `doc_save {path?, tgaBits?, ddsCompression?, ddsMipmaps?, maximizeCompatibility?}` / `doc_export {path, tgaBits?, ddsCompression?, ddsMipmaps?, maximizeCompatibility?}` | `app.save {path?, tgaBits?, ddsCompression?, ddsMipmaps?, maximizeCompatibility?}` |
|
||||
| `doc_render_preview {max_side?}` | `ui.screenshot`, returned directly as PNG image content |
|
||||
| `session_list`, `ui_inspect` | `ui.inspect` |
|
||||
| `ui_screenshot {max_side?}` | `ui.screenshot`, returned as PNG image content |
|
||||
@@ -452,7 +452,7 @@ stdio, and a value that is not a port is a usage error naming the variable. Impl
|
||||
| `doc.exportLayers` | `{dir, format? (png), prefix?, visibleOnly? (true), quality?, index?}` → `{files, skipped}` (plus `count` for workspace exports): one file per exported top-level layer, named like `file.export.layersToFiles` names them. `count` is present only for workspace exports and counts written files only. `skipped` is always an array (empty when nothing is omitted), in top-to-bottom order, of `{layer, name, reason}`: the session layer id, original unsanitized name, and `"hidden by visibleOnly"` or `"adjustment layer has no pixels of its own"`. Visibility takes precedence for hidden adjustments; `visibleOnly:false` exports hidden non-adjustment layers. Groups export merged; their children are not separate exports or skips. Existing filenames/numbering are unchanged. `dir` resolves beneath the write root, so the ambient command stays refused. The bytes come from the command's own core, so both paths write the same files; the write capability refuses to overwrite an existing file. Encode/write failures are errors, not skips |
|
||||
| `doc.exportDataSets` | `{dir, format? (png), dataSets? (all), naming? ("{name}\|{index}\|{document}"), index?}` → `{files, count}`: the capability-scoped `file.export.dataSetsAsFiles`. The batch is planned first and colliding names are refused before any file is written, as in the command |
|
||||
| `doc.importDataSets` | `{path, delimiter? (,), index?}` → `{imported}`: the capability-scoped `file.import.variableDataSets`; the CSV is read beneath the read root |
|
||||
| `doc.save` | `{path?, format?, quality?, index?, tiffLayers?, tiffCompression?, tiffTransparency?, tiffPyramid?, tgaBits?, ddsCompression?, ddsMipmaps?, pcraftFullComposite?}` (`.pcraft` native, else export by extension; set `tiffLayers: true` to preserve TIFF layers rather than exporting a flattened TIFF; `tiffCompression` (`none`, `lzw`, `zip`), `tiffTransparency` (bool: Save Transparency; unset, kept unless the document has alpha or spot channels) and `tiffPyramid` (bool) are TIFF Options; `tgaBits` 24 or 32 sets a TGA's bits per pixel, and `ddsCompression`/`ddsMipmaps` a DDS texture's compression and mip chain, as `app.save` does; `pcraftFullComposite: true` also writes the flattened document at its canvas size into a `.pcraft` file as `composite/full.png`; the save may leave it out in the cases listed in [the format guide](../book/src/formats/pcraft.md), and the manifest's `full_composite` member names it when written). Without `path` only a PSD, PSB or `.pcraft` document is written back to its own file, in its own format; anything else is an error and the file is left unchanged. A successful layered save (PSD, PSB or `.pcraft`) records the current revision as saved and makes the file the document's path, so `session.list` reports `dirty: false`; a flat export is a copy and leaves the document dirty |
|
||||
| `doc.save` | `{path?, format?, quality?, index?, tiffLayers?, tiffCompression?, tiffTransparency?, tiffPyramid?, tgaBits?, ddsCompression?, ddsMipmaps?, pcraftFullComposite?, maximizeCompatibility?}` (`.pcraft` native, else export by extension; set `tiffLayers: true` to preserve TIFF layers rather than exporting a flattened TIFF; `tiffCompression` (`none`, `lzw`, `zip`), `tiffTransparency` (bool: Save Transparency; unset, kept unless the document has alpha or spot channels) and `tiffPyramid` (bool) are TIFF Options; `tgaBits` 24 or 32 sets a TGA's bits per pixel, `ddsCompression`/`ddsMipmaps` a DDS texture's compression and mip chain, and `maximizeCompatibility: false` writes a layered PSD/PSB with Photoshop's placeholder instead of the merged image, except for Bitmap, Indexed Color and Duotone documents, as `app.save` does; `pcraftFullComposite: true` also writes the flattened document at its canvas size into a `.pcraft` file as `composite/full.png`; the save may leave it out in the cases listed in [the format guide](../book/src/formats/pcraft.md), and the manifest's `full_composite` member names it when written). Without `path` only a PSD, PSB or `.pcraft` document is written back to its own file, in its own format; anything else is an error and the file is left unchanged. A successful layered save (PSD, PSB or `.pcraft`) records the current revision as saved and makes the file the document's path, so `session.list` reports `dirty: false`; a flat export is a copy and leaves the document dirty |
|
||||
| `doc.inspect` | `{index?}`: same JSON as `document.inspect` |
|
||||
| `doc.render` | `{index?, maxSide? (1024; 0 = full), path?}`: PNG to `path`, else `{mime, base64}` |
|
||||
| `doc.select` / `doc.close` | `{index}` / `{index?}` |
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user