Files
photocraft/crates/engine/tests/shortcut_sets.rs
T
4446ec6d65 PSD: write tiff blocks without copies, inflate merged ZIP once (#5221)
* Raw: measure the Nikon D90 image area (4288x2848 at (10, 10))

Aligning the decoded D90 sensor plane with the camera's own JPEG puts the
image at (10, 10) in the 4352x2868 sensor plane, giving the camera's
4288x2848 frame (10-pixel margins; the right border reads the camera's flat
255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the
camera's frame instead of the uncropped plane, and make a measured profile
leave a file that carries its own colour calibration (a DNG) untouched.

* Codecs: open Cineon (.cin) film frames read-only

Cineon is the 10-bit logarithmic film-scan format DPX grew from. Read it from
the same Kodak header family: the generic header (big-endian, orientation at
192, channel count at 193, the channel-1 bits/dimensions), one or three
channels, three 10-bit samples packed into each 32-bit word on a word
boundary. The 10-bit code values open as U16 (round(c x 65535 / 1023)); the
file's log black/white points are a display choice and are not applied. A
depth other than 10 bits or a channel count other than 1/3 is Unsupported;
truncation errors rather than panicking. Read-only (no Cineon writer), so
.cin joins the open filters and Cineon read closes part of #1049's G10 gap.

* PSD: write tiff blocks without copies, inflate merged ZIP once

- ImageSourceData::to_bytes writes the global mask and global blocks
  straight from the model with 4-byte alignment instead of cloning each
  block (data included) to set its padding. Same bytes.
- The tiff transcoder's nested regions and blocks write into the parent's
  buffer (taken and handed back, error or not) instead of building their
  own output and copying it up per nesting level. Bodies that fail or are
  not transcoded are truncated before being copied as stored or dropped.
  Same bytes.
- PsdFile::from_bytes_and_merged decodes the merged image with the parse
  when asked: a ZIP stream is inflated once (the decode's stream check is
  the parse's); the parse-only check runs only when the decode fails, to
  keep from_bytes' errors. PSD import uses it with merged_needed.
- PsdBuilder::composite documents that Gray/CMYK composites stay straight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keyboard shortcut sets: Set menu, New Set, Save, Delete, .kys Export and Summarize (K2)

Edit › Keyboard Shortcuts gets Photoshop's set management. A set is a saved copy of the
bindings (Preferences.shortcutSets, the one in use in shortcutSet; "" is PhotoCraft Defaults),
and the Set menu shows "(modified)" when the bindings differ from it. New Set…, Save and
Delete wait for OK like the rest of the dialog. Export… writes the dialog's bindings as a .kys
file (commands then tools, Photoshop's macOS key spelling), each row carrying our id as pc-id
so PhotoCraft imports its own sets back exactly, whatever the label; Summarize… writes
Photoshop's HTML list of every bound command by menu.

edit.keyboardShortcuts takes useSet (before the call's edits), saveSet, deleteSet, sets and
activeSet (after them), exportKys and summarize; every reply reports the sets, the set in use
and whether it is modified. Bad names, keys or counts are errors that change nothing.

* .kys import: tool keys and sets saved on Windows (K3)

Photoshop's <tool> rows now import: each matches our tool by name (our tool names are
Photoshop's) or by pc-id in sets PhotoCraft exported, and a letter other than the tool's own
becomes its override, while its own letter keeps the tool cycling with the others (#5036).

A set saved on Windows spells the primary modifier Ctrl where a Mac set writes Cmd and keeps
Control for the Control key. A set with no Cmd or Opt anywhere but some Ctrl is read as a
Windows set, its Ctrl as our Cmd: Ctrl+L is the Cmd+L default, not a Control-key override that
would also mean Control on a Mac. The import reports `windows`.

* cargo fmt the extension lists

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Vikram Bhaskaran <vikram.bhaskaran@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
2026-10-15 00:32:55 -04:00

61 lines
3.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Edit › Keyboard Shortcuts › Set through `edit.keyboardShortcuts`: save, switch, delete,
//! export to `.kys` and back, Summarize, and bad params as errors.
use photocraft_engine::Session;
use serde_json::{Value, json};
fn ks(s: &mut Session, p: Value) -> Value {
s.execute("edit.keyboardShortcuts", p).expect("edit.keyboardShortcuts")
}
#[test]
fn a_set_saves_switches_exports_and_imports_back() {
let mut s = Session::new();
let r = ks(&mut s, json!({}));
assert_eq!((r["activeSet"].as_str(), r["modified"].as_bool(), r["defaultsName"].as_str()), (Some(""), Some(false), Some("PhotoCraft Defaults")));
// Edit, then save the bindings as they end up after this call's edits.
let r = ks(&mut s, json!({"set": {"edit.copy": "F9"}, "alternates": {"edit.copy": []}, "saveSet": "Mine"}));
assert_eq!((r["activeSet"].as_str(), r["modified"].as_bool(), r["sets"].clone()), (Some("Mine"), Some(false), json!(["Mine"])));
// Export the set, go back to the defaults, then import the file: the bindings return.
let xml = ks(&mut s, json!({"exportKys": true}))["kys"].as_str().expect("kys").to_string();
assert!(xml.contains("KeyboardCustomization/Mine=Mine") && xml.contains("pc-id=\"edit.copy\"><shortcut>F9</shortcut></command>"), "{xml}");
let r = ks(&mut s, json!({"useSet": ""}));
assert_eq!((r["overrides"].clone(), r["activeSet"].as_str()), (json!({}), Some("")));
let r = ks(&mut s, json!({"reset": true, "importKys": xml}));
assert_eq!(r["overrides"]["edit.copy"], "F9");
assert_eq!(r["alternateOverrides"]["edit.copy"], json!([]), "Copy's F3 stays removed");
assert_eq!(r["modified"], true, "imported over the defaults");
// Switch back to the saved set; a later unsaved edit marks it modified.
ks(&mut s, json!({"useSet": "Mine"}));
let r = ks(&mut s, json!({"set": {"edit.cut": "F8"}}));
assert_eq!((r["activeSet"].as_str(), r["modified"].as_bool()), (Some("Mine"), Some(true)));
// Summarize lists what's bound, per platform.
let html = ks(&mut s, json!({"summarize": {"mac": false}}))["html"].as_str().expect("html").to_string();
assert!(html.contains("<title>Mine</title>") && html.contains("<td>Copy</td><td>F9</td>") && html.contains("<td>Cut</td><td>F8"), "{html}");
// Delete the set in use: the defaults' name, the bindings kept.
let r = ks(&mut s, json!({"deleteSet": "Mine"}));
assert_eq!((r["sets"].clone(), r["activeSet"].as_str(), r["overrides"]["edit.copy"].as_str()), (json!([]), Some(""), Some("F9")));
}
#[test]
fn bad_set_params_change_nothing() {
let mut s = Session::new();
ks(&mut s, json!({"saveSet": "Keep"}));
let before = s.prefs().clone();
for p in [
json!({"useSet": "Missing"}),
json!({"useSet": 7}),
json!({"saveSet": ""}),
json!({"saveSet": "PhotoCraft Defaults"}),
json!({"deleteSet": "Missing"}),
json!({"activeSet": "Missing"}),
json!({"sets": "nope"}),
json!({"sets": {"A": {"shortcuts": {"edit.copy": "Cmd+Shift+Ctrl"}}}}),
// A bad key elsewhere in the call keeps the set from being saved too.
json!({"set": {"edit.copy": "Cmd+Shift+Ctrl"}, "saveSet": "New"}),
] {
assert!(s.execute("edit.keyboardShortcuts", p.clone()).is_err(), "{p}");
assert_eq!(s.prefs(), &before, "{p} changed the preferences");
}
}