[img-editor] Added video file item with animated overlay export.

This commit is contained in:
23rd
2026-09-06 14:24:44 +03:00
committed by John Preston
parent adcea79c19
commit ee9d8de349
13 changed files with 272 additions and 11 deletions

View File

@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/scene/scene_item_shape.h"
#include "editor/scene/scene_item_sticker.h"
#include "editor/scene/scene_item_text.h"
#include "editor/scene/scene_item_video.h"
#include "editor/scene/scene.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_single_player.h"
@@ -560,8 +561,22 @@ void Paint::handleMimeData(const QMimeData *data) {
_controllers->show->showBox(
Ui::MakeInformBox(tr::lng_edit_media_invalid_file()));
return;
} else if (media.video()) {
addVideoItem(std::move(media));
} else {
addImageItem(std::move(media.image));
}
addImageItem(std::move(media.image));
}
void Paint::addVideoItem(Storage::PhotoEditorMedia &&media) {
const auto data = mediaItemData(media.image.size());
addMediaItem(std::make_shared<ItemVideo>(
std::make_shared<ItemVideo::Source>(ItemVideo::Source{
.path = std::move(media.videoPath),
.thumbnail = std::move(media.image),
.duration = media.videoDuration,
}),
data));
}
void Paint::addImageItem(QImage &&image) {

View File

@@ -19,6 +19,10 @@ class QGraphicsItem;
class QGraphicsView;
class QKeyEvent;
namespace Storage {
struct PhotoEditorMedia;
} // namespace Storage
namespace Editor {
struct Controllers;
@@ -91,6 +95,7 @@ private:
ItemBase::Data mediaItemData(QSize mediaSize) const;
void addMediaItem(std::shared_ptr<ItemBase> item);
void addImageItem(QImage &&image);
void addVideoItem(Storage::PhotoEditorMedia &&media);
void applyViewTransform();
void bakeTextScales();

View File

@@ -689,9 +689,7 @@ bool Scene::hasAnimatedItems() const {
const auto animated = item->isNormalStatus()
? dynamic_cast<ItemAnimated*>(item.get())
: nullptr;
if (animated
&& animated->animated()
&& !animated->content().isEmpty()) {
if (animated && animated->animated() && animated->hasContent()) {
return true;
}
}

View File

@@ -17,6 +17,7 @@ public:
using ItemBase::ItemBase;
[[nodiscard]] virtual bool animated() const = 0;
[[nodiscard]] virtual bool hasContent() const = 0;
[[nodiscard]] virtual QByteArray content() const = 0;
[[nodiscard]] virtual crl::time loopDuration() const = 0;
virtual void releasePlayers() = 0;

View File

@@ -157,6 +157,10 @@ Media::Encode::AnimatedEntity::Kind ItemSticker::entityKind() const {
: Media::Encode::AnimatedEntity::Kind::Lottie;
}
bool ItemSticker::hasContent() const {
return !content().isEmpty();
}
QByteArray ItemSticker::content() const {
const auto &bytes = _mediaView->bytes();
if (!bytes.isEmpty()) {

View File

@@ -33,6 +33,7 @@ public:
QWidget *widget) override;
[[nodiscard]] not_null<DocumentData*> sticker() const;
[[nodiscard]] bool animated() const override;
[[nodiscard]] bool hasContent() const override;
[[nodiscard]] QByteArray content() const override;
[[nodiscard]] crl::time loopDuration() const override;
void releasePlayers() override;

View File

@@ -0,0 +1,151 @@
/*
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/scene/scene_item_video.h"
#include "ui/image/image_prepare.h"
#include "ui/rect.h"
#include <QtCore/QFile>
namespace Editor {
namespace {
constexpr auto kPlaybackFrameSide = 512;
[[nodiscard]] QSize FrameSizeFor(const QImage &thumbnail) {
const auto size = thumbnail.size();
if (size.isEmpty()) {
return Size(kPlaybackFrameSide);
} else if (size.width() <= kPlaybackFrameSide
&& size.height() <= kPlaybackFrameSide) {
return size;
}
return size.scaled(Size(kPlaybackFrameSide), Qt::KeepAspectRatio);
}
} // namespace
ItemVideo::ItemVideo(std::shared_ptr<Source> source, ItemBase::Data data)
: ItemAnimated(std::move(data))
, _source(std::move(source))
, _frameSize(FrameSizeFor(_source->thumbnail))
, _image(_source->thumbnail) {
if (flipped()) {
performFlip();
}
setAspectRatio(_image.isNull()
? 1.
: (_image.height() / float64(_image.width())));
createPlayer();
}
void ItemVideo::createPlayer() {
if (!hasContent()) {
return;
}
const auto callback = [=](::Media::Clip::Notification value) {
clipCallback(value);
};
_reader = _source->path.isEmpty()
? ::Media::Clip::MakeReader(_source->content, callback)
: ::Media::Clip::MakeReader(_source->path, callback);
}
void ItemVideo::clipCallback(::Media::Clip::Notification notification) {
using namespace ::Media::Clip;
if (notification == Notification::Reinit) {
if (_reader && _reader->state() == State::Error) {
_reader.setBad();
} else if (_reader && _reader->ready() && !_reader->started()) {
_reader->start({ .frame = _frameSize });
}
}
update();
}
bool ItemVideo::animated() const {
return _reader.valid() || _releasedAnimation;
}
bool ItemVideo::hasContent() const {
return !_source->path.isEmpty() || !_source->content.isEmpty();
}
QByteArray ItemVideo::content() const {
if (_source->path.isEmpty()) {
return _source->content;
}
auto file = QFile(_source->path);
if (file.size() > Images::kReadBytesLimit
|| !file.open(QIODevice::ReadOnly)) {
return QByteArray();
}
return file.readAll();
}
crl::time ItemVideo::loopDuration() const {
return _source->duration;
}
void ItemVideo::releasePlayers() {
if (!animated()) {
return;
}
_releasedAnimation = true;
_pendingRecreate = true;
_reader.reset();
}
void ItemVideo::setStatus(Status status) {
if (status != Status::Normal) {
releasePlayers();
}
ItemBase::setStatus(status);
}
int ItemVideo::type() const {
return Type;
}
Media::Encode::AnimatedEntity::Kind ItemVideo::entityKind() const {
return Media::Encode::AnimatedEntity::Kind::Webm;
}
QImage ItemVideo::currentFrame() {
if (_reader && _reader->started()) {
auto result = _reader->current({ .frame = _frameSize }, crl::now());
if (!result.isNull()) {
return result;
}
}
return _image;
}
void ItemVideo::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *w) {
if (_pendingRecreate && w) {
_pendingRecreate = false;
createPlayer();
}
const auto live = _reader && _reader->started();
paintFrame(p, currentFrame(), live, live && flipped());
ItemBase::paint(p, option, w);
}
void ItemVideo::performFlip() {
_image = _image.transformed(QTransform().scale(-1, 1));
update();
}
std::shared_ptr<ItemBase> ItemVideo::duplicate(ItemBase::Data data) const {
return std::make_shared<ItemVideo>(_source, std::move(data));
}
} // namespace Editor

View File

@@ -0,0 +1,59 @@
/*
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
*/
#pragma once
#include "editor/scene/scene_item_animated.h"
#include "media/clip/media_clip_reader.h"
namespace Editor {
class ItemVideo final : public ItemAnimated {
public:
enum { Type = ItemBase::Type + 4 };
struct Source {
QString path;
QByteArray content;
QImage thumbnail;
crl::time duration = 0;
};
ItemVideo(std::shared_ptr<Source> source, ItemBase::Data data);
void paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
[[nodiscard]] bool animated() const override;
[[nodiscard]] bool hasContent() const override;
[[nodiscard]] QByteArray content() const override;
[[nodiscard]] crl::time loopDuration() const override;
void releasePlayers() override;
void setStatus(Status status) override;
int type() const override;
protected:
[[nodiscard]] Media::Encode::AnimatedEntity::Kind entityKind()
const override;
void performFlip() override;
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
private:
void createPlayer();
void clipCallback(::Media::Clip::Notification notification);
[[nodiscard]] QImage currentFrame();
const std::shared_ptr<Source> _source;
const QSize _frameSize;
::Media::Clip::ReaderPointer _reader;
QImage _image;
bool _releasedAnimation = false;
bool _pendingRecreate = false;
};
} // namespace Editor

View File

@@ -622,10 +622,9 @@ bool FileLoadTask::CheckForSong(
return true;
}
bool FileLoadTask::CheckForVideo(
bool FileLoadTask::IsVideoFile(
const QString &filepath,
const QByteArray &content,
std::unique_ptr<Ui::PreparedFileInformation> &result) {
const QString &filemime) {
static const auto mimes = {
u"video/mp4"_q,
u"video/quicktime"_q,
@@ -636,7 +635,14 @@ bool FileLoadTask::CheckForVideo(
u".m4v"_q,
u".webm"_q,
};
if (!CheckMimeOrExtensions(filepath, result->filemime, mimes, extensions)) {
return CheckMimeOrExtensions(filepath, filemime, mimes, extensions);
}
bool FileLoadTask::CheckForVideo(
const QString &filepath,
const QByteArray &content,
std::unique_ptr<Ui::PreparedFileInformation> &result) {
if (!IsVideoFile(filepath, result->filemime)) {
return false;
}

View File

@@ -245,6 +245,9 @@ public:
const QString &filepath,
const QByteArray &content,
const QString &filemime);
[[nodiscard]] static bool IsVideoFile(
const QString &filepath,
const QString &filemime);
static bool FillImageInformation(
QImage &&image,
bool animated,

View File

@@ -90,9 +90,9 @@ bool ValidatePhotoEditorMediaDragData(not_null<const QMimeData*> data) {
if (url.isLocalFile()) {
using namespace Core;
const auto file = Platform::File::UrlToLocal(url);
const auto info = QFileInfo(file);
return FileIsImage(file, MimeTypeForFile(info).name())
&& QImageReader(file).canRead();
const auto mime = MimeTypeForFile(QFileInfo(file)).name();
return FileLoadTask::IsVideoFile(file, mime)
|| (FileIsImage(file, mime) && QImageReader(file).canRead());
}
}
@@ -110,6 +110,17 @@ PhotoEditorMedia ReadPhotoEditorMedia(not_null<const QMimeData*> data) {
if (const auto image = std::get_if<Image>(&information->media)) {
return { .image = std::move(image->data) };
}
using Video = PreparedFileInformation::Video;
if (const auto video = std::get_if<Video>(&information->media)) {
if (QFileInfo(path).size() > Images::kReadBytesLimit) {
return {};
}
return {
.image = std::move(video->thumbnail),
.videoPath = path,
.videoDuration = video->duration,
};
}
}
if (auto read = Core::ReadMimeImage(data)) {
return { .image = std::move(read.image) };

View File

@@ -54,7 +54,12 @@ enum class MimeDataState {
struct PhotoEditorMedia {
QImage image;
QString videoPath;
crl::time videoDuration = 0;
[[nodiscard]] bool video() const {
return !videoPath.isEmpty();
}
[[nodiscard]] explicit operator bool() const {
return !image.isNull();
}

View File

@@ -197,6 +197,8 @@ PRIVATE
editor/scene/scene_item_shape.h
editor/scene/scene_item_text.cpp
editor/scene/scene_item_text.h
editor/scene/scene_item_video.cpp
editor/scene/scene_item_video.h
editor/scene/scene_emoji_document.cpp
editor/scene/scene_emoji_document.h
editor/scene/scene_text_editing.cpp