diff --git a/crates/create/src/extract.rs b/crates/create/src/extract.rs
--- a/crates/create/src/extract.rs
+++ b/crates/create/src/extract.rs
@@ -501,7 +501,13 @@ fn gray_plane(doc: &Document, m: &Stream, w: usize, h: usize) -> Option<Vec<u8>>
         return Some(px);
     }
     let img = image::GrayImage::from_raw(mw as u32, mh as u32, px)?;
-    Some(image::imageops::resize(&img, w as u32, h as u32, image::imageops::FilterType::Triangle).into_raw())
+    let filter = image::imageops::FilterType::Triangle;
+    // `resize` scales the height first, through a buffer of the mask's width × the image's height
+    // (16 bytes a pixel). Each is at most `MAX_PIXELS`, but a mask wide where its image is tall
+    // makes that product far larger; the allocation then fails and aborts. Such a mask is scaled
+    // to the image's width first, so both steps stay within `MAX_PIXELS`.
+    let img = if (mw as u64).saturating_mul(h as u64) > MAX_PIXELS { image::imageops::resize(&img, w as u32, mh as u32, filter) } else { img };
+    Some(image::imageops::resize(&img, w as u32, h as u32, filter).into_raw())
 }
 
 fn png(w: u32, h: u32, rgb: &[u8], alpha: Option<&[u8]>) -> Result<Vec<u8>, String> {
diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs
--- a/crates/create/src/tests.rs
+++ b/crates/create/src/tests.rs
@@ -804,6 +804,41 @@ fn soft_masks_of_another_size_and_stencil_and_colour_key_masks_become_alpha() {
     assert!(rgba[4] <= 2 && rgba[7] == 128, "{rgba:?}");
 }
 
+#[test]
+fn a_soft_mask_shaped_across_its_image_is_scaled_without_a_huge_buffer() {
+    // A 1×N image with an N×1 soft mask: each is small, but scaling the mask's height first would
+    // need an N × N buffer of floats (16 TB for N = 1,000,000) before narrowing it.
+    const N: usize = 1_000_000;
+    let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap();
+    with_image(&mut doc, |doc| {
+        let m = doc.add(Object::Stream(Stream::from_raw(image_dict(N as i64, 1, "DeviceGray", 8), vec![0; N])));
+        let mut d = image_dict(1, N as i64, "DeviceGray", 8);
+        d.set(b"SMask".to_vec(), Object::Ref(m));
+        Stream::from_raw(d, vec![10; N])
+    });
+    let a = alphas(&exported_rgba(&doc).1);
+    assert_eq!(a.len(), N);
+    assert!(a.iter().all(|&v| v == 0), "a fully transparent soft mask stays transparent");
+
+    // The same shape as a stencil /Mask, every sample 1 (hidden).
+    let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap();
+    with_image(&mut doc, |doc| {
+        let mut md = Dict::new();
+        md.set(b"Type".to_vec(), Object::name("XObject"));
+        md.set(b"Subtype".to_vec(), Object::name("Image"));
+        md.set(b"Width".to_vec(), Object::Int(N as i64));
+        md.set(b"Height".to_vec(), Object::Int(1));
+        md.set(b"ImageMask".to_vec(), Object::Bool(true));
+        let m = doc.add(Object::Stream(Stream::from_raw(md, vec![0xff; N / 8])));
+        let mut d = image_dict(1, N as i64, "DeviceGray", 8);
+        d.set(b"Mask".to_vec(), Object::Ref(m));
+        Stream::from_raw(d, vec![10; N])
+    });
+    let a = alphas(&exported_rgba(&doc).1);
+    assert_eq!(a.len(), N);
+    assert!(a.iter().all(|&v| v == 0), "a stencil mask hiding every pixel still hides them");
+}
+
 #[test]
 fn damaged_masks_and_sizes_never_panic() {
     for (w, h, mw, mh, data) in [(4, 2, -3, 1, vec![]), (4, 2, i64::MAX, 2, vec![0; 8]), (-4, 2, 4, 2, vec![0; 8]), (4, 2, 4, 2, vec![])] {
