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"