diff options
Diffstat (limited to 'src/android/jpeg')
-rw-r--r-- | src/android/jpeg/encoder.h | 26 | ||||
-rw-r--r-- | src/android/jpeg/encoder_jea.cpp | 56 | ||||
-rw-r--r-- | src/android/jpeg/encoder_jea.h | 31 | ||||
-rw-r--r-- | src/android/jpeg/encoder_libjpeg.cpp | 239 | ||||
-rw-r--r-- | src/android/jpeg/encoder_libjpeg.h | 44 | ||||
-rw-r--r-- | src/android/jpeg/exif.cpp | 522 | ||||
-rw-r--r-- | src/android/jpeg/exif.h | 112 | ||||
-rw-r--r-- | src/android/jpeg/meson.build | 14 | ||||
-rw-r--r-- | src/android/jpeg/post_processor_jpeg.cpp | 223 | ||||
-rw-r--r-- | src/android/jpeg/post_processor_jpeg.h | 38 | ||||
-rw-r--r-- | src/android/jpeg/thumbnailer.cpp | 96 | ||||
-rw-r--r-- | src/android/jpeg/thumbnailer.h | 32 |
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(×tamp, &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(×tamp, &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_; +}; |