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 | |
17 | namespace |
18 | { |
19 | struct { |
20 | char [4]; |
21 | quint32_le ; |
22 | }; |
23 | |
24 | struct { |
25 | quint32_le ; |
26 | quint32_le ; // number of actual frames in the file |
27 | quint32_le ; // number of logical images |
28 | quint32_le ; |
29 | quint32_le ; |
30 | quint32_le ; |
31 | quint32_le ; |
32 | quint32_le ; |
33 | quint32_le ; // attributes (0 = bitmap images, 1 = ico/cur, 3 = "seq" block available) |
34 | }; |
35 | |
36 | struct { |
37 | quint16_le ; // always 0 |
38 | quint16_le ; // always 2 |
39 | quint16_le ; |
40 | }; |
41 | |
42 | struct 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 | |
55 | ANIHandler::ANIHandler() = default; |
56 | |
57 | bool 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 * = 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 | |
77 | bool 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 | |
128 | int ANIHandler::currentImageNumber() const |
129 | { |
130 | if (!ensureScanned()) { |
131 | return 0; |
132 | } |
133 | return m_currentImageNumber; |
134 | } |
135 | |
136 | int ANIHandler::imageCount() const |
137 | { |
138 | if (!ensureScanned()) { |
139 | return 0; |
140 | } |
141 | return m_imageCount; |
142 | } |
143 | |
144 | bool 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 | |
201 | bool 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 * = 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 | |
239 | int ANIHandler::loopCount() const |
240 | { |
241 | if (!ensureScanned()) { |
242 | return 0; |
243 | } |
244 | return -1; |
245 | } |
246 | |
247 | int 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 | |
266 | bool ANIHandler::supportsOption(ImageOption option) const |
267 | { |
268 | return option == Size || option == Name || option == Description || option == Animation; |
269 | } |
270 | |
271 | QVariant 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 | |
306 | bool 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 * = 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 = 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 | |
518 | bool 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 | |
547 | QImageIOPlugin::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 | |
566 | QImageIOHandler *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 | |