diff --git a/include/libcamera/pixelformats.h b/include/libcamera/pixelformats.h
index 544363af7a8c6f05..eb40e55ac159505a 100644
--- a/include/libcamera/pixelformats.h
+++ b/include/libcamera/pixelformats.h
@@ -7,13 +7,36 @@
 #ifndef __LIBCAMERA_PIXEL_FORMATS_H__
 #define __LIBCAMERA_PIXEL_FORMATS_H__
 
+#include <set>
 #include <stdint.h>
+#include <string>
 
 #include <linux/drm_fourcc.h>
 
 namespace libcamera {
 
-using PixelFormat = uint32_t;
+class PixelFormat
+{
+public:
+	PixelFormat();
+	PixelFormat(uint32_t fourcc, const std::set<uint64_t> &modifiers = {});
+
+	bool operator==(const PixelFormat &other) const;
+	bool operator!=(const PixelFormat &other) const { return !(*this == other); }
+	bool operator<(const PixelFormat &other) const;
+
+	bool isValid() const { return fourcc_ != 0; }
+
+	operator uint32_t() const { return fourcc_; }
+	uint32_t fourcc() const { return fourcc_; }
+	const std::set<uint64_t> &modifiers() const { return modifiers_; }
+
+	std::string toString() const;
+
+private:
+	uint32_t fourcc_;
+	std::set<uint64_t> modifiers_;
+};
 
 } /* namespace libcamera */
 
diff --git a/src/cam/main.cpp b/src/cam/main.cpp
index af516f1cbf23974a..f73e77f381779853 100644
--- a/src/cam/main.cpp
+++ b/src/cam/main.cpp
@@ -253,7 +253,7 @@ int CamApp::prepareConfig()
 
 			/* TODO: Translate 4CC string to ID. */
 			if (opt.isSet("pixelformat"))
-				cfg.pixelFormat = opt["pixelformat"];
+				cfg.pixelFormat = PixelFormat(opt["pixelformat"]);
 		}
 	}
 
