diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs
index badc8d5f..00b649fd 100644
--- a/examples/scenes/src/test_scenes.rs
+++ b/examples/scenes/src/test_scenes.rs
@@ -80,7 +80,8 @@ export_scenes!(
     many_draw_objects(many_draw_objects),
     blurred_rounded_rect(blurred_rounded_rect),
     image_sampling(image_sampling),
-    image_extend_modes(image_extend_modes)
+    image_extend_modes(image_extend_modes),
+    image_extend_modes_nearest_neighbor(image_extend_modes_nearest_neighbor),
 );
 
 /// Implementations for the test scenes.
@@ -659,7 +660,8 @@ mod impls {
         let piet_logo = params
             .images
             .from_bytes(FLOWER_IMAGE.as_ptr() as usize, FLOWER_IMAGE)
-            .unwrap();
+            .unwrap()
+            .with_alpha(((params.time * 0.5 + 200.0).sin() as f32 + 1.0) * 0.5);
 
         use PathEl::*;
         let rect = Rect::from_origin_size(Point::new(0.0, 0.0), (1000.0, 1000.0));
@@ -1810,6 +1812,19 @@ mod impls {
     pub(super) fn image_extend_modes(scene: &mut Scene, params: &mut SceneParams<'_>) {
         params.resolution = Some(Vec2::new(1500., 1500.));
         params.base_color = Some(palette::css::WHITE);
+        image_extend_modes_helper(scene, ImageQuality::Medium);
+    }
+
+    pub(super) fn image_extend_modes_nearest_neighbor(
+        scene: &mut Scene,
+        params: &mut SceneParams<'_>,
+    ) {
+        params.resolution = Some(Vec2::new(1500., 1500.));
+        params.base_color = Some(palette::css::WHITE);
+        image_extend_modes_helper(scene, ImageQuality::Low);
+    }
+
+    fn image_extend_modes_helper(scene: &mut Scene, quality: ImageQuality) {
         let mut blob: Vec<u8> = Vec::new();
         [
             palette::css::RED,
@@ -1822,14 +1837,15 @@ mod impls {
             blob.extend(c.premultiply().to_rgba8().to_u8_array());
         });
         let data = Blob::new(Arc::new(blob));
-        let image = Image::new(data, ImageFormat::Rgba8, 2, 2);
-        let image = image.with_extend(Extend::Pad);
+        let image = Image::new(data, ImageFormat::Rgba8, 2, 2).with_quality(quality);
+        let brush_offset = Some(Affine::translate((2., 2.)));
         // Pad extend mode
+        let image = image.with_extend(Extend::Pad);
         scene.fill(
             Fill::NonZero,
             Affine::scale(100.).then_translate((100., 100.).into()),
             &image,
-            Some(Affine::translate((2., 2.)).then_scale(100.)),
+            brush_offset,
             &Rect::new(0., 0., 6., 6.),
         );
         let image = image.with_extend(Extend::Reflect);
@@ -1837,7 +1853,7 @@ mod impls {
             Fill::NonZero,
             Affine::scale(100.).then_translate((100., 800.).into()),
             &image,
-            Some(Affine::translate((2., 2.))),
+            brush_offset,
             &Rect::new(0., 0., 6., 6.),
         );
         let image = image.with_extend(Extend::Repeat);
@@ -1845,7 +1861,17 @@ mod impls {
             Fill::NonZero,
             Affine::scale(100.).then_translate((800., 100.).into()),
             &image,
-            Some(Affine::translate((2., 2.))),
+            brush_offset,
+            &Rect::new(0., 0., 6., 6.),
+        );
+        let image = image
+            .with_x_extend(Extend::Repeat)
+            .with_y_extend(Extend::Reflect);
+        scene.fill(
+            Fill::NonZero,
+            Affine::scale(100.).then_translate((800., 800.).into()),
+            &image,
+            brush_offset,
             &Rect::new(0., 0., 6., 6.),
         );
     }
diff --git a/vello_encoding/src/draw.rs b/vello_encoding/src/draw.rs
index 37f6766a..c527411b 100644
--- a/vello_encoding/src/draw.rs
+++ b/vello_encoding/src/draw.rs
@@ -31,7 +31,7 @@ impl DrawTag {
     pub const SWEEP_GRADIENT: Self = Self(0x254);
 
     /// Image fill.
-    pub const IMAGE: Self = Self(0x248);
+    pub const IMAGE: Self = Self(0x28C); // info: 10, scene: 3
 
     /// Blurred rounded rectangle.
     pub const BLUR_RECT: Self = Self(0x2d4); // info: 11, scene: 5 (DrawBlurRoundedRect)
@@ -164,6 +164,8 @@ pub struct DrawImage {
     pub xy: u32,
     /// Packed image dimensions.
     pub width_height: u32,
+    /// Packed quality, extend mode and 8-bit alpha (bits `qqxxyyaaaaaaaa`).
+    pub sample_alpha: u32,
 }
 
 /// Draw data for a blurred rounded rectangle.
diff --git a/vello_encoding/src/encoding.rs b/vello_encoding/src/encoding.rs
index d581acd4..dda2d5fa 100644
--- a/vello_encoding/src/encoding.rs
+++ b/vello_encoding/src/encoding.rs
@@ -402,7 +402,7 @@ impl Encoding {
 
     /// Encodes an image brush.
     pub fn encode_image(&mut self, image: &Image, alpha: f32) {
-        let _alpha = alpha * image.alpha;
+        let alpha = (alpha * image.alpha * 255.0).round() as u8;
         // TODO: feed the alpha multiplier through the full pipeline for consistency
         // with other brushes?
         // Tracked in https://github.com/linebender/vello/issues/692
@@ -415,6 +415,10 @@ impl Encoding {
             .extend_from_slice(bytemuck::bytes_of(&DrawImage {
                 xy: 0,
                 width_height: (image.width << 16) | (image.height & 0xFFFF),
+                sample_alpha: ((image.quality as u32) << 12)
+                    | ((image.x_extend as u32) << 10)
+                    | ((image.y_extend as u32) << 8)
+                    | alpha as u32,
             }));
     }
 
diff --git a/vello_shaders/shader/draw_leaf.wgsl b/vello_shaders/shader/draw_leaf.wgsl
index e572a276..415aa81f 100644
--- a/vello_shaders/shader/draw_leaf.wgsl
+++ b/vello_shaders/shader/draw_leaf.wgsl
@@ -253,6 +253,7 @@ fn main(
                     info[di + 6u] = bitcast<u32>(inv.translate.y);
                     info[di + 7u] = scene[dd];
                     info[di + 8u] = scene[dd + 1u];
+                    info[di + 9u] = scene[dd + 2u];
                 }
                 case DRAWTAG_BLURRED_ROUNDED_RECT: {
                     info[di] = draw_flags;
diff --git a/vello_shaders/shader/fine.wgsl b/vello_shaders/shader/fine.wgsl
index e09e2ee7..24ecb5c7 100644
--- a/vello_shaders/shader/fine.wgsl
+++ b/vello_shaders/shader/fine.wgsl
@@ -25,6 +25,10 @@ var<storage> segments: array<Segment>;
 
 const GRADIENT_WIDTH = 512;
 
+const IMAGE_QUALITY_LOW = 0u;
+const IMAGE_QUALITY_MEDIUM = 1u;
+const IMAGE_QUALITY_HIGH = 2u;
+
 @group(0) @binding(2)
 var<storage> ptcl: array<u32>;
 
@@ -805,12 +809,17 @@ fn read_image(cmd_ix: u32) -> CmdImage {
     let xlat = vec2(bitcast<f32>(info[info_offset + 4u]), bitcast<f32>(info[info_offset + 5u]));
     let xy = info[info_offset + 6u];
     let width_height = info[info_offset + 7u];
+    let sample_alpha = info[info_offset + 8u];
+    let alpha = f32(sample_alpha & 0xFFu) / 255.0;
+    let quality = sample_alpha >> 12u;
+    let x_extend = (sample_alpha >> 10u) & 0x3u;
+    let y_extend = (sample_alpha >> 8u) & 0x3u;
     // The following are not intended to be bitcasts
     let x = f32(xy >> 16u);
     let y = f32(xy & 0xffffu);
     let width = f32(width_height >> 16u);
     let height = f32(width_height & 0xffffu);
-    return CmdImage(matrx, xlat, vec2(x, y), vec2(width, height));
+    return CmdImage(matrx, xlat, vec2(x, y), vec2(width, height), x_extend, y_extend, quality, alpha);
 }
 
 fn read_end_clip(cmd_ix: u32) -> CmdEndClip {
@@ -1146,26 +1155,52 @@ fn main(
             case CMD_IMAGE: {
                 let image = read_image(cmd_ix);
                 let atlas_max = image.atlas_offset + image.extents - vec2(1.0);
-                for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
-                    // We only need to load from the textures if the value will be used.
-                    if area[i] != 0.0 {
-                        let my_xy = vec2(xy.x + f32(i), xy.y);
-                        let atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat + image.atlas_offset - vec2(0.5);
-                        // This currently only implements the Pad extend mode
-                        // TODO: Support repeat and reflect
-                        // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust
-                        let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max);
-                        // We know that the floor and ceil are within the atlas area because atlas_max and
-                        // atlas_offset are integers
-                        let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped));
-                        let uv_frac = fract(atlas_uv);
-                        let a = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.xy), 0));
-                        let b = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.xw), 0));
-                        let c = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.zy), 0));
-                        let d = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.zw), 0));
-                        let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x);
-                        let fg_i = fg_rgba * area[i];
-                        rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
+                let extents_inv = vec2(1.0) / image.extents;
+                switch image.quality {
+                    case IMAGE_QUALITY_LOW: {
+                        for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
+                            // We only need to load from the textures if the value will be used.
+                            if area[i] != 0.0 {
+                                let my_xy = vec2(xy.x + f32(i), xy.y);
+                                var atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat;
+                                atlas_uv.x = extend_mode(atlas_uv.x * extents_inv.x, image.x_extend_mode) * image.extents.x;
+                                atlas_uv.y = extend_mode(atlas_uv.y * extents_inv.y, image.y_extend_mode) * image.extents.y;
+                                atlas_uv = atlas_uv + image.atlas_offset;
+                                // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust
+                                let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max);
+                                let fg_rgba = premul_alpha(textureLoad(image_atlas, vec2<i32>(atlas_uv_clamped), 0));
+                                let r = extend_mode(atlas_uv.x * extents_inv.x, image.x_extend_mode);
+                                let g = extend_mode(atlas_uv.y * extents_inv.y, image.y_extend_mode);
+                                let fg_rgba2 = vec4(r, g, 0.0, 1.0);
+                                let fg_i = fg_rgba * area[i] * image.alpha;
+                                rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
+                            }
+                        }
+                    }
+                    case IMAGE_QUALITY_MEDIUM, default: {
+                        for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
+                            // We only need to load from the textures if the value will be used.
+                            if area[i] != 0.0 {
+                                let my_xy = vec2(xy.x + f32(i), xy.y);
+                                var atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat;
+                                atlas_uv.x = extend_mode(atlas_uv.x * extents_inv.x, image.x_extend_mode) * image.extents.x;
+                                atlas_uv.y = extend_mode(atlas_uv.y * extents_inv.y, image.y_extend_mode) * image.extents.y;
+                                atlas_uv = atlas_uv + image.atlas_offset - vec2(0.5);
+                                // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust
+                                let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max);
+                                // We know that the floor and ceil are within the atlas area because atlas_max and
+                                // atlas_offset are integers
+                                let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped));
+                                let uv_frac = fract(atlas_uv);
+                                let a = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.xy), 0));
+                                let b = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.xw), 0));
+                                let c = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.zy), 0));
+                                let d = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.zw), 0));
+                                let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x);
+                                let fg_i = fg_rgba * area[i] * image.alpha;
+                                rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
+                            }
+                        }
                     }
                 }
                 cmd_ix += 2u;
