summaryrefslogtreecommitdiff
path: root/src/android/jpeg
diff options
context:
space:
mode:
Diffstat (limited to 'src/android/jpeg')
-rw-r--r--src/android/jpeg/encoder.h26
-rw-r--r--src/android/jpeg/encoder_jea.cpp56
-rw-r--r--src/android/jpeg/encoder_jea.h31
-rw-r--r--src/android/jpeg/encoder_libjpeg.cpp239
-rw-r--r--src/android/jpeg/encoder_libjpeg.h44
-rw-r--r--src/android/jpeg/exif.cpp522
-rw-r--r--src/android/jpeg/exif.h112
-rw-r--r--src/android/jpeg/meson.build14
-rw-r--r--src/android/jpeg/post_processor_jpeg.cpp223
-rw-r--r--src/android/jpeg/post_processor_jpeg.h38
-rw-r--r--src/android/jpeg/thumbnailer.cpp96
-rw-r--r--src/android/jpeg/thumbnailer.h32
12 files changed, 1433 insertions, 0 deletions
diff --git a/src/android/jpeg/encoder.h b/src/android/jpeg/encoder.h
new file mode 100644
index 00000000..ed033c19
--- /dev/null
+++ b/src/android/jpeg/encoder.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * Image encoding interface
+ */
+
+#pragma once
+
+#include <libcamera/base/span.h>
+
+#include <libcamera/framebuffer.h>
+#include <libcamera/stream.h>
+
+#include "../camera_request.h"
+
+class Encoder
+{
+public:
+ virtual ~Encoder() = default;
+
+ virtual int configure(const libcamera::StreamConfiguration &cfg) = 0;
+ virtual int encode(Camera3RequestDescriptor::StreamBuffer *buffer,
+ libcamera::Span<const uint8_t> exifData,
+ unsigned int quality) = 0;
+};
diff --git a/src/android/jpeg/encoder_jea.cpp b/src/android/jpeg/encoder_jea.cpp
new file mode 100644
index 00000000..25dc4317
--- /dev/null
+++ b/src/android/jpeg/encoder_jea.cpp
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2022, Google Inc.
+ *
+ * JPEG encoding using CrOS JEA
+ */
+
+#include "encoder_jea.h"
+
+#include "libcamera/internal/mapped_framebuffer.h"
+
+#include <cros-camera/camera_mojo_channel_manager_token.h>
+
+#include "../cros_mojo_token.h"
+#include "../hal_framebuffer.h"
+
+EncoderJea::EncoderJea() = default;
+
+EncoderJea::~EncoderJea() = default;
+
+int EncoderJea::configure(const libcamera::StreamConfiguration &cfg)
+{
+ size_ = cfg.size;
+
+ if (jpegCompressor_)
+ return 0;
+
+ if (gCrosMojoToken == nullptr)
+ return -ENOTSUP;
+
+ jpegCompressor_ = cros::JpegCompressor::GetInstance(gCrosMojoToken);
+
+ return 0;
+}
+
+int EncoderJea::encode(Camera3RequestDescriptor::StreamBuffer *buffer,
+ libcamera::Span<const uint8_t> exifData,
+ unsigned int quality)
+{
+ if (!jpegCompressor_)
+ return -ENOTSUP;
+
+ uint32_t outDataSize = 0;
+ const HALFrameBuffer *fb =
+ dynamic_cast<const HALFrameBuffer *>(buffer->srcBuffer);
+
+ if (!jpegCompressor_->CompressImageFromHandle(fb->handle(),
+ *buffer->camera3Buffer,
+ size_.width, size_.height,
+ quality, exifData.data(),
+ exifData.size(),
+ &outDataSize))
+ return -EBUSY;
+
+ return outDataSize;
+}
diff --git a/src/android/jpeg/encoder_jea.h b/src/android/jpeg/encoder_jea.h
new file mode 100644
index 00000000..91115d2e
--- /dev/null
+++ b/src/android/jpeg/encoder_jea.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2022, Google Inc.
+ *
+ * JPEG encoding using CrOS JEA
+ */
+
+#pragma once
+
+#include <libcamera/geometry.h>
+
+#include <cros-camera/jpeg_compressor.h>
+
+#include "encoder.h"
+
+class EncoderJea : public Encoder
+{
+public:
+ EncoderJea();
+ ~EncoderJea();
+
+ int configure(const libcamera::StreamConfiguration &cfg) override;
+ int encode(Camera3RequestDescriptor::StreamBuffer *buffer,
+ libcamera::Span<const uint8_t> exifData,
+ unsigned int quality) override;
+
+private:
+ libcamera::Size size_;
+
+ std::unique_ptr<cros::JpegCompressor> jpegCompressor_;
+};
diff --git a/src/android/jpeg/encoder_libjpeg.cpp b/src/android/jpeg/encoder_libjpeg.cpp
new file mode 100644
index 00000000..cb242b5e
--- /dev/null
+++ b/src/android/jpeg/encoder_libjpeg.cpp
@@ -0,0 +1,239 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * JPEG encoding using libjpeg native API
+ */
+
+#include "encoder_libjpeg.h"
+
+#include <fcntl.h>
+#include <iomanip>
+#include <iostream>
+#include <sstream>
+#include <string.h>
+#include <unistd.h>
+#include <vector>
+
+#include <libcamera/base/log.h>
+
+#include <libcamera/camera.h>
+#include <libcamera/formats.h>
+#include <libcamera/pixel_format.h>
+
+#include "libcamera/internal/formats.h"
+#include "libcamera/internal/mapped_framebuffer.h"
+
+#include "../camera_buffer.h"
+
+using namespace libcamera;
+
+LOG_DECLARE_CATEGORY(JPEG)
+
+namespace {
+
+struct JPEGPixelFormatInfo {
+ J_COLOR_SPACE colorSpace;
+ const PixelFormatInfo &pixelFormatInfo;
+ bool nvSwap;
+};
+
+const std::map<PixelFormat, JPEGPixelFormatInfo> pixelInfo{
+ { formats::R8, { JCS_GRAYSCALE, PixelFormatInfo::info(formats::R8), false } },
+
+ { formats::RGB888, { JCS_EXT_BGR, PixelFormatInfo::info(formats::RGB888), false } },
+ { formats::BGR888, { JCS_EXT_RGB, PixelFormatInfo::info(formats::BGR888), false } },
+
+ { formats::NV12, { JCS_YCbCr, PixelFormatInfo::info(formats::NV12), false } },
+ { formats::NV21, { JCS_YCbCr, PixelFormatInfo::info(formats::NV21), true } },
+ { formats::NV16, { JCS_YCbCr, PixelFormatInfo::info(formats::NV16), false } },
+ { formats::NV61, { JCS_YCbCr, PixelFormatInfo::info(formats::NV61), true } },
+ { formats::NV24, { JCS_YCbCr, PixelFormatInfo::info(formats::NV24), false } },
+ { formats::NV42, { JCS_YCbCr, PixelFormatInfo::info(formats::NV42), true } },
+};
+
+const struct JPEGPixelFormatInfo &findPixelInfo(const PixelFormat &format)
+{
+ static const struct JPEGPixelFormatInfo invalidPixelFormat {
+ JCS_UNKNOWN, PixelFormatInfo(), false
+ };
+
+ const auto iter = pixelInfo.find(format);
+ if (iter == pixelInfo.end()) {
+ LOG(JPEG, Error) << "Unsupported pixel format for JPEG encoder: "
+ << format;
+ return invalidPixelFormat;
+ }
+
+ return iter->second;
+}
+
+} /* namespace */
+
+EncoderLibJpeg::EncoderLibJpeg()
+{
+ /* \todo Expand error handling coverage with a custom handler. */
+ compress_.err = jpeg_std_error(&jerr_);
+
+ jpeg_create_compress(&compress_);
+}
+
+EncoderLibJpeg::~EncoderLibJpeg()
+{
+ jpeg_destroy_compress(&compress_);
+}
+
+int EncoderLibJpeg::configure(const StreamConfiguration &cfg)
+{
+ const struct JPEGPixelFormatInfo info = findPixelInfo(cfg.pixelFormat);
+ if (info.colorSpace == JCS_UNKNOWN)
+ return -ENOTSUP;
+
+ compress_.image_width = cfg.size.width;
+ compress_.image_height = cfg.size.height;
+ compress_.in_color_space = info.colorSpace;
+
+ compress_.input_components = info.colorSpace == JCS_GRAYSCALE ? 1 : 3;
+
+ jpeg_set_defaults(&compress_);
+
+ pixelFormatInfo_ = &info.pixelFormatInfo;
+
+ nv_ = pixelFormatInfo_->numPlanes() == 2;
+ nvSwap_ = info.nvSwap;
+
+ return 0;
+}
+
+void EncoderLibJpeg::compressRGB(const std::vector<Span<uint8_t>> &planes)
+{
+ unsigned char *src = const_cast<unsigned char *>(planes[0].data());
+ /* \todo Stride information should come from buffer configuration. */
+ unsigned int stride = pixelFormatInfo_->stride(compress_.image_width, 0);
+
+ JSAMPROW row_pointer[1];
+
+ while (compress_.next_scanline < compress_.image_height) {
+ row_pointer[0] = &src[compress_.next_scanline * stride];
+ jpeg_write_scanlines(&compress_, row_pointer, 1);
+ }
+}
+
+/*
+ * Compress the incoming buffer from a supported NV format.
+ * This naively unpacks the semi-planar NV12 to a YUV888 format for libjpeg.
+ */
+void EncoderLibJpeg::compressNV(const std::vector<Span<uint8_t>> &planes)
+{
+ std::vector<uint8_t> tmprowbuf(compress_.image_width * 3);
+
+ /*
+ * \todo Use the raw api, and only unpack the cb/cr samples to new line
+ * buffers. If possible, see if we can set appropriate pixel strides
+ * too to save even that copy.
+ *
+ * Possible hints at:
+ * https://sourceforge.net/p/libjpeg/mailman/message/30815123/
+ */
+ unsigned int y_stride = pixelFormatInfo_->stride(compress_.image_width, 0);
+ unsigned int c_stride = pixelFormatInfo_->stride(compress_.image_width, 1);
+
+ unsigned int horzSubSample = 2 * compress_.image_width / c_stride;
+ unsigned int vertSubSample = pixelFormatInfo_->planes[1].verticalSubSampling;
+
+ unsigned int c_inc = horzSubSample == 1 ? 2 : 0;
+ unsigned int cb_pos = nvSwap_ ? 1 : 0;
+ unsigned int cr_pos = nvSwap_ ? 0 : 1;
+
+ const unsigned char *src = planes[0].data();
+ const unsigned char *src_c = planes[1].data();
+
+ JSAMPROW row_pointer[1];
+ row_pointer[0] = tmprowbuf.data();
+
+ for (unsigned int y = 0; y < compress_.image_height; y++) {
+ unsigned char *dst = tmprowbuf.data();
+
+ const unsigned char *src_y = src + y * y_stride;
+ const unsigned char *src_cb = src_c + (y / vertSubSample) * c_stride + cb_pos;
+ const unsigned char *src_cr = src_c + (y / vertSubSample) * c_stride + cr_pos;
+
+ for (unsigned int x = 0; x < compress_.image_width; x += 2) {
+ dst[0] = *src_y;
+ dst[1] = *src_cb;
+ dst[2] = *src_cr;
+ src_y++;
+ src_cb += c_inc;
+ src_cr += c_inc;
+ dst += 3;
+
+ dst[0] = *src_y;
+ dst[1] = *src_cb;
+ dst[2] = *src_cr;
+ src_y++;
+ src_cb += 2;
+ src_cr += 2;
+ dst += 3;
+ }
+
+ jpeg_write_scanlines(&compress_, row_pointer, 1);
+ }
+}
+
+int EncoderLibJpeg::encode(Camera3RequestDescriptor::StreamBuffer *buffer,
+ libcamera::Span<const uint8_t> exifData,
+ unsigned int quality)
+{
+ MappedFrameBuffer frame(buffer->srcBuffer,
+ MappedFrameBuffer::MapFlag::Read);
+ if (!frame.isValid()) {
+ LOG(JPEG, Error) << "Failed to map FrameBuffer : "
+ << strerror(frame.error());
+ return frame.error();
+ }
+
+ return encode(frame.planes(), buffer->dstBuffer->plane(0),
+ exifData, quality);
+}
+
+int EncoderLibJpeg::encode(const std::vector<Span<uint8_t>> &src,
+ Span<uint8_t> dest, Span<const uint8_t> exifData,
+ unsigned int quality)
+{
+ unsigned char *destination = dest.data();
+ unsigned long size = dest.size();
+
+ jpeg_set_quality(&compress_, quality, TRUE);
+
+ /*
+ * The jpeg_mem_dest will reallocate if the required size is not
+ * sufficient. That means the output won't be written to the correct
+ * buffers.
+ *
+ * \todo Implement our own custom memory destination to prevent
+ * reallocation and prefer failure with correct reporting.
+ */
+ jpeg_mem_dest(&compress_, &destination, &size);
+
+ jpeg_start_compress(&compress_, TRUE);
+
+ if (exifData.size())
+ /* Store Exif data in the JPEG_APP1 data block. */
+ jpeg_write_marker(&compress_, JPEG_APP0 + 1,
+ static_cast<const JOCTET *>(exifData.data()),
+ exifData.size());
+
+ LOG(JPEG, Debug) << "JPEG Encode Starting:" << compress_.image_width
+ << "x" << compress_.image_height;
+
+ ASSERT(src.size() == pixelFormatInfo_->numPlanes());
+
+ if (nv_)
+ compressNV(src);
+ else
+ compressRGB(src);
+
+ jpeg_finish_compress(&compress_);
+
+ return size;
+}
diff --git a/src/android/jpeg/encoder_libjpeg.h b/src/android/jpeg/encoder_libjpeg.h
new file mode 100644
index 00000000..4ac85c22
--- /dev/null
+++ b/src/android/jpeg/encoder_libjpeg.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * JPEG encoding using libjpeg
+ */
+
+#pragma once
+
+#include "encoder.h"
+
+#include <vector>
+
+#include "libcamera/internal/formats.h"
+
+#include <jpeglib.h>
+
+class EncoderLibJpeg : public Encoder
+{
+public:
+ EncoderLibJpeg();
+ ~EncoderLibJpeg();
+
+ int configure(const libcamera::StreamConfiguration &cfg) override;
+ int encode(Camera3RequestDescriptor::StreamBuffer *buffer,
+ libcamera::Span<const uint8_t> exifData,
+ unsigned int quality) override;
+ int encode(const std::vector<libcamera::Span<uint8_t>> &planes,
+ libcamera::Span<uint8_t> destination,
+ libcamera::Span<const uint8_t> exifData,
+ unsigned int quality);
+
+private:
+ void compressRGB(const std::vector<libcamera::Span<uint8_t>> &planes);
+ void compressNV(const std::vector<libcamera::Span<uint8_t>> &planes);
+
+ struct jpeg_compress_struct compress_;
+ struct jpeg_error_mgr jerr_;
+
+ const libcamera::PixelFormatInfo *pixelFormatInfo_;
+
+ bool nv_;
+ bool nvSwap_;
+};
diff --git a/src/android/jpeg/exif.cpp b/src/android/jpeg/exif.cpp
new file mode 100644
index 00000000..b8c871df
--- /dev/null
+++ b/src/android/jpeg/exif.cpp
@@ -0,0 +1,522 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * EXIF tag creation using libexif
+ */
+
+#include "exif.h"
+
+#include <cmath>
+#include <iomanip>
+#include <map>
+#include <sstream>
+#include <tuple>
+#include <uchar.h>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(EXIF)
+
+/*
+ * List of EXIF tags that we set directly because they are not supported
+ * by libexif version 0.6.21.
+ */
+enum class _ExifTag {
+ OFFSET_TIME = 0x9010,
+ OFFSET_TIME_ORIGINAL = 0x9011,
+ OFFSET_TIME_DIGITIZED = 0x9012,
+};
+
+/*
+ * The Exif class should be instantiated and specific properties set
+ * through the exposed public API.
+ *
+ * Once all desired properties have been set, the user shall call
+ * generate() to process the entries and generate the Exif data.
+ *
+ * Calls to generate() must check the return code to determine if any error
+ * occurred during the construction of the Exif data, and if successful the
+ * data can be obtained using the data() function.
+ */
+Exif::Exif()
+ : valid_(false), data_(nullptr), order_(EXIF_BYTE_ORDER_INTEL),
+ exifData_(0), size_(0)
+{
+ /* Create an ExifMem allocator to construct entries. */
+ mem_ = exif_mem_new_default();
+ if (!mem_) {
+ LOG(EXIF, Error) << "Failed to allocate ExifMem Allocator";
+ return;
+ }
+
+ data_ = exif_data_new_mem(mem_);
+ if (!data_) {
+ LOG(EXIF, Error) << "Failed to allocate an ExifData structure";
+ return;
+ }
+
+ valid_ = true;
+
+ exif_data_set_option(data_, EXIF_DATA_OPTION_FOLLOW_SPECIFICATION);
+ exif_data_set_data_type(data_, EXIF_DATA_TYPE_COMPRESSED);
+
+ /*
+ * Big-Endian: EXIF_BYTE_ORDER_MOTOROLA
+ * Little Endian: EXIF_BYTE_ORDER_INTEL
+ */
+ exif_data_set_byte_order(data_, order_);
+
+ setString(EXIF_IFD_EXIF, EXIF_TAG_EXIF_VERSION,
+ EXIF_FORMAT_UNDEFINED, "0231");
+
+ /* Create the mandatory EXIF fields with default data. */
+ exif_data_fix(data_);
+}
+
+Exif::~Exif()
+{
+ if (exifData_)
+ free(exifData_);
+
+ if (data_) {
+ /*
+ * Reset thumbnail data to avoid getting double-freed by
+ * libexif. It is owned by the caller (i.e. PostProcessorJpeg).
+ */
+ data_->data = nullptr;
+ data_->size = 0;
+
+ exif_data_unref(data_);
+ }
+
+ if (mem_)
+ exif_mem_unref(mem_);
+}
+
+ExifEntry *Exif::createEntry(ExifIfd ifd, ExifTag tag)
+{
+ ExifContent *content = data_->ifd[ifd];
+ ExifEntry *entry = exif_content_get_entry(content, tag);
+
+ if (entry) {
+ exif_entry_ref(entry);
+ return entry;
+ }
+
+ entry = exif_entry_new_mem(mem_);
+ if (!entry) {
+ LOG(EXIF, Error) << "Failed to allocated new entry";
+ valid_ = false;
+ return nullptr;
+ }
+
+ exif_content_add_entry(content, entry);
+ exif_entry_initialize(entry, tag);
+
+ return entry;
+}
+
+ExifEntry *Exif::createEntry(ExifIfd ifd, ExifTag tag, ExifFormat format,
+ unsigned long components, unsigned int size)
+{
+ ExifContent *content = data_->ifd[ifd];
+
+ /* Replace any existing entry with the same tag. */
+ ExifEntry *existing = exif_content_get_entry(content, tag);
+ exif_content_remove_entry(content, existing);
+
+ ExifEntry *entry = exif_entry_new_mem(mem_);
+ if (!entry) {
+ LOG(EXIF, Error) << "Failed to allocated new entry";
+ valid_ = false;
+ return nullptr;
+ }
+
+ void *buffer = exif_mem_alloc(mem_, size);
+ if (!buffer) {
+ LOG(EXIF, Error) << "Failed to allocate buffer for variable entry";
+ exif_mem_unref(mem_);
+ valid_ = false;
+ return nullptr;
+ }
+
+ entry->data = static_cast<unsigned char *>(buffer);
+ entry->components = components;
+ entry->format = format;
+ entry->size = size;
+ entry->tag = tag;
+
+ exif_content_add_entry(content, entry);
+
+ return entry;
+}
+
+void Exif::setByte(ExifIfd ifd, ExifTag tag, uint8_t item)
+{
+ ExifEntry *entry = createEntry(ifd, tag, EXIF_FORMAT_BYTE, 1, 1);
+ if (!entry)
+ return;
+
+ entry->data[0] = item;
+ exif_entry_unref(entry);
+}
+
+void Exif::setShort(ExifIfd ifd, ExifTag tag, uint16_t item)
+{
+ ExifEntry *entry = createEntry(ifd, tag);
+ if (!entry)
+ return;
+
+ exif_set_short(entry->data, order_, item);
+ exif_entry_unref(entry);
+}
+
+void Exif::setLong(ExifIfd ifd, ExifTag tag, uint32_t item)
+{
+ ExifEntry *entry = createEntry(ifd, tag);
+ if (!entry)
+ return;
+
+ exif_set_long(entry->data, order_, item);
+ exif_entry_unref(entry);
+}
+
+void Exif::setRational(ExifIfd ifd, ExifTag tag, ExifRational item)
+{
+ setRational(ifd, tag, { &item, 1 });
+}
+
+void Exif::setRational(ExifIfd ifd, ExifTag tag, Span<const ExifRational> items)
+{
+ ExifEntry *entry = createEntry(ifd, tag, EXIF_FORMAT_RATIONAL,
+ items.size(),
+ items.size() * sizeof(ExifRational));
+ if (!entry)
+ return;
+
+ for (size_t i = 0; i < items.size(); i++)
+ exif_set_rational(entry->data + i * sizeof(ExifRational),
+ order_, items[i]);
+ exif_entry_unref(entry);
+}
+
+static const std::map<Exif::StringEncoding, std::array<uint8_t, 8>> stringEncodingCodes = {
+ { Exif::ASCII, { 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00 } },
+ { Exif::Unicode, { 0x55, 0x4e, 0x49, 0x43, 0x4f, 0x44, 0x45, 0x00 } },
+};
+
+void Exif::setString(ExifIfd ifd, ExifTag tag, ExifFormat format,
+ const std::string &item, StringEncoding encoding)
+{
+ std::string ascii;
+ size_t length;
+ const char *str;
+ std::vector<uint8_t> buf;
+
+ if (format == EXIF_FORMAT_ASCII) {
+ ascii = utils::toAscii(item);
+ str = ascii.c_str();
+
+ /* Pad 1 extra byte to null-terminate the ASCII string. */
+ length = ascii.length() + 1;
+ } else {
+ std::u16string u16str;
+
+ auto encodingString = stringEncodingCodes.find(encoding);
+ if (encodingString != stringEncodingCodes.end()) {
+ buf = {
+ encodingString->second.begin(),
+ encodingString->second.end()
+ };
+ }
+
+ switch (encoding) {
+ case Unicode:
+ u16str = utf8ToUtf16(item);
+
+ buf.resize(8 + u16str.size() * 2);
+ for (size_t i = 0; i < u16str.size(); i++) {
+ if (order_ == EXIF_BYTE_ORDER_INTEL) {
+ buf[8 + 2 * i] = u16str[i] & 0xff;
+ buf[8 + 2 * i + 1] = (u16str[i] >> 8) & 0xff;
+ } else {
+ buf[8 + 2 * i] = (u16str[i] >> 8) & 0xff;
+ buf[8 + 2 * i + 1] = u16str[i] & 0xff;
+ }
+ }
+
+ break;
+
+ case ASCII:
+ case NoEncoding:
+ buf.insert(buf.end(), item.begin(), item.end());
+ break;
+ }
+
+ str = reinterpret_cast<const char *>(buf.data());
+
+ /*
+ * Strings stored in different formats (EXIF_FORMAT_UNDEFINED)
+ * are not null-terminated.
+ */
+ length = buf.size();
+ }
+
+ ExifEntry *entry = createEntry(ifd, tag, format, length, length);
+ if (!entry)
+ return;
+
+ memcpy(entry->data, str, length);
+ exif_entry_unref(entry);
+}
+
+void Exif::setMake(const std::string &make)
+{
+ setString(EXIF_IFD_0, EXIF_TAG_MAKE, EXIF_FORMAT_ASCII, make);
+}
+
+void Exif::setModel(const std::string &model)
+{
+ setString(EXIF_IFD_0, EXIF_TAG_MODEL, EXIF_FORMAT_ASCII, model);
+}
+
+void Exif::setSize(const Size &size)
+{
+ setLong(EXIF_IFD_EXIF, EXIF_TAG_PIXEL_Y_DIMENSION, size.height);
+ setLong(EXIF_IFD_EXIF, EXIF_TAG_PIXEL_X_DIMENSION, size.width);
+}
+
+void Exif::setTimestamp(time_t timestamp, std::chrono::milliseconds msec)
+{
+ struct tm tm;
+ localtime_r(&timestamp, &tm);
+
+ char str[20];
+ strftime(str, sizeof(str), "%Y:%m:%d %H:%M:%S", &tm);
+ std::string ts(str);
+
+ setString(EXIF_IFD_0, EXIF_TAG_DATE_TIME, EXIF_FORMAT_ASCII, ts);
+ setString(EXIF_IFD_EXIF, EXIF_TAG_DATE_TIME_ORIGINAL, EXIF_FORMAT_ASCII, ts);
+ setString(EXIF_IFD_EXIF, EXIF_TAG_DATE_TIME_DIGITIZED, EXIF_FORMAT_ASCII, ts);
+
+ /* Query and set timezone information if available. */
+ int r = strftime(str, sizeof(str), "%z", &tm);
+ if (r <= 0)
+ return;
+
+ std::string tz(str);
+ tz.insert(3, 1, ':');
+ setString(EXIF_IFD_EXIF,
+ static_cast<ExifTag>(_ExifTag::OFFSET_TIME),
+ EXIF_FORMAT_ASCII, tz);
+ setString(EXIF_IFD_EXIF,
+ static_cast<ExifTag>(_ExifTag::OFFSET_TIME_ORIGINAL),
+ EXIF_FORMAT_ASCII, tz);
+ setString(EXIF_IFD_EXIF,
+ static_cast<ExifTag>(_ExifTag::OFFSET_TIME_DIGITIZED),
+ EXIF_FORMAT_ASCII, tz);
+
+ std::stringstream sstr;
+ sstr << std::setfill('0') << std::setw(3) << msec.count();
+ std::string subsec = sstr.str();
+
+ setString(EXIF_IFD_EXIF, EXIF_TAG_SUB_SEC_TIME,
+ EXIF_FORMAT_ASCII, subsec);
+ setString(EXIF_IFD_EXIF, EXIF_TAG_SUB_SEC_TIME_ORIGINAL,
+ EXIF_FORMAT_ASCII, subsec);
+ setString(EXIF_IFD_EXIF, EXIF_TAG_SUB_SEC_TIME_DIGITIZED,
+ EXIF_FORMAT_ASCII, subsec);
+}
+
+void Exif::setGPSDateTimestamp(time_t timestamp)
+{
+ struct tm tm;
+ gmtime_r(&timestamp, &tm);
+
+ char str[11];
+ strftime(str, sizeof(str), "%Y:%m:%d", &tm);
+ std::string tsStr(str);
+
+ setString(EXIF_IFD_GPS, static_cast<ExifTag>(EXIF_TAG_GPS_DATE_STAMP),
+ EXIF_FORMAT_ASCII, tsStr);
+
+ /* Set GPS_TIME_STAMP */
+ ExifRational ts[] = {
+ { static_cast<ExifLong>(tm.tm_hour), 1 },
+ { static_cast<ExifLong>(tm.tm_min), 1 },
+ { static_cast<ExifLong>(tm.tm_sec), 1 },
+ };
+
+ setRational(EXIF_IFD_GPS, static_cast<ExifTag>(EXIF_TAG_GPS_TIME_STAMP),
+ ts);
+}
+
+std::tuple<int, int, int> Exif::degreesToDMS(double decimalDegrees)
+{
+ int degrees = std::trunc(decimalDegrees);
+ double minutes = std::abs((decimalDegrees - degrees) * 60);
+ double seconds = (minutes - std::trunc(minutes)) * 60;
+
+ return { degrees, std::trunc(minutes), std::round(seconds) };
+}
+
+void Exif::setGPSDMS(ExifIfd ifd, ExifTag tag, int deg, int min, int sec)
+{
+ ExifRational coords[] = {
+ { static_cast<ExifLong>(deg), 1 },
+ { static_cast<ExifLong>(min), 1 },
+ { static_cast<ExifLong>(sec), 1 },
+ };
+
+ setRational(ifd, tag, coords);
+}
+
+/*
+ * \brief Set GPS location (lat, long, alt)
+ * \param[in] coords Pointer to coordinates latitude, longitude, and altitude,
+ * first two in degrees, the third in meters
+ */
+void Exif::setGPSLocation(const double *coords)
+{
+ int deg, min, sec;
+
+ std::tie<int, int, int>(deg, min, sec) = degreesToDMS(coords[0]);
+ setString(EXIF_IFD_GPS, static_cast<ExifTag>(EXIF_TAG_GPS_LATITUDE_REF),
+ EXIF_FORMAT_ASCII, deg >= 0 ? "N" : "S");
+ setGPSDMS(EXIF_IFD_GPS, static_cast<ExifTag>(EXIF_TAG_GPS_LATITUDE),
+ std::abs(deg), min, sec);
+
+ std::tie<int, int, int>(deg, min, sec) = degreesToDMS(coords[1]);
+ setString(EXIF_IFD_GPS, static_cast<ExifTag>(EXIF_TAG_GPS_LONGITUDE_REF),
+ EXIF_FORMAT_ASCII, deg >= 0 ? "E" : "W");
+ setGPSDMS(EXIF_IFD_GPS, static_cast<ExifTag>(EXIF_TAG_GPS_LONGITUDE),
+ std::abs(deg), min, sec);
+
+ setByte(EXIF_IFD_GPS, static_cast<ExifTag>(EXIF_TAG_GPS_ALTITUDE_REF),
+ coords[2] >= 0 ? 0 : 1);
+ setRational(EXIF_IFD_GPS, static_cast<ExifTag>(EXIF_TAG_GPS_ALTITUDE),
+ ExifRational{ static_cast<ExifLong>(std::abs(coords[2])), 1 });
+}
+
+void Exif::setGPSMethod(const std::string &method)
+{
+ setString(EXIF_IFD_GPS, static_cast<ExifTag>(EXIF_TAG_GPS_PROCESSING_METHOD),
+ EXIF_FORMAT_UNDEFINED, method, NoEncoding);
+}
+
+void Exif::setOrientation(int orientation)
+{
+ int value;
+ switch (orientation) {
+ case 0:
+ default:
+ value = 1;
+ break;
+ case 90:
+ value = 6;
+ break;
+ case 180:
+ value = 3;
+ break;
+ case 270:
+ value = 8;
+ break;
+ }
+
+ setShort(EXIF_IFD_0, EXIF_TAG_ORIENTATION, value);
+}
+
+void Exif::setThumbnail(std::vector<unsigned char> &&thumbnail,
+ Compression compression)
+{
+ thumbnailData_ = std::move(thumbnail);
+
+ data_->data = thumbnailData_.data();
+ data_->size = thumbnailData_.size();
+
+ setShort(EXIF_IFD_0, EXIF_TAG_COMPRESSION, compression);
+}
+
+void Exif::setFocalLength(float length)
+{
+ ExifRational rational = { static_cast<ExifLong>(length * 1000), 1000 };
+ setRational(EXIF_IFD_EXIF, EXIF_TAG_FOCAL_LENGTH, rational);
+}
+
+void Exif::setExposureTime(uint64_t nsec)
+{
+ ExifRational rational = { static_cast<ExifLong>(nsec), 1000000000 };
+ setRational(EXIF_IFD_EXIF, EXIF_TAG_EXPOSURE_TIME, rational);
+}
+
+void Exif::setAperture(float size)
+{
+ ExifRational rational = { static_cast<ExifLong>(size * 10000), 10000 };
+ setRational(EXIF_IFD_EXIF, EXIF_TAG_FNUMBER, rational);
+}
+
+void Exif::setISO(uint16_t iso)
+{
+ setShort(EXIF_IFD_EXIF, EXIF_TAG_ISO_SPEED_RATINGS, iso);
+}
+
+void Exif::setFlash(Flash flash)
+{
+ setShort(EXIF_IFD_EXIF, EXIF_TAG_FLASH, static_cast<ExifShort>(flash));
+}
+
+void Exif::setWhiteBalance(WhiteBalance wb)
+{
+ setShort(EXIF_IFD_EXIF, EXIF_TAG_WHITE_BALANCE, static_cast<ExifShort>(wb));
+}
+
+/**
+ * \brief Convert UTF-8 string to UTF-16 string
+ * \param[in] str String to convert
+ *
+ * \return \a str in UTF-16
+ */
+std::u16string Exif::utf8ToUtf16(const std::string &str)
+{
+ mbstate_t state{};
+ char16_t c16;
+ const char *ptr = str.data();
+ const char *end = ptr + str.size();
+
+ std::u16string ret;
+ while (size_t rc = mbrtoc16(&c16, ptr, end - ptr + 1, &state)) {
+ if (rc == static_cast<size_t>(-2) ||
+ rc == static_cast<size_t>(-1))
+ break;
+
+ ret.push_back(c16);
+
+ if (rc > 0)
+ ptr += rc;
+ }
+
+ return ret;
+}
+
+[[nodiscard]] int Exif::generate()
+{
+ if (exifData_) {
+ free(exifData_);
+ exifData_ = nullptr;
+ }
+
+ if (!valid_) {
+ LOG(EXIF, Error) << "Generated EXIF data is invalid";
+ return -1;
+ }
+
+ exif_data_save_data(data_, &exifData_, &size_);
+
+ LOG(EXIF, Debug) << "Created EXIF instance (" << size_ << " bytes)";
+
+ return 0;
+}
diff --git a/src/android/jpeg/exif.h b/src/android/jpeg/exif.h
new file mode 100644
index 00000000..446d53f3
--- /dev/null
+++ b/src/android/jpeg/exif.h
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * EXIF tag creator using libexif
+ */
+
+#pragma once
+
+#include <chrono>
+#include <string>
+#include <time.h>
+#include <vector>
+
+#include <libexif/exif-data.h>
+
+#include <libcamera/base/span.h>
+
+#include <libcamera/geometry.h>
+
+class Exif
+{
+public:
+ Exif();
+ ~Exif();
+
+ enum Compression {
+ None = 1,
+ JPEG = 6,
+ };
+
+ enum Flash {
+ /* bit 0 */
+ Fired = 0x01,
+ /* bits 1 and 2 */
+ StrobeDetected = 0x04,
+ StrobeNotDetected = 0x06,
+ /* bits 3 and 4 */
+ ModeCompulsoryFiring = 0x08,
+ ModeCompulsorySuppression = 0x10,
+ ModeAuto = 0x18,
+ /* bit 5 */
+ FlashNotPresent = 0x20,
+ /* bit 6 */
+ RedEye = 0x40,
+ };
+
+ enum WhiteBalance {
+ Auto = 0,
+ Manual = 1,
+ };
+
+ enum StringEncoding {
+ NoEncoding = 0,
+ ASCII = 1,
+ Unicode = 2,
+ };
+
+ void setMake(const std::string &make);
+ void setModel(const std::string &model);
+
+ void setOrientation(int orientation);
+ void setSize(const libcamera::Size &size);
+ void setThumbnail(std::vector<unsigned char> &&thumbnail,
+ Compression compression);
+ void setTimestamp(time_t timestamp, std::chrono::milliseconds msec);
+
+ void setGPSDateTimestamp(time_t timestamp);
+ void setGPSLocation(const double *coords);
+ void setGPSMethod(const std::string &method);
+
+ void setFocalLength(float length);
+ void setExposureTime(uint64_t nsec);
+ void setAperture(float size);
+ void setISO(uint16_t iso);
+ void setFlash(Flash flash);
+ void setWhiteBalance(WhiteBalance wb);
+
+ libcamera::Span<const uint8_t> data() const { return { exifData_, size_ }; }
+ [[nodiscard]] int generate();
+
+private:
+ ExifEntry *createEntry(ExifIfd ifd, ExifTag tag);
+ ExifEntry *createEntry(ExifIfd ifd, ExifTag tag, ExifFormat format,
+ unsigned long components, unsigned int size);
+
+ void setByte(ExifIfd ifd, ExifTag tag, uint8_t item);
+ void setShort(ExifIfd ifd, ExifTag tag, uint16_t item);
+ void setLong(ExifIfd ifd, ExifTag tag, uint32_t item);
+ void setString(ExifIfd ifd, ExifTag tag, ExifFormat format,
+ const std::string &item,
+ StringEncoding encoding = NoEncoding);
+ void setRational(ExifIfd ifd, ExifTag tag, ExifRational item);
+ void setRational(ExifIfd ifd, ExifTag tag,
+ libcamera::Span<const ExifRational> items);
+
+ std::tuple<int, int, int> degreesToDMS(double decimalDegrees);
+ void setGPSDMS(ExifIfd ifd, ExifTag tag, int deg, int min, int sec);
+
+ std::u16string utf8ToUtf16(const std::string &str);
+
+ bool valid_;
+
+ ExifData *data_;
+ ExifMem *mem_;
+ ExifByteOrder order_;
+
+ unsigned char *exifData_;
+ unsigned int size_;
+
+ std::vector<unsigned char> thumbnailData_;
+};
diff --git a/src/android/jpeg/meson.build b/src/android/jpeg/meson.build
new file mode 100644
index 00000000..3402e614
--- /dev/null
+++ b/src/android/jpeg/meson.build
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: CC0-1.0
+
+android_hal_sources += files([
+ 'encoder_libjpeg.cpp',
+ 'exif.cpp',
+ 'post_processor_jpeg.cpp',
+ 'thumbnailer.cpp'
+])
+
+platform = get_option('android_platform')
+if platform == 'cros'
+ android_hal_sources += files(['encoder_jea.cpp'])
+ android_deps += [dependency('libcros_camera')]
+endif
diff --git a/src/android/jpeg/post_processor_jpeg.cpp b/src/android/jpeg/post_processor_jpeg.cpp
new file mode 100644
index 00000000..89b8a401
--- /dev/null
+++ b/src/android/jpeg/post_processor_jpeg.cpp
@@ -0,0 +1,223 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * JPEG Post Processor
+ */
+
+#include "post_processor_jpeg.h"
+
+#include <chrono>
+
+#include "../camera_device.h"
+#include "../camera_metadata.h"
+#include "../camera_request.h"
+#if defined(OS_CHROMEOS)
+#include "encoder_jea.h"
+#else /* !defined(OS_CHROMEOS) */
+#include "encoder_libjpeg.h"
+#endif
+#include "exif.h"
+
+#include <libcamera/base/log.h>
+
+#include <libcamera/formats.h>
+
+using namespace libcamera;
+using namespace std::chrono_literals;
+
+LOG_DEFINE_CATEGORY(JPEG)
+
+PostProcessorJpeg::PostProcessorJpeg(CameraDevice *const device)
+ : cameraDevice_(device)
+{
+}
+
+int PostProcessorJpeg::configure(const StreamConfiguration &inCfg,
+ const StreamConfiguration &outCfg)
+{
+ if (inCfg.size != outCfg.size) {
+ LOG(JPEG, Error) << "Mismatch of input and output stream sizes";
+ return -EINVAL;
+ }
+
+ if (outCfg.pixelFormat != formats::MJPEG) {
+ LOG(JPEG, Error) << "Output stream pixel format is not JPEG";
+ return -EINVAL;
+ }
+
+ streamSize_ = outCfg.size;
+
+ thumbnailer_.configure(inCfg.size, inCfg.pixelFormat);
+
+#if defined(OS_CHROMEOS)
+ encoder_ = std::make_unique<EncoderJea>();
+#else /* !defined(OS_CHROMEOS) */
+ encoder_ = std::make_unique<EncoderLibJpeg>();
+#endif
+
+ return encoder_->configure(inCfg);
+}
+
+void PostProcessorJpeg::generateThumbnail(const FrameBuffer &source,
+ const Size &targetSize,
+ unsigned int quality,
+ std::vector<unsigned char> *thumbnail)
+{
+ /* Stores the raw scaled-down thumbnail bytes. */
+ std::vector<unsigned char> rawThumbnail;
+
+ thumbnailer_.createThumbnail(source, targetSize, &rawThumbnail);
+
+ StreamConfiguration thCfg;
+ thCfg.size = targetSize;
+ thCfg.pixelFormat = thumbnailer_.pixelFormat();
+ int ret = thumbnailEncoder_.configure(thCfg);
+
+ if (!rawThumbnail.empty() && !ret) {
+ /*
+ * \todo Avoid value-initialization of all elements of the
+ * vector.
+ */
+ thumbnail->resize(rawThumbnail.size());
+
+ /*
+ * Split planes manually as the encoder expects a vector of
+ * planes.
+ *
+ * \todo Pass a vector of planes directly to
+ * Thumbnailer::createThumbnailer above and remove the manual
+ * planes split from here.
+ */
+ std::vector<Span<uint8_t>> thumbnailPlanes;
+ const PixelFormatInfo &formatNV12 = PixelFormatInfo::info(formats::NV12);
+ size_t yPlaneSize = formatNV12.planeSize(targetSize, 0);
+ size_t uvPlaneSize = formatNV12.planeSize(targetSize, 1);
+ thumbnailPlanes.push_back({ rawThumbnail.data(), yPlaneSize });
+ thumbnailPlanes.push_back({ rawThumbnail.data() + yPlaneSize, uvPlaneSize });
+
+ int jpeg_size = thumbnailEncoder_.encode(thumbnailPlanes,
+ *thumbnail, {}, quality);
+ thumbnail->resize(jpeg_size);
+
+ LOG(JPEG, Debug)
+ << "Thumbnail compress returned "
+ << jpeg_size << " bytes";
+ }
+}
+
+void PostProcessorJpeg::process(Camera3RequestDescriptor::StreamBuffer *streamBuffer)
+{
+ ASSERT(encoder_);
+
+ const FrameBuffer &source = *streamBuffer->srcBuffer;
+ CameraBuffer *destination = streamBuffer->dstBuffer.get();
+
+ ASSERT(destination->numPlanes() == 1);
+
+ const CameraMetadata &requestMetadata = streamBuffer->request->settings_;
+ CameraMetadata *resultMetadata = streamBuffer->request->resultMetadata_.get();
+ camera_metadata_ro_entry_t entry;
+ int ret;
+
+ /* Set EXIF metadata for various tags. */
+ Exif exif;
+ exif.setMake(cameraDevice_->maker());
+ exif.setModel(cameraDevice_->model());
+
+ ret = requestMetadata.getEntry(ANDROID_JPEG_ORIENTATION, &entry);
+
+ const uint32_t jpegOrientation = ret ? *entry.data.i32 : 0;
+ resultMetadata->addEntry(ANDROID_JPEG_ORIENTATION, jpegOrientation);
+ exif.setOrientation(jpegOrientation);
+
+ exif.setSize(streamSize_);
+ /*
+ * We set the frame's EXIF timestamp as the time of encode.
+ * Since the precision we need for EXIF timestamp is only one
+ * second, it is good enough.
+ */
+ exif.setTimestamp(std::time(nullptr), 0ms);
+
+ ret = resultMetadata->getEntry(ANDROID_SENSOR_EXPOSURE_TIME, &entry);
+ exif.setExposureTime(ret ? *entry.data.i64 : 0);
+ ret = requestMetadata.getEntry(ANDROID_LENS_APERTURE, &entry);
+ if (ret)
+ exif.setAperture(*entry.data.f);
+
+ ret = resultMetadata->getEntry(ANDROID_SENSOR_SENSITIVITY, &entry);
+ exif.setISO(ret ? *entry.data.i32 : 100);
+
+ exif.setFlash(Exif::Flash::FlashNotPresent);
+ exif.setWhiteBalance(Exif::WhiteBalance::Auto);
+
+ exif.setFocalLength(1.0);
+
+ ret = requestMetadata.getEntry(ANDROID_JPEG_GPS_TIMESTAMP, &entry);
+ if (ret) {
+ exif.setGPSDateTimestamp(*entry.data.i64);
+ resultMetadata->addEntry(ANDROID_JPEG_GPS_TIMESTAMP,
+ *entry.data.i64);
+ }
+
+ ret = requestMetadata.getEntry(ANDROID_JPEG_THUMBNAIL_SIZE, &entry);
+ if (ret) {
+ const int32_t *data = entry.data.i32;
+ Size thumbnailSize = { static_cast<uint32_t>(data[0]),
+ static_cast<uint32_t>(data[1]) };
+
+ ret = requestMetadata.getEntry(ANDROID_JPEG_THUMBNAIL_QUALITY, &entry);
+ uint8_t quality = ret ? *entry.data.u8 : 95;
+ resultMetadata->addEntry(ANDROID_JPEG_THUMBNAIL_QUALITY, quality);
+
+ if (thumbnailSize != Size(0, 0)) {
+ std::vector<unsigned char> thumbnail;
+ generateThumbnail(source, thumbnailSize, quality, &thumbnail);
+ if (!thumbnail.empty())
+ exif.setThumbnail(std::move(thumbnail), Exif::Compression::JPEG);
+ }
+
+ resultMetadata->addEntry(ANDROID_JPEG_THUMBNAIL_SIZE, data, 2);
+ }
+
+ ret = requestMetadata.getEntry(ANDROID_JPEG_GPS_COORDINATES, &entry);
+ if (ret) {
+ exif.setGPSLocation(entry.data.d);
+ resultMetadata->addEntry(ANDROID_JPEG_GPS_COORDINATES,
+ entry.data.d, 3);
+ }
+
+ ret = requestMetadata.getEntry(ANDROID_JPEG_GPS_PROCESSING_METHOD, &entry);
+ if (ret) {
+ std::string method(entry.data.u8, entry.data.u8 + entry.count);
+ exif.setGPSMethod(method);
+ resultMetadata->addEntry(ANDROID_JPEG_GPS_PROCESSING_METHOD,
+ entry.data.u8, entry.count);
+ }
+
+ if (exif.generate() != 0)
+ LOG(JPEG, Error) << "Failed to generate valid EXIF data";
+
+ ret = requestMetadata.getEntry(ANDROID_JPEG_QUALITY, &entry);
+ const uint8_t quality = ret ? *entry.data.u8 : 95;
+ resultMetadata->addEntry(ANDROID_JPEG_QUALITY, quality);
+
+ int jpeg_size = encoder_->encode(streamBuffer, exif.data(), quality);
+ if (jpeg_size < 0) {
+ LOG(JPEG, Error) << "Failed to encode stream image";
+ processComplete.emit(streamBuffer, PostProcessor::Status::Error);
+ return;
+ }
+
+ /* Fill in the JPEG blob header. */
+ uint8_t *resultPtr = destination->plane(0).data()
+ + destination->jpegBufferSize(cameraDevice_->maxJpegBufferSize())
+ - sizeof(struct camera3_jpeg_blob);
+ auto *blob = reinterpret_cast<struct camera3_jpeg_blob *>(resultPtr);
+ blob->jpeg_blob_id = CAMERA3_JPEG_BLOB_ID;
+ blob->jpeg_size = jpeg_size;
+
+ /* Update the JPEG result Metadata. */
+ resultMetadata->addEntry(ANDROID_JPEG_SIZE, jpeg_size);
+ processComplete.emit(streamBuffer, PostProcessor::Status::Success);
+}
diff --git a/src/android/jpeg/post_processor_jpeg.h b/src/android/jpeg/post_processor_jpeg.h
new file mode 100644
index 00000000..6fe21457
--- /dev/null
+++ b/src/android/jpeg/post_processor_jpeg.h
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * JPEG Post Processor
+ */
+
+#pragma once
+
+#include "../post_processor.h"
+#include "encoder_libjpeg.h"
+#include "thumbnailer.h"
+
+#include <libcamera/geometry.h>
+
+class CameraDevice;
+
+class PostProcessorJpeg : public PostProcessor
+{
+public:
+ PostProcessorJpeg(CameraDevice *const device);
+
+ int configure(const libcamera::StreamConfiguration &incfg,
+ const libcamera::StreamConfiguration &outcfg) override;
+ void process(Camera3RequestDescriptor::StreamBuffer *streamBuffer) override;
+
+private:
+ void generateThumbnail(const libcamera::FrameBuffer &source,
+ const libcamera::Size &targetSize,
+ unsigned int quality,
+ std::vector<unsigned char> *thumbnail);
+
+ CameraDevice *const cameraDevice_;
+ std::unique_ptr<Encoder> encoder_;
+ libcamera::Size streamSize_;
+ EncoderLibJpeg thumbnailEncoder_;
+ Thumbnailer thumbnailer_;
+};
diff --git a/src/android/jpeg/thumbnailer.cpp b/src/android/jpeg/thumbnailer.cpp
new file mode 100644
index 00000000..adafc468
--- /dev/null
+++ b/src/android/jpeg/thumbnailer.cpp
@@ -0,0 +1,96 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * Simple image thumbnailer
+ */
+
+#include "thumbnailer.h"
+
+#include <libcamera/base/log.h>
+
+#include <libcamera/formats.h>
+
+#include "libcamera/internal/mapped_framebuffer.h"
+
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(Thumbnailer)
+
+Thumbnailer::Thumbnailer()
+ : valid_(false)
+{
+}
+
+void Thumbnailer::configure(const Size &sourceSize, PixelFormat pixelFormat)
+{
+ sourceSize_ = sourceSize;
+ pixelFormat_ = pixelFormat;
+
+ if (pixelFormat_ != formats::NV12) {
+ LOG(Thumbnailer, Error)
+ << "Failed to configure: Pixel Format "
+ << pixelFormat_ << " unsupported.";
+ return;
+ }
+
+ valid_ = true;
+}
+
+void Thumbnailer::createThumbnail(const FrameBuffer &source,
+ const Size &targetSize,
+ std::vector<unsigned char> *destination)
+{
+ MappedFrameBuffer frame(&source, MappedFrameBuffer::MapFlag::Read);
+ if (!frame.isValid()) {
+ LOG(Thumbnailer, Error)
+ << "Failed to map FrameBuffer : "
+ << strerror(frame.error());
+ return;
+ }
+
+ if (!valid_) {
+ LOG(Thumbnailer, Error) << "Config is unconfigured or invalid.";
+ return;
+ }
+
+ const unsigned int sw = sourceSize_.width;
+ const unsigned int sh = sourceSize_.height;
+ const unsigned int tw = targetSize.width;
+ const unsigned int th = targetSize.height;
+
+ ASSERT(frame.planes().size() == 2);
+ ASSERT(tw % 2 == 0 && th % 2 == 0);
+
+ /* Image scaling block implementing nearest-neighbour algorithm. */
+ unsigned char *src = frame.planes()[0].data();
+ unsigned char *srcC = frame.planes()[1].data();
+ unsigned char *srcCb, *srcCr;
+ unsigned char *dstY, *srcY;
+
+ size_t dstSize = (th * tw) + ((th / 2) * tw);
+ destination->resize(dstSize);
+ unsigned char *dst = destination->data();
+ unsigned char *dstC = dst + th * tw;
+
+ for (unsigned int y = 0; y < th; y += 2) {
+ unsigned int sourceY = (sh * y + th / 2) / th;
+
+ dstY = dst + y * tw;
+ srcY = src + sw * sourceY;
+ srcCb = srcC + (sourceY / 2) * sw + 0;
+ srcCr = srcC + (sourceY / 2) * sw + 1;
+
+ for (unsigned int x = 0; x < tw; x += 2) {
+ unsigned int sourceX = (sw * x + tw / 2) / tw;
+
+ dstY[x] = srcY[sourceX];
+ dstY[tw + x] = srcY[sw + sourceX];
+ dstY[x + 1] = srcY[sourceX + 1];
+ dstY[tw + x + 1] = srcY[sw + sourceX + 1];
+
+ dstC[(y / 2) * tw + x + 0] = srcCb[(sourceX / 2) * 2];
+ dstC[(y / 2) * tw + x + 1] = srcCr[(sourceX / 2) * 2];
+ }
+ }
+}
diff --git a/src/android/jpeg/thumbnailer.h b/src/android/jpeg/thumbnailer.h
new file mode 100644
index 00000000..1b836e59
--- /dev/null
+++ b/src/android/jpeg/thumbnailer.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * Simple image thumbnailer
+ */
+
+#pragma once
+
+#include <libcamera/framebuffer.h>
+#include <libcamera/geometry.h>
+
+#include "libcamera/internal/formats.h"
+
+class Thumbnailer
+{
+public:
+ Thumbnailer();
+
+ void configure(const libcamera::Size &sourceSize,
+ libcamera::PixelFormat pixelFormat);
+ void createThumbnail(const libcamera::FrameBuffer &source,
+ const libcamera::Size &targetSize,
+ std::vector<unsigned char> *dest);
+ const libcamera::PixelFormat &pixelFormat() const { return pixelFormat_; }
+
+private:
+ libcamera::PixelFormat pixelFormat_;
+ libcamera::Size sourceSize_;
+
+ bool valid_;
+};