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

source code of kimageformats/src/imageformats/ani.cpp