diff --git a/vello_shaders/shader/shared/drawtag.wgsl b/vello_shaders/shader/shared/drawtag.wgsl
index 245594eb..8a3bdda8 100644
--- a/vello_shaders/shader/shared/drawtag.wgsl
+++ b/vello_shaders/shader/shared/drawtag.wgsl
@@ -21,7 +21,7 @@ const DRAWTAG_FILL_COLOR = 0x44u;
 const DRAWTAG_FILL_LIN_GRADIENT = 0x114u;
 const DRAWTAG_FILL_RAD_GRADIENT = 0x29cu;
 const DRAWTAG_FILL_SWEEP_GRADIENT = 0x254u;
-const DRAWTAG_FILL_IMAGE = 0x248u;
+const DRAWTAG_FILL_IMAGE = 0x28Cu;
 const DRAWTAG_BLURRED_ROUNDED_RECT = 0x2d4u;
 const DRAWTAG_BEGIN_CLIP = 0x9u;
 const DRAWTAG_END_CLIP = 0x21u;
diff --git a/vello_shaders/shader/shared/ptcl.wgsl b/vello_shaders/shader/shared/ptcl.wgsl
index 467739c8..d0b41cbe 100644
--- a/vello_shaders/shader/shared/ptcl.wgsl
+++ b/vello_shaders/shader/shared/ptcl.wgsl
@@ -97,6 +97,10 @@ struct CmdImage {
     xlat: vec2<f32>,
     atlas_offset: vec2<f32>,
     extents: vec2<f32>,
+    x_extend_mode: u32,
+    y_extend_mode: u32,
+    quality: u32,
+    alpha: f32,
 }
 
 struct CmdEndClip {
diff --git a/vello_shaders/src/cpu/draw_leaf.rs b/vello_shaders/src/cpu/draw_leaf.rs
index 56ddfcce..09a1aeab 100644
--- a/vello_shaders/src/cpu/draw_leaf.rs
+++ b/vello_shaders/src/cpu/draw_leaf.rs
@@ -175,6 +175,7 @@ fn draw_leaf_main(
                         info[di + 6] = f32::to_bits(xform.0[5]);
                         info[di + 7] = scene[dd as usize];
                         info[di + 8] = scene[dd as usize + 1];
+                        info[di + 9] = scene[dd as usize + 2];
                     }
                     DrawTag::BLUR_RECT => {
                         info[di] = draw_flags;
diff --git a/vello_tests/snapshots/image_extend_modes.png b/vello_tests/snapshots/image_extend_modes.png
index 2bcfdde6..a53232e0 100644
--- a/vello_tests/snapshots/image_extend_modes.png
+++ b/vello_tests/snapshots/image_extend_modes.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:7f046fa46e495ad62bada74cdfbec9501f29709b4e52c5ec655063a8ffcf7ea4
-size 26146
+oid sha256:a707994259c84dc4ae5f08977de58966c3976e59f03933fb216242b776e526e7
+size 107007
diff --git a/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png b/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png
new file mode 100644
index 00000000..015c09c3
--- /dev/null
+++ b/vello_tests/snapshots/image_extend_modes_nearest_neighbor.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2e56f11690b5a99171cdb1b31f11cb5c293085c5e78d398c921525ba7be17612
+size 13764
diff --git a/vello_tests/tests/snapshot_test_scenes.rs b/vello_tests/tests/snapshot_test_scenes.rs
index a40171d9..36db6eb9 100644
--- a/vello_tests/tests/snapshot_test_scenes.rs
+++ b/vello_tests/tests/snapshot_test_scenes.rs
@@ -129,3 +129,11 @@ fn snapshot_image_extend_modes() {
     let params = TestParams::new("image_extend_modes", 375, 375);
     snapshot_test_scene(test_scene, params);
 }
+
+#[test]
+#[cfg_attr(skip_gpu_tests, ignore)]
+fn snapshot_image_extend_modes_nearest_neighbor() {
+    let test_scene = test_scenes::image_extend_modes_nearest_neighbor();
+    let params = TestParams::new("image_extend_modes_nearest_neighbor", 375, 375);
+    snapshot_test_scene(test_scene, params);
+}