diff --git a/src/libcamera/formats.cpp b/src/libcamera/formats.cpp
index 5f6552a4e06c..4a351020b0d9 100644
--- a/src/libcamera/formats.cpp
+++ b/src/libcamera/formats.cpp
@@ -9,6 +9,8 @@
 
 #include <errno.h>
 
+#include "log.h"
+
 /**
  * \file formats.h
  * \brief Types and helper methods to handle libcamera image formats
@@ -16,6 +18,8 @@
 
 namespace libcamera {
 
+LOG_DEFINE_CATEGORY(Formats)
+
 /**
  * \class ImageFormats
  * \brief Describe V4L2Device and V4L2SubDevice image formats
@@ -104,4 +108,126 @@ const std::map<unsigned int, std::vector<SizeRange>> &ImageFormats::data() const
 	return data_;
 }
 
+/**
+ * \class PixelFormatInfo
+ * \brief Information about pixel formats
+ *
+ * The PixelFormatInfo class groups together information describing a pixel
+ * format. It facilitates handling of pixel formats by providing data commonly
+ * used in pipeline handlers.
+ *
+ * \var PixelFormatInfo::format
+ * \brief The PixelFormat described by this instance
+ *
+ * \var PixelFormatInfo::v4l2Format
+ * \brief The V4L2 pixel format corresponding to the PixelFormat
+ */
+
+namespace {
+
+const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
+	/* RGB formats. */
+	{ PixelFormat(DRM_FORMAT_BGR888), {
+		.format = PixelFormat(DRM_FORMAT_BGR888),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_RGB24),
+	} },
+	{ PixelFormat(DRM_FORMAT_RGB888), {
+		.format = PixelFormat(DRM_FORMAT_RGB888),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_BGR24),
+	} },
+	{ PixelFormat(DRM_FORMAT_ABGR8888), {
+		.format = PixelFormat(DRM_FORMAT_ABGR8888),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_RGBA32),
+	} },
+	{ PixelFormat(DRM_FORMAT_ARGB8888), {
+		.format = PixelFormat(DRM_FORMAT_ARGB8888),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_ABGR32),
+	} },
+	{ PixelFormat(DRM_FORMAT_BGRA8888), {
+		.format = PixelFormat(DRM_FORMAT_BGRA8888),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_ARGB32),
+	} },
+	{ PixelFormat(DRM_FORMAT_RGBA8888), {
+		.format = PixelFormat(DRM_FORMAT_RGBA8888),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_BGRA32),
+	} },
+
+	/* YUV packed formats. */
+	{ PixelFormat(DRM_FORMAT_YUYV), {
+		.format = PixelFormat(DRM_FORMAT_YUYV),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_YUYV),
+	} },
+	{ PixelFormat(DRM_FORMAT_YVYU), {
+		.format = PixelFormat(DRM_FORMAT_YVYU),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_YVYU),
+	} },
+	{ PixelFormat(DRM_FORMAT_UYVY), {
+		.format = PixelFormat(DRM_FORMAT_UYVY),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_UYVY),
+	} },
+	{ PixelFormat(DRM_FORMAT_VYUY), {
+		.format = PixelFormat(DRM_FORMAT_VYUY),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_VYUY),
+	} },
+
+	/* YUV planar formats. */
+	{ PixelFormat(DRM_FORMAT_NV16), {
+		.format = PixelFormat(DRM_FORMAT_NV16),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_NV16),
+	} },
+	{ PixelFormat(DRM_FORMAT_NV61), {
+		.format = PixelFormat(DRM_FORMAT_NV61),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_NV61),
+	} },
+	{ PixelFormat(DRM_FORMAT_NV12), {
+		.format = PixelFormat(DRM_FORMAT_NV12),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_NV12),
+	} },
+	{ PixelFormat(DRM_FORMAT_NV21), {
+		.format = PixelFormat(DRM_FORMAT_NV21),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_NV21),
+	} },
+
+	/* Greyscale formats. */
+	{ PixelFormat(DRM_FORMAT_R8), {
+		.format = PixelFormat(DRM_FORMAT_R8),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_GREY),
+	} },
+
+	/* Compressed formats. */
+	{ PixelFormat(DRM_FORMAT_MJPEG), {
+		.format = PixelFormat(DRM_FORMAT_MJPEG),
+		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_MJPEG),
+	} },
+};
+
+} /* namespace */
+
+/**
+ * \fn bool PixelFormatInfo::isValid() const
+ * \brief Check if the pixel format info is valid
+ * \return True if the pixel format info is valid, false otherwise
+ */
+
+/**
+ * \brief Retrieve information about a pixel format
+ * \param[in] format The pixel format
+ * \return The PixelFormatInfo describing the \a format if known, or an invalid
+ * PixelFormatInfo otherwise
+ */
+const PixelFormatInfo &PixelFormatInfo::info(const PixelFormat &format)
+{
+	static const PixelFormatInfo invalid{};
+
+	const auto iter = pixelFormatInfo.find(format);
+	if (iter == pixelFormatInfo.end()) {
+		LOG(Formats, Warning)
+			<< "Unsupported pixel format "
+			<< format.toString();
+		return invalid;
+	}
+
+	return iter->second;
+}
+
 } /* namespace libcamera */
