overte-lubosz/interface/src/ui/Snapshot.cpp
Oren Hurvitz d8ea47b78f Don't call parseSnapshotData() in a static manner.
Shouldn't call non-static methods in a static manner. (Some compilers don't even allow this, although apparently Visual Studio 2017 does.)
2018-06-17 14:18:25 +03:00

483 lines
19 KiB
C++

//
// Snapshot.cpp
// interface/src/ui
//
// Created by Stojce Slavkovski on 1/26/14.
// Copyright 2014 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "Snapshot.h"
#include <QtCore/QDateTime>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QTemporaryFile>
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonArray>
#include <QtNetwork/QHttpMultiPart>
#include <QPainter>
#include <QtConcurrent/QtConcurrentRun>
#include <AccountManager.h>
#include <AddressManager.h>
#include <avatar/AvatarManager.h>
#include <avatar/MyAvatar.h>
#include <shared/FileUtils.h>
#include <NodeList.h>
#include <OffscreenUi.h>
#include <SharedUtil.h>
#include <SecondaryCamera.h>
#include <plugins/DisplayPlugin.h>
#include "Application.h"
#include "display-plugins/CompositorHelper.h"
#include "scripting/WindowScriptingInterface.h"
#include "MainWindow.h"
#include "Snapshot.h"
#include "SnapshotUploader.h"
#include "ToneMappingEffect.h"
// filename format: hifi-snap-by-%username%-on-%date%_%time%_@-%location%.jpg
// %1 <= username, %2 <= date and time, %3 <= current location
const QString FILENAME_PATH_FORMAT = "hifi-snap-by-%1-on-%2.jpg";
const QString DATETIME_FORMAT = "yyyy-MM-dd_hh-mm-ss";
const QString SNAPSHOTS_DIRECTORY = "Snapshots";
const QString URL = "highfidelity_url";
static const int SNAPSHOT_360_TIMER_INTERVAL = 350;
Snapshot::Snapshot() {
_snapshotTimer.setSingleShot(false);
_snapshotTimer.setTimerType(Qt::PreciseTimer);
_snapshotTimer.setInterval(SNAPSHOT_360_TIMER_INTERVAL);
connect(&_snapshotTimer, &QTimer::timeout, this, &Snapshot::takeNextSnapshot);
_snapshotIndex = 0;
_oldEnabled = false;
_oldAttachedEntityId = 0;
_oldOrientation = 0;
_oldvFoV = 0;
_oldNearClipPlaneDistance = 0;
_oldFarClipPlaneDistance = 0;
}
SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) {
if (!QFile(snapshotPath).exists()) {
return NULL;
}
QUrl url;
if (snapshotPath.right(3) == "jpg") {
QImage shot(snapshotPath);
// no location data stored
if (shot.text(URL).isEmpty()) {
return NULL;
}
// parsing URL
url = QUrl(shot.text(URL), QUrl::ParsingMode::StrictMode);
} else {
return NULL;
}
SnapshotMetaData* data = new SnapshotMetaData();
data->setURL(url);
return data;
}
QString Snapshot::saveSnapshot(QImage image, const QString& filename, const QString& pathname) {
QFile* snapshotFile = savedFileForSnapshot(image, false, filename, pathname);
if (snapshotFile) {
// we don't need the snapshot file, so close it, grab its filename and delete it
snapshotFile->close();
QString snapshotPath = QFileInfo(*snapshotFile).absoluteFilePath();
delete snapshotFile;
return snapshotPath;
}
return "";
}
static const float CUBEMAP_SIDE_PIXEL_DIMENSION = 2048.0f;
static const float SNAPSHOT_360_FOV = 90.0f;
static const float SNAPSHOT_360_NEARCLIP = 0.3f;
static const float SNAPSHOT_360_FARCLIP = 16384.0f;
static const glm::quat CAMERA_ORIENTATION_DOWN(glm::quat(glm::radians(glm::vec3(-90.0f, 0.0f, 0.0f))));
static const glm::quat CAMERA_ORIENTATION_FRONT(glm::quat(glm::radians(glm::vec3(0.0f, 0.0f, 0.0f))));
static const glm::quat CAMERA_ORIENTATION_LEFT(glm::quat(glm::radians(glm::vec3(0.0f, 90.0f, 0.0f))));
static const glm::quat CAMERA_ORIENTATION_BACK(glm::quat(glm::radians(glm::vec3(0.0f, 180.0f, 0.0f))));
static const glm::quat CAMERA_ORIENTATION_RIGHT(glm::quat(glm::radians(glm::vec3(0.0f, 270.0f, 0.0f))));
static const glm::quat CAMERA_ORIENTATION_UP(glm::quat(glm::radians(glm::vec3(90.0f, 0.0f, 0.0f))));
void Snapshot::save360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) {
_snapshotFilename = filename;
_notify360 = notify;
_cubemapOutputFormat = cubemapOutputFormat;
SecondaryCameraJobConfig* secondaryCameraRenderConfig = static_cast<SecondaryCameraJobConfig*>(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera"));
// Save initial values of secondary camera render config
_oldEnabled = secondaryCameraRenderConfig->isEnabled();
_oldAttachedEntityId = secondaryCameraRenderConfig->property("attachedEntityId");
_oldOrientation = secondaryCameraRenderConfig->property("orientation");
_oldvFoV = secondaryCameraRenderConfig->property("vFoV");
_oldNearClipPlaneDistance = secondaryCameraRenderConfig->property("nearClipPlaneDistance");
_oldFarClipPlaneDistance = secondaryCameraRenderConfig->property("farClipPlaneDistance");
if (!_oldEnabled) {
secondaryCameraRenderConfig->enableSecondaryCameraRenderConfigs(true);
}
// Initialize some secondary camera render config options for 360 snapshot capture
static_cast<ToneMappingConfig*>(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCameraJob.ToneMapping"))->setCurve(0);
secondaryCameraRenderConfig->resetSizeSpectatorCamera(static_cast<int>(CUBEMAP_SIDE_PIXEL_DIMENSION), static_cast<int>(CUBEMAP_SIDE_PIXEL_DIMENSION));
secondaryCameraRenderConfig->setProperty("attachedEntityId", "");
secondaryCameraRenderConfig->setPosition(cameraPosition);
secondaryCameraRenderConfig->setProperty("vFoV", SNAPSHOT_360_FOV);
secondaryCameraRenderConfig->setProperty("nearClipPlaneDistance", SNAPSHOT_360_NEARCLIP);
secondaryCameraRenderConfig->setProperty("farClipPlaneDistance", SNAPSHOT_360_FARCLIP);
// Setup for Down Image capture
secondaryCameraRenderConfig->setOrientation(CAMERA_ORIENTATION_DOWN);
_snapshotIndex = 0;
_snapshotTimer.start(SNAPSHOT_360_TIMER_INTERVAL);
}
void Snapshot::takeNextSnapshot() {
SecondaryCameraJobConfig* config = static_cast<SecondaryCameraJobConfig*>(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera"));
// Order is:
// 0. Down
// 1. Front
// 2. Left
// 3. Back
// 4. Right
// 5. Up
if (_snapshotIndex < 6) {
_imageArray[_snapshotIndex] = qApp->getActiveDisplayPlugin()->getSecondaryCameraScreenshot();
}
if (_snapshotIndex == 0) {
// Setup for Front Image capture
config->setOrientation(CAMERA_ORIENTATION_FRONT);
} else if (_snapshotIndex == 1) {
// Setup for Left Image capture
config->setOrientation(CAMERA_ORIENTATION_LEFT);
} else if (_snapshotIndex == 2) {
// Setup for Back Image capture
config->setOrientation(CAMERA_ORIENTATION_BACK);
} else if (_snapshotIndex == 3) {
// Setup for Right Image capture
config->setOrientation(CAMERA_ORIENTATION_RIGHT);
} else if (_snapshotIndex == 4) {
// Setup for Up Image capture
config->setOrientation(CAMERA_ORIENTATION_UP);
} else if (_snapshotIndex > 5) {
_snapshotTimer.stop();
// Reset secondary camera render config
static_cast<ToneMappingConfig*>(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCameraJob.ToneMapping"))->setCurve(1);
config->resetSizeSpectatorCamera(qApp->getWindow()->geometry().width(), qApp->getWindow()->geometry().height());
config->setProperty("attachedEntityId", _oldAttachedEntityId);
config->setProperty("vFoV", _oldvFoV);
config->setProperty("nearClipPlaneDistance", _oldNearClipPlaneDistance);
config->setProperty("farClipPlaneDistance", _oldFarClipPlaneDistance);
if (!_oldEnabled) {
config->enableSecondaryCameraRenderConfigs(false);
}
// Process six QImages
if (_cubemapOutputFormat) {
QtConcurrent::run([this]() { convertToCubemap(); });
} else {
QtConcurrent::run([this]() { convertToEquirectangular(); });
}
}
_snapshotIndex++;
}
void Snapshot::convertToCubemap() {
float outputImageHeight = CUBEMAP_SIDE_PIXEL_DIMENSION * 3.0f;
float outputImageWidth = CUBEMAP_SIDE_PIXEL_DIMENSION * 4.0f;
QImage outputImage(outputImageWidth, outputImageHeight, QImage::Format_RGB32);
QPainter painter(&outputImage);
QPoint destPos;
// Paint DownImage
destPos = QPoint(CUBEMAP_SIDE_PIXEL_DIMENSION, CUBEMAP_SIDE_PIXEL_DIMENSION * 2.0f);
painter.drawImage(destPos, _imageArray[0]);
// Paint FrontImage
destPos = QPoint(CUBEMAP_SIDE_PIXEL_DIMENSION, CUBEMAP_SIDE_PIXEL_DIMENSION);
painter.drawImage(destPos, _imageArray[1]);
// Paint LeftImage
destPos = QPoint(0, CUBEMAP_SIDE_PIXEL_DIMENSION);
painter.drawImage(destPos, _imageArray[2]);
// Paint BackImage
destPos = QPoint(CUBEMAP_SIDE_PIXEL_DIMENSION * 3.0f, CUBEMAP_SIDE_PIXEL_DIMENSION);
painter.drawImage(destPos, _imageArray[3]);
// Paint RightImage
destPos = QPoint(CUBEMAP_SIDE_PIXEL_DIMENSION * 2.0f, CUBEMAP_SIDE_PIXEL_DIMENSION);
painter.drawImage(destPos, _imageArray[4]);
// Paint UpImage
destPos = QPoint(CUBEMAP_SIDE_PIXEL_DIMENSION, 0);
painter.drawImage(destPos, _imageArray[5]);
painter.end();
emit DependencyManager::get<WindowScriptingInterface>()->snapshot360Taken(saveSnapshot(outputImage, _snapshotFilename),
_notify360);
}
void Snapshot::convertToEquirectangular() {
// I got help from StackOverflow while writing this code:
// https://stackoverflow.com/questions/34250742/converting-a-cubemap-into-equirectangular-panorama
int cubeFaceWidth = static_cast<int>(CUBEMAP_SIDE_PIXEL_DIMENSION);
int cubeFaceHeight = static_cast<int>(CUBEMAP_SIDE_PIXEL_DIMENSION);
float outputImageHeight = CUBEMAP_SIDE_PIXEL_DIMENSION * 2.0f;
float outputImageWidth = outputImageHeight * 2.0f;
QImage outputImage(outputImageWidth, outputImageHeight, QImage::Format_RGB32);
outputImage.fill(0);
QRgb sourceColorValue;
float phi, theta;
for (int j = 0; j < outputImageHeight; j++) {
theta = (1.0f - ((float)j / outputImageHeight)) * PI;
for (int i = 0; i < outputImageWidth; i++) {
phi = ((float)i / outputImageWidth) * 2.0f * PI;
float x = glm::sin(phi) * glm::sin(theta) * -1.0f;
float y = glm::cos(theta);
float z = glm::cos(phi) * glm::sin(theta) * -1.0f;
float a = std::max(std::max(std::abs(x), std::abs(y)), std::abs(z));
float xa = x / a;
float ya = y / a;
float za = z / a;
// Pixel in the source images
int xPixel, yPixel;
QImage sourceImage;
if (xa == 1) {
// Right image
xPixel = (int)((((za + 1.0f) / 2.0f) - 1.0f) * cubeFaceWidth);
yPixel = (int)((((ya + 1.0f) / 2.0f)) * cubeFaceHeight);
sourceImage = _imageArray[4];
} else if (xa == -1) {
// Left image
xPixel = (int)((((za + 1.0f) / 2.0f)) * cubeFaceWidth);
yPixel = (int)((((ya + 1.0f) / 2.0f)) * cubeFaceHeight);
sourceImage = _imageArray[2];
} else if (ya == 1) {
// Down image
xPixel = (int)((((xa + 1.0f) / 2.0f)) * cubeFaceWidth);
yPixel = (int)((((za + 1.0f) / 2.0f) - 1.0f) * cubeFaceHeight);
sourceImage = _imageArray[0];
} else if (ya == -1) {
// Up image
xPixel = (int)((((xa + 1.0f) / 2.0f)) * cubeFaceWidth);
yPixel = (int)((((za + 1.0f) / 2.0f)) * cubeFaceHeight);
sourceImage = _imageArray[5];
} else if (za == 1) {
// Front image
xPixel = (int)((((xa + 1.0f) / 2.0f)) * cubeFaceWidth);
yPixel = (int)((((ya + 1.0f) / 2.0f)) * cubeFaceHeight);
sourceImage = _imageArray[1];
} else if (za == -1) {
// Back image
xPixel = (int)((((xa + 1.0f) / 2.0f) - 1.0f) * cubeFaceWidth);
yPixel = (int)((((ya + 1.0f) / 2.0f)) * cubeFaceHeight);
sourceImage = _imageArray[3];
} else {
qDebug() << "Unknown face encountered when processing 360 Snapshot";
xPixel = 0;
yPixel = 0;
}
xPixel = std::min(std::abs(xPixel), 2047);
yPixel = std::min(std::abs(yPixel), 2047);
sourceColorValue = sourceImage.pixel(xPixel, yPixel);
outputImage.setPixel(i, j, sourceColorValue);
}
}
emit DependencyManager::get<WindowScriptingInterface>()->snapshot360Taken(saveSnapshot(outputImage, _snapshotFilename),
_notify360);
}
QTemporaryFile* Snapshot::saveTempSnapshot(QImage image) {
// return whatever we get back from saved file for snapshot
return static_cast<QTemporaryFile*>(savedFileForSnapshot(image, true));
}
QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary, const QString& userSelectedFilename, const QString& userSelectedPathname) {
// adding URL to snapshot
QUrl currentURL = DependencyManager::get<AddressManager>()->currentPublicAddress();
shot.setText(URL, currentURL.toString());
QString username = DependencyManager::get<AccountManager>()->getAccountInfo().getUsername();
// normalize username, replace all non alphanumeric with '-'
username.replace(QRegExp("[^A-Za-z0-9_]"), "-");
QDateTime now = QDateTime::currentDateTime();
// If user has requested specific filename then use it, else create the filename
// 'jpg" is appended, as the image is saved in jpg format. This is the case for all snapshots
// (see definition of FILENAME_PATH_FORMAT)
QString filename;
if (!userSelectedFilename.isNull()) {
filename = userSelectedFilename + ".jpg";
} else {
filename = FILENAME_PATH_FORMAT.arg(username, now.toString(DATETIME_FORMAT));
}
const int IMAGE_QUALITY = 100;
if (!isTemporary) {
// If user has requested specific path then use it, else use the application value
QString snapshotFullPath;
if (!userSelectedPathname.isNull()) {
snapshotFullPath = userSelectedPathname;
} else {
snapshotFullPath = _snapshotsLocation.get();
}
if (snapshotFullPath.isEmpty()) {
snapshotFullPath = OffscreenUi::getExistingDirectory(nullptr, "Choose Snapshots Directory", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation));
_snapshotsLocation.set(snapshotFullPath);
}
if (!snapshotFullPath.isEmpty()) { // not cancelled
if (!snapshotFullPath.endsWith(QDir::separator())) {
snapshotFullPath.append(QDir::separator());
}
snapshotFullPath.append(filename);
QFile* imageFile = new QFile(snapshotFullPath);
while (!imageFile->open(QIODevice::WriteOnly)) {
// It'd be better for the directory chooser to restore the cursor to its previous state
// after choosing a directory, but if the user has entered this codepath,
// something terrible has happened. Let's just show the user their cursor so they can get
// out of this awful state.
qApp->getApplicationCompositor().getReticleInterface()->setVisible(true);
qApp->getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(true);
snapshotFullPath = OffscreenUi::getExistingDirectory(nullptr, "Write Error - Choose New Snapshots Directory", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation));
if (snapshotFullPath.isEmpty()) {
return NULL;
}
_snapshotsLocation.set(snapshotFullPath);
if (!snapshotFullPath.endsWith(QDir::separator())) {
snapshotFullPath.append(QDir::separator());
}
snapshotFullPath.append(filename);
imageFile = new QFile(snapshotFullPath);
}
shot.save(imageFile, 0, IMAGE_QUALITY);
imageFile->close();
return imageFile;
}
}
// Either we were asked for a tempororary, or the user didn't set a directory.
QTemporaryFile* imageTempFile = new QTemporaryFile(QDir::tempPath() + "/XXXXXX-" + filename);
if (!imageTempFile->open()) {
qDebug() << "Unable to open QTemporaryFile for temp snapshot. Will not save.";
return NULL;
}
imageTempFile->setAutoRemove(isTemporary);
shot.save(imageTempFile, 0, IMAGE_QUALITY);
imageTempFile->close();
return imageTempFile;
}
void Snapshot::uploadSnapshot(const QString& filename, const QUrl& href) {
const QString SNAPSHOT_UPLOAD_URL = "/api/v1/snapshots";
QUrl url = href;
if (url.isEmpty()) {
SnapshotMetaData* snapshotData = parseSnapshotData(filename);
if (snapshotData) {
url = snapshotData->getURL();
}
delete snapshotData;
}
if (url.isEmpty()) {
url = QUrl(DependencyManager::get<AddressManager>()->currentShareableAddress());
}
SnapshotUploader* uploader = new SnapshotUploader(url, filename);
QFile* file = new QFile(filename);
Q_ASSERT(file->exists());
file->open(QIODevice::ReadOnly);
QHttpPart imagePart;
if (filename.right(3) == "gif") {
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/gif"));
} else {
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg"));
}
imagePart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant("form-data; name=\"image\"; filename=\"" + file->fileName() + "\""));
imagePart.setBodyDevice(file);
QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
file->setParent(multiPart); // we cannot delete the file now, so delete it with the multiPart
multiPart->append(imagePart);
auto accountManager = DependencyManager::get<AccountManager>();
JSONCallbackParameters callbackParams(uploader, "uploadSuccess", uploader, "uploadFailure");
accountManager->sendRequest(SNAPSHOT_UPLOAD_URL,
AccountManagerAuth::Required,
QNetworkAccessManager::PostOperation,
callbackParams,
nullptr,
multiPart);
}
QString Snapshot::getSnapshotsLocation() {
return _snapshotsLocation.get("");
}
void Snapshot::setSnapshotsLocation(const QString& location) {
_snapshotsLocation.set(location);
}