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: device())) {
60 setFormat("ani");
61 return true;
62 }
63
64 // Check if there's another frame coming
65 const QByteArray nextFrame = device()->peek(maxlen: sizeof(ChunkHeader));
66 if (nextFrame.size() == sizeof(ChunkHeader)) {
67 const auto *header = reinterpret_cast<const ChunkHeader *>(nextFrame.data());
68 if (qstrncmp(str1: header->magic, str2: "icon", len: 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(pos: m_firstFrameOffset);
85 }
86
87 const QByteArray frameType = device()->read(maxlen: 4);
88 if (frameType != "icon") {
89 return false;
90 }
91
92 const QByteArray frameSizeData = device()->read(maxlen: 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(maxlen: frameSize);
103
104 const bool ok = outImage->loadFromData(data: frameData, format: "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(i: m_currentImageNumber);
112 if (nextFrame < 0 || nextFrame >= m_frameOffsets.count()) {
113 return false;
114 }
115 const auto nextOffset = m_frameOffsets.at(i: nextFrame);
116 device()->seek(pos: nextOffset);
117 } else if (m_currentImageNumber == m_imageSequence.count()) {
118 const auto endOffset = m_frameOffsets.last();
119 if (device()->pos() != endOffset) {
120 device()->seek(pos: 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(i: imageNumber);
165
166 const auto targetOffset = m_frameOffsets.value(i: targetFrame, defaultValue: -1);
167
168 if (device()->seek(pos: 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(pos: m_firstFrameOffset)) {
186 return false;
187 }
188 }
189
190 while (m_currentImageNumber < imageNumber) {
191 if (!jumpToNextImage()) {
192 device()->seek(pos: 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(imageNumber: m_currentImageNumber + 1);
211 }
212
213 if (device()->pos() < m_firstFrameOffset) {
214 if (!device()->seek(pos: m_firstFrameOffset)) {
215 return false;
216 }
217 }
218
219 const QByteArray nextFrame = device()->peek(maxlen: 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(str1: header->magic, str2: "icon", len: sizeof(header->magic)) != 0) {
226 return false;
227 }
228
229 const qint64 seekBy = sizeof(ChunkHeader) + header->size;
230
231 if (!device()->seek(pos: 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(i: 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) {
278 case QImageIOHandler::Size:
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
284 case QImageIOHandler::Name:
285 return m_name;
286 case QImageIOHandler::Description: {
287 QString description;
288 if (!m_name.isEmpty()) {
289 description += QStringLiteral("Title: %1\n\n").arg(a: m_name);
290 }
291 if (!m_artist.isEmpty()) {
292 description += QStringLiteral("Author: %1\n\n").arg(a: m_artist);
293 }
294 return description;
295 }
296
297 case QImageIOHandler::Animation:
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(f: [this, oldPos] {
320 device()->seek(pos: oldPos);
321 });
322
323 device()->seek(pos: 0);
324
325 const QByteArray riffIntro = device()->read(maxlen: 4);
326 if (riffIntro != "RIFF") {
327 return false;
328 }
329
330 const auto riffSizeData = device()->read(maxlen: 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(maxlen: 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(maxlen: 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(maxlen: 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 const QByteArray data = device()->read(maxlen: chunkSize);
380 if (static_cast<quint32_le>(data.size()) != chunkSize || data.size() % sizeof(quint32_le) != 0) {
381 return false;
382 }
383
384 // TODO should we check that the number of rate entries matches nSteps?
385 auto *dataPtr = data.data();
386 QList<int> list;
387 for (int i = 0; i < data.size(); i += sizeof(quint32_le)) {
388 const auto entry = *(reinterpret_cast<const quint32_le *>(dataPtr + i));
389 list.append(t: entry);
390 }
391
392 if (chunkId == "rate") {
393 // should we check that the number of rate entries matches nSteps?
394 mutableThis->m_displayRates = list;
395 } else if (chunkId == "seq ") {
396 // Check if it's just an ascending sequence, don't bother with it then
397 bool isAscending = true;
398 for (int i = 0; i < list.count(); ++i) {
399 if (list.at(i) != i) {
400 isAscending = false;
401 break;
402 }
403 }
404
405 if (!isAscending) {
406 mutableThis->m_imageSequence = list;
407 }
408 }
409 // IART and INAM are technically inside LIST->INFO but "INFO" is supposedly optional
410 // so just handle those two attributes wherever we encounter them
411 } else if (chunkId == "INAM" || chunkId == "IART") {
412 const QByteArray value = device()->read(maxlen: chunkSize);
413
414 if (static_cast<quint32_le>(value.size()) != chunkSize) {
415 return false;
416 }
417
418 // DWORDs are aligned to even sizes
419 if (chunkSize % 2 != 0) {
420 device()->read(maxlen: 1);
421 }
422
423 // FIXME encoding
424 const QString stringValue = QString::fromLocal8Bit(str: value.constData(), size: std::strlen(s: value.constData()));
425 if (chunkId == "INAM") {
426 mutableThis->m_name = stringValue;
427 } else if (chunkId == "IART") {
428 mutableThis->m_artist = stringValue;
429 }
430 } else if (chunkId == "LIST") {
431 const QByteArray listType = device()->read(maxlen: 4);
432
433 if (listType == "INFO") {
434 // Technically would contain INAM and IART but we handle them anywhere above
435 } else if (listType == "fram") {
436 quint64 read = 0;
437 while (read < chunkSize) {
438 const QByteArray chunkType = device()->read(maxlen: 4);
439 read += 4;
440 if (chunkType != "icon") {
441 break;
442 }
443
444 if (!m_firstFrameOffset) {
445 mutableThis->m_firstFrameOffset = device()->pos() - 4;
446 mutableThis->m_currentImageNumber = 0;
447
448 // If size in header isn't valid, use the first frame's size instead
449 if (!m_size.isValid() || m_size.isEmpty()) {
450 const auto oldPos = device()->pos();
451
452 device()->read(maxlen: sizeof(quint32_le));
453
454 const QByteArray curHeaderData = device()->read(maxlen: sizeof(CurHeader));
455 const QByteArray cursorDirEntryData = device()->read(maxlen: sizeof(CursorDirEntry));
456
457 if (curHeaderData.length() == sizeof(CurHeader) && cursorDirEntryData.length() == sizeof(CursorDirEntry)) {
458 auto *cursorDirEntry = reinterpret_cast<const CursorDirEntry *>(cursorDirEntryData.data());
459 mutableThis->m_size = QSize(cursorDirEntry->bWidth, cursorDirEntry->bHeight);
460 }
461
462 device()->seek(pos: oldPos);
463 }
464
465 // If we don't have a custom image sequence we can stop scanning right here
466 if (m_imageSequence.isEmpty()) {
467 break;
468 }
469 }
470
471 mutableThis->m_frameOffsets.append(t: device()->pos() - 4);
472
473 const QByteArray frameSizeData = device()->read(maxlen: sizeof(quint32_le));
474 if (frameSizeData.size() != sizeof(quint32_le)) {
475 return false;
476 }
477
478 const auto frameSize = *(reinterpret_cast<const quint32_le *>(frameSizeData.data()));
479 device()->seek(pos: device()->pos() + frameSize);
480
481 read += frameSize;
482
483 if (m_frameOffsets.count() == m_frameCount) {
484 // Also record the end of frame data
485 mutableThis->m_frameOffsets.append(t: device()->pos() - 4);
486 break;
487 }
488 }
489 break;
490 }
491 }
492 }
493
494 if (m_imageCount != m_frameCount && m_imageSequence.isEmpty()) {
495 qWarning(msg: "ANIHandler: 'nSteps' is not equal to 'nFrames' but no 'seq' entries were provided");
496 return false;
497 }
498
499 if (!m_imageSequence.isEmpty() && m_imageSequence.count() != m_imageCount) {
500 qWarning(msg: "ANIHandler: count of entries in 'seq' does not match 'nSteps' in anih");
501 return false;
502 }
503
504 if (!m_displayRates.isEmpty() && m_displayRates.count() != m_imageCount) {
505 qWarning(msg: "ANIHandler: count of entries in 'rate' does not match 'nSteps' in anih");
506 return false;
507 }
508
509 if (!m_frameOffsets.isEmpty() && m_frameOffsets.count() - 1 != m_frameCount) {
510 qWarning(msg: "ANIHandler: number of actual frames does not match 'nFrames' in anih");
511 return false;
512 }
513
514 mutableThis->m_scanned = true;
515 return true;
516}
517
518bool ANIHandler::canRead(QIODevice *device)
519{
520 if (!device) {
521 qWarning(msg: "ANIHandler::canRead() called with no device");
522 return false;
523 }
524 if (device->isSequential()) {
525 return false;
526 }
527
528 const QByteArray riffIntro = device->peek(maxlen: 12);
529
530 if (riffIntro.length() != 12) {
531 return false;
532 }
533
534 if (!riffIntro.startsWith(bv: "RIFF")) {
535 return false;
536 }
537
538 // TODO sanity check chunk size?
539
540 if (riffIntro.mid(index: 4 + 4, len: 4) != "ACON") {
541 return false;
542 }
543
544 return true;
545}
546
547QImageIOPlugin::Capabilities ANIPlugin::capabilities(QIODevice *device, const QByteArray &format) const
548{
549 if (format == "ani") {
550 return Capabilities(CanRead);
551 }
552 if (!format.isEmpty()) {
553 return {};
554 }
555 if (!device->isOpen()) {
556 return {};
557 }
558
559 Capabilities cap;
560 if (device->isReadable() && ANIHandler::canRead(device)) {
561 cap |= CanRead;
562 }
563 return cap;
564}
565
566QImageIOHandler *ANIPlugin::create(QIODevice *device, const QByteArray &format) const
567{
568 QImageIOHandler *handler = new ANIHandler;
569 handler->setDevice(device);
570 handler->setFormat(format);
571 return handler;
572}
573
574#include "moc_ani_p.cpp"
575

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