diff options
author | Laurent Pinchart <laurent.pinchart@ideasonboard.com> | 2022-10-20 00:44:55 +0300 |
---|---|---|
committer | Laurent Pinchart <laurent.pinchart@ideasonboard.com> | 2022-10-20 13:36:25 +0300 |
commit | 84ad104499d9efc0253dae1a60ee070ed375ad95 (patch) | |
tree | d10fd53eb79cebb28fa3f72b18b46dddb6382b83 /src/apps | |
parent | daf3f4b59f4ea0ecb42c6a39fe909f071d3a2842 (diff) |
Move test applications to src/apps/
The cam and qcam test application share code, currently through a crude
hack that references the cam source files directly from the qcam
meson.build file. To prepare for the introduction of hosting that code
in a static library, move all applications to src/apps/.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Diffstat (limited to 'src/apps')
349 files changed, 11117 insertions, 0 deletions
diff --git a/src/apps/cam/camera_session.cpp b/src/apps/cam/camera_session.cpp new file mode 100644 index 00000000..6b409c98 --- /dev/null +++ b/src/apps/cam/camera_session.cpp @@ -0,0 +1,450 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * camera_session.cpp - Camera capture session + */ + +#include <iomanip> +#include <iostream> +#include <limits.h> +#include <sstream> + +#include <libcamera/control_ids.h> +#include <libcamera/property_ids.h> + +#include "camera_session.h" +#include "capture_script.h" +#include "event_loop.h" +#include "file_sink.h" +#ifdef HAVE_KMS +#include "kms_sink.h" +#endif +#include "main.h" +#ifdef HAVE_SDL +#include "sdl_sink.h" +#endif +#include "stream_options.h" + +using namespace libcamera; + +CameraSession::CameraSession(CameraManager *cm, + const std::string &cameraId, + unsigned int cameraIndex, + const OptionsParser::Options &options) + : options_(options), cameraIndex_(cameraIndex), last_(0), + queueCount_(0), captureCount_(0), captureLimit_(0), + printMetadata_(false) +{ + char *endptr; + unsigned long index = strtoul(cameraId.c_str(), &endptr, 10); + if (*endptr == '\0' && index > 0 && index <= cm->cameras().size()) + camera_ = cm->cameras()[index - 1]; + else + camera_ = cm->get(cameraId); + + if (!camera_) { + std::cerr << "Camera " << cameraId << " not found" << std::endl; + return; + } + + if (camera_->acquire()) { + std::cerr << "Failed to acquire camera " << cameraId + << std::endl; + return; + } + + StreamRoles roles = StreamKeyValueParser::roles(options_[OptStream]); + + std::unique_ptr<CameraConfiguration> config = + camera_->generateConfiguration(roles); + if (!config || config->size() != roles.size()) { + std::cerr << "Failed to get default stream configuration" + << std::endl; + return; + } + + /* Apply configuration if explicitly requested. */ + if (StreamKeyValueParser::updateConfiguration(config.get(), + options_[OptStream])) { + std::cerr << "Failed to update configuration" << std::endl; + return; + } + + bool strictFormats = options_.isSet(OptStrictFormats); + +#ifdef HAVE_KMS + if (options_.isSet(OptDisplay)) { + if (options_.isSet(OptFile)) { + std::cerr << "--display and --file options are mutually exclusive" + << std::endl; + return; + } + + if (roles.size() != 1) { + std::cerr << "Display doesn't support multiple streams" + << std::endl; + return; + } + + if (roles[0] != StreamRole::Viewfinder) { + std::cerr << "Display requires a viewfinder stream" + << std::endl; + return; + } + } +#endif + + if (options_.isSet(OptCaptureScript)) { + std::string scriptName = options_[OptCaptureScript].toString(); + script_ = std::make_unique<CaptureScript>(camera_, scriptName); + if (!script_->valid()) { + std::cerr << "Invalid capture script '" << scriptName + << "'" << std::endl; + return; + } + } + + switch (config->validate()) { + case CameraConfiguration::Valid: + break; + + case CameraConfiguration::Adjusted: + if (strictFormats) { + std::cout << "Adjusting camera configuration disallowed by --strict-formats argument" + << std::endl; + return; + } + std::cout << "Camera configuration adjusted" << std::endl; + break; + + case CameraConfiguration::Invalid: + std::cout << "Camera configuration invalid" << std::endl; + return; + } + + config_ = std::move(config); +} + +CameraSession::~CameraSession() +{ + if (camera_) + camera_->release(); +} + +void CameraSession::listControls() const +{ + for (const auto &[id, info] : camera_->controls()) { + std::cout << "Control: " << id->name() << ": " + << info.toString() << std::endl; + } +} + +void CameraSession::listProperties() const +{ + for (const auto &[key, value] : camera_->properties()) { + const ControlId *id = properties::properties.at(key); + + std::cout << "Property: " << id->name() << " = " + << value.toString() << std::endl; + } +} + +void CameraSession::infoConfiguration() const +{ + unsigned int index = 0; + for (const StreamConfiguration &cfg : *config_) { + std::cout << index << ": " << cfg.toString() << std::endl; + + const StreamFormats &formats = cfg.formats(); + for (PixelFormat pixelformat : formats.pixelformats()) { + std::cout << " * Pixelformat: " + << pixelformat << " " + << formats.range(pixelformat).toString() + << std::endl; + + for (const Size &size : formats.sizes(pixelformat)) + std::cout << " - " << size << std::endl; + } + + index++; + } +} + +int CameraSession::start() +{ + int ret; + + queueCount_ = 0; + captureCount_ = 0; + captureLimit_ = options_[OptCapture].toInteger(); + printMetadata_ = options_.isSet(OptMetadata); + + ret = camera_->configure(config_.get()); + if (ret < 0) { + std::cout << "Failed to configure camera" << std::endl; + return ret; + } + + streamNames_.clear(); + for (unsigned int index = 0; index < config_->size(); ++index) { + StreamConfiguration &cfg = config_->at(index); + streamNames_[cfg.stream()] = "cam" + std::to_string(cameraIndex_) + + "-stream" + std::to_string(index); + } + + camera_->requestCompleted.connect(this, &CameraSession::requestComplete); + +#ifdef HAVE_KMS + if (options_.isSet(OptDisplay)) + sink_ = std::make_unique<KMSSink>(options_[OptDisplay].toString()); +#endif + +#ifdef HAVE_SDL + if (options_.isSet(OptSDL)) + sink_ = std::make_unique<SDLSink>(); +#endif + + if (options_.isSet(OptFile)) { + if (!options_[OptFile].toString().empty()) + sink_ = std::make_unique<FileSink>(camera_.get(), streamNames_, + options_[OptFile]); + else + sink_ = std::make_unique<FileSink>(camera_.get(), streamNames_); + } + + if (sink_) { + ret = sink_->configure(*config_); + if (ret < 0) { + std::cout << "Failed to configure frame sink" + << std::endl; + return ret; + } + + sink_->requestProcessed.connect(this, &CameraSession::sinkRelease); + } + + allocator_ = std::make_unique<FrameBufferAllocator>(camera_); + + return startCapture(); +} + +void CameraSession::stop() +{ + int ret = camera_->stop(); + if (ret) + std::cout << "Failed to stop capture" << std::endl; + + if (sink_) { + ret = sink_->stop(); + if (ret) + std::cout << "Failed to stop frame sink" << std::endl; + } + + sink_.reset(); + + requests_.clear(); + + allocator_.reset(); +} + +int CameraSession::startCapture() +{ + int ret; + + /* Identify the stream with the least number of buffers. */ + unsigned int nbuffers = UINT_MAX; + for (StreamConfiguration &cfg : *config_) { + ret = allocator_->allocate(cfg.stream()); + if (ret < 0) { + std::cerr << "Can't allocate buffers" << std::endl; + return -ENOMEM; + } + + unsigned int allocated = allocator_->buffers(cfg.stream()).size(); + nbuffers = std::min(nbuffers, allocated); + } + + /* + * TODO: make cam tool smarter to support still capture by for + * example pushing a button. For now run all streams all the time. + */ + + for (unsigned int i = 0; i < nbuffers; i++) { + std::unique_ptr<Request> request = camera_->createRequest(); + if (!request) { + std::cerr << "Can't create request" << std::endl; + return -ENOMEM; + } + + for (StreamConfiguration &cfg : *config_) { + Stream *stream = cfg.stream(); + const std::vector<std::unique_ptr<FrameBuffer>> &buffers = + allocator_->buffers(stream); + const std::unique_ptr<FrameBuffer> &buffer = buffers[i]; + + ret = request->addBuffer(stream, buffer.get()); + if (ret < 0) { + std::cerr << "Can't set buffer for request" + << std::endl; + return ret; + } + + if (sink_) + sink_->mapBuffer(buffer.get()); + } + + requests_.push_back(std::move(request)); + } + + if (sink_) { + ret = sink_->start(); + if (ret) { + std::cout << "Failed to start frame sink" << std::endl; + return ret; + } + } + + ret = camera_->start(); + if (ret) { + std::cout << "Failed to start capture" << std::endl; + if (sink_) + sink_->stop(); + return ret; + } + + for (std::unique_ptr<Request> &request : requests_) { + ret = queueRequest(request.get()); + if (ret < 0) { + std::cerr << "Can't queue request" << std::endl; + camera_->stop(); + if (sink_) + sink_->stop(); + return ret; + } + } + + if (captureLimit_) + std::cout << "cam" << cameraIndex_ + << ": Capture " << captureLimit_ << " frames" + << std::endl; + else + std::cout << "cam" << cameraIndex_ + << ": Capture until user interrupts by SIGINT" + << std::endl; + + return 0; +} + +int CameraSession::queueRequest(Request *request) +{ + if (captureLimit_ && queueCount_ >= captureLimit_) + return 0; + + if (script_) + request->controls() = script_->frameControls(queueCount_); + + queueCount_++; + + return camera_->queueRequest(request); +} + +void CameraSession::requestComplete(Request *request) +{ + if (request->status() == Request::RequestCancelled) + return; + + /* + * Defer processing of the completed request to the event loop, to avoid + * blocking the camera manager thread. + */ + EventLoop::instance()->callLater([=]() { processRequest(request); }); +} + +void CameraSession::processRequest(Request *request) +{ + /* + * If we've reached the capture limit, we're done. This doesn't + * duplicate the check below that emits the captureDone signal, as this + * function will be called for each request still in flight after the + * capture limit is reached and we don't want to emit the signal every + * single time. + */ + if (captureLimit_ && captureCount_ >= captureLimit_) + return; + + const Request::BufferMap &buffers = request->buffers(); + + /* + * Compute the frame rate. The timestamp is arbitrarily retrieved from + * the first buffer, as all buffers should have matching timestamps. + */ + uint64_t ts = buffers.begin()->second->metadata().timestamp; + double fps = ts - last_; + fps = last_ != 0 && fps ? 1000000000.0 / fps : 0.0; + last_ = ts; + + bool requeue = true; + + std::stringstream info; + info << ts / 1000000000 << "." + << std::setw(6) << std::setfill('0') << ts / 1000 % 1000000 + << " (" << std::fixed << std::setprecision(2) << fps << " fps)"; + + for (const auto &[stream, buffer] : buffers) { + const FrameMetadata &metadata = buffer->metadata(); + + info << " " << streamNames_[stream] + << " seq: " << std::setw(6) << std::setfill('0') << metadata.sequence + << " bytesused: "; + + unsigned int nplane = 0; + for (const FrameMetadata::Plane &plane : metadata.planes()) { + info << plane.bytesused; + if (++nplane < metadata.planes().size()) + info << "/"; + } + } + + if (sink_) { + if (!sink_->processRequest(request)) + requeue = false; + } + + std::cout << info.str() << std::endl; + + if (printMetadata_) { + const ControlList &requestMetadata = request->metadata(); + for (const auto &[key, value] : requestMetadata) { + const ControlId *id = controls::controls.at(key); + std::cout << "\t" << id->name() << " = " + << value.toString() << std::endl; + } + } + + /* + * Notify the user that capture is complete if the limit has just been + * reached. + */ + captureCount_++; + if (captureLimit_ && captureCount_ >= captureLimit_) { + captureDone.emit(); + return; + } + + /* + * If the frame sink holds on the request, we'll requeue it later in the + * complete handler. + */ + if (!requeue) + return; + + request->reuse(Request::ReuseBuffers); + queueRequest(request); +} + +void CameraSession::sinkRelease(Request *request) +{ + request->reuse(Request::ReuseBuffers); + queueRequest(request); +} diff --git a/src/apps/cam/camera_session.h b/src/apps/cam/camera_session.h new file mode 100644 index 00000000..d562caae --- /dev/null +++ b/src/apps/cam/camera_session.h @@ -0,0 +1,79 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * camera_session.h - Camera capture session + */ + +#pragma once + +#include <memory> +#include <stdint.h> +#include <string> +#include <vector> + +#include <libcamera/base/signal.h> + +#include <libcamera/camera.h> +#include <libcamera/camera_manager.h> +#include <libcamera/framebuffer.h> +#include <libcamera/framebuffer_allocator.h> +#include <libcamera/request.h> +#include <libcamera/stream.h> + +#include "options.h" + +class CaptureScript; +class FrameSink; + +class CameraSession +{ +public: + CameraSession(libcamera::CameraManager *cm, + const std::string &cameraId, unsigned int cameraIndex, + const OptionsParser::Options &options); + ~CameraSession(); + + bool isValid() const { return config_ != nullptr; } + const OptionsParser::Options &options() { return options_; } + + libcamera::Camera *camera() { return camera_.get(); } + libcamera::CameraConfiguration *config() { return config_.get(); } + + void listControls() const; + void listProperties() const; + void infoConfiguration() const; + + int start(); + void stop(); + + libcamera::Signal<> captureDone; + +private: + int startCapture(); + + int queueRequest(libcamera::Request *request); + void requestComplete(libcamera::Request *request); + void processRequest(libcamera::Request *request); + void sinkRelease(libcamera::Request *request); + + const OptionsParser::Options &options_; + std::shared_ptr<libcamera::Camera> camera_; + std::unique_ptr<libcamera::CameraConfiguration> config_; + + std::unique_ptr<CaptureScript> script_; + + std::map<const libcamera::Stream *, std::string> streamNames_; + std::unique_ptr<FrameSink> sink_; + unsigned int cameraIndex_; + + uint64_t last_; + + unsigned int queueCount_; + unsigned int captureCount_; + unsigned int captureLimit_; + bool printMetadata_; + + std::unique_ptr<libcamera::FrameBufferAllocator> allocator_; + std::vector<std::unique_ptr<libcamera::Request>> requests_; +}; diff --git a/src/apps/cam/capture-script.yaml b/src/apps/cam/capture-script.yaml new file mode 100644 index 00000000..7118865e --- /dev/null +++ b/src/apps/cam/capture-script.yaml @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: CC0-1.0 + +# Capture script example +# +# A capture script allows to associate a list of controls and their values +# to frame numbers. +# +# The script allows defining a list of frames associated with controls +# and an optional list of properties that can control the script behaviour. + +# properties: +# # Repeat the controls every 'idx' frames. +# - loop: idx +# +# # List of frame number with associated a list of controls to be applied +# frames: +# - frame-number: +# Control1: value1 +# Control2: value2 + +# \todo Formally define the capture script structure with a schema + +# Notes: +# - Controls have to be specified by name, as defined in the +# libcamera::controls:: enumeration +# - Controls not supported by the camera currently operated are ignored +# - Frame numbers shall be monotonically incrementing, gaps are allowed +# - If a loop limit is specified, frame numbers in the 'frames' list shall be +# less than the loop control + +# Example: Turn brightness up and down every 460 frames + +properties: + - loop: 460 + +frames: + - 0: + Brightness: 0.0 + + - 40: + Brightness: 0.2 + + - 80: + Brightness: 0.4 + + - 120: + Brightness: 0.8 + + - 160: + Brightness: 0.4 + + - 200: + Brightness: 0.2 + + - 240: + Brightness: 0.0 + + - 280: + Brightness: -0.2 + + - 300: + Brightness: -0.4 + + - 340: + Brightness: -0.8 + + - 380: + Brightness: -0.4 + + - 420: + Brightness: -0.2 diff --git a/src/apps/cam/capture_script.cpp b/src/apps/cam/capture_script.cpp new file mode 100644 index 00000000..5a27361c --- /dev/null +++ b/src/apps/cam/capture_script.cpp @@ -0,0 +1,535 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy + * + * capture_script.cpp - Capture session configuration script + */ + +#include "capture_script.h" + +#include <iostream> +#include <stdio.h> +#include <stdlib.h> + +using namespace libcamera; + +CaptureScript::CaptureScript(std::shared_ptr<Camera> camera, + const std::string &fileName) + : camera_(camera), loop_(0), valid_(false) +{ + FILE *fh = fopen(fileName.c_str(), "r"); + if (!fh) { + int ret = -errno; + std::cerr << "Failed to open capture script " << fileName + << ": " << strerror(-ret) << std::endl; + return; + } + + /* + * Map the camera's controls to their name so that they can be + * easily identified when parsing the script file. + */ + for (const auto &[control, info] : camera_->controls()) + controls_[control->name()] = control; + + int ret = parseScript(fh); + fclose(fh); + if (ret) + return; + + valid_ = true; +} + +/* Retrieve the control list associated with a frame number. */ +const ControlList &CaptureScript::frameControls(unsigned int frame) +{ + static ControlList controls{}; + unsigned int idx = frame; + + /* If we loop, repeat the controls every 'loop_' frames. */ + if (loop_) + idx = frame % loop_; + + auto it = frameControls_.find(idx); + if (it == frameControls_.end()) + return controls; + + return it->second; +} + +CaptureScript::EventPtr CaptureScript::nextEvent(yaml_event_type_t expectedType) +{ + EventPtr event(new yaml_event_t); + + if (!yaml_parser_parse(&parser_, event.get())) + return nullptr; + + if (expectedType != YAML_NO_EVENT && !checkEvent(event, expectedType)) + return nullptr; + + return event; +} + +bool CaptureScript::checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const +{ + if (event->type != expectedType) { + std::cerr << "Capture script error on line " << event->start_mark.line + << " column " << event->start_mark.column << ": " + << "Expected " << eventTypeName(expectedType) + << " event, got " << eventTypeName(event->type) + << std::endl; + return false; + } + + return true; +} + +std::string CaptureScript::eventScalarValue(const EventPtr &event) +{ + return std::string(reinterpret_cast<char *>(event->data.scalar.value), + event->data.scalar.length); +} + +std::string CaptureScript::eventTypeName(yaml_event_type_t type) +{ + static const std::map<yaml_event_type_t, std::string> typeNames = { + { YAML_STREAM_START_EVENT, "stream-start" }, + { YAML_STREAM_END_EVENT, "stream-end" }, + { YAML_DOCUMENT_START_EVENT, "document-start" }, + { YAML_DOCUMENT_END_EVENT, "document-end" }, + { YAML_ALIAS_EVENT, "alias" }, + { YAML_SCALAR_EVENT, "scalar" }, + { YAML_SEQUENCE_START_EVENT, "sequence-start" }, + { YAML_SEQUENCE_END_EVENT, "sequence-end" }, + { YAML_MAPPING_START_EVENT, "mapping-start" }, + { YAML_MAPPING_END_EVENT, "mapping-end" }, + }; + + auto it = typeNames.find(type); + if (it == typeNames.end()) + return "[type " + std::to_string(type) + "]"; + + return it->second; +} + +int CaptureScript::parseScript(FILE *script) +{ + int ret = yaml_parser_initialize(&parser_); + if (!ret) { + std::cerr << "Failed to initialize yaml parser" << std::endl; + return ret; + } + + /* Delete the parser upon function exit. */ + struct ParserDeleter { + ParserDeleter(yaml_parser_t *parser) : parser_(parser) { } + ~ParserDeleter() { yaml_parser_delete(parser_); } + yaml_parser_t *parser_; + } deleter(&parser_); + + yaml_parser_set_input_file(&parser_, script); + + EventPtr event = nextEvent(YAML_STREAM_START_EVENT); + if (!event) + return -EINVAL; + + event = nextEvent(YAML_DOCUMENT_START_EVENT); + if (!event) + return -EINVAL; + + event = nextEvent(YAML_MAPPING_START_EVENT); + if (!event) + return -EINVAL; + + while (1) { + event = nextEvent(); + if (!event) + return -EINVAL; + + if (event->type == YAML_MAPPING_END_EVENT) + return 0; + + if (!checkEvent(event, YAML_SCALAR_EVENT)) + return -EINVAL; + + std::string section = eventScalarValue(event); + + if (section == "properties") { + ret = parseProperties(); + if (ret) + return ret; + } else if (section == "frames") { + ret = parseFrames(); + if (ret) + return ret; + } else { + std::cerr << "Unsupported section '" << section << "'" + << std::endl; + return -EINVAL; + } + } +} + +int CaptureScript::parseProperty() +{ + EventPtr event = nextEvent(YAML_MAPPING_START_EVENT); + if (!event) + return -EINVAL; + + std::string prop = parseScalar(); + if (prop.empty()) + return -EINVAL; + + if (prop == "loop") { + event = nextEvent(); + if (!event) + return -EINVAL; + + std::string value = eventScalarValue(event); + if (value.empty()) + return -EINVAL; + + loop_ = atoi(value.c_str()); + if (!loop_) { + std::cerr << "Invalid loop limit '" << loop_ << "'" + << std::endl; + return -EINVAL; + } + } else { + std::cerr << "Unsupported property '" << prop << "'" << std::endl; + return -EINVAL; + } + + event = nextEvent(YAML_MAPPING_END_EVENT); + if (!event) + return -EINVAL; + + return 0; +} + +int CaptureScript::parseProperties() +{ + EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT); + if (!event) + return -EINVAL; + + while (1) { + if (event->type == YAML_SEQUENCE_END_EVENT) + return 0; + + int ret = parseProperty(); + if (ret) + return ret; + + event = nextEvent(); + if (!event) + return -EINVAL; + } + + return 0; +} + +int CaptureScript::parseFrames() +{ + EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT); + if (!event) + return -EINVAL; + + while (1) { + event = nextEvent(); + if (!event) + return -EINVAL; + + if (event->type == YAML_SEQUENCE_END_EVENT) + return 0; + + int ret = parseFrame(std::move(event)); + if (ret) + return ret; + } +} + +int CaptureScript::parseFrame(EventPtr event) +{ + if (!checkEvent(event, YAML_MAPPING_START_EVENT)) + return -EINVAL; + + std::string key = parseScalar(); + if (key.empty()) + return -EINVAL; + + unsigned int frameId = atoi(key.c_str()); + if (loop_ && frameId >= loop_) { + std::cerr + << "Frame id (" << frameId << ") shall be smaller than" + << "loop limit (" << loop_ << ")" << std::endl; + return -EINVAL; + } + + event = nextEvent(YAML_MAPPING_START_EVENT); + if (!event) + return -EINVAL; + + ControlList controls{}; + + while (1) { + event = nextEvent(); + if (!event) + return -EINVAL; + + if (event->type == YAML_MAPPING_END_EVENT) + break; + + int ret = parseControl(std::move(event), controls); + if (ret) + return ret; + } + + frameControls_[frameId] = std::move(controls); + + event = nextEvent(YAML_MAPPING_END_EVENT); + if (!event) + return -EINVAL; + + return 0; +} + +int CaptureScript::parseControl(EventPtr event, ControlList &controls) +{ + /* We expect a value after a key. */ + std::string name = eventScalarValue(event); + if (name.empty()) + return -EINVAL; + + /* If the camera does not support the control just ignore it. */ + auto it = controls_.find(name); + if (it == controls_.end()) { + std::cerr << "Unsupported control '" << name << "'" << std::endl; + return -EINVAL; + } + + const ControlId *controlId = it->second; + + ControlValue val = unpackControl(controlId); + if (val.isNone()) { + std::cerr << "Error unpacking control '" << name << "'" + << std::endl; + return -EINVAL; + } + + controls.set(controlId->id(), val); + + return 0; +} + +std::string CaptureScript::parseScalar() +{ + EventPtr event = nextEvent(YAML_SCALAR_EVENT); + if (!event) + return ""; + + return eventScalarValue(event); +} + +ControlValue CaptureScript::parseRectangles() +{ + std::vector<libcamera::Rectangle> rectangles; + + std::vector<std::vector<std::string>> arrays = parseArrays(); + if (arrays.empty()) + return {}; + + for (const std::vector<std::string> &values : arrays) { + if (values.size() != 4) { + std::cerr << "Error parsing Rectangle: expected " + << "array with 4 parameters" << std::endl; + return {}; + } + + Rectangle rect = unpackRectangle(values); + rectangles.push_back(rect); + } + + ControlValue controlValue; + controlValue.set(Span<const Rectangle>(rectangles)); + + return controlValue; +} + +std::vector<std::vector<std::string>> CaptureScript::parseArrays() +{ + EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT); + if (!event) + return {}; + + event = nextEvent(); + if (!event) + return {}; + + std::vector<std::vector<std::string>> valueArrays; + + /* Parse single array. */ + if (event->type == YAML_SCALAR_EVENT) { + std::string firstValue = eventScalarValue(event); + if (firstValue.empty()) + return {}; + + std::vector<std::string> remaining = parseSingleArray(); + + std::vector<std::string> values = { firstValue }; + values.insert(std::end(values), + std::begin(remaining), std::end(remaining)); + valueArrays.push_back(values); + + return valueArrays; + } + + /* Parse array of arrays. */ + while (1) { + switch (event->type) { + case YAML_SEQUENCE_START_EVENT: { + std::vector<std::string> values = parseSingleArray(); + valueArrays.push_back(values); + break; + } + case YAML_SEQUENCE_END_EVENT: + return valueArrays; + default: + return {}; + } + + event = nextEvent(); + if (!event) + return {}; + } +} + +std::vector<std::string> CaptureScript::parseSingleArray() +{ + std::vector<std::string> values; + + while (1) { + EventPtr event = nextEvent(); + if (!event) + return {}; + + switch (event->type) { + case YAML_SCALAR_EVENT: { + std::string value = eventScalarValue(event); + if (value.empty()) + return {}; + values.push_back(value); + break; + } + case YAML_SEQUENCE_END_EVENT: + return values; + default: + return {}; + } + } +} + +void CaptureScript::unpackFailure(const ControlId *id, const std::string &repr) +{ + static const std::map<unsigned int, const char *> typeNames = { + { ControlTypeNone, "none" }, + { ControlTypeBool, "bool" }, + { ControlTypeByte, "byte" }, + { ControlTypeInteger32, "int32" }, + { ControlTypeInteger64, "int64" }, + { ControlTypeFloat, "float" }, + { ControlTypeString, "string" }, + { ControlTypeRectangle, "Rectangle" }, + { ControlTypeSize, "Size" }, + }; + + const char *typeName; + auto it = typeNames.find(id->type()); + if (it != typeNames.end()) + typeName = it->second; + else + typeName = "unknown"; + + std::cerr << "Unsupported control '" << repr << "' for " + << typeName << " control " << id->name() << std::endl; +} + +ControlValue CaptureScript::unpackControl(const ControlId *id) +{ + /* Parse complex types. */ + switch (id->type()) { + case ControlTypeRectangle: + return parseRectangles(); + case ControlTypeSize: + /* \todo Parse Sizes. */ + return {}; + default: + break; + } + + /* Parse basic types represented by a single scalar. */ + const std::string repr = parseScalar(); + if (repr.empty()) + return {}; + + ControlValue value{}; + + switch (id->type()) { + case ControlTypeNone: + break; + case ControlTypeBool: { + bool val; + + if (repr == "true") { + val = true; + } else if (repr == "false") { + val = false; + } else { + unpackFailure(id, repr); + return value; + } + + value.set<bool>(val); + break; + } + case ControlTypeByte: { + uint8_t val = strtol(repr.c_str(), NULL, 10); + value.set<uint8_t>(val); + break; + } + case ControlTypeInteger32: { + int32_t val = strtol(repr.c_str(), NULL, 10); + value.set<int32_t>(val); + break; + } + case ControlTypeInteger64: { + int64_t val = strtoll(repr.c_str(), NULL, 10); + value.set<int64_t>(val); + break; + } + case ControlTypeFloat: { + float val = strtof(repr.c_str(), NULL); + value.set<float>(val); + break; + } + case ControlTypeString: { + value.set<std::string>(repr); + break; + } + default: + std::cerr << "Unsupported control type" << std::endl; + break; + } + + return value; +} + +libcamera::Rectangle CaptureScript::unpackRectangle(const std::vector<std::string> &strVec) +{ + int x = strtol(strVec[0].c_str(), NULL, 10); + int y = strtol(strVec[1].c_str(), NULL, 10); + unsigned int width = strtoul(strVec[2].c_str(), NULL, 10); + unsigned int height = strtoul(strVec[3].c_str(), NULL, 10); + + return Rectangle(x, y, width, height); +} diff --git a/src/apps/cam/capture_script.h b/src/apps/cam/capture_script.h new file mode 100644 index 00000000..7a0ddebb --- /dev/null +++ b/src/apps/cam/capture_script.h @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy + * + * capture_script.h - Capture session configuration script + */ + +#pragma once + +#include <map> +#include <memory> +#include <string> + +#include <libcamera/camera.h> +#include <libcamera/controls.h> + +#include <yaml.h> + +class CaptureScript +{ +public: + CaptureScript(std::shared_ptr<libcamera::Camera> camera, + const std::string &fileName); + + bool valid() const { return valid_; } + + const libcamera::ControlList &frameControls(unsigned int frame); + +private: + struct EventDeleter { + void operator()(yaml_event_t *event) const + { + yaml_event_delete(event); + delete event; + } + }; + using EventPtr = std::unique_ptr<yaml_event_t, EventDeleter>; + + std::map<std::string, const libcamera::ControlId *> controls_; + std::map<unsigned int, libcamera::ControlList> frameControls_; + std::shared_ptr<libcamera::Camera> camera_; + yaml_parser_t parser_; + unsigned int loop_; + bool valid_; + + EventPtr nextEvent(yaml_event_type_t expectedType = YAML_NO_EVENT); + bool checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const; + static std::string eventScalarValue(const EventPtr &event); + static std::string eventTypeName(yaml_event_type_t type); + + int parseScript(FILE *script); + + int parseProperties(); + int parseProperty(); + int parseFrames(); + int parseFrame(EventPtr event); + int parseControl(EventPtr event, libcamera::ControlList &controls); + + std::string parseScalar(); + libcamera::ControlValue parseRectangles(); + std::vector<std::vector<std::string>> parseArrays(); + std::vector<std::string> parseSingleArray(); + + void unpackFailure(const libcamera::ControlId *id, + const std::string &repr); + libcamera::ControlValue unpackControl(const libcamera::ControlId *id); + libcamera::Rectangle unpackRectangle(const std::vector<std::string> &strVec); +}; diff --git a/src/apps/cam/dng_writer.cpp b/src/apps/cam/dng_writer.cpp new file mode 100644 index 00000000..c945edce --- /dev/null +++ b/src/apps/cam/dng_writer.cpp @@ -0,0 +1,653 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2020, Raspberry Pi Ltd + * + * dng_writer.cpp - 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) { + 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/cam/dng_writer.h b/src/apps/cam/dng_writer.h new file mode 100644 index 00000000..38f38f62 --- /dev/null +++ b/src/apps/cam/dng_writer.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2020, Raspberry Pi Ltd + * + * dng_writer.h - 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/cam/drm.cpp b/src/apps/cam/drm.cpp new file mode 100644 index 00000000..2e4d7985 --- /dev/null +++ b/src/apps/cam/drm.cpp @@ -0,0 +1,717 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Ideas on Board Oy + * + * drm.cpp - DRM/KMS Helpers + */ + +#include "drm.h" + +#include <algorithm> +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <iostream> +#include <set> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <libcamera/framebuffer.h> +#include <libcamera/geometry.h> +#include <libcamera/pixel_format.h> + +#include <libdrm/drm_mode.h> + +#include "event_loop.h" + +namespace DRM { + +Object::Object(Device *dev, uint32_t id, Type type) + : id_(id), dev_(dev), type_(type) +{ + /* Retrieve properties from the objects that support them. */ + if (type != TypeConnector && type != TypeCrtc && + type != TypeEncoder && type != TypePlane) + return; + + /* + * We can't distinguish between failures due to the object having no + * property and failures due to other conditions. Assume we use the API + * correctly and consider the object has no property. + */ + drmModeObjectProperties *properties = drmModeObjectGetProperties(dev->fd(), id, type); + if (!properties) + return; + + properties_.reserve(properties->count_props); + for (uint32_t i = 0; i < properties->count_props; ++i) + properties_.emplace_back(properties->props[i], + properties->prop_values[i]); + + drmModeFreeObjectProperties(properties); +} + +Object::~Object() +{ +} + +const Property *Object::property(const std::string &name) const +{ + for (const PropertyValue &pv : properties_) { + const Property *property = static_cast<const Property *>(dev_->object(pv.id())); + if (property && property->name() == name) + return property; + } + + return nullptr; +} + +const PropertyValue *Object::propertyValue(const std::string &name) const +{ + for (const PropertyValue &pv : properties_) { + const Property *property = static_cast<const Property *>(dev_->object(pv.id())); + if (property && property->name() == name) + return &pv; + } + + return nullptr; +} + +Property::Property(Device *dev, drmModePropertyRes *property) + : Object(dev, property->prop_id, TypeProperty), + name_(property->name), flags_(property->flags), + values_(property->values, property->values + property->count_values), + blobs_(property->blob_ids, property->blob_ids + property->count_blobs) +{ + if (drm_property_type_is(property, DRM_MODE_PROP_RANGE)) + type_ = TypeRange; + else if (drm_property_type_is(property, DRM_MODE_PROP_ENUM)) + type_ = TypeEnum; + else if (drm_property_type_is(property, DRM_MODE_PROP_BLOB)) + type_ = TypeBlob; + else if (drm_property_type_is(property, DRM_MODE_PROP_BITMASK)) + type_ = TypeBitmask; + else if (drm_property_type_is(property, DRM_MODE_PROP_OBJECT)) + type_ = TypeObject; + else if (drm_property_type_is(property, DRM_MODE_PROP_SIGNED_RANGE)) + type_ = TypeSignedRange; + else + type_ = TypeUnknown; + + for (int i = 0; i < property->count_enums; ++i) + enums_[property->enums[i].value] = property->enums[i].name; +} + +Blob::Blob(Device *dev, const libcamera::Span<const uint8_t> &data) + : Object(dev, 0, Object::TypeBlob) +{ + drmModeCreatePropertyBlob(dev->fd(), data.data(), data.size(), &id_); +} + +Blob::~Blob() +{ + if (isValid()) + drmModeDestroyPropertyBlob(device()->fd(), id()); +} + +Mode::Mode(const drmModeModeInfo &mode) + : drmModeModeInfo(mode) +{ +} + +std::unique_ptr<Blob> Mode::toBlob(Device *dev) const +{ + libcamera::Span<const uint8_t> data{ reinterpret_cast<const uint8_t *>(this), + sizeof(*this) }; + return std::make_unique<Blob>(dev, data); +} + +Crtc::Crtc(Device *dev, const drmModeCrtc *crtc, unsigned int index) + : Object(dev, crtc->crtc_id, Object::TypeCrtc), index_(index) +{ +} + +Encoder::Encoder(Device *dev, const drmModeEncoder *encoder) + : Object(dev, encoder->encoder_id, Object::TypeEncoder), + type_(encoder->encoder_type) +{ + const std::list<Crtc> &crtcs = dev->crtcs(); + possibleCrtcs_.reserve(crtcs.size()); + + for (const Crtc &crtc : crtcs) { + if (encoder->possible_crtcs & (1 << crtc.index())) + possibleCrtcs_.push_back(&crtc); + } + + possibleCrtcs_.shrink_to_fit(); +} + +namespace { + +const std::map<uint32_t, const char *> connectorTypeNames{ + { DRM_MODE_CONNECTOR_Unknown, "Unknown" }, + { DRM_MODE_CONNECTOR_VGA, "VGA" }, + { DRM_MODE_CONNECTOR_DVII, "DVI-I" }, + { DRM_MODE_CONNECTOR_DVID, "DVI-D" }, + { DRM_MODE_CONNECTOR_DVIA, "DVI-A" }, + { DRM_MODE_CONNECTOR_Composite, "Composite" }, + { DRM_MODE_CONNECTOR_SVIDEO, "S-Video" }, + { DRM_MODE_CONNECTOR_LVDS, "LVDS" }, + { DRM_MODE_CONNECTOR_Component, "Component" }, + { DRM_MODE_CONNECTOR_9PinDIN, "9-Pin-DIN" }, + { DRM_MODE_CONNECTOR_DisplayPort, "DP" }, + { DRM_MODE_CONNECTOR_HDMIA, "HDMI-A" }, + { DRM_MODE_CONNECTOR_HDMIB, "HDMI-B" }, + { DRM_MODE_CONNECTOR_TV, "TV" }, + { DRM_MODE_CONNECTOR_eDP, "eDP" }, + { DRM_MODE_CONNECTOR_VIRTUAL, "Virtual" }, + { DRM_MODE_CONNECTOR_DSI, "DSI" }, + { DRM_MODE_CONNECTOR_DPI, "DPI" }, +}; + +} /* namespace */ + +Connector::Connector(Device *dev, const drmModeConnector *connector) + : Object(dev, connector->connector_id, Object::TypeConnector), + type_(connector->connector_type) +{ + auto typeName = connectorTypeNames.find(connector->connector_type); + if (typeName == connectorTypeNames.end()) { + std::cerr + << "Invalid connector type " + << connector->connector_type << std::endl; + typeName = connectorTypeNames.find(DRM_MODE_CONNECTOR_Unknown); + } + + name_ = std::string(typeName->second) + "-" + + std::to_string(connector->connector_type_id); + + switch (connector->connection) { + case DRM_MODE_CONNECTED: + status_ = Status::Connected; + break; + + case DRM_MODE_DISCONNECTED: + status_ = Status::Disconnected; + break; + + case DRM_MODE_UNKNOWNCONNECTION: + default: + status_ = Status::Unknown; + break; + } + + const std::list<Encoder> &encoders = dev->encoders(); + + encoders_.reserve(connector->count_encoders); + + for (int i = 0; i < connector->count_encoders; ++i) { + uint32_t encoderId = connector->encoders[i]; + auto encoder = std::find_if(encoders.begin(), encoders.end(), + [=](const Encoder &e) { + return e.id() == encoderId; + }); + if (encoder == encoders.end()) { + std::cerr + << "Encoder " << encoderId << " not found" + << std::endl; + continue; + } + + encoders_.push_back(&*encoder); + } + + encoders_.shrink_to_fit(); + + modes_ = { connector->modes, connector->modes + connector->count_modes }; +} + +Plane::Plane(Device *dev, const drmModePlane *plane) + : Object(dev, plane->plane_id, Object::TypePlane), + possibleCrtcsMask_(plane->possible_crtcs) +{ + formats_ = { plane->formats, plane->formats + plane->count_formats }; + + const std::list<Crtc> &crtcs = dev->crtcs(); + possibleCrtcs_.reserve(crtcs.size()); + + for (const Crtc &crtc : crtcs) { + if (plane->possible_crtcs & (1 << crtc.index())) + possibleCrtcs_.push_back(&crtc); + } + + possibleCrtcs_.shrink_to_fit(); +} + +bool Plane::supportsFormat(const libcamera::PixelFormat &format) const +{ + return std::find(formats_.begin(), formats_.end(), format.fourcc()) + != formats_.end(); +} + +int Plane::setup() +{ + const PropertyValue *pv = propertyValue("type"); + if (!pv) + return -EINVAL; + + switch (pv->value()) { + case DRM_PLANE_TYPE_OVERLAY: + type_ = TypeOverlay; + break; + + case DRM_PLANE_TYPE_PRIMARY: + type_ = TypePrimary; + break; + + case DRM_PLANE_TYPE_CURSOR: + type_ = TypeCursor; + break; + + default: + return -EINVAL; + } + + return 0; +} + +FrameBuffer::FrameBuffer(Device *dev) + : Object(dev, 0, Object::TypeFb) +{ +} + +FrameBuffer::~FrameBuffer() +{ + for (const auto &plane : planes_) { + struct drm_gem_close gem_close = { + .handle = plane.second.handle, + .pad = 0, + }; + int ret; + + do { + ret = ioctl(device()->fd(), DRM_IOCTL_GEM_CLOSE, &gem_close); + } while (ret == -1 && (errno == EINTR || errno == EAGAIN)); + + if (ret == -1) { + ret = -errno; + std::cerr + << "Failed to close GEM object: " + << strerror(-ret) << std::endl; + } + } + + drmModeRmFB(device()->fd(), id()); +} + +AtomicRequest::AtomicRequest(Device *dev) + : dev_(dev), valid_(true) +{ + request_ = drmModeAtomicAlloc(); + if (!request_) + valid_ = false; +} + +AtomicRequest::~AtomicRequest() +{ + if (request_) + drmModeAtomicFree(request_); +} + +int AtomicRequest::addProperty(const Object *object, const std::string &property, + uint64_t value) +{ + if (!valid_) + return -EINVAL; + + const Property *prop = object->property(property); + if (!prop) { + valid_ = false; + return -EINVAL; + } + + return addProperty(object->id(), prop->id(), value); +} + +int AtomicRequest::addProperty(const Object *object, const std::string &property, + std::unique_ptr<Blob> blob) +{ + if (!valid_) + return -EINVAL; + + const Property *prop = object->property(property); + if (!prop) { + valid_ = false; + return -EINVAL; + } + + int ret = addProperty(object->id(), prop->id(), blob->id()); + if (ret < 0) + return ret; + + blobs_.emplace_back(std::move(blob)); + + return 0; +} + +int AtomicRequest::addProperty(uint32_t object, uint32_t property, uint64_t value) +{ + int ret = drmModeAtomicAddProperty(request_, object, property, value); + if (ret < 0) { + valid_ = false; + return ret; + } + + return 0; +} + +int AtomicRequest::commit(unsigned int flags) +{ + if (!valid_) + return -EINVAL; + + uint32_t drmFlags = 0; + if (flags & FlagAllowModeset) + drmFlags |= DRM_MODE_ATOMIC_ALLOW_MODESET; + if (flags & FlagAsync) + drmFlags |= DRM_MODE_PAGE_FLIP_EVENT | DRM_MODE_ATOMIC_NONBLOCK; + if (flags & FlagTestOnly) + drmFlags |= DRM_MODE_ATOMIC_TEST_ONLY; + + return drmModeAtomicCommit(dev_->fd(), request_, drmFlags, this); +} + +Device::Device() + : fd_(-1) +{ +} + +Device::~Device() +{ + if (fd_ != -1) + drmClose(fd_); +} + +int Device::init() +{ + int ret = openCard(); + if (ret < 0) { + std::cerr << "Failed to open any DRM/KMS device: " + << strerror(-ret) << std::endl; + return ret; + } + + /* + * Enable the atomic APIs. This also automatically enables the + * universal planes API. + */ + ret = drmSetClientCap(fd_, DRM_CLIENT_CAP_ATOMIC, 1); + if (ret < 0) { + ret = -errno; + std::cerr + << "Failed to enable atomic capability: " + << strerror(-ret) << std::endl; + return ret; + } + + /* List all the resources. */ + ret = getResources(); + if (ret < 0) + return ret; + + EventLoop::instance()->addFdEvent(fd_, EventLoop::Read, + std::bind(&Device::drmEvent, this)); + + return 0; +} + +int Device::openCard() +{ + const std::string dirName = "/dev/dri/"; + bool found = false; + int ret; + + /* + * Open the first DRM/KMS device beginning with /dev/dri/card. The + * libdrm drmOpen*() functions require either a module name or a bus ID, + * which we don't have, so bypass them. The automatic module loading and + * device node creation from drmOpen() is of no practical use as any + * modern system will handle that through udev or an equivalent + * component. + */ + DIR *folder = opendir(dirName.c_str()); + if (!folder) { + ret = -errno; + std::cerr << "Failed to open " << dirName + << " directory: " << strerror(-ret) << std::endl; + return ret; + } + + for (struct dirent *res; (res = readdir(folder));) { + uint64_t cap; + + if (strncmp(res->d_name, "card", 4)) + continue; + + const std::string devName = dirName + res->d_name; + fd_ = open(devName.c_str(), O_RDWR | O_CLOEXEC); + if (fd_ < 0) { + ret = -errno; + std::cerr << "Failed to open DRM/KMS device " << devName << ": " + << strerror(-ret) << std::endl; + continue; + } + + /* + * Skip devices that don't support the modeset API, to avoid + * selecting a DRM device corresponding to a GPU. There is no + * modeset capability, but the kernel returns an error for most + * caps if mode setting isn't support by the driver. The + * DRM_CAP_DUMB_BUFFER capability is one of those, other would + * do as well. The capability value itself isn't relevant. + */ + ret = drmGetCap(fd_, DRM_CAP_DUMB_BUFFER, &cap); + if (ret < 0) { + drmClose(fd_); + fd_ = -1; + continue; + } + + found = true; + break; + } + + closedir(folder); + + return found ? 0 : -ENOENT; +} + +int Device::getResources() +{ + int ret; + + std::unique_ptr<drmModeRes, decltype(&drmModeFreeResources)> resources{ + drmModeGetResources(fd_), + &drmModeFreeResources + }; + if (!resources) { + ret = -errno; + std::cerr + << "Failed to get DRM/KMS resources: " + << strerror(-ret) << std::endl; + return ret; + } + + for (int i = 0; i < resources->count_crtcs; ++i) { + drmModeCrtc *crtc = drmModeGetCrtc(fd_, resources->crtcs[i]); + if (!crtc) { + ret = -errno; + std::cerr + << "Failed to get CRTC: " << strerror(-ret) + << std::endl; + return ret; + } + + crtcs_.emplace_back(this, crtc, i); + drmModeFreeCrtc(crtc); + + Crtc &obj = crtcs_.back(); + objects_[obj.id()] = &obj; + } + + for (int i = 0; i < resources->count_encoders; ++i) { + drmModeEncoder *encoder = + drmModeGetEncoder(fd_, resources->encoders[i]); + if (!encoder) { + ret = -errno; + std::cerr + << "Failed to get encoder: " << strerror(-ret) + << std::endl; + return ret; + } + + encoders_.emplace_back(this, encoder); + drmModeFreeEncoder(encoder); + + Encoder &obj = encoders_.back(); + objects_[obj.id()] = &obj; + } + + for (int i = 0; i < resources->count_connectors; ++i) { + drmModeConnector *connector = + drmModeGetConnector(fd_, resources->connectors[i]); + if (!connector) { + ret = -errno; + std::cerr + << "Failed to get connector: " << strerror(-ret) + << std::endl; + return ret; + } + + connectors_.emplace_back(this, connector); + drmModeFreeConnector(connector); + + Connector &obj = connectors_.back(); + objects_[obj.id()] = &obj; + } + + std::unique_ptr<drmModePlaneRes, decltype(&drmModeFreePlaneResources)> planes{ + drmModeGetPlaneResources(fd_), + &drmModeFreePlaneResources + }; + if (!planes) { + ret = -errno; + std::cerr + << "Failed to get DRM/KMS planes: " + << strerror(-ret) << std::endl; + return ret; + } + + for (uint32_t i = 0; i < planes->count_planes; ++i) { + drmModePlane *plane = + drmModeGetPlane(fd_, planes->planes[i]); + if (!plane) { + ret = -errno; + std::cerr + << "Failed to get plane: " << strerror(-ret) + << std::endl; + return ret; + } + + planes_.emplace_back(this, plane); + drmModeFreePlane(plane); + + Plane &obj = planes_.back(); + objects_[obj.id()] = &obj; + } + + /* Set the possible planes for each CRTC. */ + for (Crtc &crtc : crtcs_) { + for (const Plane &plane : planes_) { + if (plane.possibleCrtcsMask_ & (1 << crtc.index())) + crtc.planes_.push_back(&plane); + } + } + + /* Collect all property IDs and create Property instances. */ + std::set<uint32_t> properties; + for (const auto &object : objects_) { + for (const PropertyValue &value : object.second->properties()) + properties.insert(value.id()); + } + + for (uint32_t id : properties) { + drmModePropertyRes *property = drmModeGetProperty(fd_, id); + if (!property) { + ret = -errno; + std::cerr + << "Failed to get property: " << strerror(-ret) + << std::endl; + continue; + } + + properties_.emplace_back(this, property); + drmModeFreeProperty(property); + + Property &obj = properties_.back(); + objects_[obj.id()] = &obj; + } + + /* Finally, perform all delayed setup of mode objects. */ + for (auto &object : objects_) { + ret = object.second->setup(); + if (ret < 0) { + std::cerr + << "Failed to setup object " << object.second->id() + << ": " << strerror(-ret) << std::endl; + return ret; + } + } + + return 0; +} + +const Object *Device::object(uint32_t id) +{ + const auto iter = objects_.find(id); + if (iter == objects_.end()) + return nullptr; + + return iter->second; +} + +std::unique_ptr<FrameBuffer> Device::createFrameBuffer( + const libcamera::FrameBuffer &buffer, + const libcamera::PixelFormat &format, + const libcamera::Size &size, + const std::array<uint32_t, 4> &strides) +{ + std::unique_ptr<FrameBuffer> fb{ new FrameBuffer(this) }; + + uint32_t handles[4] = {}; + uint32_t offsets[4] = {}; + int ret; + + const std::vector<libcamera::FrameBuffer::Plane> &planes = buffer.planes(); + + unsigned int i = 0; + for (const libcamera::FrameBuffer::Plane &plane : planes) { + int fd = plane.fd.get(); + uint32_t handle; + + auto iter = fb->planes_.find(fd); + if (iter == fb->planes_.end()) { + ret = drmPrimeFDToHandle(fd_, plane.fd.get(), &handle); + if (ret < 0) { + ret = -errno; + std::cerr + << "Unable to import framebuffer dmabuf: " + << strerror(-ret) << std::endl; + return nullptr; + } + + fb->planes_[fd] = { handle }; + } else { + handle = iter->second.handle; + } + + handles[i] = handle; + offsets[i] = plane.offset; + ++i; + } + + ret = drmModeAddFB2(fd_, size.width, size.height, format.fourcc(), handles, + strides.data(), offsets, &fb->id_, 0); + if (ret < 0) { + ret = -errno; + std::cerr + << "Failed to add framebuffer: " + << strerror(-ret) << std::endl; + return nullptr; + } + + return fb; +} + +void Device::drmEvent() +{ + drmEventContext ctx{}; + ctx.version = DRM_EVENT_CONTEXT_VERSION; + ctx.page_flip_handler = &Device::pageFlipComplete; + + drmHandleEvent(fd_, &ctx); +} + +void Device::pageFlipComplete([[maybe_unused]] int fd, + [[maybe_unused]] unsigned int sequence, + [[maybe_unused]] unsigned int tv_sec, + [[maybe_unused]] unsigned int tv_usec, + void *user_data) +{ + AtomicRequest *request = static_cast<AtomicRequest *>(user_data); + request->device()->requestComplete.emit(request); +} + +} /* namespace DRM */ diff --git a/src/apps/cam/drm.h b/src/apps/cam/drm.h new file mode 100644 index 00000000..ebaea04d --- /dev/null +++ b/src/apps/cam/drm.h @@ -0,0 +1,334 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Ideas on Board Oy + * + * drm.h - DRM/KMS Helpers + */ + +#pragma once + +#include <array> +#include <list> +#include <map> +#include <memory> +#include <stdint.h> +#include <string> +#include <vector> + +#include <libcamera/base/signal.h> +#include <libcamera/base/span.h> + +#include <libdrm/drm.h> +#include <xf86drm.h> +#include <xf86drmMode.h> + +namespace libcamera { +class FrameBuffer; +class PixelFormat; +class Size; +} /* namespace libcamera */ + +namespace DRM { + +class Device; +class Plane; +class Property; +class PropertyValue; + +class Object +{ +public: + enum Type { + TypeCrtc = DRM_MODE_OBJECT_CRTC, + TypeConnector = DRM_MODE_OBJECT_CONNECTOR, + TypeEncoder = DRM_MODE_OBJECT_ENCODER, + TypeMode = DRM_MODE_OBJECT_MODE, + TypeProperty = DRM_MODE_OBJECT_PROPERTY, + TypeFb = DRM_MODE_OBJECT_FB, + TypeBlob = DRM_MODE_OBJECT_BLOB, + TypePlane = DRM_MODE_OBJECT_PLANE, + TypeAny = DRM_MODE_OBJECT_ANY, + }; + + Object(Device *dev, uint32_t id, Type type); + virtual ~Object(); + + Device *device() const { return dev_; } + uint32_t id() const { return id_; } + Type type() const { return type_; } + + const Property *property(const std::string &name) const; + const PropertyValue *propertyValue(const std::string &name) const; + const std::vector<PropertyValue> &properties() const { return properties_; } + +protected: + virtual int setup() + { + return 0; + } + + uint32_t id_; + +private: + friend Device; + + Device *dev_; + Type type_; + std::vector<PropertyValue> properties_; +}; + +class Property : public Object +{ +public: + enum Type { + TypeUnknown = 0, + TypeRange, + TypeEnum, + TypeBlob, + TypeBitmask, + TypeObject, + TypeSignedRange, + }; + + Property(Device *dev, drmModePropertyRes *property); + + Type type() const { return type_; } + const std::string &name() const { return name_; } + + bool isImmutable() const { return flags_ & DRM_MODE_PROP_IMMUTABLE; } + + const std::vector<uint64_t> values() const { return values_; } + const std::map<uint32_t, std::string> &enums() const { return enums_; } + const std::vector<uint32_t> blobs() const { return blobs_; } + +private: + Type type_; + std::string name_; + uint32_t flags_; + std::vector<uint64_t> values_; + std::map<uint32_t, std::string> enums_; + std::vector<uint32_t> blobs_; +}; + +class PropertyValue +{ +public: + PropertyValue(uint32_t id, uint64_t value) + : id_(id), value_(value) + { + } + + uint32_t id() const { return id_; } + uint32_t value() const { return value_; } + +private: + uint32_t id_; + uint64_t value_; +}; + +class Blob : public Object +{ +public: + Blob(Device *dev, const libcamera::Span<const uint8_t> &data); + ~Blob(); + + bool isValid() const { return id() != 0; } +}; + +class Mode : public drmModeModeInfo +{ +public: + Mode(const drmModeModeInfo &mode); + + std::unique_ptr<Blob> toBlob(Device *dev) const; +}; + +class Crtc : public Object +{ +public: + Crtc(Device *dev, const drmModeCrtc *crtc, unsigned int index); + + unsigned int index() const { return index_; } + const std::vector<const Plane *> &planes() const { return planes_; } + +private: + friend Device; + + unsigned int index_; + std::vector<const Plane *> planes_; +}; + +class Encoder : public Object +{ +public: + Encoder(Device *dev, const drmModeEncoder *encoder); + + uint32_t type() const { return type_; } + + const std::vector<const Crtc *> &possibleCrtcs() const { return possibleCrtcs_; } + +private: + uint32_t type_; + std::vector<const Crtc *> possibleCrtcs_; +}; + +class Connector : public Object +{ +public: + enum Status { + Connected, + Disconnected, + Unknown, + }; + + Connector(Device *dev, const drmModeConnector *connector); + + uint32_t type() const { return type_; } + const std::string &name() const { return name_; } + + Status status() const { return status_; } + + const std::vector<const Encoder *> &encoders() const { return encoders_; } + const std::vector<Mode> &modes() const { return modes_; } + +private: + uint32_t type_; + std::string name_; + Status status_; + std::vector<const Encoder *> encoders_; + std::vector<Mode> modes_; +}; + +class Plane : public Object +{ +public: + enum Type { + TypeOverlay, + TypePrimary, + TypeCursor, + }; + + Plane(Device *dev, const drmModePlane *plane); + + Type type() const { return type_; } + const std::vector<uint32_t> &formats() const { return formats_; } + const std::vector<const Crtc *> &possibleCrtcs() const { return possibleCrtcs_; } + + bool supportsFormat(const libcamera::PixelFormat &format) const; + +protected: + int setup() override; + +private: + friend class Device; + + Type type_; + std::vector<uint32_t> formats_; + std::vector<const Crtc *> possibleCrtcs_; + uint32_t possibleCrtcsMask_; +}; + +class FrameBuffer : public Object +{ +public: + struct Plane { + uint32_t handle; + }; + + ~FrameBuffer(); + +private: + friend class Device; + + FrameBuffer(Device *dev); + + std::map<int, Plane> planes_; +}; + +class AtomicRequest +{ +public: + enum Flags { + FlagAllowModeset = (1 << 0), + FlagAsync = (1 << 1), + FlagTestOnly = (1 << 2), + }; + + AtomicRequest(Device *dev); + ~AtomicRequest(); + + Device *device() const { return dev_; } + bool isValid() const { return valid_; } + + int addProperty(const Object *object, const std::string &property, + uint64_t value); + int addProperty(const Object *object, const std::string &property, + std::unique_ptr<Blob> blob); + int commit(unsigned int flags = 0); + +private: + AtomicRequest(const AtomicRequest &) = delete; + AtomicRequest(const AtomicRequest &&) = delete; + AtomicRequest &operator=(const AtomicRequest &) = delete; + AtomicRequest &operator=(const AtomicRequest &&) = delete; + + int addProperty(uint32_t object, uint32_t property, uint64_t value); + + Device *dev_; + bool valid_; + drmModeAtomicReq *request_; + std::list<std::unique_ptr<Blob>> blobs_; +}; + +class Device +{ +public: + Device(); + ~Device(); + + int init(); + + int fd() const { return fd_; } + + const std::list<Crtc> &crtcs() const { return crtcs_; } + const std::list<Encoder> &encoders() const { return encoders_; } + const std::list<Connector> &connectors() const { return connectors_; } + const std::list<Plane> &planes() const { return planes_; } + const std::list<Property> &properties() const { return properties_; } + + const Object *object(uint32_t id); + + std::unique_ptr<FrameBuffer> createFrameBuffer( + const libcamera::FrameBuffer &buffer, + const libcamera::PixelFormat &format, + const libcamera::Size &size, + const std::array<uint32_t, 4> &strides); + + libcamera::Signal<AtomicRequest *> requestComplete; + +private: + Device(const Device &) = delete; + Device(const Device &&) = delete; + Device &operator=(const Device &) = delete; + Device &operator=(const Device &&) = delete; + + int openCard(); + int getResources(); + + void drmEvent(); + static void pageFlipComplete(int fd, unsigned int sequence, + unsigned int tv_sec, unsigned int tv_usec, + void *user_data); + + int fd_; + + std::list<Crtc> crtcs_; + std::list<Encoder> encoders_; + std::list<Connector> connectors_; + std::list<Plane> planes_; + std::list<Property> properties_; + + std::map<uint32_t, Object *> objects_; +}; + +} /* namespace DRM */ diff --git a/src/apps/cam/event_loop.cpp b/src/apps/cam/event_loop.cpp new file mode 100644 index 00000000..cb83845c --- /dev/null +++ b/src/apps/cam/event_loop.cpp @@ -0,0 +1,150 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * event_loop.cpp - 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/cam/event_loop.h b/src/apps/cam/event_loop.h new file mode 100644 index 00000000..ef79e8e5 --- /dev/null +++ b/src/apps/cam/event_loop.h @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * event_loop.h - 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/cam/file_sink.cpp b/src/apps/cam/file_sink.cpp new file mode 100644 index 00000000..9d60c04e --- /dev/null +++ b/src/apps/cam/file_sink.cpp @@ -0,0 +1,137 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * file_sink.cpp - File Sink + */ + +#include <assert.h> +#include <fcntl.h> +#include <iomanip> +#include <iostream> +#include <sstream> +#include <string.h> +#include <unistd.h> + +#include <libcamera/camera.h> + +#include "dng_writer.h" +#include "file_sink.h" +#include "image.h" + +using namespace libcamera; + +FileSink::FileSink(const libcamera::Camera *camera, + const std::map<const libcamera::Stream *, std::string> &streamNames, + const std::string &pattern) + : camera_(camera), streamNames_(streamNames), pattern_(pattern) +{ +} + +FileSink::~FileSink() +{ +} + +int FileSink::configure(const libcamera::CameraConfiguration &config) +{ + int ret = FrameSink::configure(config); + if (ret < 0) + return ret; + + return 0; +} + +void FileSink::mapBuffer(FrameBuffer *buffer) +{ + std::unique_ptr<Image> image = + Image::fromFrameBuffer(buffer, Image::MapMode::ReadOnly); + assert(image != nullptr); + + mappedBuffers_[buffer] = std::move(image); +} + +bool FileSink::processRequest(Request *request) +{ + for (auto [stream, buffer] : request->buffers()) + writeBuffer(stream, buffer, request->metadata()); + + return true; +} + +void FileSink::writeBuffer(const Stream *stream, FrameBuffer *buffer, + [[maybe_unused]] const ControlList &metadata) +{ + std::string filename; + size_t pos; + int fd, ret = 0; + + if (!pattern_.empty()) + filename = pattern_; + +#ifdef HAVE_TIFF + bool dng = filename.find(".dng", filename.size() - 4) != std::string::npos; +#endif /* HAVE_TIFF */ + + if (filename.empty() || filename.back() == '/') + filename += "frame-#.bin"; + + pos = filename.find_first_of('#'); + if (pos != std::string::npos) { + std::stringstream ss; + ss << streamNames_[stream] << "-" << std::setw(6) + << std::setfill('0') << buffer->metadata().sequence; + filename.replace(pos, 1, ss.str()); + } + + Image *image = mappedBuffers_[buffer].get(); + +#ifdef HAVE_TIFF + if (dng) { + ret = DNGWriter::write(filename.c_str(), camera_, + stream->configuration(), metadata, + buffer, image->data(0).data()); + if (ret < 0) + std::cerr << "failed to write DNG file `" << filename + << "'" << std::endl; + + return; + } +#endif /* HAVE_TIFF */ + + fd = open(filename.c_str(), O_CREAT | O_WRONLY | + (pos == std::string::npos ? O_APPEND : O_TRUNC), + S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); + if (fd == -1) { + ret = -errno; + std::cerr << "failed to open file " << filename << ": " + << strerror(-ret) << std::endl; + return; + } + + for (unsigned int i = 0; i < buffer->planes().size(); ++i) { + const FrameMetadata::Plane &meta = buffer->metadata().planes()[i]; + + Span<uint8_t> data = image->data(i); + unsigned int length = std::min<unsigned int>(meta.bytesused, data.size()); + + if (meta.bytesused > data.size()) + std::cerr << "payload size " << meta.bytesused + << " larger than plane size " << data.size() + << std::endl; + + ret = ::write(fd, data.data(), length); + if (ret < 0) { + ret = -errno; + std::cerr << "write error: " << strerror(-ret) + << std::endl; + break; + } else if (ret != (int)length) { + std::cerr << "write error: only " << ret + << " bytes written instead of " + << length << std::endl; + break; + } + } + + close(fd); +} diff --git a/src/apps/cam/file_sink.h b/src/apps/cam/file_sink.h new file mode 100644 index 00000000..9ce8b619 --- /dev/null +++ b/src/apps/cam/file_sink.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * file_sink.h - File Sink + */ + +#pragma once + +#include <map> +#include <memory> +#include <string> + +#include <libcamera/stream.h> + +#include "frame_sink.h" + +class Image; + +class FileSink : public FrameSink +{ +public: + FileSink(const libcamera::Camera *camera, + const std::map<const libcamera::Stream *, std::string> &streamNames, + const std::string &pattern = ""); + ~FileSink(); + + int configure(const libcamera::CameraConfiguration &config) override; + + void mapBuffer(libcamera::FrameBuffer *buffer) override; + + bool processRequest(libcamera::Request *request) override; + +private: + void writeBuffer(const libcamera::Stream *stream, + libcamera::FrameBuffer *buffer, + const libcamera::ControlList &metadata); + + const libcamera::Camera *camera_; + std::map<const libcamera::Stream *, std::string> streamNames_; + std::string pattern_; + std::map<libcamera::FrameBuffer *, std::unique_ptr<Image>> mappedBuffers_; +}; diff --git a/src/apps/cam/frame_sink.cpp b/src/apps/cam/frame_sink.cpp new file mode 100644 index 00000000..af21d575 --- /dev/null +++ b/src/apps/cam/frame_sink.cpp @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Ideas on Board Oy + * + * frame_sink.cpp - Base Frame Sink Class + */ + +#include "frame_sink.h" + +/** + * \class FrameSink + * \brief Abstract class to model a consumer of frames + * + * The FrameSink class models the consumer that processes frames after a request + * completes. It receives requests through processRequest(), and processes them + * synchronously or asynchronously. This allows frame sinks to hold onto frames + * for an extended period of time, for instance to display them until a new + * frame arrives. + * + * A frame sink processes whole requests, and is solely responsible for deciding + * how to handle different frame buffers in case multiple streams are captured. + */ + +FrameSink::~FrameSink() +{ +} + +int FrameSink::configure([[maybe_unused]] const libcamera::CameraConfiguration &config) +{ + return 0; +} + +void FrameSink::mapBuffer([[maybe_unused]] libcamera::FrameBuffer *buffer) +{ +} + +int FrameSink::start() +{ + return 0; +} + +int FrameSink::stop() +{ + return 0; +} + +/** + * \fn FrameSink::processRequest() + * \param[in] request The request + * + * This function is called to instruct the sink to process a request. The sink + * may process the request synchronously or queue it for asynchronous + * processing. + * + * When the request is processed synchronously, this function shall return true. + * The \a request shall not be accessed by the FrameSink after the function + * returns. + * + * When the request is processed asynchronously, the FrameSink temporarily takes + * ownership of the \a request. The function shall return false, and the + * FrameSink shall emit the requestProcessed signal when the request processing + * completes. If the stop() function is called before the request processing + * completes, it shall release the request synchronously. + * + * \return True if the request has been processed synchronously, false if + * processing has been queued + */ diff --git a/src/apps/cam/frame_sink.h b/src/apps/cam/frame_sink.h new file mode 100644 index 00000000..ca4347cb --- /dev/null +++ b/src/apps/cam/frame_sink.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Ideas on Board Oy + * + * frame_sink.h - Base Frame Sink Class + */ + +#pragma once + +#include <libcamera/base/signal.h> + +namespace libcamera { +class CameraConfiguration; +class FrameBuffer; +class Request; +} /* namespace libcamera */ + +class FrameSink +{ +public: + virtual ~FrameSink(); + + virtual int configure(const libcamera::CameraConfiguration &config); + + virtual void mapBuffer(libcamera::FrameBuffer *buffer); + + virtual int start(); + virtual int stop(); + + virtual bool processRequest(libcamera::Request *request) = 0; + libcamera::Signal<libcamera::Request *> requestProcessed; +}; diff --git a/src/apps/cam/image.cpp b/src/apps/cam/image.cpp new file mode 100644 index 00000000..fe2cc6da --- /dev/null +++ b/src/apps/cam/image.cpp @@ -0,0 +1,109 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2021, Ideas on Board Oy + * + * image.cpp - 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/cam/image.h b/src/apps/cam/image.h new file mode 100644 index 00000000..7953b177 --- /dev/null +++ b/src/apps/cam/image.h @@ -0,0 +1,50 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2021, Ideas on Board Oy + * + * image.h - 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/cam/kms_sink.cpp b/src/apps/cam/kms_sink.cpp new file mode 100644 index 00000000..754b061e --- /dev/null +++ b/src/apps/cam/kms_sink.cpp @@ -0,0 +1,538 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Ideas on Board Oy + * + * kms_sink.cpp - KMS Sink + */ + +#include "kms_sink.h" + +#include <array> +#include <algorithm> +#include <assert.h> +#include <iostream> +#include <limits.h> +#include <memory> +#include <stdint.h> +#include <string.h> + +#include <libcamera/camera.h> +#include <libcamera/formats.h> +#include <libcamera/framebuffer.h> +#include <libcamera/stream.h> + +#include "drm.h" + +KMSSink::KMSSink(const std::string &connectorName) + : connector_(nullptr), crtc_(nullptr), plane_(nullptr), mode_(nullptr) +{ + int ret = dev_.init(); + if (ret < 0) + return; + + /* + * Find the requested connector. If no specific connector is requested, + * pick the first connected connector or, if no connector is connected, + * the first connector with unknown status. + */ + for (const DRM::Connector &conn : dev_.connectors()) { + if (!connectorName.empty()) { + if (conn.name() != connectorName) + continue; + + connector_ = &conn; + break; + } + + if (conn.status() == DRM::Connector::Connected) { + connector_ = &conn; + break; + } + + if (!connector_ && conn.status() == DRM::Connector::Unknown) + connector_ = &conn; + } + + if (!connector_) { + if (!connectorName.empty()) + std::cerr + << "Connector " << connectorName << " not found" + << std::endl; + else + std::cerr << "No connected connector found" << std::endl; + return; + } + + dev_.requestComplete.connect(this, &KMSSink::requestComplete); +} + +void KMSSink::mapBuffer(libcamera::FrameBuffer *buffer) +{ + std::array<uint32_t, 4> strides = {}; + + /* \todo Should libcamera report per-plane strides ? */ + unsigned int uvStrideMultiplier; + + switch (format_) { + case libcamera::formats::NV24: + case libcamera::formats::NV42: + uvStrideMultiplier = 4; + break; + case libcamera::formats::YUV420: + case libcamera::formats::YVU420: + case libcamera::formats::YUV422: + uvStrideMultiplier = 1; + break; + default: + uvStrideMultiplier = 2; + break; + } + + strides[0] = stride_; + for (unsigned int i = 1; i < buffer->planes().size(); ++i) + strides[i] = stride_ * uvStrideMultiplier / 2; + + std::unique_ptr<DRM::FrameBuffer> drmBuffer = + dev_.createFrameBuffer(*buffer, format_, size_, strides); + if (!drmBuffer) + return; + + buffers_.emplace(std::piecewise_construct, + std::forward_as_tuple(buffer), + std::forward_as_tuple(std::move(drmBuffer))); +} + +int KMSSink::configure(const libcamera::CameraConfiguration &config) +{ + if (!connector_) + return -EINVAL; + + crtc_ = nullptr; + plane_ = nullptr; + mode_ = nullptr; + + const libcamera::StreamConfiguration &cfg = config.at(0); + + /* Find the best mode for the stream size. */ + const std::vector<DRM::Mode> &modes = connector_->modes(); + + unsigned int cfgArea = cfg.size.width * cfg.size.height; + unsigned int bestDistance = UINT_MAX; + + for (const DRM::Mode &mode : modes) { + unsigned int modeArea = mode.hdisplay * mode.vdisplay; + unsigned int distance = modeArea > cfgArea ? modeArea - cfgArea + : cfgArea - modeArea; + + if (distance < bestDistance) { + mode_ = &mode; + bestDistance = distance; + + /* + * If the sizes match exactly, there will be no better + * match. + */ + if (distance == 0) + break; + } + } + + if (!mode_) { + std::cerr << "No modes\n"; + return -EINVAL; + } + + int ret = configurePipeline(cfg.pixelFormat); + if (ret < 0) + return ret; + + size_ = cfg.size; + stride_ = cfg.stride; + + /* Configure color space. */ + colorEncoding_ = std::nullopt; + colorRange_ = std::nullopt; + + if (cfg.colorSpace->ycbcrEncoding == libcamera::ColorSpace::YcbcrEncoding::None) + return 0; + + /* + * The encoding and range enums are defined in the kernel but not + * exposed in public headers. + */ + enum drm_color_encoding { + DRM_COLOR_YCBCR_BT601, + DRM_COLOR_YCBCR_BT709, + DRM_COLOR_YCBCR_BT2020, + }; + + enum drm_color_range { + DRM_COLOR_YCBCR_LIMITED_RANGE, + DRM_COLOR_YCBCR_FULL_RANGE, + }; + + const DRM::Property *colorEncoding = plane_->property("COLOR_ENCODING"); + const DRM::Property *colorRange = plane_->property("COLOR_RANGE"); + + if (colorEncoding) { + drm_color_encoding encoding; + + switch (cfg.colorSpace->ycbcrEncoding) { + case libcamera::ColorSpace::YcbcrEncoding::Rec601: + default: + encoding = DRM_COLOR_YCBCR_BT601; + break; + case libcamera::ColorSpace::YcbcrEncoding::Rec709: + encoding = DRM_COLOR_YCBCR_BT709; + break; + case libcamera::ColorSpace::YcbcrEncoding::Rec2020: + encoding = DRM_COLOR_YCBCR_BT2020; + break; + } + + for (const auto &[id, name] : colorEncoding->enums()) { + if (id == encoding) { + colorEncoding_ = encoding; + break; + } + } + } + + if (colorRange) { + drm_color_range range; + + switch (cfg.colorSpace->range) { + case libcamera::ColorSpace::Range::Limited: + default: + range = DRM_COLOR_YCBCR_LIMITED_RANGE; + break; + case libcamera::ColorSpace::Range::Full: + range = DRM_COLOR_YCBCR_FULL_RANGE; + break; + } + + for (const auto &[id, name] : colorRange->enums()) { + if (id == range) { + colorRange_ = range; + break; + } + } + } + + if (!colorEncoding_ || !colorRange_) + std::cerr << "Color space " << cfg.colorSpace->toString() + << " not supported by the display device." + << " Colors may be wrong." << std::endl; + + return 0; +} + +int KMSSink::selectPipeline(const libcamera::PixelFormat &format) +{ + /* + * If the requested format has an alpha channel, also consider the X + * variant. + */ + libcamera::PixelFormat xFormat; + + switch (format) { + case libcamera::formats::ABGR8888: + xFormat = libcamera::formats::XBGR8888; + break; + case libcamera::formats::ARGB8888: + xFormat = libcamera::formats::XRGB8888; + break; + case libcamera::formats::BGRA8888: + xFormat = libcamera::formats::BGRX8888; + break; + case libcamera::formats::RGBA8888: + xFormat = libcamera::formats::RGBX8888; + break; + } + + /* + * Find a CRTC and plane suitable for the request format and the + * connector at the end of the pipeline. Restrict the search to primary + * planes for now. + */ + for (const DRM::Encoder *encoder : connector_->encoders()) { + for (const DRM::Crtc *crtc : encoder->possibleCrtcs()) { + for (const DRM::Plane *plane : crtc->planes()) { + if (plane->type() != DRM::Plane::TypePrimary) + continue; + + if (plane->supportsFormat(format)) { + crtc_ = crtc; + plane_ = plane; + format_ = format; + return 0; + } + + if (plane->supportsFormat(xFormat)) { + crtc_ = crtc; + plane_ = plane; + format_ = xFormat; + return 0; + } + } + } + } + + return -EPIPE; +} + +int KMSSink::configurePipeline(const libcamera::PixelFormat &format) +{ + const int ret = selectPipeline(format); + if (ret) { + std::cerr + << "Unable to find display pipeline for format " + << format << std::endl; + + return ret; + } + + std::cout + << "Using KMS plane " << plane_->id() << ", CRTC " << crtc_->id() + << ", connector " << connector_->name() + << " (" << connector_->id() << "), mode " << mode_->hdisplay + << "x" << mode_->vdisplay << "@" << mode_->vrefresh << std::endl; + + return 0; +} + +int KMSSink::start() +{ + std::unique_ptr<DRM::AtomicRequest> request; + + int ret = FrameSink::start(); + if (ret < 0) + return ret; + + /* Disable all CRTCs and planes to start from a known valid state. */ + request = std::make_unique<DRM::AtomicRequest>(&dev_); + + for (const DRM::Crtc &crtc : dev_.crtcs()) + request->addProperty(&crtc, "ACTIVE", 0); + + for (const DRM::Plane &plane : dev_.planes()) { + request->addProperty(&plane, "CRTC_ID", 0); + request->addProperty(&plane, "FB_ID", 0); + } + + ret = request->commit(DRM::AtomicRequest::FlagAllowModeset); + if (ret < 0) { + std::cerr + << "Failed to disable CRTCs and planes: " + << strerror(-ret) << std::endl; + return ret; + } + + return 0; +} + +int KMSSink::stop() +{ + /* Display pipeline. */ + DRM::AtomicRequest request(&dev_); + + request.addProperty(connector_, "CRTC_ID", 0); + request.addProperty(crtc_, "ACTIVE", 0); + request.addProperty(crtc_, "MODE_ID", 0); + request.addProperty(plane_, "CRTC_ID", 0); + request.addProperty(plane_, "FB_ID", 0); + + int ret = request.commit(DRM::AtomicRequest::FlagAllowModeset); + if (ret < 0) { + std::cerr + << "Failed to stop display pipeline: " + << strerror(-ret) << std::endl; + return ret; + } + + /* Free all buffers. */ + pending_.reset(); + queued_.reset(); + active_.reset(); + buffers_.clear(); + + return FrameSink::stop(); +} + +bool KMSSink::testModeSet(DRM::FrameBuffer *drmBuffer, + const libcamera::Rectangle &src, + const libcamera::Rectangle &dst) +{ + DRM::AtomicRequest drmRequest{ &dev_ }; + + drmRequest.addProperty(connector_, "CRTC_ID", crtc_->id()); + + drmRequest.addProperty(crtc_, "ACTIVE", 1); + drmRequest.addProperty(crtc_, "MODE_ID", mode_->toBlob(&dev_)); + + drmRequest.addProperty(plane_, "CRTC_ID", crtc_->id()); + drmRequest.addProperty(plane_, "FB_ID", drmBuffer->id()); + drmRequest.addProperty(plane_, "SRC_X", src.x << 16); + drmRequest.addProperty(plane_, "SRC_Y", src.y << 16); + drmRequest.addProperty(plane_, "SRC_W", src.width << 16); + drmRequest.addProperty(plane_, "SRC_H", src.height << 16); + drmRequest.addProperty(plane_, "CRTC_X", dst.x); + drmRequest.addProperty(plane_, "CRTC_Y", dst.y); + drmRequest.addProperty(plane_, "CRTC_W", dst.width); + drmRequest.addProperty(plane_, "CRTC_H", dst.height); + + return !drmRequest.commit(DRM::AtomicRequest::FlagAllowModeset | + DRM::AtomicRequest::FlagTestOnly); +} + +bool KMSSink::setupComposition(DRM::FrameBuffer *drmBuffer) +{ + /* + * Test composition options, from most to least desirable, to select the + * best one. + */ + const libcamera::Rectangle framebuffer{ size_ }; + const libcamera::Rectangle display{ 0, 0, mode_->hdisplay, mode_->vdisplay }; + + /* 1. Scale the frame buffer to full screen, preserving aspect ratio. */ + libcamera::Rectangle src = framebuffer; + libcamera::Rectangle dst = display.size().boundedToAspectRatio(framebuffer.size()) + .centeredTo(display.center()); + + if (testModeSet(drmBuffer, src, dst)) { + std::cout << "KMS: full-screen scaled output, square pixels" + << std::endl; + src_ = src; + dst_ = dst; + return true; + } + + /* + * 2. Scale the frame buffer to full screen, without preserving aspect + * ratio. + */ + src = framebuffer; + dst = display; + + if (testModeSet(drmBuffer, src, dst)) { + std::cout << "KMS: full-screen scaled output, non-square pixels" + << std::endl; + src_ = src; + dst_ = dst; + return true; + } + + /* 3. Center the frame buffer on the display. */ + src = display.size().centeredTo(framebuffer.center()).boundedTo(framebuffer); + dst = framebuffer.size().centeredTo(display.center()).boundedTo(display); + + if (testModeSet(drmBuffer, src, dst)) { + std::cout << "KMS: centered output" << std::endl; + src_ = src; + dst_ = dst; + return true; + } + + /* 4. Align the frame buffer on the top-left of the display. */ + src = framebuffer.boundedTo(display); + dst = display.boundedTo(framebuffer); + + if (testModeSet(drmBuffer, src, dst)) { + std::cout << "KMS: top-left aligned output" << std::endl; + src_ = src; + dst_ = dst; + return true; + } + + return false; +} + +bool KMSSink::processRequest(libcamera::Request *camRequest) +{ + /* + * Perform a very crude rate adaptation by simply dropping the request + * if the display queue is full. + */ + if (pending_) + return true; + + libcamera::FrameBuffer *buffer = camRequest->buffers().begin()->second; + auto iter = buffers_.find(buffer); + if (iter == buffers_.end()) + return true; + + DRM::FrameBuffer *drmBuffer = iter->second.get(); + + unsigned int flags = DRM::AtomicRequest::FlagAsync; + std::unique_ptr<DRM::AtomicRequest> drmRequest = + std::make_unique<DRM::AtomicRequest>(&dev_); + drmRequest->addProperty(plane_, "FB_ID", drmBuffer->id()); + + if (!active_ && !queued_) { + /* Enable the display pipeline on the first frame. */ + if (!setupComposition(drmBuffer)) { + std::cerr << "Failed to setup composition" << std::endl; + return true; + } + + drmRequest->addProperty(connector_, "CRTC_ID", crtc_->id()); + + drmRequest->addProperty(crtc_, "ACTIVE", 1); + drmRequest->addProperty(crtc_, "MODE_ID", mode_->toBlob(&dev_)); + + drmRequest->addProperty(plane_, "CRTC_ID", crtc_->id()); + drmRequest->addProperty(plane_, "SRC_X", src_.x << 16); + drmRequest->addProperty(plane_, "SRC_Y", src_.y << 16); + drmRequest->addProperty(plane_, "SRC_W", src_.width << 16); + drmRequest->addProperty(plane_, "SRC_H", src_.height << 16); + drmRequest->addProperty(plane_, "CRTC_X", dst_.x); + drmRequest->addProperty(plane_, "CRTC_Y", dst_.y); + drmRequest->addProperty(plane_, "CRTC_W", dst_.width); + drmRequest->addProperty(plane_, "CRTC_H", dst_.height); + + if (colorEncoding_) + drmRequest->addProperty(plane_, "COLOR_ENCODING", *colorEncoding_); + if (colorRange_) + drmRequest->addProperty(plane_, "COLOR_RANGE", *colorRange_); + + flags |= DRM::AtomicRequest::FlagAllowModeset; + } + + pending_ = std::make_unique<Request>(std::move(drmRequest), camRequest); + + std::lock_guard<std::mutex> lock(lock_); + + if (!queued_) { + int ret = pending_->drmRequest_->commit(flags); + if (ret < 0) { + std::cerr + << "Failed to commit atomic request: " + << strerror(-ret) << std::endl; + /* \todo Implement error handling */ + } + + queued_ = std::move(pending_); + } + + return false; +} + +void KMSSink::requestComplete(DRM::AtomicRequest *request) +{ + std::lock_guard<std::mutex> lock(lock_); + + assert(queued_ && queued_->drmRequest_.get() == request); + + /* Complete the active request, if any. */ + if (active_) + requestProcessed.emit(active_->camRequest_); + + /* The queued request becomes active. */ + active_ = std::move(queued_); + + /* Queue the pending request, if any. */ + if (pending_) { + pending_->drmRequest_->commit(DRM::AtomicRequest::FlagAsync); + queued_ = std::move(pending_); + } +} diff --git a/src/apps/cam/kms_sink.h b/src/apps/cam/kms_sink.h new file mode 100644 index 00000000..e2c618a1 --- /dev/null +++ b/src/apps/cam/kms_sink.h @@ -0,0 +1,83 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Ideas on Board Oy + * + * kms_sink.h - KMS Sink + */ + +#pragma once + +#include <list> +#include <memory> +#include <mutex> +#include <optional> +#include <string> +#include <utility> + +#include <libcamera/base/signal.h> + +#include <libcamera/geometry.h> +#include <libcamera/pixel_format.h> + +#include "drm.h" +#include "frame_sink.h" + +class KMSSink : public FrameSink +{ +public: + KMSSink(const std::string &connectorName); + + void mapBuffer(libcamera::FrameBuffer *buffer) override; + + int configure(const libcamera::CameraConfiguration &config) override; + int start() override; + int stop() override; + + bool processRequest(libcamera::Request *request) override; + +private: + class Request + { + public: + Request(std::unique_ptr<DRM::AtomicRequest> drmRequest, + libcamera::Request *camRequest) + : drmRequest_(std::move(drmRequest)), camRequest_(camRequest) + { + } + + std::unique_ptr<DRM::AtomicRequest> drmRequest_; + libcamera::Request *camRequest_; + }; + + int selectPipeline(const libcamera::PixelFormat &format); + int configurePipeline(const libcamera::PixelFormat &format); + bool testModeSet(DRM::FrameBuffer *drmBuffer, + const libcamera::Rectangle &src, + const libcamera::Rectangle &dst); + bool setupComposition(DRM::FrameBuffer *drmBuffer); + + void requestComplete(DRM::AtomicRequest *request); + + DRM::Device dev_; + + const DRM::Connector *connector_; + const DRM::Crtc *crtc_; + const DRM::Plane *plane_; + const DRM::Mode *mode_; + + libcamera::PixelFormat format_; + libcamera::Size size_; + unsigned int stride_; + std::optional<unsigned int> colorEncoding_; + std::optional<unsigned int> colorRange_; + + libcamera::Rectangle src_; + libcamera::Rectangle dst_; + + std::map<libcamera::FrameBuffer *, std::unique_ptr<DRM::FrameBuffer>> buffers_; + + std::mutex lock_; + std::unique_ptr<Request> pending_; + std::unique_ptr<Request> queued_; + std::unique_ptr<Request> active_; +}; diff --git a/src/apps/cam/main.cpp b/src/apps/cam/main.cpp new file mode 100644 index 00000000..d70130e2 --- /dev/null +++ b/src/apps/cam/main.cpp @@ -0,0 +1,362 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * main.cpp - cam - The libcamera swiss army knife + */ + +#include <atomic> +#include <iomanip> +#include <iostream> +#include <signal.h> +#include <string.h> + +#include <libcamera/libcamera.h> +#include <libcamera/property_ids.h> + +#include "camera_session.h" +#include "event_loop.h" +#include "main.h" +#include "options.h" +#include "stream_options.h" + +using namespace libcamera; + +class CamApp +{ +public: + CamApp(); + + static CamApp *instance(); + + int init(int argc, char **argv); + void cleanup(); + + int exec(); + void quit(); + +private: + void cameraAdded(std::shared_ptr<Camera> cam); + void cameraRemoved(std::shared_ptr<Camera> cam); + void captureDone(); + int parseOptions(int argc, char *argv[]); + int run(); + + static std::string cameraName(const Camera *camera); + + static CamApp *app_; + OptionsParser::Options options_; + + std::unique_ptr<CameraManager> cm_; + + std::atomic_uint loopUsers_; + EventLoop loop_; +}; + +CamApp *CamApp::app_ = nullptr; + +CamApp::CamApp() + : loopUsers_(0) +{ + CamApp::app_ = this; +} + +CamApp *CamApp::instance() +{ + return CamApp::app_; +} + +int CamApp::init(int argc, char **argv) +{ + int ret; + + ret = parseOptions(argc, argv); + if (ret < 0) + return ret; + + cm_ = std::make_unique<CameraManager>(); + + ret = cm_->start(); + if (ret) { + std::cout << "Failed to start camera manager: " + << strerror(-ret) << std::endl; + return ret; + } + + return 0; +} + +void CamApp::cleanup() +{ + cm_->stop(); +} + +int CamApp::exec() +{ + int ret; + + ret = run(); + cleanup(); + + return ret; +} + +void CamApp::quit() +{ + loop_.exit(); +} + +int CamApp::parseOptions(int argc, char *argv[]) +{ + StreamKeyValueParser streamKeyValue; + + OptionsParser parser; + parser.addOption(OptCamera, OptionString, + "Specify which camera to operate on, by id or by index", "camera", + ArgumentRequired, "camera", true); + parser.addOption(OptHelp, OptionNone, "Display this help message", + "help"); + parser.addOption(OptInfo, OptionNone, + "Display information about stream(s)", "info"); + parser.addOption(OptList, OptionNone, "List all cameras", "list"); + parser.addOption(OptListControls, OptionNone, "List cameras controls", + "list-controls"); + parser.addOption(OptListProperties, OptionNone, "List cameras properties", + "list-properties"); + parser.addOption(OptMonitor, OptionNone, + "Monitor for hotplug and unplug camera events", + "monitor"); + + /* Sub-options of OptCamera: */ + parser.addOption(OptCapture, OptionInteger, + "Capture until interrupted by user or until <count> frames captured", + "capture", ArgumentOptional, "count", false, + OptCamera); +#ifdef HAVE_KMS + parser.addOption(OptDisplay, OptionString, + "Display viewfinder through DRM/KMS on specified connector", + "display", ArgumentOptional, "connector", false, + OptCamera); +#endif + parser.addOption(OptFile, OptionString, + "Write captured frames to disk\n" + "If the file name ends with a '/', it sets the directory in which\n" + "to write files, using the default file name. Otherwise it sets the\n" + "full file path and name. The first '#' character in the file name\n" + "is expanded to the camera index, stream name and frame sequence number.\n" +#ifdef HAVE_TIFF + "If the file name ends with '.dng', then the frame will be written to\n" + "the output file(s) in DNG format.\n" +#endif + "The default file name is 'frame-#.bin'.", + "file", ArgumentOptional, "filename", false, + OptCamera); +#ifdef HAVE_SDL + parser.addOption(OptSDL, OptionNone, "Display viewfinder through SDL", + "sdl", ArgumentNone, "", false, OptCamera); +#endif + parser.addOption(OptStream, &streamKeyValue, + "Set configuration of a camera stream", "stream", true, + OptCamera); + parser.addOption(OptStrictFormats, OptionNone, + "Do not allow requested stream format(s) to be adjusted", + "strict-formats", ArgumentNone, nullptr, false, + OptCamera); + parser.addOption(OptMetadata, OptionNone, + "Print the metadata for completed requests", + "metadata", ArgumentNone, nullptr, false, + OptCamera); + parser.addOption(OptCaptureScript, OptionString, + "Load a capture session configuration script from a file", + "script", ArgumentRequired, "script", false, + OptCamera); + + options_ = parser.parse(argc, argv); + if (!options_.valid()) + return -EINVAL; + + if (options_.empty() || options_.isSet(OptHelp)) { + parser.usage(); + return options_.empty() ? -EINVAL : -EINTR; + } + + return 0; +} + +void CamApp::cameraAdded(std::shared_ptr<Camera> cam) +{ + std::cout << "Camera Added: " << cam->id() << std::endl; +} + +void CamApp::cameraRemoved(std::shared_ptr<Camera> cam) +{ + std::cout << "Camera Removed: " << cam->id() << std::endl; +} + +void CamApp::captureDone() +{ + if (--loopUsers_ == 0) + EventLoop::instance()->exit(0); +} + +int CamApp::run() +{ + int ret; + + /* 1. List all cameras. */ + if (options_.isSet(OptList)) { + std::cout << "Available cameras:" << std::endl; + + unsigned int index = 1; + for (const std::shared_ptr<Camera> &cam : cm_->cameras()) { + std::cout << index << ": " << cameraName(cam.get()) << std::endl; + index++; + } + } + + /* 2. Create the camera sessions. */ + std::vector<std::unique_ptr<CameraSession>> sessions; + + if (options_.isSet(OptCamera)) { + unsigned int index = 0; + + for (const OptionValue &camera : options_[OptCamera].toArray()) { + std::unique_ptr<CameraSession> session = + std::make_unique<CameraSession>(cm_.get(), + camera.toString(), + index, + camera.children()); + if (!session->isValid()) { + std::cout << "Failed to create camera session" << std::endl; + return -EINVAL; + } + + std::cout << "Using camera " << session->camera()->id() + << " as cam" << index << std::endl; + + session->captureDone.connect(this, &CamApp::captureDone); + + sessions.push_back(std::move(session)); + index++; + } + } + + /* 3. Print camera information. */ + if (options_.isSet(OptListControls) || + options_.isSet(OptListProperties) || + options_.isSet(OptInfo)) { + for (const auto &session : sessions) { + if (options_.isSet(OptListControls)) + session->listControls(); + if (options_.isSet(OptListProperties)) + session->listProperties(); + if (options_.isSet(OptInfo)) + session->infoConfiguration(); + } + } + + /* 4. Start capture. */ + for (const auto &session : sessions) { + if (!session->options().isSet(OptCapture)) + continue; + + ret = session->start(); + if (ret) { + std::cout << "Failed to start camera session" << std::endl; + return ret; + } + + loopUsers_++; + } + + /* 5. Enable hotplug monitoring. */ + if (options_.isSet(OptMonitor)) { + std::cout << "Monitoring new hotplug and unplug events" << std::endl; + std::cout << "Press Ctrl-C to interrupt" << std::endl; + + cm_->cameraAdded.connect(this, &CamApp::cameraAdded); + cm_->cameraRemoved.connect(this, &CamApp::cameraRemoved); + + loopUsers_++; + } + + if (loopUsers_) + loop_.exec(); + + /* 6. Stop capture. */ + for (const auto &session : sessions) { + if (!session->options().isSet(OptCapture)) + continue; + + session->stop(); + } + + return 0; +} + +std::string CamApp::cameraName(const Camera *camera) +{ + const ControlList &props = camera->properties(); + bool addModel = true; + std::string name; + + /* + * Construct the name from the camera location, model and ID. The model + * is only used if the location isn't present or is set to External. + */ + const auto &location = props.get(properties::Location); + if (location) { + switch (*location) { + case properties::CameraLocationFront: + addModel = false; + name = "Internal front camera "; + break; + case properties::CameraLocationBack: + addModel = false; + name = "Internal back camera "; + break; + case properties::CameraLocationExternal: + name = "External camera "; + break; + } + } + + if (addModel) { + /* + * If the camera location is not availble use the camera model + * to build the camera name. + */ + const auto &model = props.get(properties::Model); + if (model) + name = "'" + *model + "' "; + } + + name += "(" + camera->id() + ")"; + + return name; +} + +void signalHandler([[maybe_unused]] int signal) +{ + std::cout << "Exiting" << std::endl; + CamApp::instance()->quit(); +} + +int main(int argc, char **argv) +{ + CamApp app; + int ret; + + ret = app.init(argc, argv); + if (ret) + return ret == -EINTR ? 0 : EXIT_FAILURE; + + struct sigaction sa = {}; + sa.sa_handler = &signalHandler; + sigaction(SIGINT, &sa, nullptr); + + if (app.exec()) + return EXIT_FAILURE; + + return 0; +} diff --git a/src/apps/cam/main.h b/src/apps/cam/main.h new file mode 100644 index 00000000..526aecec --- /dev/null +++ b/src/apps/cam/main.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * main.h - Cam application + */ + +#pragma once + +enum { + OptCamera = 'c', + OptCapture = 'C', + OptDisplay = 'D', + OptFile = 'F', + OptHelp = 'h', + OptInfo = 'I', + OptList = 'l', + OptListProperties = 'p', + OptMonitor = 'm', + OptSDL = 'S', + OptStream = 's', + OptListControls = 256, + OptStrictFormats = 257, + OptMetadata = 258, + OptCaptureScript = 259, +}; diff --git a/src/apps/cam/meson.build b/src/apps/cam/meson.build new file mode 100644 index 00000000..06dbea06 --- /dev/null +++ b/src/apps/cam/meson.build @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: CC0-1.0 + +libevent = dependency('libevent_pthreads', required : get_option('cam')) + +if not libevent.found() + cam_enabled = false + subdir_done() +endif + +cam_enabled = true + +cam_sources = files([ + 'camera_session.cpp', + 'capture_script.cpp', + 'event_loop.cpp', + 'file_sink.cpp', + 'frame_sink.cpp', + 'image.cpp', + 'main.cpp', + 'options.cpp', + 'stream_options.cpp', +]) + +cam_cpp_args = [] + +libdrm = dependency('libdrm', required : false) +libjpeg = dependency('libjpeg', required : false) +libsdl2 = dependency('SDL2', required : false) +libtiff = dependency('libtiff-4', required : false) + +if libdrm.found() + cam_cpp_args += [ '-DHAVE_KMS' ] + cam_sources += files([ + 'drm.cpp', + 'kms_sink.cpp' + ]) +endif + +if libsdl2.found() + cam_cpp_args += ['-DHAVE_SDL'] + cam_sources += files([ + 'sdl_sink.cpp', + 'sdl_texture.cpp', + 'sdl_texture_yuv.cpp', + ]) + + if libjpeg.found() + cam_cpp_args += ['-DHAVE_LIBJPEG'] + cam_sources += files([ + 'sdl_texture_mjpg.cpp' + ]) + endif +endif + +if libtiff.found() + cam_cpp_args += ['-DHAVE_TIFF'] + cam_sources += files([ + 'dng_writer.cpp', + ]) +endif + +cam = executable('cam', cam_sources, + dependencies : [ + libatomic, + libcamera_public, + libdrm, + libevent, + libjpeg, + libsdl2, + libtiff, + libyaml, + ], + cpp_args : cam_cpp_args, + install : true) diff --git a/src/apps/cam/options.cpp b/src/apps/cam/options.cpp new file mode 100644 index 00000000..4f7e8691 --- /dev/null +++ b/src/apps/cam/options.cpp @@ -0,0 +1,1141 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * options.cpp - 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/cam/options.h b/src/apps/cam/options.h new file mode 100644 index 00000000..4ddd4987 --- /dev/null +++ b/src/apps/cam/options.h @@ -0,0 +1,157 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * options.h - 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/cam/sdl_sink.cpp b/src/apps/cam/sdl_sink.cpp new file mode 100644 index 00000000..ee177227 --- /dev/null +++ b/src/apps/cam/sdl_sink.cpp @@ -0,0 +1,214 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy + * + * sdl_sink.h - SDL Sink + */ + +#include "sdl_sink.h" + +#include <assert.h> +#include <fcntl.h> +#include <iomanip> +#include <iostream> +#include <signal.h> +#include <sstream> +#include <string.h> +#include <unistd.h> + +#include <libcamera/camera.h> +#include <libcamera/formats.h> + +#include "event_loop.h" +#include "image.h" +#ifdef HAVE_LIBJPEG +#include "sdl_texture_mjpg.h" +#endif +#include "sdl_texture_yuv.h" + +using namespace libcamera; + +using namespace std::chrono_literals; + +SDLSink::SDLSink() + : window_(nullptr), renderer_(nullptr), rect_({}), + init_(false) +{ +} + +SDLSink::~SDLSink() +{ + stop(); +} + +int SDLSink::configure(const libcamera::CameraConfiguration &config) +{ + int ret = FrameSink::configure(config); + if (ret < 0) + return ret; + + if (config.size() > 1) { + std::cerr + << "SDL sink only supports one camera stream at present, streaming first camera stream" + << std::endl; + } else if (config.empty()) { + std::cerr << "Require at least one camera stream to process" + << std::endl; + return -EINVAL; + } + + const libcamera::StreamConfiguration &cfg = config.at(0); + rect_.w = cfg.size.width; + rect_.h = cfg.size.height; + + switch (cfg.pixelFormat) { +#ifdef HAVE_LIBJPEG + case libcamera::formats::MJPEG: + texture_ = std::make_unique<SDLTextureMJPG>(rect_); + break; +#endif +#if SDL_VERSION_ATLEAST(2, 0, 16) + case libcamera::formats::NV12: + texture_ = std::make_unique<SDLTextureNV12>(rect_, cfg.stride); + break; +#endif + case libcamera::formats::YUYV: + texture_ = std::make_unique<SDLTextureYUYV>(rect_, cfg.stride); + break; + default: + std::cerr << "Unsupported pixel format " + << cfg.pixelFormat.toString() << std::endl; + return -EINVAL; + }; + + return 0; +} + +int SDLSink::start() +{ + int ret = SDL_Init(SDL_INIT_VIDEO); + if (ret) { + std::cerr << "Failed to initialize SDL: " << SDL_GetError() + << std::endl; + return ret; + } + + init_ = true; + window_ = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, rect_.w, + rect_.h, + SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); + if (!window_) { + std::cerr << "Failed to create SDL window: " << SDL_GetError() + << std::endl; + return -EINVAL; + } + + renderer_ = SDL_CreateRenderer(window_, -1, 0); + if (!renderer_) { + std::cerr << "Failed to create SDL renderer: " << SDL_GetError() + << std::endl; + return -EINVAL; + } + + /* + * Set for scaling purposes, not critical, don't return in case of + * error. + */ + ret = SDL_RenderSetLogicalSize(renderer_, rect_.w, rect_.h); + if (ret) + std::cerr << "Failed to set SDL render logical size: " + << SDL_GetError() << std::endl; + + ret = texture_->create(renderer_); + if (ret) { + return ret; + } + + /* \todo Make the event cancellable to support stop/start cycles. */ + EventLoop::instance()->addTimerEvent( + 10ms, std::bind(&SDLSink::processSDLEvents, this)); + + return 0; +} + +int SDLSink::stop() +{ + texture_.reset(); + + if (renderer_) { + SDL_DestroyRenderer(renderer_); + renderer_ = nullptr; + } + + if (window_) { + SDL_DestroyWindow(window_); + window_ = nullptr; + } + + if (init_) { + SDL_Quit(); + init_ = false; + } + + return FrameSink::stop(); +} + +void SDLSink::mapBuffer(FrameBuffer *buffer) +{ + std::unique_ptr<Image> image = + Image::fromFrameBuffer(buffer, Image::MapMode::ReadOnly); + assert(image != nullptr); + + mappedBuffers_[buffer] = std::move(image); +} + +bool SDLSink::processRequest(Request *request) +{ + for (auto [stream, buffer] : request->buffers()) { + renderBuffer(buffer); + break; /* to be expanded to launch SDL window per buffer */ + } + + return true; +} + +/* + * Process SDL events, required for things like window resize and quit button + */ +void SDLSink::processSDLEvents() +{ + for (SDL_Event e; SDL_PollEvent(&e);) { + if (e.type == SDL_QUIT) { + /* Click close icon then quit */ + EventLoop::instance()->exit(0); + } + } +} + +void SDLSink::renderBuffer(FrameBuffer *buffer) +{ + Image *image = mappedBuffers_[buffer].get(); + + std::vector<Span<const uint8_t>> planes; + unsigned int i = 0; + + planes.reserve(buffer->metadata().planes().size()); + + for (const FrameMetadata::Plane &meta : buffer->metadata().planes()) { + Span<uint8_t> data = image->data(i); + if (meta.bytesused > data.size()) + std::cerr << "payload size " << meta.bytesused + << " larger than plane size " << data.size() + << std::endl; + + planes.push_back(data); + i++; + } + + texture_->update(planes); + + SDL_RenderClear(renderer_); + SDL_RenderCopy(renderer_, texture_->get(), nullptr, nullptr); + SDL_RenderPresent(renderer_); +} diff --git a/src/apps/cam/sdl_sink.h b/src/apps/cam/sdl_sink.h new file mode 100644 index 00000000..6c19c663 --- /dev/null +++ b/src/apps/cam/sdl_sink.h @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy + * + * sdl_sink.h - SDL Sink + */ + +#pragma once + +#include <map> +#include <memory> + +#include <libcamera/stream.h> + +#include <SDL2/SDL.h> + +#include "frame_sink.h" + +class Image; +class SDLTexture; + +class SDLSink : public FrameSink +{ +public: + SDLSink(); + ~SDLSink(); + + int configure(const libcamera::CameraConfiguration &config) override; + int start() override; + int stop() override; + void mapBuffer(libcamera::FrameBuffer *buffer) override; + + bool processRequest(libcamera::Request *request) override; + +private: + void renderBuffer(libcamera::FrameBuffer *buffer); + void processSDLEvents(); + + std::map<libcamera::FrameBuffer *, std::unique_ptr<Image>> + mappedBuffers_; + + std::unique_ptr<SDLTexture> texture_; + + SDL_Window *window_; + SDL_Renderer *renderer_; + SDL_Rect rect_; + bool init_; +}; diff --git a/src/apps/cam/sdl_texture.cpp b/src/apps/cam/sdl_texture.cpp new file mode 100644 index 00000000..e9040bc5 --- /dev/null +++ b/src/apps/cam/sdl_texture.cpp @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy + * + * sdl_texture.cpp - SDL Texture + */ + +#include "sdl_texture.h" + +#include <iostream> + +SDLTexture::SDLTexture(const SDL_Rect &rect, uint32_t pixelFormat, + const int stride) + : ptr_(nullptr), rect_(rect), pixelFormat_(pixelFormat), stride_(stride) +{ +} + +SDLTexture::~SDLTexture() +{ + if (ptr_) + SDL_DestroyTexture(ptr_); +} + +int SDLTexture::create(SDL_Renderer *renderer) +{ + ptr_ = SDL_CreateTexture(renderer, pixelFormat_, + SDL_TEXTUREACCESS_STREAMING, rect_.w, + rect_.h); + if (!ptr_) { + std::cerr << "Failed to create SDL texture: " << SDL_GetError() + << std::endl; + return -ENOMEM; + } + + return 0; +} diff --git a/src/apps/cam/sdl_texture.h b/src/apps/cam/sdl_texture.h new file mode 100644 index 00000000..6ccd85ea --- /dev/null +++ b/src/apps/cam/sdl_texture.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy + * + * sdl_texture.h - SDL Texture + */ + +#pragma once + +#include <vector> + +#include <SDL2/SDL.h> + +#include "image.h" + +class SDLTexture +{ +public: + SDLTexture(const SDL_Rect &rect, uint32_t pixelFormat, const int stride); + virtual ~SDLTexture(); + int create(SDL_Renderer *renderer); + virtual void update(const std::vector<libcamera::Span<const uint8_t>> &data) = 0; + SDL_Texture *get() const { return ptr_; } + +protected: + SDL_Texture *ptr_; + const SDL_Rect rect_; + const uint32_t pixelFormat_; + const int stride_; +}; diff --git a/src/apps/cam/sdl_texture_mjpg.cpp b/src/apps/cam/sdl_texture_mjpg.cpp new file mode 100644 index 00000000..da958e03 --- /dev/null +++ b/src/apps/cam/sdl_texture_mjpg.cpp @@ -0,0 +1,83 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy + * + * sdl_texture_mjpg.cpp - SDL Texture MJPG + */ + +#include "sdl_texture_mjpg.h" + +#include <iostream> +#include <setjmp.h> +#include <stdio.h> + +#include <jpeglib.h> + +using namespace libcamera; + +struct JpegErrorManager : public jpeg_error_mgr { + JpegErrorManager() + { + jpeg_std_error(this); + error_exit = errorExit; + output_message = outputMessage; + } + + static void errorExit(j_common_ptr cinfo) + { + JpegErrorManager *self = + static_cast<JpegErrorManager *>(cinfo->err); + longjmp(self->escape_, 1); + } + + static void outputMessage([[maybe_unused]] j_common_ptr cinfo) + { + } + + jmp_buf escape_; +}; + +SDLTextureMJPG::SDLTextureMJPG(const SDL_Rect &rect) + : SDLTexture(rect, SDL_PIXELFORMAT_RGB24, rect.w * 3), + rgb_(std::make_unique<unsigned char[]>(stride_ * rect.h)) +{ +} + +int SDLTextureMJPG::decompress(Span<const uint8_t> data) +{ + struct jpeg_decompress_struct cinfo; + + JpegErrorManager errorManager; + if (setjmp(errorManager.escape_)) { + /* libjpeg found an error */ + jpeg_destroy_decompress(&cinfo); + std::cerr << "JPEG decompression error" << std::endl; + return -EINVAL; + } + + cinfo.err = &errorManager; + jpeg_create_decompress(&cinfo); + + jpeg_mem_src(&cinfo, data.data(), data.size()); + + jpeg_read_header(&cinfo, TRUE); + + jpeg_start_decompress(&cinfo); + + for (int i = 0; cinfo.output_scanline < cinfo.output_height; ++i) { + JSAMPROW rowptr = rgb_.get() + i * stride_; + jpeg_read_scanlines(&cinfo, &rowptr, 1); + } + + jpeg_finish_decompress(&cinfo); + + jpeg_destroy_decompress(&cinfo); + + return 0; +} + +void SDLTextureMJPG::update(const std::vector<libcamera::Span<const uint8_t>> &data) +{ + decompress(data[0]); + SDL_UpdateTexture(ptr_, nullptr, rgb_.get(), stride_); +} diff --git a/src/apps/cam/sdl_texture_mjpg.h b/src/apps/cam/sdl_texture_mjpg.h new file mode 100644 index 00000000..814ca79a --- /dev/null +++ b/src/apps/cam/sdl_texture_mjpg.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy + * + * sdl_texture_mjpg.h - SDL Texture MJPG + */ + +#pragma once + +#include "sdl_texture.h" + +class SDLTextureMJPG : public SDLTexture +{ +public: + SDLTextureMJPG(const SDL_Rect &rect); + + void update(const std::vector<libcamera::Span<const uint8_t>> &data) override; + +private: + int decompress(libcamera::Span<const uint8_t> data); + + std::unique_ptr<unsigned char[]> rgb_; +}; diff --git a/src/apps/cam/sdl_texture_yuv.cpp b/src/apps/cam/sdl_texture_yuv.cpp new file mode 100644 index 00000000..b29c3b93 --- /dev/null +++ b/src/apps/cam/sdl_texture_yuv.cpp @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy + * + * sdl_texture_yuv.cpp - SDL YUV Textures + */ + +#include "sdl_texture_yuv.h" + +using namespace libcamera; + +#if SDL_VERSION_ATLEAST(2, 0, 16) +SDLTextureNV12::SDLTextureNV12(const SDL_Rect &rect, unsigned int stride) + : SDLTexture(rect, SDL_PIXELFORMAT_NV12, stride) +{ +} + +void SDLTextureNV12::update(const std::vector<libcamera::Span<const uint8_t>> &data) +{ + SDL_UpdateNVTexture(ptr_, &rect_, data[0].data(), stride_, + data[1].data(), stride_); +} +#endif + +SDLTextureYUYV::SDLTextureYUYV(const SDL_Rect &rect, unsigned int stride) + : SDLTexture(rect, SDL_PIXELFORMAT_YUY2, stride) +{ +} + +void SDLTextureYUYV::update(const std::vector<libcamera::Span<const uint8_t>> &data) +{ + SDL_UpdateTexture(ptr_, &rect_, data[0].data(), stride_); +} diff --git a/src/apps/cam/sdl_texture_yuv.h b/src/apps/cam/sdl_texture_yuv.h new file mode 100644 index 00000000..310e4e50 --- /dev/null +++ b/src/apps/cam/sdl_texture_yuv.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy + * + * sdl_texture_yuv.h - SDL YUV Textures + */ + +#pragma once + +#include "sdl_texture.h" + +#if SDL_VERSION_ATLEAST(2, 0, 16) +class SDLTextureNV12 : public SDLTexture +{ +public: + SDLTextureNV12(const SDL_Rect &rect, unsigned int stride); + void update(const std::vector<libcamera::Span<const uint8_t>> &data) override; +}; +#endif + +class SDLTextureYUYV : public SDLTexture +{ +public: + SDLTextureYUYV(const SDL_Rect &rect, unsigned int stride); + void update(const std::vector<libcamera::Span<const uint8_t>> &data) override; +}; diff --git a/src/apps/cam/stream_options.cpp b/src/apps/cam/stream_options.cpp new file mode 100644 index 00000000..3a5625f5 --- /dev/null +++ b/src/apps/cam/stream_options.cpp @@ -0,0 +1,134 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Raspberry Pi Ltd + * + * stream_options.cpp - 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); + StreamRole role; + + if (options.valid() && options.isSet("role") && + !parseRole(&role, options)) { + std::cerr << "Unknown stream role " + << options["role"].toString() << std::endl; + options.invalidate(); + } + + return options; +} + +StreamRoles 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(); + + StreamRoles roles; + for (auto const &value : streamParameters) { + StreamRole role; + + /* If role is invalid or not set default to viewfinder. */ + if (!parseRole(&role, value.toKeyValues())) + role = StreamRole::Viewfinder; + + roles.push_back(role); + } + + 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; +} + +bool StreamKeyValueParser::parseRole(StreamRole *role, + const KeyValueParser::Options &options) +{ + if (!options.isSet("role")) + return false; + + std::string name = options["role"].toString(); + + if (name == "viewfinder") { + *role = StreamRole::Viewfinder; + return true; + } else if (name == "video") { + *role = StreamRole::VideoRecording; + return true; + } else if (name == "still") { + *role = StreamRole::StillCapture; + return true; + } else if (name == "raw") { + *role = StreamRole::Raw; + return true; + } + + return false; +} diff --git a/src/apps/cam/stream_options.h b/src/apps/cam/stream_options.h new file mode 100644 index 00000000..35e4e7c0 --- /dev/null +++ b/src/apps/cam/stream_options.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Raspberry Pi Ltd + * + * stream_options.h - Helper to parse options for streams + */ + +#pragma once + +#include <libcamera/camera.h> + +#include "options.h" + +class StreamKeyValueParser : public KeyValueParser +{ +public: + StreamKeyValueParser(); + + KeyValueParser::Options parse(const char *arguments) override; + + static libcamera::StreamRoles roles(const OptionValue &values); + static int updateConfiguration(libcamera::CameraConfiguration *config, + const OptionValue &values); + +private: + static bool parseRole(libcamera::StreamRole *role, + const KeyValueParser::Options &options); +}; diff --git a/src/apps/lc-compliance/capture_test.cpp b/src/apps/lc-compliance/capture_test.cpp new file mode 100644 index 00000000..52578207 --- /dev/null +++ b/src/apps/lc-compliance/capture_test.cpp @@ -0,0 +1,128 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Google Inc. + * Copyright (C) 2021, Collabora Ltd. + * + * capture_test.cpp - Test camera capture + */ + +#include <iostream> + +#include <gtest/gtest.h> + +#include "environment.h" +#include "simple_capture.h" + +using namespace libcamera; + +const std::vector<int> NUMREQUESTS = { 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; +const std::vector<StreamRole> ROLES = { Raw, StillCapture, VideoRecording, Viewfinder }; + +class SingleStream : public testing::TestWithParam<std::tuple<StreamRole, int>> +{ +public: + static std::string nameParameters(const testing::TestParamInfo<SingleStream::ParamType> &info); + +protected: + void SetUp() override; + void TearDown() override; + + std::shared_ptr<Camera> camera_; +}; + +/* + * We use gtest's SetUp() and TearDown() instead of constructor and destructor + * in order to be able to assert on them. + */ +void SingleStream::SetUp() +{ + Environment *env = Environment::get(); + + camera_ = env->cm()->get(env->cameraId()); + + ASSERT_EQ(camera_->acquire(), 0); +} + +void SingleStream::TearDown() +{ + if (!camera_) + return; + + camera_->release(); + camera_.reset(); +} + +std::string SingleStream::nameParameters(const testing::TestParamInfo<SingleStream::ParamType> &info) +{ + std::map<StreamRole, std::string> rolesMap = { { Raw, "Raw" }, + { StillCapture, "StillCapture" }, + { VideoRecording, "VideoRecording" }, + { Viewfinder, "Viewfinder" } }; + + std::string roleName = rolesMap[std::get<0>(info.param)]; + std::string numRequestsName = std::to_string(std::get<1>(info.param)); + + return roleName + "_" + numRequestsName; +} + +/* + * Test single capture cycles + * + * Makes sure the camera completes the exact number of requests queued. Example + * failure is a camera that completes less requests than the number of requests + * queued. + */ +TEST_P(SingleStream, Capture) +{ + auto [role, numRequests] = GetParam(); + + SimpleCaptureBalanced capture(camera_); + + capture.configure(role); + + capture.capture(numRequests); +} + +/* + * Test multiple start/stop cycles + * + * Makes sure the camera supports multiple start/stop cycles. Example failure is + * a camera that does not clean up correctly in its error path but is only + * tested by single-capture applications. + */ +TEST_P(SingleStream, CaptureStartStop) +{ + auto [role, numRequests] = GetParam(); + unsigned int numRepeats = 3; + + SimpleCaptureBalanced capture(camera_); + + capture.configure(role); + + for (unsigned int starts = 0; starts < numRepeats; starts++) + capture.capture(numRequests); +} + +/* + * Test unbalanced stop + * + * Makes sure the camera supports a stop with requests queued. Example failure + * is a camera that does not handle cancelation of buffers coming back from the + * video device while stopping. + */ +TEST_P(SingleStream, UnbalancedStop) +{ + auto [role, numRequests] = GetParam(); + + SimpleCaptureUnbalanced capture(camera_); + + capture.configure(role); + + capture.capture(numRequests); +} + +INSTANTIATE_TEST_SUITE_P(CaptureTests, + SingleStream, + testing::Combine(testing::ValuesIn(ROLES), + testing::ValuesIn(NUMREQUESTS)), + SingleStream::nameParameters); diff --git a/src/apps/lc-compliance/environment.cpp b/src/apps/lc-compliance/environment.cpp new file mode 100644 index 00000000..5eb3775f --- /dev/null +++ b/src/apps/lc-compliance/environment.cpp @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Collabora Ltd. + * + * environment.cpp - Common environment for tests + */ + +#include "environment.h" + +using namespace libcamera; + +Environment *Environment::get() +{ + static Environment instance; + return &instance; +} + +void Environment::setup(CameraManager *cm, std::string cameraId) +{ + cm_ = cm; + cameraId_ = cameraId; +} diff --git a/src/apps/lc-compliance/environment.h b/src/apps/lc-compliance/environment.h new file mode 100644 index 00000000..0debbcce --- /dev/null +++ b/src/apps/lc-compliance/environment.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Collabora Ltd. + * + * environment.h - Common environment for tests + */ + +#pragma once + +#include <libcamera/libcamera.h> + +class Environment +{ +public: + static Environment *get(); + + void setup(libcamera::CameraManager *cm, std::string cameraId); + + const std::string &cameraId() const { return cameraId_; } + libcamera::CameraManager *cm() const { return cm_; } + +private: + Environment() = default; + + std::string cameraId_; + libcamera::CameraManager *cm_; +}; diff --git a/src/apps/lc-compliance/main.cpp b/src/apps/lc-compliance/main.cpp new file mode 100644 index 00000000..7eb52ae4 --- /dev/null +++ b/src/apps/lc-compliance/main.cpp @@ -0,0 +1,193 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Google Inc. + * Copyright (C) 2021, Collabora Ltd. + * + * main.cpp - lc-compliance - The libcamera compliance tool + */ + +#include <iomanip> +#include <iostream> +#include <string.h> + +#include <gtest/gtest.h> + +#include <libcamera/libcamera.h> + +#include "environment.h" +#include "../cam/options.h" + +using namespace libcamera; + +enum { + OptCamera = 'c', + OptList = 'l', + OptFilter = 'f', + OptHelp = 'h', +}; + +/* + * Make asserts act like exceptions, otherwise they only fail (or skip) the + * current function. From gtest documentation: + * https://google.github.io/googletest/advanced.html#asserting-on-subroutines-with-an-exception + */ +class ThrowListener : public testing::EmptyTestEventListener +{ + void OnTestPartResult(const testing::TestPartResult &result) override + { + if (result.type() == testing::TestPartResult::kFatalFailure || + result.type() == testing::TestPartResult::kSkip) + throw testing::AssertionException(result); + } +}; + +static void listCameras(CameraManager *cm) +{ + for (const std::shared_ptr<Camera> &cam : cm->cameras()) + std::cout << "- " << cam.get()->id() << std::endl; +} + +static int initCamera(CameraManager *cm, OptionsParser::Options options) +{ + std::shared_ptr<Camera> camera; + + int ret = cm->start(); + if (ret) { + std::cout << "Failed to start camera manager: " + << strerror(-ret) << std::endl; + return ret; + } + + if (!options.isSet(OptCamera)) { + std::cout << "No camera specified, available cameras:" << std::endl; + listCameras(cm); + return -ENODEV; + } + + const std::string &cameraId = options[OptCamera]; + camera = cm->get(cameraId); + if (!camera) { + std::cout << "Camera " << cameraId << " not found, available cameras:" << std::endl; + listCameras(cm); + return -ENODEV; + } + + Environment::get()->setup(cm, cameraId); + + std::cout << "Using camera " << cameraId << std::endl; + + return 0; +} + +static int initGtestParameters(char *arg0, OptionsParser::Options options) +{ + const std::map<std::string, std::string> gtestFlags = { { "list", "--gtest_list_tests" }, + { "filter", "--gtest_filter" } }; + + int argc = 0; + std::string filterParam; + + /* + * +2 to have space for both the 0th argument that is needed but not + * used and the null at the end. + */ + char **argv = new char *[(gtestFlags.size() + 2)]; + if (!argv) + return -ENOMEM; + + argv[0] = arg0; + argc++; + + if (options.isSet(OptList)) { + argv[argc] = const_cast<char *>(gtestFlags.at("list").c_str()); + argc++; + } + + if (options.isSet(OptFilter)) { + /* + * The filter flag needs to be passed as a single parameter, in + * the format --gtest_filter=filterStr + */ + filterParam = gtestFlags.at("filter") + "=" + + static_cast<const std::string &>(options[OptFilter]); + + argv[argc] = const_cast<char *>(filterParam.c_str()); + argc++; + } + + argv[argc] = nullptr; + + ::testing::InitGoogleTest(&argc, argv); + + delete[] argv; + + return 0; +} + +static int initGtest(char *arg0, OptionsParser::Options options) +{ + int ret = initGtestParameters(arg0, options); + if (ret) + return ret; + + testing::UnitTest::GetInstance()->listeners().Append(new ThrowListener); + + return 0; +} + +static int parseOptions(int argc, char **argv, OptionsParser::Options *options) +{ + OptionsParser parser; + parser.addOption(OptCamera, OptionString, + "Specify which camera to operate on, by id", "camera", + ArgumentRequired, "camera"); + parser.addOption(OptList, OptionNone, "List all tests and exit", "list"); + parser.addOption(OptFilter, OptionString, + "Specify which tests to run", "filter", + ArgumentRequired, "filter"); + parser.addOption(OptHelp, OptionNone, "Display this help message", + "help"); + + *options = parser.parse(argc, argv); + if (!options->valid()) + return -EINVAL; + + if (options->isSet(OptHelp)) { + parser.usage(); + std::cerr << "Further options from Googletest can be passed as environment variables" + << std::endl; + return -EINTR; + } + + return 0; +} + +int main(int argc, char **argv) +{ + OptionsParser::Options options; + int ret = parseOptions(argc, argv, &options); + if (ret == -EINTR) + return EXIT_SUCCESS; + if (ret < 0) + return EXIT_FAILURE; + + std::unique_ptr<CameraManager> cm = std::make_unique<CameraManager>(); + + /* No need to initialize the camera if we'll just list tests */ + if (!options.isSet(OptList)) { + ret = initCamera(cm.get(), options); + if (ret) + return ret; + } + + ret = initGtest(argv[0], options); + if (ret) + return ret; + + ret = RUN_ALL_TESTS(); + + if (!options.isSet(OptList)) + cm->stop(); + + return ret; +} diff --git a/src/apps/lc-compliance/meson.build b/src/apps/lc-compliance/meson.build new file mode 100644 index 00000000..8b57474b --- /dev/null +++ b/src/apps/lc-compliance/meson.build @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: CC0-1.0 + +libevent = dependency('libevent_pthreads', required : get_option('lc-compliance')) +libgtest = dependency('gtest', required : get_option('lc-compliance'), + fallback : ['gtest', 'gtest_dep']) + +if not (libevent.found() and libgtest.found()) + lc_compliance_enabled = false + subdir_done() +endif + +lc_compliance_enabled = true + +lc_compliance_sources = files([ + '../cam/event_loop.cpp', + '../cam/options.cpp', + 'environment.cpp', + 'main.cpp', + 'simple_capture.cpp', + 'capture_test.cpp', +]) + +lc_compliance = executable('lc-compliance', lc_compliance_sources, + cpp_args : [ '-fexceptions' ], + dependencies : [ + libatomic, + libcamera_public, + libevent, + libgtest, + ], + install : true) diff --git a/src/apps/lc-compliance/simple_capture.cpp b/src/apps/lc-compliance/simple_capture.cpp new file mode 100644 index 00000000..ab5cb35c --- /dev/null +++ b/src/apps/lc-compliance/simple_capture.cpp @@ -0,0 +1,191 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020-2021, Google Inc. + * + * simple_capture.cpp - Simple capture helper + */ + +#include <gtest/gtest.h> + +#include "simple_capture.h" + +using namespace libcamera; + +SimpleCapture::SimpleCapture(std::shared_ptr<Camera> camera) + : loop_(nullptr), camera_(camera), + allocator_(std::make_unique<FrameBufferAllocator>(camera)) +{ +} + +SimpleCapture::~SimpleCapture() +{ + stop(); +} + +void SimpleCapture::configure(StreamRole role) +{ + config_ = camera_->generateConfiguration({ role }); + + if (!config_) { + std::cout << "Role not supported by camera" << std::endl; + GTEST_SKIP(); + } + + if (config_->validate() != CameraConfiguration::Valid) { + config_.reset(); + FAIL() << "Configuration not valid"; + } + + if (camera_->configure(config_.get())) { + config_.reset(); + FAIL() << "Failed to configure camera"; + } +} + +void SimpleCapture::start() +{ + Stream *stream = config_->at(0).stream(); + int count = allocator_->allocate(stream); + + ASSERT_GE(count, 0) << "Failed to allocate buffers"; + EXPECT_EQ(count, config_->at(0).bufferCount) << "Allocated less buffers than expected"; + + camera_->requestCompleted.connect(this, &SimpleCapture::requestComplete); + + ASSERT_EQ(camera_->start(), 0) << "Failed to start camera"; +} + +void SimpleCapture::stop() +{ + if (!config_ || !allocator_->allocated()) + return; + + camera_->stop(); + + camera_->requestCompleted.disconnect(this); + + Stream *stream = config_->at(0).stream(); + allocator_->free(stream); +} + +/* SimpleCaptureBalanced */ + +SimpleCaptureBalanced::SimpleCaptureBalanced(std::shared_ptr<Camera> camera) + : SimpleCapture(camera) +{ +} + +void SimpleCaptureBalanced::capture(unsigned int numRequests) +{ + start(); + + Stream *stream = config_->at(0).stream(); + const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream); + + /* No point in testing less requests then the camera depth. */ + if (buffers.size() > numRequests) { + std::cout << "Camera needs " + std::to_string(buffers.size()) + + " requests, can't test only " + + std::to_string(numRequests) << std::endl; + GTEST_SKIP(); + } + + queueCount_ = 0; + captureCount_ = 0; + captureLimit_ = numRequests; + + /* Queue the recommended number of reqeuests. */ + std::vector<std::unique_ptr<libcamera::Request>> requests; + for (const std::unique_ptr<FrameBuffer> &buffer : buffers) { + std::unique_ptr<Request> request = camera_->createRequest(); + ASSERT_TRUE(request) << "Can't create request"; + + ASSERT_EQ(request->addBuffer(stream, buffer.get()), 0) << "Can't set buffer for request"; + + ASSERT_EQ(queueRequest(request.get()), 0) << "Failed to queue request"; + + requests.push_back(std::move(request)); + } + + /* Run capture session. */ + loop_ = new EventLoop(); + loop_->exec(); + stop(); + delete loop_; + + ASSERT_EQ(captureCount_, captureLimit_); +} + +int SimpleCaptureBalanced::queueRequest(Request *request) +{ + queueCount_++; + if (queueCount_ > captureLimit_) + return 0; + + return camera_->queueRequest(request); +} + +void SimpleCaptureBalanced::requestComplete(Request *request) +{ + captureCount_++; + if (captureCount_ >= captureLimit_) { + loop_->exit(0); + return; + } + + request->reuse(Request::ReuseBuffers); + if (queueRequest(request)) + loop_->exit(-EINVAL); +} + +/* SimpleCaptureUnbalanced */ + +SimpleCaptureUnbalanced::SimpleCaptureUnbalanced(std::shared_ptr<Camera> camera) + : SimpleCapture(camera) +{ +} + +void SimpleCaptureUnbalanced::capture(unsigned int numRequests) +{ + start(); + + Stream *stream = config_->at(0).stream(); + const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream); + + captureCount_ = 0; + captureLimit_ = numRequests; + + /* Queue the recommended number of reqeuests. */ + std::vector<std::unique_ptr<libcamera::Request>> requests; + for (const std::unique_ptr<FrameBuffer> &buffer : buffers) { + std::unique_ptr<Request> request = camera_->createRequest(); + ASSERT_TRUE(request) << "Can't create request"; + + ASSERT_EQ(request->addBuffer(stream, buffer.get()), 0) << "Can't set buffer for request"; + + ASSERT_EQ(camera_->queueRequest(request.get()), 0) << "Failed to queue request"; + + requests.push_back(std::move(request)); + } + + /* Run capture session. */ + loop_ = new EventLoop(); + int status = loop_->exec(); + stop(); + delete loop_; + + ASSERT_EQ(status, 0); +} + +void SimpleCaptureUnbalanced::requestComplete(Request *request) +{ + captureCount_++; + if (captureCount_ >= captureLimit_) { + loop_->exit(0); + return; + } + + request->reuse(Request::ReuseBuffers); + if (camera_->queueRequest(request)) + loop_->exit(-EINVAL); +} diff --git a/src/apps/lc-compliance/simple_capture.h b/src/apps/lc-compliance/simple_capture.h new file mode 100644 index 00000000..9d31f7cb --- /dev/null +++ b/src/apps/lc-compliance/simple_capture.h @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020-2021, Google Inc. + * + * simple_capture.h - Simple capture helper + */ + +#pragma once + +#include <memory> + +#include <libcamera/libcamera.h> + +#include "../cam/event_loop.h" + +class SimpleCapture +{ +public: + void configure(libcamera::StreamRole role); + +protected: + SimpleCapture(std::shared_ptr<libcamera::Camera> camera); + virtual ~SimpleCapture(); + + void start(); + void stop(); + + virtual void requestComplete(libcamera::Request *request) = 0; + + EventLoop *loop_; + + std::shared_ptr<libcamera::Camera> camera_; + std::unique_ptr<libcamera::FrameBufferAllocator> allocator_; + std::unique_ptr<libcamera::CameraConfiguration> config_; +}; + +class SimpleCaptureBalanced : public SimpleCapture +{ +public: + SimpleCaptureBalanced(std::shared_ptr<libcamera::Camera> camera); + + void capture(unsigned int numRequests); + +private: + int queueRequest(libcamera::Request *request); + void requestComplete(libcamera::Request *request) override; + + unsigned int queueCount_; + unsigned int captureCount_; + unsigned int captureLimit_; +}; + +class SimpleCaptureUnbalanced : public SimpleCapture +{ +public: + SimpleCaptureUnbalanced(std::shared_ptr<libcamera::Camera> camera); + + void capture(unsigned int numRequests); + +private: + void requestComplete(libcamera::Request *request) override; + + unsigned int captureCount_; + unsigned int captureLimit_; +}; diff --git a/src/apps/meson.build b/src/apps/meson.build new file mode 100644 index 00000000..9e4388bd --- /dev/null +++ b/src/apps/meson.build @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: CC0-1.0 + +subdir('lc-compliance') + +subdir('cam') +subdir('qcam') diff --git a/src/apps/qcam/assets/feathericons/activity.svg b/src/apps/qcam/assets/feathericons/activity.svg new file mode 100644 index 00000000..669a57a7 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/activity.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-activity"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/airplay.svg b/src/apps/qcam/assets/feathericons/airplay.svg new file mode 100644 index 00000000..7ce73022 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/airplay.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-airplay"><path d="M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"></path><polygon points="12 15 17 21 7 21 12 15"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/alert-circle.svg b/src/apps/qcam/assets/feathericons/alert-circle.svg new file mode 100644 index 00000000..8d02b7d1 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/alert-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/alert-octagon.svg b/src/apps/qcam/assets/feathericons/alert-octagon.svg new file mode 100644 index 00000000..de9b03f2 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/alert-octagon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-octagon"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/alert-triangle.svg b/src/apps/qcam/assets/feathericons/alert-triangle.svg new file mode 100644 index 00000000..6dcb0963 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/alert-triangle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/align-center.svg b/src/apps/qcam/assets/feathericons/align-center.svg new file mode 100644 index 00000000..5b8842ea --- /dev/null +++ b/src/apps/qcam/assets/feathericons/align-center.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-center"><line x1="18" y1="10" x2="6" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="18" y1="18" x2="6" y2="18"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/align-justify.svg b/src/apps/qcam/assets/feathericons/align-justify.svg new file mode 100644 index 00000000..0539876f --- /dev/null +++ b/src/apps/qcam/assets/feathericons/align-justify.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-justify"><line x1="21" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="3" y2="18"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/align-left.svg b/src/apps/qcam/assets/feathericons/align-left.svg new file mode 100644 index 00000000..9ac852a5 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/align-left.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/align-right.svg b/src/apps/qcam/assets/feathericons/align-right.svg new file mode 100644 index 00000000..ef139ffa --- /dev/null +++ b/src/apps/qcam/assets/feathericons/align-right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-right"><line x1="21" y1="10" x2="7" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="7" y2="18"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/anchor.svg b/src/apps/qcam/assets/feathericons/anchor.svg new file mode 100644 index 00000000..e01627a3 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/anchor.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor"><circle cx="12" cy="5" r="3"></circle><line x1="12" y1="22" x2="12" y2="8"></line><path d="M5 12H2a10 10 0 0 0 20 0h-3"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/aperture.svg b/src/apps/qcam/assets/feathericons/aperture.svg new file mode 100644 index 00000000..9936e868 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/aperture.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-aperture"><circle cx="12" cy="12" r="10"></circle><line x1="14.31" y1="8" x2="20.05" y2="17.94"></line><line x1="9.69" y1="8" x2="21.17" y2="8"></line><line x1="7.38" y1="12" x2="13.12" y2="2.06"></line><line x1="9.69" y1="16" x2="3.95" y2="6.06"></line><line x1="14.31" y1="16" x2="2.83" y2="16"></line><line x1="16.62" y1="12" x2="10.88" y2="21.94"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/archive.svg b/src/apps/qcam/assets/feathericons/archive.svg new file mode 100644 index 00000000..428882c8 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/archive.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-archive"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-down-circle.svg b/src/apps/qcam/assets/feathericons/arrow-down-circle.svg new file mode 100644 index 00000000..3238091b --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-down-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="8 12 12 16 16 12"></polyline><line x1="12" y1="8" x2="12" y2="16"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-down-left.svg b/src/apps/qcam/assets/feathericons/arrow-down-left.svg new file mode 100644 index 00000000..72483584 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-down-left.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-left"><line x1="17" y1="7" x2="7" y2="17"></line><polyline points="17 17 7 17 7 7"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-down-right.svg b/src/apps/qcam/assets/feathericons/arrow-down-right.svg new file mode 100644 index 00000000..81d9822b --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-down-right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-right"><line x1="7" y1="7" x2="17" y2="17"></line><polyline points="17 7 17 17 7 17"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-down.svg b/src/apps/qcam/assets/feathericons/arrow-down.svg new file mode 100644 index 00000000..4f84f627 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-left-circle.svg b/src/apps/qcam/assets/feathericons/arrow-left-circle.svg new file mode 100644 index 00000000..3b19ff8a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-left-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="12 8 8 12 12 16"></polyline><line x1="16" y1="12" x2="8" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-left.svg b/src/apps/qcam/assets/feathericons/arrow-left.svg new file mode 100644 index 00000000..a5058fc7 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-left.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-right-circle.svg b/src/apps/qcam/assets/feathericons/arrow-right-circle.svg new file mode 100644 index 00000000..ff01dd58 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-right-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="12 16 16 12 12 8"></polyline><line x1="8" y1="12" x2="16" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-right.svg b/src/apps/qcam/assets/feathericons/arrow-right.svg new file mode 100644 index 00000000..939b57c5 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-up-circle.svg b/src/apps/qcam/assets/feathericons/arrow-up-circle.svg new file mode 100644 index 00000000..044a75d3 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-up-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="16 12 12 8 8 12"></polyline><line x1="12" y1="16" x2="12" y2="8"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-up-left.svg b/src/apps/qcam/assets/feathericons/arrow-up-left.svg new file mode 100644 index 00000000..cea55e87 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-up-left.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-left"><line x1="17" y1="17" x2="7" y2="7"></line><polyline points="7 17 7 7 17 7"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-up-right.svg b/src/apps/qcam/assets/feathericons/arrow-up-right.svg new file mode 100644 index 00000000..95678e00 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-up-right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-right"><line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/arrow-up.svg b/src/apps/qcam/assets/feathericons/arrow-up.svg new file mode 100644 index 00000000..16b13aba --- /dev/null +++ b/src/apps/qcam/assets/feathericons/arrow-up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/at-sign.svg b/src/apps/qcam/assets/feathericons/at-sign.svg new file mode 100644 index 00000000..5a5e5d0d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/at-sign.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-at-sign"><circle cx="12" cy="12" r="4"></circle><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/award.svg b/src/apps/qcam/assets/feathericons/award.svg new file mode 100644 index 00000000..be70d5a1 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/award.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-award"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/bar-chart-2.svg b/src/apps/qcam/assets/feathericons/bar-chart-2.svg new file mode 100644 index 00000000..864167a6 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/bar-chart-2.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart-2"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/bar-chart.svg b/src/apps/qcam/assets/feathericons/bar-chart.svg new file mode 100644 index 00000000..074d7c1a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/bar-chart.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart"><line x1="12" y1="20" x2="12" y2="10"></line><line x1="18" y1="20" x2="18" y2="4"></line><line x1="6" y1="20" x2="6" y2="16"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/battery-charging.svg b/src/apps/qcam/assets/feathericons/battery-charging.svg new file mode 100644 index 00000000..644cb59c --- /dev/null +++ b/src/apps/qcam/assets/feathericons/battery-charging.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-battery-charging"><path d="M5 18H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3.19M15 6h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3.19"></path><line x1="23" y1="13" x2="23" y2="11"></line><polyline points="11 6 7 12 13 12 9 18"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/battery.svg b/src/apps/qcam/assets/feathericons/battery.svg new file mode 100644 index 00000000..7fe87710 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/battery.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-battery"><rect x="1" y="6" width="18" height="12" rx="2" ry="2"></rect><line x1="23" y1="13" x2="23" y2="11"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/bell-off.svg b/src/apps/qcam/assets/feathericons/bell-off.svg new file mode 100644 index 00000000..4b07c848 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/bell-off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bell-off"><path d="M13.73 21a2 2 0 0 1-3.46 0"></path><path d="M18.63 13A17.89 17.89 0 0 1 18 8"></path><path d="M6.26 6.26A5.86 5.86 0 0 0 6 8c0 7-3 9-3 9h14"></path><path d="M18 8a6 6 0 0 0-9.33-5"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/bell.svg b/src/apps/qcam/assets/feathericons/bell.svg new file mode 100644 index 00000000..bba561c1 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/bell.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bell"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/bluetooth.svg b/src/apps/qcam/assets/feathericons/bluetooth.svg new file mode 100644 index 00000000..cebed7b1 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/bluetooth.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bluetooth"><polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/bold.svg b/src/apps/qcam/assets/feathericons/bold.svg new file mode 100644 index 00000000..d1a4efd3 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/bold.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bold"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/book-open.svg b/src/apps/qcam/assets/feathericons/book-open.svg new file mode 100644 index 00000000..5e0ca0ab --- /dev/null +++ b/src/apps/qcam/assets/feathericons/book-open.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book-open"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/book.svg b/src/apps/qcam/assets/feathericons/book.svg new file mode 100644 index 00000000..12ffcbc4 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/book.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/bookmark.svg b/src/apps/qcam/assets/feathericons/bookmark.svg new file mode 100644 index 00000000..2239cc58 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/bookmark.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bookmark"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/box.svg b/src/apps/qcam/assets/feathericons/box.svg new file mode 100644 index 00000000..d89be30f --- /dev/null +++ b/src/apps/qcam/assets/feathericons/box.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-box"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/briefcase.svg b/src/apps/qcam/assets/feathericons/briefcase.svg new file mode 100644 index 00000000..e3af0506 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/briefcase.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-briefcase"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/calendar.svg b/src/apps/qcam/assets/feathericons/calendar.svg new file mode 100644 index 00000000..6c7fd870 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/calendar.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-calendar"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/camera-off.svg b/src/apps/qcam/assets/feathericons/camera-off.svg new file mode 100644 index 00000000..daa3e25f --- /dev/null +++ b/src/apps/qcam/assets/feathericons/camera-off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-camera-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M21 21H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3m3-3h6l2 3h4a2 2 0 0 1 2 2v9.34m-7.72-2.06a4 4 0 1 1-5.56-5.56"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/camera.svg b/src/apps/qcam/assets/feathericons/camera.svg new file mode 100644 index 00000000..0e7f0603 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/camera.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-camera"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/cast.svg b/src/apps/qcam/assets/feathericons/cast.svg new file mode 100644 index 00000000..63c954d9 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/cast.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cast"><path d="M2 16.1A5 5 0 0 1 5.9 20M2 12.05A9 9 0 0 1 9.95 20M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"></path><line x1="2" y1="20" x2="2.01" y2="20"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/check-circle.svg b/src/apps/qcam/assets/feathericons/check-circle.svg new file mode 100644 index 00000000..f2f4fd1a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/check-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-circle"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/check-square.svg b/src/apps/qcam/assets/feathericons/check-square.svg new file mode 100644 index 00000000..72ab7a80 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/check-square.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-square"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/check.svg b/src/apps/qcam/assets/feathericons/check.svg new file mode 100644 index 00000000..1c209899 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/check.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/chevron-down.svg b/src/apps/qcam/assets/feathericons/chevron-down.svg new file mode 100644 index 00000000..278c6a31 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/chevron-down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/chevron-left.svg b/src/apps/qcam/assets/feathericons/chevron-left.svg new file mode 100644 index 00000000..747d46d9 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/chevron-left.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-left"><polyline points="15 18 9 12 15 6"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/chevron-right.svg b/src/apps/qcam/assets/feathericons/chevron-right.svg new file mode 100644 index 00000000..258de414 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/chevron-right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/chevron-up.svg b/src/apps/qcam/assets/feathericons/chevron-up.svg new file mode 100644 index 00000000..4eb5ecc3 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/chevron-up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/chevrons-down.svg b/src/apps/qcam/assets/feathericons/chevrons-down.svg new file mode 100644 index 00000000..e67ef2fb --- /dev/null +++ b/src/apps/qcam/assets/feathericons/chevrons-down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-down"><polyline points="7 13 12 18 17 13"></polyline><polyline points="7 6 12 11 17 6"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/chevrons-left.svg b/src/apps/qcam/assets/feathericons/chevrons-left.svg new file mode 100644 index 00000000..c32e3983 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/chevrons-left.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-left"><polyline points="11 17 6 12 11 7"></polyline><polyline points="18 17 13 12 18 7"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/chevrons-right.svg b/src/apps/qcam/assets/feathericons/chevrons-right.svg new file mode 100644 index 00000000..f5068145 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/chevrons-right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-right"><polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/chevrons-up.svg b/src/apps/qcam/assets/feathericons/chevrons-up.svg new file mode 100644 index 00000000..0eaf5183 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/chevrons-up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-up"><polyline points="17 11 12 6 7 11"></polyline><polyline points="17 18 12 13 7 18"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/chrome.svg b/src/apps/qcam/assets/feathericons/chrome.svg new file mode 100644 index 00000000..9189815e --- /dev/null +++ b/src/apps/qcam/assets/feathericons/chrome.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chrome"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="21.17" y1="8" x2="12" y2="8"></line><line x1="3.95" y1="6.06" x2="8.54" y2="14"></line><line x1="10.88" y1="21.94" x2="15.46" y2="14"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/circle.svg b/src/apps/qcam/assets/feathericons/circle.svg new file mode 100644 index 00000000..b0090882 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-circle"><circle cx="12" cy="12" r="10"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/clipboard.svg b/src/apps/qcam/assets/feathericons/clipboard.svg new file mode 100644 index 00000000..ccee454d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/clipboard.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clipboard"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/clock.svg b/src/apps/qcam/assets/feathericons/clock.svg new file mode 100644 index 00000000..ea3f5e50 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/clock.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clock"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/cloud-drizzle.svg b/src/apps/qcam/assets/feathericons/cloud-drizzle.svg new file mode 100644 index 00000000..13af6bb5 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/cloud-drizzle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-drizzle"><line x1="8" y1="19" x2="8" y2="21"></line><line x1="8" y1="13" x2="8" y2="15"></line><line x1="16" y1="19" x2="16" y2="21"></line><line x1="16" y1="13" x2="16" y2="15"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="12" y1="15" x2="12" y2="17"></line><path d="M20 16.58A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/cloud-lightning.svg b/src/apps/qcam/assets/feathericons/cloud-lightning.svg new file mode 100644 index 00000000..32d154cc --- /dev/null +++ b/src/apps/qcam/assets/feathericons/cloud-lightning.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-lightning"><path d="M19 16.9A5 5 0 0 0 18 7h-1.26a8 8 0 1 0-11.62 9"></path><polyline points="13 11 9 17 15 17 11 23"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/cloud-off.svg b/src/apps/qcam/assets/feathericons/cloud-off.svg new file mode 100644 index 00000000..1e1e7d60 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/cloud-off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-off"><path d="M22.61 16.95A5 5 0 0 0 18 10h-1.26a8 8 0 0 0-7.05-6M5 5a8 8 0 0 0 4 15h9a5 5 0 0 0 1.7-.3"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/cloud-rain.svg b/src/apps/qcam/assets/feathericons/cloud-rain.svg new file mode 100644 index 00000000..3e0b85b0 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/cloud-rain.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-rain"><line x1="16" y1="13" x2="16" y2="21"></line><line x1="8" y1="13" x2="8" y2="21"></line><line x1="12" y1="15" x2="12" y2="23"></line><path d="M20 16.58A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/cloud-snow.svg b/src/apps/qcam/assets/feathericons/cloud-snow.svg new file mode 100644 index 00000000..e4eb8207 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/cloud-snow.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-snow"><path d="M20 17.58A5 5 0 0 0 18 8h-1.26A8 8 0 1 0 4 16.25"></path><line x1="8" y1="16" x2="8.01" y2="16"></line><line x1="8" y1="20" x2="8.01" y2="20"></line><line x1="12" y1="18" x2="12.01" y2="18"></line><line x1="12" y1="22" x2="12.01" y2="22"></line><line x1="16" y1="16" x2="16.01" y2="16"></line><line x1="16" y1="20" x2="16.01" y2="20"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/cloud.svg b/src/apps/qcam/assets/feathericons/cloud.svg new file mode 100644 index 00000000..0ee0c632 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/cloud.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/code.svg b/src/apps/qcam/assets/feathericons/code.svg new file mode 100644 index 00000000..c4954b55 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/code.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-code"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/codepen.svg b/src/apps/qcam/assets/feathericons/codepen.svg new file mode 100644 index 00000000..ab2a815a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/codepen.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-codepen"><polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"></polygon><line x1="12" y1="22" x2="12" y2="15.5"></line><polyline points="22 8.5 12 15.5 2 8.5"></polyline><polyline points="2 15.5 12 8.5 22 15.5"></polyline><line x1="12" y1="2" x2="12" y2="8.5"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/codesandbox.svg b/src/apps/qcam/assets/feathericons/codesandbox.svg new file mode 100644 index 00000000..49848f52 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/codesandbox.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-codesandbox"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="7.5 4.21 12 6.81 16.5 4.21"></polyline><polyline points="7.5 19.79 7.5 14.6 3 12"></polyline><polyline points="21 12 16.5 14.6 16.5 19.79"></polyline><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/coffee.svg b/src/apps/qcam/assets/feathericons/coffee.svg new file mode 100644 index 00000000..32905e52 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/coffee.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-coffee"><path d="M18 8h1a4 4 0 0 1 0 8h-1"></path><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path><line x1="6" y1="1" x2="6" y2="4"></line><line x1="10" y1="1" x2="10" y2="4"></line><line x1="14" y1="1" x2="14" y2="4"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/columns.svg b/src/apps/qcam/assets/feathericons/columns.svg new file mode 100644 index 00000000..d264b557 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/columns.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-columns"><path d="M12 3h7a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-7m0-18H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7m0-18v18"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/command.svg b/src/apps/qcam/assets/feathericons/command.svg new file mode 100644 index 00000000..93f554c3 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/command.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-command"><path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/compass.svg b/src/apps/qcam/assets/feathericons/compass.svg new file mode 100644 index 00000000..32962608 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/compass.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-compass"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/copy.svg b/src/apps/qcam/assets/feathericons/copy.svg new file mode 100644 index 00000000..4e0b09f1 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/copy.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-copy"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/corner-down-left.svg b/src/apps/qcam/assets/feathericons/corner-down-left.svg new file mode 100644 index 00000000..9fffb3e9 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/corner-down-left.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-down-left"><polyline points="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/corner-down-right.svg b/src/apps/qcam/assets/feathericons/corner-down-right.svg new file mode 100644 index 00000000..b27d408d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/corner-down-right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-down-right"><polyline points="15 10 20 15 15 20"></polyline><path d="M4 4v7a4 4 0 0 0 4 4h12"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/corner-left-down.svg b/src/apps/qcam/assets/feathericons/corner-left-down.svg new file mode 100644 index 00000000..24b8375c --- /dev/null +++ b/src/apps/qcam/assets/feathericons/corner-left-down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-left-down"><polyline points="14 15 9 20 4 15"></polyline><path d="M20 4h-7a4 4 0 0 0-4 4v12"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/corner-left-up.svg b/src/apps/qcam/assets/feathericons/corner-left-up.svg new file mode 100644 index 00000000..e54527cd --- /dev/null +++ b/src/apps/qcam/assets/feathericons/corner-left-up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-left-up"><polyline points="14 9 9 4 4 9"></polyline><path d="M20 20h-7a4 4 0 0 1-4-4V4"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/corner-right-down.svg b/src/apps/qcam/assets/feathericons/corner-right-down.svg new file mode 100644 index 00000000..a49e6d6c --- /dev/null +++ b/src/apps/qcam/assets/feathericons/corner-right-down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-right-down"><polyline points="10 15 15 20 20 15"></polyline><path d="M4 4h7a4 4 0 0 1 4 4v12"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/corner-right-up.svg b/src/apps/qcam/assets/feathericons/corner-right-up.svg new file mode 100644 index 00000000..a5c5dce5 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/corner-right-up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-right-up"><polyline points="10 9 15 4 20 9"></polyline><path d="M4 20h7a4 4 0 0 0 4-4V4"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/corner-up-left.svg b/src/apps/qcam/assets/feathericons/corner-up-left.svg new file mode 100644 index 00000000..0a1ffd61 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/corner-up-left.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-up-left"><polyline points="9 14 4 9 9 4"></polyline><path d="M20 20v-7a4 4 0 0 0-4-4H4"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/corner-up-right.svg b/src/apps/qcam/assets/feathericons/corner-up-right.svg new file mode 100644 index 00000000..0b8f961b --- /dev/null +++ b/src/apps/qcam/assets/feathericons/corner-up-right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-up-right"><polyline points="15 14 20 9 15 4"></polyline><path d="M4 20v-7a4 4 0 0 1 4-4h12"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/cpu.svg b/src/apps/qcam/assets/feathericons/cpu.svg new file mode 100644 index 00000000..2ed16ef7 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/cpu.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/credit-card.svg b/src/apps/qcam/assets/feathericons/credit-card.svg new file mode 100644 index 00000000..1b7fd029 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/credit-card.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-credit-card"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect><line x1="1" y1="10" x2="23" y2="10"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/crop.svg b/src/apps/qcam/assets/feathericons/crop.svg new file mode 100644 index 00000000..ffbfd045 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/crop.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-crop"><path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"></path><path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/crosshair.svg b/src/apps/qcam/assets/feathericons/crosshair.svg new file mode 100644 index 00000000..ba394015 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/crosshair.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-crosshair"><circle cx="12" cy="12" r="10"></circle><line x1="22" y1="12" x2="18" y2="12"></line><line x1="6" y1="12" x2="2" y2="12"></line><line x1="12" y1="6" x2="12" y2="2"></line><line x1="12" y1="22" x2="12" y2="18"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/database.svg b/src/apps/qcam/assets/feathericons/database.svg new file mode 100644 index 00000000..c296fbcf --- /dev/null +++ b/src/apps/qcam/assets/feathericons/database.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-database"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/delete.svg b/src/apps/qcam/assets/feathericons/delete.svg new file mode 100644 index 00000000..8c6074b9 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/delete.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-delete"><path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="18" y1="9" x2="12" y2="15"></line><line x1="12" y1="9" x2="18" y2="15"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/disc.svg b/src/apps/qcam/assets/feathericons/disc.svg new file mode 100644 index 00000000..2595b444 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/disc.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-disc"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="3"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/dollar-sign.svg b/src/apps/qcam/assets/feathericons/dollar-sign.svg new file mode 100644 index 00000000..1a124d26 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/dollar-sign.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-dollar-sign"><line x1="12" y1="1" x2="12" y2="23"></line><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/download-cloud.svg b/src/apps/qcam/assets/feathericons/download-cloud.svg new file mode 100644 index 00000000..f3126fc3 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/download-cloud.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download-cloud"><polyline points="8 17 12 21 16 17"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/download.svg b/src/apps/qcam/assets/feathericons/download.svg new file mode 100644 index 00000000..76767a92 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/download.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/droplet.svg b/src/apps/qcam/assets/feathericons/droplet.svg new file mode 100644 index 00000000..ca093014 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/droplet.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-droplet"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/edit-2.svg b/src/apps/qcam/assets/feathericons/edit-2.svg new file mode 100644 index 00000000..06830c9d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/edit-2.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit-2"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/edit-3.svg b/src/apps/qcam/assets/feathericons/edit-3.svg new file mode 100644 index 00000000..d728efcc --- /dev/null +++ b/src/apps/qcam/assets/feathericons/edit-3.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit-3"><path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/edit.svg b/src/apps/qcam/assets/feathericons/edit.svg new file mode 100644 index 00000000..ec7b4ca2 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/edit.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/external-link.svg b/src/apps/qcam/assets/feathericons/external-link.svg new file mode 100644 index 00000000..6236df3e --- /dev/null +++ b/src/apps/qcam/assets/feathericons/external-link.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/eye-off.svg b/src/apps/qcam/assets/feathericons/eye-off.svg new file mode 100644 index 00000000..77c54cb4 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/eye-off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye-off"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/eye.svg b/src/apps/qcam/assets/feathericons/eye.svg new file mode 100644 index 00000000..9cde2437 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/eye.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/facebook.svg b/src/apps/qcam/assets/feathericons/facebook.svg new file mode 100644 index 00000000..2570f56a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/facebook.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-facebook"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/fast-forward.svg b/src/apps/qcam/assets/feathericons/fast-forward.svg new file mode 100644 index 00000000..fa39877a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/fast-forward.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-fast-forward"><polygon points="13 19 22 12 13 5 13 19"></polygon><polygon points="2 19 11 12 2 5 2 19"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/feather.svg b/src/apps/qcam/assets/feathericons/feather.svg new file mode 100644 index 00000000..ac3b868d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/feather.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-feather"><path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"></path><line x1="16" y1="8" x2="2" y2="22"></line><line x1="17.5" y1="15" x2="9" y2="15"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/feathericons.qrc b/src/apps/qcam/assets/feathericons/feathericons.qrc new file mode 100644 index 00000000..c5302040 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/feathericons.qrc @@ -0,0 +1,11 @@ +<!-- SPDX-License-Identifier: GPL-2.0-or-later --> +<!DOCTYPE RCC><RCC version="1.0"> +<qresource> + <file>aperture.svg</file> + <file>camera-off.svg</file> + <file>play-circle.svg</file> + <file>save.svg</file> + <file>stop-circle.svg</file> + <file>x-circle.svg</file> +</qresource> +</RCC> diff --git a/src/apps/qcam/assets/feathericons/figma.svg b/src/apps/qcam/assets/feathericons/figma.svg new file mode 100644 index 00000000..66fd2178 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/figma.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-figma"><path d="M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z"></path><path d="M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z"></path><path d="M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"></path><path d="M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z"></path><path d="M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/file-minus.svg b/src/apps/qcam/assets/feathericons/file-minus.svg new file mode 100644 index 00000000..345756ef --- /dev/null +++ b/src/apps/qcam/assets/feathericons/file-minus.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-minus"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="9" y1="15" x2="15" y2="15"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/file-plus.svg b/src/apps/qcam/assets/feathericons/file-plus.svg new file mode 100644 index 00000000..eed12004 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/file-plus.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-plus"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="12" y1="18" x2="12" y2="12"></line><line x1="9" y1="15" x2="15" y2="15"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/file-text.svg b/src/apps/qcam/assets/feathericons/file-text.svg new file mode 100644 index 00000000..4197ddd4 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/file-text.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/file.svg b/src/apps/qcam/assets/feathericons/file.svg new file mode 100644 index 00000000..378519ab --- /dev/null +++ b/src/apps/qcam/assets/feathericons/file.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/film.svg b/src/apps/qcam/assets/feathericons/film.svg new file mode 100644 index 00000000..ac46360d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/film.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-film"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/filter.svg b/src/apps/qcam/assets/feathericons/filter.svg new file mode 100644 index 00000000..38a47e04 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/filter.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-filter"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/flag.svg b/src/apps/qcam/assets/feathericons/flag.svg new file mode 100644 index 00000000..037737cb --- /dev/null +++ b/src/apps/qcam/assets/feathericons/flag.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-flag"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/folder-minus.svg b/src/apps/qcam/assets/feathericons/folder-minus.svg new file mode 100644 index 00000000..d5b7af65 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/folder-minus.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder-minus"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><line x1="9" y1="14" x2="15" y2="14"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/folder-plus.svg b/src/apps/qcam/assets/feathericons/folder-plus.svg new file mode 100644 index 00000000..898f2fc9 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/folder-plus.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder-plus"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><line x1="12" y1="11" x2="12" y2="17"></line><line x1="9" y1="14" x2="15" y2="14"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/folder.svg b/src/apps/qcam/assets/feathericons/folder.svg new file mode 100644 index 00000000..134458b9 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/folder.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/framer.svg b/src/apps/qcam/assets/feathericons/framer.svg new file mode 100644 index 00000000..3e663478 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/framer.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-framer"><path d="M5 16V9h14V2H5l14 14h-7m-7 0l7 7v-7m-7 0h7"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/frown.svg b/src/apps/qcam/assets/feathericons/frown.svg new file mode 100644 index 00000000..f3122547 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/frown.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-frown"><circle cx="12" cy="12" r="10"></circle><path d="M16 16s-1.5-2-4-2-4 2-4 2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/gift.svg b/src/apps/qcam/assets/feathericons/gift.svg new file mode 100644 index 00000000..d2c14bd6 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/gift.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-gift"><polyline points="20 12 20 22 4 22 4 12"></polyline><rect x="2" y="7" width="20" height="5"></rect><line x1="12" y1="22" x2="12" y2="7"></line><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/git-branch.svg b/src/apps/qcam/assets/feathericons/git-branch.svg new file mode 100644 index 00000000..44003726 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/git-branch.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-branch"><line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/git-commit.svg b/src/apps/qcam/assets/feathericons/git-commit.svg new file mode 100644 index 00000000..e959d725 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/git-commit.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-commit"><circle cx="12" cy="12" r="4"></circle><line x1="1.05" y1="12" x2="7" y2="12"></line><line x1="17.01" y1="12" x2="22.96" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/git-merge.svg b/src/apps/qcam/assets/feathericons/git-merge.svg new file mode 100644 index 00000000..c65fffdd --- /dev/null +++ b/src/apps/qcam/assets/feathericons/git-merge.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-merge"><circle cx="18" cy="18" r="3"></circle><circle cx="6" cy="6" r="3"></circle><path d="M6 21V9a9 9 0 0 0 9 9"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/git-pull-request.svg b/src/apps/qcam/assets/feathericons/git-pull-request.svg new file mode 100644 index 00000000..fc80bdfd --- /dev/null +++ b/src/apps/qcam/assets/feathericons/git-pull-request.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-pull-request"><circle cx="18" cy="18" r="3"></circle><circle cx="6" cy="6" r="3"></circle><path d="M13 6h3a2 2 0 0 1 2 2v7"></path><line x1="6" y1="9" x2="6" y2="21"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/github.svg b/src/apps/qcam/assets/feathericons/github.svg new file mode 100644 index 00000000..ff0af481 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/github.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/gitlab.svg b/src/apps/qcam/assets/feathericons/gitlab.svg new file mode 100644 index 00000000..85d54a1e --- /dev/null +++ b/src/apps/qcam/assets/feathericons/gitlab.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-gitlab"><path d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/globe.svg b/src/apps/qcam/assets/feathericons/globe.svg new file mode 100644 index 00000000..0a0586d3 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/globe.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/grid.svg b/src/apps/qcam/assets/feathericons/grid.svg new file mode 100644 index 00000000..8ef2e9d8 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/grid.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-grid"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/hard-drive.svg b/src/apps/qcam/assets/feathericons/hard-drive.svg new file mode 100644 index 00000000..8e90fa1b --- /dev/null +++ b/src/apps/qcam/assets/feathericons/hard-drive.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-hard-drive"><line x1="22" y1="12" x2="2" y2="12"></line><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path><line x1="6" y1="16" x2="6.01" y2="16"></line><line x1="10" y1="16" x2="10.01" y2="16"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/hash.svg b/src/apps/qcam/assets/feathericons/hash.svg new file mode 100644 index 00000000..c9c8d41f --- /dev/null +++ b/src/apps/qcam/assets/feathericons/hash.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-hash"><line x1="4" y1="9" x2="20" y2="9"></line><line x1="4" y1="15" x2="20" y2="15"></line><line x1="10" y1="3" x2="8" y2="21"></line><line x1="16" y1="3" x2="14" y2="21"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/headphones.svg b/src/apps/qcam/assets/feathericons/headphones.svg new file mode 100644 index 00000000..fd8915b4 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/headphones.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-headphones"><path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/heart.svg b/src/apps/qcam/assets/feathericons/heart.svg new file mode 100644 index 00000000..a083b7e2 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/heart.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/help-circle.svg b/src/apps/qcam/assets/feathericons/help-circle.svg new file mode 100644 index 00000000..51fddd80 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/help-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/hexagon.svg b/src/apps/qcam/assets/feathericons/hexagon.svg new file mode 100644 index 00000000..eae7f255 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/hexagon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-hexagon"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/home.svg b/src/apps/qcam/assets/feathericons/home.svg new file mode 100644 index 00000000..7bb31b23 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/home.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/image.svg b/src/apps/qcam/assets/feathericons/image.svg new file mode 100644 index 00000000..a7d84b98 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/image.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-image"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/inbox.svg b/src/apps/qcam/assets/feathericons/inbox.svg new file mode 100644 index 00000000..03a13b4e --- /dev/null +++ b/src/apps/qcam/assets/feathericons/inbox.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-inbox"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/info.svg b/src/apps/qcam/assets/feathericons/info.svg new file mode 100644 index 00000000..a09fa5f1 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/info.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/instagram.svg b/src/apps/qcam/assets/feathericons/instagram.svg new file mode 100644 index 00000000..9fdb8e35 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/instagram.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-instagram"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/italic.svg b/src/apps/qcam/assets/feathericons/italic.svg new file mode 100644 index 00000000..a123d371 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/italic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-italic"><line x1="19" y1="4" x2="10" y2="4"></line><line x1="14" y1="20" x2="5" y2="20"></line><line x1="15" y1="4" x2="9" y2="20"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/key.svg b/src/apps/qcam/assets/feathericons/key.svg new file mode 100644 index 00000000..e778e74e --- /dev/null +++ b/src/apps/qcam/assets/feathericons/key.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-key"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/layers.svg b/src/apps/qcam/assets/feathericons/layers.svg new file mode 100644 index 00000000..ea788c22 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/layers.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/layout.svg b/src/apps/qcam/assets/feathericons/layout.svg new file mode 100644 index 00000000..28743d92 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/layout.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layout"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/life-buoy.svg b/src/apps/qcam/assets/feathericons/life-buoy.svg new file mode 100644 index 00000000..54c2bd7d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/life-buoy.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-life-buoy"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="4.93" y1="4.93" x2="9.17" y2="9.17"></line><line x1="14.83" y1="14.83" x2="19.07" y2="19.07"></line><line x1="14.83" y1="9.17" x2="19.07" y2="4.93"></line><line x1="14.83" y1="9.17" x2="18.36" y2="5.64"></line><line x1="4.93" y1="19.07" x2="9.17" y2="14.83"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/link-2.svg b/src/apps/qcam/assets/feathericons/link-2.svg new file mode 100644 index 00000000..8cc7f6dd --- /dev/null +++ b/src/apps/qcam/assets/feathericons/link-2.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link-2"><path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path><line x1="8" y1="12" x2="16" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/link.svg b/src/apps/qcam/assets/feathericons/link.svg new file mode 100644 index 00000000..c89dd41c --- /dev/null +++ b/src/apps/qcam/assets/feathericons/link.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/linkedin.svg b/src/apps/qcam/assets/feathericons/linkedin.svg new file mode 100644 index 00000000..39531094 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/linkedin.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-linkedin"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/list.svg b/src/apps/qcam/assets/feathericons/list.svg new file mode 100644 index 00000000..5ce38eaa --- /dev/null +++ b/src/apps/qcam/assets/feathericons/list.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-list"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/loader.svg b/src/apps/qcam/assets/feathericons/loader.svg new file mode 100644 index 00000000..e1a70c12 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/loader.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-loader"><line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/lock.svg b/src/apps/qcam/assets/feathericons/lock.svg new file mode 100644 index 00000000..de09d9db --- /dev/null +++ b/src/apps/qcam/assets/feathericons/lock.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-lock"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/log-in.svg b/src/apps/qcam/assets/feathericons/log-in.svg new file mode 100644 index 00000000..ba0da59a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/log-in.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-log-in"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" y1="12" x2="3" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/log-out.svg b/src/apps/qcam/assets/feathericons/log-out.svg new file mode 100644 index 00000000..c9002c90 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/log-out.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-log-out"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/mail.svg b/src/apps/qcam/assets/feathericons/mail.svg new file mode 100644 index 00000000..2af169e8 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/mail.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mail"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/map-pin.svg b/src/apps/qcam/assets/feathericons/map-pin.svg new file mode 100644 index 00000000..d5548e92 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/map-pin.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-map-pin"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/map.svg b/src/apps/qcam/assets/feathericons/map.svg new file mode 100644 index 00000000..ecebd7bf --- /dev/null +++ b/src/apps/qcam/assets/feathericons/map.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-map"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/maximize-2.svg b/src/apps/qcam/assets/feathericons/maximize-2.svg new file mode 100644 index 00000000..e41fc0b7 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/maximize-2.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" y1="3" x2="14" y2="10"></line><line x1="3" y1="21" x2="10" y2="14"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/maximize.svg b/src/apps/qcam/assets/feathericons/maximize.svg new file mode 100644 index 00000000..fc305189 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/maximize.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-maximize"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/meh.svg b/src/apps/qcam/assets/feathericons/meh.svg new file mode 100644 index 00000000..6f57fff2 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/meh.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-meh"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="15" x2="16" y2="15"></line><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/menu.svg b/src/apps/qcam/assets/feathericons/menu.svg new file mode 100644 index 00000000..e8a84a95 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/menu.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/message-circle.svg b/src/apps/qcam/assets/feathericons/message-circle.svg new file mode 100644 index 00000000..4b21b32b --- /dev/null +++ b/src/apps/qcam/assets/feathericons/message-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-circle"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/message-square.svg b/src/apps/qcam/assets/feathericons/message-square.svg new file mode 100644 index 00000000..6a2e4e59 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/message-square.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-square"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/mic-off.svg b/src/apps/qcam/assets/feathericons/mic-off.svg new file mode 100644 index 00000000..0786219c --- /dev/null +++ b/src/apps/qcam/assets/feathericons/mic-off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mic-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path><path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/mic.svg b/src/apps/qcam/assets/feathericons/mic.svg new file mode 100644 index 00000000..dc5f780c --- /dev/null +++ b/src/apps/qcam/assets/feathericons/mic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mic"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/minimize-2.svg b/src/apps/qcam/assets/feathericons/minimize-2.svg new file mode 100644 index 00000000..a720fa6c --- /dev/null +++ b/src/apps/qcam/assets/feathericons/minimize-2.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minimize-2"><polyline points="4 14 10 14 10 20"></polyline><polyline points="20 10 14 10 14 4"></polyline><line x1="14" y1="10" x2="21" y2="3"></line><line x1="3" y1="21" x2="10" y2="14"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/minimize.svg b/src/apps/qcam/assets/feathericons/minimize.svg new file mode 100644 index 00000000..46d61196 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/minimize.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minimize"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/minus-circle.svg b/src/apps/qcam/assets/feathericons/minus-circle.svg new file mode 100644 index 00000000..80c0de1e --- /dev/null +++ b/src/apps/qcam/assets/feathericons/minus-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="12" x2="16" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/minus-square.svg b/src/apps/qcam/assets/feathericons/minus-square.svg new file mode 100644 index 00000000..4862832a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/minus-square.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minus-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="8" y1="12" x2="16" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/minus.svg b/src/apps/qcam/assets/feathericons/minus.svg new file mode 100644 index 00000000..93cc7340 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/minus.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minus"><line x1="5" y1="12" x2="19" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/monitor.svg b/src/apps/qcam/assets/feathericons/monitor.svg new file mode 100644 index 00000000..6c3556db --- /dev/null +++ b/src/apps/qcam/assets/feathericons/monitor.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-monitor"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/moon.svg b/src/apps/qcam/assets/feathericons/moon.svg new file mode 100644 index 00000000..dbf7c6cf --- /dev/null +++ b/src/apps/qcam/assets/feathericons/moon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-moon"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/more-horizontal.svg b/src/apps/qcam/assets/feathericons/more-horizontal.svg new file mode 100644 index 00000000..dc6a8556 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/more-horizontal.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-horizontal"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/more-vertical.svg b/src/apps/qcam/assets/feathericons/more-vertical.svg new file mode 100644 index 00000000..cba6958f --- /dev/null +++ b/src/apps/qcam/assets/feathericons/more-vertical.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-vertical"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/mouse-pointer.svg b/src/apps/qcam/assets/feathericons/mouse-pointer.svg new file mode 100644 index 00000000..f5af5591 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/mouse-pointer.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mouse-pointer"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"></path><path d="M13 13l6 6"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/move.svg b/src/apps/qcam/assets/feathericons/move.svg new file mode 100644 index 00000000..4e251b56 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/move.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-move"><polyline points="5 9 2 12 5 15"></polyline><polyline points="9 5 12 2 15 5"></polyline><polyline points="15 19 12 22 9 19"></polyline><polyline points="19 9 22 12 19 15"></polyline><line x1="2" y1="12" x2="22" y2="12"></line><line x1="12" y1="2" x2="12" y2="22"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/music.svg b/src/apps/qcam/assets/feathericons/music.svg new file mode 100644 index 00000000..7bee2f7e --- /dev/null +++ b/src/apps/qcam/assets/feathericons/music.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-music"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/navigation-2.svg b/src/apps/qcam/assets/feathericons/navigation-2.svg new file mode 100644 index 00000000..ae31db96 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/navigation-2.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-navigation-2"><polygon points="12 2 19 21 12 17 5 21 12 2"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/navigation.svg b/src/apps/qcam/assets/feathericons/navigation.svg new file mode 100644 index 00000000..f600a414 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/navigation.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-navigation"><polygon points="3 11 22 2 13 21 11 13 3 11"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/octagon.svg b/src/apps/qcam/assets/feathericons/octagon.svg new file mode 100644 index 00000000..124c5483 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/octagon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-octagon"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/package.svg b/src/apps/qcam/assets/feathericons/package.svg new file mode 100644 index 00000000..f1e09eec --- /dev/null +++ b/src/apps/qcam/assets/feathericons/package.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-package"><line x1="16.5" y1="9.4" x2="7.5" y2="4.21"></line><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/paperclip.svg b/src/apps/qcam/assets/feathericons/paperclip.svg new file mode 100644 index 00000000..b1f69b7a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/paperclip.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-paperclip"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/pause-circle.svg b/src/apps/qcam/assets/feathericons/pause-circle.svg new file mode 100644 index 00000000..f6b1a8df --- /dev/null +++ b/src/apps/qcam/assets/feathericons/pause-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pause-circle"><circle cx="12" cy="12" r="10"></circle><line x1="10" y1="15" x2="10" y2="9"></line><line x1="14" y1="15" x2="14" y2="9"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/pause.svg b/src/apps/qcam/assets/feathericons/pause.svg new file mode 100644 index 00000000..4e78038d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/pause.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pause"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/pen-tool.svg b/src/apps/qcam/assets/feathericons/pen-tool.svg new file mode 100644 index 00000000..0d26fa1e --- /dev/null +++ b/src/apps/qcam/assets/feathericons/pen-tool.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pen-tool"><path d="M12 19l7-7 3 3-7 7-3-3z"></path><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path><path d="M2 2l7.586 7.586"></path><circle cx="11" cy="11" r="2"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/percent.svg b/src/apps/qcam/assets/feathericons/percent.svg new file mode 100644 index 00000000..2cb9719d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/percent.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-percent"><line x1="19" y1="5" x2="5" y2="19"></line><circle cx="6.5" cy="6.5" r="2.5"></circle><circle cx="17.5" cy="17.5" r="2.5"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/phone-call.svg b/src/apps/qcam/assets/feathericons/phone-call.svg new file mode 100644 index 00000000..8b866602 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/phone-call.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-call"><path d="M15.05 5A5 5 0 0 1 19 8.95M15.05 1A9 9 0 0 1 23 8.94m-1 7.98v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/phone-forwarded.svg b/src/apps/qcam/assets/feathericons/phone-forwarded.svg new file mode 100644 index 00000000..aa21befc --- /dev/null +++ b/src/apps/qcam/assets/feathericons/phone-forwarded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-forwarded"><polyline points="19 1 23 5 19 9"></polyline><line x1="15" y1="5" x2="23" y2="5"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/phone-incoming.svg b/src/apps/qcam/assets/feathericons/phone-incoming.svg new file mode 100644 index 00000000..b2d523a8 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/phone-incoming.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-incoming"><polyline points="16 2 16 8 22 8"></polyline><line x1="23" y1="1" x2="16" y2="8"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/phone-missed.svg b/src/apps/qcam/assets/feathericons/phone-missed.svg new file mode 100644 index 00000000..4950f09f --- /dev/null +++ b/src/apps/qcam/assets/feathericons/phone-missed.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-missed"><line x1="23" y1="1" x2="17" y2="7"></line><line x1="17" y1="1" x2="23" y2="7"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/phone-off.svg b/src/apps/qcam/assets/feathericons/phone-off.svg new file mode 100644 index 00000000..4d00fb3d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/phone-off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-off"><path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"></path><line x1="23" y1="1" x2="1" y2="23"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/phone-outgoing.svg b/src/apps/qcam/assets/feathericons/phone-outgoing.svg new file mode 100644 index 00000000..fea27a37 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/phone-outgoing.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-outgoing"><polyline points="23 7 23 1 17 1"></polyline><line x1="16" y1="8" x2="23" y2="1"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/phone.svg b/src/apps/qcam/assets/feathericons/phone.svg new file mode 100644 index 00000000..2a35154a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/phone.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/pie-chart.svg b/src/apps/qcam/assets/feathericons/pie-chart.svg new file mode 100644 index 00000000..b5bbe67c --- /dev/null +++ b/src/apps/qcam/assets/feathericons/pie-chart.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pie-chart"><path d="M21.21 15.89A10 10 0 1 1 8 2.83"></path><path d="M22 12A10 10 0 0 0 12 2v10z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/play-circle.svg b/src/apps/qcam/assets/feathericons/play-circle.svg new file mode 100644 index 00000000..8766dc7b --- /dev/null +++ b/src/apps/qcam/assets/feathericons/play-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-play-circle"><circle cx="12" cy="12" r="10"></circle><polygon points="10 8 16 12 10 16 10 8"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/play.svg b/src/apps/qcam/assets/feathericons/play.svg new file mode 100644 index 00000000..fd76e30d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/play.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-play"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/plus-circle.svg b/src/apps/qcam/assets/feathericons/plus-circle.svg new file mode 100644 index 00000000..4291ff05 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/plus-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/plus-square.svg b/src/apps/qcam/assets/feathericons/plus-square.svg new file mode 100644 index 00000000..c380e24b --- /dev/null +++ b/src/apps/qcam/assets/feathericons/plus-square.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/plus.svg b/src/apps/qcam/assets/feathericons/plus.svg new file mode 100644 index 00000000..703c5b7b --- /dev/null +++ b/src/apps/qcam/assets/feathericons/plus.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/pocket.svg b/src/apps/qcam/assets/feathericons/pocket.svg new file mode 100644 index 00000000..a3b25619 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/pocket.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pocket"><path d="M4 3h16a2 2 0 0 1 2 2v6a10 10 0 0 1-10 10A10 10 0 0 1 2 11V5a2 2 0 0 1 2-2z"></path><polyline points="8 10 12 14 16 10"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/power.svg b/src/apps/qcam/assets/feathericons/power.svg new file mode 100644 index 00000000..598308fc --- /dev/null +++ b/src/apps/qcam/assets/feathericons/power.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-power"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/printer.svg b/src/apps/qcam/assets/feathericons/printer.svg new file mode 100644 index 00000000..8a9a7ace --- /dev/null +++ b/src/apps/qcam/assets/feathericons/printer.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-printer"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/radio.svg b/src/apps/qcam/assets/feathericons/radio.svg new file mode 100644 index 00000000..5abfcd13 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/radio.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-radio"><circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/refresh-ccw.svg b/src/apps/qcam/assets/feathericons/refresh-ccw.svg new file mode 100644 index 00000000..10cff0ec --- /dev/null +++ b/src/apps/qcam/assets/feathericons/refresh-ccw.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-ccw"><polyline points="1 4 1 10 7 10"></polyline><polyline points="23 20 23 14 17 14"></polyline><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/refresh-cw.svg b/src/apps/qcam/assets/feathericons/refresh-cw.svg new file mode 100644 index 00000000..06c358dd --- /dev/null +++ b/src/apps/qcam/assets/feathericons/refresh-cw.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/repeat.svg b/src/apps/qcam/assets/feathericons/repeat.svg new file mode 100644 index 00000000..c7657b08 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/repeat.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-repeat"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/rewind.svg b/src/apps/qcam/assets/feathericons/rewind.svg new file mode 100644 index 00000000..7b0fa3d5 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/rewind.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rewind"><polygon points="11 19 2 12 11 5 11 19"></polygon><polygon points="22 19 13 12 22 5 22 19"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/rotate-ccw.svg b/src/apps/qcam/assets/feathericons/rotate-ccw.svg new file mode 100644 index 00000000..ade5dc42 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/rotate-ccw.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-ccw"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/rotate-cw.svg b/src/apps/qcam/assets/feathericons/rotate-cw.svg new file mode 100644 index 00000000..83dca351 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/rotate-cw.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-cw"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/rss.svg b/src/apps/qcam/assets/feathericons/rss.svg new file mode 100644 index 00000000..c9a13684 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/rss.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rss"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/save.svg b/src/apps/qcam/assets/feathericons/save.svg new file mode 100644 index 00000000..46c72990 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/save.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-save"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/scissors.svg b/src/apps/qcam/assets/feathericons/scissors.svg new file mode 100644 index 00000000..fd0647ff --- /dev/null +++ b/src/apps/qcam/assets/feathericons/scissors.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-scissors"><circle cx="6" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><line x1="20" y1="4" x2="8.12" y2="15.88"></line><line x1="14.47" y1="14.48" x2="20" y2="20"></line><line x1="8.12" y1="8.12" x2="12" y2="12"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/search.svg b/src/apps/qcam/assets/feathericons/search.svg new file mode 100644 index 00000000..8710306d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/search.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/send.svg b/src/apps/qcam/assets/feathericons/send.svg new file mode 100644 index 00000000..42ef2a24 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/send.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-send"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/server.svg b/src/apps/qcam/assets/feathericons/server.svg new file mode 100644 index 00000000..54ce094a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/server.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-server"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/settings.svg b/src/apps/qcam/assets/feathericons/settings.svg new file mode 100644 index 00000000..19c27265 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/settings.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-settings"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/share-2.svg b/src/apps/qcam/assets/feathericons/share-2.svg new file mode 100644 index 00000000..09b1c7bc --- /dev/null +++ b/src/apps/qcam/assets/feathericons/share-2.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-share-2"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/share.svg b/src/apps/qcam/assets/feathericons/share.svg new file mode 100644 index 00000000..df38c14d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/share.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-share"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/shield-off.svg b/src/apps/qcam/assets/feathericons/shield-off.svg new file mode 100644 index 00000000..18692ddd --- /dev/null +++ b/src/apps/qcam/assets/feathericons/shield-off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shield-off"><path d="M19.69 14a6.9 6.9 0 0 0 .31-2V5l-8-3-3.16 1.18"></path><path d="M4.73 4.73L4 5v7c0 6 8 10 8 10a20.29 20.29 0 0 0 5.62-4.38"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/shield.svg b/src/apps/qcam/assets/feathericons/shield.svg new file mode 100644 index 00000000..c7c48413 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/shield.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shield"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/shopping-bag.svg b/src/apps/qcam/assets/feathericons/shopping-bag.svg new file mode 100644 index 00000000..eaa39e81 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/shopping-bag.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-bag"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/shopping-cart.svg b/src/apps/qcam/assets/feathericons/shopping-cart.svg new file mode 100644 index 00000000..17a40bf4 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/shopping-cart.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-cart"><circle cx="9" cy="21" r="1"></circle><circle cx="20" cy="21" r="1"></circle><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/shuffle.svg b/src/apps/qcam/assets/feathericons/shuffle.svg new file mode 100644 index 00000000..8cfb5db5 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/shuffle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shuffle"><polyline points="16 3 21 3 21 8"></polyline><line x1="4" y1="20" x2="21" y2="3"></line><polyline points="21 16 21 21 16 21"></polyline><line x1="15" y1="15" x2="21" y2="21"></line><line x1="4" y1="4" x2="9" y2="9"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/sidebar.svg b/src/apps/qcam/assets/feathericons/sidebar.svg new file mode 100644 index 00000000..8ba817e6 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/sidebar.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sidebar"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="3" x2="9" y2="21"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/skip-back.svg b/src/apps/qcam/assets/feathericons/skip-back.svg new file mode 100644 index 00000000..88d024e2 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/skip-back.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-skip-back"><polygon points="19 20 9 12 19 4 19 20"></polygon><line x1="5" y1="19" x2="5" y2="5"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/skip-forward.svg b/src/apps/qcam/assets/feathericons/skip-forward.svg new file mode 100644 index 00000000..f3fdac3a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/skip-forward.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-skip-forward"><polygon points="5 4 15 12 5 20 5 4"></polygon><line x1="19" y1="5" x2="19" y2="19"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/slack.svg b/src/apps/qcam/assets/feathericons/slack.svg new file mode 100644 index 00000000..5d973466 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/slack.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-slack"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"></path><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"></path><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"></path><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"></path><path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z"></path><path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"></path><path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z"></path><path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/slash.svg b/src/apps/qcam/assets/feathericons/slash.svg new file mode 100644 index 00000000..f4131b85 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/slash.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-slash"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/sliders.svg b/src/apps/qcam/assets/feathericons/sliders.svg new file mode 100644 index 00000000..19c93852 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/sliders.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sliders"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/smartphone.svg b/src/apps/qcam/assets/feathericons/smartphone.svg new file mode 100644 index 00000000..0171a95a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/smartphone.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-smartphone"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect><line x1="12" y1="18" x2="12.01" y2="18"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/smile.svg b/src/apps/qcam/assets/feathericons/smile.svg new file mode 100644 index 00000000..24dc8a26 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/smile.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-smile"><circle cx="12" cy="12" r="10"></circle><path d="M8 14s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/speaker.svg b/src/apps/qcam/assets/feathericons/speaker.svg new file mode 100644 index 00000000..75d5ff9c --- /dev/null +++ b/src/apps/qcam/assets/feathericons/speaker.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-speaker"><rect x="4" y="2" width="16" height="20" rx="2" ry="2"></rect><circle cx="12" cy="14" r="4"></circle><line x1="12" y1="6" x2="12.01" y2="6"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/square.svg b/src/apps/qcam/assets/feathericons/square.svg new file mode 100644 index 00000000..6eabc77d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/square.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/star.svg b/src/apps/qcam/assets/feathericons/star.svg new file mode 100644 index 00000000..bcdc31aa --- /dev/null +++ b/src/apps/qcam/assets/feathericons/star.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/stop-circle.svg b/src/apps/qcam/assets/feathericons/stop-circle.svg new file mode 100644 index 00000000..c10d9d47 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/stop-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-stop-circle"><circle cx="12" cy="12" r="10"></circle><rect x="9" y="9" width="6" height="6"></rect></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/sun.svg b/src/apps/qcam/assets/feathericons/sun.svg new file mode 100644 index 00000000..7f51b94d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/sun.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sun"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/sunrise.svg b/src/apps/qcam/assets/feathericons/sunrise.svg new file mode 100644 index 00000000..eff4b1e4 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/sunrise.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sunrise"><path d="M17 18a5 5 0 0 0-10 0"></path><line x1="12" y1="2" x2="12" y2="9"></line><line x1="4.22" y1="10.22" x2="5.64" y2="11.64"></line><line x1="1" y1="18" x2="3" y2="18"></line><line x1="21" y1="18" x2="23" y2="18"></line><line x1="18.36" y1="11.64" x2="19.78" y2="10.22"></line><line x1="23" y1="22" x2="1" y2="22"></line><polyline points="8 6 12 2 16 6"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/sunset.svg b/src/apps/qcam/assets/feathericons/sunset.svg new file mode 100644 index 00000000..a5a22215 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/sunset.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sunset"><path d="M17 18a5 5 0 0 0-10 0"></path><line x1="12" y1="9" x2="12" y2="2"></line><line x1="4.22" y1="10.22" x2="5.64" y2="11.64"></line><line x1="1" y1="18" x2="3" y2="18"></line><line x1="21" y1="18" x2="23" y2="18"></line><line x1="18.36" y1="11.64" x2="19.78" y2="10.22"></line><line x1="23" y1="22" x2="1" y2="22"></line><polyline points="16 5 12 9 8 5"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/tablet.svg b/src/apps/qcam/assets/feathericons/tablet.svg new file mode 100644 index 00000000..9c80b40a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/tablet.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tablet"><rect x="4" y="2" width="16" height="20" rx="2" ry="2"></rect><line x1="12" y1="18" x2="12.01" y2="18"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/tag.svg b/src/apps/qcam/assets/feathericons/tag.svg new file mode 100644 index 00000000..7219b15f --- /dev/null +++ b/src/apps/qcam/assets/feathericons/tag.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tag"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/target.svg b/src/apps/qcam/assets/feathericons/target.svg new file mode 100644 index 00000000..be84b17c --- /dev/null +++ b/src/apps/qcam/assets/feathericons/target.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-target"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="6"></circle><circle cx="12" cy="12" r="2"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/terminal.svg b/src/apps/qcam/assets/feathericons/terminal.svg new file mode 100644 index 00000000..af459c04 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/terminal.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-terminal"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/thermometer.svg b/src/apps/qcam/assets/feathericons/thermometer.svg new file mode 100644 index 00000000..33142ccc --- /dev/null +++ b/src/apps/qcam/assets/feathericons/thermometer.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thermometer"><path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/thumbs-down.svg b/src/apps/qcam/assets/feathericons/thumbs-down.svg new file mode 100644 index 00000000..3e7bcd6d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/thumbs-down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-down"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/thumbs-up.svg b/src/apps/qcam/assets/feathericons/thumbs-up.svg new file mode 100644 index 00000000..226c44d8 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/thumbs-up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-up"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/toggle-left.svg b/src/apps/qcam/assets/feathericons/toggle-left.svg new file mode 100644 index 00000000..240be290 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/toggle-left.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-toggle-left"><rect x="1" y="5" width="22" height="14" rx="7" ry="7"></rect><circle cx="8" cy="12" r="3"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/toggle-right.svg b/src/apps/qcam/assets/feathericons/toggle-right.svg new file mode 100644 index 00000000..fc6e81c1 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/toggle-right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-toggle-right"><rect x="1" y="5" width="22" height="14" rx="7" ry="7"></rect><circle cx="16" cy="12" r="3"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/tool.svg b/src/apps/qcam/assets/feathericons/tool.svg new file mode 100644 index 00000000..f3cbf3d9 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/tool.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tool"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/trash-2.svg b/src/apps/qcam/assets/feathericons/trash-2.svg new file mode 100644 index 00000000..f24d55bf --- /dev/null +++ b/src/apps/qcam/assets/feathericons/trash-2.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trash-2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/trash.svg b/src/apps/qcam/assets/feathericons/trash.svg new file mode 100644 index 00000000..55650bd4 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/trash.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trash"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/trello.svg b/src/apps/qcam/assets/feathericons/trello.svg new file mode 100644 index 00000000..b2f599b6 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/trello.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trello"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><rect x="7" y="7" width="3" height="9"></rect><rect x="14" y="7" width="3" height="5"></rect></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/trending-down.svg b/src/apps/qcam/assets/feathericons/trending-down.svg new file mode 100644 index 00000000..a9d4cfa5 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/trending-down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trending-down"><polyline points="23 18 13.5 8.5 8.5 13.5 1 6"></polyline><polyline points="17 18 23 18 23 12"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/trending-up.svg b/src/apps/qcam/assets/feathericons/trending-up.svg new file mode 100644 index 00000000..52026a4d --- /dev/null +++ b/src/apps/qcam/assets/feathericons/trending-up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trending-up"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline><polyline points="17 6 23 6 23 12"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/triangle.svg b/src/apps/qcam/assets/feathericons/triangle.svg new file mode 100644 index 00000000..274b6528 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/triangle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/truck.svg b/src/apps/qcam/assets/feathericons/truck.svg new file mode 100644 index 00000000..33898373 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/truck.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-truck"><rect x="1" y="3" width="15" height="13"></rect><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"></polygon><circle cx="5.5" cy="18.5" r="2.5"></circle><circle cx="18.5" cy="18.5" r="2.5"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/tv.svg b/src/apps/qcam/assets/feathericons/tv.svg new file mode 100644 index 00000000..955bbfff --- /dev/null +++ b/src/apps/qcam/assets/feathericons/tv.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tv"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"></rect><polyline points="17 2 12 7 7 2"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/twitch.svg b/src/apps/qcam/assets/feathericons/twitch.svg new file mode 100644 index 00000000..17062495 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/twitch.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-twitch"><path d="M21 2H3v16h5v4l4-4h5l4-4V2zm-10 9V7m5 4V7"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/twitter.svg b/src/apps/qcam/assets/feathericons/twitter.svg new file mode 100644 index 00000000..f8886eca --- /dev/null +++ b/src/apps/qcam/assets/feathericons/twitter.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-twitter"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/type.svg b/src/apps/qcam/assets/feathericons/type.svg new file mode 100644 index 00000000..c6b2de33 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/type.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-type"><polyline points="4 7 4 4 20 4 20 7"></polyline><line x1="9" y1="20" x2="15" y2="20"></line><line x1="12" y1="4" x2="12" y2="20"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/umbrella.svg b/src/apps/qcam/assets/feathericons/umbrella.svg new file mode 100644 index 00000000..dc77c0cb --- /dev/null +++ b/src/apps/qcam/assets/feathericons/umbrella.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-umbrella"><path d="M23 12a11.05 11.05 0 0 0-22 0zm-5 7a3 3 0 0 1-6 0v-7"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/underline.svg b/src/apps/qcam/assets/feathericons/underline.svg new file mode 100644 index 00000000..044945d4 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/underline.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-underline"><path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"></path><line x1="4" y1="21" x2="20" y2="21"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/unlock.svg b/src/apps/qcam/assets/feathericons/unlock.svg new file mode 100644 index 00000000..01dc3597 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/unlock.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-unlock"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/upload-cloud.svg b/src/apps/qcam/assets/feathericons/upload-cloud.svg new file mode 100644 index 00000000..a1db297c --- /dev/null +++ b/src/apps/qcam/assets/feathericons/upload-cloud.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-upload-cloud"><polyline points="16 16 12 12 8 16"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"></path><polyline points="16 16 12 12 8 16"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/upload.svg b/src/apps/qcam/assets/feathericons/upload.svg new file mode 100644 index 00000000..91eaff75 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/upload.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-upload"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/user-check.svg b/src/apps/qcam/assets/feathericons/user-check.svg new file mode 100644 index 00000000..42f91b29 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/user-check.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user-check"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/user-minus.svg b/src/apps/qcam/assets/feathericons/user-minus.svg new file mode 100644 index 00000000..44b75f5a --- /dev/null +++ b/src/apps/qcam/assets/feathericons/user-minus.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user-minus"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="23" y1="11" x2="17" y2="11"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/user-plus.svg b/src/apps/qcam/assets/feathericons/user-plus.svg new file mode 100644 index 00000000..21460f6e --- /dev/null +++ b/src/apps/qcam/assets/feathericons/user-plus.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user-plus"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/user-x.svg b/src/apps/qcam/assets/feathericons/user-x.svg new file mode 100644 index 00000000..0c41a481 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/user-x.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user-x"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="18" y1="8" x2="23" y2="13"></line><line x1="23" y1="8" x2="18" y2="13"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/user.svg b/src/apps/qcam/assets/feathericons/user.svg new file mode 100644 index 00000000..7bb5f291 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/user.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/users.svg b/src/apps/qcam/assets/feathericons/users.svg new file mode 100644 index 00000000..aacf6b08 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/users.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/video-off.svg b/src/apps/qcam/assets/feathericons/video-off.svg new file mode 100644 index 00000000..08ec6973 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/video-off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-video-off"><path d="M16 16v1a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2m5.66 0H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/video.svg b/src/apps/qcam/assets/feathericons/video.svg new file mode 100644 index 00000000..8ff156aa --- /dev/null +++ b/src/apps/qcam/assets/feathericons/video.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-video"><polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/voicemail.svg b/src/apps/qcam/assets/feathericons/voicemail.svg new file mode 100644 index 00000000..5d78a8e7 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/voicemail.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-voicemail"><circle cx="5.5" cy="11.5" r="4.5"></circle><circle cx="18.5" cy="11.5" r="4.5"></circle><line x1="5.5" y1="16" x2="18.5" y2="16"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/volume-1.svg b/src/apps/qcam/assets/feathericons/volume-1.svg new file mode 100644 index 00000000..150e875f --- /dev/null +++ b/src/apps/qcam/assets/feathericons/volume-1.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-1"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/volume-2.svg b/src/apps/qcam/assets/feathericons/volume-2.svg new file mode 100644 index 00000000..03d521c7 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/volume-2.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/volume-x.svg b/src/apps/qcam/assets/feathericons/volume-x.svg new file mode 100644 index 00000000..be442406 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/volume-x.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-x"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><line x1="23" y1="9" x2="17" y2="15"></line><line x1="17" y1="9" x2="23" y2="15"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/volume.svg b/src/apps/qcam/assets/feathericons/volume.svg new file mode 100644 index 00000000..53bfe15e --- /dev/null +++ b/src/apps/qcam/assets/feathericons/volume.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/watch.svg b/src/apps/qcam/assets/feathericons/watch.svg new file mode 100644 index 00000000..a1099da3 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/watch.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-watch"><circle cx="12" cy="12" r="7"></circle><polyline points="12 9 12 12 13.5 13.5"></polyline><path d="M16.51 17.35l-.35 3.83a2 2 0 0 1-2 1.82H9.83a2 2 0 0 1-2-1.82l-.35-3.83m.01-10.7l.35-3.83A2 2 0 0 1 9.83 1h4.35a2 2 0 0 1 2 1.82l.35 3.83"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/wifi-off.svg b/src/apps/qcam/assets/feathericons/wifi-off.svg new file mode 100644 index 00000000..35eae43b --- /dev/null +++ b/src/apps/qcam/assets/feathericons/wifi-off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-wifi-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path><path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/wifi.svg b/src/apps/qcam/assets/feathericons/wifi.svg new file mode 100644 index 00000000..748c285e --- /dev/null +++ b/src/apps/qcam/assets/feathericons/wifi.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-wifi"><path d="M5 12.55a11 11 0 0 1 14.08 0"></path><path d="M1.42 9a16 16 0 0 1 21.16 0"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/wind.svg b/src/apps/qcam/assets/feathericons/wind.svg new file mode 100644 index 00000000..82b36468 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/wind.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-wind"><path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2"></path></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/x-circle.svg b/src/apps/qcam/assets/feathericons/x-circle.svg new file mode 100644 index 00000000..94aad5e5 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/x-circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-circle"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/x-octagon.svg b/src/apps/qcam/assets/feathericons/x-octagon.svg new file mode 100644 index 00000000..85431985 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/x-octagon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-octagon"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/x-square.svg b/src/apps/qcam/assets/feathericons/x-square.svg new file mode 100644 index 00000000..7677c387 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/x-square.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/x.svg b/src/apps/qcam/assets/feathericons/x.svg new file mode 100644 index 00000000..7d5875ca --- /dev/null +++ b/src/apps/qcam/assets/feathericons/x.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/youtube.svg b/src/apps/qcam/assets/feathericons/youtube.svg new file mode 100644 index 00000000..c4824385 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/youtube.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-youtube"><path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z"></path><polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/zap-off.svg b/src/apps/qcam/assets/feathericons/zap-off.svg new file mode 100644 index 00000000..c636f8bb --- /dev/null +++ b/src/apps/qcam/assets/feathericons/zap-off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap-off"><polyline points="12.41 6.75 13 2 10.57 4.92"></polyline><polyline points="18.57 12.91 21 10 15.66 10"></polyline><polyline points="8 8 3 14 12 14 11 22 16 16"></polyline><line x1="1" y1="1" x2="23" y2="23"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/zap.svg b/src/apps/qcam/assets/feathericons/zap.svg new file mode 100644 index 00000000..8fdafa93 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/zap.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/zoom-in.svg b/src/apps/qcam/assets/feathericons/zoom-in.svg new file mode 100644 index 00000000..da4572d2 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/zoom-in.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zoom-in"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/feathericons/zoom-out.svg b/src/apps/qcam/assets/feathericons/zoom-out.svg new file mode 100644 index 00000000..fd678d72 --- /dev/null +++ b/src/apps/qcam/assets/feathericons/zoom-out.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zoom-out"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
\ No newline at end of file diff --git a/src/apps/qcam/assets/shader/RGB.frag b/src/apps/qcam/assets/shader/RGB.frag new file mode 100644 index 00000000..4c374ac9 --- /dev/null +++ b/src/apps/qcam/assets/shader/RGB.frag @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2020, Laurent Pinchart + * + * RGB.frag - Fragment shader code for RGB formats + */ + +#ifdef GL_ES +precision mediump float; +#endif + +varying vec2 textureOut; +uniform sampler2D tex_y; + +void main(void) +{ + vec3 rgb; + + rgb = texture2D(tex_y, textureOut).RGB_PATTERN; + + gl_FragColor = vec4(rgb, 1.0); +} diff --git a/src/apps/qcam/assets/shader/YUV_2_planes.frag b/src/apps/qcam/assets/shader/YUV_2_planes.frag new file mode 100644 index 00000000..1d5d1206 --- /dev/null +++ b/src/apps/qcam/assets/shader/YUV_2_planes.frag @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2020, Linaro + * + * YUV_2_planes.frag - Fragment shader code for NV12, NV16 and NV24 formats + */ + +#ifdef GL_ES +precision mediump float; +#endif + +varying vec2 textureOut; +uniform sampler2D tex_y; +uniform sampler2D tex_u; + +const mat3 yuv2rgb_matrix = mat3( + YUV2RGB_MATRIX +); + +const vec3 yuv2rgb_offset = vec3( + YUV2RGB_Y_OFFSET / 255.0, 128.0 / 255.0, 128.0 / 255.0 +); + +void main(void) +{ + vec3 yuv; + + yuv.x = texture2D(tex_y, textureOut).r; +#if defined(YUV_PATTERN_UV) + yuv.y = texture2D(tex_u, textureOut).r; + yuv.z = texture2D(tex_u, textureOut).a; +#elif defined(YUV_PATTERN_VU) + yuv.y = texture2D(tex_u, textureOut).a; + yuv.z = texture2D(tex_u, textureOut).r; +#else +#error Invalid pattern +#endif + + vec3 rgb = yuv2rgb_matrix * (yuv - yuv2rgb_offset); + + gl_FragColor = vec4(rgb, 1.0); +} diff --git a/src/apps/qcam/assets/shader/YUV_3_planes.frag b/src/apps/qcam/assets/shader/YUV_3_planes.frag new file mode 100644 index 00000000..8f788e90 --- /dev/null +++ b/src/apps/qcam/assets/shader/YUV_3_planes.frag @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2020, Linaro + * + * YUV_3_planes_UV.frag - Fragment shader code for YUV420 format + */ + +#ifdef GL_ES +precision mediump float; +#endif + +varying vec2 textureOut; +uniform sampler2D tex_y; +uniform sampler2D tex_u; +uniform sampler2D tex_v; + +const mat3 yuv2rgb_matrix = mat3( + YUV2RGB_MATRIX +); + +const vec3 yuv2rgb_offset = vec3( + YUV2RGB_Y_OFFSET / 255.0, 128.0 / 255.0, 128.0 / 255.0 +); + +void main(void) +{ + vec3 yuv; + + yuv.x = texture2D(tex_y, textureOut).r; + yuv.y = texture2D(tex_u, textureOut).r; + yuv.z = texture2D(tex_v, textureOut).r; + + vec3 rgb = yuv2rgb_matrix * (yuv - yuv2rgb_offset); + + gl_FragColor = vec4(rgb, 1.0); +} diff --git a/src/apps/qcam/assets/shader/YUV_packed.frag b/src/apps/qcam/assets/shader/YUV_packed.frag new file mode 100644 index 00000000..b9ef9d41 --- /dev/null +++ b/src/apps/qcam/assets/shader/YUV_packed.frag @@ -0,0 +1,83 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2020, Laurent Pinchart <laurent.pinchart@ideasonboard.com> + * + * YUV_packed.frag - Fragment shader code for YUYV packed formats + */ + +#ifdef GL_ES +precision mediump float; +#endif + +varying vec2 textureOut; + +uniform sampler2D tex_y; +uniform vec2 tex_step; + +const mat3 yuv2rgb_matrix = mat3( + YUV2RGB_MATRIX +); + +const vec3 yuv2rgb_offset = vec3( + YUV2RGB_Y_OFFSET / 255.0, 128.0 / 255.0, 128.0 / 255.0 +); + +void main(void) +{ + /* + * The sampler won't interpolate the texture correctly along the X axis, + * as each RGBA pixel effectively stores two pixels. We thus need to + * interpolate manually. + * + * In integer texture coordinates, the Y values are layed out in the + * texture memory as follows: + * + * ...| Y U Y V | Y U Y V | Y U Y V |... + * ...| R G B A | R G B A | R G B A |... + * ^ ^ ^ ^ ^ ^ + * | | | | | | + * n-1 n-0.5 n n+0.5 n+1 n+1.5 + * + * For a texture location x in the interval [n, n+1[, sample the left + * and right pixels at n and n+1, and interpolate them with + * + * left.r * (1 - a) + left.b * a if fract(x) < 0.5 + * left.b * (1 - a) + right.r * a if fract(x) >= 0.5 + * + * with a = fract(x * 2) which can also be written + * + * a = fract(x) * 2 if fract(x) < 0.5 + * a = fract(x) * 2 - 1 if fract(x) >= 0.5 + */ + vec2 pos = textureOut; + float f_x = fract(pos.x / tex_step.x); + + vec4 left = texture2D(tex_y, vec2(pos.x - f_x * tex_step.x, pos.y)); + vec4 right = texture2D(tex_y, vec2(pos.x + (1.0 - f_x) * tex_step.x , pos.y)); + +#if defined(YUV_PATTERN_UYVY) + float y_left = mix(left.g, left.a, f_x * 2.0); + float y_right = mix(left.a, right.g, f_x * 2.0 - 1.0); + vec2 uv = mix(left.rb, right.rb, f_x); +#elif defined(YUV_PATTERN_VYUY) + float y_left = mix(left.g, left.a, f_x * 2.0); + float y_right = mix(left.a, right.g, f_x * 2.0 - 1.0); + vec2 uv = mix(left.br, right.br, f_x); +#elif defined(YUV_PATTERN_YUYV) + float y_left = mix(left.r, left.b, f_x * 2.0); + float y_right = mix(left.b, right.r, f_x * 2.0 - 1.0); + vec2 uv = mix(left.ga, right.ga, f_x); +#elif defined(YUV_PATTERN_YVYU) + float y_left = mix(left.r, left.b, f_x * 2.0); + float y_right = mix(left.b, right.r, f_x * 2.0 - 1.0); + vec2 uv = mix(left.ag, right.ag, f_x); +#else +#error Invalid pattern +#endif + + float y = mix(y_left, y_right, step(0.5, f_x)); + + vec3 rgb = yuv2rgb_matrix * (vec3(y, uv) - yuv2rgb_offset); + + gl_FragColor = vec4(rgb, 1.0); +} diff --git a/src/apps/qcam/assets/shader/bayer_1x_packed.frag b/src/apps/qcam/assets/shader/bayer_1x_packed.frag new file mode 100644 index 00000000..f53f5575 --- /dev/null +++ b/src/apps/qcam/assets/shader/bayer_1x_packed.frag @@ -0,0 +1,216 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * Based on the code from http://jgt.akpeters.com/papers/McGuire08/ + * + * Efficient, High-Quality Bayer Demosaic Filtering on GPUs + * + * Morgan McGuire + * + * This paper appears in issue Volume 13, Number 4. + * --------------------------------------------------------- + * Copyright (c) 2008, Morgan McGuire. All rights reserved. + * + * + * Modified by Linaro Ltd for 10/12-bit packed vs 8-bit raw Bayer format, + * and for simpler demosaic algorithm. + * Copyright (C) 2020, Linaro + * + * bayer_1x_packed.frag - Fragment shader code for raw Bayer 10-bit and 12-bit + * packed formats + */ + +#ifdef GL_ES +precision mediump float; +#endif + +/* + * These constants are used to select the bytes containing the HS part of + * the pixel value: + * BPP - bytes per pixel, + * THRESHOLD_L = fract(BPP) * 0.5 + 0.02 + * THRESHOLD_H = 1.0 - fract(BPP) * 1.5 + 0.02 + * Let X is the x coordinate in the texture measured in bytes (so that the + * range is from 0 to (stride_-1)) aligned on the nearest pixel. + * E.g. for RAW10P: + * -------------+-------------------+-------------------+-- + * pixel No | 0 1 2 3 | 4 5 6 7 | ... + * -------------+-------------------+-------------------+-- + * byte offset | 0 1 2 3 4 | 5 6 7 8 9 | ... + * -------------+-------------------+-------------------+-- + * X | 0.0 1.25 2.5 3.75 | 5.0 6.25 7.5 8.75 | ... + * -------------+-------------------+-------------------+-- + * If fract(X) < THRESHOLD_L then the previous byte contains the LS + * bits of the pixel values and needs to be skipped. + * If fract(X) > THRESHOLD_H then the next byte contains the LS bits + * of the pixel values and needs to be skipped. + */ +#if defined(RAW10P) +#define BPP 1.25 +#define THRESHOLD_L 0.14 +#define THRESHOLD_H 0.64 +#elif defined(RAW12P) +#define BPP 1.5 +#define THRESHOLD_L 0.27 +#define THRESHOLD_H 0.27 +#else +#error Invalid raw format +#endif + + +varying vec2 textureOut; + +/* the texture size in pixels */ +uniform vec2 tex_size; +uniform vec2 tex_step; +uniform vec2 tex_bayer_first_red; + +uniform sampler2D tex_y; + +void main(void) +{ + vec3 rgb; + + /* + * center_bytes holds the coordinates of the MS byte of the pixel + * being sampled on the [0, stride-1/height-1] range. + * center_pixel holds the coordinates of the pixel being sampled + * on the [0, width/height-1] range. + */ + vec2 center_bytes; + vec2 center_pixel; + + /* + * x- and y-positions of the adjacent pixels on the [0, 1] range. + */ + vec2 xcoords; + vec2 ycoords; + + /* + * The coordinates passed to the shader in textureOut may point + * to a place in between the pixels if the texture format doesn't + * match the image format. In particular, MIPI packed raw Bayer + * formats don't have a matching texture format. + * In this case align the coordinates to the left nearest pixel + * by hand. + */ + center_pixel = floor(textureOut * tex_size); + center_bytes.y = center_pixel.y; + + /* + * Add a small number (a few mantissa's LSBs) to avoid float + * representation issues. Maybe paranoic. + */ + center_bytes.x = BPP * center_pixel.x + 0.02; + + float fract_x = fract(center_bytes.x); + + /* + * The below floor() call ensures that center_bytes.x points + * at one of the bytes representing the 8 higher bits of + * the pixel value, not at the byte containing the LS bits + * of the group of the pixels. + */ + center_bytes.x = floor(center_bytes.x); + center_bytes *= tex_step; + + xcoords = center_bytes.x + vec2(-tex_step.x, tex_step.x); + ycoords = center_bytes.y + vec2(-tex_step.y, tex_step.y); + + /* + * If xcoords[0] points at the byte containing the LS bits + * of the previous group of the pixels, move xcoords[0] one + * byte back. + */ + xcoords[0] += (fract_x < THRESHOLD_L) ? -tex_step.x : 0.0; + + /* + * If xcoords[1] points at the byte containing the LS bits + * of the current group of the pixels, move xcoords[1] one + * byte forward. + */ + xcoords[1] += (fract_x > THRESHOLD_H) ? tex_step.x : 0.0; + + vec2 alternate = mod(center_pixel.xy + tex_bayer_first_red, 2.0); + bool even_col = alternate.x < 1.0; + bool even_row = alternate.y < 1.0; + + /* + * We need to sample the central pixel and the ones with offset + * of -1 to +1 pixel in both X and Y directions. Let's name these + * pixels as below, where C is the central pixel: + * + * +----+----+----+----+ + * | \ x| | | | + * |y \ | -1 | 0 | +1 | + * +----+----+----+----+ + * | +1 | D2 | A1 | D3 | + * +----+----+----+----+ + * | 0 | B0 | C | B1 | + * +----+----+----+----+ + * | -1 | D0 | A0 | D1 | + * +----+----+----+----+ + * + * In the below equations (0,-1).r means "r component of the texel + * shifted by -tex_step.y from the center_bytes one" etc. + * + * In the even row / even column (EE) case the colour values are: + * R = C = (0,0).r, + * G = (A0 + A1 + B0 + B1) / 4.0 = + * ( (0,-1).r + (0,1).r + (-1,0).r + (1,0).r ) / 4.0, + * B = (D0 + D1 + D2 + D3) / 4.0 = + * ( (-1,-1).r + (1,-1).r + (-1,1).r + (1,1).r ) / 4.0 + * + * For even row / odd column (EO): + * R = (B0 + B1) / 2.0 = ( (-1,0).r + (1,0).r ) / 2.0, + * G = C = (0,0).r, + * B = (A0 + A1) / 2.0 = ( (0,-1).r + (0,1).r ) / 2.0 + * + * For odd row / even column (OE): + * R = (A0 + A1) / 2.0 = ( (0,-1).r + (0,1).r ) / 2.0, + * G = C = (0,0).r, + * B = (B0 + B1) / 2.0 = ( (-1,0).r + (1,0).r ) / 2.0 + * + * For odd row / odd column (OO): + * R = (D0 + D1 + D2 + D3) / 4.0 = + * ( (-1,-1).r + (1,-1).r + (-1,1).r + (1,1).r ) / 4.0, + * G = (A0 + A1 + B0 + B1) / 4.0 = + * ( (0,-1).r + (0,1).r + (-1,0).r + (1,0).r ) / 4.0, + * B = C = (0,0).r + */ + + /* + * Fetch the values and precalculate the terms: + * patterns.x = (A0 + A1) / 2.0 + * patterns.y = (B0 + B1) / 2.0 + * patterns.z = (A0 + A1 + B0 + B1) / 4.0 + * patterns.w = (D0 + D1 + D2 + D3) / 4.0 + */ + #define fetch(x, y) texture2D(tex_y, vec2(x, y)).r + + float C = texture2D(tex_y, center_bytes).r; + vec4 patterns = vec4( + fetch(center_bytes.x, ycoords[0]), /* A0: (0,-1) */ + fetch(xcoords[0], center_bytes.y), /* B0: (-1,0) */ + fetch(xcoords[0], ycoords[0]), /* D0: (-1,-1) */ + fetch(xcoords[1], ycoords[0])); /* D1: (1,-1) */ + vec4 temp = vec4( + fetch(center_bytes.x, ycoords[1]), /* A1: (0,1) */ + fetch(xcoords[1], center_bytes.y), /* B1: (1,0) */ + fetch(xcoords[1], ycoords[1]), /* D3: (1,1) */ + fetch(xcoords[0], ycoords[1])); /* D2: (-1,1) */ + patterns = (patterns + temp) * 0.5; + /* .x = (A0 + A1) / 2.0, .y = (B0 + B1) / 2.0 */ + /* .z = (D0 + D3) / 2.0, .w = (D1 + D2) / 2.0 */ + patterns.w = (patterns.z + patterns.w) * 0.5; + patterns.z = (patterns.x + patterns.y) * 0.5; + + rgb = even_col ? + (even_row ? + vec3(C, patterns.zw) : + vec3(patterns.x, C, patterns.y)) : + (even_row ? + vec3(patterns.y, C, patterns.x) : + vec3(patterns.wz, C)); + + gl_FragColor = vec4(rgb, 1.0); +} diff --git a/src/apps/qcam/assets/shader/bayer_8.frag b/src/apps/qcam/assets/shader/bayer_8.frag new file mode 100644 index 00000000..7e35ca88 --- /dev/null +++ b/src/apps/qcam/assets/shader/bayer_8.frag @@ -0,0 +1,107 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* +From http://jgt.akpeters.com/papers/McGuire08/ + +Efficient, High-Quality Bayer Demosaic Filtering on GPUs + +Morgan McGuire + +This paper appears in issue Volume 13, Number 4. +--------------------------------------------------------- +Copyright (c) 2008, Morgan McGuire. All rights reserved. + +Modified by Linaro Ltd to integrate it into libcamera. +Copyright (C) 2021, Linaro +*/ + +//Pixel Shader +#ifdef GL_ES +precision mediump float; +#endif + +/** Monochrome RGBA or GL_LUMINANCE Bayer encoded texture.*/ +uniform sampler2D tex_y; +varying vec4 center; +varying vec4 yCoord; +varying vec4 xCoord; + +void main(void) { + #define fetch(x, y) texture2D(tex_y, vec2(x, y)).r + + float C = texture2D(tex_y, center.xy).r; // ( 0, 0) + const vec4 kC = vec4( 4.0, 6.0, 5.0, 5.0) / 8.0; + + // Determine which of four types of pixels we are on. + vec2 alternate = mod(floor(center.zw), 2.0); + + vec4 Dvec = vec4( + fetch(xCoord[1], yCoord[1]), // (-1,-1) + fetch(xCoord[1], yCoord[2]), // (-1, 1) + fetch(xCoord[2], yCoord[1]), // ( 1,-1) + fetch(xCoord[2], yCoord[2])); // ( 1, 1) + + vec4 PATTERN = (kC.xyz * C).xyzz; + + // Can also be a dot product with (1,1,1,1) on hardware where that is + // specially optimized. + // Equivalent to: D = Dvec[0] + Dvec[1] + Dvec[2] + Dvec[3]; + Dvec.xy += Dvec.zw; + Dvec.x += Dvec.y; + + vec4 value = vec4( + fetch(center.x, yCoord[0]), // ( 0,-2) + fetch(center.x, yCoord[1]), // ( 0,-1) + fetch(xCoord[0], center.y), // (-2, 0) + fetch(xCoord[1], center.y)); // (-1, 0) + + vec4 temp = vec4( + fetch(center.x, yCoord[3]), // ( 0, 2) + fetch(center.x, yCoord[2]), // ( 0, 1) + fetch(xCoord[3], center.y), // ( 2, 0) + fetch(xCoord[2], center.y)); // ( 1, 0) + + // Even the simplest compilers should be able to constant-fold these to + // avoid the division. + // Note that on scalar processors these constants force computation of some + // identical products twice. + const vec4 kA = vec4(-1.0, -1.5, 0.5, -1.0) / 8.0; + const vec4 kB = vec4( 2.0, 0.0, 0.0, 4.0) / 8.0; + const vec4 kD = vec4( 0.0, 2.0, -1.0, -1.0) / 8.0; + + // Conserve constant registers and take advantage of free swizzle on load + #define kE (kA.xywz) + #define kF (kB.xywz) + + value += temp; + + // There are five filter patterns (identity, cross, checker, + // theta, phi). Precompute the terms from all of them and then + // use swizzles to assign to color channels. + // + // Channel Matches + // x cross (e.g., EE G) + // y checker (e.g., EE B) + // z theta (e.g., EO R) + // w phi (e.g., EO R) + #define A (value[0]) + #define B (value[1]) + #define D (Dvec.x) + #define E (value[2]) + #define F (value[3]) + + // Avoid zero elements. On a scalar processor this saves two MADDs + // and it has no effect on a vector processor. + PATTERN.yzw += (kD.yz * D).xyy; + + PATTERN += (kA.xyz * A).xyzx + (kE.xyw * E).xyxz; + PATTERN.xw += kB.xw * B; + PATTERN.xz += kF.xz * F; + + gl_FragColor.rgb = (alternate.y == 0.0) ? + ((alternate.x == 0.0) ? + vec3(C, PATTERN.xy) : + vec3(PATTERN.z, C, PATTERN.w)) : + ((alternate.x == 0.0) ? + vec3(PATTERN.w, C, PATTERN.z) : + vec3(PATTERN.yx, C)); +} diff --git a/src/apps/qcam/assets/shader/bayer_8.vert b/src/apps/qcam/assets/shader/bayer_8.vert new file mode 100644 index 00000000..3695a5e9 --- /dev/null +++ b/src/apps/qcam/assets/shader/bayer_8.vert @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* +From http://jgt.akpeters.com/papers/McGuire08/ + +Efficient, High-Quality Bayer Demosaic Filtering on GPUs + +Morgan McGuire + +This paper appears in issue Volume 13, Number 4. +--------------------------------------------------------- +Copyright (c) 2008, Morgan McGuire. All rights reserved. + +Modified by Linaro Ltd to integrate it into libcamera. +Copyright (C) 2021, Linaro +*/ + +//Vertex Shader + +attribute vec4 vertexIn; +attribute vec2 textureIn; + +uniform vec2 tex_size; /* The texture size in pixels */ +uniform vec2 tex_step; + +/** Pixel position of the first red pixel in the */ +/** Bayer pattern. [{0,1}, {0, 1}]*/ +uniform vec2 tex_bayer_first_red; + +/** .xy = Pixel being sampled in the fragment shader on the range [0, 1] + .zw = ...on the range [0, sourceSize], offset by firstRed */ +varying vec4 center; + +/** center.x + (-2/w, -1/w, 1/w, 2/w); These are the x-positions */ +/** of the adjacent pixels.*/ +varying vec4 xCoord; + +/** center.y + (-2/h, -1/h, 1/h, 2/h); These are the y-positions */ +/** of the adjacent pixels.*/ +varying vec4 yCoord; + +void main(void) { + center.xy = textureIn; + center.zw = textureIn * tex_size + tex_bayer_first_red; + + xCoord = center.x + vec4(-2.0 * tex_step.x, + -tex_step.x, tex_step.x, 2.0 * tex_step.x); + yCoord = center.y + vec4(-2.0 * tex_step.y, + -tex_step.y, tex_step.y, 2.0 * tex_step.y); + + gl_Position = vertexIn; +} diff --git a/src/apps/qcam/assets/shader/identity.vert b/src/apps/qcam/assets/shader/identity.vert new file mode 100644 index 00000000..12c41377 --- /dev/null +++ b/src/apps/qcam/assets/shader/identity.vert @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2020, Linaro + * + * identity.vert - Identity vertex shader for pixel format conversion + */ + +attribute vec4 vertexIn; +attribute vec2 textureIn; +varying vec2 textureOut; + +uniform float stride_factor; + +void main(void) +{ + gl_Position = vertexIn; + textureOut = vec2(textureIn.x * stride_factor, textureIn.y); +} diff --git a/src/apps/qcam/assets/shader/shaders.qrc b/src/apps/qcam/assets/shader/shaders.qrc new file mode 100644 index 00000000..96c709f9 --- /dev/null +++ b/src/apps/qcam/assets/shader/shaders.qrc @@ -0,0 +1,13 @@ +<!-- SPDX-License-Identifier: LGPL-2.1-or-later --> +<!DOCTYPE RCC><RCC version="1.0"> +<qresource> + <file>RGB.frag</file> + <file>YUV_2_planes.frag</file> + <file>YUV_3_planes.frag</file> + <file>YUV_packed.frag</file> + <file>bayer_1x_packed.frag</file> + <file>bayer_8.frag</file> + <file>bayer_8.vert</file> + <file>identity.vert</file> +</qresource> +</RCC> diff --git a/src/apps/qcam/cam_select_dialog.cpp b/src/apps/qcam/cam_select_dialog.cpp new file mode 100644 index 00000000..3c8b12a9 --- /dev/null +++ b/src/apps/qcam/cam_select_dialog.cpp @@ -0,0 +1,111 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Utkarsh Tiwari <utkarsh02t@gmail.com> + * + * cam_select_dialog.cpp - qcam - Camera Selection dialog + */ + +#include "cam_select_dialog.h" + +#include <memory> + +#include <libcamera/camera.h> +#include <libcamera/camera_manager.h> + +#include <QComboBox> +#include <QDialogButtonBox> +#include <QFormLayout> +#include <QLabel> +#include <QString> + +CameraSelectorDialog::CameraSelectorDialog(libcamera::CameraManager *cameraManager, + QWidget *parent) + : QDialog(parent), cm_(cameraManager) +{ + /* Use a QFormLayout for the dialog. */ + QFormLayout *layout = new QFormLayout(this); + + /* Setup the camera id combo-box. */ + cameraIdComboBox_ = new QComboBox; + for (const auto &cam : cm_->cameras()) + cameraIdComboBox_->addItem(QString::fromStdString(cam->id())); + + /* Set camera information labels. */ + cameraLocation_ = new QLabel; + cameraModel_ = new QLabel; + + updateCameraInfo(cameraIdComboBox_->currentText()); + connect(cameraIdComboBox_, &QComboBox::currentTextChanged, + this, &CameraSelectorDialog::updateCameraInfo); + + /* Setup the QDialogButton Box */ + QDialogButtonBox *buttonBox = + new QDialogButtonBox(QDialogButtonBox::Ok | + QDialogButtonBox::Cancel); + + connect(buttonBox, &QDialogButtonBox::accepted, + this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, + this, &QDialog::reject); + + /* Set the layout. */ + layout->addRow("Camera:", cameraIdComboBox_); + layout->addRow("Location:", cameraLocation_); + layout->addRow("Model:", cameraModel_); + layout->addWidget(buttonBox); +} + +CameraSelectorDialog::~CameraSelectorDialog() = default; + +std::string CameraSelectorDialog::getCameraId() +{ + return cameraIdComboBox_->currentText().toStdString(); +} + +/* Hotplug / Unplug Support. */ +void CameraSelectorDialog::addCamera(QString cameraId) +{ + cameraIdComboBox_->addItem(cameraId); +} + +void CameraSelectorDialog::removeCamera(QString cameraId) +{ + int cameraIndex = cameraIdComboBox_->findText(cameraId); + cameraIdComboBox_->removeItem(cameraIndex); +} + +/* Camera Information */ +void CameraSelectorDialog::updateCameraInfo(QString cameraId) +{ + const std::shared_ptr<libcamera::Camera> &camera = + cm_->get(cameraId.toStdString()); + + if (!camera) + return; + + const libcamera::ControlList &properties = camera->properties(); + + const auto &location = properties.get(libcamera::properties::Location); + if (location) { + switch (*location) { + case libcamera::properties::CameraLocationFront: + cameraLocation_->setText("Internal front camera"); + break; + case libcamera::properties::CameraLocationBack: + cameraLocation_->setText("Internal back camera"); + break; + case libcamera::properties::CameraLocationExternal: + cameraLocation_->setText("External camera"); + break; + default: + cameraLocation_->setText("Unknown"); + } + } else { + cameraLocation_->setText("Unknown"); + } + + const auto &model = properties.get(libcamera::properties::Model) + .value_or("Unknown"); + + cameraModel_->setText(QString::fromStdString(model)); +} diff --git a/src/apps/qcam/cam_select_dialog.h b/src/apps/qcam/cam_select_dialog.h new file mode 100644 index 00000000..0b7709ed --- /dev/null +++ b/src/apps/qcam/cam_select_dialog.h @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Utkarsh Tiwari <utkarsh02t@gmail.com> + * + * cam_select_dialog.h - qcam - Camera Selection dialog + */ + +#pragma once + +#include <string> + +#include <libcamera/camera.h> +#include <libcamera/camera_manager.h> +#include <libcamera/controls.h> +#include <libcamera/property_ids.h> + +#include <QDialog> +#include <QString> + +class QComboBox; +class QLabel; + +class CameraSelectorDialog : public QDialog +{ + Q_OBJECT +public: + CameraSelectorDialog(libcamera::CameraManager *cameraManager, + QWidget *parent); + ~CameraSelectorDialog(); + + std::string getCameraId(); + + /* Hotplug / Unplug Support. */ + void addCamera(QString cameraId); + void removeCamera(QString cameraId); + + /* Camera Information */ + void updateCameraInfo(QString cameraId); + +private: + libcamera::CameraManager *cm_; + + /* UI elements. */ + QComboBox *cameraIdComboBox_; + QLabel *cameraLocation_; + QLabel *cameraModel_; +}; diff --git a/src/apps/qcam/format_converter.cpp b/src/apps/qcam/format_converter.cpp new file mode 100644 index 00000000..9331da0c --- /dev/null +++ b/src/apps/qcam/format_converter.cpp @@ -0,0 +1,359 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * format_convert.cpp - qcam - Convert buffer to RGB + */ + +#include "format_converter.h" + +#include <errno.h> +#include <utility> + +#include <QImage> + +#include <libcamera/formats.h> + +#include "../cam/image.h" + +#define RGBSHIFT 8 +#ifndef MAX +#define MAX(a,b) ((a)>(b)?(a):(b)) +#endif +#ifndef MIN +#define MIN(a,b) ((a)<(b)?(a):(b)) +#endif +#ifndef CLAMP +#define CLAMP(a,low,high) MAX((low),MIN((high),(a))) +#endif +#ifndef CLIP +#define CLIP(x) CLAMP(x,0,255) +#endif + +int FormatConverter::configure(const libcamera::PixelFormat &format, + const QSize &size, unsigned int stride) +{ + switch (format) { + case libcamera::formats::NV12: + formatFamily_ = YUVSemiPlanar; + horzSubSample_ = 2; + vertSubSample_ = 2; + nvSwap_ = false; + break; + case libcamera::formats::NV21: + formatFamily_ = YUVSemiPlanar; + horzSubSample_ = 2; + vertSubSample_ = 2; + nvSwap_ = true; + break; + case libcamera::formats::NV16: + formatFamily_ = YUVSemiPlanar; + horzSubSample_ = 2; + vertSubSample_ = 1; + nvSwap_ = false; + break; + case libcamera::formats::NV61: + formatFamily_ = YUVSemiPlanar; + horzSubSample_ = 2; + vertSubSample_ = 1; + nvSwap_ = true; + break; + case libcamera::formats::NV24: + formatFamily_ = YUVSemiPlanar; + horzSubSample_ = 1; + vertSubSample_ = 1; + nvSwap_ = false; + break; + case libcamera::formats::NV42: + formatFamily_ = YUVSemiPlanar; + horzSubSample_ = 1; + vertSubSample_ = 1; + nvSwap_ = true; + break; + + case libcamera::formats::R8: + formatFamily_ = RGB; + r_pos_ = 0; + g_pos_ = 0; + b_pos_ = 0; + bpp_ = 1; + break; + case libcamera::formats::RGB888: + formatFamily_ = RGB; + r_pos_ = 2; + g_pos_ = 1; + b_pos_ = 0; + bpp_ = 3; + break; + case libcamera::formats::BGR888: + formatFamily_ = RGB; + r_pos_ = 0; + g_pos_ = 1; + b_pos_ = 2; + bpp_ = 3; + break; + case libcamera::formats::ARGB8888: + case libcamera::formats::XRGB8888: + formatFamily_ = RGB; + r_pos_ = 2; + g_pos_ = 1; + b_pos_ = 0; + bpp_ = 4; + break; + case libcamera::formats::RGBA8888: + case libcamera::formats::RGBX8888: + formatFamily_ = RGB; + r_pos_ = 3; + g_pos_ = 2; + b_pos_ = 1; + bpp_ = 4; + break; + case libcamera::formats::ABGR8888: + case libcamera::formats::XBGR8888: + formatFamily_ = RGB; + r_pos_ = 0; + g_pos_ = 1; + b_pos_ = 2; + bpp_ = 4; + break; + case libcamera::formats::BGRA8888: + case libcamera::formats::BGRX8888: + formatFamily_ = RGB; + r_pos_ = 1; + g_pos_ = 2; + b_pos_ = 3; + bpp_ = 4; + break; + + case libcamera::formats::VYUY: + formatFamily_ = YUVPacked; + y_pos_ = 1; + cb_pos_ = 2; + break; + case libcamera::formats::YVYU: + formatFamily_ = YUVPacked; + y_pos_ = 0; + cb_pos_ = 3; + break; + case libcamera::formats::UYVY: + formatFamily_ = YUVPacked; + y_pos_ = 1; + cb_pos_ = 0; + break; + case libcamera::formats::YUYV: + formatFamily_ = YUVPacked; + y_pos_ = 0; + cb_pos_ = 1; + break; + + case libcamera::formats::YUV420: + formatFamily_ = YUVPlanar; + horzSubSample_ = 2; + vertSubSample_ = 2; + nvSwap_ = false; + break; + case libcamera::formats::YVU420: + formatFamily_ = YUVPlanar; + horzSubSample_ = 2; + vertSubSample_ = 2; + nvSwap_ = true; + break; + case libcamera::formats::YUV422: + formatFamily_ = YUVPlanar; + horzSubSample_ = 2; + vertSubSample_ = 1; + nvSwap_ = false; + break; + + case libcamera::formats::MJPEG: + formatFamily_ = MJPEG; + break; + + default: + return -EINVAL; + }; + + format_ = format; + width_ = size.width(); + height_ = size.height(); + stride_ = stride; + + return 0; +} + +void FormatConverter::convert(const Image *src, size_t size, QImage *dst) +{ + switch (formatFamily_) { + case MJPEG: + dst->loadFromData(src->data(0).data(), size, "JPEG"); + break; + case RGB: + convertRGB(src, dst->bits()); + break; + case YUVPacked: + convertYUVPacked(src, dst->bits()); + break; + case YUVSemiPlanar: + convertYUVSemiPlanar(src, dst->bits()); + break; + case YUVPlanar: + convertYUVPlanar(src, dst->bits()); + break; + }; +} + +static void yuv_to_rgb(int y, int u, int v, int *r, int *g, int *b) +{ + int c = y - 16; + int d = u - 128; + int e = v - 128; + *r = CLIP(( 298 * c + 409 * e + 128) >> RGBSHIFT); + *g = CLIP(( 298 * c - 100 * d - 208 * e + 128) >> RGBSHIFT); + *b = CLIP(( 298 * c + 516 * d + 128) >> RGBSHIFT); +} + +void FormatConverter::convertRGB(const Image *srcImage, unsigned char *dst) +{ + const unsigned char *src = srcImage->data(0).data(); + unsigned int x, y; + int r, g, b; + + for (y = 0; y < height_; y++) { + for (x = 0; x < width_; x++) { + r = src[bpp_ * x + r_pos_]; + g = src[bpp_ * x + g_pos_]; + b = src[bpp_ * x + b_pos_]; + + dst[4 * x + 0] = b; + dst[4 * x + 1] = g; + dst[4 * x + 2] = r; + dst[4 * x + 3] = 0xff; + } + + src += stride_; + dst += width_ * 4; + } +} + +void FormatConverter::convertYUVPacked(const Image *srcImage, unsigned char *dst) +{ + const unsigned char *src = srcImage->data(0).data(); + unsigned int src_x, src_y, dst_x, dst_y; + unsigned int src_stride; + unsigned int dst_stride; + unsigned int cr_pos; + int r, g, b, y, cr, cb; + + cr_pos = (cb_pos_ + 2) % 4; + src_stride = stride_; + dst_stride = width_ * 4; + + for (src_y = 0, dst_y = 0; dst_y < height_; src_y++, dst_y++) { + for (src_x = 0, dst_x = 0; dst_x < width_; ) { + cb = src[src_y * src_stride + src_x * 4 + cb_pos_]; + cr = src[src_y * src_stride + src_x * 4 + cr_pos]; + + y = src[src_y * src_stride + src_x * 4 + y_pos_]; + yuv_to_rgb(y, cb, cr, &r, &g, &b); + dst[dst_y * dst_stride + 4 * dst_x + 0] = b; + dst[dst_y * dst_stride + 4 * dst_x + 1] = g; + dst[dst_y * dst_stride + 4 * dst_x + 2] = r; + dst[dst_y * dst_stride + 4 * dst_x + 3] = 0xff; + dst_x++; + + y = src[src_y * src_stride + src_x * 4 + y_pos_ + 2]; + yuv_to_rgb(y, cb, cr, &r, &g, &b); + dst[dst_y * dst_stride + 4 * dst_x + 0] = b; + dst[dst_y * dst_stride + 4 * dst_x + 1] = g; + dst[dst_y * dst_stride + 4 * dst_x + 2] = r; + dst[dst_y * dst_stride + 4 * dst_x + 3] = 0xff; + dst_x++; + + src_x++; + } + } +} + +void FormatConverter::convertYUVPlanar(const Image *srcImage, unsigned char *dst) +{ + unsigned int c_stride = stride_ / horzSubSample_; + unsigned int c_inc = horzSubSample_ == 1 ? 1 : 0; + const unsigned char *src_y = srcImage->data(0).data(); + const unsigned char *src_cb = srcImage->data(1).data(); + const unsigned char *src_cr = srcImage->data(2).data(); + int r, g, b; + + if (nvSwap_) + std::swap(src_cb, src_cr); + + for (unsigned int y = 0; y < height_; y++) { + const unsigned char *line_y = src_y + y * stride_; + const unsigned char *line_cb = src_cb + (y / vertSubSample_) * + c_stride; + const unsigned char *line_cr = src_cr + (y / vertSubSample_) * + c_stride; + + for (unsigned int x = 0; x < width_; x += 2) { + yuv_to_rgb(*line_y, *line_cb, *line_cr, &r, &g, &b); + dst[0] = b; + dst[1] = g; + dst[2] = r; + dst[3] = 0xff; + line_y++; + line_cb += c_inc; + line_cr += c_inc; + dst += 4; + + yuv_to_rgb(*line_y, *line_cb, *line_cr, &r, &g, &b); + dst[0] = b; + dst[1] = g; + dst[2] = r; + dst[3] = 0xff; + line_y++; + line_cb += 1; + line_cr += 1; + dst += 4; + } + } +} + +void FormatConverter::convertYUVSemiPlanar(const Image *srcImage, unsigned char *dst) +{ + unsigned int c_stride = stride_ * (2 / horzSubSample_); + 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 = srcImage->data(0).data(); + const unsigned char *src_c = srcImage->data(1).data(); + int r, g, b; + + for (unsigned int y = 0; y < height_; y++) { + const unsigned char *src_y = src + 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 < width_; x += 2) { + yuv_to_rgb(*src_y, *src_cb, *src_cr, &r, &g, &b); + dst[0] = b; + dst[1] = g; + dst[2] = r; + dst[3] = 0xff; + src_y++; + src_cb += c_inc; + src_cr += c_inc; + dst += 4; + + yuv_to_rgb(*src_y, *src_cb, *src_cr, &r, &g, &b); + dst[0] = b; + dst[1] = g; + dst[2] = r; + dst[3] = 0xff; + src_y++; + src_cb += 2; + src_cr += 2; + dst += 4; + } + } +} diff --git a/src/apps/qcam/format_converter.h b/src/apps/qcam/format_converter.h new file mode 100644 index 00000000..37dbfae2 --- /dev/null +++ b/src/apps/qcam/format_converter.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * format_convert.h - qcam - Convert buffer to RGB + */ + +#pragma once + +#include <stddef.h> + +#include <QSize> + +#include <libcamera/pixel_format.h> + +class Image; +class QImage; + +class FormatConverter +{ +public: + int configure(const libcamera::PixelFormat &format, const QSize &size, + unsigned int stride); + + void convert(const Image *src, size_t size, QImage *dst); + +private: + enum FormatFamily { + MJPEG, + RGB, + YUVPacked, + YUVPlanar, + YUVSemiPlanar, + }; + + void convertRGB(const Image *src, unsigned char *dst); + void convertYUVPacked(const Image *src, unsigned char *dst); + void convertYUVPlanar(const Image *src, unsigned char *dst); + void convertYUVSemiPlanar(const Image *src, unsigned char *dst); + + libcamera::PixelFormat format_; + unsigned int width_; + unsigned int height_; + unsigned int stride_; + + enum FormatFamily formatFamily_; + + /* NV parameters */ + unsigned int horzSubSample_; + unsigned int vertSubSample_; + bool nvSwap_; + + /* RGB parameters */ + unsigned int bpp_; + unsigned int r_pos_; + unsigned int g_pos_; + unsigned int b_pos_; + + /* YUV parameters */ + unsigned int y_pos_; + unsigned int cb_pos_; +}; diff --git a/src/apps/qcam/main.cpp b/src/apps/qcam/main.cpp new file mode 100644 index 00000000..d3f01a85 --- /dev/null +++ b/src/apps/qcam/main.cpp @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * main.cpp - qcam - The libcamera GUI test application + */ + +#include <signal.h> +#include <string.h> + +#include <QApplication> +#include <QtDebug> + +#include <libcamera/camera_manager.h> + +#include "../cam/options.h" +#include "../cam/stream_options.h" +#include "main_window.h" +#include "message_handler.h" + +using namespace libcamera; + +void signalHandler([[maybe_unused]] int signal) +{ + qInfo() << "Exiting"; + qApp->quit(); +} + +OptionsParser::Options parseOptions(int argc, char *argv[]) +{ + StreamKeyValueParser streamKeyValue; + + OptionsParser parser; + parser.addOption(OptCamera, OptionString, + "Specify which camera to operate on", "camera", + ArgumentRequired, "camera"); + parser.addOption(OptHelp, OptionNone, "Display this help message", + "help"); + parser.addOption(OptRenderer, OptionString, + "Choose the renderer type {qt,gles} (default: qt)", + "renderer", ArgumentRequired, "renderer"); + parser.addOption(OptStream, &streamKeyValue, + "Set configuration of a camera stream", "stream", true); + parser.addOption(OptVerbose, OptionNone, + "Print verbose log messages", "verbose"); + + OptionsParser::Options options = parser.parse(argc, argv); + if (options.isSet(OptHelp)) + parser.usage(); + + return options; +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + int ret; + + OptionsParser::Options options = parseOptions(argc, argv); + if (!options.valid()) + return EXIT_FAILURE; + if (options.isSet(OptHelp)) + return 0; + + MessageHandler msgHandler(options.isSet(OptVerbose)); + + struct sigaction sa = {}; + sa.sa_handler = &signalHandler; + sigaction(SIGINT, &sa, nullptr); + + CameraManager *cm = new libcamera::CameraManager(); + + ret = cm->start(); + if (ret) { + qInfo() << "Failed to start camera manager:" + << strerror(-ret); + return EXIT_FAILURE; + } + + MainWindow *mainWindow = new MainWindow(cm, options); + mainWindow->show(); + ret = app.exec(); + delete mainWindow; + + cm->stop(); + delete cm; + + return ret; +} diff --git a/src/apps/qcam/main_window.cpp b/src/apps/qcam/main_window.cpp new file mode 100644 index 00000000..f553ccb0 --- /dev/null +++ b/src/apps/qcam/main_window.cpp @@ -0,0 +1,790 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * main_window.cpp - qcam - Main application window + */ + +#include "main_window.h" + +#include <assert.h> +#include <iomanip> +#include <string> + +#include <libcamera/camera_manager.h> +#include <libcamera/version.h> + +#include <QCoreApplication> +#include <QFileDialog> +#include <QImage> +#include <QImageWriter> +#include <QMutexLocker> +#include <QStandardPaths> +#include <QStringList> +#include <QTimer> +#include <QToolBar> +#include <QToolButton> +#include <QtDebug> + +#include "../cam/dng_writer.h" +#include "../cam/image.h" + +#include "cam_select_dialog.h" +#ifndef QT_NO_OPENGL +#include "viewfinder_gl.h" +#endif +#include "viewfinder_qt.h" + +using namespace libcamera; + +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) +/* + * Qt::fixed was introduced in v5.14, and ::fixed deprecated in v5.15. Allow + * usage of Qt::fixed unconditionally. + */ +namespace Qt { +constexpr auto fixed = ::fixed; +} /* namespace Qt */ +#endif + +/** + * \brief Custom QEvent to signal capture completion + */ +class CaptureEvent : public QEvent +{ +public: + CaptureEvent() + : QEvent(type()) + { + } + + static Type type() + { + static int type = QEvent::registerEventType(); + return static_cast<Type>(type); + } +}; + +/** + * \brief Custom QEvent to signal hotplug or unplug + */ +class HotplugEvent : public QEvent +{ +public: + enum PlugEvent { + HotPlug, + HotUnplug + }; + + HotplugEvent(std::shared_ptr<Camera> camera, PlugEvent event) + : QEvent(type()), camera_(std::move(camera)), plugEvent_(event) + { + } + + static Type type() + { + static int type = QEvent::registerEventType(); + return static_cast<Type>(type); + } + + PlugEvent hotplugEvent() const { return plugEvent_; } + Camera *camera() const { return camera_.get(); } + +private: + std::shared_ptr<Camera> camera_; + PlugEvent plugEvent_; +}; + +MainWindow::MainWindow(CameraManager *cm, const OptionsParser::Options &options) + : saveRaw_(nullptr), options_(options), cm_(cm), allocator_(nullptr), + isCapturing_(false), captureRaw_(false) +{ + int ret; + + /* + * Initialize the UI: Create the toolbar, set the window title and + * create the viewfinder widget. + */ + createToolbars(); + + title_ = "QCam " + QString::fromStdString(CameraManager::version()); + setWindowTitle(title_); + connect(&titleTimer_, SIGNAL(timeout()), this, SLOT(updateTitle())); + + /* Renderer type Qt or GLES, select Qt by default. */ + std::string renderType = "qt"; + if (options_.isSet(OptRenderer)) + renderType = options_[OptRenderer].toString(); + + if (renderType == "qt") { + ViewFinderQt *viewfinder = new ViewFinderQt(this); + connect(viewfinder, &ViewFinderQt::renderComplete, + this, &MainWindow::renderComplete); + viewfinder_ = viewfinder; + setCentralWidget(viewfinder); +#ifndef QT_NO_OPENGL + } else if (renderType == "gles") { + ViewFinderGL *viewfinder = new ViewFinderGL(this); + connect(viewfinder, &ViewFinderGL::renderComplete, + this, &MainWindow::renderComplete); + viewfinder_ = viewfinder; + setCentralWidget(viewfinder); +#endif + } else { + qWarning() << "Invalid render type" + << QString::fromStdString(renderType); + quit(); + return; + } + + adjustSize(); + + /* Hotplug/unplug support */ + cm_->cameraAdded.connect(this, &MainWindow::addCamera); + cm_->cameraRemoved.connect(this, &MainWindow::removeCamera); + + cameraSelectorDialog_ = new CameraSelectorDialog(cm_, this); + + /* Open the camera and start capture. */ + ret = openCamera(); + if (ret < 0) { + quit(); + return; + } + + startStopAction_->setChecked(true); +} + +MainWindow::~MainWindow() +{ + if (camera_) { + stopCapture(); + camera_->release(); + camera_.reset(); + } +} + +bool MainWindow::event(QEvent *e) +{ + if (e->type() == CaptureEvent::type()) { + processCapture(); + return true; + } else if (e->type() == HotplugEvent::type()) { + processHotplug(static_cast<HotplugEvent *>(e)); + return true; + } + + return QMainWindow::event(e); +} + +int MainWindow::createToolbars() +{ + QAction *action; + + toolbar_ = addToolBar("Main"); + + /* Disable right click context menu. */ + toolbar_->setContextMenuPolicy(Qt::PreventContextMenu); + + /* Quit action. */ + action = toolbar_->addAction(QIcon::fromTheme("application-exit", + QIcon(":x-circle.svg")), + "Quit"); + action->setShortcut(Qt::CTRL | Qt::Key_Q); + connect(action, &QAction::triggered, this, &MainWindow::quit); + + /* Camera selector. */ + cameraSelectButton_ = new QPushButton; + connect(cameraSelectButton_, &QPushButton::clicked, + this, &MainWindow::switchCamera); + + toolbar_->addWidget(cameraSelectButton_); + + toolbar_->addSeparator(); + + /* Start/Stop action. */ + iconPlay_ = QIcon::fromTheme("media-playback-start", + QIcon(":play-circle.svg")); + iconStop_ = QIcon::fromTheme("media-playback-stop", + QIcon(":stop-circle.svg")); + + action = toolbar_->addAction(iconPlay_, "Start Capture"); + action->setCheckable(true); + action->setShortcut(Qt::Key_Space); + connect(action, &QAction::toggled, this, &MainWindow::toggleCapture); + startStopAction_ = action; + + /* Save As... action. */ + action = toolbar_->addAction(QIcon::fromTheme("document-save-as", + QIcon(":save.svg")), + "Save As..."); + action->setShortcut(QKeySequence::SaveAs); + connect(action, &QAction::triggered, this, &MainWindow::saveImageAs); + +#ifdef HAVE_DNG + /* Save Raw action. */ + action = toolbar_->addAction(QIcon::fromTheme("camera-photo", + QIcon(":aperture.svg")), + "Save Raw"); + action->setEnabled(false); + connect(action, &QAction::triggered, this, &MainWindow::captureRaw); + saveRaw_ = action; +#endif + + return 0; +} + +void MainWindow::quit() +{ + QTimer::singleShot(0, QCoreApplication::instance(), + &QCoreApplication::quit); +} + +void MainWindow::updateTitle() +{ + /* Calculate the average frame rate over the last period. */ + unsigned int duration = frameRateInterval_.elapsed(); + unsigned int frames = framesCaptured_ - previousFrames_; + double fps = frames * 1000.0 / duration; + + /* Restart counters. */ + frameRateInterval_.start(); + previousFrames_ = framesCaptured_; + + setWindowTitle(title_ + " : " + QString::number(fps, 'f', 2) + " fps"); +} + +/* ----------------------------------------------------------------------------- + * Camera Selection + */ + +void MainWindow::switchCamera() +{ + /* Get and acquire the new camera. */ + std::string newCameraId = chooseCamera(); + + if (newCameraId.empty()) + return; + + if (camera_ && newCameraId == camera_->id()) + return; + + const std::shared_ptr<Camera> &cam = cm_->get(newCameraId); + + if (cam->acquire()) { + qInfo() << "Failed to acquire camera" << cam->id().c_str(); + return; + } + + qInfo() << "Switching to camera" << cam->id().c_str(); + + /* + * Stop the capture session, release the current camera, replace it with + * the new camera and start a new capture session. + */ + startStopAction_->setChecked(false); + + if (camera_) + camera_->release(); + + camera_ = cam; + + startStopAction_->setChecked(true); + + /* Display the current cameraId in the toolbar .*/ + cameraSelectButton_->setText(QString::fromStdString(newCameraId)); +} + +std::string MainWindow::chooseCamera() +{ + if (cameraSelectorDialog_->exec() != QDialog::Accepted) + return std::string(); + + return cameraSelectorDialog_->getCameraId(); +} + +int MainWindow::openCamera() +{ + std::string cameraName; + + /* + * Use the camera specified on the command line, if any, or display the + * camera selection dialog box otherwise. + */ + if (options_.isSet(OptCamera)) + cameraName = static_cast<std::string>(options_[OptCamera]); + else + cameraName = chooseCamera(); + + if (cameraName == "") + return -EINVAL; + + /* Get and acquire the camera. */ + camera_ = cm_->get(cameraName); + if (!camera_) { + qInfo() << "Camera" << cameraName.c_str() << "not found"; + return -ENODEV; + } + + if (camera_->acquire()) { + qInfo() << "Failed to acquire camera"; + camera_.reset(); + return -EBUSY; + } + + /* Set the camera switch button with the currently selected Camera id. */ + cameraSelectButton_->setText(QString::fromStdString(cameraName)); + + return 0; +} + +/* ----------------------------------------------------------------------------- + * Capture Start & Stop + */ + +void MainWindow::toggleCapture(bool start) +{ + if (start) { + startCapture(); + startStopAction_->setIcon(iconStop_); + startStopAction_->setText("Stop Capture"); + } else { + stopCapture(); + startStopAction_->setIcon(iconPlay_); + startStopAction_->setText("Start Capture"); + } +} + +/** + * \brief Start capture with the current camera + * + * This function shall not be called directly, use toggleCapture() instead. + */ +int MainWindow::startCapture() +{ + StreamRoles roles = StreamKeyValueParser::roles(options_[OptStream]); + int ret; + + /* Verify roles are supported. */ + switch (roles.size()) { + case 1: + if (roles[0] != StreamRole::Viewfinder) { + qWarning() << "Only viewfinder supported for single stream"; + return -EINVAL; + } + break; + case 2: + if (roles[0] != StreamRole::Viewfinder || + roles[1] != StreamRole::Raw) { + qWarning() << "Only viewfinder + raw supported for dual streams"; + return -EINVAL; + } + break; + default: + if (roles.size() != 1) { + qWarning() << "Unsupported stream configuration"; + return -EINVAL; + } + break; + } + + /* Configure the camera. */ + config_ = camera_->generateConfiguration(roles); + if (!config_) { + qWarning() << "Failed to generate configuration from roles"; + return -EINVAL; + } + + StreamConfiguration &vfConfig = config_->at(0); + + /* Use a format supported by the viewfinder if available. */ + std::vector<PixelFormat> formats = vfConfig.formats().pixelformats(); + for (const PixelFormat &format : viewfinder_->nativeFormats()) { + auto match = std::find_if(formats.begin(), formats.end(), + [&](const PixelFormat &f) { + return f == format; + }); + if (match != formats.end()) { + vfConfig.pixelFormat = format; + break; + } + } + + /* Allow user to override configuration. */ + if (StreamKeyValueParser::updateConfiguration(config_.get(), + options_[OptStream])) { + qWarning() << "Failed to update configuration"; + return -EINVAL; + } + + CameraConfiguration::Status validation = config_->validate(); + if (validation == CameraConfiguration::Invalid) { + qWarning() << "Failed to create valid camera configuration"; + return -EINVAL; + } + + if (validation == CameraConfiguration::Adjusted) + qInfo() << "Stream configuration adjusted to " + << vfConfig.toString().c_str(); + + ret = camera_->configure(config_.get()); + if (ret < 0) { + qInfo() << "Failed to configure camera"; + return ret; + } + + /* Store stream allocation. */ + vfStream_ = config_->at(0).stream(); + if (config_->size() == 2) + rawStream_ = config_->at(1).stream(); + else + rawStream_ = nullptr; + + /* + * Configure the viewfinder. If no color space is reported, default to + * sYCC. + */ + ret = viewfinder_->setFormat(vfConfig.pixelFormat, + QSize(vfConfig.size.width, vfConfig.size.height), + vfConfig.colorSpace.value_or(ColorSpace::Sycc), + vfConfig.stride); + if (ret < 0) { + qInfo() << "Failed to set viewfinder format"; + return ret; + } + + adjustSize(); + + /* Configure the raw capture button. */ + if (saveRaw_) + saveRaw_->setEnabled(config_->size() == 2); + + /* Allocate and map buffers. */ + allocator_ = new FrameBufferAllocator(camera_); + for (StreamConfiguration &config : *config_) { + Stream *stream = config.stream(); + + ret = allocator_->allocate(stream); + if (ret < 0) { + qWarning() << "Failed to allocate capture buffers"; + goto error; + } + + for (const std::unique_ptr<FrameBuffer> &buffer : allocator_->buffers(stream)) { + /* Map memory buffers and cache the mappings. */ + std::unique_ptr<Image> image = + Image::fromFrameBuffer(buffer.get(), Image::MapMode::ReadOnly); + assert(image != nullptr); + mappedBuffers_[buffer.get()] = std::move(image); + + /* Store buffers on the free list. */ + freeBuffers_[stream].enqueue(buffer.get()); + } + } + + /* Create requests and fill them with buffers from the viewfinder. */ + while (!freeBuffers_[vfStream_].isEmpty()) { + FrameBuffer *buffer = freeBuffers_[vfStream_].dequeue(); + + std::unique_ptr<Request> request = camera_->createRequest(); + if (!request) { + qWarning() << "Can't create request"; + ret = -ENOMEM; + goto error; + } + + ret = request->addBuffer(vfStream_, buffer); + if (ret < 0) { + qWarning() << "Can't set buffer for request"; + goto error; + } + + requests_.push_back(std::move(request)); + } + + /* Start the title timer and the camera. */ + titleTimer_.start(2000); + frameRateInterval_.start(); + previousFrames_ = 0; + framesCaptured_ = 0; + lastBufferTime_ = 0; + + ret = camera_->start(); + if (ret) { + qInfo() << "Failed to start capture"; + goto error; + } + + camera_->requestCompleted.connect(this, &MainWindow::requestComplete); + + /* Queue all requests. */ + for (std::unique_ptr<Request> &request : requests_) { + ret = queueRequest(request.get()); + if (ret < 0) { + qWarning() << "Can't queue request"; + goto error_disconnect; + } + } + + isCapturing_ = true; + + return 0; + +error_disconnect: + camera_->requestCompleted.disconnect(this); + camera_->stop(); + +error: + requests_.clear(); + + mappedBuffers_.clear(); + + freeBuffers_.clear(); + + delete allocator_; + allocator_ = nullptr; + + return ret; +} + +/** + * \brief Stop ongoing capture + * + * This function may be called directly when tearing down the MainWindow. Use + * toggleCapture() instead in all other cases. + */ +void MainWindow::stopCapture() +{ + if (!isCapturing_) + return; + + viewfinder_->stop(); + if (saveRaw_) + saveRaw_->setEnabled(false); + captureRaw_ = false; + + int ret = camera_->stop(); + if (ret) + qInfo() << "Failed to stop capture"; + + camera_->requestCompleted.disconnect(this); + + mappedBuffers_.clear(); + + requests_.clear(); + freeQueue_.clear(); + + delete allocator_; + + isCapturing_ = false; + + config_.reset(); + + /* + * A CaptureEvent may have been posted before we stopped the camera, + * but not processed yet. Clear the queue of done buffers to avoid + * racing with the event handler. + */ + freeBuffers_.clear(); + doneQueue_.clear(); + + titleTimer_.stop(); + setWindowTitle(title_); +} + +/* ----------------------------------------------------------------------------- + * Camera hotplugging support + */ + +void MainWindow::processHotplug(HotplugEvent *e) +{ + Camera *camera = e->camera(); + QString cameraId = QString::fromStdString(camera->id()); + HotplugEvent::PlugEvent event = e->hotplugEvent(); + + if (event == HotplugEvent::HotPlug) { + cameraSelectorDialog_->addCamera(cameraId); + } else if (event == HotplugEvent::HotUnplug) { + /* Check if the currently-streaming camera is removed. */ + if (camera == camera_.get()) { + toggleCapture(false); + camera_->release(); + camera_.reset(); + } + + cameraSelectorDialog_->removeCamera(cameraId); + } +} + +void MainWindow::addCamera(std::shared_ptr<Camera> camera) +{ + qInfo() << "Adding new camera:" << camera->id().c_str(); + QCoreApplication::postEvent(this, + new HotplugEvent(std::move(camera), + HotplugEvent::HotPlug)); +} + +void MainWindow::removeCamera(std::shared_ptr<Camera> camera) +{ + qInfo() << "Removing camera:" << camera->id().c_str(); + QCoreApplication::postEvent(this, + new HotplugEvent(std::move(camera), + HotplugEvent::HotUnplug)); +} + +/* ----------------------------------------------------------------------------- + * Image Save + */ + +void MainWindow::saveImageAs() +{ + QImage image = viewfinder_->getCurrentImage(); + QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + + QString filename = QFileDialog::getSaveFileName(this, "Save Image", defaultPath, + "Image Files (*.png *.jpg *.jpeg)"); + if (filename.isEmpty()) + return; + + QImageWriter writer(filename); + writer.setQuality(95); + writer.write(image); +} + +void MainWindow::captureRaw() +{ + captureRaw_ = true; +} + +void MainWindow::processRaw(FrameBuffer *buffer, + [[maybe_unused]] const ControlList &metadata) +{ +#ifdef HAVE_DNG + QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + QString filename = QFileDialog::getSaveFileName(this, "Save DNG", defaultPath, + "DNG Files (*.dng)"); + + if (!filename.isEmpty()) { + uint8_t *memory = mappedBuffers_[buffer]->data(0).data(); + DNGWriter::write(filename.toStdString().c_str(), camera_.get(), + rawStream_->configuration(), metadata, buffer, + memory); + } +#endif + + { + QMutexLocker locker(&mutex_); + freeBuffers_[rawStream_].enqueue(buffer); + } +} + +/* ----------------------------------------------------------------------------- + * Request Completion Handling + */ + +void MainWindow::requestComplete(Request *request) +{ + if (request->status() == Request::RequestCancelled) + return; + + /* + * We're running in the libcamera thread context, expensive operations + * are not allowed. Add the buffer to the done queue and post a + * CaptureEvent for the application thread to handle. + */ + { + QMutexLocker locker(&mutex_); + doneQueue_.enqueue(request); + } + + QCoreApplication::postEvent(this, new CaptureEvent); +} + +void MainWindow::processCapture() +{ + /* + * Retrieve the next buffer from the done queue. The queue may be empty + * if stopCapture() has been called while a CaptureEvent was posted but + * not processed yet. Return immediately in that case. + */ + Request *request; + { + QMutexLocker locker(&mutex_); + if (doneQueue_.isEmpty()) + return; + + request = doneQueue_.dequeue(); + } + + /* Process buffers. */ + if (request->buffers().count(vfStream_)) + processViewfinder(request->buffers().at(vfStream_)); + + if (request->buffers().count(rawStream_)) + processRaw(request->buffers().at(rawStream_), request->metadata()); + + request->reuse(); + QMutexLocker locker(&mutex_); + freeQueue_.enqueue(request); +} + +void MainWindow::processViewfinder(FrameBuffer *buffer) +{ + framesCaptured_++; + + const FrameMetadata &metadata = buffer->metadata(); + + double fps = metadata.timestamp - lastBufferTime_; + fps = lastBufferTime_ && fps ? 1000000000.0 / fps : 0.0; + lastBufferTime_ = metadata.timestamp; + + QStringList bytesused; + for (const FrameMetadata::Plane &plane : metadata.planes()) + bytesused << QString::number(plane.bytesused); + + qDebug().noquote() + << QString("seq: %1").arg(metadata.sequence, 6, 10, QLatin1Char('0')) + << "bytesused: {" << bytesused.join(", ") + << "} timestamp:" << metadata.timestamp + << "fps:" << Qt::fixed << qSetRealNumberPrecision(2) << fps; + + /* Render the frame on the viewfinder. */ + viewfinder_->render(buffer, mappedBuffers_[buffer].get()); +} + +void MainWindow::renderComplete(FrameBuffer *buffer) +{ + Request *request; + { + QMutexLocker locker(&mutex_); + if (freeQueue_.isEmpty()) + return; + + request = freeQueue_.dequeue(); + } + + request->addBuffer(vfStream_, buffer); + + if (captureRaw_) { + FrameBuffer *rawBuffer = nullptr; + + { + QMutexLocker locker(&mutex_); + if (!freeBuffers_[rawStream_].isEmpty()) + rawBuffer = freeBuffers_[rawStream_].dequeue(); + } + + if (rawBuffer) { + request->addBuffer(rawStream_, rawBuffer); + captureRaw_ = false; + } else { + qWarning() << "No free buffer available for RAW capture"; + } + } + queueRequest(request); +} + +int MainWindow::queueRequest(Request *request) +{ + return camera_->queueRequest(request); +} diff --git a/src/apps/qcam/main_window.h b/src/apps/qcam/main_window.h new file mode 100644 index 00000000..95b64124 --- /dev/null +++ b/src/apps/qcam/main_window.h @@ -0,0 +1,133 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * main_window.h - qcam - Main application window + */ + +#pragma once + +#include <memory> +#include <vector> + +#include <libcamera/camera.h> +#include <libcamera/camera_manager.h> +#include <libcamera/controls.h> +#include <libcamera/framebuffer.h> +#include <libcamera/framebuffer_allocator.h> +#include <libcamera/request.h> +#include <libcamera/stream.h> + +#include <QElapsedTimer> +#include <QIcon> +#include <QMainWindow> +#include <QMutex> +#include <QObject> +#include <QPushButton> +#include <QQueue> +#include <QTimer> + +#include "../cam/stream_options.h" + +#include "viewfinder.h" + +class QAction; + +class CameraSelectorDialog; +class Image; +class HotplugEvent; + +enum { + OptCamera = 'c', + OptHelp = 'h', + OptRenderer = 'r', + OptStream = 's', + OptVerbose = 'v', +}; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(libcamera::CameraManager *cm, + const OptionsParser::Options &options); + ~MainWindow(); + + bool event(QEvent *e) override; + +private Q_SLOTS: + void quit(); + void updateTitle(); + + void switchCamera(); + void toggleCapture(bool start); + + void saveImageAs(); + void captureRaw(); + void processRaw(libcamera::FrameBuffer *buffer, + const libcamera::ControlList &metadata); + + void renderComplete(libcamera::FrameBuffer *buffer); + +private: + int createToolbars(); + + std::string chooseCamera(); + int openCamera(); + + int startCapture(); + void stopCapture(); + + void addCamera(std::shared_ptr<libcamera::Camera> camera); + void removeCamera(std::shared_ptr<libcamera::Camera> camera); + + int queueRequest(libcamera::Request *request); + void requestComplete(libcamera::Request *request); + void processCapture(); + void processHotplug(HotplugEvent *e); + void processViewfinder(libcamera::FrameBuffer *buffer); + + /* UI elements */ + QToolBar *toolbar_; + QAction *startStopAction_; + QPushButton *cameraSelectButton_; + QAction *saveRaw_; + ViewFinder *viewfinder_; + + QIcon iconPlay_; + QIcon iconStop_; + + QString title_; + QTimer titleTimer_; + + CameraSelectorDialog *cameraSelectorDialog_; + + /* Options */ + const OptionsParser::Options &options_; + + /* Camera manager, camera, configuration and buffers */ + libcamera::CameraManager *cm_; + std::shared_ptr<libcamera::Camera> camera_; + libcamera::FrameBufferAllocator *allocator_; + + std::unique_ptr<libcamera::CameraConfiguration> config_; + std::map<libcamera::FrameBuffer *, std::unique_ptr<Image>> mappedBuffers_; + + /* Capture state, buffers queue and statistics */ + bool isCapturing_; + bool captureRaw_; + libcamera::Stream *vfStream_; + libcamera::Stream *rawStream_; + std::map<const libcamera::Stream *, QQueue<libcamera::FrameBuffer *>> freeBuffers_; + QQueue<libcamera::Request *> doneQueue_; + QQueue<libcamera::Request *> freeQueue_; + QMutex mutex_; /* Protects freeBuffers_, doneQueue_, and freeQueue_ */ + + uint64_t lastBufferTime_; + QElapsedTimer frameRateInterval_; + uint32_t previousFrames_; + uint32_t framesCaptured_; + + std::vector<std::unique_ptr<libcamera::Request>> requests_; +}; diff --git a/src/apps/qcam/meson.build b/src/apps/qcam/meson.build new file mode 100644 index 00000000..d5916d0d --- /dev/null +++ b/src/apps/qcam/meson.build @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: CC0-1.0 + +qt5 = import('qt5') +qt5_dep = dependency('qt5', + method : 'pkg-config', + modules : ['Core', 'Gui', 'Widgets'], + required : get_option('qcam'), + version : '>=5.4') + +if not qt5_dep.found() + qcam_enabled = false + subdir_done() +endif + +qcam_enabled = true + +qcam_sources = files([ + '../cam/image.cpp', + '../cam/options.cpp', + '../cam/stream_options.cpp', + 'cam_select_dialog.cpp', + 'format_converter.cpp', + 'main.cpp', + 'main_window.cpp', + 'message_handler.cpp', + 'viewfinder_qt.cpp', +]) + +qcam_moc_headers = files([ + 'cam_select_dialog.h', + 'main_window.h', + 'viewfinder_qt.h', +]) + +qcam_resources = files([ + 'assets/feathericons/feathericons.qrc', +]) + +qt5_cpp_args = ['-DQT_NO_KEYWORDS'] + +tiff_dep = dependency('libtiff-4', required : false) +if tiff_dep.found() + qt5_cpp_args += ['-DHAVE_TIFF'] + qcam_sources += files([ + '../cam/dng_writer.cpp', + ]) +endif + +if cxx.has_header_symbol('QOpenGLWidget', 'QOpenGLWidget', + dependencies : qt5_dep, args : '-fPIC') + qcam_sources += files([ + 'viewfinder_gl.cpp', + ]) + qcam_moc_headers += files([ + 'viewfinder_gl.h', + ]) + qcam_resources += files([ + 'assets/shader/shaders.qrc' + ]) +endif + +# gcc 9 introduced a deprecated-copy warning that is triggered by Qt until +# Qt 5.13. clang 10 introduced the same warning, but detects more issues +# that are not fixed in Qt yet. Disable the warning manually in both cases. +if ((cc.get_id() == 'gcc' and cc.version().version_compare('>=9.0') and + qt5_dep.version().version_compare('<5.13')) or + (cc.get_id() == 'clang' and cc.version().version_compare('>=10.0'))) + qt5_cpp_args += ['-Wno-deprecated-copy'] +endif + +resources = qt5.preprocess(moc_headers: qcam_moc_headers, + qresources : qcam_resources, + dependencies: qt5_dep) + +qcam = executable('qcam', qcam_sources, resources, + install : true, + dependencies : [ + libatomic, + libcamera_public, + qt5_dep, + tiff_dep, + ], + cpp_args : qt5_cpp_args) diff --git a/src/apps/qcam/message_handler.cpp b/src/apps/qcam/message_handler.cpp new file mode 100644 index 00000000..261623e1 --- /dev/null +++ b/src/apps/qcam/message_handler.cpp @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Laurent Pinchart <laurent.pinchart@ideasonboard.com> + * + * message_handler.cpp - qcam - Log message handling + */ + +#include "message_handler.h" + +QtMessageHandler MessageHandler::handler_ = nullptr; +bool MessageHandler::verbose_ = false; + +MessageHandler::MessageHandler(bool verbose) +{ + verbose_ = verbose; + handler_ = qInstallMessageHandler(&MessageHandler::handleMessage); +} + +void MessageHandler::handleMessage(QtMsgType type, + const QMessageLogContext &context, + const QString &msg) +{ + if (type == QtDebugMsg && !verbose_) + return; + + handler_(type, context, msg); +} diff --git a/src/apps/qcam/message_handler.h b/src/apps/qcam/message_handler.h new file mode 100644 index 00000000..56294d37 --- /dev/null +++ b/src/apps/qcam/message_handler.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Laurent Pinchart <laurent.pinchart@ideasonboard.com> + * + * message_handler.cpp - qcam - Log message handling + */ + +#pragma once + +#include <QtGlobal> + +class MessageHandler +{ +public: + MessageHandler(bool verbose); + +private: + static void handleMessage(QtMsgType type, + const QMessageLogContext &context, + const QString &msg); + + static QtMessageHandler handler_; + static bool verbose_; +}; diff --git a/src/apps/qcam/viewfinder.h b/src/apps/qcam/viewfinder.h new file mode 100644 index 00000000..a57446e8 --- /dev/null +++ b/src/apps/qcam/viewfinder.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * viewfinder.h - qcam - Viewfinder base class + */ + +#pragma once + +#include <QImage> +#include <QList> +#include <QSize> + +#include <libcamera/color_space.h> +#include <libcamera/formats.h> +#include <libcamera/framebuffer.h> + +class Image; + +class ViewFinder +{ +public: + virtual ~ViewFinder() = default; + + virtual const QList<libcamera::PixelFormat> &nativeFormats() const = 0; + + virtual int setFormat(const libcamera::PixelFormat &format, const QSize &size, + const libcamera::ColorSpace &colorSpace, + unsigned int stride) = 0; + virtual void render(libcamera::FrameBuffer *buffer, Image *image) = 0; + virtual void stop() = 0; + + virtual QImage getCurrentImage() = 0; +}; diff --git a/src/apps/qcam/viewfinder_gl.cpp b/src/apps/qcam/viewfinder_gl.cpp new file mode 100644 index 00000000..38ddad58 --- /dev/null +++ b/src/apps/qcam/viewfinder_gl.cpp @@ -0,0 +1,835 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2020, Linaro + * + * viewfinderGL.cpp - OpenGL Viewfinder for rendering by OpenGL shader + */ + +#include "viewfinder_gl.h" + +#include <array> + +#include <QByteArray> +#include <QFile> +#include <QImage> +#include <QStringList> + +#include <libcamera/formats.h> + +#include "../cam/image.h" + +static const QList<libcamera::PixelFormat> supportedFormats{ + /* YUV - packed (single plane) */ + libcamera::formats::UYVY, + libcamera::formats::VYUY, + libcamera::formats::YUYV, + libcamera::formats::YVYU, + /* YUV - semi planar (two planes) */ + libcamera::formats::NV12, + libcamera::formats::NV21, + libcamera::formats::NV16, + libcamera::formats::NV61, + libcamera::formats::NV24, + libcamera::formats::NV42, + /* YUV - fully planar (three planes) */ + libcamera::formats::YUV420, + libcamera::formats::YVU420, + /* RGB */ + libcamera::formats::ABGR8888, + libcamera::formats::ARGB8888, + libcamera::formats::BGRA8888, + libcamera::formats::RGBA8888, + libcamera::formats::BGR888, + libcamera::formats::RGB888, + /* Raw Bayer 8-bit */ + libcamera::formats::SBGGR8, + libcamera::formats::SGBRG8, + libcamera::formats::SGRBG8, + libcamera::formats::SRGGB8, + /* Raw Bayer 10-bit packed */ + libcamera::formats::SBGGR10_CSI2P, + libcamera::formats::SGBRG10_CSI2P, + libcamera::formats::SGRBG10_CSI2P, + libcamera::formats::SRGGB10_CSI2P, + /* Raw Bayer 12-bit packed */ + libcamera::formats::SBGGR12_CSI2P, + libcamera::formats::SGBRG12_CSI2P, + libcamera::formats::SGRBG12_CSI2P, + libcamera::formats::SRGGB12_CSI2P, +}; + +ViewFinderGL::ViewFinderGL(QWidget *parent) + : QOpenGLWidget(parent), buffer_(nullptr), + colorSpace_(libcamera::ColorSpace::Raw), image_(nullptr), + vertexBuffer_(QOpenGLBuffer::VertexBuffer) +{ +} + +ViewFinderGL::~ViewFinderGL() +{ + removeShader(); +} + +const QList<libcamera::PixelFormat> &ViewFinderGL::nativeFormats() const +{ + return supportedFormats; +} + +int ViewFinderGL::setFormat(const libcamera::PixelFormat &format, const QSize &size, + const libcamera::ColorSpace &colorSpace, + unsigned int stride) +{ + if (format != format_ || colorSpace != colorSpace_) { + /* + * If the fragment already exists, remove it and create a new + * one for the new format. + */ + if (shaderProgram_.isLinked()) { + shaderProgram_.release(); + shaderProgram_.removeShader(fragmentShader_.get()); + fragmentShader_.reset(); + } + + if (!selectFormat(format)) + return -1; + + selectColorSpace(colorSpace); + + format_ = format; + colorSpace_ = colorSpace; + } + + size_ = size; + stride_ = stride; + + updateGeometry(); + return 0; +} + +void ViewFinderGL::stop() +{ + if (buffer_) { + renderComplete(buffer_); + buffer_ = nullptr; + image_ = nullptr; + } +} + +QImage ViewFinderGL::getCurrentImage() +{ + QMutexLocker locker(&mutex_); + + return grabFramebuffer(); +} + +void ViewFinderGL::render(libcamera::FrameBuffer *buffer, Image *image) +{ + if (buffer_) + renderComplete(buffer_); + + image_ = image; + update(); + buffer_ = buffer; +} + +bool ViewFinderGL::selectFormat(const libcamera::PixelFormat &format) +{ + bool ret = true; + + /* Set min/mag filters to GL_LINEAR by default. */ + textureMinMagFilters_ = GL_LINEAR; + + /* Use identity.vert as the default vertex shader. */ + vertexShaderFile_ = ":identity.vert"; + + fragmentShaderDefines_.clear(); + + switch (format) { + case libcamera::formats::NV12: + horzSubSample_ = 2; + vertSubSample_ = 2; + fragmentShaderDefines_.append("#define YUV_PATTERN_UV"); + fragmentShaderFile_ = ":YUV_2_planes.frag"; + break; + case libcamera::formats::NV21: + horzSubSample_ = 2; + vertSubSample_ = 2; + fragmentShaderDefines_.append("#define YUV_PATTERN_VU"); + fragmentShaderFile_ = ":YUV_2_planes.frag"; + break; + case libcamera::formats::NV16: + horzSubSample_ = 2; + vertSubSample_ = 1; + fragmentShaderDefines_.append("#define YUV_PATTERN_UV"); + fragmentShaderFile_ = ":YUV_2_planes.frag"; + break; + case libcamera::formats::NV61: + horzSubSample_ = 2; + vertSubSample_ = 1; + fragmentShaderDefines_.append("#define YUV_PATTERN_VU"); + fragmentShaderFile_ = ":YUV_2_planes.frag"; + break; + case libcamera::formats::NV24: + horzSubSample_ = 1; + vertSubSample_ = 1; + fragmentShaderDefines_.append("#define YUV_PATTERN_UV"); + fragmentShaderFile_ = ":YUV_2_planes.frag"; + break; + case libcamera::formats::NV42: + horzSubSample_ = 1; + vertSubSample_ = 1; + fragmentShaderDefines_.append("#define YUV_PATTERN_VU"); + fragmentShaderFile_ = ":YUV_2_planes.frag"; + break; + case libcamera::formats::YUV420: + horzSubSample_ = 2; + vertSubSample_ = 2; + fragmentShaderFile_ = ":YUV_3_planes.frag"; + break; + case libcamera::formats::YVU420: + horzSubSample_ = 2; + vertSubSample_ = 2; + fragmentShaderFile_ = ":YUV_3_planes.frag"; + break; + case libcamera::formats::UYVY: + fragmentShaderDefines_.append("#define YUV_PATTERN_UYVY"); + fragmentShaderFile_ = ":YUV_packed.frag"; + break; + case libcamera::formats::VYUY: + fragmentShaderDefines_.append("#define YUV_PATTERN_VYUY"); + fragmentShaderFile_ = ":YUV_packed.frag"; + break; + case libcamera::formats::YUYV: + fragmentShaderDefines_.append("#define YUV_PATTERN_YUYV"); + fragmentShaderFile_ = ":YUV_packed.frag"; + break; + case libcamera::formats::YVYU: + fragmentShaderDefines_.append("#define YUV_PATTERN_YVYU"); + fragmentShaderFile_ = ":YUV_packed.frag"; + break; + case libcamera::formats::ABGR8888: + fragmentShaderDefines_.append("#define RGB_PATTERN rgb"); + fragmentShaderFile_ = ":RGB.frag"; + break; + case libcamera::formats::ARGB8888: + fragmentShaderDefines_.append("#define RGB_PATTERN bgr"); + fragmentShaderFile_ = ":RGB.frag"; + break; + case libcamera::formats::BGRA8888: + fragmentShaderDefines_.append("#define RGB_PATTERN gba"); + fragmentShaderFile_ = ":RGB.frag"; + break; + case libcamera::formats::RGBA8888: + fragmentShaderDefines_.append("#define RGB_PATTERN abg"); + fragmentShaderFile_ = ":RGB.frag"; + break; + case libcamera::formats::BGR888: + fragmentShaderDefines_.append("#define RGB_PATTERN rgb"); + fragmentShaderFile_ = ":RGB.frag"; + break; + case libcamera::formats::RGB888: + fragmentShaderDefines_.append("#define RGB_PATTERN bgr"); + fragmentShaderFile_ = ":RGB.frag"; + break; + case libcamera::formats::SBGGR8: + firstRed_.setX(1.0); + firstRed_.setY(1.0); + vertexShaderFile_ = ":bayer_8.vert"; + fragmentShaderFile_ = ":bayer_8.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + case libcamera::formats::SGBRG8: + firstRed_.setX(0.0); + firstRed_.setY(1.0); + vertexShaderFile_ = ":bayer_8.vert"; + fragmentShaderFile_ = ":bayer_8.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + case libcamera::formats::SGRBG8: + firstRed_.setX(1.0); + firstRed_.setY(0.0); + vertexShaderFile_ = ":bayer_8.vert"; + fragmentShaderFile_ = ":bayer_8.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + case libcamera::formats::SRGGB8: + firstRed_.setX(0.0); + firstRed_.setY(0.0); + vertexShaderFile_ = ":bayer_8.vert"; + fragmentShaderFile_ = ":bayer_8.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + case libcamera::formats::SBGGR10_CSI2P: + firstRed_.setX(1.0); + firstRed_.setY(1.0); + fragmentShaderDefines_.append("#define RAW10P"); + fragmentShaderFile_ = ":bayer_1x_packed.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + case libcamera::formats::SGBRG10_CSI2P: + firstRed_.setX(0.0); + firstRed_.setY(1.0); + fragmentShaderDefines_.append("#define RAW10P"); + fragmentShaderFile_ = ":bayer_1x_packed.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + case libcamera::formats::SGRBG10_CSI2P: + firstRed_.setX(1.0); + firstRed_.setY(0.0); + fragmentShaderDefines_.append("#define RAW10P"); + fragmentShaderFile_ = ":bayer_1x_packed.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + case libcamera::formats::SRGGB10_CSI2P: + firstRed_.setX(0.0); + firstRed_.setY(0.0); + fragmentShaderDefines_.append("#define RAW10P"); + fragmentShaderFile_ = ":bayer_1x_packed.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + case libcamera::formats::SBGGR12_CSI2P: + firstRed_.setX(1.0); + firstRed_.setY(1.0); + fragmentShaderDefines_.append("#define RAW12P"); + fragmentShaderFile_ = ":bayer_1x_packed.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + case libcamera::formats::SGBRG12_CSI2P: + firstRed_.setX(0.0); + firstRed_.setY(1.0); + fragmentShaderDefines_.append("#define RAW12P"); + fragmentShaderFile_ = ":bayer_1x_packed.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + case libcamera::formats::SGRBG12_CSI2P: + firstRed_.setX(1.0); + firstRed_.setY(0.0); + fragmentShaderDefines_.append("#define RAW12P"); + fragmentShaderFile_ = ":bayer_1x_packed.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + case libcamera::formats::SRGGB12_CSI2P: + firstRed_.setX(0.0); + firstRed_.setY(0.0); + fragmentShaderDefines_.append("#define RAW12P"); + fragmentShaderFile_ = ":bayer_1x_packed.frag"; + textureMinMagFilters_ = GL_NEAREST; + break; + default: + ret = false; + qWarning() << "[ViewFinderGL]:" + << "format not supported."; + break; + }; + + return ret; +} + +void ViewFinderGL::selectColorSpace(const libcamera::ColorSpace &colorSpace) +{ + std::array<double, 9> yuv2rgb; + + /* OpenGL stores arrays in column-major order. */ + switch (colorSpace.ycbcrEncoding) { + case libcamera::ColorSpace::YcbcrEncoding::None: + default: + yuv2rgb = { + 1.0000, 0.0000, 0.0000, + 0.0000, 1.0000, 0.0000, + 0.0000, 0.0000, 1.0000, + }; + break; + + case libcamera::ColorSpace::YcbcrEncoding::Rec601: + yuv2rgb = { + 1.0000, 1.0000, 1.0000, + 0.0000, -0.3441, 1.7720, + 1.4020, -0.7141, 0.0000, + }; + break; + + case libcamera::ColorSpace::YcbcrEncoding::Rec709: + yuv2rgb = { + 1.0000, 1.0000, 1.0000, + 0.0000, -0.1873, 1.8856, + 1.5748, -0.4681, 0.0000, + }; + break; + + case libcamera::ColorSpace::YcbcrEncoding::Rec2020: + yuv2rgb = { + 1.0000, 1.0000, 1.0000, + 0.0000, -0.1646, 1.8814, + 1.4746, -0.5714, 0.0000, + }; + break; + } + + double offset; + + switch (colorSpace.range) { + case libcamera::ColorSpace::Range::Full: + default: + offset = 0.0; + break; + + case libcamera::ColorSpace::Range::Limited: + offset = 16.0; + + for (unsigned int i = 0; i < 3; ++i) + yuv2rgb[i] *= 255.0 / 219.0; + for (unsigned int i = 4; i < 9; ++i) + yuv2rgb[i] *= 255.0 / 224.0; + break; + } + + QStringList matrix; + + for (double coeff : yuv2rgb) + matrix.append(QString::number(coeff, 'f')); + + fragmentShaderDefines_.append("#define YUV2RGB_MATRIX " + matrix.join(", ")); + fragmentShaderDefines_.append(QString("#define YUV2RGB_Y_OFFSET %1") + .arg(offset, 0, 'f', 1)); +} + +bool ViewFinderGL::createVertexShader() +{ + /* Create Vertex Shader */ + vertexShader_ = std::make_unique<QOpenGLShader>(QOpenGLShader::Vertex, this); + + /* Compile the vertex shader */ + if (!vertexShader_->compileSourceFile(vertexShaderFile_)) { + qWarning() << "[ViewFinderGL]:" << vertexShader_->log(); + return false; + } + + shaderProgram_.addShader(vertexShader_.get()); + return true; +} + +bool ViewFinderGL::createFragmentShader() +{ + int attributeVertex; + int attributeTexture; + + /* + * Create the fragment shader, compile it, and add it to the shader + * program. The #define macros stored in fragmentShaderDefines_, if + * any, are prepended to the source code. + */ + fragmentShader_ = std::make_unique<QOpenGLShader>(QOpenGLShader::Fragment, this); + + QFile file(fragmentShaderFile_); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning() << "Shader" << fragmentShaderFile_ << "not found"; + return false; + } + + QString defines = fragmentShaderDefines_.join('\n') + "\n"; + QByteArray src = file.readAll(); + src.prepend(defines.toUtf8()); + + if (!fragmentShader_->compileSourceCode(src)) { + qWarning() << "[ViewFinderGL]:" << fragmentShader_->log(); + return false; + } + + shaderProgram_.addShader(fragmentShader_.get()); + + /* Link shader pipeline */ + if (!shaderProgram_.link()) { + qWarning() << "[ViewFinderGL]:" << shaderProgram_.log(); + close(); + } + + /* Bind shader pipeline for use */ + if (!shaderProgram_.bind()) { + qWarning() << "[ViewFinderGL]:" << shaderProgram_.log(); + close(); + } + + attributeVertex = shaderProgram_.attributeLocation("vertexIn"); + attributeTexture = shaderProgram_.attributeLocation("textureIn"); + + shaderProgram_.enableAttributeArray(attributeVertex); + shaderProgram_.setAttributeBuffer(attributeVertex, + GL_FLOAT, + 0, + 2, + 2 * sizeof(GLfloat)); + + shaderProgram_.enableAttributeArray(attributeTexture); + shaderProgram_.setAttributeBuffer(attributeTexture, + GL_FLOAT, + 8 * sizeof(GLfloat), + 2, + 2 * sizeof(GLfloat)); + + textureUniformY_ = shaderProgram_.uniformLocation("tex_y"); + textureUniformU_ = shaderProgram_.uniformLocation("tex_u"); + textureUniformV_ = shaderProgram_.uniformLocation("tex_v"); + textureUniformStep_ = shaderProgram_.uniformLocation("tex_step"); + textureUniformSize_ = shaderProgram_.uniformLocation("tex_size"); + textureUniformStrideFactor_ = shaderProgram_.uniformLocation("stride_factor"); + textureUniformBayerFirstRed_ = shaderProgram_.uniformLocation("tex_bayer_first_red"); + + /* Create the textures. */ + for (std::unique_ptr<QOpenGLTexture> &texture : textures_) { + if (texture) + continue; + + texture = std::make_unique<QOpenGLTexture>(QOpenGLTexture::Target2D); + texture->create(); + } + + return true; +} + +void ViewFinderGL::configureTexture(QOpenGLTexture &texture) +{ + glBindTexture(GL_TEXTURE_2D, texture.textureId()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, + textureMinMagFilters_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + textureMinMagFilters_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +} + +void ViewFinderGL::removeShader() +{ + if (shaderProgram_.isLinked()) { + shaderProgram_.release(); + shaderProgram_.removeAllShaders(); + } +} + +void ViewFinderGL::initializeGL() +{ + initializeOpenGLFunctions(); + glEnable(GL_TEXTURE_2D); + glDisable(GL_DEPTH_TEST); + + static const GLfloat coordinates[2][4][2]{ + { + /* Vertex coordinates */ + { -1.0f, -1.0f }, + { -1.0f, +1.0f }, + { +1.0f, +1.0f }, + { +1.0f, -1.0f }, + }, + { + /* Texture coordinates */ + { 0.0f, 1.0f }, + { 0.0f, 0.0f }, + { 1.0f, 0.0f }, + { 1.0f, 1.0f }, + }, + }; + + vertexBuffer_.create(); + vertexBuffer_.bind(); + vertexBuffer_.allocate(coordinates, sizeof(coordinates)); + + /* Create Vertex Shader */ + if (!createVertexShader()) + qWarning() << "[ViewFinderGL]: create vertex shader failed."; + + glClearColor(1.0f, 1.0f, 1.0f, 0.0f); +} + +void ViewFinderGL::doRender() +{ + /* Stride of the first plane, in pixels. */ + unsigned int stridePixels; + + switch (format_) { + case libcamera::formats::NV12: + case libcamera::formats::NV21: + case libcamera::formats::NV16: + case libcamera::formats::NV61: + case libcamera::formats::NV24: + case libcamera::formats::NV42: + /* Activate texture Y */ + glActiveTexture(GL_TEXTURE0); + configureTexture(*textures_[0]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_LUMINANCE, + stride_, + size_.height(), + 0, + GL_LUMINANCE, + GL_UNSIGNED_BYTE, + image_->data(0).data()); + shaderProgram_.setUniformValue(textureUniformY_, 0); + + /* Activate texture UV/VU */ + glActiveTexture(GL_TEXTURE1); + configureTexture(*textures_[1]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_LUMINANCE_ALPHA, + stride_ / horzSubSample_, + size_.height() / vertSubSample_, + 0, + GL_LUMINANCE_ALPHA, + GL_UNSIGNED_BYTE, + image_->data(1).data()); + shaderProgram_.setUniformValue(textureUniformU_, 1); + + stridePixels = stride_; + break; + + case libcamera::formats::YUV420: + /* Activate texture Y */ + glActiveTexture(GL_TEXTURE0); + configureTexture(*textures_[0]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_LUMINANCE, + stride_, + size_.height(), + 0, + GL_LUMINANCE, + GL_UNSIGNED_BYTE, + image_->data(0).data()); + shaderProgram_.setUniformValue(textureUniformY_, 0); + + /* Activate texture U */ + glActiveTexture(GL_TEXTURE1); + configureTexture(*textures_[1]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_LUMINANCE, + stride_ / horzSubSample_, + size_.height() / vertSubSample_, + 0, + GL_LUMINANCE, + GL_UNSIGNED_BYTE, + image_->data(1).data()); + shaderProgram_.setUniformValue(textureUniformU_, 1); + + /* Activate texture V */ + glActiveTexture(GL_TEXTURE2); + configureTexture(*textures_[2]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_LUMINANCE, + stride_ / horzSubSample_, + size_.height() / vertSubSample_, + 0, + GL_LUMINANCE, + GL_UNSIGNED_BYTE, + image_->data(2).data()); + shaderProgram_.setUniformValue(textureUniformV_, 2); + + stridePixels = stride_; + break; + + case libcamera::formats::YVU420: + /* Activate texture Y */ + glActiveTexture(GL_TEXTURE0); + configureTexture(*textures_[0]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_LUMINANCE, + stride_, + size_.height(), + 0, + GL_LUMINANCE, + GL_UNSIGNED_BYTE, + image_->data(0).data()); + shaderProgram_.setUniformValue(textureUniformY_, 0); + + /* Activate texture V */ + glActiveTexture(GL_TEXTURE2); + configureTexture(*textures_[2]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_LUMINANCE, + stride_ / horzSubSample_, + size_.height() / vertSubSample_, + 0, + GL_LUMINANCE, + GL_UNSIGNED_BYTE, + image_->data(1).data()); + shaderProgram_.setUniformValue(textureUniformV_, 2); + + /* Activate texture U */ + glActiveTexture(GL_TEXTURE1); + configureTexture(*textures_[1]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_LUMINANCE, + stride_ / horzSubSample_, + size_.height() / vertSubSample_, + 0, + GL_LUMINANCE, + GL_UNSIGNED_BYTE, + image_->data(2).data()); + shaderProgram_.setUniformValue(textureUniformU_, 1); + + stridePixels = stride_; + break; + + case libcamera::formats::UYVY: + case libcamera::formats::VYUY: + case libcamera::formats::YUYV: + case libcamera::formats::YVYU: + /* + * Packed YUV formats are stored in a RGBA texture to match the + * OpenGL texel size with the 4 bytes repeating pattern in YUV. + * The texture width is thus half of the image_ with. + */ + glActiveTexture(GL_TEXTURE0); + configureTexture(*textures_[0]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_RGBA, + stride_ / 4, + size_.height(), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + image_->data(0).data()); + shaderProgram_.setUniformValue(textureUniformY_, 0); + + /* + * The shader needs the step between two texture pixels in the + * horizontal direction, expressed in texture coordinate units + * ([0, 1]). There are exactly width - 1 steps between the + * leftmost and rightmost texels. + */ + shaderProgram_.setUniformValue(textureUniformStep_, + 1.0f / (size_.width() / 2 - 1), + 1.0f /* not used */); + + stridePixels = stride_ / 2; + break; + + case libcamera::formats::ABGR8888: + case libcamera::formats::ARGB8888: + case libcamera::formats::BGRA8888: + case libcamera::formats::RGBA8888: + glActiveTexture(GL_TEXTURE0); + configureTexture(*textures_[0]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_RGBA, + stride_ / 4, + size_.height(), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + image_->data(0).data()); + shaderProgram_.setUniformValue(textureUniformY_, 0); + + stridePixels = stride_ / 4; + break; + + case libcamera::formats::BGR888: + case libcamera::formats::RGB888: + glActiveTexture(GL_TEXTURE0); + configureTexture(*textures_[0]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_RGB, + stride_ / 3, + size_.height(), + 0, + GL_RGB, + GL_UNSIGNED_BYTE, + image_->data(0).data()); + shaderProgram_.setUniformValue(textureUniformY_, 0); + + stridePixels = stride_ / 3; + break; + + case libcamera::formats::SBGGR8: + case libcamera::formats::SGBRG8: + case libcamera::formats::SGRBG8: + case libcamera::formats::SRGGB8: + case libcamera::formats::SBGGR10_CSI2P: + case libcamera::formats::SGBRG10_CSI2P: + case libcamera::formats::SGRBG10_CSI2P: + case libcamera::formats::SRGGB10_CSI2P: + case libcamera::formats::SBGGR12_CSI2P: + case libcamera::formats::SGBRG12_CSI2P: + case libcamera::formats::SGRBG12_CSI2P: + case libcamera::formats::SRGGB12_CSI2P: + /* + * Raw Bayer 8-bit, and packed raw Bayer 10-bit/12-bit formats + * are stored in a GL_LUMINANCE texture. The texture width is + * equal to the stride. + */ + glActiveTexture(GL_TEXTURE0); + configureTexture(*textures_[0]); + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_LUMINANCE, + stride_, + size_.height(), + 0, + GL_LUMINANCE, + GL_UNSIGNED_BYTE, + image_->data(0).data()); + shaderProgram_.setUniformValue(textureUniformY_, 0); + shaderProgram_.setUniformValue(textureUniformBayerFirstRed_, + firstRed_); + shaderProgram_.setUniformValue(textureUniformSize_, + size_.width(), /* in pixels */ + size_.height()); + shaderProgram_.setUniformValue(textureUniformStep_, + 1.0f / (stride_ - 1), + 1.0f / (size_.height() - 1)); + + /* + * The stride is already taken into account in the shaders, set + * the generic stride factor to 1.0. + */ + stridePixels = size_.width(); + break; + + default: + stridePixels = size_.width(); + break; + }; + + /* + * Compute the stride factor for the vertex shader, to map the + * horizontal texture coordinate range [0.0, 1.0] to the active portion + * of the image. + */ + shaderProgram_.setUniformValue(textureUniformStrideFactor_, + static_cast<float>(size_.width() - 1) / + (stridePixels - 1)); +} + +void ViewFinderGL::paintGL() +{ + if (!fragmentShader_) + if (!createFragmentShader()) { + qWarning() << "[ViewFinderGL]:" + << "create fragment shader failed."; + } + + if (image_) { + glClearColor(0.0, 0.0, 0.0, 1.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + doRender(); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + } +} + +void ViewFinderGL::resizeGL(int w, int h) +{ + glViewport(0, 0, w, h); +} + +QSize ViewFinderGL::sizeHint() const +{ + return size_.isValid() ? size_ : QSize(640, 480); +} diff --git a/src/apps/qcam/viewfinder_gl.h b/src/apps/qcam/viewfinder_gl.h new file mode 100644 index 00000000..68c2912d --- /dev/null +++ b/src/apps/qcam/viewfinder_gl.h @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2020, Linaro + * + * viewfinder_GL.h - OpenGL Viewfinder for rendering by OpenGL shader + * + */ + +#pragma once + +#include <array> +#include <memory> + +#include <QImage> +#include <QMutex> +#include <QOpenGLBuffer> +#include <QOpenGLFunctions> +#include <QOpenGLShader> +#include <QOpenGLShaderProgram> +#include <QOpenGLTexture> +#include <QOpenGLWidget> +#include <QSize> + +#include <libcamera/formats.h> +#include <libcamera/framebuffer.h> + +#include "viewfinder.h" + +class ViewFinderGL : public QOpenGLWidget, + public ViewFinder, + protected QOpenGLFunctions +{ + Q_OBJECT + +public: + ViewFinderGL(QWidget *parent = nullptr); + ~ViewFinderGL(); + + const QList<libcamera::PixelFormat> &nativeFormats() const override; + + int setFormat(const libcamera::PixelFormat &format, const QSize &size, + const libcamera::ColorSpace &colorSpace, + unsigned int stride) override; + void render(libcamera::FrameBuffer *buffer, Image *image) override; + void stop() override; + + QImage getCurrentImage() override; + +Q_SIGNALS: + void renderComplete(libcamera::FrameBuffer *buffer); + +protected: + void initializeGL() override; + void paintGL() override; + void resizeGL(int w, int h) override; + QSize sizeHint() const override; + +private: + bool selectFormat(const libcamera::PixelFormat &format); + void selectColorSpace(const libcamera::ColorSpace &colorSpace); + + void configureTexture(QOpenGLTexture &texture); + bool createFragmentShader(); + bool createVertexShader(); + void removeShader(); + void doRender(); + + /* Captured image size, format and buffer */ + libcamera::FrameBuffer *buffer_; + libcamera::PixelFormat format_; + libcamera::ColorSpace colorSpace_; + QSize size_; + unsigned int stride_; + Image *image_; + + /* Shaders */ + QOpenGLShaderProgram shaderProgram_; + std::unique_ptr<QOpenGLShader> vertexShader_; + std::unique_ptr<QOpenGLShader> fragmentShader_; + QString vertexShaderFile_; + QString fragmentShaderFile_; + QStringList fragmentShaderDefines_; + + /* Vertex buffer */ + QOpenGLBuffer vertexBuffer_; + + /* Textures */ + std::array<std::unique_ptr<QOpenGLTexture>, 3> textures_; + + /* Common texture parameters */ + GLuint textureMinMagFilters_; + + /* YUV texture parameters */ + GLuint textureUniformU_; + GLuint textureUniformV_; + GLuint textureUniformY_; + GLuint textureUniformStep_; + unsigned int horzSubSample_; + unsigned int vertSubSample_; + + /* Raw Bayer texture parameters */ + GLuint textureUniformSize_; + GLuint textureUniformStrideFactor_; + GLuint textureUniformBayerFirstRed_; + QPointF firstRed_; + + QMutex mutex_; /* Prevent concurrent access to image_ */ +}; diff --git a/src/apps/qcam/viewfinder_qt.cpp b/src/apps/qcam/viewfinder_qt.cpp new file mode 100644 index 00000000..c20fd6bc --- /dev/null +++ b/src/apps/qcam/viewfinder_qt.cpp @@ -0,0 +1,181 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * viewfinder_qt.cpp - qcam - QPainter-based ViewFinder + */ + +#include "viewfinder_qt.h" + +#include <assert.h> +#include <stdint.h> +#include <utility> + +#include <libcamera/formats.h> + +#include <QImage> +#include <QImageWriter> +#include <QMap> +#include <QMutexLocker> +#include <QPainter> +#include <QtDebug> + +#include "../cam/image.h" + +#include "format_converter.h" + +static const QMap<libcamera::PixelFormat, QImage::Format> nativeFormats +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0) + { libcamera::formats::ABGR8888, QImage::Format_RGBX8888 }, + { libcamera::formats::XBGR8888, QImage::Format_RGBX8888 }, +#endif + { libcamera::formats::ARGB8888, QImage::Format_RGB32 }, + { libcamera::formats::XRGB8888, QImage::Format_RGB32 }, +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + { libcamera::formats::RGB888, QImage::Format_BGR888 }, +#endif + { libcamera::formats::BGR888, QImage::Format_RGB888 }, +}; + +ViewFinderQt::ViewFinderQt(QWidget *parent) + : QWidget(parent), buffer_(nullptr) +{ + icon_ = QIcon(":camera-off.svg"); +} + +ViewFinderQt::~ViewFinderQt() +{ +} + +const QList<libcamera::PixelFormat> &ViewFinderQt::nativeFormats() const +{ + static const QList<libcamera::PixelFormat> formats = ::nativeFormats.keys(); + return formats; +} + +int ViewFinderQt::setFormat(const libcamera::PixelFormat &format, const QSize &size, + [[maybe_unused]] const libcamera::ColorSpace &colorSpace, + unsigned int stride) +{ + image_ = QImage(); + + /* + * If format conversion is needed, configure the converter and allocate + * the destination image. + */ + if (!::nativeFormats.contains(format)) { + int ret = converter_.configure(format, size, stride); + if (ret < 0) + return ret; + + image_ = QImage(size, QImage::Format_RGB32); + + qInfo() << "Using software format conversion from" << format; + } else { + qInfo() << "Zero-copy enabled"; + } + + format_ = format; + size_ = size; + + updateGeometry(); + return 0; +} + +void ViewFinderQt::render(libcamera::FrameBuffer *buffer, Image *image) +{ + size_t size = buffer->metadata().planes()[0].bytesused; + + { + QMutexLocker locker(&mutex_); + + if (::nativeFormats.contains(format_)) { + /* + * If the frame format is identical to the display + * format, create a QImage that references the frame + * and store a reference to the frame buffer. The + * previously stored frame buffer, if any, will be + * released. + * + * \todo Get the stride from the buffer instead of + * computing it naively + */ + assert(buffer->planes().size() == 1); + image_ = QImage(image->data(0).data(), size_.width(), + size_.height(), size / size_.height(), + ::nativeFormats[format_]); + std::swap(buffer, buffer_); + } else { + /* + * Otherwise, convert the format and release the frame + * buffer immediately. + */ + converter_.convert(image, size, &image_); + } + } + + update(); + + if (buffer) + renderComplete(buffer); +} + +void ViewFinderQt::stop() +{ + image_ = QImage(); + + if (buffer_) { + renderComplete(buffer_); + buffer_ = nullptr; + } + + update(); +} + +QImage ViewFinderQt::getCurrentImage() +{ + QMutexLocker locker(&mutex_); + + return image_.copy(); +} + +void ViewFinderQt::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + + /* If we have an image, draw it. */ + if (!image_.isNull()) { + painter.drawImage(rect(), image_, image_.rect()); + return; + } + + /* + * Otherwise, draw the camera stopped icon. Render it to the pixmap if + * the size has changed. + */ + constexpr int margin = 20; + + if (vfSize_ != size() || pixmap_.isNull()) { + QSize vfSize = size() - QSize{ 2 * margin, 2 * margin }; + QSize pixmapSize{ 1, 1 }; + pixmapSize.scale(vfSize, Qt::KeepAspectRatio); + pixmap_ = icon_.pixmap(pixmapSize); + + vfSize_ = size(); + } + + QPoint point{ margin, margin }; + if (pixmap_.width() < width() - 2 * margin) + point.setX((width() - pixmap_.width()) / 2); + else + point.setY((height() - pixmap_.height()) / 2); + + painter.setBackgroundMode(Qt::OpaqueMode); + painter.drawPixmap(point, pixmap_); +} + +QSize ViewFinderQt::sizeHint() const +{ + return size_.isValid() ? size_ : QSize(640, 480); +} diff --git a/src/apps/qcam/viewfinder_qt.h b/src/apps/qcam/viewfinder_qt.h new file mode 100644 index 00000000..eb3a9988 --- /dev/null +++ b/src/apps/qcam/viewfinder_qt.h @@ -0,0 +1,64 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * viewfinder_qt.h - qcam - QPainter-based ViewFinder + */ + +#pragma once + +#include <QIcon> +#include <QImage> +#include <QList> +#include <QMutex> +#include <QSize> +#include <QWidget> + +#include <libcamera/formats.h> +#include <libcamera/framebuffer.h> +#include <libcamera/pixel_format.h> + +#include "format_converter.h" +#include "viewfinder.h" + +class ViewFinderQt : public QWidget, public ViewFinder +{ + Q_OBJECT + +public: + ViewFinderQt(QWidget *parent); + ~ViewFinderQt(); + + const QList<libcamera::PixelFormat> &nativeFormats() const override; + + int setFormat(const libcamera::PixelFormat &format, const QSize &size, + const libcamera::ColorSpace &colorSpace, + unsigned int stride) override; + void render(libcamera::FrameBuffer *buffer, Image *image) override; + void stop() override; + + QImage getCurrentImage() override; + +Q_SIGNALS: + void renderComplete(libcamera::FrameBuffer *buffer); + +protected: + void paintEvent(QPaintEvent *) override; + QSize sizeHint() const override; + +private: + FormatConverter converter_; + + libcamera::PixelFormat format_; + QSize size_; + + /* Camera stopped icon */ + QSize vfSize_; + QIcon icon_; + QPixmap pixmap_; + + /* Buffer and render image */ + libcamera::FrameBuffer *buffer_; + QImage image_; + QMutex mutex_; /* Prevent concurrent access to image_ */ +}; |