diff --git a/src/libcamera/include/formats.h b/src/libcamera/include/formats.h
index f43bc8c004f6..560df07c4451 100644
--- a/src/libcamera/include/formats.h
+++ b/src/libcamera/include/formats.h
@@ -12,6 +12,9 @@
 #include <vector>
 
 #include <libcamera/geometry.h>
+#include <libcamera/pixelformats.h>
+
+#include "v4l2_pixelformat.h"
 
 namespace libcamera {
 
@@ -29,6 +32,18 @@ private:
 	std::map<unsigned int, std::vector<SizeRange>> data_;
 };
 
+class PixelFormatInfo
+{
+public:
+	bool isValid() const { return format.isValid(); }
+
+	static const PixelFormatInfo &info(const PixelFormat &format);
+
+	/* \todo Add support for non-contiguous memory planes */
+	PixelFormat format;
+	V4L2PixelFormat v4l2Format;
+};
+
 } /* namespace libcamera */
 
 #endif /* __LIBCAMERA_FORMATS_H__ */
diff --git a/src/libcamera/v4l2_pixelformat.cpp b/src/libcamera/v4l2_pixelformat.cpp
index e1c96b9862c3..580c0fc9d983 100644
--- a/src/libcamera/v4l2_pixelformat.cpp
+++ b/src/libcamera/v4l2_pixelformat.cpp
@@ -16,6 +16,7 @@
 
 #include <libcamera/pixelformats.h>
 
+#include "formats.h"
 #include "log.h"
 
 /**
@@ -43,71 +44,6 @@ LOG_DECLARE_CATEGORY(V4L2)
 
 namespace {
 
-struct PixelFormatInfo {
-	/* \todo Add support for non-contiguous memory planes */
-	V4L2PixelFormat v4l2Format;
-};
-
-const std::map<PixelFormat, PixelFormatInfo> pf2vpf{
-	/* RGB formats. */
-	{ PixelFormat(DRM_FORMAT_BGR888), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_RGB24),
-	} },
-	{ PixelFormat(DRM_FORMAT_RGB888), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_BGR24),
-	} },
-	{ PixelFormat(DRM_FORMAT_ABGR8888), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_RGBA32),
-	} },
-	{ PixelFormat(DRM_FORMAT_ARGB8888), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_ABGR32),
-	} },
-	{ PixelFormat(DRM_FORMAT_BGRA8888), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_ARGB32),
-	} },
-	{ PixelFormat(DRM_FORMAT_RGBA8888), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_BGRA32),
-	} },
-
-	/* YUV packed formats. */
-	{ PixelFormat(DRM_FORMAT_YUYV), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_YUYV),
-	} },
-	{ PixelFormat(DRM_FORMAT_YVYU), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_YVYU),
-	} },
-	{ PixelFormat(DRM_FORMAT_UYVY), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_UYVY),
-	} },
-	{ PixelFormat(DRM_FORMAT_VYUY), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_VYUY),
-	} },
-
-	/* YUV planar formats. */
-	{ PixelFormat(DRM_FORMAT_NV16), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_NV16),
-	} },
-	{ PixelFormat(DRM_FORMAT_NV61), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_NV61),
-	} },
-	{ PixelFormat(DRM_FORMAT_NV12), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_NV12),
-	} },
-	{ PixelFormat(DRM_FORMAT_NV21), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_NV21),
-	} },
-
-	/* Greyscale formats. */
-	{ PixelFormat(DRM_FORMAT_R8), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_GREY),
-	} },
-
-	/* Compressed formats. */
-	{ PixelFormat(DRM_FORMAT_MJPEG), {
-		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_MJPEG),
-	} },
-};
-
 const std::map<V4L2PixelFormat, PixelFormat> vpf2pf{
 	/* RGB formats. */
 	{ V4L2PixelFormat(V4L2_PIX_FMT_RGB24), PixelFormat(DRM_FORMAT_BGR888) },
@@ -233,15 +169,10 @@ PixelFormat V4L2PixelFormat::toPixelFormat() const
 V4L2PixelFormat V4L2PixelFormat::fromPixelFormat(const PixelFormat &pixelFormat,
 						 bool multiplanar)
 {
-	const auto iter = pf2vpf.find(pixelFormat);
-	if (iter == pf2vpf.end()) {
-		LOG(V4L2, Warning)
-			<< "Unsupported pixel format "
-			<< pixelFormat.toString();
+	const PixelFormatInfo &info = PixelFormatInfo::info(pixelFormat);
+	if (!info.isValid())
 		return V4L2PixelFormat();
-	}
 
-	const PixelFormatInfo &info = iter->second;
 	return info.v4l2Format;
 }
 
