diff --git a/crates/compose/src/bounds.rs b/crates/compose/src/bounds.rs
--- a/crates/compose/src/bounds.rs
+++ b/crates/compose/src/bounds.rs
@@ -41,7 +41,14 @@ pub fn content_bounds(s: &Surface) -> Rect {
     }
     let fmt = s.format();
     let mut dp = vec![0u8; fmt.bytes_per_pixel()];
-    photocraft_raster::encode_pixel(&fmt, &s.default_pixel(), &mut dp);
+    let default = s.default_pixel();
+    photocraft_raster::encode_pixel(&fmt, &default, &mut dp);
+    // As in `Surface::content_bounds`: a clear pixel's colour is not content, so a surface that
+    // has an alpha channel and defaults to transparent measures by its alpha.
+    let sample = fmt.sample;
+    let alpha_at = fmt.mode.color_channels() * sample.bytes();
+    let by_alpha = fmt.alpha && default.get(fmt.mode.color_channels()).is_some_and(|&alpha| alpha == 0.0);
+    let bpp = fmt.bytes_per_pixel();
     let mut c = cache().lock().unwrap_or_else(|e| e.into_inner());
     if c.entries.len() > c.sweep_at {
         c.entries.retain(|_, e| e.tile.strong_count() > 0);
@@ -54,7 +61,7 @@ pub fn content_bounds(s: &Surface) -> Rect {
         let b = match hit {
             Some(b) => b,
             None => {
-                let b = t.content_bounds(&dp);
+                let b = if by_alpha { t.alpha_bounds(alpha_at, bpp, sample) } else { t.content_bounds(&dp) };
                 c.entries.insert(key, Entry { tile: Arc::downgrade(t), default: dp.clone().into_boxed_slice(), bounds: b });
                 b
             }
@@ -88,4 +95,15 @@ mod tests {
         assert_eq!(content_bounds(&s), Rect::new(-300, 10, 1300, 900));
         assert_eq!(content_bounds(&s), s.content_bounds(), "cached");
     }
+
+    #[test]
+    fn a_clear_pixels_colour_is_not_content_here_either() {
+        // The cached path measures by alpha like `Surface::content_bounds`, so the transform box
+        // and the layer listing agree on a file whose clear pixels hold white.
+        let mut s = Surface::new(photocraft_color::PixelFormat::RGBA8);
+        s.fill_rect(Rect::new(0, 0, 300, 300), &[1.0, 1.0, 1.0, 0.0]);
+        s.fill_rect(Rect::new(70, 41, 270, 241), &[0.0, 0.0, 0.0, 1.0]);
+        assert_eq!(content_bounds(&s), Rect::new(70, 41, 270, 241));
+        assert_eq!(content_bounds(&s), s.content_bounds(), "cached and surface agree");
+    }
 }
diff --git a/crates/compose/src/filter_mask.rs b/crates/compose/src/filter_mask.rs
--- a/crates/compose/src/filter_mask.rs
+++ b/crates/compose/src/filter_mask.rs
@@ -15,7 +15,11 @@ use photocraft_raster::Surface;
 /// channel layout. Pixels the mask hides entirely are `base`'s.
 pub fn mix(base: &Surface, filtered: &Surface, mask: &LayerMask) -> Surface {
     let fmt = base.format();
-    let area = base.content_bounds().union(&filtered.content_bounds());
+    // Every tile that holds data, not the exact content: Photoshop's cache carries white under
+    // the clear pixels, and where the mask hides the filter those have to be written empty, so a
+    // tile holding only clear-but-white pixels still needs visiting (`content_bounds` no longer
+    // reports them — a clear pixel's colour is not content).
+    let area = base.tile_bounds().union(&filtered.tile_bounds());
     let mut out = filtered.clone();
     if area.is_empty() || filtered.format() != fmt {
         return out;
@@ -66,7 +70,8 @@ const HIDDEN: f32 = 1.0 / 1024.0;
 /// doesn't show there.
 pub fn unmix(base: &Surface, mixed: &Surface, mask: &LayerMask) -> Surface {
     let fmt = base.format();
-    let area = base.content_bounds().union(&mixed.content_bounds());
+    // As in [`mix`]: tiles, since a tile of clear-but-white pixels still has to be written.
+    let area = base.tile_bounds().union(&mixed.tile_bounds());
     let mut out = mixed.clone();
     if area.is_empty() || mixed.format() != fmt {
         return out;
diff --git a/crates/engine/src/cmd/mode_cmds.rs b/crates/engine/src/cmd/mode_cmds.rs
--- a/crates/engine/src/cmd/mode_cmds.rs
+++ b/crates/engine/src/cmd/mode_cmds.rs
@@ -371,7 +371,9 @@ fn color_table(s: &mut Session, p: &Value) -> Result<Value> {
     s.edit("Color Table", |doc, _| {
         let old = cur.clone();
         for surf in doc.layers.iter_mut().filter_map(Layer::surface_mut) {
-            let r = surf.content_bounds();
+            // Every stored pixel, not just the visible ones: a pixel the old transparent entry
+            // cleared holds its colour with a zero alpha, and clearing the entry restores it.
+            let r = surf.stored_bounds();
             if r.is_empty() {
                 continue;
             }
diff --git a/crates/io/tests/it/flat.rs b/crates/io/tests/it/flat.rs
--- a/crates/io/tests/it/flat.rs
+++ b/crates/io/tests/it/flat.rs
@@ -61,6 +61,31 @@ codec_rt!(cin_gray16, "cin", ColorMode::Grayscale, SampleType::U16, false, 0.5 /
 codec_rt!(cin_rgb16, "cin", ColorMode::Rgb, SampleType::U16, false, 0.5 / 1023.0);
 codec_rt!(iff_rgb8_opaque, "iff", ColorMode::Rgb, SampleType::U8, false, 0.0);
 
+#[test]
+fn a_clear_pixels_colour_is_not_the_layers_bounds() {
+    // A PNG may store white under its clear pixels (unassociated alpha). Photoshop opens the
+    // document at the file's size but bounds the layer by the pixels whose alpha is not zero;
+    // measuring whole pixels instead made the layer span the canvas and Free Transform report
+    // the canvas instead of the artwork.
+    use photocraft_codecs::{ChannelLayout, EncodeOptions, Format, Image, SampleType as CS};
+    let (w, h) = (8u32, 6u32);
+    let mut px = Vec::new();
+    for y in 0..h {
+        for x in 0..w {
+            let painted = (2..6).contains(&x) && (1..5).contains(&y);
+            px.extend_from_slice(if painted { &[0u8, 0, 0, 255] } else { &[255, 255, 255, 0] });
+        }
+    }
+    let img = Image::from_u8(w, h, ChannelLayout::Rgba, px).expect("image");
+    let bytes = photocraft_codecs::encode(&img, Format::Png, &EncodeOptions::default()).expect("encode");
+    let d = import("clear.png", &bytes).expect("import").document;
+    assert_eq!(d.size, photocraft_geom::Size::new(w, h), "the canvas is the file's");
+    let s = d.layers[0].surface().expect("raster layer");
+    assert_eq!(s.content_bounds(), photocraft_geom::Rect::new(2, 1, 6, 5), "the artwork's box");
+    // The colour under the clear pixels is kept: only the bounds ignore it.
+    assert_eq!(s.pixel(0, 0), vec![1.0, 1.0, 1.0, 0.0]);
+}
+
 fn indexed_bmp_8() -> Vec<u8> {
     const FILE_HEADER_SIZE: usize = 14;
     const INFO_HEADER_SIZE: usize = 40;
diff --git a/crates/raster/src/lib.rs b/crates/raster/src/lib.rs
--- a/crates/raster/src/lib.rs
+++ b/crates/raster/src/lib.rs
@@ -113,6 +113,37 @@ impl Tile {
         }
         (x0 != usize::MAX).then_some((x0 as u16, y0 as u16, x1 as u16, y1 as u16))
     }
+
+    /// Tile-local half-open bounds `(x0, y0, x1, y1)` of the pixels whose alpha sample is not
+    /// zero, or `None` when every pixel is clear. `alpha_at` is the alpha sample's byte offset
+    /// within a pixel of `bpp` bytes.
+    ///
+    /// The colour stored under a clear pixel is invisible, so it is not content: comparing whole
+    /// pixels against a transparent default (`content_bounds`) would count the white an encoder
+    /// leaves there, and a layer whose clear pixels are white would measure as its whole canvas
+    /// where Photoshop measures the artwork.
+    pub fn alpha_bounds(&self, alpha_at: usize, bpp: usize, sample: SampleType) -> Option<(u16, u16, u16, u16)> {
+        if bpp == 0 || alpha_at + sample.bytes() > bpp {
+            return None;
+        }
+        let clear = |px: &[u8]| match sample {
+            SampleType::U8 => px[alpha_at] == 0,
+            SampleType::U16 => px[alpha_at] == 0 && px[alpha_at + 1] == 0,
+            SampleType::F32 => f32::from_ne_bytes([px[alpha_at], px[alpha_at + 1], px[alpha_at + 2], px[alpha_at + 3]]) == 0.0,
+        };
+        let ts = TILE_SIZE as usize;
+        let (mut x0, mut y0, mut x1, mut y1) = (usize::MAX, usize::MAX, 0usize, 0usize);
+        for (y, row) in self.data.chunks_exact(ts * bpp).enumerate() {
+            let pixels = row.chunks_exact(bpp);
+            let Some(first) = pixels.clone().position(|px| !clear(px)) else { continue };
+            let last = pixels.clone().rposition(|px| !clear(px)).unwrap_or(first);
+            x0 = x0.min(first);
+            x1 = x1.max(last + 1);
+            y0 = y0.min(y);
+            y1 = y + 1;
+        }
+        (x0 != usize::MAX).then_some((x0 as u16, y0 as u16, x1 as u16, y1 as u16))
+    }
 }
 
 /// Bytes compared at once while looking for a difference in a tile row.
@@ -438,11 +469,32 @@ impl Surface {
         self.tiles.keys().fold(Rect::EMPTY, |r, c| r.union(&c.rect()))
     }
 
-    /// Exact bounds of pixels that differ from the default pixel.
+    /// Exact bounds of the pixels that differ from the default pixel — everything the surface
+    /// stores, clear or not. A reader that re-derives something from the stored samples (the
+    /// alpha an Indexed document's colour table implies, say) needs these: [`content_bounds`]
+    /// leaves out a clear pixel whose colour an encoder left behind, and that pixel is stored.
+    pub fn stored_bounds(&self) -> Rect {
+        self.scan_bounds(false)
+    }
+
+    /// Exact bounds of pixels that differ from the default pixel; for a surface that carries an
+    /// alpha channel and defaults to transparent, the bounds of the pixels that are not clear
+    /// (see [`Tile::alpha_bounds`]).
     pub fn content_bounds(&self) -> Rect {
+        self.scan_bounds(true)
+    }
+
+    fn scan_bounds(&self, content: bool) -> Rect {
         if self.tiles.is_empty() {
             return Rect::EMPTY;
         }
+        // A clear pixel's colour is invisible, so it is not content: a layer whose clear pixels
+        // hold white measures as the artwork, as Photoshop's bounds have it, instead of as its
+        // whole canvas.
+        let bpp = self.format.bytes_per_pixel();
+        let sample = self.format.sample;
+        let alpha_at = self.format.mode.color_channels() * sample.bytes();
+        let by_alpha = content && self.format.alpha && self.default_pixel[alpha_at..alpha_at + sample.bytes()].iter().all(|&b| b == 0);
         // Scan tiles from the outside of the tile grid inwards; a tile lying entirely inside the
         // bounds found so far cannot extend them and is skipped (so a full-canvas selection scans
         // roughly its outer ring of tiles instead of every pixel).
@@ -458,7 +510,8 @@ impl Surface {
             if !out.is_empty() && out.contains_rect(&origin) {
                 continue;
             }
-            if let Some((x0, y0, x1, y1)) = t.content_bounds(&self.default_pixel) {
+            let found = if by_alpha { t.alpha_bounds(alpha_at, bpp, sample) } else { t.content_bounds(&self.default_pixel) };
+            if let Some((x0, y0, x1, y1)) = found {
                 out = out.union(&Rect::new(x0.into(), y0.into(), x1.into(), y1.into()).translate(origin.x0, origin.y0));
             }
         }
diff --git a/crates/raster/src/tests.rs b/crates/raster/src/tests.rs
--- a/crates/raster/src/tests.rs
+++ b/crates/raster/src/tests.rs
@@ -392,6 +392,31 @@ fn content_bounds_and_prune() {
     assert_eq!(s.content_bounds(), Rect::new(300, 10, 310, 20));
 }
 
+#[test]
+fn a_clear_pixels_colour_is_not_content() {
+    // An encoder may leave white under the clear pixels (unassociated alpha). The layer's bounds
+    // are the pixels whose alpha is not zero, as Photoshop measures them, not the white's box.
+    let mut s = Surface::new(PixelFormat::RGBA8);
+    s.fill_rect(Rect::new(0, 0, 100, 100), &[1.0, 1.0, 1.0, 0.0]);
+    s.fill_rect(Rect::new(40, 30, 60, 50), &[0.0, 0.0, 0.0, 1.0]);
+    assert_eq!(s.content_bounds(), Rect::new(40, 30, 60, 50));
+    // The colour is still there: only the bounds ignore it.
+    assert_eq!(s.pixel(0, 0), vec![1.0, 1.0, 1.0, 0.0]);
+    // The sample type decides what "clear" is.
+    let mut s16 = Surface::new(PixelFormat::RGBA16);
+    s16.fill_rect(Rect::new(0, 0, 20, 20), &[1.0, 1.0, 1.0, 0.0]);
+    s16.fill_rect(Rect::new(3, 4, 9, 10), &[0.0, 0.0, 0.0, 1.0]);
+    assert_eq!(s16.content_bounds(), Rect::new(3, 4, 9, 10));
+    // A surface with no alpha channel has only its colour to measure.
+    let mut g = Surface::new(PixelFormat::GRAY8);
+    g.fill_rect(Rect::new(0, 0, 10, 10), &[1.0]);
+    assert_eq!(g.content_bounds(), Rect::new(0, 0, 10, 10));
+    // Nor does an opaque default pixel turn a colour into a clear one.
+    let mut opaque = Surface::with_default(PixelFormat::RGBA8, &[1.0, 1.0, 1.0, 1.0]);
+    opaque.fill_rect(Rect::new(2, 2, 5, 5), &[0.0, 0.0, 0.0, 0.0]);
+    assert_eq!(opaque.content_bounds(), Rect::new(2, 2, 5, 5));
+}
+
 #[test]
 fn interleaved_roundtrip_crossing_tiles() {
     let r = Rect::new(-20, 250, 280, 262);
