mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
This commit is contained in:
@@ -1849,6 +1849,63 @@ fn open_to(_: &str, p: &Value) -> Option<Descriptor> {
|
||||
Some(desc("null").with("null", path_value(jstr(p, "path")?)))
|
||||
}
|
||||
|
||||
/// Save As format classes (`As`) and the extensions PhotoCraft saves them under. The first
|
||||
/// extension of a class is the one exported, and the first class of an extension is the one
|
||||
/// written back. `WebPFormat` is Photoshop's built-in WebP (checked against Photoshop 2026); the
|
||||
/// Google classes come from the older WebPShop plug-in and are only read.
|
||||
const SAVE_FORMATS: &[(&str, &[&str])] = &[
|
||||
("Pht3", &["psd"]),
|
||||
("PNGF", &["png"]),
|
||||
("JPEG", &["jpg", "jpeg"]),
|
||||
("TIFF", &["tif", "tiff"]),
|
||||
("BMPF", &["bmp"]),
|
||||
("WebPFormat", &["webp"]),
|
||||
("Google WebPShop", &["webp"]),
|
||||
("Google WebP", &["webp"]),
|
||||
];
|
||||
|
||||
fn path_extension(path: &str) -> Option<String> {
|
||||
let name = path.rsplit(['/', '\\']).next()?;
|
||||
let (stem, ext) = name.rsplit_once('.')?;
|
||||
(!stem.is_empty() && !ext.is_empty()).then(|| ext.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
/// `save`: a plain Save, or a Save As (`As` format, `In` destination, `Cpy` As a Copy).
|
||||
/// Only a destination that is a file of the recorded format imports: a folder (#5430), a missing
|
||||
/// or unknown format or a mismatched extension stays a kept Photoshop step, so playback stops
|
||||
/// before writing rather than saving over the played file (#7102).
|
||||
fn save_from(d: &Descriptor) -> Mapped {
|
||||
let unsupported = Mapped::Unsupported { harmless: false };
|
||||
if d.get("As ").is_none() && d.get("In ").is_none() {
|
||||
return step("file.save", json!({}));
|
||||
}
|
||||
// A Save As needs a format descriptor to check the destination against: without one (or
|
||||
// with a format that isn't a descriptor) the step is kept.
|
||||
let Some(format) = get_obj(d, "As ") else { return unsupported };
|
||||
let Some(path) = d.get("In ").and_then(path_text) else { return unsupported };
|
||||
if get_bool(d, "DIDr") == Some(true) {
|
||||
return unsupported;
|
||||
}
|
||||
let Some(ext) = path_extension(&path) else { return unsupported };
|
||||
let class = trimmed(&format.class_id);
|
||||
let Some((_, exts)) = SAVE_FORMATS.iter().find(|(c, _)| *c == class) else { return unsupported };
|
||||
if !exts.contains(&ext.as_str()) {
|
||||
return unsupported;
|
||||
}
|
||||
step(if get_bool(d, "Cpy ") == Some(true) { "file.saveACopy" } else { "file.saveAs" }, json!({"path": path}))
|
||||
}
|
||||
|
||||
fn save_to(id: &str, p: &Value) -> Option<Descriptor> {
|
||||
if id == "file.save" {
|
||||
return Some(desc("null"));
|
||||
}
|
||||
let path = jstr(p, "path")?;
|
||||
let ext = path_extension(path)?;
|
||||
let (class, _) = SAVE_FORMATS.iter().find(|(_, exts)| exts.contains(&ext.as_str()))?;
|
||||
let d = desc("null").with("As ", D::Descriptor(desc(class))).with("In ", path_value(path));
|
||||
Some(if id == "file.saveACopy" { d.with("Cpy ", D::Boolean(true)) } else { d })
|
||||
}
|
||||
|
||||
/// Photoshop's four-character condition codes that differ from the string ids.
|
||||
const CONDITION_CODES: &[(&str, &str)] = &[("Lyrs", "hasLayers")];
|
||||
|
||||
@@ -3666,7 +3723,7 @@ const EVENTS: &[Event] = &[
|
||||
Event { code: "Plc ", name: "placeEvent", label: "Place", ids: &["file.placeEmbedded", "file.placeLinked"], from: place_from, to: place_to },
|
||||
Event { code: "Opn ", name: "open", label: "Open", ids: &["file.openAs"], from: open_from, to: open_to },
|
||||
Event { code: "", name: "conditional", label: "If", ids: &["actions.conditional"], from: conditional_from, to: conditional_to },
|
||||
ev!("save", "save", "Save", "file.save", &[]),
|
||||
Event { code: "save", name: "save", label: "Save", ids: &["file.save", "file.saveAs", "file.saveACopy"], from: save_from, to: save_to },
|
||||
ev!("Rvrt", "revert", "Revert", "file.revert", &[]),
|
||||
Event { code: "Ply ", name: "play", label: "Play", ids: &["actions.play"], from: play_from, to: play_to },
|
||||
Event { code: "", name: "applyComp", label: "Apply Layer Comp", ids: &["layerComp.apply"], from: apply_comp_from, to: apply_comp_to },
|
||||
|
||||
@@ -552,6 +552,8 @@ fn every_mapped_command_exports_and_imports_back() {
|
||||
("file.placeEmbedded", json!({"path": "/tmp/logo.png", "scale": 50})),
|
||||
("file.placeLinked", json!({"path": "/tmp/logo.png"})),
|
||||
("file.openAs", json!({"path": "/tmp/a.psd"})),
|
||||
("file.saveAs", json!({"path": "/tmp/out/a.png"})),
|
||||
("file.saveACopy", json!({"path": "/tmp/out/a.tif"})),
|
||||
("actions.conditional", json!({"if": "hasLayers", "then": "FS8-Layers", "else": "FS8-No-Layers", "set": "S"})),
|
||||
("actions.conditional", json!({"if": "landscape", "then": "Wide", "else": null, "set": "S"})),
|
||||
("tools.setColors", json!({"foreground": "#102030"})),
|
||||
@@ -2749,3 +2751,98 @@ fn hue_saturation_uses_photoshops_hue_key() {
|
||||
normalise(&mut p);
|
||||
assert_eq!(p["hue"], json!(30), "{p}");
|
||||
}
|
||||
|
||||
fn one_action(items: Vec<AtnItem>) -> AtnFile {
|
||||
AtnFile {
|
||||
name: UnicodeString::new_nul("Saves"),
|
||||
expanded: false,
|
||||
actions: vec![AtnAction { name: UnicodeString::new_nul("A"), items, ..Default::default() }],
|
||||
warnings: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn save_as(format: &str, path: &str) -> Descriptor {
|
||||
desc("null").with("As ", D::Descriptor(desc(format))).with("In ", path_value(path)).with("DocI", long(53.0))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_to_a_file_imports_with_its_destination() {
|
||||
let file = one_action(vec![
|
||||
item("save", "Save", Some(save_as("PNGF", "/out/result.png"))),
|
||||
item("save", "Save", Some(save_as("Google WebPShop", "C:\\out\\result.webp").with("Cpy ", D::Boolean(true)))),
|
||||
item("save", "Save", Some(save_as("JPEG", "/out/result.jpeg"))),
|
||||
item("save", "Save", Some(save_as("WebPFormat", "/out/result.webp"))),
|
||||
item("save", "Save", None),
|
||||
item("save", "Save", Some(desc("null").with("DocI", long(53.0)))),
|
||||
]);
|
||||
let imp = import(&atn::write(&file).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
imp.actions[0].steps,
|
||||
[
|
||||
("file.saveAs".to_string(), json!({"path": "/out/result.png"})),
|
||||
("file.saveACopy".to_string(), json!({"path": "C:\\out\\result.webp"})),
|
||||
("file.saveAs".to_string(), json!({"path": "/out/result.jpeg"})),
|
||||
("file.saveAs".to_string(), json!({"path": "/out/result.webp"})),
|
||||
("file.save".to_string(), json!({})),
|
||||
("file.save".to_string(), json!({})),
|
||||
]
|
||||
);
|
||||
assert!(imp.unsupported.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_that_cannot_be_mapped_stops_playback_instead_of_saving_over_the_file() {
|
||||
// Before #7102 each of these imported as a plain File › Save, which overwrote the file the
|
||||
// action was played on.
|
||||
let file = one_action(vec![
|
||||
item("save", "Save", Some(save_as("PNGF", "/out/folder"))),
|
||||
item("save", "Save", Some(save_as("PNGF", "/out/v1.png").with("DIDr", D::Boolean(true)))),
|
||||
item("save", "Save", Some(save_as("PNGF", "/out/result.webp"))),
|
||||
item("save", "Save", Some(save_as("GIFf", "/out/result.gif"))),
|
||||
item("save", "Save", Some(desc("null").with("As ", D::Descriptor(desc("PNGF"))))),
|
||||
// A format that isn't a descriptor, with and without a destination.
|
||||
item("save", "Save", Some(desc("null").with("As ", long(1.0)))),
|
||||
item("save", "Save", Some(desc("null").with("As ", long(1.0)).with("In ", path_value("/out/result.png")))),
|
||||
// A destination with no format at all.
|
||||
item("save", "Save", Some(desc("null").with("In ", path_value("/out/result.png")))),
|
||||
]);
|
||||
let imp = import(&atn::write(&file).unwrap()).unwrap();
|
||||
let ids: Vec<&str> = imp.actions[0].steps.iter().map(|(id, _)| id.as_str()).collect();
|
||||
assert_eq!(ids, [PHOTOSHOP_STEP; 8]);
|
||||
assert!(imp.actions[0].disabled.is_empty(), "kept saves stay on, so playback stops on them");
|
||||
// The kept steps are written back byte for byte.
|
||||
let a = &imp.actions[0];
|
||||
let (bytes, skipped) = export("Saves", &[a], &[], false).unwrap();
|
||||
assert!(skipped.is_empty());
|
||||
let back = atn::parse(&bytes).unwrap();
|
||||
for (out, orig) in back.actions[0].items.iter().zip(&file.actions[0].items) {
|
||||
assert_eq!(out.descriptor, orig.descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saves_round_trip_through_photoshop_events() {
|
||||
let steps = vec![
|
||||
("file.saveAs".to_string(), json!({"path": "/out/result.psd"})),
|
||||
("file.saveACopy".to_string(), json!({"path": "/out/copy.webp"})),
|
||||
("file.save".to_string(), json!({})),
|
||||
];
|
||||
let a = Action::new("A", steps.clone());
|
||||
let (bytes, skipped) = export("Saves", &[&a], &[], false).unwrap();
|
||||
assert!(skipped.is_empty());
|
||||
let file = atn::parse(&bytes).unwrap();
|
||||
let items = &file.actions[0].items;
|
||||
assert!(items.iter().all(|i| i.event.id() == "save"));
|
||||
let first = items[0].descriptor.as_ref().unwrap();
|
||||
assert_eq!(get_obj(first, "As ").map(|f| trimmed(&f.class_id)).as_deref(), Some("Pht3"));
|
||||
assert_eq!(first.get("In ").and_then(path_text).as_deref(), Some("/out/result.psd"));
|
||||
assert_eq!(get_bool(items[1].descriptor.as_ref().unwrap(), "Cpy "), Some(true));
|
||||
// Photoshop's own WebP class, not the plug-in's: Photoshop 2026 rejects `Google WebPShop`.
|
||||
assert_eq!(get_obj(items[1].descriptor.as_ref().unwrap(), "As ").map(|f| trimmed(&f.class_id)).as_deref(), Some("WebPFormat"));
|
||||
assert_eq!(import(&bytes).unwrap().actions[0].steps, steps);
|
||||
|
||||
// A Save As PhotoCraft can't express in Photoshop's terms is skipped, never exported as a
|
||||
// plain Save.
|
||||
let odd = Action::new("B", vec![("file.saveAs".to_string(), json!({"path": "/out/result.xcf"}))]);
|
||||
assert!(export("Saves", &[&odd], &[], false).is_err());
|
||||
}
|
||||
|
||||
@@ -1687,6 +1687,74 @@ mod tests {
|
||||
assert!(!b_changed && !a_changed);
|
||||
}
|
||||
|
||||
/// Loads a Photoshop set holding Invert + a Save As into `dest` and plays it on a PSD.
|
||||
/// Returns the played result, whether the PSD changed on disk and the files in `out`.
|
||||
fn play_imported_save_as(dest: &str) -> (Value, bool, Vec<String>) {
|
||||
use photocraft_psd::atn::{AtnAction, AtnEvent, AtnFile, AtnItem};
|
||||
use photocraft_psd::descriptor::{Descriptor, UnicodeString, Value as D};
|
||||
let dir = std::env::temp_dir().join(format!("photocraft-atn-save-as-{}-{}", dest.len(), std::process::id()));
|
||||
let out = dir.join("out");
|
||||
std::fs::create_dir_all(&out).unwrap();
|
||||
let src = dir.join("src.psd").to_string_lossy().into_owned();
|
||||
let path = |text: &str| {
|
||||
let units: Vec<u16> = text.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let mut b = b"txtu".to_vec();
|
||||
b.extend_from_slice(&((12 + units.len() * 2) as u32).to_le_bytes());
|
||||
b.extend_from_slice(&(units.len() as u32).to_le_bytes());
|
||||
units.iter().for_each(|u| b.extend_from_slice(&u.to_le_bytes()));
|
||||
D::Path(b)
|
||||
};
|
||||
let save = Descriptor::new("null")
|
||||
.with("As ", D::Descriptor(Descriptor::new("PNGF")))
|
||||
.with("In ", path(&out.join(dest).to_string_lossy()))
|
||||
.with("DocI", D::Integer(53));
|
||||
let item = |code: &[u8; 4], label: &str, d: Option<Descriptor>| AtnItem {
|
||||
event: AtnEvent::Code(*code),
|
||||
dictionary: label.into(),
|
||||
descriptor: d,
|
||||
..Default::default()
|
||||
};
|
||||
let set = AtnFile {
|
||||
name: UnicodeString::new_nul("Export"),
|
||||
actions: vec![AtnAction {
|
||||
name: UnicodeString::new_nul("Invert and save"),
|
||||
items: vec![item(b"Invr", "Invert", None), item(b"save", "Save", Some(save))],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let mut app = disk_app(vec![]);
|
||||
app.run("file.new", json!({"width": 16, "height": 16})).unwrap();
|
||||
app.save_as(Some(src.clone())).unwrap();
|
||||
let before = std::fs::read(&src).unwrap();
|
||||
app.run("actions.load", json!({"data": photocraft_engine::paint::tile::b64_encode(&photocraft_psd::atn::write(&set).unwrap())})).unwrap();
|
||||
app.open_path(&src).unwrap();
|
||||
let played = app.run("actions.play", json!({"action": 0})).unwrap();
|
||||
let mut written: Vec<String> = std::fs::read_dir(&out).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect();
|
||||
written.sort();
|
||||
let r = (played, std::fs::read(&src).unwrap() != before, written);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
r
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imported_save_as_writes_the_recorded_file_not_the_played_one() {
|
||||
// #7102: the Save As imported as File › Save, which wrote over src.psd.
|
||||
let (played, src_changed, written) = play_imported_save_as("result.png");
|
||||
assert!(played.get("failed").is_none(), "{played}");
|
||||
assert!(!src_changed, "the Save As wrote over the file the action was played on");
|
||||
assert_eq!(written, ["result.png"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imported_save_as_into_a_folder_stops_before_writing() {
|
||||
// A folder destination isn't mapped yet (#5430): playback stops on it, nothing is saved.
|
||||
let (played, src_changed, written) = play_imported_save_as("folder");
|
||||
assert!(played.get("failed").is_some(), "{played}");
|
||||
assert!(!src_changed);
|
||||
assert!(written.is_empty(), "{written:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_folders_group_actions_and_collapse() {
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (`.asl` layer style libraries read and written, #1051; JPEG Options on File › Save, Save As and Save a Copy, FILE-215-19 partial (no Matte, progressive Scans or size preview); 32-bit Hue/Saturation unclipped and Colorize as Photoshop does them, Photoshop-oracle floor 190/258, #5781; JPEG Options on Save As and Save a Copy, FILE-215-19; TIFF compression for scripted saves, `tiffCompression`, #5301; the Nikon D4 white level follows the sample depth (12 and 14 bit), #5978; Apply Layer Comp action mapping, #7101; Cineon write: 10-bit gray/RGB; animated WebP frames load into video layers, #1198; Photoshop mixed-order TIFF type geometry, #4758; DDS textures save: uncompressed or BC1–BC3, optional mip chain, DDS Options in Save As (#1327); PSD smart filters: 53 filters (39 more: Pinch, Polar Coordinates, Ripple, Spherize, Twirl, Wave, ZigZag, Invert, Blur, Surface Blur, Smart Sharpen, Diffuse, Wind, Offset, Mezzotint, Clouds, Difference Clouds, Fibers, Lens Flare and others) plus Hue/Saturation and Perspective Warp mapped both ways, #6296, #6298, #6300, #6302, #6304, #6314, #6316, #6267, #6268, FILE-215-2; large-file fixtures: a 20 MP layered PSD, a 150-layer layout, a 2 GB PSD and a 4.4 GB PSB generated at test time and round-tripped, FILE-216-2 done, #5107; earlier: smart objects keep Photoshop's Cylinder warp through re-renders and saves, #4112; PNG `sTER` stereo pairing kept on save, #6063; PDF write one flattened page via Save As / Save a Copy / CLI, #3401; CMYK Invert, Posterize and Threshold on the inks; smart filter masks on PSD open and save; Photoshop-oracle floor 188/258; lossy DNG, compression 34892, decodes with its levels and LinearizationTable applied to the stored 8-bit samples, FILE-215-11; CMYK Hue/Saturation and Color Balance keep the black ink; CMYK Posterize on the stored values, psd-tools oracle floor 242/309; CMYK adjustment layers composite on the document's inks; Photoshop-oracle floor 165/258; CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; modern Brightness/Contrast's brightness curve; Photoshop-oracle floor 151/258; Lab documents composite on their stored L, a, b; Photoshop-oracle floor 152/258; Levels on whole levels at every step with Photoshop's shadow toe; Photoshop-oracle floor 146/258, psd-tools 238/309; 32-bit adjustments unclipped; Photoshop-oracle floor 149/258; mask feathers blur with their exact variance; Threshold splits on Photoshop's luminosity; a pass-through clipping base with an adjustment renders isolated; psd-tools oracle floor 240/309; 16-bit Posterize as Photoshop bins it; psd-tools oracle floor 241/309; Gradient Map luminosity; Photoshop-oracle floor 147/258; PSD: `Mt16`/`Mt32` merged transparency read; palette PNGs open in Indexed Color mode, #4804; 16-bit Lab Invert; Photoshop-oracle floor 145/258; `.atn` import maps 112 events; DDS textures open, #1327; DPX read and write, #1049; #2465 fixed: CS2 placed layers with only a `PlLd` block open their contents; Photoshop Actions `.atn` read and write; `.shc` contour files read and write; earlier the same day: indexed BMP palette mode; Save keeps a JPEG's quality and a WebP's encoding) · **Target:** Adobe Photoshop 2026
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (`.atn`: a recorded Save As imports with its format and file (`file.saveAs`, `file.saveACopy` for As a Copy) instead of as a plain Save that wrote over the played file, #7102; folder destinations stop playback until #5430; `.asl` layer style libraries read and written, #1051; JPEG Options on File › Save, Save As and Save a Copy, FILE-215-19 partial (no Matte, progressive Scans or size preview); 32-bit Hue/Saturation unclipped and Colorize as Photoshop does them, Photoshop-oracle floor 190/258, #5781; JPEG Options on Save As and Save a Copy, FILE-215-19; TIFF compression for scripted saves, `tiffCompression`, #5301; the Nikon D4 white level follows the sample depth (12 and 14 bit), #5978; Apply Layer Comp action mapping, #7101; Cineon write: 10-bit gray/RGB; animated WebP frames load into video layers, #1198; Photoshop mixed-order TIFF type geometry, #4758; DDS textures save: uncompressed or BC1–BC3, optional mip chain, DDS Options in Save As (#1327); PSD smart filters: 53 filters (39 more: Pinch, Polar Coordinates, Ripple, Spherize, Twirl, Wave, ZigZag, Invert, Blur, Surface Blur, Smart Sharpen, Diffuse, Wind, Offset, Mezzotint, Clouds, Difference Clouds, Fibers, Lens Flare and others) plus Hue/Saturation and Perspective Warp mapped both ways, #6296, #6298, #6300, #6302, #6304, #6314, #6316, #6267, #6268, FILE-215-2; large-file fixtures: a 20 MP layered PSD, a 150-layer layout, a 2 GB PSD and a 4.4 GB PSB generated at test time and round-tripped, FILE-216-2 done, #5107; earlier: smart objects keep Photoshop's Cylinder warp through re-renders and saves, #4112; PNG `sTER` stereo pairing kept on save, #6063; PDF write one flattened page via Save As / Save a Copy / CLI, #3401; CMYK Invert, Posterize and Threshold on the inks; smart filter masks on PSD open and save; Photoshop-oracle floor 188/258; lossy DNG, compression 34892, decodes with its levels and LinearizationTable applied to the stored 8-bit samples, FILE-215-11; CMYK Hue/Saturation and Color Balance keep the black ink; CMYK Posterize on the stored values, psd-tools oracle floor 242/309; CMYK adjustment layers composite on the document's inks; Photoshop-oracle floor 165/258; CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; modern Brightness/Contrast's brightness curve; Photoshop-oracle floor 151/258; Lab documents composite on their stored L, a, b; Photoshop-oracle floor 152/258; Levels on whole levels at every step with Photoshop's shadow toe; Photoshop-oracle floor 146/258, psd-tools 238/309; 32-bit adjustments unclipped; Photoshop-oracle floor 149/258; mask feathers blur with their exact variance; Threshold splits on Photoshop's luminosity; a pass-through clipping base with an adjustment renders isolated; psd-tools oracle floor 240/309; 16-bit Posterize as Photoshop bins it; psd-tools oracle floor 241/309; Gradient Map luminosity; Photoshop-oracle floor 147/258; PSD: `Mt16`/`Mt32` merged transparency read; palette PNGs open in Indexed Color mode, #4804; 16-bit Lab Invert; Photoshop-oracle floor 145/258; `.atn` import maps 112 events; DDS textures open, #1327; DPX read and write, #1049; #2465 fixed: CS2 placed layers with only a `PlLd` block open their contents; Photoshop Actions `.atn` read and write; `.shc` contour files read and write; earlier the same day: indexed BMP palette mode; Save keeps a JPEG's quality and a WebP's encoding) · **Target:** Adobe Photoshop 2026
|
||||
|
||||
Every format Photoshop 2026 (27.11.0) reads or writes, with PhotoCraft's support, fidelity and
|
||||
tests. Photoshop's list comes from its `Info.plist` `CFBundleDocumentTypes`, the PiPL read/write
|
||||
@@ -131,7 +131,7 @@ remaining gaps in #1606 stay tracked with per-sample corpus ceilings.
|
||||
| LUTs `.cube` / `.3dl` / `.look` | R/W (Export Color Lookup) | R/W / R / R | `cms/src/lutfile.rs` |
|
||||
| Keyboard shortcuts `.kys` | R/W | R/W | `engine/src/kys.rs` (`write`: Edit › Keyboard Shortcuts › Export…, rows carry our id; Photoshop reading our files is unverified) |
|
||||
| ICC profiles | R | R | Own CMS |
|
||||
| Actions `.atn` | R/W | R/W | `psd/src/atn.rs` (version 16 sets, exact round trip), `engine/src/atn_cmds.rs` (Photoshop events ↔ commands, including Apply Layer Comp by name, #7101; other steps kept verbatim). Panel menu, CLI `batch --actions set.atn`, `actions.load`/`actions.save`. Photoshop 5-era version 12 files unsupported (AUTO-219-2) |
|
||||
| Actions `.atn` | R/W | R/W | `psd/src/atn.rs` (version 16 sets, exact round trip), `engine/src/atn_cmds.rs` (Photoshop events ↔ commands, including Apply Layer Comp by name, #7101; other steps kept verbatim; Save As maps to `file.saveAs`/`file.saveACopy` when its destination is a file, #7102). Panel menu, CLI `batch --actions set.atn`, `actions.load`/`actions.save`. Photoshop 5-era version 12 files unsupported (AUTO-219-2) |
|
||||
| Styles `.asl` | R/W | R/W | `psd/src/asl.rs`, `io/src/asl_map.rs`; Styles panel Import/Export Styles…, `style.presets.importAsl`/`exportAsl` (#1051) |
|
||||
| Contours `.shc` | R/W | R/W | `psd/src/shc.rs`; Contour Editor Load…/Save… (FILE-215-15). No user contour library; not yet checked against a Photoshop-written file |
|
||||
| Custom shapes `.csh`, tool presets `.tpl` | R/W | no | |
|
||||
@@ -145,6 +145,7 @@ remaining gaps in #1606 stay tracked with per-sample corpus ceilings.
|
||||
| 2026-10-22 | minor | 32-bit Hue/Saturation as Photoshop does it: the raised saturation isn't clipped (saturated colours leave 0..1), the chroma is scaled around the lightness by Photoshop's saturation factor (measured at every whole percent), so colours at or above lightness 1 keep their hue, and Colorize builds its colour with the saturation times 1 − \|Lightness\| (8 and 16 bits unchanged): photoshop rgb32 hue-saturation and hue-saturation-colorize pass; Photoshop-oracle floor 188 → 190 of 258 (#5781) |
|
||||
| 2026-10-19 | minor | JPEG Options on File › Save, Save As and Save a Copy (FILE-215-19 partial: no Matte, progressive Scans or size preview) |
|
||||
| 2026-10-18 | minor | Scripted TIFF saves take `tiffCompression` (none, LZW, ZIP), and the CLI `--tiff-compression` (#5301) |
|
||||
| 2026-10-17 | minor | `.atn`: a recorded Save As imports with its format and file (`file.saveAs`, `file.saveACopy` for As a Copy) instead of as a plain Save that wrote over the played file, #7102; folder destinations stop playback until #5430 |
|
||||
| 2026-10-17 | minor | `.atn`: Apply Layer Comp by name maps to `layerComp.apply` and exports as `applyComp` (#7101) |
|
||||
| 2026-10-15 | minor | Cineon write: Save As `.cin` stores 10-bit log gray/RGB frames (Cineon 4.5 header, three samples to a 32-bit word); a Cineon frame saved back keeps its code values |
|
||||
| 2026-10-14 | minor | 32-bit Hue/Saturation as Photoshop does it: the raised saturation isn't clipped (saturated colours leave 0..1) and the slider is cut to whole 8-bit levels, and Colorize builds its colour with the saturation times 1 − \|Lightness\| (8 and 16 bits unchanged): photoshop rgb32 hue-saturation and hue-saturation-colorize pass |
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
# Where PhotoCraft falls short of Photoshop
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (adjustment clipping stays visible in compact Properties, #5231; G9: Color panel modes, ramp and copy commands, the same in every theme, UI-217-17, #6000; G17: Help › Check for Updates and an opt-out check once per launch, DIST-220-1 partial, #5485; G10: `.asl` style libraries landed, #1051; G8: area type (TYPE-218-10); G1: smart objects keep Photoshop's Cylinder warp, #4112; G8: paragraph direction buttons in the Paragraph panel and options bar, TYPE-218-2 partial; G1: 32-bit Hue/Saturation and Colorize as Photoshop does them, Photoshop-oracle floor 190/258, #5781; G11: compound layer properties export as separate Photoshop Set events, rename last, #8304; G11: Batch and droplets take the result at the action's Save step, so actions ending in Save As and Close work; G11: `.atn` steps that switch to another open document import, play and save back, #7103; G14: the Nikon D4 white level follows the sample depth, so 12-bit D4 NEFs no longer open 2.6 stops dark, #5978; G14: a Nikon NEF's Adobe RGB preview is read as Adobe RGB (maker-note ColorSpace 0x001E), #5987; Apply Layer Comp action mapping, #7101; G10: JPEG Options dialog landed, FILE-215-19 partial (no Matte, progressive Scans or size preview); G10: Cineon write; G10: DPX read and write, #1049, Cineon and Amiga IFF read; Classical Object Finder improvement; learned segmentation remains open; G4: named, stateful section locks and visible keyboard focus in Brush Settings; G4: independent Brush Pose tilt-axis overrides, #3825 partial; G4: Transfer Mixer-only controls, #3825 partial; G9: Preferences › File Handling › Save in Background takes effect, #6207, 24 of 169 preferences do nothing; G7: Content-Aware Move previews every selection the command takes, coarse then exact, TOOL-213-4 done, #6680; G4: Pattern Stamp and Background Eraser draw live, TOOL-213-7; G9: Animated Zoom pref read: holding the Zoom tool zooms continuously, #6210, prefs that do nothing 25 of 166; G2: a cancelled 24 MP Gaussian Blur job stops in at most 44 ms, P25 under its 200 ms budget in local runs, baseline not re-run, #6205; G1: generated large-file fixtures, a 4.38 GB PSB opens on Windows through io, FILE-216-2 done, #5107; G9: fill and adjustment layers copy and paste whole, #2344; G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; G1: modern Brightness/Contrast's brightness curve matches Photoshop, Photoshop-oracle floor 151/258; G1: Lab documents composite on their stored L, a, b, Photoshop-oracle floor 152/258; G1: Levels on whole levels at every step, Photoshop-oracle floor 146/258, psd-tools 238/309; 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-22 · **Change:** minor (G11: a recorded Save As imports with its format and file (`file.saveAs`, `file.saveACopy` for As a Copy) instead of as a plain Save that wrote over the played file, #7102; folder destinations stop playback until #5430; adjustment clipping stays visible in compact Properties, #5231; G9: Color panel modes, ramp and copy commands, the same in every theme, UI-217-17, #6000; G17: Help › Check for Updates and an opt-out check once per launch, DIST-220-1 partial, #5485; G10: `.asl` style libraries landed, #1051; G8: area type (TYPE-218-10); G1: smart objects keep Photoshop's Cylinder warp, #4112; G8: paragraph direction buttons in the Paragraph panel and options bar, TYPE-218-2 partial; G1: 32-bit Hue/Saturation and Colorize as Photoshop does them, Photoshop-oracle floor 190/258, #5781; G11: compound layer properties export as separate Photoshop Set events, rename last, #8304; G11: Batch and droplets take the result at the action's Save step, so actions ending in Save As and Close work; G11: `.atn` steps that switch to another open document import, play and save back, #7103; G14: the Nikon D4 white level follows the sample depth, so 12-bit D4 NEFs no longer open 2.6 stops dark, #5978; G14: a Nikon NEF's Adobe RGB preview is read as Adobe RGB (maker-note ColorSpace 0x001E), #5987; Apply Layer Comp action mapping, #7101; G10: JPEG Options dialog landed, FILE-215-19 partial (no Matte, progressive Scans or size preview); G10: Cineon write; G10: DPX read and write, #1049, Cineon and Amiga IFF read; Classical Object Finder improvement; learned segmentation remains open; G4: named, stateful section locks and visible keyboard focus in Brush Settings; G4: independent Brush Pose tilt-axis overrides, #3825 partial; G4: Transfer Mixer-only controls, #3825 partial; G9: Preferences › File Handling › Save in Background takes effect, #6207, 24 of 169 preferences do nothing; G7: Content-Aware Move previews every selection the command takes, coarse then exact, TOOL-213-4 done, #6680; G4: Pattern Stamp and Background Eraser draw live, TOOL-213-7; G9: Animated Zoom pref read: holding the Zoom tool zooms continuously, #6210, prefs that do nothing 25 of 166; G2: a cancelled 24 MP Gaussian Blur job stops in at most 44 ms, P25 under its 200 ms budget in local runs, baseline not re-run, #6205; G1: generated large-file fixtures, a 4.38 GB PSB opens on Windows through io, FILE-216-2 done, #5107; G9: fill and adjustment layers copy and paste whole, #2344; G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; G1: modern Brightness/Contrast's brightness curve matches Photoshop, Photoshop-oracle floor 151/258; G1: Lab documents composite on their stored L, a, b, Photoshop-oracle floor 152/258; G1: Levels on whole levels at every step, Photoshop-oracle floor 146/258, psd-tools 238/309; 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
|
||||
@@ -130,6 +130,11 @@ what an agent can't finish alone.
|
||||
play and save back in the same shape, so actions that copy from one open document and paste into
|
||||
another no longer stop at the switch (#7103). An offset past the first or last open document is
|
||||
refused, not wrapped.
|
||||
- **Done 2026-10-17:** a recorded Save As imports with its format and destination file
|
||||
(`file.saveAs`, or `file.saveACopy` for As a Copy) instead of as a plain Save, which wrote
|
||||
over the file the action was played on (#7102). A folder destination, an unmapped format or a
|
||||
mismatched extension stays a kept Photoshop step, so playback stops before writing; folder
|
||||
saves wait for #5430.
|
||||
- **Done 2026-10-12:** Load/Save Actions `.atn` in the panel menu, `batch --actions set.atn`
|
||||
and `actions.load`/`actions.save`; 112 Photoshop events map to commands both ways (Transform,
|
||||
Apply Image, Save for Web, smart-object edits, Gradient Map, Quick Mask, polygon selections,
|
||||
@@ -361,6 +366,7 @@ what an agent can't finish alone.
|
||||
| 2026-10-22 | minor | G8: Auto / Left-to-right / Right-to-left paragraph direction buttons in the Paragraph panel, Properties and the Type options bar (#6793); TYPE-218-2 partial (the visual RTL caret is still missing, and PSD doesn't store the direction yet) |
|
||||
| 2026-10-19 | minor | G10: JPEG Options dialog landed (FILE-215-19 partial: no Matte, progressive Scans or size preview); no readiness percentage or estimate changed |
|
||||
| 2026-10-19 | minor | G11: `.atn` steps that select another open document by offset import as `document.activate {"offset"}`, play and save back (#7103); estimate unchanged |
|
||||
| 2026-10-19 | minor | G11: a recorded Save As imports with its format and file (`file.saveAs`, `file.saveACopy` for As a Copy) instead of as a plain Save that wrote over the played file, #7102; folder destinations stop playback until #5430 |
|
||||
| 2026-10-18 | minor | G13: Russian gained the 1,229 dynamic UI labels other catalogs already had (filter and adjustment parameters, Camera Raw, lighting, History/Layers/paths menus, metadata, System Info, status templates, dialogs); engine messages and native review remain open |
|
||||
| 2026-10-17 | minor | Classical Object Finder improvement; learned segmentation remains open |
|
||||
| 2026-10-15 | minor | G10: Cineon write landed (10-bit log gray/RGB, Save As `.cin`) |
|
||||
|
||||
Reference in New Issue
Block a user