Coverage Report

Created: 2026-03-12 07:14

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
16.2k
    : KIO::ThumbnailCreator(parent, args)
21
16.2k
{
22
16.2k
}
23
24
16.2k
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
16.2k
{
30
16.2k
    std::unique_ptr<QIODevice> device = std::make_unique<QFile>(request.url().toLocalFile());
31
16.2k
    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
16.2k
    QByteArray header = device->peek(4);
37
16.2k
    if (header.size() == 4) {
38
16.1k
        const uint8_t *h = reinterpret_cast<const uint8_t *>(header.constData());
39
16.1k
        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
16.1k
        if (magic == 0xFD2FB528 || (magic >> 4) == 0x184D2A5) {
48
10.7k
            device = std::make_unique<KCompressionDevice>(std::move(device), KCompressionDevice::Zstd);
49
10.7k
            if (!device->open(QIODevice::ReadOnly)) {
50
0
                return KIO::ThumbnailResult::fail();
51
0
            }
52
10.7k
        }
53
        // In earlier versions of Blender, files were compressed using gzip.
54
5.46k
        else if (header.startsWith("\x1F\x8B")) { // gzip magic (each gzip member starts with ID1(0x1f) and ID2(0x8b))
55
3.87k
            device = std::make_unique<KCompressionDevice>(std::move(device), KCompressionDevice::GZip);
56
3.87k
            if (!device->open(QIODevice::ReadOnly)) {
57
0
                return KIO::ThumbnailResult::fail();
58
0
            }
59
3.87k
        }
60
16.1k
    }
61
62
16.2k
    QDataStream blendStream;
63
16.2k
    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
16.2k
    QByteArray head(12, '\0');
76
16.2k
    blendStream.readRawData(head.data(), 12);
77
16.2k
    if(!head.startsWith("BLENDER") || head.right(3).toInt() < 250 /*blender pre 2.5 had no thumbs*/) {
78
12.2k
        return KIO::ThumbnailResult::fail();
79
12.2k
    }
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
4.03k
    const bool isLittleEndian = head[8] == 'v';
91
17.0k
    auto toInt32 = [isLittleEndian](const QByteArray &bytes) {
92
17.0k
        return isLittleEndian ? qFromLittleEndian<qint32>(bytes.constData())
93
17.0k
                              : qFromBigEndian<qint32>(bytes.constData());
94
17.0k
    };
95
96
4.03k
    const bool is64Bit = head[7] == '-';
97
4.03k
    const int fileBlockHeaderSize = is64Bit ? 24 : 20; // size of file block header fields 1 to 5
98
4.03k
    QByteArray fileBlockHeader(fileBlockHeaderSize, '\0');
99
4.03k
    qint32 fileBlockSize = 0;
100
19.7k
    while (true) {
101
19.7k
        const int read = blendStream.readRawData(fileBlockHeader.data(), fileBlockHeaderSize);
102
19.7k
        if (read != fileBlockHeaderSize) {
103
3.45k
            return KIO::ThumbnailResult::fail();
104
3.45k
        }
105
16.2k
        fileBlockSize = toInt32(fileBlockHeader.mid(4, 4)); // second header field
106
        // skip actual file-block data.
107
16.2k
        if (fileBlockHeader.startsWith("REND")) {
108
15.6k
            blendStream.skipRawData(fileBlockSize);
109
15.6k
        } else {
110
584
            break;
111
584
        }
112
16.2k
    }
113
114
584
    if(!fileBlockHeader.startsWith("TEST")) {
115
179
        return KIO::ThumbnailResult::fail();
116
179
    }
117
118
    // Now comes actual thumbnail image data.
119
405
    QByteArray xy(8, '\0');
120
405
    blendStream.readRawData(xy.data(), 8);
121
405
    const qint32 x = toInt32(xy.left(4));
122
405
    const qint32 y = toInt32(xy.right(4));
123
405
    const qint32 imgSize = fileBlockSize - 8;
124
405
    if (imgSize <= 0 || x <= 0 || y <= 0) {
125
117
        return KIO::ThumbnailResult::fail();
126
117
    }
127
288
    if (imgSize / 4 / y != x) {
128
65
        return KIO::ThumbnailResult::fail();
129
65
    }
130
131
223
    QByteArray imgBuffer(imgSize, '\0');
132
223
    const qint32 readData = blendStream.readRawData(imgBuffer.data(), imgSize);
133
223
    if (readData != imgSize) {
134
144
        return KIO::ThumbnailResult::fail();
135
144
    }
136
79
    QImage thumbnail((const uchar*)imgBuffer.constData(), x, y, QImage::Format_ARGB32);
137
79
    if(request.targetSize().width() != 128) {
138
0
        thumbnail = thumbnail.scaledToWidth(request.targetSize().width(), Qt::SmoothTransformation);
139
0
    }
140
79
    if(request.targetSize().height() != 128) {
141
0
        thumbnail = thumbnail.scaledToHeight(request.targetSize().height(), Qt::SmoothTransformation);
142
0
    }
143
79
    thumbnail = thumbnail.rgbSwapped();
144
79
    thumbnail = thumbnail.mirrored();
145
79
    QImage img = thumbnail.convertToFormat(QImage::Format_ARGB32_Premultiplied);
146
147
79
    return !img.isNull() ? KIO::ThumbnailResult::pass(img) : KIO::ThumbnailResult::fail();
148
223
}
149
150
#include "blendercreator.moc"