mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 08:03:45 +08:00
[img-editor] Added video file item with animated overlay export.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
151
Telegram/SourceFiles/editor/scene/scene_item_video.cpp
Normal file
151
Telegram/SourceFiles/editor/scene/scene_item_video.cpp
Normal 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
|
||||
59
Telegram/SourceFiles/editor/scene/scene_item_video.h
Normal file
59
Telegram/SourceFiles/editor/scene/scene_item_video.h
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) };
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user