File indexing completed on 2024-04-28 15:25:38
0001 /* 0002 SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <kde@broulik.de> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "ani_p.h" 0008 0009 #include <QDebug> 0010 #include <QImage> 0011 #include <QScopeGuard> 0012 #include <QVariant> 0013 #include <QtEndian> 0014 0015 #include <cstring> 0016 0017 namespace 0018 { 0019 struct ChunkHeader { 0020 char magic[4]; 0021 quint32_le size; 0022 }; 0023 0024 struct AniHeader { 0025 quint32_le cbSize; 0026 quint32_le nFrames; // number of actual frames in the file 0027 quint32_le nSteps; // number of logical images 0028 quint32_le iWidth; 0029 quint32_le iHeight; 0030 quint32_le iBitCount; 0031 quint32_le nPlanes; 0032 quint32_le iDispRate; 0033 quint32_le bfAttributes; // attributes (0 = bitmap images, 1 = ico/cur, 3 = "seq" block available) 0034 }; 0035 0036 struct CurHeader { 0037 quint16_le wReserved; // always 0 0038 quint16_le wResID; // always 2 0039 quint16_le wNumImages; 0040 }; 0041 0042 struct CursorDirEntry { 0043 quint8 bWidth; 0044 quint8 bHeight; 0045 quint8 bColorCount; 0046 quint8 bReserved; // always 0 0047 quint16_le wHotspotX; 0048 quint16_le wHotspotY; 0049 quint32_le dwBytesInImage; 0050 quint32_le dwImageOffset; 0051 }; 0052 0053 } // namespace 0054 0055 ANIHandler::ANIHandler() = default; 0056 0057 bool ANIHandler::canRead() const 0058 { 0059 if (canRead(device())) { 0060 setFormat("ani"); 0061 return true; 0062 } 0063 0064 // Check if there's another frame coming 0065 const QByteArray nextFrame = device()->peek(sizeof(ChunkHeader)); 0066 if (nextFrame.size() == sizeof(ChunkHeader)) { 0067 const auto *header = reinterpret_cast<const ChunkHeader *>(nextFrame.data()); 0068 if (qstrncmp(header->magic, "icon", sizeof(header->magic)) == 0 && header->size > 0) { 0069 setFormat("ani"); 0070 return true; 0071 } 0072 } 0073 0074 return false; 0075 } 0076 0077 bool ANIHandler::read(QImage *outImage) 0078 { 0079 if (!ensureScanned()) { 0080 return false; 0081 } 0082 0083 if (device()->pos() < m_firstFrameOffset) { 0084 device()->seek(m_firstFrameOffset); 0085 } 0086 0087 const QByteArray frameType = device()->read(4); 0088 if (frameType != "icon") { 0089 return false; 0090 } 0091 0092 const QByteArray frameSizeData = device()->read(sizeof(quint32_le)); 0093 if (frameSizeData.count() != sizeof(quint32_le)) { 0094 return false; 0095 } 0096 0097 const auto frameSize = *(reinterpret_cast<const quint32_le *>(frameSizeData.data())); 0098 if (!frameSize) { 0099 return false; 0100 } 0101 0102 const QByteArray frameData = device()->read(frameSize); 0103 0104 const bool ok = outImage->loadFromData(frameData, "cur"); 0105 0106 ++m_currentImageNumber; 0107 0108 // When we have a custom image sequence, seek to before the frame that would follow 0109 if (!m_imageSequence.isEmpty()) { 0110 if (m_currentImageNumber < m_imageSequence.count()) { 0111 const int nextFrame = m_imageSequence.at(m_currentImageNumber); 0112 if (nextFrame < 0 || nextFrame >= m_frameOffsets.count()) { 0113 return false; 0114 } 0115 const auto nextOffset = m_frameOffsets.at(nextFrame); 0116 device()->seek(nextOffset); 0117 } else if (m_currentImageNumber == m_imageSequence.count()) { 0118 const auto endOffset = m_frameOffsets.last(); 0119 if (device()->pos() != endOffset) { 0120 device()->seek(endOffset); 0121 } 0122 } 0123 } 0124 0125 return ok; 0126 } 0127 0128 int ANIHandler::currentImageNumber() const 0129 { 0130 if (!ensureScanned()) { 0131 return 0; 0132 } 0133 return m_currentImageNumber; 0134 } 0135 0136 int ANIHandler::imageCount() const 0137 { 0138 if (!ensureScanned()) { 0139 return 0; 0140 } 0141 return m_imageCount; 0142 } 0143 0144 bool ANIHandler::jumpToImage(int imageNumber) 0145 { 0146 if (!ensureScanned()) { 0147 return false; 0148 } 0149 0150 if (imageNumber < 0) { 0151 return false; 0152 } 0153 0154 if (imageNumber == m_currentImageNumber) { 0155 return true; 0156 } 0157 0158 // If we have a custom image sequence we have a index of frames we can jump to 0159 if (!m_imageSequence.isEmpty()) { 0160 if (imageNumber >= m_imageSequence.count()) { 0161 return false; 0162 } 0163 0164 const int targetFrame = m_imageSequence.at(imageNumber); 0165 0166 const auto targetOffset = m_frameOffsets.value(targetFrame, -1); 0167 0168 if (device()->seek(targetOffset)) { 0169 m_currentImageNumber = imageNumber; 0170 return true; 0171 } 0172 0173 return false; 0174 } 0175 0176 if (imageNumber >= m_frameCount) { 0177 return false; 0178 } 0179 0180 // otherwise we need to jump from frame to frame 0181 const auto oldPos = device()->pos(); 0182 0183 if (imageNumber < m_currentImageNumber) { 0184 // start from the beginning 0185 if (!device()->seek(m_firstFrameOffset)) { 0186 return false; 0187 } 0188 } 0189 0190 while (m_currentImageNumber < imageNumber) { 0191 if (!jumpToNextImage()) { 0192 device()->seek(oldPos); 0193 return false; 0194 } 0195 } 0196 0197 m_currentImageNumber = imageNumber; 0198 return true; 0199 } 0200 0201 bool ANIHandler::jumpToNextImage() 0202 { 0203 if (!ensureScanned()) { 0204 return false; 0205 } 0206 0207 // If we have a custom image sequence we have a index of frames we can jump to 0208 // Delegate to jumpToImage 0209 if (!m_imageSequence.isEmpty()) { 0210 return jumpToImage(m_currentImageNumber + 1); 0211 } 0212 0213 if (device()->pos() < m_firstFrameOffset) { 0214 if (!device()->seek(m_firstFrameOffset)) { 0215 return false; 0216 } 0217 } 0218 0219 const QByteArray nextFrame = device()->peek(sizeof(ChunkHeader)); 0220 if (nextFrame.size() != sizeof(ChunkHeader)) { 0221 return false; 0222 } 0223 0224 const auto *header = reinterpret_cast<const ChunkHeader *>(nextFrame.data()); 0225 if (qstrncmp(header->magic, "icon", sizeof(header->magic)) != 0) { 0226 return false; 0227 } 0228 0229 const qint64 seekBy = sizeof(ChunkHeader) + header->size; 0230 0231 if (!device()->seek(device()->pos() + seekBy)) { 0232 return false; 0233 } 0234 0235 ++m_currentImageNumber; 0236 return true; 0237 } 0238 0239 int ANIHandler::loopCount() const 0240 { 0241 if (!ensureScanned()) { 0242 return 0; 0243 } 0244 return -1; 0245 } 0246 0247 int ANIHandler::nextImageDelay() const 0248 { 0249 if (!ensureScanned()) { 0250 return 0; 0251 } 0252 0253 int rate = m_displayRate; 0254 0255 if (!m_displayRates.isEmpty()) { 0256 int previousImage = m_currentImageNumber - 1; 0257 if (previousImage < 0) { 0258 previousImage = m_displayRates.count() - 1; 0259 } 0260 rate = m_displayRates.at(previousImage); 0261 } 0262 0263 return rate * 1000 / 60; 0264 } 0265 0266 bool ANIHandler::supportsOption(ImageOption option) const 0267 { 0268 return option == Size || option == Name || option == Description || option == Animation; 0269 } 0270 0271 QVariant ANIHandler::option(ImageOption option) const 0272 { 0273 if (!supportsOption(option) || !ensureScanned()) { 0274 return QVariant(); 0275 } 0276 0277 switch (option) { 0278 case QImageIOHandler::Size: 0279 return m_size; 0280 // TODO QImageIOHandler::Format 0281 // but both iBitCount in AniHeader and bColorCount are just zero most of the time 0282 // so one would probably need to traverse even further down into IcoHeader and IconDirEntry... 0283 // but Qt's ICO/CUR handler always seems to give us a ARB 0284 case QImageIOHandler::Name: 0285 return m_name; 0286 case QImageIOHandler::Description: { 0287 QString description; 0288 if (!m_name.isEmpty()) { 0289 description += QStringLiteral("Title: %1\n\n").arg(m_name); 0290 } 0291 if (!m_artist.isEmpty()) { 0292 description += QStringLiteral("Author: %1\n\n").arg(m_artist); 0293 } 0294 return description; 0295 } 0296 0297 case QImageIOHandler::Animation: 0298 return true; 0299 default: 0300 break; 0301 } 0302 0303 return QVariant(); 0304 } 0305 0306 bool ANIHandler::ensureScanned() const 0307 { 0308 if (m_scanned) { 0309 return true; 0310 } 0311 0312 if (device()->isSequential()) { 0313 return false; 0314 } 0315 0316 auto *mutableThis = const_cast<ANIHandler *>(this); 0317 0318 const auto oldPos = device()->pos(); 0319 auto cleanup = qScopeGuard([this, oldPos] { 0320 device()->seek(oldPos); 0321 }); 0322 0323 device()->seek(0); 0324 0325 const QByteArray riffIntro = device()->read(4); 0326 if (riffIntro != "RIFF") { 0327 return false; 0328 } 0329 0330 const auto riffSizeData = device()->read(sizeof(quint32_le)); 0331 if (riffSizeData.size() != sizeof(quint32_le)) { 0332 return false; 0333 } 0334 const auto riffSize = *(reinterpret_cast<const quint32_le *>(riffSizeData.data())); 0335 // TODO do a basic sanity check if the size is enough to hold some metadata and a frame? 0336 if (riffSize == 0) { 0337 return false; 0338 } 0339 0340 mutableThis->m_displayRates.clear(); 0341 mutableThis->m_imageSequence.clear(); 0342 0343 while (device()->pos() < riffSize) { 0344 const QByteArray chunkId = device()->read(4); 0345 if (chunkId.length() != 4) { 0346 return false; 0347 } 0348 0349 if (chunkId == "ACON") { 0350 continue; 0351 } 0352 0353 const QByteArray chunkSizeData = device()->read(sizeof(quint32_le)); 0354 if (chunkSizeData.length() != sizeof(quint32_le)) { 0355 return false; 0356 } 0357 auto chunkSize = *(reinterpret_cast<const quint32_le *>(chunkSizeData.data())); 0358 0359 if (chunkId == "anih") { 0360 if (chunkSize != sizeof(AniHeader)) { 0361 qWarning() << "anih chunk size does not match ANIHEADER size"; 0362 return false; 0363 } 0364 0365 const QByteArray anihData = device()->read(sizeof(AniHeader)); 0366 if (anihData.size() != sizeof(AniHeader)) { 0367 return false; 0368 } 0369 0370 auto *aniHeader = reinterpret_cast<const AniHeader *>(anihData.data()); 0371 0372 // The size in the ani header is usually 0 unfortunately, 0373 // so we'll also check the first frame for its size further below 0374 mutableThis->m_size = QSize(aniHeader->iWidth, aniHeader->iHeight); 0375 mutableThis->m_frameCount = aniHeader->nFrames; 0376 mutableThis->m_imageCount = aniHeader->nSteps; 0377 mutableThis->m_displayRate = aniHeader->iDispRate; 0378 } else if (chunkId == "rate" || chunkId == "seq ") { 0379 const QByteArray data = device()->read(chunkSize); 0380 if (static_cast<quint32_le>(data.size()) != chunkSize || data.size() % sizeof(quint32_le) != 0) { 0381 return false; 0382 } 0383 0384 // TODO should we check that the number of rate entries matches nSteps? 0385 auto *dataPtr = data.data(); 0386 QVector<int> list; 0387 for (int i = 0; i < data.count(); i += sizeof(quint32_le)) { 0388 const auto entry = *(reinterpret_cast<const quint32_le *>(dataPtr + i)); 0389 list.append(entry); 0390 } 0391 0392 if (chunkId == "rate") { 0393 // should we check that the number of rate entries matches nSteps? 0394 mutableThis->m_displayRates = list; 0395 } else if (chunkId == "seq ") { 0396 // Check if it's just an ascending sequence, don't bother with it then 0397 bool isAscending = true; 0398 for (int i = 0; i < list.count(); ++i) { 0399 if (list.at(i) != i) { 0400 isAscending = false; 0401 break; 0402 } 0403 } 0404 0405 if (!isAscending) { 0406 mutableThis->m_imageSequence = list; 0407 } 0408 } 0409 // IART and INAM are technically inside LIST->INFO but "INFO" is supposedly optional 0410 // so just handle those two attributes wherever we encounter them 0411 } else if (chunkId == "INAM" || chunkId == "IART") { 0412 const QByteArray value = device()->read(chunkSize); 0413 0414 if (static_cast<quint32_le>(value.size()) != chunkSize) { 0415 return false; 0416 } 0417 0418 // DWORDs are aligned to even sizes 0419 if (chunkSize % 2 != 0) { 0420 device()->read(1); 0421 } 0422 0423 // FIXME encoding 0424 const QString stringValue = QString::fromLocal8Bit(value.constData(), std::strlen(value.constData())); 0425 if (chunkId == "INAM") { 0426 mutableThis->m_name = stringValue; 0427 } else if (chunkId == "IART") { 0428 mutableThis->m_artist = stringValue; 0429 } 0430 } else if (chunkId == "LIST") { 0431 const QByteArray listType = device()->read(4); 0432 0433 if (listType == "INFO") { 0434 // Technically would contain INAM and IART but we handle them anywhere above 0435 } else if (listType == "fram") { 0436 quint64 read = 0; 0437 while (read < chunkSize) { 0438 const QByteArray chunkType = device()->read(4); 0439 read += 4; 0440 if (chunkType != "icon") { 0441 break; 0442 } 0443 0444 if (!m_firstFrameOffset) { 0445 mutableThis->m_firstFrameOffset = device()->pos() - 4; 0446 mutableThis->m_currentImageNumber = 0; 0447 0448 // If size in header isn't valid, use the first frame's size instead 0449 if (!m_size.isValid() || m_size.isEmpty()) { 0450 const auto oldPos = device()->pos(); 0451 0452 device()->read(sizeof(quint32_le)); 0453 0454 const QByteArray curHeaderData = device()->read(sizeof(CurHeader)); 0455 const QByteArray cursorDirEntryData = device()->read(sizeof(CursorDirEntry)); 0456 0457 if (curHeaderData.length() == sizeof(CurHeader) && cursorDirEntryData.length() == sizeof(CursorDirEntry)) { 0458 auto *cursorDirEntry = reinterpret_cast<const CursorDirEntry *>(cursorDirEntryData.data()); 0459 mutableThis->m_size = QSize(cursorDirEntry->bWidth, cursorDirEntry->bHeight); 0460 } 0461 0462 device()->seek(oldPos); 0463 } 0464 0465 // If we don't have a custom image sequence we can stop scanning right here 0466 if (m_imageSequence.isEmpty()) { 0467 break; 0468 } 0469 } 0470 0471 mutableThis->m_frameOffsets.append(device()->pos() - 4); 0472 0473 const QByteArray frameSizeData = device()->read(sizeof(quint32_le)); 0474 if (frameSizeData.size() != sizeof(quint32_le)) { 0475 return false; 0476 } 0477 0478 const auto frameSize = *(reinterpret_cast<const quint32_le *>(frameSizeData.data())); 0479 device()->seek(device()->pos() + frameSize); 0480 0481 read += frameSize; 0482 0483 if (m_frameOffsets.count() == m_frameCount) { 0484 // Also record the end of frame data 0485 mutableThis->m_frameOffsets.append(device()->pos() - 4); 0486 break; 0487 } 0488 } 0489 break; 0490 } 0491 } 0492 } 0493 0494 if (m_imageCount != m_frameCount && m_imageSequence.isEmpty()) { 0495 qWarning("ANIHandler: 'nSteps' is not equal to 'nFrames' but no 'seq' entries were provided"); 0496 return false; 0497 } 0498 0499 if (!m_imageSequence.isEmpty() && m_imageSequence.count() != m_imageCount) { 0500 qWarning("ANIHandler: count of entries in 'seq' does not match 'nSteps' in anih"); 0501 return false; 0502 } 0503 0504 if (!m_displayRates.isEmpty() && m_displayRates.count() != m_imageCount) { 0505 qWarning("ANIHandler: count of entries in 'rate' does not match 'nSteps' in anih"); 0506 return false; 0507 } 0508 0509 if (!m_frameOffsets.isEmpty() && m_frameOffsets.count() - 1 != m_frameCount) { 0510 qWarning("ANIHandler: number of actual frames does not match 'nFrames' in anih"); 0511 return false; 0512 } 0513 0514 mutableThis->m_scanned = true; 0515 return true; 0516 } 0517 0518 bool ANIHandler::canRead(QIODevice *device) 0519 { 0520 if (!device) { 0521 qWarning("ANIHandler::canRead() called with no device"); 0522 return false; 0523 } 0524 if (device->isSequential()) { 0525 return false; 0526 } 0527 0528 const QByteArray riffIntro = device->peek(12); 0529 0530 if (riffIntro.length() != 12) { 0531 return false; 0532 } 0533 0534 if (!riffIntro.startsWith("RIFF")) { 0535 return false; 0536 } 0537 0538 // TODO sanity check chunk size? 0539 0540 if (riffIntro.mid(4 + 4, 4) != "ACON") { 0541 return false; 0542 } 0543 0544 return true; 0545 } 0546 0547 QImageIOPlugin::Capabilities ANIPlugin::capabilities(QIODevice *device, const QByteArray &format) const 0548 { 0549 if (format == "ani") { 0550 return Capabilities(CanRead); 0551 } 0552 if (!format.isEmpty()) { 0553 return {}; 0554 } 0555 if (!device->isOpen()) { 0556 return {}; 0557 } 0558 0559 Capabilities cap; 0560 if (device->isReadable() && ANIHandler::canRead(device)) { 0561 cap |= CanRead; 0562 } 0563 return cap; 0564 } 0565 0566 QImageIOHandler *ANIPlugin::create(QIODevice *device, const QByteArray &format) const 0567 { 0568 QImageIOHandler *handler = new ANIHandler; 0569 handler->setDevice(device); 0570 handler->setFormat(format); 0571 return handler; 0572 } 0573 0574 #include "moc_ani_p.cpp"