summaryrefslogtreecommitdiff
path: root/src/apps/common
diff options
context:
space:
mode:
Diffstat (limited to 'src/apps/common')
-rw-r--r--src/apps/common/dng_writer.cpp654
-rw-r--r--src/apps/common/dng_writer.h27
-rw-r--r--src/apps/common/event_loop.cpp150
-rw-r--r--src/apps/common/event_loop.h68
-rw-r--r--src/apps/common/image.cpp109
-rw-r--r--src/apps/common/image.h50
-rw-r--r--src/apps/common/meson.build27
-rw-r--r--src/apps/common/options.cpp1141
-rw-r--r--src/apps/common/options.h157
-rw-r--r--src/apps/common/ppm_writer.cpp53
-rw-r--r--src/apps/common/ppm_writer.h20
-rw-r--r--src/apps/common/stream_options.cpp121
-rw-r--r--src/apps/common/stream_options.h29
13 files changed, 2606 insertions, 0 deletions
diff --git a/src/apps/common/dng_writer.cpp b/src/apps/common/dng_writer.cpp
new file mode 100644
index 00000000..59f1fa23
--- /dev/null
+++ b/src/apps/common/dng_writer.cpp
@@ -0,0 +1,654 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * DNG writer
+ */
+
+#include "dng_writer.h"
+
+#include <algorithm>
+#include <iostream>
+#include <map>
+
+#include <tiffio.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/formats.h>
+#include <libcamera/property_ids.h>
+
+using namespace libcamera;
+
+enum CFAPatternColour : uint8_t {
+ CFAPatternRed = 0,
+ CFAPatternGreen = 1,
+ CFAPatternBlue = 2,
+};
+
+struct FormatInfo {
+ uint8_t bitsPerSample;
+ CFAPatternColour pattern[4];
+ void (*packScanline)(void *output, const void *input,
+ unsigned int width);
+ void (*thumbScanline)(const FormatInfo &info, void *output,
+ const void *input, unsigned int width,
+ unsigned int stride);
+};
+
+struct Matrix3d {
+ Matrix3d()
+ {
+ }
+
+ Matrix3d(float m0, float m1, float m2,
+ float m3, float m4, float m5,
+ float m6, float m7, float m8)
+ {
+ m[0] = m0, m[1] = m1, m[2] = m2;
+ m[3] = m3, m[4] = m4, m[5] = m5;
+ m[6] = m6, m[7] = m7, m[8] = m8;
+ }
+
+ Matrix3d(const Span<const float> &span)
+ : Matrix3d(span[0], span[1], span[2],
+ span[3], span[4], span[5],
+ span[6], span[7], span[8])
+ {
+ }
+
+ static Matrix3d diag(float diag0, float diag1, float diag2)
+ {
+ return Matrix3d(diag0, 0, 0, 0, diag1, 0, 0, 0, diag2);
+ }
+
+ static Matrix3d identity()
+ {
+ return Matrix3d(1, 0, 0, 0, 1, 0, 0, 0, 1);
+ }
+
+ Matrix3d transpose() const
+ {
+ return { m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8] };
+ }
+
+ Matrix3d cofactors() const
+ {
+ return { m[4] * m[8] - m[5] * m[7],
+ -(m[3] * m[8] - m[5] * m[6]),
+ m[3] * m[7] - m[4] * m[6],
+ -(m[1] * m[8] - m[2] * m[7]),
+ m[0] * m[8] - m[2] * m[6],
+ -(m[0] * m[7] - m[1] * m[6]),
+ m[1] * m[5] - m[2] * m[4],
+ -(m[0] * m[5] - m[2] * m[3]),
+ m[0] * m[4] - m[1] * m[3] };
+ }
+
+ Matrix3d adjugate() const
+ {
+ return cofactors().transpose();
+ }
+
+ float determinant() const
+ {
+ return m[0] * (m[4] * m[8] - m[5] * m[7]) -
+ m[1] * (m[3] * m[8] - m[5] * m[6]) +
+ m[2] * (m[3] * m[7] - m[4] * m[6]);
+ }
+
+ Matrix3d inverse() const
+ {
+ return adjugate() * (1.0 / determinant());
+ }
+
+ Matrix3d operator*(const Matrix3d &other) const
+ {
+ Matrix3d result;
+ for (unsigned int i = 0; i < 3; i++) {
+ for (unsigned int j = 0; j < 3; j++) {
+ result.m[i * 3 + j] =
+ m[i * 3 + 0] * other.m[0 + j] +
+ m[i * 3 + 1] * other.m[3 + j] +
+ m[i * 3 + 2] * other.m[6 + j];
+ }
+ }
+ return result;
+ }
+
+ Matrix3d operator*(float f) const
+ {
+ Matrix3d result;
+ for (unsigned int i = 0; i < 9; i++)
+ result.m[i] = m[i] * f;
+ return result;
+ }
+
+ float m[9];
+};
+
+void packScanlineSBGGR8(void *output, const void *input, unsigned int width)
+{
+ const uint8_t *in = static_cast<const uint8_t *>(input);
+ uint8_t *out = static_cast<uint8_t *>(output);
+
+ std::copy(in, in + width, out);
+}
+
+void packScanlineSBGGR10P(void *output, const void *input, unsigned int width)
+{
+ const uint8_t *in = static_cast<const uint8_t *>(input);
+ uint8_t *out = static_cast<uint8_t *>(output);
+
+ /* \todo Can this be made more efficient? */
+ for (unsigned int x = 0; x < width; x += 4) {
+ *out++ = in[0];
+ *out++ = (in[4] & 0x03) << 6 | in[1] >> 2;
+ *out++ = (in[1] & 0x03) << 6 | (in[4] & 0x0c) << 2 | in[2] >> 4;
+ *out++ = (in[2] & 0x0f) << 4 | (in[4] & 0x30) >> 2 | in[3] >> 6;
+ *out++ = (in[3] & 0x3f) << 2 | (in[4] & 0xc0) >> 6;
+ in += 5;
+ }
+}
+
+void packScanlineSBGGR12P(void *output, const void *input, unsigned int width)
+{
+ const uint8_t *in = static_cast<const uint8_t *>(input);
+ uint8_t *out = static_cast<uint8_t *>(output);
+
+ /* \todo Can this be made more efficient? */
+ for (unsigned int i = 0; i < width; i += 2) {
+ *out++ = in[0];
+ *out++ = (in[2] & 0x0f) << 4 | in[1] >> 4;
+ *out++ = (in[1] & 0x0f) << 4 | in[2] >> 4;
+ in += 3;
+ }
+}
+
+void thumbScanlineSBGGRxxP(const FormatInfo &info, void *output,
+ const void *input, unsigned int width,
+ unsigned int stride)
+{
+ const uint8_t *in = static_cast<const uint8_t *>(input);
+ uint8_t *out = static_cast<uint8_t *>(output);
+
+ /* Number of bytes corresponding to 16 pixels. */
+ unsigned int skip = info.bitsPerSample * 16 / 8;
+
+ for (unsigned int x = 0; x < width; x++) {
+ uint8_t value = (in[0] + in[1] + in[stride] + in[stride + 1]) >> 2;
+ *out++ = value;
+ *out++ = value;
+ *out++ = value;
+ in += skip;
+ }
+}
+
+void packScanlineIPU3(void *output, const void *input, unsigned int width)
+{
+ const uint8_t *in = static_cast<const uint8_t *>(input);
+ uint16_t *out = static_cast<uint16_t *>(output);
+
+ /*
+ * Upscale the 10-bit format to 16-bit as it's not trivial to pack it
+ * as 10-bit without gaps.
+ *
+ * \todo Improve packing to keep the 10-bit sample size.
+ */
+ unsigned int x = 0;
+ while (true) {
+ for (unsigned int i = 0; i < 6; i++) {
+ *out++ = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
+ if (++x >= width)
+ return;
+
+ *out++ = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
+ if (++x >= width)
+ return;
+
+ *out++ = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
+ if (++x >= width)
+ return;
+
+ *out++ = (in[4] & 0xff) << 8 | (in[3] & 0xc0) << 0;
+ if (++x >= width)
+ return;
+
+ in += 5;
+ }
+
+ *out++ = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
+ if (++x >= width)
+ return;
+
+ in += 2;
+ }
+}
+
+void thumbScanlineIPU3([[maybe_unused]] const FormatInfo &info, void *output,
+ const void *input, unsigned int width,
+ unsigned int stride)
+{
+ uint8_t *out = static_cast<uint8_t *>(output);
+
+ for (unsigned int x = 0; x < width; x++) {
+ unsigned int pixel = x * 16;
+ unsigned int block = pixel / 25;
+ unsigned int pixelInBlock = pixel - block * 25;
+
+ /*
+ * If the pixel is the last in the block cheat a little and
+ * move one pixel backward to avoid reading between two blocks
+ * and having to deal with the padding bits.
+ */
+ if (pixelInBlock == 24)
+ pixelInBlock--;
+
+ const uint8_t *in = static_cast<const uint8_t *>(input)
+ + block * 32 + (pixelInBlock / 4) * 5;
+
+ uint16_t val1, val2, val3, val4;
+ switch (pixelInBlock % 4) {
+ default:
+ case 0:
+ val1 = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
+ val2 = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
+ val3 = (in[stride + 1] & 0x03) << 14 | (in[stride + 0] & 0xff) << 6;
+ val4 = (in[stride + 2] & 0x0f) << 12 | (in[stride + 1] & 0xfc) << 4;
+ break;
+ case 1:
+ val1 = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
+ val2 = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
+ val3 = (in[stride + 2] & 0x0f) << 12 | (in[stride + 1] & 0xfc) << 4;
+ val4 = (in[stride + 3] & 0x3f) << 10 | (in[stride + 2] & 0xf0) << 2;
+ break;
+ case 2:
+ val1 = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
+ val2 = (in[4] & 0xff) << 8 | (in[3] & 0xc0) << 0;
+ val3 = (in[stride + 3] & 0x3f) << 10 | (in[stride + 2] & 0xf0) << 2;
+ val4 = (in[stride + 4] & 0xff) << 8 | (in[stride + 3] & 0xc0) << 0;
+ break;
+ case 3:
+ val1 = (in[4] & 0xff) << 8 | (in[3] & 0xc0) << 0;
+ val2 = (in[6] & 0x03) << 14 | (in[5] & 0xff) << 6;
+ val3 = (in[stride + 4] & 0xff) << 8 | (in[stride + 3] & 0xc0) << 0;
+ val4 = (in[stride + 6] & 0x03) << 14 | (in[stride + 5] & 0xff) << 6;
+ break;
+ }
+
+ uint8_t value = (val1 + val2 + val3 + val4) >> 10;
+ *out++ = value;
+ *out++ = value;
+ *out++ = value;
+ }
+}
+
+static const std::map<PixelFormat, FormatInfo> formatInfo = {
+ { formats::SBGGR8, {
+ .bitsPerSample = 8,
+ .pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
+ .packScanline = packScanlineSBGGR8,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGBRG8, {
+ .bitsPerSample = 8,
+ .pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR8,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGRBG8, {
+ .bitsPerSample = 8,
+ .pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR8,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SRGGB8, {
+ .bitsPerSample = 8,
+ .pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
+ .packScanline = packScanlineSBGGR8,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SBGGR10_CSI2P, {
+ .bitsPerSample = 10,
+ .pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
+ .packScanline = packScanlineSBGGR10P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGBRG10_CSI2P, {
+ .bitsPerSample = 10,
+ .pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR10P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGRBG10_CSI2P, {
+ .bitsPerSample = 10,
+ .pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR10P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SRGGB10_CSI2P, {
+ .bitsPerSample = 10,
+ .pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
+ .packScanline = packScanlineSBGGR10P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SBGGR12_CSI2P, {
+ .bitsPerSample = 12,
+ .pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
+ .packScanline = packScanlineSBGGR12P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGBRG12_CSI2P, {
+ .bitsPerSample = 12,
+ .pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR12P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGRBG12_CSI2P, {
+ .bitsPerSample = 12,
+ .pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR12P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SRGGB12_CSI2P, {
+ .bitsPerSample = 12,
+ .pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
+ .packScanline = packScanlineSBGGR12P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SBGGR10_IPU3, {
+ .bitsPerSample = 16,
+ .pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
+ .packScanline = packScanlineIPU3,
+ .thumbScanline = thumbScanlineIPU3,
+ } },
+ { formats::SGBRG10_IPU3, {
+ .bitsPerSample = 16,
+ .pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
+ .packScanline = packScanlineIPU3,
+ .thumbScanline = thumbScanlineIPU3,
+ } },
+ { formats::SGRBG10_IPU3, {
+ .bitsPerSample = 16,
+ .pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
+ .packScanline = packScanlineIPU3,
+ .thumbScanline = thumbScanlineIPU3,
+ } },
+ { formats::SRGGB10_IPU3, {
+ .bitsPerSample = 16,
+ .pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
+ .packScanline = packScanlineIPU3,
+ .thumbScanline = thumbScanlineIPU3,
+ } },
+};
+
+int DNGWriter::write(const char *filename, const Camera *camera,
+ const StreamConfiguration &config,
+ const ControlList &metadata,
+ [[maybe_unused]] const FrameBuffer *buffer,
+ const void *data)
+{
+ const ControlList &cameraProperties = camera->properties();
+
+ const auto it = formatInfo.find(config.pixelFormat);
+ if (it == formatInfo.cend()) {
+ std::cerr << "Unsupported pixel format" << std::endl;
+ return -EINVAL;
+ }
+ const FormatInfo *info = &it->second;
+
+ TIFF *tif = TIFFOpen(filename, "w");
+ if (!tif) {
+ std::cerr << "Failed to open tiff file" << std::endl;
+ return -EINVAL;
+ }
+
+ /*
+ * Scanline buffer, has to be large enough to store both a RAW scanline
+ * or a thumbnail scanline. The latter will always be much smaller than
+ * the former as we downscale by 16 in both directions.
+ */
+ uint8_t scanline[(config.size.width * info->bitsPerSample + 7) / 8];
+
+ toff_t rawIFDOffset = 0;
+ toff_t exifIFDOffset = 0;
+
+ /*
+ * Start with a thumbnail in IFD 0 for compatibility with TIFF baseline
+ * readers, as required by the TIFF/EP specification. Tags that apply to
+ * the whole file are stored here.
+ */
+ const uint8_t version[] = { 1, 2, 0, 0 };
+
+ TIFFSetField(tif, TIFFTAG_DNGVERSION, version);
+ TIFFSetField(tif, TIFFTAG_DNGBACKWARDVERSION, version);
+ TIFFSetField(tif, TIFFTAG_FILLORDER, FILLORDER_MSB2LSB);
+ TIFFSetField(tif, TIFFTAG_MAKE, "libcamera");
+
+ const auto &model = cameraProperties.get(properties::Model);
+ if (model) {
+ TIFFSetField(tif, TIFFTAG_MODEL, model->c_str());
+ /* \todo set TIFFTAG_UNIQUECAMERAMODEL. */
+ }
+
+ TIFFSetField(tif, TIFFTAG_SOFTWARE, "qcam");
+ TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);
+
+ /*
+ * Thumbnail-specific tags. The thumbnail is stored as an RGB image
+ * with 1/16 of the raw image resolution. Greyscale would save space,
+ * but doesn't seem well supported by RawTherapee.
+ */
+ TIFFSetField(tif, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE);
+ TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, config.size.width / 16);
+ TIFFSetField(tif, TIFFTAG_IMAGELENGTH, config.size.height / 16);
+ TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8);
+ TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
+ TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
+ TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3);
+ TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
+ TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
+
+ /*
+ * Fill in some reasonable colour information in the DNG. We supply
+ * the "neutral" colour values which determine the white balance, and the
+ * "ColorMatrix1" which converts XYZ to (un-white-balanced) camera RGB.
+ * Note that this is not a "proper" colour calibration for the DNG,
+ * nonetheless, many tools should be able to render the colours better.
+ */
+ float neutral[3] = { 1, 1, 1 };
+ Matrix3d wbGain = Matrix3d::identity();
+ /* From http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html */
+ const Matrix3d rgb2xyz(0.4124564, 0.3575761, 0.1804375,
+ 0.2126729, 0.7151522, 0.0721750,
+ 0.0193339, 0.1191920, 0.9503041);
+ Matrix3d ccm = Matrix3d::identity();
+ /*
+ * Pick a reasonable number eps to protect against singularities. It
+ * should be comfortably larger than the point at which we run into
+ * numerical trouble, yet smaller than any plausible gain that we might
+ * apply to a colour, either explicitly or as part of the colour matrix.
+ */
+ const double eps = 1e-2;
+
+ const auto &colourGains = metadata.get(controls::ColourGains);
+ if (colourGains) {
+ if ((*colourGains)[0] > eps && (*colourGains)[1] > eps) {
+ wbGain = Matrix3d::diag((*colourGains)[0], 1, (*colourGains)[1]);
+ neutral[0] = 1.0 / (*colourGains)[0]; /* red */
+ neutral[2] = 1.0 / (*colourGains)[1]; /* blue */
+ }
+ }
+
+ const auto &ccmControl = metadata.get(controls::ColourCorrectionMatrix);
+ if (ccmControl) {
+ Matrix3d ccmSupplied(*ccmControl);
+ if (ccmSupplied.determinant() > eps)
+ ccm = ccmSupplied;
+ }
+
+ /*
+ * rgb2xyz is known to be invertible, and we've ensured above that both
+ * the ccm and wbGain matrices are non-singular, so the product of all
+ * three is guaranteed to be invertible too.
+ */
+ Matrix3d colorMatrix1 = (rgb2xyz * ccm * wbGain).inverse();
+
+ TIFFSetField(tif, TIFFTAG_COLORMATRIX1, 9, colorMatrix1.m);
+ TIFFSetField(tif, TIFFTAG_ASSHOTNEUTRAL, 3, neutral);
+
+ /*
+ * Reserve space for the SubIFD and ExifIFD tags, pointing to the IFD
+ * for the raw image and EXIF data respectively. The real offsets will
+ * be set later.
+ */
+ TIFFSetField(tif, TIFFTAG_SUBIFD, 1, &rawIFDOffset);
+ TIFFSetField(tif, TIFFTAG_EXIFIFD, exifIFDOffset);
+
+ /* Write the thumbnail. */
+ const uint8_t *row = static_cast<const uint8_t *>(data);
+ for (unsigned int y = 0; y < config.size.height / 16; y++) {
+ info->thumbScanline(*info, &scanline, row,
+ config.size.width / 16, config.stride);
+
+ if (TIFFWriteScanline(tif, &scanline, y, 0) != 1) {
+ std::cerr << "Failed to write thumbnail scanline"
+ << std::endl;
+ TIFFClose(tif);
+ return -EINVAL;
+ }
+
+ row += config.stride * 16;
+ }
+
+ TIFFWriteDirectory(tif);
+
+ /* Create a new IFD for the RAW image. */
+ const uint16_t cfaRepeatPatternDim[] = { 2, 2 };
+ const uint8_t cfaPlaneColor[] = {
+ CFAPatternRed,
+ CFAPatternGreen,
+ CFAPatternBlue
+ };
+
+ TIFFSetField(tif, TIFFTAG_SUBFILETYPE, 0);
+ TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, config.size.width);
+ TIFFSetField(tif, TIFFTAG_IMAGELENGTH, config.size.height);
+ TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, info->bitsPerSample);
+ TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
+ TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_CFA);
+ TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1);
+ TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
+ TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
+ TIFFSetField(tif, TIFFTAG_CFAREPEATPATTERNDIM, cfaRepeatPatternDim);
+ if (TIFFLIB_VERSION < 20201219)
+ TIFFSetField(tif, TIFFTAG_CFAPATTERN, info->pattern);
+ else
+ TIFFSetField(tif, TIFFTAG_CFAPATTERN, 4, info->pattern);
+ TIFFSetField(tif, TIFFTAG_CFAPLANECOLOR, 3, cfaPlaneColor);
+ TIFFSetField(tif, TIFFTAG_CFALAYOUT, 1);
+
+ const uint16_t blackLevelRepeatDim[] = { 2, 2 };
+ float blackLevel[] = { 0.0f, 0.0f, 0.0f, 0.0f };
+ uint32_t whiteLevel = (1 << info->bitsPerSample) - 1;
+
+ const auto &blackLevels = metadata.get(controls::SensorBlackLevels);
+ if (blackLevels) {
+ Span<const int32_t, 4> levels = *blackLevels;
+
+ /*
+ * The black levels control is specified in R, Gr, Gb, B order.
+ * Map it to the TIFF tag that is specified in CFA pattern
+ * order.
+ */
+ unsigned int green = (info->pattern[0] == CFAPatternRed ||
+ info->pattern[1] == CFAPatternRed)
+ ? 0 : 1;
+
+ for (unsigned int i = 0; i < 4; ++i) {
+ unsigned int level;
+
+ switch (info->pattern[i]) {
+ case CFAPatternRed:
+ level = levels[0];
+ break;
+ case CFAPatternGreen:
+ level = levels[green + 1];
+ green = (green + 1) % 2;
+ break;
+ case CFAPatternBlue:
+ default:
+ level = levels[3];
+ break;
+ }
+
+ /* Map the 16-bit value to the bits per sample range. */
+ blackLevel[i] = level >> (16 - info->bitsPerSample);
+ }
+ }
+
+ TIFFSetField(tif, TIFFTAG_BLACKLEVELREPEATDIM, &blackLevelRepeatDim);
+ TIFFSetField(tif, TIFFTAG_BLACKLEVEL, 4, &blackLevel);
+ TIFFSetField(tif, TIFFTAG_WHITELEVEL, 1, &whiteLevel);
+
+ /* Write RAW content. */
+ row = static_cast<const uint8_t *>(data);
+ for (unsigned int y = 0; y < config.size.height; y++) {
+ info->packScanline(&scanline, row, config.size.width);
+
+ if (TIFFWriteScanline(tif, &scanline, y, 0) != 1) {
+ std::cerr << "Failed to write RAW scanline"
+ << std::endl;
+ TIFFClose(tif);
+ return -EINVAL;
+ }
+
+ row += config.stride;
+ }
+
+ /* Checkpoint the IFD to retrieve its offset, and write it out. */
+ TIFFCheckpointDirectory(tif);
+ rawIFDOffset = TIFFCurrentDirOffset(tif);
+ TIFFWriteDirectory(tif);
+
+ /* Create a new IFD for the EXIF data and fill it. */
+ TIFFCreateEXIFDirectory(tif);
+
+ /* Store creation time. */
+ time_t rawtime;
+ struct tm *timeinfo;
+ char strTime[20];
+
+ time(&rawtime);
+ timeinfo = localtime(&rawtime);
+ strftime(strTime, 20, "%Y:%m:%d %H:%M:%S", timeinfo);
+
+ /*
+ * \todo Handle timezone information by setting OffsetTimeOriginal and
+ * OffsetTimeDigitized once libtiff catches up to the specification and
+ * has EXIFTAG_ defines to handle them.
+ */
+ TIFFSetField(tif, EXIFTAG_DATETIMEORIGINAL, strTime);
+ TIFFSetField(tif, EXIFTAG_DATETIMEDIGITIZED, strTime);
+
+ const auto &analogGain = metadata.get(controls::AnalogueGain);
+ if (analogGain) {
+ uint16_t iso = std::min(std::max(*analogGain * 100, 0.0f), 65535.0f);
+ TIFFSetField(tif, EXIFTAG_ISOSPEEDRATINGS, 1, &iso);
+ }
+
+ const auto &exposureTime = metadata.get(controls::ExposureTime);
+ if (exposureTime)
+ TIFFSetField(tif, EXIFTAG_EXPOSURETIME, *exposureTime / 1e6);
+
+ TIFFWriteCustomDirectory(tif, &exifIFDOffset);
+
+ /* Update the IFD offsets and close the file. */
+ TIFFSetDirectory(tif, 0);
+ TIFFSetField(tif, TIFFTAG_SUBIFD, 1, &rawIFDOffset);
+ TIFFSetField(tif, TIFFTAG_EXIFIFD, exifIFDOffset);
+ TIFFWriteDirectory(tif);
+
+ TIFFClose(tif);
+
+ return 0;
+}
diff --git a/src/apps/common/dng_writer.h b/src/apps/common/dng_writer.h
new file mode 100644
index 00000000..917713e6
--- /dev/null
+++ b/src/apps/common/dng_writer.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * DNG writer
+ */
+
+#pragma once
+
+#ifdef HAVE_TIFF
+#define HAVE_DNG
+
+#include <libcamera/camera.h>
+#include <libcamera/controls.h>
+#include <libcamera/framebuffer.h>
+#include <libcamera/stream.h>
+
+class DNGWriter
+{
+public:
+ static int write(const char *filename, const libcamera::Camera *camera,
+ const libcamera::StreamConfiguration &config,
+ const libcamera::ControlList &metadata,
+ const libcamera::FrameBuffer *buffer, const void *data);
+};
+
+#endif /* HAVE_TIFF */
diff --git a/src/apps/common/event_loop.cpp b/src/apps/common/event_loop.cpp
new file mode 100644
index 00000000..f7f9afa0
--- /dev/null
+++ b/src/apps/common/event_loop.cpp
@@ -0,0 +1,150 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * cam - Event loop
+ */
+
+#include "event_loop.h"
+
+#include <assert.h>
+#include <event2/event.h>
+#include <event2/thread.h>
+#include <iostream>
+
+EventLoop *EventLoop::instance_ = nullptr;
+
+EventLoop::EventLoop()
+{
+ assert(!instance_);
+
+ evthread_use_pthreads();
+ base_ = event_base_new();
+ instance_ = this;
+}
+
+EventLoop::~EventLoop()
+{
+ instance_ = nullptr;
+
+ events_.clear();
+ event_base_free(base_);
+ libevent_global_shutdown();
+}
+
+EventLoop *EventLoop::instance()
+{
+ return instance_;
+}
+
+int EventLoop::exec()
+{
+ exitCode_ = -1;
+ event_base_loop(base_, EVLOOP_NO_EXIT_ON_EMPTY);
+ return exitCode_;
+}
+
+void EventLoop::exit(int code)
+{
+ exitCode_ = code;
+ event_base_loopbreak(base_);
+}
+
+void EventLoop::callLater(const std::function<void()> &func)
+{
+ {
+ std::unique_lock<std::mutex> locker(lock_);
+ calls_.push_back(func);
+ }
+
+ event_base_once(base_, -1, EV_TIMEOUT, dispatchCallback, this, nullptr);
+}
+
+void EventLoop::addFdEvent(int fd, EventType type,
+ const std::function<void()> &callback)
+{
+ std::unique_ptr<Event> event = std::make_unique<Event>(callback);
+ short events = (type & Read ? EV_READ : 0)
+ | (type & Write ? EV_WRITE : 0)
+ | EV_PERSIST;
+
+ event->event_ = event_new(base_, fd, events, &EventLoop::Event::dispatch,
+ event.get());
+ if (!event->event_) {
+ std::cerr << "Failed to create event for fd " << fd << std::endl;
+ return;
+ }
+
+ int ret = event_add(event->event_, nullptr);
+ if (ret < 0) {
+ std::cerr << "Failed to add event for fd " << fd << std::endl;
+ return;
+ }
+
+ events_.push_back(std::move(event));
+}
+
+void EventLoop::addTimerEvent(const std::chrono::microseconds period,
+ const std::function<void()> &callback)
+{
+ std::unique_ptr<Event> event = std::make_unique<Event>(callback);
+ event->event_ = event_new(base_, -1, EV_PERSIST, &EventLoop::Event::dispatch,
+ event.get());
+ if (!event->event_) {
+ std::cerr << "Failed to create timer event" << std::endl;
+ return;
+ }
+
+ struct timeval tv;
+ tv.tv_sec = period.count() / 1000000ULL;
+ tv.tv_usec = period.count() % 1000000ULL;
+
+ int ret = event_add(event->event_, &tv);
+ if (ret < 0) {
+ std::cerr << "Failed to add timer event" << std::endl;
+ return;
+ }
+
+ events_.push_back(std::move(event));
+}
+
+void EventLoop::dispatchCallback([[maybe_unused]] evutil_socket_t fd,
+ [[maybe_unused]] short flags, void *param)
+{
+ EventLoop *loop = static_cast<EventLoop *>(param);
+ loop->dispatchCall();
+}
+
+void EventLoop::dispatchCall()
+{
+ std::function<void()> call;
+
+ {
+ std::unique_lock<std::mutex> locker(lock_);
+ if (calls_.empty())
+ return;
+
+ call = calls_.front();
+ calls_.pop_front();
+ }
+
+ call();
+}
+
+EventLoop::Event::Event(const std::function<void()> &callback)
+ : callback_(callback), event_(nullptr)
+{
+}
+
+EventLoop::Event::~Event()
+{
+ event_del(event_);
+ event_free(event_);
+}
+
+void EventLoop::Event::dispatch([[maybe_unused]] int fd,
+ [[maybe_unused]] short events, void *arg)
+{
+ Event *event = static_cast<Event *>(arg);
+ event->callback_();
+}
diff --git a/src/apps/common/event_loop.h b/src/apps/common/event_loop.h
new file mode 100644
index 00000000..ef129b9a
--- /dev/null
+++ b/src/apps/common/event_loop.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * cam - Event loop
+ */
+
+#pragma once
+
+#include <chrono>
+#include <functional>
+#include <list>
+#include <memory>
+#include <mutex>
+
+#include <event2/util.h>
+
+struct event_base;
+
+class EventLoop
+{
+public:
+ enum EventType {
+ Read = 1,
+ Write = 2,
+ };
+
+ EventLoop();
+ ~EventLoop();
+
+ static EventLoop *instance();
+
+ int exec();
+ void exit(int code = 0);
+
+ void callLater(const std::function<void()> &func);
+
+ void addFdEvent(int fd, EventType type,
+ const std::function<void()> &handler);
+
+ using duration = std::chrono::steady_clock::duration;
+ void addTimerEvent(const std::chrono::microseconds period,
+ const std::function<void()> &handler);
+
+private:
+ struct Event {
+ Event(const std::function<void()> &callback);
+ ~Event();
+
+ static void dispatch(int fd, short events, void *arg);
+
+ std::function<void()> callback_;
+ struct event *event_;
+ };
+
+ static EventLoop *instance_;
+
+ struct event_base *base_;
+ int exitCode_;
+
+ std::list<std::function<void()>> calls_;
+ std::list<std::unique_ptr<Event>> events_;
+ std::mutex lock_;
+
+ static void dispatchCallback(evutil_socket_t fd, short flags,
+ void *param);
+ void dispatchCall();
+};
diff --git a/src/apps/common/image.cpp b/src/apps/common/image.cpp
new file mode 100644
index 00000000..a2a0f58f
--- /dev/null
+++ b/src/apps/common/image.cpp
@@ -0,0 +1,109 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2021, Ideas on Board Oy
+ *
+ * Multi-planar image with access to pixel data
+ */
+
+#include "image.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <iostream>
+#include <map>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+using namespace libcamera;
+
+std::unique_ptr<Image> Image::fromFrameBuffer(const FrameBuffer *buffer, MapMode mode)
+{
+ std::unique_ptr<Image> image{ new Image() };
+
+ assert(!buffer->planes().empty());
+
+ int mmapFlags = 0;
+
+ if (mode & MapMode::ReadOnly)
+ mmapFlags |= PROT_READ;
+
+ if (mode & MapMode::WriteOnly)
+ mmapFlags |= PROT_WRITE;
+
+ struct MappedBufferInfo {
+ uint8_t *address = nullptr;
+ size_t mapLength = 0;
+ size_t dmabufLength = 0;
+ };
+ std::map<int, MappedBufferInfo> mappedBuffers;
+
+ for (const FrameBuffer::Plane &plane : buffer->planes()) {
+ const int fd = plane.fd.get();
+ if (mappedBuffers.find(fd) == mappedBuffers.end()) {
+ const size_t length = lseek(fd, 0, SEEK_END);
+ mappedBuffers[fd] = MappedBufferInfo{ nullptr, 0, length };
+ }
+
+ const size_t length = mappedBuffers[fd].dmabufLength;
+
+ if (plane.offset > length ||
+ plane.offset + plane.length > length) {
+ std::cerr << "plane is out of buffer: buffer length="
+ << length << ", plane offset=" << plane.offset
+ << ", plane length=" << plane.length
+ << std::endl;
+ return nullptr;
+ }
+ size_t &mapLength = mappedBuffers[fd].mapLength;
+ mapLength = std::max(mapLength,
+ static_cast<size_t>(plane.offset + plane.length));
+ }
+
+ for (const FrameBuffer::Plane &plane : buffer->planes()) {
+ const int fd = plane.fd.get();
+ auto &info = mappedBuffers[fd];
+ if (!info.address) {
+ void *address = mmap(nullptr, info.mapLength, mmapFlags,
+ MAP_SHARED, fd, 0);
+ if (address == MAP_FAILED) {
+ int error = -errno;
+ std::cerr << "Failed to mmap plane: "
+ << strerror(-error) << std::endl;
+ return nullptr;
+ }
+
+ info.address = static_cast<uint8_t *>(address);
+ image->maps_.emplace_back(info.address, info.mapLength);
+ }
+
+ image->planes_.emplace_back(info.address + plane.offset, plane.length);
+ }
+
+ return image;
+}
+
+Image::Image() = default;
+
+Image::~Image()
+{
+ for (Span<uint8_t> &map : maps_)
+ munmap(map.data(), map.size());
+}
+
+unsigned int Image::numPlanes() const
+{
+ return planes_.size();
+}
+
+Span<uint8_t> Image::data(unsigned int plane)
+{
+ assert(plane <= planes_.size());
+ return planes_[plane];
+}
+
+Span<const uint8_t> Image::data(unsigned int plane) const
+{
+ assert(plane <= planes_.size());
+ return planes_[plane];
+}
diff --git a/src/apps/common/image.h b/src/apps/common/image.h
new file mode 100644
index 00000000..e47e446b
--- /dev/null
+++ b/src/apps/common/image.h
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2021, Ideas on Board Oy
+ *
+ * Multi-planar image with access to pixel data
+ */
+
+#pragma once
+
+#include <memory>
+#include <stdint.h>
+#include <vector>
+
+#include <libcamera/base/class.h>
+#include <libcamera/base/flags.h>
+#include <libcamera/base/span.h>
+
+#include <libcamera/framebuffer.h>
+
+class Image
+{
+public:
+ enum class MapMode {
+ ReadOnly = 1 << 0,
+ WriteOnly = 1 << 1,
+ ReadWrite = ReadOnly | WriteOnly,
+ };
+
+ static std::unique_ptr<Image> fromFrameBuffer(const libcamera::FrameBuffer *buffer,
+ MapMode mode);
+
+ ~Image();
+
+ unsigned int numPlanes() const;
+
+ libcamera::Span<uint8_t> data(unsigned int plane);
+ libcamera::Span<const uint8_t> data(unsigned int plane) const;
+
+private:
+ LIBCAMERA_DISABLE_COPY(Image)
+
+ Image();
+
+ std::vector<libcamera::Span<uint8_t>> maps_;
+ std::vector<libcamera::Span<uint8_t>> planes_;
+};
+
+namespace libcamera {
+LIBCAMERA_FLAGS_ENABLE_OPERATORS(Image::MapMode)
+}
diff --git a/src/apps/common/meson.build b/src/apps/common/meson.build
new file mode 100644
index 00000000..5b683390
--- /dev/null
+++ b/src/apps/common/meson.build
@@ -0,0 +1,27 @@
+# SPDX-License-Identifier: CC0-1.0
+
+apps_sources = files([
+ 'image.cpp',
+ 'options.cpp',
+ 'ppm_writer.cpp',
+ 'stream_options.cpp',
+])
+
+apps_cpp_args = []
+
+if libevent.found()
+ apps_sources += files([
+ 'event_loop.cpp',
+ ])
+endif
+
+if libtiff.found()
+ apps_cpp_args += ['-DHAVE_TIFF']
+ apps_sources += files([
+ 'dng_writer.cpp',
+ ])
+endif
+
+apps_lib = static_library('apps', apps_sources,
+ cpp_args : apps_cpp_args,
+ dependencies : [libcamera_public])
diff --git a/src/apps/common/options.cpp b/src/apps/common/options.cpp
new file mode 100644
index 00000000..ab19aa3d
--- /dev/null
+++ b/src/apps/common/options.cpp
@@ -0,0 +1,1141 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * cam - Options parsing
+ */
+
+#include <assert.h>
+#include <getopt.h>
+#include <iomanip>
+#include <iostream>
+#include <string.h>
+
+#include "options.h"
+
+/**
+ * \enum OptionArgument
+ * \brief Indicate if an option takes an argument
+ *
+ * \var OptionArgument::ArgumentNone
+ * \brief The option doesn't accept any argument
+ *
+ * \var OptionArgument::ArgumentRequired
+ * \brief The option requires an argument
+ *
+ * \var OptionArgument::ArgumentOptional
+ * \brief The option accepts an optional argument
+ */
+
+/**
+ * \enum OptionType
+ * \brief The type of argument for an option
+ *
+ * \var OptionType::OptionNone
+ * \brief No argument type, used for options that take no argument
+ *
+ * \var OptionType::OptionInteger
+ * \brief Integer argument type, with an optional base prefix (`0` for base 8,
+ * `0x` for base 16, none for base 10)
+ *
+ * \var OptionType::OptionString
+ * \brief String argument
+ *
+ * \var OptionType::OptionKeyValue
+ * \brief key=value list argument
+ */
+
+/* -----------------------------------------------------------------------------
+ * Option
+ */
+
+/**
+ * \struct Option
+ * \brief Store metadata about an option
+ *
+ * \var Option::opt
+ * \brief The option identifier
+ *
+ * \var Option::type
+ * \brief The type of the option argument
+ *
+ * \var Option::name
+ * \brief The option name
+ *
+ * \var Option::argument
+ * \brief Whether the option accepts an optional argument, a mandatory
+ * argument, or no argument at all
+ *
+ * \var Option::argumentName
+ * \brief The argument name used in the help text
+ *
+ * \var Option::help
+ * \brief The help text (may be a multi-line string)
+ *
+ * \var Option::keyValueParser
+ * \brief For options of type OptionType::OptionKeyValue, the key-value parser
+ * to parse the argument
+ *
+ * \var Option::isArray
+ * \brief Whether the option can appear once or multiple times
+ *
+ * \var Option::parent
+ * \brief The parent option
+ *
+ * \var Option::children
+ * \brief List of child options, storing all options whose parent is this option
+ *
+ * \fn Option::hasShortOption()
+ * \brief Tell if the option has a short option specifier (e.g. `-f`)
+ * \return True if the option has a short option specifier, false otherwise
+ *
+ * \fn Option::hasLongOption()
+ * \brief Tell if the option has a long option specifier (e.g. `--foo`)
+ * \return True if the option has a long option specifier, false otherwise
+ */
+struct Option {
+ int opt;
+ OptionType type;
+ const char *name;
+ OptionArgument argument;
+ const char *argumentName;
+ const char *help;
+ KeyValueParser *keyValueParser;
+ bool isArray;
+ Option *parent;
+ std::list<Option> children;
+
+ bool hasShortOption() const { return isalnum(opt); }
+ bool hasLongOption() const { return name != nullptr; }
+ const char *typeName() const;
+ std::string optionName() const;
+};
+
+/**
+ * \brief Retrieve a string describing the option type
+ * \return A string describing the option type
+ */
+const char *Option::typeName() const
+{
+ switch (type) {
+ case OptionNone:
+ return "none";
+
+ case OptionInteger:
+ return "integer";
+
+ case OptionString:
+ return "string";
+
+ case OptionKeyValue:
+ return "key=value";
+ }
+
+ return "unknown";
+}
+
+/**
+ * \brief Retrieve a string describing the option name, with leading dashes
+ * \return A string describing the option name, as a long option identifier
+ * (double dash) if the option has a name, or a short option identifier (single
+ * dash) otherwise
+ */
+std::string Option::optionName() const
+{
+ if (name)
+ return "--" + std::string(name);
+ else
+ return "-" + std::string(1, opt);
+}
+
+/* -----------------------------------------------------------------------------
+ * OptionBase<T>
+ */
+
+/**
+ * \class template<typename T> OptionBase
+ * \brief Container to store the values of parsed options
+ * \tparam T The type through which options are identified
+ *
+ * The OptionsBase class is generated by a parser (either OptionsParser or
+ * KeyValueParser) when parsing options. It stores values for all the options
+ * found, and exposes accessor functions to retrieve them. The options are
+ * accessed through an identifier to type \a T, which is an int referencing an
+ * Option::opt for OptionsParser, or a std::string referencing an Option::name
+ * for KeyValueParser.
+ */
+
+/**
+ * \fn OptionsBase::OptionsBase()
+ * \brief Construct an OptionsBase instance
+ *
+ * The constructed instance is initially invalid, and will be populated by the
+ * options parser.
+ */
+
+/**
+ * \brief Tell if the stored options list is empty
+ * \return True if the container is empty, false otherwise
+ */
+template<typename T>
+bool OptionsBase<T>::empty() const
+{
+ return values_.empty();
+}
+
+/**
+ * \brief Tell if the options parsing completed successfully
+ * \return True if the container is returned after successfully parsing
+ * options, false if it is returned after an error was detected during parsing
+ */
+template<typename T>
+bool OptionsBase<T>::valid() const
+{
+ return valid_;
+}
+
+/**
+ * \brief Tell if the option \a opt is specified
+ * \param[in] opt The option to search for
+ * \return True if the \a opt option is set, false otherwise
+ */
+template<typename T>
+bool OptionsBase<T>::isSet(const T &opt) const
+{
+ return values_.find(opt) != values_.end();
+}
+
+/**
+ * \brief Retrieve the value of option \a opt
+ * \param[in] opt The option to retrieve
+ * \return The value of option \a opt if found, an empty OptionValue otherwise
+ */
+template<typename T>
+const OptionValue &OptionsBase<T>::operator[](const T &opt) const
+{
+ static const OptionValue empty;
+
+ auto it = values_.find(opt);
+ if (it != values_.end())
+ return it->second;
+ return empty;
+}
+
+/**
+ * \brief Mark the container as invalid
+ *
+ * This function can be used in a key-value parser's override of the
+ * KeyValueParser::parse() function to mark the returned options as invalid if
+ * a validation error occurs.
+ */
+template<typename T>
+void OptionsBase<T>::invalidate()
+{
+ valid_ = false;
+}
+
+template<typename T>
+bool OptionsBase<T>::parseValue(const T &opt, const Option &option,
+ const char *arg)
+{
+ OptionValue value;
+
+ switch (option.type) {
+ case OptionNone:
+ break;
+
+ case OptionInteger:
+ unsigned int integer;
+
+ if (arg) {
+ char *endptr;
+ integer = strtoul(arg, &endptr, 0);
+ if (*endptr != '\0')
+ return false;
+ } else {
+ integer = 0;
+ }
+
+ value = OptionValue(integer);
+ break;
+
+ case OptionString:
+ value = OptionValue(arg ? arg : "");
+ break;
+
+ case OptionKeyValue:
+ KeyValueParser *kvParser = option.keyValueParser;
+ KeyValueParser::Options keyValues = kvParser->parse(arg);
+ if (!keyValues.valid())
+ return false;
+
+ value = OptionValue(keyValues);
+ break;
+ }
+
+ if (option.isArray)
+ values_[opt].addValue(value);
+ else
+ values_[opt] = value;
+
+ return true;
+}
+
+template class OptionsBase<int>;
+template class OptionsBase<std::string>;
+
+/* -----------------------------------------------------------------------------
+ * KeyValueParser
+ */
+
+/**
+ * \class KeyValueParser
+ * \brief A specialized parser for list of key-value pairs
+ *
+ * The KeyValueParser is an options parser for comma-separated lists of
+ * `key=value` pairs. The supported keys are added to the parser with
+ * addOption(). A given key can only appear once in the parsed list.
+ *
+ * Instances of this class can be passed to the OptionsParser::addOption()
+ * function to create options that take key-value pairs as an option argument.
+ * Specialized versions of the key-value parser can be created by inheriting
+ * from this class, to pre-build the options list in the constructor, and to add
+ * custom validation by overriding the parse() function.
+ */
+
+/**
+ * \class KeyValueParser::Options
+ * \brief An option list generated by the key-value parser
+ *
+ * This is a specialization of OptionsBase with the option reference type set to
+ * std::string.
+ */
+
+KeyValueParser::KeyValueParser() = default;
+KeyValueParser::~KeyValueParser() = default;
+
+/**
+ * \brief Add a supported option to the parser
+ * \param[in] name The option name, corresponding to the key name in the
+ * key=value pair. The name shall be unique.
+ * \param[in] type The type of the value in the key=value pair
+ * \param[in] help The help text
+ * \param[in] argument Whether the value is optional, mandatory or not allowed.
+ * Shall be ArgumentNone if \a type is OptionNone.
+ *
+ * \sa OptionsParser
+ *
+ * \return True if the option was added successfully, false if an error
+ * occurred.
+ */
+bool KeyValueParser::addOption(const char *name, OptionType type,
+ const char *help, OptionArgument argument)
+{
+ if (!name)
+ return false;
+ if (!help || help[0] == '\0')
+ return false;
+ if (argument != ArgumentNone && type == OptionNone)
+ return false;
+
+ /* Reject duplicate options. */
+ if (optionsMap_.find(name) != optionsMap_.end())
+ return false;
+
+ optionsMap_[name] = Option({ 0, type, name, argument, nullptr,
+ help, nullptr, false, nullptr, {} });
+ return true;
+}
+
+/**
+ * \brief Parse a string containing a list of key-value pairs
+ * \param[in] arguments The key-value pairs string to parse
+ *
+ * If a parsing error occurs, the parsing stops and the function returns an
+ * invalid container. The container is populated with the options successfully
+ * parsed so far.
+ *
+ * \return A valid container with the list of parsed options on success, or an
+ * invalid container otherwise
+ */
+KeyValueParser::Options KeyValueParser::parse(const char *arguments)
+{
+ Options options;
+
+ for (const char *pair = arguments; *arguments != '\0'; pair = arguments) {
+ const char *comma = strchrnul(arguments, ',');
+ size_t len = comma - pair;
+
+ /* Skip over the comma. */
+ arguments = *comma == ',' ? comma + 1 : comma;
+
+ /* Skip to the next pair if the pair is empty. */
+ if (!len)
+ continue;
+
+ std::string key;
+ std::string value;
+
+ const char *separator = static_cast<const char *>(memchr(pair, '=', len));
+ if (!separator) {
+ key = std::string(pair, len);
+ value = "";
+ } else {
+ key = std::string(pair, separator - pair);
+ value = std::string(separator + 1, comma - separator - 1);
+ }
+
+ /* The key is mandatory, the value might be optional. */
+ if (key.empty())
+ continue;
+
+ if (optionsMap_.find(key) == optionsMap_.end()) {
+ std::cerr << "Invalid option " << key << std::endl;
+ return options;
+ }
+
+ OptionArgument arg = optionsMap_[key].argument;
+ if (value.empty() && arg == ArgumentRequired) {
+ std::cerr << "Option " << key << " requires an argument"
+ << std::endl;
+ return options;
+ } else if (!value.empty() && arg == ArgumentNone) {
+ std::cerr << "Option " << key << " takes no argument"
+ << std::endl;
+ return options;
+ }
+
+ const Option &option = optionsMap_[key];
+ if (!options.parseValue(key, option, value.c_str())) {
+ std::cerr << "Failed to parse '" << value << "' as "
+ << option.typeName() << " for option " << key
+ << std::endl;
+ return options;
+ }
+ }
+
+ options.valid_ = true;
+ return options;
+}
+
+unsigned int KeyValueParser::maxOptionLength() const
+{
+ unsigned int maxLength = 0;
+
+ for (auto const &iter : optionsMap_) {
+ const Option &option = iter.second;
+ unsigned int length = 10 + strlen(option.name);
+ if (option.argument != ArgumentNone)
+ length += 1 + strlen(option.typeName());
+ if (option.argument == ArgumentOptional)
+ length += 2;
+
+ if (length > maxLength)
+ maxLength = length;
+ }
+
+ return maxLength;
+}
+
+void KeyValueParser::usage(int indent)
+{
+ for (auto const &iter : optionsMap_) {
+ const Option &option = iter.second;
+ std::string argument = std::string(" ") + option.name;
+
+ if (option.argument != ArgumentNone) {
+ if (option.argument == ArgumentOptional)
+ argument += "[=";
+ else
+ argument += "=";
+ argument += option.typeName();
+ if (option.argument == ArgumentOptional)
+ argument += "]";
+ }
+
+ std::cerr << std::setw(indent) << argument;
+
+ for (const char *help = option.help, *end = help; end;) {
+ end = strchr(help, '\n');
+ if (end) {
+ std::cerr << std::string(help, end - help + 1);
+ std::cerr << std::setw(indent) << " ";
+ help = end + 1;
+ } else {
+ std::cerr << help << std::endl;
+ }
+ }
+ }
+}
+
+/* -----------------------------------------------------------------------------
+ * OptionValue
+ */
+
+/**
+ * \class OptionValue
+ * \brief Container to store the value of an option
+ *
+ * The OptionValue class is a variant-type container to store the value of an
+ * option. It supports empty values, integers, strings, key-value lists, as well
+ * as arrays of those types. For array values, all array elements shall have the
+ * same type.
+ *
+ * OptionValue instances are organized in a tree-based structure that matches
+ * the parent-child relationship of the options added to the parser. Children
+ * are retrieved with the children() function, and are stored as an
+ * OptionsBase<int>.
+ */
+
+/**
+ * \enum OptionValue::ValueType
+ * \brief The option value type
+ *
+ * \var OptionValue::ValueType::ValueNone
+ * \brief Empty value
+ *
+ * \var OptionValue::ValueType::ValueInteger
+ * \brief Integer value (int)
+ *
+ * \var OptionValue::ValueType::ValueString
+ * \brief String value (std::string)
+ *
+ * \var OptionValue::ValueType::ValueKeyValue
+ * \brief Key-value list value (KeyValueParser::Options)
+ *
+ * \var OptionValue::ValueType::ValueArray
+ * \brief Array value
+ */
+
+/**
+ * \brief Construct an empty OptionValue instance
+ *
+ * The value type is set to ValueType::ValueNone.
+ */
+OptionValue::OptionValue()
+ : type_(ValueNone), integer_(0)
+{
+}
+
+/**
+ * \brief Construct an integer OptionValue instance
+ * \param[in] value The integer value
+ *
+ * The value type is set to ValueType::ValueInteger.
+ */
+OptionValue::OptionValue(int value)
+ : type_(ValueInteger), integer_(value)
+{
+}
+
+/**
+ * \brief Construct a string OptionValue instance
+ * \param[in] value The string value
+ *
+ * The value type is set to ValueType::ValueString.
+ */
+OptionValue::OptionValue(const char *value)
+ : type_(ValueString), integer_(0), string_(value)
+{
+}
+
+/**
+ * \brief Construct a string OptionValue instance
+ * \param[in] value The string value
+ *
+ * The value type is set to ValueType::ValueString.
+ */
+OptionValue::OptionValue(const std::string &value)
+ : type_(ValueString), integer_(0), string_(value)
+{
+}
+
+/**
+ * \brief Construct a key-value OptionValue instance
+ * \param[in] value The key-value list
+ *
+ * The value type is set to ValueType::ValueKeyValue.
+ */
+OptionValue::OptionValue(const KeyValueParser::Options &value)
+ : type_(ValueKeyValue), integer_(0), keyValues_(value)
+{
+}
+
+/**
+ * \brief Add an entry to an array value
+ * \param[in] value The entry value
+ *
+ * This function can only be called if the OptionValue type is
+ * ValueType::ValueNone or ValueType::ValueArray. Upon return, the type will be
+ * set to ValueType::ValueArray.
+ */
+void OptionValue::addValue(const OptionValue &value)
+{
+ assert(type_ == ValueNone || type_ == ValueArray);
+
+ type_ = ValueArray;
+ array_.push_back(value);
+}
+
+/**
+ * \fn OptionValue::type()
+ * \brief Retrieve the value type
+ * \return The value type
+ */
+
+/**
+ * \fn OptionValue::empty()
+ * \brief Check if the value is empty
+ * \return True if the value is empty (type set to ValueType::ValueNone), or
+ * false otherwise
+ */
+
+/**
+ * \brief Cast the value to an int
+ * \return The option value as an int, or 0 if the value type isn't
+ * ValueType::ValueInteger
+ */
+OptionValue::operator int() const
+{
+ return toInteger();
+}
+
+/**
+ * \brief Cast the value to a std::string
+ * \return The option value as an std::string, or an empty string if the value
+ * type isn't ValueType::ValueString
+ */
+OptionValue::operator std::string() const
+{
+ return toString();
+}
+
+/**
+ * \brief Retrieve the value as an int
+ * \return The option value as an int, or 0 if the value type isn't
+ * ValueType::ValueInteger
+ */
+int OptionValue::toInteger() const
+{
+ if (type_ != ValueInteger)
+ return 0;
+
+ return integer_;
+}
+
+/**
+ * \brief Retrieve the value as a std::string
+ * \return The option value as a std::string, or an empty string if the value
+ * type isn't ValueType::ValueString
+ */
+std::string OptionValue::toString() const
+{
+ if (type_ != ValueString)
+ return std::string();
+
+ return string_;
+}
+
+/**
+ * \brief Retrieve the value as a key-value list
+ *
+ * The behaviour is undefined if the value type isn't ValueType::ValueKeyValue.
+ *
+ * \return The option value as a KeyValueParser::Options
+ */
+const KeyValueParser::Options &OptionValue::toKeyValues() const
+{
+ assert(type_ == ValueKeyValue);
+ return keyValues_;
+}
+
+/**
+ * \brief Retrieve the value as an array
+ *
+ * The behaviour is undefined if the value type isn't ValueType::ValueArray.
+ *
+ * \return The option value as a std::vector of OptionValue
+ */
+const std::vector<OptionValue> &OptionValue::toArray() const
+{
+ assert(type_ == ValueArray);
+ return array_;
+}
+
+/**
+ * \brief Retrieve the list of child values
+ * \return The list of child values
+ */
+const OptionsParser::Options &OptionValue::children() const
+{
+ return children_;
+}
+
+/* -----------------------------------------------------------------------------
+ * OptionsParser
+ */
+
+/**
+ * \class OptionsParser
+ * \brief A command line options parser
+ *
+ * The OptionsParser class is an easy to use options parser for POSIX-style
+ * command line options. Supports short (e.g. `-f`) and long (e.g. `--foo`)
+ * options, optional and mandatory arguments, automatic parsing arguments for
+ * integer types and comma-separated list of key=value pairs, and multi-value
+ * arguments. It handles help text generation automatically.
+ *
+ * An OptionsParser instance is initialized by adding supported options with
+ * addOption(). Options are specified by an identifier and a name. If the
+ * identifier is an alphanumeric character, it will be used by the parser as a
+ * short option identifier (e.g. `-f`). The name, if specified, will be used as
+ * a long option identifier (e.g. `--foo`). It should not include the double
+ * dashes. The name is optional if the option identifier is an alphanumeric
+ * character and mandatory otherwise.
+ *
+ * An option has a mandatory help text, which is used to print the full options
+ * list with the usage() function. The help text may be a multi-line string.
+ * Correct indentation of the help text is handled automatically.
+ *
+ * Options accept arguments when created with OptionArgument::ArgumentRequired
+ * or OptionArgument::ArgumentOptional. If the argument is required, it can be
+ * specified as a positional argument after the option (e.g. `-f bar`,
+ * `--foo bar`), collated with the short option (e.g. `-fbar`) or separated from
+ * the long option by an equal sign (e.g. `--foo=bar`'). When the argument is
+ * optional, it must be collated with the short option or separated from the
+ * long option by an equal sign.
+ *
+ * If an option has a required or optional argument, an argument name must be
+ * set when adding the option. The argument name is used in the help text as a
+ * place holder for an argument value. For instance, a `--write` option that
+ * takes a file name as an argument could set the argument name to `filename`,
+ * and the help text would display `--write filename`. This is only used to
+ * clarify the help text and has no effect on option parsing.
+ *
+ * The option type tells the parser how to process the argument. Arguments for
+ * string options (OptionType::OptionString) are stored as-is without any
+ * processing. Arguments for integer options (OptionType::OptionInteger) are
+ * converted to an integer value, using an optional base prefix (`0` for base 8,
+ * `0x` for base 16, none for base 10). Arguments for key-value options are
+ * parsed by a KeyValueParser given to addOption().
+ *
+ * By default, a given option can appear once only in the parsed command line.
+ * If the option is created as an array option, the parser will accept multiple
+ * instances of the option. The order in which identical options are specified
+ * is preserved in the values of an array option.
+ *
+ * After preparing the parser, it can be used any number of times to parse
+ * command line options with the parse() function. The function returns an
+ * Options instance that stores the values for the parsed options. The
+ * Options::isSet() function can be used to test if an option has been found,
+ * and is the only way to access options that take no argument (specified by
+ * OptionType::OptionNone and OptionArgument::ArgumentNone). For options that
+ * accept an argument, the option value can be access by Options::operator[]()
+ * using the option identifier as the key. The order in which different options
+ * are specified on the command line isn't preserved.
+ *
+ * Options can be created with parent-child relationships to organize them as a
+ * tree instead of a flat list. When parsing a command line, the child options
+ * are considered related to the parent option that precedes them. This is
+ * useful when the parent is an array option. The Options values list generated
+ * by the parser then turns into a tree, which each parent value storing the
+ * values of child options that follow that instance of the parent option.
+ * For instance, with a `capture` option specified as a child of a `camera`
+ * array option, parsing the command line
+ *
+ * `--camera 1 --capture=10 --camera 2 --capture=20`
+ *
+ * will return an Options instance containing a single OptionValue instance of
+ * array type, for the `camera` option. The OptionValue will contain two
+ * entries, with the first entry containing the integer value 1 and the second
+ * entry the integer value 2. Each of those entries will in turn store an
+ * Options instance that contains the respective children. The first entry will
+ * store in its children a `capture` option of value 10, and the second entry a
+ * `capture` option of value 20.
+ *
+ * The command line
+ *
+ * `--capture=10 --camera 1`
+ *
+ * would result in a parsing error, as the `capture` option has no preceding
+ * `camera` option on the command line.
+ */
+
+/**
+ * \class OptionsParser::Options
+ * \brief An option list generated by the options parser
+ *
+ * This is a specialization of OptionsBase with the option reference type set to
+ * int.
+ */
+
+OptionsParser::OptionsParser() = default;
+OptionsParser::~OptionsParser() = default;
+
+/**
+ * \brief Add an option to the parser
+ * \param[in] opt The option identifier
+ * \param[in] type The type of the option argument
+ * \param[in] help The help text (may be a multi-line string)
+ * \param[in] name The option name
+ * \param[in] argument Whether the option accepts an optional argument, a
+ * mandatory argument, or no argument at all
+ * \param[in] argumentName The argument name used in the help text
+ * \param[in] array Whether the option can appear once or multiple times
+ * \param[in] parent The identifier of the parent option (optional)
+ *
+ * \return True if the option was added successfully, false if an error
+ * occurred.
+ */
+bool OptionsParser::addOption(int opt, OptionType type, const char *help,
+ const char *name, OptionArgument argument,
+ const char *argumentName, bool array, int parent)
+{
+ /*
+ * Options must have at least a short or long name, and a text message.
+ * If an argument is accepted, it must be described by argumentName.
+ */
+ if (!isalnum(opt) && !name)
+ return false;
+ if (!help || help[0] == '\0')
+ return false;
+ if (argument != ArgumentNone && !argumentName)
+ return false;
+
+ /* Reject duplicate options. */
+ if (optionsMap_.find(opt) != optionsMap_.end())
+ return false;
+
+ /*
+ * If a parent is specified, create the option as a child of its parent.
+ * Otherwise, create it in the parser's options list.
+ */
+ Option *option;
+
+ if (parent) {
+ auto iter = optionsMap_.find(parent);
+ if (iter == optionsMap_.end())
+ return false;
+
+ Option *parentOpt = iter->second;
+ parentOpt->children.push_back({
+ opt, type, name, argument, argumentName, help, nullptr,
+ array, parentOpt, {}
+ });
+ option = &parentOpt->children.back();
+ } else {
+ options_.push_back({ opt, type, name, argument, argumentName,
+ help, nullptr, array, nullptr, {} });
+ option = &options_.back();
+ }
+
+ optionsMap_[opt] = option;
+
+ return true;
+}
+
+/**
+ * \brief Add a key-value pair option to the parser
+ * \param[in] opt The option identifier
+ * \param[in] parser The KeyValueParser for the option value
+ * \param[in] help The help text (may be a multi-line string)
+ * \param[in] name The option name
+ * \param[in] array Whether the option can appear once or multiple times
+ *
+ * \sa Option
+ *
+ * \return True if the option was added successfully, false if an error
+ * occurred.
+ */
+bool OptionsParser::addOption(int opt, KeyValueParser *parser, const char *help,
+ const char *name, bool array, int parent)
+{
+ if (!addOption(opt, OptionKeyValue, help, name, ArgumentRequired,
+ "key=value[,key=value,...]", array, parent))
+ return false;
+
+ optionsMap_[opt]->keyValueParser = parser;
+ return true;
+}
+
+/**
+ * \brief Parse command line arguments
+ * \param[in] argc The number of arguments in the \a argv array
+ * \param[in] argv The array of arguments
+ *
+ * If a parsing error occurs, the parsing stops, the function prints an error
+ * message that identifies the invalid argument, prints usage information with
+ * usage(), and returns an invalid container. The container is populated with
+ * the options successfully parsed so far.
+ *
+ * \return A valid container with the list of parsed options on success, or an
+ * invalid container otherwise
+ */
+OptionsParser::Options OptionsParser::parse(int argc, char **argv)
+{
+ OptionsParser::Options options;
+
+ /*
+ * Allocate short and long options arrays large enough to contain all
+ * options.
+ */
+ char shortOptions[optionsMap_.size() * 3 + 2];
+ struct option longOptions[optionsMap_.size() + 1];
+ unsigned int ids = 0;
+ unsigned int idl = 0;
+
+ shortOptions[ids++] = ':';
+
+ for (const auto [opt, option] : optionsMap_) {
+ if (option->hasShortOption()) {
+ shortOptions[ids++] = opt;
+ if (option->argument != ArgumentNone)
+ shortOptions[ids++] = ':';
+ if (option->argument == ArgumentOptional)
+ shortOptions[ids++] = ':';
+ }
+
+ if (option->hasLongOption()) {
+ longOptions[idl].name = option->name;
+
+ switch (option->argument) {
+ case ArgumentNone:
+ longOptions[idl].has_arg = no_argument;
+ break;
+ case ArgumentRequired:
+ longOptions[idl].has_arg = required_argument;
+ break;
+ case ArgumentOptional:
+ longOptions[idl].has_arg = optional_argument;
+ break;
+ }
+
+ longOptions[idl].flag = 0;
+ longOptions[idl].val = option->opt;
+ idl++;
+ }
+ }
+
+ shortOptions[ids] = '\0';
+ memset(&longOptions[idl], 0, sizeof(longOptions[idl]));
+
+ opterr = 0;
+
+ while (true) {
+ int c = getopt_long(argc, argv, shortOptions, longOptions, nullptr);
+
+ if (c == -1)
+ break;
+
+ if (c == '?' || c == ':') {
+ if (c == '?')
+ std::cerr << "Invalid option ";
+ else
+ std::cerr << "Missing argument for option ";
+ std::cerr << argv[optind - 1] << std::endl;
+
+ usage();
+ return options;
+ }
+
+ const Option &option = *optionsMap_[c];
+ if (!parseValue(option, optarg, &options)) {
+ usage();
+ return options;
+ }
+ }
+
+ if (optind < argc) {
+ std::cerr << "Invalid non-option argument '" << argv[optind]
+ << "'" << std::endl;
+ usage();
+ return options;
+ }
+
+ options.valid_ = true;
+ return options;
+}
+
+/**
+ * \brief Print usage text to std::cerr
+ *
+ * The usage text list all the supported option with their arguments. It is
+ * generated automatically from the options added to the parser. Caller of this
+ * function may print additional usage information for the application before
+ * the list of options.
+ */
+void OptionsParser::usage()
+{
+ unsigned int indent = 0;
+
+ for (const auto &opt : optionsMap_) {
+ const Option *option = opt.second;
+ unsigned int length = 14;
+ if (option->hasLongOption())
+ length += 2 + strlen(option->name);
+ if (option->argument != ArgumentNone)
+ length += 1 + strlen(option->argumentName);
+ if (option->argument == ArgumentOptional)
+ length += 2;
+ if (option->isArray)
+ length += 4;
+
+ if (length > indent)
+ indent = length;
+
+ if (option->keyValueParser) {
+ length = option->keyValueParser->maxOptionLength();
+ if (length > indent)
+ indent = length;
+ }
+ }
+
+ indent = (indent + 7) / 8 * 8;
+
+ std::cerr << "Options:" << std::endl;
+
+ std::ios_base::fmtflags f(std::cerr.flags());
+ std::cerr << std::left;
+
+ usageOptions(options_, indent);
+
+ std::cerr.flags(f);
+}
+
+void OptionsParser::usageOptions(const std::list<Option> &options,
+ unsigned int indent)
+{
+ std::vector<const Option *> parentOptions;
+
+ for (const Option &option : options) {
+ std::string argument;
+ if (option.hasShortOption())
+ argument = std::string(" -")
+ + static_cast<char>(option.opt);
+ else
+ argument = " ";
+
+ if (option.hasLongOption()) {
+ if (option.hasShortOption())
+ argument += ", ";
+ else
+ argument += " ";
+ argument += std::string("--") + option.name;
+ }
+
+ if (option.argument != ArgumentNone) {
+ if (option.argument == ArgumentOptional)
+ argument += "[=";
+ else
+ argument += " ";
+ argument += option.argumentName;
+ if (option.argument == ArgumentOptional)
+ argument += "]";
+ }
+
+ if (option.isArray)
+ argument += " ...";
+
+ std::cerr << std::setw(indent) << argument;
+
+ for (const char *help = option.help, *end = help; end; ) {
+ end = strchr(help, '\n');
+ if (end) {
+ std::cerr << std::string(help, end - help + 1);
+ std::cerr << std::setw(indent) << " ";
+ help = end + 1;
+ } else {
+ std::cerr << help << std::endl;
+ }
+ }
+
+ if (option.keyValueParser)
+ option.keyValueParser->usage(indent);
+
+ if (!option.children.empty())
+ parentOptions.push_back(&option);
+ }
+
+ if (parentOptions.empty())
+ return;
+
+ for (const Option *option : parentOptions) {
+ std::cerr << std::endl << "Options valid in the context of "
+ << option->optionName() << ":" << std::endl;
+ usageOptions(option->children, indent);
+ }
+}
+
+std::tuple<OptionsParser::Options *, const Option *>
+OptionsParser::childOption(const Option *parent, Options *options)
+{
+ /*
+ * The parent argument points to the parent of the leaf node Option,
+ * and the options argument to the root node of the Options tree. Use
+ * recursive calls to traverse the Option tree up to the root node while
+ * traversing the Options tree down to the leaf node:
+ */
+
+ /*
+ * - If we have no parent, we've reached the root node of the Option
+ * tree, the options argument is what we need.
+ */
+ if (!parent)
+ return { options, nullptr };
+
+ /*
+ * - If the parent has a parent, use recursion to move one level up the
+ * Option tree. This returns the Options corresponding to parent, or
+ * nullptr if a suitable Options child isn't found.
+ */
+ if (parent->parent) {
+ const Option *error;
+ std::tie(options, error) = childOption(parent->parent, options);
+
+ /* Propagate the error all the way back up the call stack. */
+ if (!error)
+ return { options, error };
+ }
+
+ /*
+ * - The parent has no parent, we're now one level down the root.
+ * Return the Options child corresponding to the parent. The child may
+ * not exist if options are specified in an incorrect order.
+ */
+ if (!options->isSet(parent->opt))
+ return { nullptr, parent };
+
+ /*
+ * If the child value is of array type, children are not stored in the
+ * value .children() list, but in the .children() of the value's array
+ * elements. Use the last array element in that case, as a child option
+ * relates to the last instance of its parent option.
+ */
+ const OptionValue *value = &(*options)[parent->opt];
+ if (value->type() == OptionValue::ValueArray)
+ value = &value->toArray().back();
+
+ return { const_cast<Options *>(&value->children()), nullptr };
+}
+
+bool OptionsParser::parseValue(const Option &option, const char *arg,
+ Options *options)
+{
+ const Option *error;
+
+ std::tie(options, error) = childOption(option.parent, options);
+ if (error) {
+ std::cerr << "Option " << option.optionName() << " requires a "
+ << error->optionName() << " context" << std::endl;
+ return false;
+ }
+
+ if (!options->parseValue(option.opt, option, arg)) {
+ std::cerr << "Can't parse " << option.typeName()
+ << " argument for option " << option.optionName()
+ << std::endl;
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/apps/common/options.h b/src/apps/common/options.h
new file mode 100644
index 00000000..9771aa7a
--- /dev/null
+++ b/src/apps/common/options.h
@@ -0,0 +1,157 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * cam - Options parsing
+ */
+
+#pragma once
+
+#include <ctype.h>
+#include <list>
+#include <map>
+#include <tuple>
+#include <vector>
+
+class KeyValueParser;
+class OptionValue;
+struct Option;
+
+enum OptionArgument {
+ ArgumentNone,
+ ArgumentRequired,
+ ArgumentOptional,
+};
+
+enum OptionType {
+ OptionNone,
+ OptionInteger,
+ OptionString,
+ OptionKeyValue,
+};
+
+template<typename T>
+class OptionsBase
+{
+public:
+ OptionsBase() : valid_(false) {}
+
+ bool empty() const;
+ bool valid() const;
+ bool isSet(const T &opt) const;
+ const OptionValue &operator[](const T &opt) const;
+
+ void invalidate();
+
+private:
+ friend class KeyValueParser;
+ friend class OptionsParser;
+
+ bool parseValue(const T &opt, const Option &option, const char *value);
+
+ std::map<T, OptionValue> values_;
+ bool valid_;
+};
+
+class KeyValueParser
+{
+public:
+ class Options : public OptionsBase<std::string>
+ {
+ };
+
+ KeyValueParser();
+ virtual ~KeyValueParser();
+
+ bool addOption(const char *name, OptionType type, const char *help,
+ OptionArgument argument = ArgumentNone);
+
+ virtual Options parse(const char *arguments);
+
+private:
+ KeyValueParser(const KeyValueParser &) = delete;
+ KeyValueParser &operator=(const KeyValueParser &) = delete;
+
+ friend class OptionsParser;
+ unsigned int maxOptionLength() const;
+ void usage(int indent);
+
+ std::map<std::string, Option> optionsMap_;
+};
+
+class OptionsParser
+{
+public:
+ class Options : public OptionsBase<int>
+ {
+ };
+
+ OptionsParser();
+ ~OptionsParser();
+
+ bool addOption(int opt, OptionType type, const char *help,
+ const char *name = nullptr,
+ OptionArgument argument = ArgumentNone,
+ const char *argumentName = nullptr, bool array = false,
+ int parent = 0);
+ bool addOption(int opt, KeyValueParser *parser, const char *help,
+ const char *name = nullptr, bool array = false,
+ int parent = 0);
+
+ Options parse(int argc, char *argv[]);
+ void usage();
+
+private:
+ OptionsParser(const OptionsParser &) = delete;
+ OptionsParser &operator=(const OptionsParser &) = delete;
+
+ void usageOptions(const std::list<Option> &options, unsigned int indent);
+
+ std::tuple<OptionsParser::Options *, const Option *>
+ childOption(const Option *parent, Options *options);
+ bool parseValue(const Option &option, const char *arg, Options *options);
+
+ std::list<Option> options_;
+ std::map<unsigned int, Option *> optionsMap_;
+};
+
+class OptionValue
+{
+public:
+ enum ValueType {
+ ValueNone,
+ ValueInteger,
+ ValueString,
+ ValueKeyValue,
+ ValueArray,
+ };
+
+ OptionValue();
+ OptionValue(int value);
+ OptionValue(const char *value);
+ OptionValue(const std::string &value);
+ OptionValue(const KeyValueParser::Options &value);
+
+ void addValue(const OptionValue &value);
+
+ ValueType type() const { return type_; }
+ bool empty() const { return type_ == ValueType::ValueNone; }
+
+ operator int() const;
+ operator std::string() const;
+
+ int toInteger() const;
+ std::string toString() const;
+ const KeyValueParser::Options &toKeyValues() const;
+ const std::vector<OptionValue> &toArray() const;
+
+ const OptionsParser::Options &children() const;
+
+private:
+ ValueType type_;
+ int integer_;
+ std::string string_;
+ KeyValueParser::Options keyValues_;
+ std::vector<OptionValue> array_;
+ OptionsParser::Options children_;
+};
diff --git a/src/apps/common/ppm_writer.cpp b/src/apps/common/ppm_writer.cpp
new file mode 100644
index 00000000..d6c8641d
--- /dev/null
+++ b/src/apps/common/ppm_writer.cpp
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Red Hat, Inc.
+ *
+ * PPM writer
+ */
+
+#include "ppm_writer.h"
+
+#include <fstream>
+#include <iostream>
+
+#include <libcamera/formats.h>
+#include <libcamera/pixel_format.h>
+
+using namespace libcamera;
+
+int PPMWriter::write(const char *filename,
+ const StreamConfiguration &config,
+ const Span<uint8_t> &data)
+{
+ if (config.pixelFormat != formats::BGR888) {
+ std::cerr << "Only BGR888 output pixel format is supported ("
+ << config.pixelFormat << " requested)" << std::endl;
+ return -EINVAL;
+ }
+
+ std::ofstream output(filename, std::ios::binary);
+ if (!output) {
+ std::cerr << "Failed to open ppm file: " << filename << std::endl;
+ return -EINVAL;
+ }
+
+ output << "P6" << std::endl
+ << config.size.width << " " << config.size.height << std::endl
+ << "255" << std::endl;
+ if (!output) {
+ std::cerr << "Failed to write the file header" << std::endl;
+ return -EINVAL;
+ }
+
+ const unsigned int rowLength = config.size.width * 3;
+ const char *row = reinterpret_cast<const char *>(data.data());
+ for (unsigned int y = 0; y < config.size.height; y++, row += config.stride) {
+ output.write(row, rowLength);
+ if (!output) {
+ std::cerr << "Failed to write image data at row " << y << std::endl;
+ return -EINVAL;
+ }
+ }
+
+ return 0;
+}
diff --git a/src/apps/common/ppm_writer.h b/src/apps/common/ppm_writer.h
new file mode 100644
index 00000000..8c8d2e15
--- /dev/null
+++ b/src/apps/common/ppm_writer.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat, Inc.
+ *
+ * PPM writer
+ */
+
+#pragma once
+
+#include <libcamera/base/span.h>
+
+#include <libcamera/stream.h>
+
+class PPMWriter
+{
+public:
+ static int write(const char *filename,
+ const libcamera::StreamConfiguration &config,
+ const libcamera::Span<uint8_t> &data);
+};
diff --git a/src/apps/common/stream_options.cpp b/src/apps/common/stream_options.cpp
new file mode 100644
index 00000000..99239e07
--- /dev/null
+++ b/src/apps/common/stream_options.cpp
@@ -0,0 +1,121 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * Helper to parse options for streams
+ */
+#include "stream_options.h"
+
+#include <iostream>
+
+#include <libcamera/color_space.h>
+
+using namespace libcamera;
+
+StreamKeyValueParser::StreamKeyValueParser()
+{
+ addOption("role", OptionString,
+ "Role for the stream (viewfinder, video, still, raw)",
+ ArgumentRequired);
+ addOption("width", OptionInteger, "Width in pixels",
+ ArgumentRequired);
+ addOption("height", OptionInteger, "Height in pixels",
+ ArgumentRequired);
+ addOption("pixelformat", OptionString, "Pixel format name",
+ ArgumentRequired);
+ addOption("colorspace", OptionString, "Color space",
+ ArgumentRequired);
+}
+
+KeyValueParser::Options StreamKeyValueParser::parse(const char *arguments)
+{
+ KeyValueParser::Options options = KeyValueParser::parse(arguments);
+
+ if (options.valid() && options.isSet("role") && !parseRole(options)) {
+ std::cerr << "Unknown stream role "
+ << options["role"].toString() << std::endl;
+ options.invalidate();
+ }
+
+ return options;
+}
+
+std::vector<StreamRole> StreamKeyValueParser::roles(const OptionValue &values)
+{
+ /* If no configuration values to examine default to viewfinder. */
+ if (values.empty())
+ return { StreamRole::Viewfinder };
+
+ const std::vector<OptionValue> &streamParameters = values.toArray();
+
+ std::vector<StreamRole> roles;
+ for (auto const &value : streamParameters) {
+ /* If a role is invalid default it to viewfinder. */
+ roles.push_back(parseRole(value.toKeyValues()).value_or(StreamRole::Viewfinder));
+ }
+
+ return roles;
+}
+
+int StreamKeyValueParser::updateConfiguration(CameraConfiguration *config,
+ const OptionValue &values)
+{
+ if (!config) {
+ std::cerr << "No configuration provided" << std::endl;
+ return -EINVAL;
+ }
+
+ /* If no configuration values nothing to do. */
+ if (values.empty())
+ return 0;
+
+ const std::vector<OptionValue> &streamParameters = values.toArray();
+
+ if (config->size() != streamParameters.size()) {
+ std::cerr
+ << "Number of streams in configuration "
+ << config->size()
+ << " does not match number of streams parsed "
+ << streamParameters.size()
+ << std::endl;
+ return -EINVAL;
+ }
+
+ unsigned int i = 0;
+ for (auto const &value : streamParameters) {
+ KeyValueParser::Options opts = value.toKeyValues();
+ StreamConfiguration &cfg = config->at(i++);
+
+ if (opts.isSet("width") && opts.isSet("height")) {
+ cfg.size.width = opts["width"];
+ cfg.size.height = opts["height"];
+ }
+
+ if (opts.isSet("pixelformat"))
+ cfg.pixelFormat = PixelFormat::fromString(opts["pixelformat"].toString());
+
+ if (opts.isSet("colorspace"))
+ cfg.colorSpace = ColorSpace::fromString(opts["colorspace"].toString());
+ }
+
+ return 0;
+}
+
+std::optional<libcamera::StreamRole> StreamKeyValueParser::parseRole(const KeyValueParser::Options &options)
+{
+ if (!options.isSet("role"))
+ return {};
+
+ std::string name = options["role"].toString();
+
+ if (name == "viewfinder")
+ return StreamRole::Viewfinder;
+ else if (name == "video")
+ return StreamRole::VideoRecording;
+ else if (name == "still")
+ return StreamRole::StillCapture;
+ else if (name == "raw")
+ return StreamRole::Raw;
+
+ return {};
+}
diff --git a/src/apps/common/stream_options.h b/src/apps/common/stream_options.h
new file mode 100644
index 00000000..a93f104c
--- /dev/null
+++ b/src/apps/common/stream_options.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * Helper to parse options for streams
+ */
+
+#pragma once
+
+#include <optional>
+
+#include <libcamera/camera.h>
+
+#include "options.h"
+
+class StreamKeyValueParser : public KeyValueParser
+{
+public:
+ StreamKeyValueParser();
+
+ KeyValueParser::Options parse(const char *arguments) override;
+
+ static std::vector<libcamera::StreamRole> roles(const OptionValue &values);
+ static int updateConfiguration(libcamera::CameraConfiguration *config,
+ const OptionValue &values);
+
+private:
+ static std::optional<libcamera::StreamRole> parseRole(const KeyValueParser::Options &options);
+};