diff --git a/include/libcamera/ipa/raspberrypi.h b/include/libcamera/ipa/raspberrypi.h
index ca62990..361b47d 100644
--- a/include/libcamera/ipa/raspberrypi.h
+++ b/include/libcamera/ipa/raspberrypi.h
@@ -57,6 +57,7 @@ static const ControlInfoMap RPiControls = {
 	{ &controls::Saturation, ControlInfo(0.0f, 32.0f) },
 	{ &controls::Sharpness, ControlInfo(0.0f, 16.0f, 1.0f) },
 	{ &controls::ColourCorrectionMatrix, ControlInfo(-16.0f, 16.0f) },
+	{ &controls::SensorCrop, ControlInfo(Rectangle(0, 0, 0, 0), Rectangle(65535, 65535, 65535, 65535), Rectangle(0, 0, 0, 0)) },
 };
 
 } /* namespace libcamera */
diff --git a/src/ipa/raspberrypi/raspberrypi.cpp b/src/ipa/raspberrypi/raspberrypi.cpp
index 4557016..4a364b7 100644
--- a/src/ipa/raspberrypi/raspberrypi.cpp
+++ b/src/ipa/raspberrypi/raspberrypi.cpp
@@ -687,6 +687,13 @@ void IPARPi::queueRequest(const ControlList &controls)
 			break;
 		}
 
+		case controls::SENSOR_CROP: {
+			/* Just copy the information back. */
+			Rectangle crop = ctrl.second.get<Rectangle>();
+			libcameraMetadata_.set(controls::SensorCrop, crop);
+			break;
+		}
+
 		default:
 			LOG(IPARPI, Warning)
 				<< "Ctrl " << controls::controls.at(ctrl.first)->name()
diff --git a/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp b/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
index 7f151cb..3196a61 100644
--- a/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
+++ b/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
@@ -335,6 +335,11 @@ public:
 	std::queue<FrameBuffer *> embeddedQueue_;
 	std::deque<Request *> requestQueue_;
 
+	/* For handling digital zoom. */
+	Size ispMinSize_;
+	Size sensorOutputSize_;
+	Rectangle lastSensorCrop_;
+
 private:
 	void checkRequestCompleted();
 	void tryRunPipeline();
@@ -737,6 +742,16 @@ int PipelineHandlerRPi::configure(Camera *camera, CameraConfiguration *config)
 		return ret;
 	}
 
+	/*
+	 * Figure out the smallest selection the ISP will allow. We also store
+	 * the output image size, in pixels, from the sensor. These will be
+	 * used for digital zoom.
+	 */
+	Rectangle testCrop(0, 0, 1, 1);
+	data->isp_[Isp::Input].dev()->setSelection(V4L2_SEL_TGT_CROP, &testCrop);
+	data->ispMinSize_ = testCrop.size();
+	data->sensorOutputSize_ = sensorFormat.size;
+
 	/* Adjust aspect ratio by providing crops on the input image. */
 	Rectangle crop{ 0, 0, sensorFormat.size };
 
@@ -753,6 +768,8 @@ int PipelineHandlerRPi::configure(Camera *camera, CameraConfiguration *config)
 	crop.y = (sensorFormat.size.height - crop.height) >> 1;
 	data->isp_[Isp::Input].dev()->setSelection(V4L2_SEL_TGT_CROP, &crop);
 
+	data->lastSensorCrop_ = crop;
+
 	ret = data->configureIPA();
 	if (ret)
 		LOG(RPI, Error) << "Failed to configure the IPA: " << ret;
@@ -1545,6 +1562,28 @@ void RPiCameraData::tryRunPipeline()
 	 */
 	Request *request = requestQueue_.front();
 
+	if (request->controls().contains(controls::SensorCrop)) {
+		Rectangle crop = request->controls().get<Rectangle>(controls::SensorCrop);
+		if (crop.width && crop.height) {
+			/*
+			 * The crop that we set must be:
+			 * 1. At least as big as ispMinSize_, once that's been
+			 *    enlarged to the same aspect ratio.
+			 * 2. With the same mid-point, if possible.
+			 * 3. But it can't go outside the sensor area.
+			 */
+			Size minSize = ispMinSize_.aspectRatioUp(crop.size());
+			Size size = crop.size().expandedTo(minSize);
+			crop = size.centre(crop).clamp(Rectangle(sensorOutputSize_));
+
+			if (crop != lastSensorCrop_)
+				isp_[Isp::Input].dev()->setSelection(V4L2_SEL_TGT_CROP, &crop);
+			lastSensorCrop_ = crop;
+		}
+
+		request->controls().set(controls::SensorCrop, lastSensorCrop_);
+	}
+
 	/*
 	 * Process all the user controls by the IPA. Once this is complete, we
 	 * queue the ISP output buffer listed in the request to start the HW
