KImageFormats

ani.cpp
1/*
2 SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <kde@broulik.de>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "ani_p.h"
8
9#include <QDebug>
10#include <QImage>
11#include <QScopeGuard>
12#include <QVariant>
13#include <QtEndian>
14
15#include <cstring>
16
17namespace
18{
19struct ChunkHeader {
20 char magic[4];
21 quint32_le size;
22};
23
24struct AniHeader {
25 quint32_le cbSize;
26 quint32_le nFrames; // number of actual frames in the file
27 quint32_le nSteps; // number of logical images
28 quint32_le iWidth;
29 quint32_le iHeight;
30 quint32_le iBitCount;
31 quint32_le nPlanes;
32 quint32_le iDispRate;
33 quint32_le bfAttributes; // attributes (0 = bitmap images, 1 = ico/cur, 3 = "seq" block available)
34};
35
36struct CurHeader {
37 quint16_le wReserved; // always 0
38 quint16_le wResID; // always 2
39 quint16_le wNumImages;
40};
41
42struct CursorDirEntry {
43 quint8 bWidth;
44 quint8 bHeight;
45 quint8 bColorCount;
46 quint8 bReserved; // always 0
47 quint16_le wHotspotX;
48 quint16_le wHotspotY;
49 quint32_le dwBytesInImage;
50 quint32_le dwImageOffset;
51};
52
53} // namespace
54
55ANIHandler::ANIHandler() = default;
56
57bool ANIHandler::canRead() const
58{
59 if (canRead(device())) {
60 setFormat("ani");
61 return true;
62 }
63
64 // Check if there's another frame coming
65 const QByteArray nextFrame = device()->peek(sizeof(ChunkHeader));
66 if (nextFrame.size() == sizeof(ChunkHeader)) {
67 const auto *header = reinterpret_cast<const ChunkHeader *>(nextFrame.data());
68 if (qstrncmp(header->magic, "icon", sizeof(header->magic)) == 0 && header->size > 0) {
69 setFormat("ani");
70 return true;
71 }
72 }
73
74 return false;
75}
76
77bool ANIHandler::read(QImage *outImage)
78{
79 if (!ensureScanned()) {
80 return false;
81 }
82
83 if (device()->pos() < m_firstFrameOffset) {
84 device()->seek(m_firstFrameOffset);
85 }
86
87 const QByteArray frameType = device()->read(4);
88 if (frameType != "icon") {
89 return false;
90 }
91
92 const QByteArray frameSizeData = device()->read(sizeof(quint32_le));
93 if (frameSizeData.size() != sizeof(quint32_le)) {
94 return false;
95 }
96
97 const auto frameSize = *(reinterpret_cast<const quint32_le *>(frameSizeData.data()));
98 if (!frameSize) {
99 return false;
100 }
101
102 const QByteArray frameData = device()->read(frameSize);
103
104 const bool ok = outImage->loadFromData(frameData, "cur");
105
106 ++m_currentImageNumber;
107
108 // When we have a custom image sequence, seek to before the frame that would follow
109 if (!m_imageSequence.isEmpty()) {
110 if (m_currentImageNumber < m_imageSequence.count()) {
111 const int nextFrame = m_imageSequence.at(m_currentImageNumber);
112 if (nextFrame < 0 || nextFrame >= m_frameOffsets.count()) {
113 return false;
114 }
115 const auto nextOffset = m_frameOffsets.at(nextFrame);
116 device()->seek(nextOffset);
117 } else if (m_currentImageNumber == m_imageSequence.count()) {
118 const auto endOffset = m_frameOffsets.last();
119 if (device()->pos() != endOffset) {
120 device()->seek(endOffset);
121 }
122 }
123 }
124
125 return ok;
126}
127
128int ANIHandler::currentImageNumber() const
129{
130 if (!ensureScanned()) {
131 return 0;
132 }
133 return m_currentImageNumber;
134}
135
136int ANIHandler::imageCount() const
137{
138 if (!ensureScanned()) {
139 return 0;
140 }
141 return m_imageCount;
142}
143
144bool ANIHandler::jumpToImage(int imageNumber)
145{
146 if (!ensureScanned()) {
147 return false;
148 }
149
150 if (imageNumber < 0) {
151 return false;
152 }
153
154 if (imageNumber == m_currentImageNumber) {
155 return true;
156 }
157
158 // If we have a custom image sequence we have a index of frames we can jump to
159 if (!m_imageSequence.isEmpty()) {
160 if (imageNumber >= m_imageSequence.count()) {
161 return false;
162 }
163
164 const int targetFrame = m_imageSequence.at(imageNumber);
165
166 const auto targetOffset = m_frameOffsets.value(targetFrame, -1);
167
168 if (device()->seek(targetOffset)) {
169 m_currentImageNumber = imageNumber;
170 return true;
171 }
172
173 return false;
174 }
175
176 if (imageNumber >= m_frameCount) {
177 return false;
178 }
179
180 // otherwise we need to jump from frame to frame
181 const auto oldPos = device()->pos();
182
183 if (imageNumber < m_currentImageNumber) {
184 // start from the beginning
185 if (!device()->seek(m_firstFrameOffset)) {
186 return false;
187 }
188 }
189
190 while (m_currentImageNumber < imageNumber) {
191 if (!jumpToNextImage()) {
192 device()->seek(oldPos);
193 return false;
194 }
195 }
196
197 m_currentImageNumber = imageNumber;
198 return true;
199}
200
201bool ANIHandler::jumpToNextImage()
202{
203 if (!ensureScanned()) {
204 return false;
205 }
206
207 // If we have a custom image sequence we have a index of frames we can jump to
208 // Delegate to jumpToImage
209 if (!m_imageSequence.isEmpty()) {
210 return jumpToImage(m_currentImageNumber + 1);
211 }
212
213 if (device()->pos() < m_firstFrameOffset) {
214 if (!device()->seek(m_firstFrameOffset)) {
215 return false;
216 }
217 }
218
219 const QByteArray nextFrame = device()->peek(sizeof(ChunkHeader));
220 if (nextFrame.size() != sizeof(ChunkHeader)) {
221 return false;
222 }
223
224 const auto *header = reinterpret_cast<const ChunkHeader *>(nextFrame.data());
225 if (qstrncmp(header->magic, "icon", sizeof(header->magic)) != 0) {
226 return false;
227 }
228
229 const qint64 seekBy = sizeof(ChunkHeader) + header->size;
230
231 if (!device()->seek(device()->pos() + seekBy)) {
232 return false;
233 }
234
235 ++m_currentImageNumber;
236 return true;
237}
238
239int ANIHandler::loopCount() const
240{
241 if (!ensureScanned()) {
242 return 0;
243 }
244 return -1;
245}
246
247int ANIHandler::nextImageDelay() const
248{
249 if (!ensureScanned()) {
250 return 0;
251 }
252
253 int rate = m_displayRate;
254
255 if (!m_displayRates.isEmpty()) {
256 int previousImage = m_currentImageNumber - 1;
257 if (previousImage < 0) {
258 previousImage = m_displayRates.count() - 1;
259 }
260 rate = m_displayRates.at(previousImage);
261 }
262
263 return rate * 1000 / 60;
264}
265
266bool ANIHandler::supportsOption(ImageOption option) const
267{
268 return option == Size || option == Name || option == Description || option == Animation;
269}
270
271QVariant ANIHandler::option(ImageOption option) const
272{
273 if (!supportsOption(option) || !ensureScanned()) {
274 return QVariant();
275 }
276
277 switch (option) {
279 return m_size;
280 // TODO QImageIOHandler::Format
281 // but both iBitCount in AniHeader and bColorCount are just zero most of the time
282 // so one would probably need to traverse even further down into IcoHeader and IconDirEntry...
283 // but Qt's ICO/CUR handler always seems to give us a ARB
285 return m_name;
287 QString description;
288 if (!m_name.isEmpty()) {
289 description += QStringLiteral("Title: %1\n\n").arg(m_name);
290 }
291 if (!m_artist.isEmpty()) {
292 description += QStringLiteral("Author: %1\n\n").arg(m_artist);
293 }
294 return description;
295 }
296
298 return true;
299 default:
300 break;
301 }
302
303 return QVariant();
304}
305
306bool ANIHandler::ensureScanned() const
307{
308 if (m_scanned) {
309 return true;
310 }
311
312 if (device()->isSequential()) {
313 return false;
314 }
315
316 auto *mutableThis = const_cast<ANIHandler *>(this);
317
318 const auto oldPos = device()->pos();
319 auto cleanup = qScopeGuard([this, oldPos] {
320 device()->seek(oldPos);
321 });
322
323 device()->seek(0);
324
325 const QByteArray riffIntro = device()->read(4);
326 if (riffIntro != "RIFF") {
327 return false;
328 }
329
330 const auto riffSizeData = device()->read(sizeof(quint32_le));
331 if (riffSizeData.size() != sizeof(quint32_le)) {
332 return false;
333 }
334 const auto riffSize = *(reinterpret_cast<const quint32_le *>(riffSizeData.data()));
335 // TODO do a basic sanity check if the size is enough to hold some metadata and a frame?
336 if (riffSize == 0) {
337 return false;
338 }
339
340 mutableThis->m_displayRates.clear();
341 mutableThis->m_imageSequence.clear();
342
343 while (device()->pos() < riffSize) {
344 const QByteArray chunkId = device()->read(4);
345 if (chunkId.length() != 4) {
346 return false;
347 }
348
349 if (chunkId == "ACON") {
350 continue;
351 }
352
353 const QByteArray chunkSizeData = device()->read(sizeof(quint32_le));
354 if (chunkSizeData.length() != sizeof(quint32_le)) {
355 return false;
356 }
357 auto chunkSize = *(reinterpret_cast<const quint32_le *>(chunkSizeData.data()));
358
359 if (chunkId == "anih") {
360 if (chunkSize != sizeof(AniHeader)) {
361 qWarning() << "anih chunk size does not match ANIHEADER size";
362 return false;
363 }
364
365 const QByteArray anihData = device()->read(sizeof(AniHeader));
366 if (anihData.size() != sizeof(AniHeader)) {
367 return false;
368 }
369
370 auto *aniHeader = reinterpret_cast<const AniHeader *>(anihData.data());
371
372 // The size in the ani header is usually 0 unfortunately,
373 // so we'll also check the first frame for its size further below
374 mutableThis->m_size = QSize(aniHeader->iWidth, aniHeader->iHeight);
375 mutableThis->m_frameCount = aniHeader->nFrames;
376 mutableThis->m_imageCount = aniHeader->nSteps;
377 mutableThis->m_displayRate = aniHeader->iDispRate;
378 } else if (chunkId == "rate" || chunkId == "seq ") {
379 if (chunkSize % sizeof(quint32_le) != 0) {
380 return false;
381 }
382
383 // TODO should we check that the number of rate entries matches nSteps?
385 for (unsigned int i = 0; i < chunkSize; i += sizeof(quint32_le)) {
386 const QByteArray data = device()->read(sizeof(quint32_le));
387 if (data.size() != sizeof(quint32_le)) {
388 return false;
389 }
390 const auto entry = *(reinterpret_cast<const quint32_le *>(data.data()));
391 list.append(entry);
392 }
393
394 if (chunkId == "rate") {
395 // should we check that the number of rate entries matches nSteps?
396 mutableThis->m_displayRates = list;
397 } else if (chunkId == "seq ") {
398 // Check if it's just an ascending sequence, don't bother with it then
399 bool isAscending = true;
400 for (int i = 0; i < list.count(); ++i) {
401 if (list.at(i) != i) {
402 isAscending = false;
403 break;
404 }
405 }
406
407 if (!isAscending) {
408 mutableThis->m_imageSequence = list;
409 }
410 }
411 // IART and INAM are technically inside LIST->INFO but "INFO" is supposedly optional
412 // so just handle those two attributes wherever we encounter them
413 } else if (chunkId == "INAM" || chunkId == "IART") {
414 const QByteArray value = device()->read(chunkSize);
415
416 if (static_cast<quint32_le>(value.size()) != chunkSize) {
417 return false;
418 }
419
420 // DWORDs are aligned to even sizes
421 if (chunkSize % 2 != 0) {
422 device()->read(1);
423 }
424
425 // FIXME encoding
426 const QString stringValue = QString::fromLocal8Bit(value.constData(), std::strlen(value.constData()));
427 if (chunkId == "INAM") {
428 mutableThis->m_name = stringValue;
429 } else if (chunkId == "IART") {
430 mutableThis->m_artist = stringValue;
431 }
432 } else if (chunkId == "LIST") {
433 const QByteArray listType = device()->read(4);
434
435 if (listType == "INFO") {
436 // Technically would contain INAM and IART but we handle them anywhere above
437 } else if (listType == "fram") {
438 quint64 read = 0;
439 while (read < chunkSize) {
440 const QByteArray chunkType = device()->read(4);
441 read += 4;
442 if (chunkType != "icon") {
443 break;
444 }
445
446 if (!m_firstFrameOffset) {
447 mutableThis->m_firstFrameOffset = device()->pos() - 4;
448 mutableThis->m_currentImageNumber = 0;
449
450 // If size in header isn't valid, use the first frame's size instead
451 if (!m_size.isValid() || m_size.isEmpty()) {
452 const auto oldPos = device()->pos();
453
454 device()->read(sizeof(quint32_le));
455
456 const QByteArray curHeaderData = device()->read(sizeof(CurHeader));
457 const QByteArray cursorDirEntryData = device()->read(sizeof(CursorDirEntry));
458
459 if (curHeaderData.length() == sizeof(CurHeader) && cursorDirEntryData.length() == sizeof(CursorDirEntry)) {
460 auto *cursorDirEntry = reinterpret_cast<const CursorDirEntry *>(cursorDirEntryData.data());
461 mutableThis->m_size = QSize(cursorDirEntry->bWidth, cursorDirEntry->bHeight);
462 }
463
464 device()->seek(oldPos);
465 }
466
467 // If we don't have a custom image sequence we can stop scanning right here
468 if (m_imageSequence.isEmpty()) {
469 break;
470 }
471 }
472
473 mutableThis->m_frameOffsets.append(device()->pos() - 4);
474
475 const QByteArray frameSizeData = device()->read(sizeof(quint32_le));
476 if (frameSizeData.size() != sizeof(quint32_le)) {
477 return false;
478 }
479
480 const auto frameSize = *(reinterpret_cast<const quint32_le *>(frameSizeData.data()));
481 device()->seek(device()->pos() + frameSize);
482
483 read += frameSize;
484
485 if (m_frameOffsets.count() == m_frameCount) {
486 // Also record the end of frame data
487 mutableThis->m_frameOffsets.append(device()->pos() - 4);
488 break;
489 }
490 }
491 break;
492 }
493 }
494 }
495
496 if (m_imageCount != m_frameCount && m_imageSequence.isEmpty()) {
497 qWarning("ANIHandler: 'nSteps' is not equal to 'nFrames' but no 'seq' entries were provided");
498 return false;
499 }
500
501 if (!m_imageSequence.isEmpty() && m_imageSequence.count() != m_imageCount) {
502 qWarning("ANIHandler: count of entries in 'seq' does not match 'nSteps' in anih");
503 return false;
504 }
505
506 if (!m_displayRates.isEmpty() && m_displayRates.count() != m_imageCount) {
507 qWarning("ANIHandler: count of entries in 'rate' does not match 'nSteps' in anih");
508 return false;
509 }
510
511 if (!m_frameOffsets.isEmpty() && m_frameOffsets.count() - 1 != m_frameCount) {
512 qWarning("ANIHandler: number of actual frames does not match 'nFrames' in anih");
513 return false;
514 }
515
516 mutableThis->m_scanned = true;
517 return true;
518}
519
520bool ANIHandler::canRead(QIODevice *device)
521{
522 if (!device) {
523 qWarning("ANIHandler::canRead() called with no device");
524 return false;
525 }
526 if (device->isSequential()) {
527 return false;
528 }
529
530 const QByteArray riffIntro = device->peek(12);
531
532 if (riffIntro.length() != 12) {
533 return false;
534 }
535
536 if (!riffIntro.startsWith("RIFF")) {
537 return false;
538 }
539
540 // TODO sanity check chunk size?
541
542 if (riffIntro.mid(4 + 4, 4) != "ACON") {
543 return false;
544 }
545
546 return true;
547}
548
549QImageIOPlugin::Capabilities ANIPlugin::capabilities(QIODevice *device, const QByteArray &format) const
550{
551 if (format == "ani") {
552 return Capabilities(CanRead);
553 }
554 if (!format.isEmpty()) {
555 return {};
556 }
557 if (!device->isOpen()) {
558 return {};
559 }
560
561 Capabilities cap;
562 if (device->isReadable() && ANIHandler::canRead(device)) {
563 cap |= CanRead;
564 }
565 return cap;
566}
567
568QImageIOHandler *ANIPlugin::create(QIODevice *device, const QByteArray &format) const
569{
570 QImageIOHandler *handler = new ANIHandler;
571 handler->setDevice(device);
572 handler->setFormat(format);
573 return handler;
574}
575
576#include "moc_ani_p.cpp"
QFlags< Capability > Capabilities
QVariant read(const QByteArray &data, int versionOverride=0)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
char at(qsizetype i) const const
void clear()
const char * constData() const const
char * data()
bool isEmpty() const const
qsizetype length() const const
QByteArray mid(qsizetype pos, qsizetype len) const const
qsizetype size() const const
bool startsWith(QByteArrayView bv) const const
bool loadFromData(QByteArrayView data, const char *format)
void setDevice(QIODevice *device)
void setFormat(const QByteArray &format)
typedef Capabilities
bool isOpen() const const
bool isReadable() const const
virtual bool isSequential() const const
QByteArray peek(qint64 maxSize)
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
qsizetype count() const const
QString arg(Args &&... args) const const
QString fromLocal8Bit(QByteArrayView str)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:53:42 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.