#include "Document/EditorSession.h" #include "Document/DocumentLimits.h" #include "Document/ProjectWorkspace.h" #include "Document/PixelAdjust.h" #include "IO/PSD/PSDReader.h" #include "IO/RawImporter.h" #include "Logging.h" #include bool EditorSession::canStartProjectOperation() const { return !m_selectionAmountOperation && !m_textDraft && !m_isProjectBusy && !m_isImporting && !m_brushStroke && !m_warpStroke && !m_showsNewDocument && !m_showsImporter && !m_renamingLayerID && !m_importError && !m_levels && !m_adjustmentEditingID; } void EditorSession::waitForFileRequest(std::function ready) { if (canStartProjectOperation()) ready(); else m_fileRequestWaiters.push_back(std::move(ready)); } void EditorSession::resumeFileRequests() { if (!canStartProjectOperation()) return; // Each waiter looks again when its turn comes. for (const std::function &waiter : std::exchange(m_fileRequestWaiters, {})) QMetaObject::invokeMethod(this, [this, waiter] { waitForFileRequest(waiter); }, Qt::QueuedConnection); } void EditorSession::waitForProjectAccess(std::function ready) { if (!m_isProjectBusy) ready(); else m_projectWaiters.push_back(std::move(ready)); } void EditorSession::updateBusyIndicator() { // Quick operations never dim the controls. if (m_isProjectBusy) { if (!m_busyTimer.isActive() && !m_showsBusy) m_busyTimer.start(); } else { m_busyTimer.stop(); m_showsBusy = false; } } void EditorSession::importImages(const QList &urls, std::optional point, std::function done) { if (urls.isEmpty()) { if (done) done(); return; } if (m_brushStroke) finishBrush(); cancelCrop(); commitTransform(); waitForProjectAccess([this, urls, point, done] { m_pendingImports.push_back({urls, point, done}); if (!m_isImporting) { setIsImporting(true); drainImports(); } }); } void EditorSession::drainImports() { if (m_pendingImports.empty()) { setIsImporting(false); if (!m_importFailures.isEmpty()) setImportError(std::exchange(m_importFailures, {}).join("\n\n")); return; } const QList &urls = m_pendingImports.front().urls; const bool photoshop = std::all_of(urls.begin(), urls.end(), [](const QUrl &url) { return url.isLocalFile() && PSDReader::matches(url.toLocalFile()); }); beginEdit(photoshop ? QStringLiteral("Import Photoshop File") : QStringLiteral("Import Images")); // Without a canvas the first image makes it. m_importPoint = m_document ? m_pendingImports.front().point : std::nullopt; m_importIndex = 0; decodeNext(); } void EditorSession::decodeNext() { const ImportRequest &request = m_pendingImports.front(); if (m_importIndex == request.urls.size()) { endEdit(); qCInfo(lcApp) << "imported a request of" << request.urls.size() << "files"; // The caller goes on once the queue's state is settled. if (request.done) QMetaObject::invokeMethod(this, request.done, Qt::QueuedConnection); m_pendingImports.pop_front(); drainImports(); return; } qint64 usedPixels = 0; if (m_document) { for (const ImageLayer &layer : m_document->layers) { if (layer.asset) usedPixels += qint64(layer.asset->size().width()) * layer.asset->size().height(); } } const QUrl url = request.urls[m_importIndex]; if (url.isLocalFile() && RawImporter::matches(url.toLocalFile())) { decodeRaw(url.toLocalFile(), DocumentLimits::documentPixelBudget() - usedPixels); return; } if (url.isLocalFile() && PSDReader::matches(url.toLocalFile())) { beginPSDReading(QStringLiteral("Open “%1”?").arg(url.fileName()), QStringLiteral("Import")); m_photoshopReader.setFuture(QtConcurrent::run([path = url.toLocalFile(), remaining = DocumentLimits::documentPixelBudget() - usedPixels]() -> PhotoshopRead { try { PSDDocument document = ImageImporter::loadPhotoshop(path, remaining); std::map assets = ImageImporter::photoshopAssets(document); return {std::move(document), std::move(assets), QString()}; } catch (const ImageImportError &error) { return {std::nullopt, {}, QString::fromUtf8(error.what())}; } catch (const PSDError &error) { return {std::nullopt, {}, QString::fromUtf8(error.what())}; } catch (const ExportError &error) { return {std::nullopt, {}, QString::fromUtf8(error.what())}; } })); return; } m_decoder.setFuture(QtConcurrent::run([url, remaining = DocumentLimits::documentPixelBudget() - usedPixels]() -> Decoded { try { if (!url.isLocalFile()) throw ImageImportError(ImageImportError::Kind::unsupported); return {ImageImporter::decode(url.toLocalFile(), remaining), QString()}; } catch (const ImageImportError &error) { return {std::nullopt, url.fileName() + ": " + error.what()}; } })); } void EditorSession::decodeRaw(const QString &path, qint64 remaining) { const QString name = QFileInfo(path).fileName(); const std::optional size = RawImporter::pixelSize(path); const auto refuse = [&](ImageImportError::Kind kind) { m_importFailures << name + ": " + ImageImportError(kind).what(); m_importIndex += 1; decodeNext(); }; if (!size) return refuse(ImageImportError::Kind::unreadable); if (size->width() > DocumentLimits::maxSide || size->height() > DocumentLimits::maxSide || qint64(size->width()) * size->height() > remaining) return refuse(ImageImportError::Kind::tooLarge); developRaw(path, [this, path, name](std::optional settings) { if (!settings) { m_importIndex += 1; decodeNext(); return; } // Seconds of work: a worker, so the window keeps drawing. m_decoder.setFuture(QtConcurrent::run([path, name, settings = *settings]() -> Decoded { try { const QImage developed = RawImporter::Queue::shared().develop(path, settings, std::nullopt); return {ImportedImage(developed, PixelAdjust::thumbnail(developed), ProjectTab::nameWithoutSuffix(path)), QString()}; } catch (const ImageImportError &error) { return {std::nullopt, name + ": " + error.what()}; } catch (const ExportError &error) { return {std::nullopt, name + ": " + error.what()}; } })); }); } void EditorSession::developRaw(const QString &path, std::function)> answer) { m_rawAnswer = std::move(answer); m_rawDevelop = RawDevelopRequest{path, RawImporter::asShot(path).value_or(RawDevelopSettings())}; notify(); } void EditorSession::finishRawDevelop(std::optional settings) { m_rawDevelop = std::nullopt; notify(); // Frees the preview's decode on a worker, behind any preview. (void)QtConcurrent::run([] { RawImporter::Queue::shared().release(); }); // The import resumes from the event loop, as a task. if (const auto answer = std::exchange(m_rawAnswer, {})) QMetaObject::invokeMethod(this, [answer, settings] { answer(settings); }, Qt::QueuedConnection); } void EditorSession::finishDecode() { const Decoded decoded = m_decoder.result(); if (decoded.asset) insert(*decoded.asset, m_importPoint); else m_importFailures << decoded.failure; m_importIndex += 1; decodeNext(); } void EditorSession::finishPhotoshopRead() { const PhotoshopRead read = m_photoshopReader.result(); const QUrl url = m_pendingImports.front().urls[m_importIndex]; const auto next = [this] { m_importIndex += 1; decodeNext(); }; // Every image has its asset: making layers cannot fail. const std::optional imported = read.document ? std::optional(PSDDocumentBuilder::makeImport(*read.document, read.assets)) : std::nullopt; if (!imported) { endPSDReading(); m_importFailures << url.fileName() + ": " + read.failure; next(); return; } const std::vector conversions = imported->conversions; finishPSDReading(conversions, [this, imported = std::move(*imported), url, next](bool confirmed) { if (confirmed) { try { insertPhotoshop(imported, ProjectTab::nameWithoutSuffix(url.toLocalFile()), m_importPoint); } catch (const std::runtime_error &error) { m_importFailures << url.fileName() + ": " + QString::fromUtf8(error.what()); } } next(); }); } void EditorSession::beginPSDReading(const QString &title, const QString &confirmTitle) { m_conversionCancelled = false; m_conversionRequest = PSDConversionRequest{title, confirmTitle, {}, true}; m_showsConversionSheet = true; notify(); } void EditorSession::finishPSDReading(const std::vector &conversions, std::function answer) { if (m_conversionCancelled || conversions.empty()) { const bool confirmed = !m_conversionCancelled; endPSDReading(); answer(confirmed); return; } // The sheet waits for Cancel or the confirm button. m_conversionAnswer = std::move(answer); m_conversionRequest->conversions = conversions; m_conversionRequest->isReading = false; notify(); } void EditorSession::endPSDReading() { if (m_conversionAnswer) return; m_showsConversionSheet = false; m_conversionRequest = std::nullopt; notify(); } void EditorSession::finishConversion(bool confirmed) { if (!confirmed && m_conversionRequest && m_conversionRequest->isReading) m_conversionCancelled = true; m_showsConversionSheet = false; m_conversionRequest = std::nullopt; notify(); // The import resumes from the event loop, as a task. if (const std::function answer = std::exchange(m_conversionAnswer, {})) QMetaObject::invokeMethod(this, [answer, confirmed] { answer(confirmed); }, Qt::QueuedConnection); } void EditorSession::insertPhotoshop(const PSDImport &imported, const QString &named, std::optional centeredAt) { std::vector incoming = imported.layers; const size_t existing = m_document ? m_document->layers.size() : 0; if (existing + incoming.size() + (m_document ? 1 : 0) > 10'000) throw ImageImportError(ImageImportError::Kind::tooLarge); std::vector layers = m_document ? m_document->layers : std::vector(); std::optional group; if (m_document) { // An open canvas takes the file as one folder. group = ImageLayer(named, m_document->size()); group->isGroup = true; const std::optional active = activeLayer(); group->parentID = active && active->isGroup ? m_activeLayerID : active ? active->parentID : std::nullopt; std::optional box; for (const ImageLayer &layer : incoming) if (!layer.isGroup) box = box ? box->united(QRectF(layer.origin(), layer.size())) : QRectF(layer.origin(), layer.size()); if (centeredAt && box) { const QPointF shift = *centeredAt - box->center(); for (ImageLayer &layer : incoming) layer.transform.origin += shift; } for (ImageLayer &layer : incoming) if (!layer.parentID) layer.parentID = group->id; layers.push_back(*group); } layers.insert(layers.end(), incoming.begin(), incoming.end()); // Folders nested past Swift's limit refuse the file. std::vector records; for (const ImageLayer &layer : layers) records.push_back(layer.hierarchyRecord()); try { LayerHierarchy::validate(records); } catch (const ProjectError &) { qCWarning(lcIO) << "a Photoshop file nests its folders too deep"; throw PSDError(PSDError::Kind::truncated); } beginEdit(QStringLiteral("Import Photoshop File")); if (!group) { CanvasDocument document(imported.width, imported.height); document.layers = std::move(layers); document.resolution = imported.resolution; m_document = std::move(document); viewport.fit(m_document->size()); // The topmost root layer, else the topmost layer. std::optional active = incoming.empty() ? std::nullopt : std::optional(incoming.back().id); for (size_t index = incoming.size(); index-- > 0;) { if (!incoming[index].parentID) { active = incoming[index].id; break; } } setActiveLayerID(active); } else { m_document->layers = std::move(layers); if (group->parentID) m_collapsedGroupIDs.remove(*group->parentID); setActiveLayerID(group->id); } endEdit(); qCInfo(lcIO) << "imported a Photoshop file of" << incoming.size() << "layers"; }