@@ -304,8 +304,8 @@ int CamApp::infoConfiguration()
 
 		const StreamFormats &formats = cfg.formats();
 		for (PixelFormat pixelformat : formats.pixelformats()) {
-			std::cout << " * Pixelformat: 0x" << std::hex
-				  << std::setw(8) << pixelformat << " "
+			std::cout << " * Pixelformat: "
+				  << pixelformat.toString() << " "
 				  << formats.range(pixelformat).toString()
 				  << std::endl;
 
diff --git a/src/gstreamer/gstlibcamera-utils.cpp b/src/gstreamer/gstlibcamera-utils.cpp
index 3b3973bcea3dc759..f21e94c3eef92737 100644
--- a/src/gstreamer/gstlibcamera-utils.cpp
+++ b/src/gstreamer/gstlibcamera-utils.cpp
@@ -80,11 +80,11 @@ gst_libcamera_stream_formats_to_caps(const StreamFormats &formats)
 	GstCaps *caps = gst_caps_new_empty();
 
 	for (PixelFormat pixelformat : formats.pixelformats()) {
-		g_autoptr(GstStructure) bare_s = bare_structure_from_fourcc(pixelformat);
+		g_autoptr(GstStructure) bare_s = bare_structure_from_fourcc(pixelformat.fourcc());
 
 		if (!bare_s) {
 			GST_WARNING("Unsupported DRM format %" GST_FOURCC_FORMAT,
-				    GST_FOURCC_ARGS(pixelformat));
+				    GST_FOURCC_ARGS(pixelformat.fourcc()));
 			continue;
 		}
 
@@ -120,7 +120,7 @@ GstCaps *
 gst_libcamera_stream_configuration_to_caps(const StreamConfiguration &stream_cfg)
 {
 	GstCaps *caps = gst_caps_new_empty();
-	GstStructure *s = bare_structure_from_fourcc(stream_cfg.pixelFormat);
+	GstStructure *s = bare_structure_from_fourcc(stream_cfg.pixelFormat.fourcc());
 
 	gst_structure_set(s,
 			  "width", G_TYPE_INT, stream_cfg.size.width,
@@ -135,7 +135,7 @@ void
 gst_libcamera_configure_stream_from_caps(StreamConfiguration &stream_cfg,
 					 GstCaps *caps)
 {
-	GstVideoFormat gst_format = drm_to_gst_format(stream_cfg.pixelFormat);
+	GstVideoFormat gst_format = drm_to_gst_format(stream_cfg.pixelFormat.fourcc());
 
 	/* First fixate the caps using default configuration value. */
 	g_assert(gst_caps_is_writable(caps));
diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
index a95ae48ac8cfbc98..8a11deb814bc0bfb 100644
--- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp
+++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
@@ -789,7 +789,7 @@ int PipelineHandlerRkISP1::start(Camera *camera)
 	/* Inform IPA of stream configuration and sensor controls. */
 	std::map<unsigned int, IPAStream> streamConfig;
 	streamConfig[0] = {
-		.pixelFormat = data->stream_.configuration().pixelFormat,
+		.pixelFormat = data->stream_.configuration().pixelFormat.fourcc(),
 		.size = data->stream_.configuration().size,
 	};
 
diff --git a/src/libcamera/pipeline/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo.cpp
index 1de091e0c0e57f7c..731149755728f209 100644
--- a/src/libcamera/pipeline/uvcvideo.cpp
+++ b/src/libcamera/pipeline/uvcvideo.cpp
@@ -113,8 +113,9 @@ CameraConfiguration::Status UVCCameraConfiguration::validate()
 	if (iter == pixelFormats.end()) {
 		cfg.pixelFormat = pixelFormats.front();
 		LOG(UVC, Debug)
-			<< "Adjusting pixel format from " << pixelFormat
-			<< " to " << cfg.pixelFormat;
+			<< "Adjusting pixel format from "
+			<< pixelFormat.toString() << " to "
+			<< cfg.pixelFormat.toString();
 		status = Adjusted;
 	}
 
diff --git a/src/libcamera/pipeline/vimc.cpp b/src/libcamera/pipeline/vimc.cpp
index 04cad94e739e9ae9..2e2162b2bf4477c5 100644
--- a/src/libcamera/pipeline/vimc.cpp
+++ b/src/libcamera/pipeline/vimc.cpp
@@ -103,7 +103,7 @@ private:
 
 namespace {
 
-constexpr std::array<PixelFormat, 3> pixelformats{
+static const std::array<PixelFormat, 3> pixelformats{
 	DRM_FORMAT_RGB888,
 	DRM_FORMAT_BGR888,
 	DRM_FORMAT_BGRA8888,
diff --git a/src/libcamera/pixelformats.cpp b/src/libcamera/pixelformats.cpp
index c03335400b709d9b..929578fed3e22c92 100644
--- a/src/libcamera/pixelformats.cpp
+++ b/src/libcamera/pixelformats.cpp
@@ -15,14 +15,91 @@
 namespace libcamera {
 
 /**
- * \typedef PixelFormat
+ * \class PixelFormat
  * \brief libcamera image pixel format
  *
  * The PixelFormat type describes the format of images in the public libcamera
- * API. It stores a FourCC value as a 32-bit unsigned integer. The values are
- * defined in the Linux kernel DRM/KMS API (see linux/drm_fourcc.h).
- *
- * \todo Add support for format modifiers
+ * API. It stores a FourCC value as a 32-bit unsigned integer and a set of
+ * modifiers. The FourCC and modifiers values are defined in the Linux kernel
+ * DRM/KMS API (see linux/drm_fourcc.h).
  */
 
+PixelFormat::PixelFormat()
+	: fourcc_(0)
+{
+}
+
+/**
+ * \brief Construct a PixelFormat from a DRM FourCC and a set of modifiers
+ * \param[in] fourcc A DRM FourCC
+ * \param[in] modifiers A set of DRM FourCC modifiers
+ */
+PixelFormat::PixelFormat(uint32_t fourcc, const std::set<uint64_t> &modifiers)
+	: fourcc_(fourcc), modifiers_(modifiers)
+{
+}
+
+/**
+ * \brief Compare pixel formats for equality
+ * \return True if the two pixel formats are equal, false otherwise
+ */
+bool PixelFormat::operator==(const PixelFormat &other) const
+{
+	return fourcc_ == other.fourcc() && modifiers_ == other.modifiers_;
+}
+
+/**
+ * \fn bool PixelFormat::operator!=(const PixelFormat &other) const
+ * \brief Compare pixel formats for inequality
+ * \return True if the two pixel formats are not equal, false otherwise
+ */
+
+/**
+ * \brief Compare pixel formats for smaller than order
+ * \return True if \a this is smaller than \a other, false otherwise
+ */
+bool PixelFormat::operator<(const PixelFormat &other) const
+{
+	if (fourcc_ < other.fourcc_)
+		return true;
+	if (fourcc_ > other.fourcc_)
+		return false;
+	return modifiers_ < modifiers_;
+}
+
+/**
+ * \fn bool PixelFormat::isValid() const
+ * \brief Check if the pixel format is valid
+ * \return True if the pixel format has a non-zero value, false otherwise
+ */
+
+/**
+ * \fn PixelFormat::operator uint32_t() const
+ * \brief Convert the the pixel format numerical value
+ * \return The pixel format numerical value
+ */
+
+/**
+ * \fn PixelFormat::fourcc() const
+ * \brief Retrieve the pixel format FourCC
+ * \return DRM FourCC
+ */
+
+/**
+ * \fn PixelFormat::modifiers() const
+ * \brief Retrieve the pixel format modifiers
+ * \return Set of DRM modifiers
+ */
+
+/**
+ * \brief Assemble and return a string describing the pixel format
+ * \return A string describing the pixel format
+ */
+std::string PixelFormat::toString() const
+{
+	char str[11];
+	snprintf(str, 11, "0x%08x", fourcc_);
+	return str;
+}
+
 } /* namespace libcamera */
diff --git a/src/libcamera/stream.cpp b/src/libcamera/stream.cpp
index 13789e9eb344f95c..0716de388bd81d80 100644
--- a/src/libcamera/stream.cpp
+++ b/src/libcamera/stream.cpp
@@ -347,9 +347,7 @@ StreamConfiguration::StreamConfiguration(const StreamFormats &formats)
  */
 std::string StreamConfiguration::toString() const
 {
-	std::stringstream ss;
-	ss << size.toString() << "-" << utils::hex(pixelFormat);
-	return ss.str();
+	return size.toString() + "-" + pixelFormat.toString();
 }
 
 /**
diff --git a/src/libcamera/v4l2_videodevice.cpp b/src/libcamera/v4l2_videodevice.cpp
index 3869766046236f34..c8ba0f8cebedb91a 100644
--- a/src/libcamera/v4l2_videodevice.cpp
+++ b/src/libcamera/v4l2_videodevice.cpp
@@ -1647,7 +1647,7 @@ uint32_t V4L2VideoDevice::toV4L2Fourcc(PixelFormat pixelFormat)
  */
 uint32_t V4L2VideoDevice::toV4L2Fourcc(PixelFormat pixelFormat, bool multiplanar)
 {
-	switch (pixelFormat) {
+	switch (pixelFormat.fourcc()) {
 	/* RGB formats. */
 	case DRM_FORMAT_BGR888:
 		return V4L2_PIX_FMT_RGB24;
@@ -1691,8 +1691,7 @@ uint32_t V4L2VideoDevice::toV4L2Fourcc(PixelFormat pixelFormat, bool multiplanar
 	 * class. Until we fix the logger, work around it.
 	 */
 	libcamera::_log(__FILE__, __LINE__, _LOG_CATEGORY(V4L2)(), LogError).stream()
-		<< "Unsupported V4L2 pixel format "
-		<< utils::hex(pixelFormat);
+		<< "Unsupported V4L2 pixel format " << pixelFormat.toString();
 	return 0;
 }
 
diff --git a/src/qcam/format_converter.cpp b/src/qcam/format_converter.cpp
index 7b8ad77c42fe85d4..66d07025ac9578ca 100644
--- a/src/qcam/format_converter.cpp
+++ b/src/qcam/format_converter.cpp
@@ -28,7 +28,7 @@
 int FormatConverter::configure(libcamera::PixelFormat format, unsigned int width,
 			       unsigned int height)
 {
-	switch (format) {
+	switch (format.fourcc()) {
 	case DRM_FORMAT_NV12:
 		formatFamily_ = NV;
 		horzSubSample_ = 2;
diff --git a/src/v4l2/v4l2_camera_proxy.cpp b/src/v4l2/v4l2_camera_proxy.cpp
index e349fbddc2a4d5a2..c6d1e5030b58b630 100644
--- a/src/v4l2/v4l2_camera_proxy.cpp
+++ b/src/v4l2/v4l2_camera_proxy.cpp
@@ -533,7 +533,7 @@ struct PixelFormatInfo {
 
 namespace {
 
-constexpr std::array<PixelFormatInfo, 13> pixelFormatInfo = {{
+static const std::array<PixelFormatInfo, 13> pixelFormatInfo = {{
 	/* RGB formats. */
 	{ DRM_FORMAT_RGB888,	V4L2_PIX_FMT_BGR24,	1, {{ { 24, 1, 1 }, {  0, 0, 0 }, {  0, 0, 0 } }} },
 	{ DRM_FORMAT_BGR888,	V4L2_PIX_FMT_RGB24,	1, {{ { 24, 1, 1 }, {  0, 0, 0 }, {  0, 0, 0 } }} },
@@ -605,7 +605,7 @@ uint32_t V4L2CameraProxy::drmToV4L2(PixelFormat format)
 					 return info.format == format;
 				 });
 	if (info == pixelFormatInfo.end())
-		return format;
+		return format.fourcc();
 
 	return info->v4l2Format;
 }
