Coverage Report

Created: 2026-01-25 07:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/kdegraphics-thumbnailers/blend/blendercreator.cpp
Line
Count
Source
1
/*
2
 * SPDX-FileCopyrightText: 2019 Chinmoy Ranjan Pradhan <chinmoyrp65@gmail.com>
3
 *
4
 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5
 */
6
7
#include "blendercreator.h"
8
9
#include <QFile>
10
#include <QImage>
11
#include <QPointer>
12
#include <QtEndian>
13
14
#include <KCompressionDevice>
15
#include <KPluginFactory>
16
17
0
K_PLUGIN_CLASS_WITH_JSON(BlenderCreator, "blenderthumbnail.json")
Unexecuted instantiation: blenderthumbnail_factory::tr(char const*, char const*, int)
Unexecuted instantiation: blenderthumbnail_factory::~blenderthumbnail_factory()
18
0
19
0
BlenderCreator::BlenderCreator(QObject *parent, const QVariantList &args)
20
11.5k
    : KIO::ThumbnailCreator(parent, args)
21
11.5k
{
22
11.5k
}
23
24
11.5k
BlenderCreator::~BlenderCreator() = default;
25
26
// For more info. see https://developer.blender.org/diffusion/B/browse/master/release/bin/blender-thumbnailer.py
27
28
KIO::ThumbnailResult BlenderCreator::create(const KIO::ThumbnailRequest &request)
29
11.5k
{
30
11.5k
    std::unique_ptr<QIODevice> device = std::make_unique<QFile>(request.url().toLocalFile());
31
11.5k
    if(!device->open(QIODevice::ReadOnly)) {
32
0
        return KIO::ThumbnailResult::fail();
33
0
    }
34
35
    // Blender has an option to save files with zstd or gzip compression. First check if we are dealing with such files.
36
11.5k
    QByteArray header = device->peek(4);
37
11.5k
    if (header.size() == 4) {
38
11.4k
        const uint8_t *h = reinterpret_cast<const uint8_t *>(header.constData());
39
11.4k
        uint32_t magic = h[0] | (h[1] << 8) | (h[2] << 16) | (h[3] << 24);
40
        // A zstd archive may start with a regular or skippable frame
41
        // - 0xFD2FB528 is the magic number for zstd regular frame
42
        // - 0x184D2A5 is used to detect skippable frames by checking the top 28 bits (ignoring the lower 4 bits)
43
        //   skippable frame magic values are in the range 0x184D2A50..0x184D2A5F, so shifting right by 4 masks them all
44
        //   see:
45
        // - https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#zstandard-frames
46
        // - https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#skippable-frames
47
11.4k
        if (magic == 0xFD2FB528 || (magic >> 4) == 0x184D2A5) {
48
7.50k
            device = std::make_unique<KCompressionDevice>(std::move(device), KCompressionDevice::Zstd);
49
7.50k
            if (!device->open(QIODevice::ReadOnly)) {
50
0
                return KIO::ThumbnailResult::fail();
51
0
            }
52
7.50k
        }
53
        // In earlier versions of Blender, files were compressed using gzip.
54
3.92k
        else if (header.startsWith("\x1F\x8B")) { // gzip magic (each gzip member starts with ID1(0x1f) and ID2(0x8b))
55
2.70k
            device = std::make_unique<KCompressionDevice>(std::move(device), KCompressionDevice::GZip);
56
2.70k
            if (!device->open(QIODevice::ReadOnly)) {
57
0
                return KIO::ThumbnailResult::fail();
58
0
            }
59
2.70k
        }
60
11.4k
    }
61
62
11.5k
    QDataStream blendStream;
63
11.5k
    blendStream.setDevice(device.get());
64
65
    // First to check is file header.
66
    // BLEND file header format
67
    // Reference      Content                                     Size
68
    // id             "BLENDER"                                    7
69
    // pointer-size   _ (underscore)(32 bit)/ - (minus)(64 bit)    1
70
    // endianness     v (little) / V (big)                         1
71
    // version        "248" = 2.48 etc.                            3
72
73
    // Example header: "BLENDER-v257"
74
75
11.5k
    QByteArray head(12, '\0');
76
11.5k
    blendStream.readRawData(head.data(), 12);
77
11.5k
    if(!head.startsWith("BLENDER") || head.right(3).toInt() < 250 /*blender pre 2.5 had no thumbs*/) {
78
9.38k
        return KIO::ThumbnailResult::fail();
79
9.38k
    }
80
81
    // Next is file block. This we have to skip.
82
    // File block header format
83
    //    Reference      Content                                               Size
84
    // 1. id             "REND","TEST", etc.                                    4
85
    // 2. size           Total length of the data after the file-block-header   4
86
    // 3. old mem. addr  Mem. address.                                          pointer-size i.e, 4(32bit)/8(64bit)
87
    // 4. SDNA index     Index of SDNA struct                                   4
88
    // 5. count          No. of struct in file-block                            4
89
90
2.13k
    const bool isLittleEndian = head[8] == 'v';
91
3.46k
    auto toInt32 = [isLittleEndian](const QByteArray &bytes) {
92
3.46k
        return isLittleEndian ? qFromLittleEndian<qint32>(bytes.constData())
93
3.46k
                              : qFromBigEndian<qint32>(bytes.constData());
94
3.46k
    };
95
96
2.13k
    const bool is64Bit = head[7] == '-';
97
2.13k
    const int fileBlockHeaderSize = is64Bit ? 24 : 20; // size of file block header fields 1 to 5
98
2.13k
    QByteArray fileBlockHeader(fileBlockHeaderSize, '\0');
99
2.13k
    qint32 fileBlockSize = 0;
100
4.32k
    while (true) {
101
4.32k
        const int read = blendStream.readRawData(fileBlockHeader.data(), fileBlockHeaderSize);
102
4.32k
        if (read != fileBlockHeaderSize) {
103
1.66k
            return KIO::ThumbnailResult::fail();
104
1.66k
        }
105
2.65k
        fileBlockSize = toInt32(fileBlockHeader.mid(4, 4)); // second header field
106
        // skip actual file-block data.
107
2.65k
        if (fileBlockHeader.startsWith("REND")) {
108
2.19k
            blendStream.skipRawData(fileBlockSize);
109
2.19k
        } else {
110
465
            break;
111
465
        }
112
2.65k
    }
113
114
465
    if(!fileBlockHeader.startsWith("TEST")) {
115
64
        return KIO::ThumbnailResult::fail();
116
64
    }
117
118
    // Now comes actual thumbnail image data.
119
401
    QByteArray xy(8, '\0');
120
401
    blendStream.readRawData(xy.data(), 8);
121
401
    const qint32 x = toInt32(xy.left(4));
122
401
    const qint32 y = toInt32(xy.right(4));
123
401
    const qint32 imgSize = fileBlockSize - 8;
124
401
    if (imgSize <= 0 || x <= 0 || y <= 0) {
125
143
        return KIO::ThumbnailResult::fail();
126
143
    }
127
258
    if (imgSize / 4 / y != x) {
128
70
        return KIO::ThumbnailResult::fail();
129
70
    }
130
131
188
    QByteArray imgBuffer(imgSize, '\0');
132
188
    blendStream.readRawData(imgBuffer.data(), imgSize);
133
188
    QImage thumbnail((const uchar*)imgBuffer.constData(), x, y, QImage::Format_ARGB32);
134
188
    if(request.targetSize().width() != 128) {
135
0
        thumbnail = thumbnail.scaledToWidth(request.targetSize().width(), Qt::SmoothTransformation);
136
0
    }
137
188
    if(request.targetSize().height() != 128) {
138
0
        thumbnail = thumbnail.scaledToHeight(request.targetSize().height(), Qt::SmoothTransformation);
139
0
    }
140
188
    thumbnail = thumbnail.rgbSwapped();
141
188
    thumbnail = thumbnail.mirrored();
142
188
    QImage img = thumbnail.convertToFormat(QImage::Format_ARGB32_Premultiplied);
143
144
188
    return !img.isNull() ? KIO::ThumbnailResult::pass(img) : KIO::ThumbnailResult::fail();
145
258
}
146
147
#include "blendercreator.moc"