mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-12 05:02:51 +00:00
Actions: map Photoshop's Distort filter steps (#10605)
Pinch, Polar Coordinates, Spherize, Twirl, Wave and ZigZag steps load as filter.distort.*, play, and save back with every key in recorded order. Out-of-range values are clamped as Photoshop clamps them and missing keys take Photoshop's defaults (Wave's undefined areas are Repeat Edge Pixels). A Wave scaled below 100 % stays a Photoshop step, as the command has no scale. Shear is still kept verbatim: its curve points need a step recorded through its dialog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f984bffb3
commit
a03120e8b4
5 files changed
+329
-1
No files matched your search
@@ -32,6 +32,7 @@ mod descriptors;
|
||||
mod events;
|
||||
mod fields;
|
||||
mod files;
|
||||
mod steps_distort;
|
||||
mod steps_doc;
|
||||
mod steps_image;
|
||||
mod steps_layer;
|
||||
|
||||
@@ -2,6 +2,7 @@ const DIFFUSE_MODE: &[(&str, &str)] = &[("Nrml", "normal"), ("DrkO", "darkenOnly
|
||||
use super::adjustments::{adjustment_by_class, adjustment_steps, levels_from, levels_to, shadows_highlights_from, shadows_highlights_to};
|
||||
use super::descriptors::{desc, get_bool, get_ref, jbool, layer_ref, layer_target, ref_class};
|
||||
use super::fields::{F, Kind, Mapped, fields_from, fields_to, none, step};
|
||||
use super::steps_distort::{PINCH, POLAR, SPHERIZE, TWIRL, ZIGZAG, keys_from, keys_to, wave_from, wave_to};
|
||||
use super::steps_doc::{
|
||||
align_from, align_to, canvas_size_from, canvas_size_to, close_from, close_to, conditional_from, conditional_to, convert_mode_from, convert_mode_to,
|
||||
crop_from, crop_to, export_from, export_to, image_size_from, image_size_to, merge_visible_from, merge_visible_to, open_from, open_to, paste_from, paste_to,
|
||||
@@ -247,6 +248,48 @@ pub(super) const EVENTS: &[Event] = &[
|
||||
"filter.other.offset",
|
||||
&[F("Hrzn", "horizontal", Kind::Long), F("Vrtc", "vertical", Kind::Long), F("Fl ", "undefinedAreas", Kind::Enum("FlMd", OFFSET_FILL))]
|
||||
),
|
||||
// Distort (#10605): every key in recorded order, Photoshop's limits and defaults.
|
||||
Event {
|
||||
code: "Pnch",
|
||||
name: "pinch",
|
||||
label: "Pinch",
|
||||
ids: &["filter.distort.pinch"],
|
||||
from: |d| keys_from(d, "filter.distort.pinch", PINCH),
|
||||
to: |_, p| keys_to(p, PINCH, &[("amount", json!(50))], &[]),
|
||||
},
|
||||
Event {
|
||||
code: "Plr ",
|
||||
name: "polar",
|
||||
label: "Polar Coordinates",
|
||||
ids: &["filter.distort.polarCoordinates"],
|
||||
from: |d| keys_from(d, "filter.distort.polarCoordinates", POLAR),
|
||||
to: |_, p| keys_to(p, POLAR, &[("mode", json!("rectangularToPolar"))], &[]),
|
||||
},
|
||||
Event {
|
||||
code: "Sphr",
|
||||
name: "spherize",
|
||||
label: "Spherize",
|
||||
ids: &["filter.distort.spherize"],
|
||||
from: |d| keys_from(d, "filter.distort.spherize", SPHERIZE),
|
||||
to: |_, p| keys_to(p, SPHERIZE, &[("amount", json!(100)), ("mode", json!("normal"))], &[]),
|
||||
},
|
||||
Event {
|
||||
code: "Twrl",
|
||||
name: "twirl",
|
||||
label: "Twirl",
|
||||
ids: &["filter.distort.twirl"],
|
||||
from: |d| keys_from(d, "filter.distort.twirl", TWIRL),
|
||||
to: |_, p| keys_to(p, TWIRL, &[("angle", json!(50))], &[]),
|
||||
},
|
||||
Event { code: "Wave", name: "wave", label: "Wave", ids: &["filter.distort.wave"], from: wave_from, to: wave_to },
|
||||
Event {
|
||||
code: "ZgZg",
|
||||
name: "zigZag",
|
||||
label: "ZigZag",
|
||||
ids: &["filter.distort.zigZag"],
|
||||
from: |d| keys_from(d, "filter.distort.zigZag", ZIGZAG),
|
||||
to: |_, p| keys_to(p, ZIGZAG, &[("amount", json!(10)), ("ridges", json!(5)), ("style", json!("pondRipples"))], &[]),
|
||||
},
|
||||
ev!("Rple", "ripple", "Ripple", "filter.distort.ripple", &[F("Amnt", "amount", Kind::ExactLong), F("RplS", "size", Kind::Enum("RplS", RIPPLE_SIZE))]),
|
||||
ev!("Crst", "crystallize", "Crystallize", "filter.pixelate.crystallize", &[F("ClSz", "cellSize", Kind::ExactLong)]),
|
||||
ev!("Dfs ", "diffuse", "Diffuse", "filter.stylize.diffuse", &[F("Md ", "mode", Kind::Enum("DfsM", DIFFUSE_MODE)), F("FlRs", "seed", Kind::Long)]),
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
//! Photoshop's Distort filter steps (#10605): Pinch, Polar Coordinates, Spherize, Twirl, Wave and
|
||||
//! ZigZag, each key in recorded order with the limits and defaults Photoshop measures.
|
||||
|
||||
use super::descriptors::{desc, enm, get_enum, get_num, trimmed};
|
||||
use super::events::pixel_target;
|
||||
use super::fields::{Mapped, step};
|
||||
use super::tables::{lookup, lookup_back};
|
||||
use super::{D, Descriptor, Map, Value, json};
|
||||
|
||||
/// Whether every key of `d` is one of `keys` (codes compare without their space padding).
|
||||
fn only_keys(d: &Descriptor, keys: &[&str]) -> bool {
|
||||
d.items.iter().all(|(k, _)| keys.contains(&trimmed(k).as_str()))
|
||||
}
|
||||
|
||||
/// Whether `p` sets nothing but `allowed` (a `null` counts as unset).
|
||||
fn sets_only(p: &Value, allowed: &[&str]) -> bool {
|
||||
p.as_object().is_none_or(|m| m.iter().all(|(k, v)| v.is_null() || allowed.contains(&k.as_str())))
|
||||
}
|
||||
|
||||
/// One key of a Distort filter's step, in recorded order.
|
||||
pub(super) enum Key {
|
||||
/// An integer clamped to `min..=max` on import, as Photoshop clamps it; `default` when the
|
||||
/// step leaves it out.
|
||||
Int { key: &'static str, param: &'static str, min: i32, max: i32, default: i32 },
|
||||
/// An enumeration (`ty`, Photoshop ↔ PhotoCraft values); `default` is Photoshop's value when
|
||||
/// the step leaves it out. An unknown value keeps the step.
|
||||
Enum { key: &'static str, param: &'static str, ty: &'static str, table: &'static [(&'static str, &'static str)], default: &'static str },
|
||||
}
|
||||
|
||||
impl Key {
|
||||
fn param(&self) -> &'static str {
|
||||
match self {
|
||||
Key::Int { param, .. } | Key::Enum { param, .. } => param,
|
||||
}
|
||||
}
|
||||
|
||||
fn key(&self) -> &'static str {
|
||||
match self {
|
||||
Key::Int { key, .. } | Key::Enum { key, .. } => key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A step with exactly `keys` (any missing): each value clamped or looked up, Photoshop's
|
||||
/// default for what it leaves out. Any other key keeps the step.
|
||||
pub(super) fn keys_from(d: &Descriptor, id: &str, keys: &[Key]) -> Mapped {
|
||||
keys_map(d, keys, &[]).map_or(Mapped::Unsupported { harmless: false }, |m| step(id, Value::Object(m)))
|
||||
}
|
||||
|
||||
fn keys_map(d: &Descriptor, keys: &[Key], extra: &[&'static str]) -> Option<Map<String, Value>> {
|
||||
let known: Vec<&str> = keys.iter().map(Key::key).chain(extra.iter().copied()).map(str::trim_end).collect();
|
||||
if !only_keys(d, &known) {
|
||||
return None;
|
||||
}
|
||||
let mut m = Map::new();
|
||||
for k in keys {
|
||||
let v = match k {
|
||||
Key::Int { key, min, max, default, .. } => {
|
||||
Value::from(get_num(d, key).map_or(*default, |n| n.round().clamp(f64::from(*min), f64::from(*max)) as i32))
|
||||
}
|
||||
Key::Enum { key, table, default, .. } => Value::from(lookup(table, &get_enum(d, key).unwrap_or_else(|| (*default).to_string()))?),
|
||||
};
|
||||
m.insert(k.param().into(), v);
|
||||
}
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// Every key in recorded order, the command's default for what the step leaves out
|
||||
/// (`ours`, by param). An integer key refuses a fractional value, which would replay rounded.
|
||||
pub(super) fn keys_to(p: &Value, keys: &[Key], ours: &[(&str, Value)], extra: &[&'static str]) -> Option<Descriptor> {
|
||||
let params: Vec<&str> = keys.iter().map(Key::param).chain(extra.iter().copied()).chain(["target"]).collect();
|
||||
if !pixel_target(p) || !sets_only(p, ¶ms) {
|
||||
return None;
|
||||
}
|
||||
let value = |param: &str| match p.get(param) {
|
||||
None | Some(Value::Null) => ours.iter().find(|(k, _)| *k == param).map(|(_, v)| v.clone()),
|
||||
Some(v) => Some(v.clone()),
|
||||
};
|
||||
let mut d = desc("null");
|
||||
for k in keys {
|
||||
let v = value(k.param())?;
|
||||
d = match k {
|
||||
Key::Int { key, .. } => {
|
||||
let n = v.as_f64()?;
|
||||
if n.fract() != 0.0 || !(f64::from(i32::MIN)..=f64::from(i32::MAX)).contains(&n) {
|
||||
return None;
|
||||
}
|
||||
d.with(key, D::Integer(n as i32))
|
||||
}
|
||||
Key::Enum { key, ty, table, .. } => d.with(key, enm(ty, lookup_back(table, v.as_str()?)?)),
|
||||
};
|
||||
}
|
||||
Some(d)
|
||||
}
|
||||
|
||||
pub(super) const PINCH: &[Key] = &[Key::Int { key: "Amnt", param: "amount", min: -100, max: 100, default: 50 }];
|
||||
|
||||
pub(super) const TWIRL: &[Key] = &[Key::Int { key: "Angl", param: "angle", min: -999, max: 999, default: 50 }];
|
||||
|
||||
pub(super) const POLAR: &[Key] =
|
||||
&[Key::Enum { key: "Cnvr", param: "mode", ty: "Cnvr", table: &[("RctP", "rectangularToPolar"), ("PlrR", "polarToRectangular")], default: "RctP" }];
|
||||
|
||||
pub(super) const SPHERIZE: &[Key] = &[
|
||||
Key::Int { key: "Amnt", param: "amount", min: -100, max: 100, default: 100 },
|
||||
Key::Enum { key: "SphM", param: "mode", ty: "SphM", table: &[("Nrml", "normal"), ("HrzO", "horizontalOnly"), ("VrtO", "verticalOnly")], default: "Nrml" },
|
||||
];
|
||||
|
||||
pub(super) const ZIGZAG: &[Key] = &[
|
||||
Key::Int { key: "Amnt", param: "amount", min: -100, max: 100, default: 10 },
|
||||
Key::Int { key: "NmbR", param: "ridges", min: 0, max: 20, default: 5 },
|
||||
Key::Enum {
|
||||
key: "ZZTy",
|
||||
param: "style",
|
||||
ty: "ZZTy",
|
||||
table: &[("PndR", "pondRipples"), ("OtFr", "outFromCenter"), ("ArnC", "aroundCenter")],
|
||||
default: "PndR",
|
||||
},
|
||||
];
|
||||
|
||||
/// Wave's keys before its scale, undefined areas and seed (handled in `wave_from` / `wave_to`).
|
||||
const WAVE: &[Key] = &[
|
||||
Key::Enum { key: "Wvtp", param: "type", ty: "Wvtp", table: &[("WvSn", "sine"), ("WvSq", "square"), ("WvTr", "triangle")], default: "WvSn" },
|
||||
Key::Int { key: "NmbG", param: "generators", min: 1, max: 999, default: 5 },
|
||||
Key::Int { key: "WLMn", param: "wavelengthMin", min: 1, max: 999, default: 10 },
|
||||
Key::Int { key: "WLMx", param: "wavelengthMax", min: 1, max: 999, default: 120 },
|
||||
Key::Int { key: "AmMn", param: "amplitudeMin", min: 1, max: 999, default: 5 },
|
||||
Key::Int { key: "AmMx", param: "amplitudeMax", min: 1, max: 999, default: 35 },
|
||||
];
|
||||
|
||||
const WAVE_UNDEFINED: &[(&str, &str)] = &[("RptE", "repeat"), ("WrpA", "wrap")];
|
||||
|
||||
/// Wave (`Wave`). The command has no horizontal or vertical scale, so a step that scales the
|
||||
/// waves below 100 % keeps the Photoshop step. Missing undefined areas are Photoshop's Repeat
|
||||
/// Edge Pixels (the command's own default is wrap); `RndS` keeps its 32 bits as the seed.
|
||||
pub(super) fn wave_from(d: &Descriptor) -> Mapped {
|
||||
let unsupported = Mapped::Unsupported { harmless: false };
|
||||
let Some(mut m) = keys_map(d, WAVE, &["SclH", "SclV", "UndA", "RndS"]) else { return unsupported };
|
||||
if ["SclH", "SclV"].iter().any(|k| get_num(d, k).is_some_and(|n| n.round().clamp(1.0, 100.0) != 100.0)) {
|
||||
return unsupported;
|
||||
}
|
||||
let Some(undefined) = lookup(WAVE_UNDEFINED, &get_enum(d, "UndA").unwrap_or_else(|| "RptE".into())) else { return unsupported };
|
||||
m.insert("undefinedAreas".into(), undefined.into());
|
||||
if let Some(seed) = get_num(d, "RndS") {
|
||||
m.insert("seed".into(), Value::from(seed as i32 as u32));
|
||||
}
|
||||
step("filter.distort.wave", Value::Object(m))
|
||||
}
|
||||
|
||||
pub(super) fn wave_to(_: &str, p: &Value) -> Option<Descriptor> {
|
||||
let ours = [
|
||||
("type", json!("sine")),
|
||||
("generators", json!(5)),
|
||||
("wavelengthMin", json!(10)),
|
||||
("wavelengthMax", json!(120)),
|
||||
("amplitudeMin", json!(5)),
|
||||
("amplitudeMax", json!(35)),
|
||||
];
|
||||
let d = keys_to(p, WAVE, &ours, &["undefinedAreas", "seed"])?;
|
||||
let undefined = match p.get("undefinedAreas") {
|
||||
None | Some(Value::Null) => "WrpA",
|
||||
Some(v) => lookup_back(WAVE_UNDEFINED, v.as_str()?)?,
|
||||
};
|
||||
let seed = match p.get("seed") {
|
||||
None | Some(Value::Null) => 0,
|
||||
Some(v) => u32::try_from(v.as_u64()?).ok()? as i32,
|
||||
};
|
||||
Some(d.with("SclH", D::Integer(100)).with("SclV", D::Integer(100)).with("UndA", enm("UndA", undefined)).with("RndS", D::Integer(seed)))
|
||||
}
|
||||
@@ -485,6 +485,16 @@ fn every_mapped_command_exports_and_imports_back() {
|
||||
("filter.blur.smartBlur", json!({"radius": 2.5, "threshold": 40, "quality": "medium", "mode": "overlayEdge"})),
|
||||
("filter.other.offset", json!({"horizontal": 10, "vertical": -5, "undefinedAreas": "wrap"})),
|
||||
("filter.blur.blur", json!({})),
|
||||
("filter.distort.pinch", json!({"amount": -30})),
|
||||
("filter.distort.polarCoordinates", json!({"mode": "polarToRectangular"})),
|
||||
("filter.distort.spherize", json!({"amount": -50, "mode": "verticalOnly"})),
|
||||
("filter.distort.twirl", json!({"angle": -200})),
|
||||
(
|
||||
"filter.distort.wave",
|
||||
json!({"type": "square", "generators": 3, "wavelengthMin": 20, "wavelengthMax": 200, "amplitudeMin": 4, "amplitudeMax": 40,
|
||||
"undefinedAreas": "wrap", "seed": 4_000_000_000_u32}),
|
||||
),
|
||||
("filter.distort.zigZag", json!({"amount": -20, "ridges": 12, "style": "aroundCenter"})),
|
||||
(
|
||||
"image.adjustments.levels",
|
||||
json!({"inBlack": 5, "inWhite": 250, "gamma": 0.9, "outBlack": 0, "outWhite": 255, "red": {"inBlack": 2, "inWhite": 255, "gamma": 1, "outBlack": 0, "outWhite": 255}}),
|
||||
@@ -3175,3 +3185,108 @@ fn long_names_are_clipped_instead_of_copied_into_every_diagnostic() {
|
||||
assert_eq!(imp.unsupported.len(), 50);
|
||||
assert!(imp.unsupported.iter().all(|u| u.action.chars().count() == MAX_NAME_CHARS));
|
||||
}
|
||||
|
||||
/// The step `d` of Photoshop event `code`, as PhotoCraft loads it.
|
||||
fn distort(code: &str, d: Descriptor) -> Mapped {
|
||||
let event = AtnEvent::Code(code.as_bytes().try_into().unwrap());
|
||||
(events::event_for(&event).unwrap_or_else(|| panic!("{code} is unknown")).from)(&d)
|
||||
}
|
||||
|
||||
fn one_step(m: Mapped) -> (String, Value) {
|
||||
match m {
|
||||
Mapped::Steps(mut s) if s.len() == 1 => s.remove(0),
|
||||
_ => panic!("not one step"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Photoshop's Distort filter steps load with its defaults for missing keys and its clamping for
|
||||
/// out-of-range values, play, and save back with every key in recorded order (#10605).
|
||||
#[test]
|
||||
fn distort_filter_steps_load_clamp_play_and_save_back() {
|
||||
// Photoshop's defaults, as its steps record them.
|
||||
let wave = desc("null")
|
||||
.with("Wvtp", enm("Wvtp", "WvSn"))
|
||||
.with("NmbG", D::Integer(5))
|
||||
.with("WLMn", D::Integer(10))
|
||||
.with("WLMx", D::Integer(120))
|
||||
.with("AmMn", D::Integer(5))
|
||||
.with("AmMx", D::Integer(35))
|
||||
.with("SclH", D::Integer(100))
|
||||
.with("SclV", D::Integer(100))
|
||||
.with("UndA", enm("UndA", "RptE"))
|
||||
.with("RndS", D::Integer(-7));
|
||||
let zigzag = desc("null").with("Amnt", D::Integer(10)).with("NmbR", D::Integer(5)).with("ZZTy", enm("ZZTy", "PndR"));
|
||||
let cases = [
|
||||
("Pnch", desc("null").with("Amnt", D::Integer(50)), "filter.distort.pinch", json!({"amount": 50})),
|
||||
("Plr ", desc("null").with("Cnvr", enm("Cnvr", "RctP")), "filter.distort.polarCoordinates", json!({"mode": "rectangularToPolar"})),
|
||||
(
|
||||
"Sphr",
|
||||
desc("null").with("Amnt", D::Integer(100)).with("SphM", enm("SphM", "Nrml")),
|
||||
"filter.distort.spherize",
|
||||
json!({"amount": 100, "mode": "normal"}),
|
||||
),
|
||||
("Twrl", desc("null").with("Angl", D::Integer(50)), "filter.distort.twirl", json!({"angle": 50})),
|
||||
(
|
||||
"Wave",
|
||||
wave,
|
||||
"filter.distort.wave",
|
||||
json!({"type": "sine", "generators": 5, "wavelengthMin": 10, "wavelengthMax": 120, "amplitudeMin": 5, "amplitudeMax": 35,
|
||||
"undefinedAreas": "repeat", "seed": 4_294_967_289_u32}),
|
||||
),
|
||||
("ZgZg", zigzag, "filter.distort.zigZag", json!({"amount": 10, "ridges": 5, "style": "pondRipples"})),
|
||||
];
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width": 24, "height": 24})).unwrap();
|
||||
s.execute("filter.render.clouds", json!({})).unwrap();
|
||||
for (code, d, id, params) in cases {
|
||||
let (got, p) = one_step(distort(code, d.clone()));
|
||||
assert_eq!((got.as_str(), &p), (id, ¶ms), "{code}");
|
||||
// Every key, in the order Photoshop records them.
|
||||
assert_eq!(export_item(id, &p, "S").unwrap().descriptor.unwrap().items, d.items, "{code}");
|
||||
s.actions.list.push(Action::new(code, vec![(id.to_string(), p)]));
|
||||
let played = s.execute("actions.play", json!({"action": code})).unwrap();
|
||||
assert_eq!(played["ran"], 1, "{code}: {played}");
|
||||
}
|
||||
// A step that leaves its keys out takes Photoshop's defaults: Wave's undefined areas are
|
||||
// Repeat Edge Pixels, where the command's own default is wrap.
|
||||
assert_eq!(one_step(distort("Wave", desc("null"))).1["undefinedAreas"], "repeat");
|
||||
assert_eq!(one_step(distort("Sphr", desc("null"))).1, json!({"amount": 100, "mode": "normal"}));
|
||||
// Out-of-range values are clamped, as Photoshop clamps them.
|
||||
assert_eq!(one_step(distort("Pnch", desc("null").with("Amnt", D::Integer(500)))).1, json!({"amount": 100}));
|
||||
assert_eq!(one_step(distort("Twrl", desc("null").with("Angl", D::Integer(-5000)))).1, json!({"angle": -999}));
|
||||
assert_eq!(one_step(distort("ZgZg", desc("null").with("NmbR", D::Integer(40)))).1["ridges"], 20);
|
||||
assert_eq!(one_step(distort("Wave", desc("null").with("NmbG", D::Integer(0)))).1["generators"], 1);
|
||||
}
|
||||
|
||||
/// What the commands cannot play keeps the Photoshop step: an unknown enumeration value, a Wave
|
||||
/// scaled below 100 % (the command has no scale), or a key the event does not have (#10605).
|
||||
#[test]
|
||||
fn distort_filter_steps_the_commands_cannot_play_are_kept() {
|
||||
let kept = |m: Mapped| matches!(m, Mapped::Unsupported { harmless: false });
|
||||
assert!(kept(distort("Sphr", desc("null").with("SphM", enm("SphM", "Dgnl")))));
|
||||
assert!(kept(distort("Plr ", desc("null").with("Cnvr", enm("Cnvr", "Sprl")))));
|
||||
assert!(kept(distort("Wave", desc("null").with("SclH", D::Integer(50)))));
|
||||
assert!(kept(distort("Wave", desc("null").with("UndA", enm("UndA", "Trns")))));
|
||||
assert!(kept(distort("Pnch", desc("null").with("Rds ", D::Integer(3)))));
|
||||
// A scale of 100 % (or above, which Photoshop clamps to 100) plays.
|
||||
assert!(!kept(distort("Wave", desc("null").with("SclH", D::Integer(100)).with("SclV", D::Integer(250)))));
|
||||
}
|
||||
|
||||
/// A setting the events cannot hold refuses the step instead of replaying differently: a
|
||||
/// fractional amount (the keys are integers), a mask target, a seed past 32 bits (#10605).
|
||||
#[test]
|
||||
fn distort_filter_steps_that_cannot_be_written_are_refused() {
|
||||
for (id, params) in [
|
||||
("filter.distort.pinch", json!({"amount": 12.5})),
|
||||
("filter.distort.twirl", json!({"angle": 10, "target": "mask"})),
|
||||
("filter.distort.zigZag", json!({"ridges": 2.5})),
|
||||
("filter.distort.wave", json!({"seed": 4_294_967_296_u64})),
|
||||
("filter.distort.wave", json!({"undefinedAreas": "transparent"})),
|
||||
("filter.distort.spherize", json!({"mode": "diagonal"})),
|
||||
] {
|
||||
assert!(export_item(id, ¶ms, "S").is_none(), "{id} {params} exported");
|
||||
}
|
||||
// Left out, a setting is written as the command replays it: Wave wraps.
|
||||
let wave = export_item("filter.distort.wave", &json!({}), "S").unwrap().descriptor.unwrap();
|
||||
assert_eq!(get_enum(&wave, "UndA").as_deref(), Some("WrpA"));
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-26 · **Change:** minor (TIFFs up to 300,000 px a side open, decoded a group of rows at a time into the layer, #8169; embedded AI/PDF/EPS save-back retains native format with a source-bound SVG display preview; PSD Filter Gallery: Halftone Pattern dot/circle/line settings survive PSD/PSB saves (#8940); Save for Web writes WebP, lossy or lossless, #6391; Photoshop's `MTrn` merged-transparency marker, from a layered TIFF saved with Save Transparency, marks the merged transparency, #8877; layered TIFF writes the TySh text version and descriptor in PSD big-endian order while the header, matrix, warp and bounds keep container order, #9098; layered TIFF writes Photoshop's 14-byte global layer mask, so Photoshop opens the layers instead of only the composite, #9095; legacy PSD point text placed from the layout ink box after font resolution, #6094; `.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; Amiga IFF write: 24-plane ILBM, Save As `.iff`; PSD stores spot channels as Photoshop does, ink 0 = solid, and numbers them in 1053, #7415 split; TIFF writes and reads alpha and spot channels as extra samples with Photoshop's channel resources, PSD output unchanged, FILE-215-7, FILE-215-17's TIFF part, #7415; Photoshop's `MTrn` merged-transparency marker, from a layered TIFF saved with Save Transparency, marks the merged transparency) · **Target:** Adobe Photoshop 2026
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-31 · **Change:** minor (`.atn`: Pinch, Polar Coordinates, Spherize, Twirl, Wave and ZigZag steps load, play and save back with Photoshop's limits and defaults, #10605; TIFFs up to 300,000 px a side open, decoded a group of rows at a time into the layer, #8169; embedded AI/PDF/EPS save-back retains native format with a source-bound SVG display preview; PSD Filter Gallery: Halftone Pattern dot/circle/line settings survive PSD/PSB saves (#8940); Save for Web writes WebP, lossy or lossless, #6391; Photoshop's `MTrn` merged-transparency marker, from a layered TIFF saved with Save Transparency, marks the merged transparency, #8877; layered TIFF writes the TySh text version and descriptor in PSD big-endian order while the header, matrix, warp and bounds keep container order, #9098; layered TIFF writes Photoshop's 14-byte global layer mask, so Photoshop opens the layers instead of only the composite, #9095; legacy PSD point text placed from the layout ink box after font resolution, #6094; `.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; Amiga IFF write: 24-plane ILBM, Save As `.iff`; PSD stores spot channels as Photoshop does, ink 0 = solid, and numbers them in 1053, #7415 split; TIFF writes and reads alpha and spot channels as extra samples with Photoshop's channel resources, PSD output unchanged, FILE-215-7, FILE-215-17's TIFF part, #7415; Photoshop's `MTrn` merged-transparency marker, from a layered TIFF saved with Save Transparency, marks the merged transparency) · **Target:** Adobe Photoshop 2026
|
||||
|
||||
**PSD Filter Gallery (#8940, 2026-10-23):** Halftone Pattern maps its size, contrast and dot/circle/line pattern as editable settings on import and export, with the reference application's descriptor keys (`HlSz`, `Cntr`, `ScrT`: `ScrD`/`ScrC`/`ScrL`). PSD/PSB round trips pass at 8/16/32 bits. This does not claim pixel equivalence for the existing renderer. Unknown or malformed settings remain verbatim on import.
|
||||
|
||||
@@ -148,6 +148,7 @@ Embedded SVG and PDF-compatible AI/PDF/EPS payloads can use an explicit VectorCr
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-31 | minor | `.atn`: Pinch (`Pnch`), Polar Coordinates (`Plr `), Spherize (`Sphr`), Twirl (`Twrl`), Wave (`Wave`) and ZigZag (`ZgZg`) map to `filter.distort.*` with every key in recorded order; out-of-range values clamped and missing keys given Photoshop's defaults on import; a Wave scaled below 100 % stays a Photoshop step (the command has no scale); Shear still kept verbatim (#10605) |
|
||||
| 2026-10-26 | minor | Embedded AI/PDF/EPS save-back retains native format and a separate source-bound display preview; original sources remain undoable. Native Adobe interoperability and standalone EPS/DCS remain unverified; no corpus floor or percentage changed. |
|
||||
| 2026-10-22 | minor | Embedded PSD vectors gain explicit bounded SVG editor/save-back; standalone EPS/DCS and native-format fidelity remain open. |
|
||||
| 2026-10-22 | minor | TIFF: flat pages up to 300,000 px a side open (was 2^30 px, about 32,768²): `codecs::TiffBands` decodes a group of rows at a time into the layer's tiles. The 117,504 × 22,464 px banner from #8456 opens in 3.3 s at 11.45 GB peak for a 10.56 GB layer; 30,000² 6.8 s / 6.43 GB → 1.1 s / 3.86 GB; 36 MP 220 → 52 ms. Layered TIFFs still decode whole (#8169) |
|
||||
|
||||
Reference in new issue
Block a user