Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0438cb6
Add initial Lottie JSON importer
pgilfernandez Jun 16, 2026
1b0548c
Improve Lottie shape coverage
pgilfernandez Jun 16, 2026
5c2ebd8
Support dotLottie imports
pgilfernandez Jun 16, 2026
c705780
Import Lottie gradients
pgilfernandez Jun 16, 2026
700119a
Import Lottie stroke line styles
pgilfernandez Jun 16, 2026
4eef4ac
Import animated Lottie shape geometry
pgilfernandez Jun 16, 2026
13b4c7a
Import Lottie layer visibility ranges
pgilfernandez Jun 16, 2026
c54ec53
Skip hidden Lottie items
pgilfernandez Jun 16, 2026
684ce49
Preserve Lottie group opacity
pgilfernandez Jun 16, 2026
2f8106a
Import animated Lottie rectangle radius
pgilfernandez Jun 16, 2026
4e8515d
Import Lottie path modifiers
pgilfernandez Jun 16, 2026
2bd415d
Import Lottie structural layers
pgilfernandez Jun 16, 2026
cdd30dc
Import Lottie layer parenting
pgilfernandez Jun 16, 2026
67de94d
Import Lottie image layers
pgilfernandez Jun 16, 2026
5f50575
Import animated Lottie polystars
pgilfernandez Jun 17, 2026
66a231f
Import Lottie trim as sub-path effect
pgilfernandez Jun 17, 2026
6cd37ee
Import Lottie skew transforms
pgilfernandez Jun 17, 2026
614a0ce
Import Lottie blend modes
pgilfernandez Jun 17, 2026
45530a1
Import Lottie layer masks
pgilfernandez Jun 17, 2026
9d0f0c6
Import Lottie track mattes
pgilfernandez Jun 17, 2026
ae4796d
Import Lottie masks on more layer types
pgilfernandez Jun 18, 2026
2b723b5
Import basic Lottie text layers
pgilfernandez Jun 18, 2026
1960c15
Extract embedded dotLottie assets
pgilfernandez Jun 18, 2026
e8e3aa5
Import animated Lottie text content
pgilfernandez Jun 18, 2026
c7658e3
Import Lottie text spacing
pgilfernandez Jun 18, 2026
55eb986
Add Lottie scale scene import mode
pgilfernandez Jun 18, 2026
412bd07
Fix Lottie import file dialog
pgilfernandez Jul 10, 2026
fc6f575
Use bundled zlib fallback for Lottie import
pgilfernandez Jul 10, 2026
a0f1be0
Install zlib for Windows CI
pgilfernandez Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Support dotLottie imports
  • Loading branch information
pgilfernandez committed Jul 10, 2026
commit 5c2ebd8495dd443298875cc5af496d7771c8dfbd
2 changes: 2 additions & 0 deletions src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ include(friction-meta)
include(friction-common)
#include(friction-quazip)
include(friction-ffmpeg)
find_package(ZLIB REQUIRED)

message("-- ffmpeg include: ${FFMPEG_INCLUDE_DIRS}")
message("-- ffmpeg libraries: ${FFMPEG_LIBRARIES_DIRS} ${FFMPEG_LIBRARIES}")
Expand Down Expand Up @@ -767,6 +768,7 @@ target_link_libraries(
#${QUAZIP_LIBRARIES}
${FFMPEG_LIBRARIES}
${SKIA_LIBRARIES}
ZLIB::ZLIB
)

if(APPLE)
Expand Down
195 changes: 188 additions & 7 deletions src/core/lottie/lottieimporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QMap>
#include <QtEndian>
#include <QtMath>

#include <cstring>

#include <zlib.h>

