- node_save_load_test: Node Save/Load round trips for values, keyframes, hints, caches, connections, positions - node_polygon_folder_test: Folder child management, PolygonGenerator rasterization/gizmos, TextGeneratorV1/V2 - footage_probe_test: real FFmpeg/OIIO probing of demo.mp4/img.png, metadata cache, footage state transitions - render_tail_test: DynamicRenderer, color-context shader plumbing, PreviewAutoCacher pause/clear paths, DiskManager edges, AudioPlaybackCache segment I/O Also fixes AudioPlaybackCache::WritePartOfSampleBuffer, found by the new tests: zero padding was written via QFile::write(const char*) which treats the buffer as a NUL-terminated string, so no padding bytes were ever written; the write length was also computed from the segment end instead of the range end, making WriteSilence() spin forever on an empty buffer
1057 lines
35 KiB
C++
1057 lines
35 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <QBuffer>
|
|
#include <QPointF>
|
|
#include <QVector2D>
|
|
#include <QXmlStreamReader>
|
|
#include <QXmlStreamWriter>
|
|
|
|
#include "core.h"
|
|
#include "codec/frame.h"
|
|
#include "node/color/colormanager/colormanager.h"
|
|
#include "node/factory.h"
|
|
#include "node/generator/polygon/polygon.h"
|
|
#include "node/generator/shape/generatorwithmerge.h"
|
|
#include "node/generator/shape/shapenodebase.h"
|
|
#include "node/generator/solid/solid.h"
|
|
#include "node/generator/text/textv1.h"
|
|
#include "node/generator/text/textv2.h"
|
|
#include "node/gizmo/line.h"
|
|
#include "node/gizmo/path.h"
|
|
#include "node/gizmo/point.h"
|
|
#include "node/globals.h"
|
|
#include "node/nodeundo.h"
|
|
#include "node/project.h"
|
|
#include "node/project/folder/folder.h"
|
|
#include "node/project/serializer/serializer.h"
|
|
#include "node/traverser.h"
|
|
#include "olive/core/util/color.h"
|
|
#include "render/diskmanager.h"
|
|
#include "render/job/generatejob.h"
|
|
#include "render/loopmode.h"
|
|
#include "render/texture.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
// Node that pushes a fixed dummy texture, used to feed the base input of
|
|
// merge-capable generators without any renderer.
|
|
class ConstantTextureNode : public olive::Node {
|
|
public:
|
|
ConstantTextureNode() = default;
|
|
|
|
NODE_DEFAULT_FUNCTIONS(ConstantTextureNode)
|
|
|
|
virtual QString Name() const override
|
|
{
|
|
return QStringLiteral("Test Texture");
|
|
}
|
|
|
|
virtual QString id() const override
|
|
{
|
|
return QStringLiteral("org.oak.test.constant_texture");
|
|
}
|
|
|
|
virtual QVector<CategoryID> Category() const override
|
|
{
|
|
return { kCategoryGenerator };
|
|
}
|
|
|
|
void SetTexture(const olive::TexturePtr &texture)
|
|
{
|
|
texture_ = texture;
|
|
}
|
|
|
|
virtual void Value(const olive::NodeValueRow &value,
|
|
const olive::NodeGlobals &globals,
|
|
olive::NodeValueTable *table) const override
|
|
{
|
|
Q_UNUSED(value)
|
|
Q_UNUSED(globals)
|
|
|
|
table->Push(olive::NodeValue(olive::NodeValue::kTexture, texture_, this));
|
|
}
|
|
|
|
private:
|
|
olive::TexturePtr texture_;
|
|
};
|
|
|
|
template <typename T> T *AddNode(olive::Project *project)
|
|
{
|
|
T *node = new T();
|
|
node->setParent(project);
|
|
return node;
|
|
}
|
|
|
|
olive::TimeRange FirstFrame()
|
|
{
|
|
return olive::TimeRange(olive::rational(0), olive::rational(1, 30));
|
|
}
|
|
|
|
// A fresh traverser per call: NodeTraverser caches tables per node/range, so
|
|
// reusing one would return stale results after changing standard values.
|
|
olive::NodeValueTable GenerateTable(const olive::Node *node,
|
|
const olive::VideoParams &vparams)
|
|
{
|
|
olive::NodeTraverser traverser;
|
|
traverser.SetCacheVideoParams(vparams);
|
|
return traverser.GenerateTable(node, FirstFrame());
|
|
}
|
|
|
|
olive::NodeValueRow GenerateRow(const olive::Node *node)
|
|
{
|
|
olive::NodeTraverser traverser;
|
|
return traverser.GenerateRow(node, FirstFrame());
|
|
}
|
|
|
|
olive::TexturePtr GetOutputTexture(const olive::NodeValueTable &table)
|
|
{
|
|
return table.Get(olive::NodeValue::kTexture).toTexture();
|
|
}
|
|
|
|
// Project save/load touches the DiskManager singleton, which itself touches Core
|
|
void EnsureAppSingletons()
|
|
{
|
|
if (!olive::Core::instance()) {
|
|
new olive::Core(olive::Core::CoreParams()); // intentionally leaked
|
|
}
|
|
if (!olive::DiskManager::instance()) {
|
|
olive::DiskManager::CreateInstance();
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(Folder, MetadataAndChildInputDefinition)
|
|
{
|
|
olive::Folder folder;
|
|
EXPECT_EQ(folder.id(), QStringLiteral("org.olivevideoeditor.Olive.folder"));
|
|
EXPECT_EQ(folder.Name(), QStringLiteral("Folder"));
|
|
EXPECT_FALSE(folder.Description().isEmpty());
|
|
EXPECT_TRUE(folder.Category().contains(olive::Node::kCategoryProject));
|
|
EXPECT_TRUE(folder.IsItem());
|
|
|
|
// The child input is a non-keyframable array that accepts any node
|
|
EXPECT_TRUE(folder.HasInputWithID(olive::Folder::kChildInput));
|
|
EXPECT_TRUE(folder.InputIsArray(olive::Folder::kChildInput));
|
|
EXPECT_EQ(int(folder.GetInputDataType(olive::Folder::kChildInput)),
|
|
int(olive::NodeValue::kNone));
|
|
EXPECT_FALSE(folder.IsInputKeyframable(olive::Folder::kChildInput));
|
|
|
|
// Folders provide their own icon; every other data type falls through to
|
|
// the Node base implementation
|
|
EXPECT_TRUE(folder.data(olive::Node::ICON).isValid());
|
|
EXPECT_FALSE(folder.data(olive::Node::TOOLTIP).isValid());
|
|
}
|
|
|
|
TEST(Folder, RetranslateSetsChildInputName)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *folder = AddNode<olive::Folder>(&project);
|
|
folder->Retranslate();
|
|
|
|
EXPECT_EQ(folder->GetInputName(olive::Folder::kChildInput),
|
|
QStringLiteral("Children"));
|
|
}
|
|
|
|
TEST(Folder, AddChildAppendsAndEmitsSignals)
|
|
{
|
|
// Declared before the project so teardown signals never outlive them
|
|
QVector<olive::Node *> inserted_items;
|
|
QVector<int> inserted_indices;
|
|
int insert_ends = 0;
|
|
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
olive::Folder *folder = project.root();
|
|
auto *child = AddNode<olive::SolidGenerator>(&project);
|
|
|
|
QObject::connect(folder, &olive::Folder::BeginInsertItem,
|
|
[&inserted_items, &inserted_indices](olive::Node *n,
|
|
int index) {
|
|
inserted_items.append(n);
|
|
inserted_indices.append(index);
|
|
});
|
|
QObject::connect(folder, &olive::Folder::EndInsertItem,
|
|
[&insert_ends]() { ++insert_ends; });
|
|
|
|
olive::FolderAddChild(folder, child).redo_now();
|
|
|
|
ASSERT_EQ(folder->item_child_count(), 1);
|
|
EXPECT_EQ(folder->item_child(0), child);
|
|
EXPECT_EQ(folder->children().first(), child);
|
|
EXPECT_EQ(folder->index_of_child(child), 0);
|
|
EXPECT_EQ(folder->index_of_child_in_array(child), 0);
|
|
EXPECT_EQ(child->folder(), folder);
|
|
|
|
// The insert index is always the append position: the internal model only
|
|
// ever appends, sorting is left to a proxy model (see folder.cpp)
|
|
ASSERT_EQ(inserted_items.size(), 1);
|
|
EXPECT_EQ(inserted_items.first(), child);
|
|
EXPECT_EQ(inserted_indices.first(), 0);
|
|
EXPECT_EQ(insert_ends, 1);
|
|
}
|
|
|
|
TEST(Folder, AddChildUndoRemovesChildAndEmitsSignals)
|
|
{
|
|
// Declared before the project so teardown signals never outlive them
|
|
QVector<olive::Node *> removed_items;
|
|
QVector<int> removed_indices;
|
|
int remove_ends = 0;
|
|
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
olive::Folder *folder = project.root();
|
|
auto *child = AddNode<olive::SolidGenerator>(&project);
|
|
|
|
QObject::connect(folder, &olive::Folder::BeginRemoveItem,
|
|
[&removed_items, &removed_indices](olive::Node *n,
|
|
int index) {
|
|
removed_items.append(n);
|
|
removed_indices.append(index);
|
|
});
|
|
QObject::connect(folder, &olive::Folder::EndRemoveItem,
|
|
[&remove_ends]() { ++remove_ends; });
|
|
|
|
olive::FolderAddChild add(folder, child);
|
|
add.redo_now();
|
|
ASSERT_EQ(folder->item_child_count(), 1);
|
|
|
|
add.undo_now();
|
|
|
|
EXPECT_EQ(folder->item_child_count(), 0);
|
|
EXPECT_EQ(folder->index_of_child(child), -1);
|
|
EXPECT_EQ(folder->index_of_child_in_array(child), -1);
|
|
EXPECT_EQ(child->folder(), nullptr);
|
|
|
|
ASSERT_EQ(removed_items.size(), 1);
|
|
EXPECT_EQ(removed_items.first(), child);
|
|
EXPECT_EQ(removed_indices.first(), 0);
|
|
EXPECT_EQ(remove_ends, 1);
|
|
}
|
|
|
|
TEST(Folder, RemoveElementCommandRemovesAndRestores)
|
|
{
|
|
// Declared before the project so teardown signals never outlive them
|
|
QVector<olive::Node *> removed_items;
|
|
QVector<int> removed_indices;
|
|
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
olive::Folder *folder = project.root();
|
|
auto *first = AddNode<olive::Folder>(&project);
|
|
auto *second = AddNode<olive::Folder>(&project);
|
|
olive::FolderAddChild(folder, first).redo_now();
|
|
olive::FolderAddChild(folder, second).redo_now();
|
|
ASSERT_EQ(folder->item_child_count(), 2);
|
|
|
|
QObject::connect(folder, &olive::Folder::BeginRemoveItem,
|
|
[&removed_items, &removed_indices](olive::Node *n,
|
|
int index) {
|
|
removed_items.append(n);
|
|
removed_indices.append(index);
|
|
});
|
|
|
|
olive::Folder::RemoveElementCommand remove(folder, first);
|
|
remove.redo_now();
|
|
|
|
EXPECT_EQ(folder->item_child_count(), 1);
|
|
EXPECT_EQ(folder->item_child(0), second);
|
|
EXPECT_EQ(first->folder(), nullptr);
|
|
// RemoveElementCommand removes the edge and the array element as separate
|
|
// subcommands, each of which fires BeginRemoveItem
|
|
ASSERT_EQ(removed_items.size(), 2);
|
|
EXPECT_EQ(removed_items.first(), first);
|
|
EXPECT_EQ(removed_indices.first(), 0);
|
|
|
|
remove.undo_now();
|
|
|
|
// The connection is restored at its original array element, but
|
|
// Folder::InputConnectedEvent only ever appends to the internal model, so
|
|
// the restored child lands at the end of the children list
|
|
ASSERT_EQ(folder->item_child_count(), 2);
|
|
EXPECT_EQ(folder->item_child(0), second);
|
|
EXPECT_EQ(folder->item_child(1), first);
|
|
EXPECT_EQ(folder->index_of_child(first), 1);
|
|
EXPECT_EQ(folder->index_of_child_in_array(first), 0);
|
|
EXPECT_EQ(first->folder(), folder);
|
|
}
|
|
|
|
TEST(Folder, RemoveElementCommandIgnoresForeignChild)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
olive::Folder *folder = project.root();
|
|
auto *child = AddNode<olive::Folder>(&project);
|
|
auto *stranger = AddNode<olive::Folder>(&project);
|
|
olive::FolderAddChild(folder, child).redo_now();
|
|
ASSERT_EQ(folder->item_child_count(), 1);
|
|
|
|
// A node that was never added has no array element, so the command is a
|
|
// no-op rather than an error
|
|
olive::Folder::RemoveElementCommand remove(folder, stranger);
|
|
remove.redo_now();
|
|
EXPECT_EQ(folder->item_child_count(), 1);
|
|
EXPECT_EQ(folder->item_child(0), child);
|
|
|
|
remove.undo_now();
|
|
EXPECT_EQ(folder->item_child_count(), 1);
|
|
}
|
|
|
|
TEST(Folder, GetChildWithNameFindsNestedChildren)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
olive::Folder *folder = project.root();
|
|
auto *sub = AddNode<olive::Folder>(&project);
|
|
sub->SetLabel(QStringLiteral("Sub"));
|
|
auto *nested = AddNode<olive::SolidGenerator>(&project);
|
|
nested->SetLabel(QStringLiteral("Nested"));
|
|
|
|
olive::FolderAddChild(folder, sub).redo_now();
|
|
olive::FolderAddChild(sub, nested).redo_now();
|
|
|
|
// Lookup by label recurses into subfolders
|
|
EXPECT_EQ(folder->GetChildWithName(QStringLiteral("Sub")), sub);
|
|
EXPECT_EQ(folder->GetChildWithName(QStringLiteral("Nested")), nested);
|
|
EXPECT_TRUE(folder->ChildExistsWithName(QStringLiteral("Nested")));
|
|
|
|
EXPECT_EQ(folder->GetChildWithName(QStringLiteral("Missing")), nullptr);
|
|
EXPECT_FALSE(folder->ChildExistsWithName(QStringLiteral("Missing")));
|
|
}
|
|
|
|
TEST(Folder, HasChildRecursiveFindsNestedChildren)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
olive::Folder *folder = project.root();
|
|
auto *sub = AddNode<olive::Folder>(&project);
|
|
auto *nested = AddNode<olive::Folder>(&project);
|
|
auto *outsider = AddNode<olive::Folder>(&project);
|
|
|
|
olive::FolderAddChild(folder, sub).redo_now();
|
|
olive::FolderAddChild(sub, nested).redo_now();
|
|
|
|
EXPECT_TRUE(folder->HasChildRecursive(sub));
|
|
EXPECT_TRUE(folder->HasChildRecursive(nested));
|
|
EXPECT_FALSE(folder->HasChildRecursive(outsider));
|
|
EXPECT_FALSE(folder->HasChildRecursive(folder));
|
|
}
|
|
|
|
TEST(Folder, ListChildrenOfTypeRecursesIntoSubfolders)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
olive::Folder *folder = project.root();
|
|
auto *sub = AddNode<olive::Folder>(&project);
|
|
auto *nested = AddNode<olive::Folder>(&project);
|
|
auto *item = AddNode<olive::SolidGenerator>(&project);
|
|
|
|
olive::FolderAddChild(folder, sub).redo_now();
|
|
olive::FolderAddChild(sub, nested).redo_now();
|
|
olive::FolderAddChild(folder, item).redo_now();
|
|
|
|
// Folders are collected recursively while other items are skipped
|
|
const QVector<olive::Folder *> folders =
|
|
folder->ListChildrenOfType<olive::Folder>();
|
|
ASSERT_EQ(folders.size(), 2);
|
|
EXPECT_TRUE(folders.contains(sub));
|
|
EXPECT_TRUE(folders.contains(nested));
|
|
EXPECT_FALSE(folders.contains(static_cast<olive::Node *>(item)));
|
|
|
|
const QVector<olive::SolidGenerator *> solids =
|
|
folder->ListChildrenOfType<olive::SolidGenerator>();
|
|
ASSERT_EQ(solids.size(), 1);
|
|
EXPECT_EQ(solids.first(), item);
|
|
}
|
|
|
|
TEST(Folder, ChildStructureSurvivesSerializationRoundTrip)
|
|
{
|
|
EnsureAppSingletons();
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::NodeFactory::Initialize();
|
|
// Guard against serializer instances left over by another test
|
|
olive::ProjectSerializer::Destroy();
|
|
olive::ProjectSerializer::Initialize();
|
|
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *sub = AddNode<olive::Folder>(&project);
|
|
sub->SetLabel(QStringLiteral("Sub"));
|
|
olive::FolderAddChild(project.root(), sub).redo_now();
|
|
|
|
auto *nested = AddNode<olive::Folder>(&project);
|
|
nested->SetLabel(QStringLiteral("Nested"));
|
|
olive::FolderAddChild(sub, nested).redo_now();
|
|
|
|
olive::ProjectSerializer::SaveData save_data(
|
|
olive::ProjectSerializer::kProject, &project, QString());
|
|
|
|
QByteArray xml;
|
|
QBuffer buffer(&xml);
|
|
buffer.open(QIODevice::WriteOnly);
|
|
QXmlStreamWriter writer(&buffer);
|
|
ASSERT_EQ(olive::ProjectSerializer::Save(&writer, save_data).code(),
|
|
olive::ProjectSerializer::kSuccess);
|
|
buffer.close();
|
|
|
|
olive::Project loaded_project;
|
|
QBuffer read_buffer(&xml);
|
|
read_buffer.open(QIODevice::ReadOnly);
|
|
QXmlStreamReader reader(&read_buffer);
|
|
olive::ProjectSerializer::Result result = olive::ProjectSerializer::Load(
|
|
&loaded_project, &reader, olive::ProjectSerializer::kProject);
|
|
ASSERT_EQ(result.code(), olive::ProjectSerializer::kSuccess);
|
|
|
|
// The child connections of kChildInput are re-established on load,
|
|
// rebuilding the folder hierarchy
|
|
olive::Folder *loaded_root = loaded_project.root();
|
|
ASSERT_NE(loaded_root, nullptr);
|
|
ASSERT_EQ(loaded_root->item_child_count(), 1);
|
|
|
|
olive::Node *loaded_sub = loaded_root->item_child(0);
|
|
EXPECT_EQ(loaded_sub->GetLabel(), QStringLiteral("Sub"));
|
|
EXPECT_EQ(loaded_sub->folder(), loaded_root);
|
|
|
|
auto *loaded_sub_folder = dynamic_cast<olive::Folder *>(loaded_sub);
|
|
ASSERT_NE(loaded_sub_folder, nullptr);
|
|
ASSERT_EQ(loaded_sub_folder->item_child_count(), 1);
|
|
EXPECT_EQ(loaded_sub_folder->item_child(0)->GetLabel(),
|
|
QStringLiteral("Nested"));
|
|
EXPECT_EQ(loaded_sub_folder->item_child(0)->folder(), loaded_sub_folder);
|
|
|
|
EXPECT_EQ(loaded_root->GetChildWithName(QStringLiteral("Nested")),
|
|
loaded_sub_folder->item_child(0));
|
|
EXPECT_TRUE(
|
|
loaded_root->HasChildRecursive(loaded_sub_folder->item_child(0)));
|
|
|
|
olive::ProjectSerializer::Destroy();
|
|
}
|
|
|
|
TEST(PolygonGenerator, GenerateFrameRasterizesDefaultPentagon)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::PolygonGenerator>(&project);
|
|
|
|
const olive::VideoParams vparams(320, 240, olive::core::PixelFormat::U8,
|
|
olive::VideoParams::kRGBAChannelCount);
|
|
olive::FramePtr frame = olive::Frame::Create();
|
|
frame->set_video_params(vparams);
|
|
frame->allocate();
|
|
|
|
node->GenerateFrame(frame, olive::GenerateJob(GenerateRow(node)));
|
|
|
|
auto pixel = [&frame](int x, int y) -> const uchar * {
|
|
return reinterpret_cast<const uchar *>(frame->data()) +
|
|
y * frame->linesize_bytes() +
|
|
x * olive::VideoParams::kRGBAChannelCount;
|
|
};
|
|
|
|
// The pentagon is filled white: the frame center is well inside it
|
|
const uchar *center = pixel(160, 120);
|
|
EXPECT_EQ(int(center[0]), 255);
|
|
EXPECT_EQ(int(center[1]), 255);
|
|
EXPECT_EQ(int(center[2]), 255);
|
|
EXPECT_EQ(int(center[3]), 255);
|
|
|
|
const uchar *lower = pixel(160, 200);
|
|
EXPECT_EQ(int(lower[3]), 255);
|
|
|
|
// The corners are outside the pentagon and stay transparent
|
|
for (int y : { 0, 239 }) {
|
|
for (int x : { 0, 319 }) {
|
|
const uchar *corner = pixel(x, y);
|
|
EXPECT_EQ(int(corner[3]), 0) << "corner " << x << ", " << y;
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(PolygonGenerator, UpdateGizmoPositionsCreatesHandlesForEachPoint)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::PolygonGenerator>(&project);
|
|
|
|
// Only the path gizmo exists before the first update
|
|
ASSERT_EQ(node->GetGizmos().size(), 1);
|
|
|
|
const olive::VideoParams vparams(320, 240, olive::core::PixelFormat::F32,
|
|
olive::VideoParams::kRGBAChannelCount);
|
|
const olive::NodeGlobals globals(vparams, olive::core::AudioParams(),
|
|
olive::rational(0),
|
|
olive::LoopMode::kLoopModeOff);
|
|
|
|
node->UpdateGizmoPositions(GenerateRow(node), globals);
|
|
|
|
// Path gizmo + one position handle, two bezier handles and two bezier
|
|
// lines per point of the default pentagon
|
|
ASSERT_EQ(node->GetGizmos().size(), 1 + 5 + 10 + 10);
|
|
|
|
// Without a base texture the gizmos are anchored at half the sequence
|
|
// resolution on top of each point
|
|
const double expected[5][2] = {
|
|
{ 0, -135 }, { 135, -45 }, { 90, 120 }, { -90, 120 }, { -135, -45 }
|
|
};
|
|
for (int i = 0; i < 5; i++) {
|
|
auto *position =
|
|
static_cast<olive::PointGizmo *>(node->GetGizmos().at(1 + i));
|
|
EXPECT_EQ(position->GetPoint(),
|
|
QPointF(expected[i][0] + 160, expected[i][1] + 120))
|
|
<< "Wrong position handle for point " << i;
|
|
}
|
|
|
|
// Bezier handles default to the point position (zero control point offsets)
|
|
auto *bezier = static_cast<olive::PointGizmo *>(node->GetGizmos().at(6));
|
|
EXPECT_EQ(int(bezier->GetShape()), int(olive::PointGizmo::kCircle));
|
|
EXPECT_EQ(bezier->GetPoint(), QPointF(160, -15));
|
|
|
|
auto *line = static_cast<olive::LineGizmo *>(node->GetGizmos().at(16));
|
|
EXPECT_EQ(line->GetLine(), QLineF(QPointF(160, -15), QPointF(160, -15)));
|
|
|
|
auto *path = dynamic_cast<olive::PathGizmo *>(node->GetGizmos().first());
|
|
ASSERT_NE(path, nullptr);
|
|
// moveTo plus one cubic segment (3 elements) per edge of the pentagon
|
|
EXPECT_EQ(path->GetPath().elementCount(), 16);
|
|
|
|
// Shrinking the point array shrinks the gizmo vectors with it
|
|
node->InputArrayResize(olive::PolygonGenerator::kPointsInput, 3);
|
|
node->UpdateGizmoPositions(GenerateRow(node), globals);
|
|
EXPECT_EQ(node->GetGizmos().size(), 1 + 3 + 6 + 6);
|
|
|
|
// With a base texture connected the gizmos anchor at half the texture's
|
|
// virtual resolution instead of the sequence's
|
|
const olive::TexturePtr base = std::make_shared<olive::Texture>(
|
|
olive::VideoParams(64, 48, olive::core::PixelFormat::U8,
|
|
olive::VideoParams::kRGBAChannelCount));
|
|
olive::NodeValueRow row = GenerateRow(node);
|
|
row.insert(olive::GeneratorWithMerge::kBaseInput,
|
|
olive::NodeValue(olive::NodeValue::kTexture, base));
|
|
node->UpdateGizmoPositions(row, globals);
|
|
|
|
auto *position =
|
|
static_cast<olive::PointGizmo *>(node->GetGizmos().at(1));
|
|
EXPECT_EQ(position->GetPoint(), QPointF(32, -111));
|
|
}
|
|
|
|
TEST(PolygonGenerator, DraggingPositionGizmoUpdatesPointTracks)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::PolygonGenerator>(&project);
|
|
|
|
const olive::NodeValueRow row = GenerateRow(node);
|
|
node->UpdateGizmoPositions(row, olive::NodeGlobals());
|
|
ASSERT_EQ(node->GetGizmos().size(), 26);
|
|
|
|
// The first position handle drives the X/Y tracks of the first point
|
|
auto *gizmo = static_cast<olive::PointGizmo *>(node->GetGizmos().at(1));
|
|
gizmo->DragStart(row, 0, 0, olive::rational(0));
|
|
gizmo->DragMove(10, -20, Qt::NoModifier);
|
|
|
|
EXPECT_DOUBLE_EQ(node->GetSplitStandardValueOnTrack(
|
|
olive::PolygonGenerator::kPointsInput, 0, 0)
|
|
.toDouble(),
|
|
10.0);
|
|
EXPECT_DOUBLE_EQ(node->GetSplitStandardValueOnTrack(
|
|
olive::PolygonGenerator::kPointsInput, 1, 0)
|
|
.toDouble(),
|
|
-155.0);
|
|
|
|
// The other points are untouched
|
|
EXPECT_DOUBLE_EQ(node->GetSplitStandardValueOnTrack(
|
|
olive::PolygonGenerator::kPointsInput, 0, 1)
|
|
.toDouble(),
|
|
135.0);
|
|
|
|
olive::MultiUndoCommand command;
|
|
gizmo->DragEnd(&command);
|
|
}
|
|
|
|
TEST(TextGeneratorV2, MetadataIsCorrect)
|
|
{
|
|
olive::TextGeneratorV2 node;
|
|
EXPECT_EQ(node.id(), QStringLiteral("org.olivevideoeditor.Olive.text2"));
|
|
EXPECT_EQ(node.Name(), QStringLiteral("Text (Legacy)"));
|
|
EXPECT_FALSE(node.Description().isEmpty());
|
|
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryGenerator));
|
|
|
|
// Hidden from the create menu: superseded by TextGeneratorV3
|
|
EXPECT_TRUE(node.GetFlags() & olive::Node::kDontShowInCreateMenu);
|
|
}
|
|
|
|
TEST(TextGeneratorV2, InputDefaults)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV2>(&project);
|
|
|
|
EXPECT_EQ(node->GetStandardValue(olive::TextGeneratorV2::kTextInput)
|
|
.toString(),
|
|
QStringLiteral("Sample Text"));
|
|
|
|
EXPECT_EQ(int(node->GetInputDataType(olive::TextGeneratorV2::kHtmlInput)),
|
|
int(olive::NodeValue::kBoolean));
|
|
EXPECT_FALSE(node->GetStandardValue(olive::TextGeneratorV2::kHtmlInput)
|
|
.toBool());
|
|
|
|
EXPECT_EQ(int(node->GetInputDataType(olive::TextGeneratorV2::kVAlignInput)),
|
|
int(olive::NodeValue::kCombo));
|
|
EXPECT_EQ(node->GetStandardValue(olive::TextGeneratorV2::kVAlignInput)
|
|
.toInt(),
|
|
0);
|
|
|
|
EXPECT_EQ(int(node->GetInputDataType(olive::TextGeneratorV2::kFontInput)),
|
|
int(olive::NodeValue::kFont));
|
|
|
|
EXPECT_EQ(
|
|
int(node->GetInputDataType(olive::TextGeneratorV2::kFontSizeInput)),
|
|
int(olive::NodeValue::kFloat));
|
|
EXPECT_DOUBLE_EQ(node->GetStandardValue(olive::TextGeneratorV2::kFontSizeInput)
|
|
.toDouble(),
|
|
72.0);
|
|
|
|
// From ShapeNodeBase: white text on a 400x300 box
|
|
const olive::core::Color color =
|
|
node->GetStandardValue(olive::ShapeNodeBase::kColorInput)
|
|
.value<olive::core::Color>();
|
|
EXPECT_FLOAT_EQ(color.red(), 1.0f);
|
|
EXPECT_FLOAT_EQ(color.green(), 1.0f);
|
|
EXPECT_FLOAT_EQ(color.blue(), 1.0f);
|
|
EXPECT_FLOAT_EQ(color.alpha(), 1.0f);
|
|
EXPECT_EQ(node->GetStandardValue(olive::ShapeNodeBase::kSizeInput)
|
|
.value<QVector2D>(),
|
|
QVector2D(400.0f, 300.0f));
|
|
}
|
|
|
|
TEST(TextGeneratorV2, RetranslateSetsNamesAndComboStrings)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV2>(&project);
|
|
node->Retranslate();
|
|
|
|
EXPECT_EQ(node->GetInputName(olive::TextGeneratorV2::kTextInput),
|
|
QStringLiteral("Text"));
|
|
EXPECT_EQ(node->GetInputName(olive::TextGeneratorV2::kHtmlInput),
|
|
QStringLiteral("Enable HTML"));
|
|
EXPECT_EQ(node->GetInputName(olive::TextGeneratorV2::kFontInput),
|
|
QStringLiteral("Font"));
|
|
EXPECT_EQ(node->GetInputName(olive::TextGeneratorV2::kFontSizeInput),
|
|
QStringLiteral("Font Size"));
|
|
EXPECT_EQ(node->GetInputName(olive::TextGeneratorV2::kVAlignInput),
|
|
QStringLiteral("Vertical Align"));
|
|
|
|
// Inherited names from ShapeNodeBase and GeneratorWithMerge
|
|
EXPECT_EQ(node->GetInputName(olive::ShapeNodeBase::kPositionInput),
|
|
QStringLiteral("Position"));
|
|
EXPECT_EQ(node->GetInputName(olive::ShapeNodeBase::kSizeInput),
|
|
QStringLiteral("Size"));
|
|
EXPECT_EQ(node->GetInputName(olive::ShapeNodeBase::kColorInput),
|
|
QStringLiteral("Color"));
|
|
EXPECT_EQ(node->GetInputName(olive::GeneratorWithMerge::kBaseInput),
|
|
QStringLiteral("Base"));
|
|
|
|
const QStringList aligns =
|
|
node->GetComboBoxStrings(olive::TextGeneratorV2::kVAlignInput);
|
|
ASSERT_EQ(aligns.size(), 3);
|
|
EXPECT_EQ(aligns.at(0), QStringLiteral("Top"));
|
|
EXPECT_EQ(aligns.at(1), QStringLiteral("Center"));
|
|
EXPECT_EQ(aligns.at(2), QStringLiteral("Bottom"));
|
|
}
|
|
|
|
TEST(TextGeneratorV2, ValuePushesFloatTextureWithGenerateJob)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV2>(&project);
|
|
|
|
const olive::VideoParams vparams(320, 240, olive::core::PixelFormat::U8,
|
|
olive::VideoParams::kRGBAChannelCount);
|
|
olive::NodeValueTable table = GenerateTable(node, vparams);
|
|
|
|
// Text always renders to a 32-bit float buffer regardless of sequence depth
|
|
olive::TexturePtr texture = GetOutputTexture(table);
|
|
ASSERT_TRUE(texture);
|
|
ASSERT_TRUE(texture->IsJob());
|
|
EXPECT_EQ(texture->params().width(), vparams.width());
|
|
EXPECT_EQ(texture->params().height(), vparams.height());
|
|
EXPECT_EQ(int(texture->params().format()),
|
|
int(olive::core::PixelFormat::F32));
|
|
|
|
auto *job = dynamic_cast<olive::GenerateJob *>(texture->job());
|
|
ASSERT_TRUE(job);
|
|
EXPECT_EQ(job->Get(olive::TextGeneratorV2::kTextInput).toString(),
|
|
QStringLiteral("Sample Text"));
|
|
EXPECT_DOUBLE_EQ(job->Get(olive::TextGeneratorV2::kFontSizeInput).toDouble(),
|
|
72.0);
|
|
EXPECT_EQ(job->Get(olive::TextGeneratorV2::kVAlignInput).toInt(), 0);
|
|
EXPECT_EQ(job->Get(olive::ShapeNodeBase::kSizeInput).toVec2(),
|
|
QVector2D(400.0f, 300.0f));
|
|
}
|
|
|
|
TEST(TextGeneratorV2, EmptyTextPushesNothing)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV2>(&project);
|
|
node->SetStandardValue(olive::TextGeneratorV2::kTextInput, QString());
|
|
|
|
olive::NodeValueTable table = GenerateTable(
|
|
node, olive::VideoParams(320, 240, olive::core::PixelFormat::U8,
|
|
olive::VideoParams::kRGBAChannelCount));
|
|
|
|
EXPECT_TRUE(GetOutputTexture(table) == nullptr);
|
|
}
|
|
|
|
TEST(TextGeneratorV2, ValueIgnoresBaseInput)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV2>(&project);
|
|
auto *constant = AddNode<ConstantTextureNode>(&project);
|
|
|
|
const olive::TexturePtr base = std::make_shared<olive::Texture>(
|
|
olive::VideoParams(64, 48, olive::core::PixelFormat::U8,
|
|
olive::VideoParams::kRGBAChannelCount));
|
|
constant->SetTexture(base);
|
|
olive::Node::ConnectEdge(constant,
|
|
olive::NodeInput(
|
|
node, olive::GeneratorWithMerge::kBaseInput));
|
|
|
|
olive::NodeValueTable table = GenerateTable(
|
|
node, olive::VideoParams(320, 240, olive::core::PixelFormat::F32,
|
|
olive::VideoParams::kRGBAChannelCount));
|
|
|
|
// Unlike TextGeneratorV3, which composites its text over the base input,
|
|
// the legacy V2 node never looks at it: the output is its own generate
|
|
// job at sequence params, not a merge with the base
|
|
olive::TexturePtr texture = GetOutputTexture(table);
|
|
ASSERT_TRUE(texture);
|
|
ASSERT_TRUE(texture->IsJob());
|
|
EXPECT_EQ(texture->params().width(), 320);
|
|
EXPECT_EQ(texture->params().height(), 240);
|
|
EXPECT_TRUE(dynamic_cast<olive::GenerateJob *>(texture->job()));
|
|
}
|
|
|
|
TEST(TextGeneratorV2, GenerateFrameWithEmptyTextLeavesFrameTransparent)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV2>(&project);
|
|
node->SetStandardValue(olive::TextGeneratorV2::kTextInput, QString());
|
|
|
|
// Walk the vertical alignment switch and the HTML branch with empty text:
|
|
// no glyphs are drawn, so the transplant loop writes pure zeros
|
|
olive::NodeValueRow row = GenerateRow(node);
|
|
for (int valign = 0; valign <= 2; valign++) {
|
|
for (int html = 0; html <= 1; html++) {
|
|
row[olive::TextGeneratorV2::kVAlignInput] =
|
|
olive::NodeValue(olive::NodeValue::kCombo, valign);
|
|
row[olive::TextGeneratorV2::kHtmlInput] =
|
|
olive::NodeValue(olive::NodeValue::kBoolean, bool(html));
|
|
|
|
olive::FramePtr frame = olive::Frame::Create();
|
|
frame->set_video_params(
|
|
olive::VideoParams(64, 48, olive::core::PixelFormat::F32,
|
|
olive::VideoParams::kRGBAChannelCount));
|
|
frame->allocate();
|
|
|
|
node->GenerateFrame(frame, olive::GenerateJob(row));
|
|
|
|
const float *data = reinterpret_cast<const float *>(frame->data());
|
|
const int pixel_count =
|
|
frame->linesize_pixels() * frame->height() *
|
|
olive::VideoParams::kRGBAChannelCount;
|
|
float max_abs = 0.0f;
|
|
for (int i = 0; i < pixel_count; i++) {
|
|
max_abs = qMax(max_abs, qAbs(data[i]));
|
|
}
|
|
EXPECT_FLOAT_EQ(max_abs, 0.0f)
|
|
<< "valign " << valign << ", html " << html;
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(TextGeneratorV2, GenerateFrameRasterizesTextInColor)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV2>(&project);
|
|
node->SetStandardValue(
|
|
olive::ShapeNodeBase::kColorInput,
|
|
QVariant::fromValue(olive::core::Color(1.0f, 0.0f, 0.0f, 1.0f)));
|
|
|
|
olive::FramePtr frame = olive::Frame::Create();
|
|
frame->set_video_params(
|
|
olive::VideoParams(320, 240, olive::core::PixelFormat::F32,
|
|
olive::VideoParams::kRGBAChannelCount));
|
|
frame->allocate();
|
|
|
|
node->GenerateFrame(frame, olive::GenerateJob(GenerateRow(node)));
|
|
|
|
// The alpha mask of the rendered glyphs is tinted by the color input:
|
|
// red premultiplied text has red == alpha and zero green/blue everywhere
|
|
const float *data = reinterpret_cast<const float *>(frame->data());
|
|
bool any_alpha = false;
|
|
bool any_green_or_blue = false;
|
|
bool red_matches_alpha = true;
|
|
for (int y = 0; y < frame->height(); y++) {
|
|
for (int x = 0; x < frame->width(); x++) {
|
|
const float *px = data +
|
|
(y * frame->linesize_pixels() + x) *
|
|
olive::VideoParams::kRGBAChannelCount;
|
|
any_alpha |= px[3] > 0.0f;
|
|
any_green_or_blue |= (px[1] != 0.0f || px[2] != 0.0f);
|
|
red_matches_alpha &= (px[0] == px[3]);
|
|
}
|
|
}
|
|
EXPECT_TRUE(any_alpha);
|
|
EXPECT_FALSE(any_green_or_blue);
|
|
EXPECT_TRUE(red_matches_alpha);
|
|
}
|
|
|
|
TEST(TextGeneratorV1, MetadataIsCorrect)
|
|
{
|
|
olive::TextGeneratorV1 node;
|
|
EXPECT_EQ(node.id(),
|
|
QStringLiteral("org.olivevideoeditor.Olive.textgenerator"));
|
|
EXPECT_EQ(node.Name(), QStringLiteral("Text (Legacy)"));
|
|
EXPECT_FALSE(node.Description().isEmpty());
|
|
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryGenerator));
|
|
|
|
// Hidden from the create menu: superseded by TextGeneratorV3
|
|
EXPECT_TRUE(node.GetFlags() & olive::Node::kDontShowInCreateMenu);
|
|
}
|
|
|
|
TEST(TextGeneratorV1, InputDefaults)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV1>(&project);
|
|
|
|
EXPECT_EQ(node->GetStandardValue(olive::TextGeneratorV1::kTextInput)
|
|
.toString(),
|
|
QStringLiteral("Sample Text"));
|
|
|
|
EXPECT_EQ(int(node->GetInputDataType(olive::TextGeneratorV1::kHtmlInput)),
|
|
int(olive::NodeValue::kBoolean));
|
|
EXPECT_FALSE(node->GetStandardValue(olive::TextGeneratorV1::kHtmlInput)
|
|
.toBool());
|
|
|
|
EXPECT_EQ(int(node->GetInputDataType(olive::TextGeneratorV1::kColorInput)),
|
|
int(olive::NodeValue::kColor));
|
|
const olive::core::Color color =
|
|
node->GetStandardValue(olive::TextGeneratorV1::kColorInput)
|
|
.value<olive::core::Color>();
|
|
EXPECT_FLOAT_EQ(color.red(), 1.0f);
|
|
EXPECT_FLOAT_EQ(color.green(), 1.0f);
|
|
EXPECT_FLOAT_EQ(color.blue(), 1.0f);
|
|
EXPECT_FLOAT_EQ(color.alpha(), 1.0f);
|
|
|
|
// Unlike V2, V1 defaults to centered vertical alignment
|
|
EXPECT_EQ(int(node->GetInputDataType(olive::TextGeneratorV1::kVAlignInput)),
|
|
int(olive::NodeValue::kCombo));
|
|
EXPECT_EQ(node->GetStandardValue(olive::TextGeneratorV1::kVAlignInput)
|
|
.toInt(),
|
|
1);
|
|
|
|
EXPECT_EQ(
|
|
int(node->GetInputDataType(olive::TextGeneratorV1::kFontSizeInput)),
|
|
int(olive::NodeValue::kFloat));
|
|
EXPECT_DOUBLE_EQ(node->GetStandardValue(olive::TextGeneratorV1::kFontSizeInput)
|
|
.toDouble(),
|
|
72.0);
|
|
}
|
|
|
|
TEST(TextGeneratorV1, RetranslateSetsNamesAndComboStrings)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV1>(&project);
|
|
node->Retranslate();
|
|
|
|
EXPECT_EQ(node->GetInputName(olive::TextGeneratorV1::kTextInput),
|
|
QStringLiteral("Text"));
|
|
EXPECT_EQ(node->GetInputName(olive::TextGeneratorV1::kHtmlInput),
|
|
QStringLiteral("Enable HTML"));
|
|
EXPECT_EQ(node->GetInputName(olive::TextGeneratorV1::kFontInput),
|
|
QStringLiteral("Font"));
|
|
EXPECT_EQ(node->GetInputName(olive::TextGeneratorV1::kFontSizeInput),
|
|
QStringLiteral("Font Size"));
|
|
EXPECT_EQ(node->GetInputName(olive::TextGeneratorV1::kColorInput),
|
|
QStringLiteral("Color"));
|
|
EXPECT_EQ(node->GetInputName(olive::TextGeneratorV1::kVAlignInput),
|
|
QStringLiteral("Vertical Align"));
|
|
|
|
const QStringList aligns =
|
|
node->GetComboBoxStrings(olive::TextGeneratorV1::kVAlignInput);
|
|
ASSERT_EQ(aligns.size(), 3);
|
|
EXPECT_EQ(aligns.at(0), QStringLiteral("Top"));
|
|
EXPECT_EQ(aligns.at(1), QStringLiteral("Center"));
|
|
EXPECT_EQ(aligns.at(2), QStringLiteral("Bottom"));
|
|
}
|
|
|
|
TEST(TextGeneratorV1, ValuePushesTextureAtSequenceParams)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV1>(&project);
|
|
|
|
const olive::VideoParams vparams(320, 240, olive::core::PixelFormat::U8,
|
|
olive::VideoParams::kRGBAChannelCount);
|
|
olive::NodeValueTable table = GenerateTable(node, vparams);
|
|
|
|
// Unlike V2, V1 keeps the sequence pixel format for its output
|
|
olive::TexturePtr texture = GetOutputTexture(table);
|
|
ASSERT_TRUE(texture);
|
|
ASSERT_TRUE(texture->IsJob());
|
|
EXPECT_EQ(texture->params().width(), vparams.width());
|
|
EXPECT_EQ(texture->params().height(), vparams.height());
|
|
EXPECT_EQ(int(texture->params().format()),
|
|
int(olive::core::PixelFormat::U8));
|
|
|
|
auto *job = dynamic_cast<olive::GenerateJob *>(texture->job());
|
|
ASSERT_TRUE(job);
|
|
EXPECT_EQ(job->Get(olive::TextGeneratorV1::kTextInput).toString(),
|
|
QStringLiteral("Sample Text"));
|
|
EXPECT_DOUBLE_EQ(job->Get(olive::TextGeneratorV1::kFontSizeInput).toDouble(),
|
|
72.0);
|
|
}
|
|
|
|
TEST(TextGeneratorV1, EmptyTextPushesNothing)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV1>(&project);
|
|
node->SetStandardValue(olive::TextGeneratorV1::kTextInput, QString());
|
|
|
|
olive::NodeValueTable table = GenerateTable(
|
|
node, olive::VideoParams(320, 240, olive::core::PixelFormat::U8,
|
|
olive::VideoParams::kRGBAChannelCount));
|
|
|
|
EXPECT_TRUE(GetOutputTexture(table) == nullptr);
|
|
}
|
|
|
|
TEST(TextGeneratorV1, GenerateFrameWithEmptyTextLeavesFrameBlack)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV1>(&project);
|
|
node->SetStandardValue(olive::TextGeneratorV1::kTextInput, QString());
|
|
|
|
// Walk the vertical alignment switch and the HTML branch with empty text:
|
|
// no glyphs are drawn, so every pixel is set to transparent black
|
|
olive::NodeValueRow row = GenerateRow(node);
|
|
for (int valign = 0; valign <= 2; valign++) {
|
|
for (int html = 0; html <= 1; html++) {
|
|
row[olive::TextGeneratorV1::kVAlignInput] =
|
|
olive::NodeValue(olive::NodeValue::kCombo, valign);
|
|
row[olive::TextGeneratorV1::kHtmlInput] =
|
|
olive::NodeValue(olive::NodeValue::kBoolean, bool(html));
|
|
|
|
olive::FramePtr frame = olive::Frame::Create();
|
|
frame->set_video_params(
|
|
olive::VideoParams(64, 48, olive::core::PixelFormat::F32,
|
|
olive::VideoParams::kRGBAChannelCount));
|
|
frame->allocate();
|
|
|
|
node->GenerateFrame(frame, olive::GenerateJob(row));
|
|
|
|
const float *data = reinterpret_cast<const float *>(frame->data());
|
|
const int pixel_count =
|
|
frame->linesize_pixels() * frame->height() *
|
|
olive::VideoParams::kRGBAChannelCount;
|
|
float max_abs = 0.0f;
|
|
for (int i = 0; i < pixel_count; i++) {
|
|
max_abs = qMax(max_abs, qAbs(data[i]));
|
|
}
|
|
EXPECT_FLOAT_EQ(max_abs, 0.0f)
|
|
<< "valign " << valign << ", html " << html;
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(TextGeneratorV1, GenerateFrameRasterizesTextPixels)
|
|
{
|
|
olive::ColorManager::SetUpDefaultConfig();
|
|
olive::Project project;
|
|
project.Initialize();
|
|
|
|
auto *node = AddNode<olive::TextGeneratorV1>(&project);
|
|
|
|
olive::FramePtr frame = olive::Frame::Create();
|
|
frame->set_video_params(
|
|
olive::VideoParams(320, 240, olive::core::PixelFormat::F32,
|
|
olive::VideoParams::kRGBAChannelCount));
|
|
frame->allocate();
|
|
|
|
node->GenerateFrame(frame, olive::GenerateJob(GenerateRow(node)));
|
|
|
|
// The default white text is written premultiplied: any covered pixel has
|
|
// all channels equal to its alpha
|
|
const float *data = reinterpret_cast<const float *>(frame->data());
|
|
bool any_alpha = false;
|
|
bool channels_match_alpha = true;
|
|
for (int y = 0; y < frame->height(); y++) {
|
|
for (int x = 0; x < frame->width(); x++) {
|
|
const float *px = data +
|
|
(y * frame->linesize_pixels() + x) *
|
|
olive::VideoParams::kRGBAChannelCount;
|
|
any_alpha |= px[3] > 0.0f;
|
|
channels_match_alpha &=
|
|
(px[0] == px[3] && px[1] == px[3] && px[2] == px[3]);
|
|
}
|
|
}
|
|
EXPECT_TRUE(any_alpha);
|
|
EXPECT_TRUE(channels_match_alpha);
|
|
}
|