mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-21 00:23:56 +08:00
237 lines
6.0 KiB
C++
237 lines
6.0 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "editor/photo_editor_common.h"
|
|
|
|
#include "editor/scene/scene.h"
|
|
#include "editor/scene/scene_item_animated.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/userpic_view.h"
|
|
|
|
namespace Editor {
|
|
namespace {
|
|
|
|
constexpr auto kAnimatedMaxSide = 854;
|
|
constexpr auto kAnimatedFps = 30.;
|
|
constexpr auto kAnimatedMinDuration = crl::time(1000);
|
|
constexpr auto kAnimatedMaxDuration = crl::time(3000);
|
|
|
|
} // namespace
|
|
|
|
void ApplyShapeMask(QImage &image, const PhotoModifications &mods) {
|
|
if (mods.cropMode != EditorData::CropMode::Mask) {
|
|
return;
|
|
}
|
|
const auto type = mods.cropType;
|
|
if (type == EditorData::CropType::Rect) {
|
|
return;
|
|
}
|
|
const auto multiplier = (type == EditorData::CropType::RoundedRect)
|
|
? RoundedCornersMultiplier(mods.cornersLevel)
|
|
: Ui::ForumUserpicRadiusMultiplier();
|
|
if (type == EditorData::CropType::RoundedRect && multiplier <= 0.) {
|
|
return;
|
|
}
|
|
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
|
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
|
}
|
|
auto mask = QImage(image.size(), QImage::Format_ARGB32_Premultiplied);
|
|
mask.fill(Qt::transparent);
|
|
{
|
|
auto p = QPainter(&mask);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(Qt::white);
|
|
const auto rect = QRectF(QPointF(), QSizeF(image.size()));
|
|
if (type == EditorData::CropType::Ellipse) {
|
|
p.drawEllipse(rect);
|
|
} else {
|
|
const auto radius = std::min(rect.width(), rect.height())
|
|
* multiplier;
|
|
p.drawRoundedRect(rect, radius, radius);
|
|
}
|
|
}
|
|
auto p = QPainter(&image);
|
|
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
|
p.drawImage(0, 0, mask);
|
|
}
|
|
|
|
float64 RoundedCornersMultiplier(RoundedCornersLevel level) {
|
|
switch (level) {
|
|
case RoundedCornersLevel::Large: return Ui::ForumUserpicRadiusMultiplier();
|
|
case RoundedCornersLevel::Medium: return 0.2;
|
|
case RoundedCornersLevel::Small: return 0.12;
|
|
case RoundedCornersLevel::None: return 0.;
|
|
}
|
|
Unexpected("Unknown RoundedCornersLevel in RoundedCornersMultiplier.");
|
|
}
|
|
|
|
QImage ImageModified(QImage image, const PhotoModifications &mods) {
|
|
Expects(!image.isNull());
|
|
|
|
if (!mods) {
|
|
return image;
|
|
}
|
|
if (mods.paint) {
|
|
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
|
image = image.convertToFormat(
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
}
|
|
|
|
Painter p(&image);
|
|
PainterHighQualityEnabler hq(p);
|
|
|
|
mods.paint->render(&p, image.rect());
|
|
}
|
|
auto cropped = mods.crop.isValid()
|
|
? image.copy(mods.crop)
|
|
: image;
|
|
QTransform transform;
|
|
if (mods.flipped) {
|
|
transform.scale(-1, 1);
|
|
}
|
|
if (mods.angle) {
|
|
transform.rotate(mods.angle);
|
|
}
|
|
return cropped.transformed(transform);
|
|
}
|
|
|
|
Media::Encode::Job ComposeAnimatedJob(
|
|
const QImage &image,
|
|
const PhotoModifications &mods) {
|
|
Expects(mods.paint != nullptr);
|
|
Expects(!image.isNull());
|
|
|
|
const auto scene = mods.paint.get();
|
|
const auto crop = mods.crop.isValid()
|
|
? mods.crop
|
|
: QRect(QPoint(), image.size());
|
|
auto transform = QTransform();
|
|
if (mods.flipped) {
|
|
transform.scale(-1, 1);
|
|
}
|
|
if (mods.angle) {
|
|
transform.rotate(mods.angle);
|
|
}
|
|
const auto matrix = QImage::trueMatrix(
|
|
transform,
|
|
crop.width(),
|
|
crop.height());
|
|
const auto rotated = matrix.mapRect(
|
|
QRectF(QPointF(), QSizeF(crop.size()))).size();
|
|
if (rotated.isEmpty()) {
|
|
return {};
|
|
}
|
|
const auto fit = std::min({
|
|
kAnimatedMaxSide / rotated.width(),
|
|
kAnimatedMaxSide / rotated.height(),
|
|
1.,
|
|
});
|
|
const auto target = QSize(
|
|
std::max(int(rotated.width() * fit) & ~1, 2),
|
|
std::max(int(rotated.height() * fit) & ~1, 2));
|
|
const auto sceneToCanvas = QTransform::fromTranslate(
|
|
-crop.x(),
|
|
-crop.y()
|
|
) * matrix * QTransform::fromScale(
|
|
target.width() / rotated.width(),
|
|
target.height() / rotated.height());
|
|
|
|
const auto bake = [&](const QImage &source) {
|
|
auto cropped = source.copy(crop);
|
|
return cropped.transformed(transform, Qt::SmoothTransformation)
|
|
.scaled(
|
|
target,
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation);
|
|
};
|
|
|
|
auto job = Media::Encode::Job();
|
|
const auto items = scene->items(Qt::AscendingOrder);
|
|
auto normal = std::vector<NumberedItem*>();
|
|
for (const auto &item : items) {
|
|
if (item->isNormalStatus()) {
|
|
normal.push_back(item.get());
|
|
}
|
|
}
|
|
ranges::stable_sort(normal, ranges::less(), &QGraphicsItem::zValue);
|
|
auto run = std::vector<NumberedItem*>();
|
|
const auto flushRun = [&] {
|
|
if (run.empty()) {
|
|
return;
|
|
}
|
|
for (const auto item : normal) {
|
|
item->setVisible(false);
|
|
}
|
|
for (const auto item : run) {
|
|
item->setVisible(true);
|
|
}
|
|
auto layer = QImage(
|
|
image.size(),
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
layer.fill(Qt::transparent);
|
|
{
|
|
auto p = Painter(&layer);
|
|
PainterHighQualityEnabler hq(p);
|
|
scene->render(&p, layer.rect());
|
|
}
|
|
for (const auto item : normal) {
|
|
item->setVisible(true);
|
|
}
|
|
job.overlay.push_back(bake(layer));
|
|
run.clear();
|
|
};
|
|
|
|
auto longest = crl::time(0);
|
|
for (const auto item : normal) {
|
|
const auto animated = dynamic_cast<ItemAnimated*>(item);
|
|
if (!animated || !animated->animated()) {
|
|
run.push_back(item);
|
|
continue;
|
|
}
|
|
auto entity = animated->animatedEntity(sceneToCanvas);
|
|
if (entity.bytes.isEmpty()) {
|
|
run.push_back(item);
|
|
continue;
|
|
}
|
|
flushRun();
|
|
job.overlay.push_back(std::move(entity));
|
|
const auto duration = animated->loopDuration();
|
|
longest = std::max(
|
|
longest,
|
|
duration ? duration : kAnimatedMaxDuration);
|
|
}
|
|
flushRun();
|
|
|
|
job.source = Media::Encode::StillSource{
|
|
.base = bake(image),
|
|
.duration = std::clamp(
|
|
longest,
|
|
kAnimatedMinDuration,
|
|
kAnimatedMaxDuration),
|
|
.fps = kAnimatedFps,
|
|
};
|
|
job.silentLoop = true;
|
|
return job;
|
|
}
|
|
|
|
bool PhotoModifications::empty() const {
|
|
return !angle && !flipped && !crop.isValid() && !paint;
|
|
}
|
|
|
|
PhotoModifications::operator bool() const {
|
|
return !empty();
|
|
}
|
|
|
|
PhotoModifications::~PhotoModifications() {
|
|
if (paint && (paint.use_count() == 1)) {
|
|
paint->deleteLater();
|
|
}
|
|
}
|
|
|
|
} // namespace Editor
|