diff --git a/graphics/wincursor.cpp b/graphics/wincursor.cpp
index e5634a0fdca..15e7aba7718 100644
--- a/graphics/wincursor.cpp
+++ b/graphics/wincursor.cpp
@@ -31,7 +31,7 @@ namespace Graphics {
/** A Windows cursor. */
class WinCursor : public Cursor {
public:
- WinCursor();
+ WinCursor(uint16 hotspotX, uint16 hotspotY);
~WinCursor();
/** Return the cursor's width. */
@@ -56,6 +56,8 @@ public:
bool readFromStream(Common::SeekableReadStream &stream);
private:
+ WinCursor() = delete;
+
byte *_surface;
byte *_mask;
byte _palette[256 * 3];
@@ -70,11 +72,11 @@ private:
void clear();
};
-WinCursor::WinCursor() {
+WinCursor::WinCursor(uint16 hotspotX, uint16 hotspotY) {
_width = 0;
_height = 0;
- _hotspotX = 0;
- _hotspotY = 0;
+ _hotspotX = hotspotX;
+ _hotspotY = hotspotY;
_surface = nullptr;
_mask = nullptr;
_keyColor = 0;
@@ -111,9 +113,6 @@ bool WinCursor::readFromStream(Common::SeekableReadStream &stream) {
const bool supportOpacity = g_system->hasFeature(OSystem::kFeatureCursorMask);
const bool supportInvert = g_system->hasFeature(OSystem::kFeatureCursorMaskInvert);
- _hotspotX = stream.readUint16LE();
- _hotspotY = stream.readUint16LE();
-
// Check header size
if (stream.readUint32LE() != 40)
return false;
@@ -151,8 +150,10 @@ bool WinCursor::readFromStream(Common::SeekableReadStream &stream) {
if (numColors == 0)
numColors = 1 << bitsPerPixel;
+ // Skip number of important colors
+ stream.skip(4);
+
// Reading the palette
- stream.seek(40 + 4);
for (uint32 i = 0 ; i < numColors; i++) {
_palette[i * 3 + 2] = stream.readByte();
_palette[i * 3 + 1] = stream.readByte();
@@ -311,11 +312,14 @@ WinCursorGroup *WinCursorGroup::createCursorGroup(Common::WinResources *exe, con
return 0;
}
- WinCursor *cursor = new WinCursor();
- if (!cursor->readFromStream(*cursorStream)) {
- delete cursor;
+ uint16 hotspotX = cursorStream->readUint16LE();
+ uint16 hotspotY = cursorStream->readUint16LE();
+
+ Cursor *cursor = loadWindowsCursorFromDIB(*cursorStream, hotspotX, hotspotY);
+
+ if (!cursor) {
delete group;
- return 0;
+ return nullptr;
}
CursorItem item;
@@ -448,4 +452,14 @@ Cursor *makeBusyWinCursor() {
return new BusyWinCursor();
}
+Cursor *loadWindowsCursorFromDIB(Common::SeekableReadStream &stream, uint16 hotspotX, uint16 hotspotY) {
+ WinCursor *cursor = new WinCursor(hotspotX, hotspotY);
+ if (!cursor->readFromStream(stream)) {
+ delete cursor;
+ return nullptr;
+ }
+
+ return cursor;
+}
+
} // End of namespace Graphics
diff --git a/graphics/wincursor.h b/graphics/wincursor.h
index 92e6479575e..210128fdd8f 100644
--- a/graphics/wincursor.h
+++ b/graphics/wincursor.h
@@ -80,6 +80,13 @@ Cursor *makeDefaultWinCursor();
*/
Cursor *makeBusyWinCursor();
+/**
+ * Create a Cursor from DIB-format data, i.e. starting with a BITMAPINFOHEADER
+ *
+ * @note The calling code is responsible for deleting the returned pointer.
+ */
+Cursor *loadWindowsCursorFromDIB(Common::SeekableReadStream &stream, uint16 hotspotX, uint16 hotspotY);
+
/** @} */
} // End of namespace Graphics
diff --git a/image/ani.cpp b/image/ani.cpp
new file mode 100644
index 00000000000..c556e32815f
--- /dev/null
+++ b/image/ani.cpp
@@ -0,0 +1,304 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#include "common/memstream.h"
+#include "common/stream.h"
+#include "common/substream.h"
+
+#include "image/ani.h"
+
+namespace Image {
+
+AniDecoder::Metadata::Metadata()
+ : numFrames(0), numSteps(0), width(0), height(0), bitCount(0),
+ numPlanes(0), perFrameDelay(0), haveSeqData(false), isCURFormat(false) {
+}
+
+AniDecoder::FrameDef::FrameDef() : delay(0), imageIndex(0) {
+}
+
+AniDecoder::AniDecoder() : _stream(nullptr), _disposeAfterUse(DisposeAfterUse::NO) {
+}
+
+AniDecoder::~AniDecoder() {
+ close();
+}
+
+void AniDecoder::close() {
+ if (_disposeAfterUse == DisposeAfterUse::YES && _stream != nullptr)
+ delete _stream;
+
+ _stream = nullptr;
+}
+
+const AniDecoder::Metadata &AniDecoder::getMetadata() const {
+ return _metadata;
+}
+
+AniDecoder::FrameDef AniDecoder::getSequenceFrame(uint sequenceIndex) const {
+ FrameDef frameDef;
+
+ if (sequenceIndex >= _rateData.size())
+ frameDef.delay = _metadata.perFrameDelay;
+ else
+ frameDef.delay = _rateData[sequenceIndex];
+
+ if (sequenceIndex >= _seqData.size())
+ frameDef.imageIndex = sequenceIndex;
+ else
+ frameDef.imageIndex = _seqData[sequenceIndex];
+
+ return frameDef;
+}
+
+Common::SeekableReadStream *AniDecoder::openImageStream(uint imageIndex) const {
+ if (imageIndex >= _frameDataLocations.size())
+ error("Invalid ANI image index");
+
+ const FrameDataRange &frameDataRange = _frameDataLocations[imageIndex];
+
+ return new Common::SafeSeekableSubReadStream(_stream, frameDataRange.pos, frameDataRange.pos + frameDataRange.size);
+}
+
+bool AniDecoder::open(Common::SeekableReadStream &stream, DisposeAfterUse::Flag disposeAfterUse) {
+ close();
+
+ _stream = &stream;
+ _disposeAfterUse = disposeAfterUse;
+
+ bool loadedOK = load();
+ if (!loadedOK)
+ close();
+
+ return loadedOK;
+}
+
+bool AniDecoder::load() {
+ if (!parseRIFFChunks(*_stream, Common::Functor2Mem(this, &AniDecoder::parseTopLevelChunk))) {
+ warning("AniDecoder::load: Failed to load ANI container");
+ return false;
+ }
+
+ return true;
+}
+
+bool AniDecoder::parseRIFFChunks(Common::SeekableReadStream &stream, const RIFFChunkParseFunc_t &callback) {
+ int64 nextChunkStartPos = 0;
+ int64 endPos = stream.size();
+
+ while (nextChunkStartPos < endPos) {
+ if (!stream.seek(nextChunkStartPos)) {
+ warning("AniDecoder::parseRIFFChunks: Failed to reset to start of RIFF chunk");
+ return false;
+ }
+
+ byte riffChunkHeader[8];
+
+ if (stream.read(riffChunkHeader, 8) != 8) {
+ warning("AniDecoder::parseRIFFChunks: Failed to read RIFF chunk header");
+ return false;
+ }
+
+ uint32 chunkSize = READ_LE_UINT32(riffChunkHeader + 4);
+
+ int64 actualChunkSize = chunkSize;
+ if (chunkSize & 1)
+ actualChunkSize++;
+
+ int64 chunkAvailable = stream.size() - stream.pos();
+ if (chunkAvailable < actualChunkSize) {
+ warning("AniDecoder::parseRIFFChunk: RIFF chunk is too large");
+ return false;
+ }
+
+ RIFFChunkDef chunkDef;
+ chunkDef.id = READ_BE_UINT32(riffChunkHeader);
+ chunkDef.size = chunkSize;
+
+ Common::SeekableSubReadStream substream(&stream, static_cast(stream.pos()), static_cast(stream.pos()) + chunkSize);
+ if (!callback(chunkDef, substream))
+ return false;
+
+ nextChunkStartPos += actualChunkSize + 8;
+ }
+
+ return true;
+}
+
+bool AniDecoder::parseRIFFContainer(Common::SeekableReadStream &chunkStream, const RIFFChunkDef &chunkDef, const RIFFContainerParseFunc_t &callback) {
+ if (chunkDef.size < 4) {
+ warning("AniDecoder::parseRIFFContainer: RIFF container is too small");
+ return false;
+ }
+
+ byte containerTypeID[4];
+ if (chunkStream.read(containerTypeID, 4) != 4) {
+ warning("AniDecoder::parseRIFFContainer: Failed to read RIFF container type");
+ return false;
+ }
+
+ RIFFContainerDef containerDef;
+ containerDef.id = READ_BE_UINT32(containerTypeID);
+ containerDef.size = chunkDef.size - 4;
+
+ Common::SeekableSubReadStream substream(&chunkStream, 4, chunkDef.size);
+ return callback(containerDef, substream);
+}
+
+bool AniDecoder::parseTopLevelChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+ if (chunk.id != MKTAG('R', 'I', 'F', 'F')) {
+ warning("AniDecoder::parseTopLevelChunk: Top-level chunk isn't RIFF");
+ return false;
+ }
+
+ return parseRIFFContainer(stream, chunk, Common::Functor2Mem(this, &AniDecoder::parseTopLevelContainer));
+}
+
+bool AniDecoder::parseTopLevelContainer(const RIFFContainerDef &container, Common::SeekableReadStream &stream) {
+ if (container.id == MKTAG('A', 'C', 'O', 'N'))
+ return parseRIFFChunks(stream, Common::Functor2Mem(this, &AniDecoder::parseSecondLevelChunk));
+
+ warning("AniDecoder::parseTopLevelContainer: Top-level container isn't ACON");
+ return false;
+}
+
+bool AniDecoder::parseSecondLevelChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+ if (chunk.id == MKTAG('L', 'I', 'S', 'T'))
+ return parseRIFFContainer(stream, chunk, Common::Functor2Mem(this, &AniDecoder::parseListContainer));
+
+ if (chunk.id == MKTAG('a', 'n', 'i', 'h'))
+ return parseAnimHeaderChunk(chunk, stream);
+
+ if (chunk.id == MKTAG('s', 'e', 'q', ' '))
+ return parseSeqChunk(chunk, stream);
+
+ if (chunk.id == MKTAG('r', 'a', 't', 'e'))
+ return parseRateChunk(chunk, stream);
+
+ return true;
+}
+
+bool AniDecoder::parseListContainer(const RIFFContainerDef &container, Common::SeekableReadStream &stream) {
+ if (container.id == MKTAG('f', 'r', 'a', 'm'))
+ return parseRIFFChunks(stream, Common::Functor2Mem(this, &AniDecoder::parseIconChunk));
+
+ return true;
+}
+
+bool AniDecoder::parseAnimHeaderChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+ byte animHeader[36];
+ for (byte &b : animHeader)
+ b = 0;
+
+ uint32 amountToRead = 36;
+ if (chunk.size < amountToRead)
+ amountToRead = chunk.size;
+
+ if (amountToRead > 0 && stream.read(animHeader, amountToRead) != amountToRead) {
+ warning("AniDecoder::parseAnimHeaderChunk: Read failed");
+ return false;
+ }
+
+ uint32 structSize = READ_LE_UINT32(animHeader);
+ if (structSize < 36) {
+ for (uint i = structSize; i < 36; i++)
+ animHeader[i] = 0;
+ }
+
+ _metadata.numFrames = READ_LE_UINT32(animHeader + 4);
+ _metadata.numSteps = READ_LE_UINT32(animHeader + 8);
+ _metadata.width = READ_LE_UINT32(animHeader + 12);
+ _metadata.height = READ_LE_UINT32(animHeader + 16);
+ _metadata.bitCount = READ_LE_UINT32(animHeader + 20);
+ _metadata.numPlanes = READ_LE_UINT32(animHeader + 24);
+ _metadata.perFrameDelay = READ_LE_UINT32(animHeader + 28);
+
+ uint32 flags = READ_LE_UINT32(animHeader + 32);
+ _metadata.isCURFormat = ((flags & 1) != 0);
+ _metadata.haveSeqData = ((flags & 2) != 0);
+
+ return true;
+}
+
+bool AniDecoder::parseSeqChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+ uint32 numFrames = chunk.size / 4u;
+
+ if (numFrames > 1000u) {
+ warning("AniDecoder::parseRateChunk: Too many frames");
+ return false;
+ }
+
+ if (numFrames > _seqData.size())
+ _seqData.resize(numFrames);
+
+ for (uint i = 0; i < numFrames; i++) {
+ byte seqData[4];
+
+ if (stream.read(seqData, 4) != 4) {
+ warning("AniDecoder::parseRateChunk: Failed to read sequence information");
+ return false;
+ }
+
+ _seqData[i] = READ_LE_UINT32(seqData);
+ }
+
+ return true;
+}
+
+bool AniDecoder::parseRateChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+ uint32 numFrames = chunk.size / 4u;
+
+ if (numFrames > 1000u) {
+ warning("AniDecoder::parseRateChunk: Too many frames");
+ return false;
+ }
+
+ if (numFrames > _rateData.size())
+ _rateData.resize(numFrames);
+
+ for (uint i = 0; i < numFrames; i++) {
+ byte rateData[4];
+
+ if (stream.read(rateData, 4) != 4) {
+ warning("AniDecoder::parseRateChunk: Failed to read rate information");
+ return false;
+ }
+
+ _rateData[i] = READ_LE_UINT32(rateData);
+ }
+
+ return true;
+}
+
+bool AniDecoder::parseIconChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+ FrameDataRange frameDataRange;
+
+ // Get the global stream position
+ frameDataRange.pos = static_cast(_stream->pos());
+ frameDataRange.size = chunk.size;
+
+ _frameDataLocations.push_back(frameDataRange);
+
+ return true;
+}
+
+
+} // End of namespace Image
diff --git a/image/ani.h b/image/ani.h
new file mode 100644
index 00000000000..dc47b62bb08
--- /dev/null
+++ b/image/ani.h
@@ -0,0 +1,136 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#ifndef GFX_ANI_H
+#define GFX_ANI_H
+
+#include "common/array.h"
+#include "common/types.h"
+#include "common/func.h"
+
+namespace Common {
+
+class SeekableReadStream;
+struct IFFChunk;
+
+} // End of namespace Common
+
+namespace Graphics {
+
+class Cursor;
+struct Surface;
+
+} // End of namespace Graphics
+
+namespace Image {
+
+class AniDecoder {
+public:
+ struct Metadata {
+ Metadata();
+
+ uint32 numFrames; // Number of images
+ uint32 numSteps; // Number of frames (use the FrameDef to determine which frame)
+ uint32 width;
+ uint32 height;
+ uint32 bitCount;
+ uint32 numPlanes;
+ uint32 perFrameDelay;
+ bool haveSeqData;
+ bool isCURFormat;
+ };
+
+ struct FrameDef {
+ FrameDef();
+
+ uint32 imageIndex;
+ uint32 delay; // In 1/60 sec
+ };
+
+ AniDecoder();
+ ~AniDecoder();
+
+ bool open(Common::SeekableReadStream &stream, DisposeAfterUse::Flag = DisposeAfterUse::NO);
+ void close();
+
+ const Metadata &getMetadata() const;
+ FrameDef getSequenceFrame(uint sequenceIndex) const;
+
+ /**
+ * Opens a substream for an image. If the metadata field
+ * "isCURFormat" is set, you can pass the stream to IcoCurDecoder to
+ * read it. Otherwise, you must determine the format. The stream
+ * is valid for as long as the stream used to construct the AniDecoder
+ * is valid.
+ *
+ * @param imageIndex The index of the image in the ANI file.
+ * @return A substream for the image.
+ */
+ Common::SeekableReadStream *openImageStream(uint imageIndex) const;
+
+private:
+ struct RIFFContainerDef {
+ uint32 id;
+ uint32 size;
+ };
+
+ struct RIFFChunkDef {
+ uint32 id;
+ uint32 size;
+ };
+
+ struct FrameDataRange {
+ uint32 pos;
+ uint32 size;
+ };
+
+ typedef Common::Functor2 RIFFContainerParseFunc_t;
+ typedef Common::Functor2 RIFFChunkParseFunc_t;
+
+ bool load();
+
+ static bool parseRIFFChunks(Common::SeekableReadStream &stream, const RIFFChunkParseFunc_t &callback);
+ static bool parseRIFFContainer(Common::SeekableReadStream &stream, const RIFFChunkDef &chunkDef, const RIFFContainerParseFunc_t &callback);
+
+ bool parseTopLevelChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+ bool parseTopLevelContainer(const RIFFContainerDef &container, Common::SeekableReadStream &stream);
+
+ bool parseSecondLevelChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+
+ bool parseListContainer(const RIFFContainerDef &container, Common::SeekableReadStream &stream);
+
+ bool parseAnimHeaderChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+ bool parseSeqChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+ bool parseRateChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+ bool parseIconChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+
+ Metadata _metadata;
+ Common::Array _rateData;
+ Common::Array _seqData;
+ Common::Array _frameDataLocations;
+
+ Common::SeekableReadStream *_stream;
+ DisposeAfterUse::Flag _disposeAfterUse;
+};
+
+} // End of namespace Image
+
+#endif
diff --git a/image/icocur.cpp b/image/icocur.cpp
new file mode 100644
index 00000000000..60876facf3a
--- /dev/null
+++ b/image/icocur.cpp
@@ -0,0 +1,142 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#include "common/stream.h"
+#include "common/substream.h"
+#include "common/memstream.h"
+
+#include "image/icocur.h"
+
+#include "graphics/wincursor.h"
+
+namespace Image {
+
+IcoCurDecoder::IcoCurDecoder() : _type(kTypeInvalid), _stream(nullptr), _disposeAfterUse(DisposeAfterUse::NO) {
+}
+
+IcoCurDecoder::~IcoCurDecoder() {
+ close();
+}
+
+void IcoCurDecoder::close() {
+ if (_disposeAfterUse == DisposeAfterUse::YES && _stream != nullptr)
+ delete _stream;
+
+ _stream = nullptr;
+ _type = kTypeInvalid;
+ _items.clear();
+}
+
+bool IcoCurDecoder::open(Common::SeekableReadStream &stream, DisposeAfterUse::Flag disposeAfterUse) {
+ close();
+
+ _stream = &stream;
+ _disposeAfterUse = disposeAfterUse;
+
+ bool loadedOK = load();
+ if (!loadedOK)
+ close();
+
+ return loadedOK;
+}
+
+bool IcoCurDecoder::load() {
+ uint8 iconDirData[6];
+
+ if (_stream->read(iconDirData, 6) != 6)
+ return false;
+
+ if (iconDirData[0] != 0 || iconDirData[1] != 0 || (iconDirData[2] != 1 && iconDirData[2] != 2) || iconDirData[3] != 0) {
+ warning("Malformed ICO/CUR header");
+ return false;
+ }
+
+ uint16 numImages = READ_LE_UINT16(iconDirData + 4);
+ _type = static_cast(iconDirData[2]);
+
+ if (numImages == 0)
+ return true;
+
+ uint32 dirSize = static_cast(numImages) * 16;
+
+ Common::Array iconDir;
+ iconDir.resize(dirSize);
+
+ if (_stream->read(&iconDir[0], dirSize) != dirSize)
+ return false;
+
+ _items.resize(numImages);
+ for (uint i = 0; i < numImages; i++) {
+ const uint8 *entryData = &iconDir[i * 16u];
+ Item &item = _items[i];
+
+ item.width = entryData[0];
+ if (item.width == 0)
+ item.width = 256;
+
+ item.height = entryData[1];
+ if (item.height == 0)
+ item.height = 256;
+
+ item.numColors = entryData[2];
+
+ item.data.ico.numPlanes = READ_LE_UINT16(entryData + 4);
+ item.data.ico.bitsPerPixel = READ_LE_UINT16(entryData + 6);
+ item.dataSize = READ_LE_UINT32(entryData + 8);
+ item.dataOffset = READ_LE_UINT32(entryData + 12);
+ }
+
+ return true;
+}
+
+IcoCurDecoder::Type IcoCurDecoder::getType() const {
+ return _type;
+}
+
+uint IcoCurDecoder::numItems() const {
+ return _items.size();
+}
+
+const IcoCurDecoder::Item &IcoCurDecoder::getItem(uint itemIndex) const {
+ return _items[itemIndex];
+}
+
+Graphics::Cursor *IcoCurDecoder::loadItemAsCursor(uint itemIndex) const {
+ const IcoCurDecoder::Item &dirItem = _items[itemIndex];
+
+ if (_type != kTypeCUR)
+ warning("ICO/CUR file type wasn't a cursor, but is being requested as a cursor anyway");
+
+ if (static_cast(dirItem.dataOffset) > _stream->size()) {
+ warning("ICO/CUR data offset was outside of the file");
+ return nullptr;
+ }
+
+ if (_stream->size() - static_cast(dirItem.dataOffset) < static_cast(dirItem.dataSize)) {
+ warning("ICO/CUR data bounds were outside of the file");
+ return nullptr;
+ }
+
+ Common::SeekableSubReadStream substream(_stream, dirItem.dataOffset, dirItem.dataOffset + dirItem.dataSize);
+ return Graphics::loadWindowsCursorFromDIB(substream, dirItem.data.cur.hotspotX, dirItem.data.cur.hotspotY);
+}
+
+} // End of namespace Image
diff --git a/image/icocur.h b/image/icocur.h
new file mode 100644
index 00000000000..b0858a98aa3
--- /dev/null
+++ b/image/icocur.h
@@ -0,0 +1,106 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#ifndef GFX_ICOCUR_H
+#define GFX_ICOCUR_H
+
+#include "common/array.h"
+#include "common/types.h"
+
+namespace Common {
+
+class SeekableReadStream;
+
+} // End of namespace Common
+
+namespace Graphics {
+
+class Cursor;
+struct Surface;
+
+} // End of namespace Graphics
+
+namespace Image {
+
+class IcoCurDecoder {
+public:
+ enum Type {
+ kTypeInvalid,
+
+ kTypeICO,
+ kTypeCUR,
+ };
+
+ struct Item {
+ struct IconData {
+ uint16 numPlanes;
+ uint16 bitsPerPixel;
+ };
+
+ struct CursorData {
+ uint16 hotspotX;
+ uint16 hotspotY;
+ };
+
+ union DataUnion {
+ IconData ico;
+ CursorData cur;
+ };
+
+ uint16 width;
+ uint16 height;
+ uint8 numColors; // May be 0
+ DataUnion data;
+ uint32 dataSize;
+ uint32 dataOffset;
+ };
+
+ IcoCurDecoder();
+ ~IcoCurDecoder();
+
+ bool open(Common::SeekableReadStream &stream, DisposeAfterUse::Flag = DisposeAfterUse::NO);
+ void close();
+
+ Type getType() const;
+ uint numItems() const;
+ const Item &getItem(uint itemIndex) const;
+
+ /**
+ * Loads an item from the directory as a cursor.
+ *
+ * @param itemIndex The index of the item in the directory.
+ * @return Loaded cursor.
+ */
+ Graphics::Cursor *loadItemAsCursor(uint itemIndex) const;
+
+private:
+ bool load();
+
+ Type _type;
+ Common::Array- _items;
+
+ Common::SeekableReadStream *_stream;
+ DisposeAfterUse::Flag _disposeAfterUse;
+};
+
+} // End of namespace Image
+
+#endif
diff --git a/image/module.mk b/image/module.mk
index df24ea3f61f..5bcc928f43a 100644
--- a/image/module.mk
+++ b/image/module.mk
@@ -1,9 +1,11 @@
MODULE := image
MODULE_OBJS := \
+ ani.o \
bmp.o \
cel_3do.o \
gif.o \
+ icocur.o \
iff.o \
jpeg.o \
neo.o \