/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" |