namespace {

struct KeyValue {
Expand All @@ -53,24 +59,199 @@ struct PaintStyle {
qreal strokeWidth = 1;
};

QJsonObject readLottieRoot(const QString& filename)
struct ZipEntry {
QString name;
quint16 method = 0;
quint32 compressedSize = 0;
quint32 uncompressedSize = 0;
quint32 localHeaderOffset = 0;
};

quint16 readLe16(const QByteArray& data, const int offset)
{
if (filename.endsWith(QStringLiteral(".lottie"), Qt::CaseInsensitive)) {
RuntimeThrow("dotLottie import is not implemented yet. Use Lottie JSON.");
if (offset < 0 || offset + 2 > data.size()) { RuntimeThrow("Invalid ZIP data"); }
return qFromLittleEndian<quint16>(
reinterpret_cast<const uchar*>(data.constData() + offset));
}

quint32 readLe32(const QByteArray& data, const int offset)
{
if (offset < 0 || offset + 4 > data.size()) { RuntimeThrow("Invalid ZIP data"); }
return qFromLittleEndian<quint32>(
reinterpret_cast<const uchar*>(data.constData() + offset));
}

int findZipEndOfCentralDirectory(const QByteArray& data)
{
const int first = qMax(0, data.size() - 65557);
for (int offset = data.size() - 22; offset >= first; --offset) {
if (readLe32(data, offset) == 0x06054b50) { return offset; }
}
RuntimeThrow("Could not find dotLottie ZIP directory");
}

QFile file(filename);
if (!file.open(QIODevice::ReadOnly)) {
RuntimeThrow("Cannot open Lottie file " + filename);
QList<ZipEntry> readZipEntries(const QByteArray& data)
{
const int end = findZipEndOfCentralDirectory(data);
const quint16 entryCount = readLe16(data, end + 10);
const quint32 directorySize = readLe32(data, end + 12);
const quint32 directoryOffset = readLe32(data, end + 16);
if (directoryOffset + directorySize > static_cast<quint32>(data.size())) {
RuntimeThrow("Invalid dotLottie ZIP directory");
}

QList<ZipEntry> entries;
int offset = directoryOffset;
for (int i = 0; i < entryCount; ++i) {
if (readLe32(data, offset) != 0x02014b50) {
RuntimeThrow("Invalid dotLottie ZIP entry");
}
const quint16 nameLength = readLe16(data, offset + 28);
const quint16 extraLength = readLe16(data, offset + 30);
const quint16 commentLength = readLe16(data, offset + 32);
const int nameOffset = offset + 46;
if (nameOffset + nameLength > data.size()) {
RuntimeThrow("Invalid dotLottie ZIP file name");
}

ZipEntry entry;
entry.method = readLe16(data, offset + 10);
entry.compressedSize = readLe32(data, offset + 20);
entry.uncompressedSize = readLe32(data, offset + 24);
entry.localHeaderOffset = readLe32(data, offset + 42);
entry.name = QString::fromUtf8(data.constData() + nameOffset, nameLength);
entries.append(entry);
offset = nameOffset + nameLength + extraLength + commentLength;
}
return entries;
}

QByteArray inflateRawDeflate(const QByteArray& input, const int outputSize)
{
QByteArray output(outputSize, Qt::Uninitialized);
z_stream stream;
memset(&stream, 0, sizeof(stream));
stream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(input.constData()));
stream.avail_in = input.size();
stream.next_out = reinterpret_cast<Bytef*>(output.data());
stream.avail_out = output.size();

if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) {
RuntimeThrow("Could not initialize dotLottie decompression");
}
const int result = inflate(&stream, Z_FINISH);
inflateEnd(&stream);
if (result != Z_STREAM_END) {
RuntimeThrow("Could not decompress dotLottie entry");
}
output.truncate(stream.total_out);
return output;
}

QByteArray readZipEntry(const QByteArray& data, const ZipEntry& entry)
{
const int headerOffset = entry.localHeaderOffset;
if (readLe32(data, headerOffset) != 0x04034b50) {
RuntimeThrow("Invalid dotLottie ZIP local entry");
}
const quint16 nameLength = readLe16(data, headerOffset + 26);
const quint16 extraLength = readLe16(data, headerOffset + 28);
const int dataOffset = headerOffset + 30 + nameLength + extraLength;
if (dataOffset + entry.compressedSize > static_cast<quint32>(data.size())) {
RuntimeThrow("Invalid dotLottie ZIP entry data");
}

const QByteArray compressed = data.mid(dataOffset, entry.compressedSize);
if (entry.method == 0) { return compressed; }
if (entry.method == 8) {
return inflateRawDeflate(compressed, entry.uncompressedSize);
}
RuntimeThrow("Unsupported dotLottie ZIP compression method");
}

QJsonObject jsonObjectFromBytes(const QByteArray& bytes, const QString& filename)
{
QJsonParseError error;
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &error);
const QJsonDocument document = QJsonDocument::fromJson(bytes, &error);
if (error.error != QJsonParseError::NoError || !document.isObject()) {
RuntimeThrow("Cannot parse Lottie file " + filename);
}
return document.object();
}

QJsonObject readDotLottieRoot(const QString& filename)
{
QFile file(filename);
if (!file.open(QIODevice::ReadOnly)) {
RuntimeThrow("Cannot open Lottie file " + filename);
}
const QByteArray zipData = file.readAll();
const QList<ZipEntry> entries = readZipEntries(zipData);

QMap<QString, ZipEntry> files;
for (const ZipEntry& entry : entries) {
if (!entry.name.endsWith(QLatin1Char('/'))) {
files.insert(entry.name, entry);
}
}

QString animationPath;
if (files.contains(QStringLiteral("manifest.json"))) {
const QJsonObject manifest = jsonObjectFromBytes(
readZipEntry(zipData, files.value(QStringLiteral("manifest.json"))),
filename + QStringLiteral(":manifest.json"));
const QJsonArray animations = manifest.value(QStringLiteral("animations")).toArray();
if (!animations.isEmpty()) {
const QJsonObject animation = animations.first().toObject();
animationPath = animation.value(QStringLiteral("url")).toString();
if (animationPath.isEmpty()) {
const QString id = animation.value(QStringLiteral("id")).toString();
if (!id.isEmpty()) {
animationPath = QStringLiteral("animations/%1.json").arg(id);
}
}
}
}

if (animationPath.isEmpty() || !files.contains(animationPath)) {
for (const QString& path : files.keys()) {
if (path.startsWith(QStringLiteral("animations/")) &&
path.endsWith(QStringLiteral(".json"), Qt::CaseInsensitive)) {
animationPath = path;
break;
}
}
}
if (animationPath.isEmpty() || !files.contains(animationPath)) {
for (const QString& path : files.keys()) {
if (path != QStringLiteral("manifest.json") &&
path.endsWith(QStringLiteral(".json"), Qt::CaseInsensitive)) {
animationPath = path;
break;
}
}
}
if (animationPath.isEmpty() || !files.contains(animationPath)) {
RuntimeThrow("No animation JSON found in dotLottie file");
}

return jsonObjectFromBytes(readZipEntry(zipData, files.value(animationPath)),
filename + QStringLiteral(":") + animationPath);
}

QJsonObject readLottieRoot(const QString& filename)
{
if (filename.endsWith(QStringLiteral(".lottie"), Qt::CaseInsensitive)) {
return readDotLottieRoot(filename);
}

QFile file(filename);
if (!file.open(QIODevice::ReadOnly)) {
RuntimeThrow("Cannot open Lottie file " + filename);
}
return jsonObjectFromBytes(file.readAll(), filename);
}

QJsonValue propertyValue(const QJsonObject& property)
{
if (!property.contains(QStringLiteral("k"))) { return {}; }
Expand Down