Place: wire Resize Image During Place into the implicit fit (#204) (#2538)

* feat(engine): Resize Image During Place drives the implicit Place fit (#204)

* Merge origin/main; regenerate docs/scorecard.md

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Reynold Hu 2026-10-11 05:53:25 +08:00 committed by GitHub
parent a27d717597
commit a3d2a6d486
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 36 additions and 12 deletions

View File

@ -400,9 +400,9 @@ fn open_as(s: &mut Session, p: &Value) -> Result<Value> {
/// History label of an embedded place.
pub const PLACE_EMBEDDED: &str = "Place Embedded";
/// Place a file's bytes as a smart object layer, centred and (when larger than the canvas)
/// scaled down to fit, like Photoshop's Place with "Resize Image During Place". `linked` makes it
/// a linked smart object that refers to that path instead of embedding the bytes.
/// Place a file's bytes as a smart object layer, centred, and (with Preferences ▸ General ▸
/// Resize Image During Place, on by default) scaled down to fit when larger than the canvas.
/// `linked` makes it a linked smart object that refers to that path instead of embedding the bytes.
pub fn place_bytes(s: &mut Session, name: &str, bytes: Vec<u8>, linked: Option<String>, p: &Value) -> Result<Value> {
let d = s.active().ok_or(EngineError::NoDocument)?;
let (cw, ch) = (d.doc.size.width as f64, d.doc.size.height as f64);
@ -417,9 +417,11 @@ pub fn place_bytes(s: &mut Session, name: &str, bytes: Vec<u8>, linked: Option<S
}
let src = imported.document;
let (w, h) = (src.size.width as f64, src.size.height as f64);
// The implicit `fit` is Preferences ▸ General ▸ Resize Image During Place (on by default).
let fit = p.get("fit").and_then(Value::as_bool).unwrap_or(s.prefs().general.resize_image_during_place);
let scale = match f64_param(p, "scale") {
Some(k) => (k / 100.0).max(1e-4),
None if p.get("fit").and_then(Value::as_bool) != Some(false) && (w > cw || h > ch) => (cw / w).min(ch / h),
None if fit && (w > cw || h > ch) => (cw / w).min(ch / h),
None => 1.0,
};
let center = match p.get("center").and_then(Value::as_array) {
@ -1302,13 +1304,19 @@ pub fn specs() -> Vec<CommandSpec> {
"Place Embedded…",
&["File"],
None,
r##"{"path":str,"scale":%? (default: fit when larger than the canvas),"fit":bool=true,"center":[x,y]?}"##,
r##"{"path":str,"scale":%? (default: fit when larger than the canvas),"fit":bool? (default: Preferences > General > Resize Image During Place),"center":[x,y]?}"##,
native_doc,
|s, p| place(s, p, false)
),
spec!("file.placeLinked", "Place Linked…", &["File"], None, r##"{"path":str,"scale":%?,"fit":bool=true,"center":[x,y]?}"##, native_doc, |s, p| place(
s, p, true
)),
spec!(
"file.placeLinked",
"Place Linked…",
&["File"],
None,
r##"{"path":str,"scale":%?,"fit":bool? (default: Preferences > General > Resize Image During Place),"center":[x,y]?}"##,
native_doc,
|s, p| place(s, p, true)
),
spec!(
"file.fileInfo",
"File Info…",

View File

@ -180,6 +180,23 @@ fn place_embedded_centres_fits_and_embeds() {
}
}
/// Preferences ▸ General ▸ Resize Image During Place: off, a larger image keeps its natural size
/// instead of being fitted to the canvas; an explicit `"fit"` still wins.
#[test]
fn resize_image_during_place_preference_controls_the_fit() {
let dir = tmp("place-fit-pref");
let big = png(&dir, "big.png", 200, 100, "#ff0000");
let mut s = session(100, 100, 8);
s.execute("prefs.set", json!({"path": "general.resizeImageDuringPlace", "value": false})).unwrap();
let r = s.execute("file.placeEmbedded", json!({"path": big})).unwrap();
assert!((r["scale"].as_f64().unwrap() - 100.0).abs() < 1e-9, "natural size: {r}");
let b = r["bounds"].as_array().unwrap();
assert!(b[2].as_f64().unwrap() > 100.0, "wider than the canvas: {b:?}");
// An explicit "fit": true overrides the preference.
let r = s.execute("file.placeEmbedded", json!({"path": big, "fit": true})).unwrap();
assert!((r["scale"].as_f64().unwrap() - 50.0).abs() < 1e-9, "fitted on request: {r}");
}
/// A 40×20 JPEG (left half red, right half blue) tagged EXIF Orientation = 6: shown upright it
/// is 20×40, red on top.
fn rotated_jpeg(dir: &str) -> String {

View File

@ -972,7 +972,6 @@ pub const SECTIONS: [(&str, &str); 18] = [
pub const HIDDEN_UNTIL_IMPLEMENTED: &[&str] = &[
"general.colorPicker",
"general.beepWhenDone",
"general.resizeImageDuringPlace",
"general.alwaysCreateSmartObjectsWhenPlacing",
"general.animatedZoom",
"general.zoomResizesWindows",

View File

@ -9,7 +9,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js
| [Performance](#performance) | 3 | 22 | 22 | 56 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 15 not run, 7 not measurable yet (baseline 2026-10-05) |
| [File compatibility](#file-compatibility) | 5 | 11 | 11 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 236/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 |
| [Tools](#tools) | 21 | 15 | 6 | |
| [Workspace and UI](#workspace-and-ui) | 6 | 7 | 10 | settings that do nothing: 44 of 149 (target 0) |
| [Workspace and UI](#workspace-and-ui) | 6 | 7 | 10 | settings that do nothing: 43 of 149 (target 0) |
| [Type](#type) | 0 | 3 | 6 | |
| [Automation](#automation) | 3 | 2 | 6 | 628 of 628 menu items live (100.0%) |
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 28 of 28 crates; panic_hunt present |
@ -175,9 +175,9 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
## Workspace and UI
**Settings that do nothing: 44 of 149** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
**Settings that do nothing: 43 of 149** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
Unread: `general.color_picker`, `general.beep_when_done`, `general.resize_image_during_place`, `general.always_create_smart_objects_when_placing`, `general.animated_zoom`, `general.zoom_resizes_windows`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `workspace.enable_narrow_options_bar`, `tools.enable_flick_panning`, `tools.vary_round_brush_hardness_on_hud`, `tools.show_transformation_values`, `file_handling.image_previews`, `file_handling.save_in_background`, `file_handling.ignore_exif_profile_tag`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `cursors.brush_preview_color`, `units_and_rulers.type_units`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `units_and_rulers.print_resolution`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.missing_glyph_protection`, `type_.show_font_names_in_english`, `type_.text_engine`, `type_.font_preview`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `enhanced_controls.zoom_with_trackpad_pinch`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`.
Unread: `general.color_picker`, `general.beep_when_done`, `general.always_create_smart_objects_when_placing`, `general.animated_zoom`, `general.zoom_resizes_windows`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `workspace.enable_narrow_options_bar`, `tools.enable_flick_panning`, `tools.vary_round_brush_hardness_on_hud`, `tools.show_transformation_values`, `file_handling.image_previews`, `file_handling.save_in_background`, `file_handling.ignore_exif_profile_tag`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `cursors.brush_preview_color`, `units_and_rulers.type_units`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `units_and_rulers.print_resolution`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.missing_glyph_protection`, `type_.show_font_names_in_english`, `type_.text_engine`, `type_.font_preview`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `enhanced_controls.zoom_with_trackpad_pinch`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`.
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|