From 5a935b2830c2eb48842e8bfa999c19dea817eba4 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 16:23:05 -0500 Subject: [PATCH] Place: refuse a scale that makes the placed image exceed the render pixel limit (#11229) * Place: refuse a scale that makes the placed image exceed the render pixel limit * address review: bound the rounded-up resampled size when placing at a scale * address review: the oversized-scale error names the placing command --- crates/engine/src/cmd/file_cmds/place_steps.rs | 13 +++++++++++++ crates/engine/src/cmd/file_cmds/tests.rs | 15 +++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/crates/engine/src/cmd/file_cmds/place_steps.rs b/crates/engine/src/cmd/file_cmds/place_steps.rs index 8d35d54704..f449eaee8a 100644 --- a/crates/engine/src/cmd/file_cmds/place_steps.rs +++ b/crates/engine/src/cmd/file_cmds/place_steps.rs @@ -171,6 +171,19 @@ impl PlaceSteps { Some(c) if c.len() == 2 => (c[0].as_f64().unwrap_or(cw / 2.0), c[1].as_f64().unwrap_or(ch / 2.0)), _ => (cw / 2.0, ch / 2.0), }; + // `scale` is the caller's: bound the resampled size before allocating it (a frame's image + // renders through its smart object, which has its own budget). Resampling rounds the far + // output edges outward; an unresampled placement keeps its size. + if self.input.frame.is_none() { + let (ow, oh) = if (scale - 1.0).abs() > 1e-9 { ((w * scale).ceil(), (h * scale).ceil()) } else { (w, h) }; + let scaled = ow * oh; + if scaled.is_nan() || scaled > photocraft_compose::MAX_RENDER_PIXELS as f64 { + return Err(EngineError::BadParams { + cmd: self.input.command().into(), + msg: format!("scaling {w}×{h} by {scale} makes an image too large to place"), + }); + } + } let (dx, dy) = ((center.0 - w * scale / 2.0).round(), (center.1 - h * scale / 2.0).round()); self.plan = Plan { w, h, scale, dx, dy }; // A frame's image renders through its smart object, which refuses a placement past the diff --git a/crates/engine/src/cmd/file_cmds/tests.rs b/crates/engine/src/cmd/file_cmds/tests.rs index 94680e496a..fa37375abb 100644 --- a/crates/engine/src/cmd/file_cmds/tests.rs +++ b/crates/engine/src/cmd/file_cmds/tests.rs @@ -2004,6 +2004,21 @@ fn place_embedded_centres_fits_and_embeds() { } } +/// A caller-chosen placement scale cannot ask for an image beyond the render limit. +#[test] +fn placing_refuses_a_scale_that_would_exceed_the_pixel_limit() { + let dir = tmp("place-huge-scale"); + let small = png(&dir, "small.png", 200, 100, "#ff0000"); + let mut s = session(100, 100, 8); + for scale in [1e9, 1e300, 1e308, 23170.25] { + assert!(matches!(s.execute("file.placeEmbedded", json!({"path": small, "scale": scale})), Err(EngineError::BadParams { .. })), "{scale}"); + } + assert!(s.execute("file.placeEmbedded", json!({"path": small, "scale": 50})).is_ok()); + // The error names the command that asked. + let linked = s.execute("file.placeLinked", json!({"path": small, "scale": 1e9})); + assert!(matches!(linked, Err(EngineError::BadParams { ref cmd, .. }) if cmd == "file.placeLinked"), "{linked:?}"); +} + /// Preferences ▸ General ▸ Always Create Smart Objects When Placing: unchecked, an embedded place /// lands as a plain pixel layer; a linked place is always a smart object and `"smartObject"` wins. #[test]