KImageFormats

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

KDE's Doxygen guidelines are available online.