/* 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/debug.h" #include "common/endian.h" #include "common/rect.h" #include "common/textconsole.h" #include "common/str.h" #include "common/stream.h" #include "common/system.h" #include "common/platform.h" #include "graphics/managed_surface.h" #include "dgds/dgds.h" #include "dgds/includes.h" #include "dgds/image.h" #include "dgds/resource.h" namespace Dgds { Image::Image(ResourceManager *resourceMan, Decompressor *decompressor) : _resourceMan(resourceMan), _decompressor(decompressor), _matrixX(0), _matrixY(0) { } Image::~Image() { } void Image::drawScreen(const Common::String &filename, Graphics::ManagedSurface &surface) { const char *dot; DGDS_EX ex; if (filename.empty()) { warning("Image::drawScreen Tried to draw empty image"); return; } // surface should be allocated already. assert(!surface.empty()); Common::SeekableReadStream *fileStream = _resourceMan->getResource(filename); if (!fileStream) error("Couldn't get image resource %s", filename.c_str()); if ((dot = strrchr(filename.c_str(), '.'))) { ex = MKTAG24(toupper(dot[1]), toupper(dot[2]), toupper(dot[3])); } else { ex = 0; } if (ex != EX_SCR) { warning("Unknown screen tag: %d", ex); delete fileStream; return; } _filename = filename; surface.fillRect(Common::Rect(surface.w, surface.h), 0); if (DgdsEngine::getInstance()->getPlatform() == Common::kPlatformAmiga) { loadAmigaScreen(surface, fileStream, ex); } else { loadPCScreen(surface, fileStream, ex); } delete fileStream; } void Image::loadPCScreen(Graphics::ManagedSurface &surface, Common::SeekableReadStream *fileStream, DGDS_EX ex) { uint16 xsize = surface.w; uint16 ysize = surface.h; DgdsChunkReader chunk(fileStream); while (chunk.readNextHeader(ex, _filename)) { if (chunk.isContainer()) { continue; } chunk.readContent(_decompressor); Common::SeekableReadStream *stream = chunk.getContent(); if (chunk.isSection(ID_BIN)) { loadBitmap4(&surface, 0, stream, false, xsize, ysize); } else if (chunk.isSection(ID_DIM)) { xsize = stream->readUint16LE(); ysize = stream->readUint16LE(); if (xsize > surface.w || ysize > surface.h) { error("Trying to load SCR size %d x %d which is larger than screen %d x %d", xsize, ysize, surface.w, surface.h); } debug(1, "screen file %s dims %d x %d into surface %d x %d", _filename.c_str(), xsize, ysize, surface.w, surface.h); } else if (chunk.isSection(ID_VGA)) { loadBitmap4(&surface, 0, stream, true, xsize, ysize); } else if (chunk.isSection(ID_MA8)) { loadBitmap8(&surface, 0, stream, xsize, ysize); } else if (chunk.isSection(ID_VQT)) { loadVQT(&surface, 0, stream); } } } void Image::loadAmigaScreen(Graphics::ManagedSurface &surface, Common::SeekableReadStream *fileStream, DGDS_EX ex) { uint32 magic = fileStream->readUint32BE(); if (magic != MKTAG('B', 'A', 'S', 'H')) error("Unexpected magic %08x in Amiga screen %s", magic, _filename.c_str()); error("Image::loadAmigaScreen: TODO - Finish this."); /* uint16 ysize = fileStream->readUint16BE(); uint16 xsize = surface.w; uint16 dummy = fileStream->readUint16BE(); */ } int Image::frameCount(const Common::String &filename) { Common::SeekableReadStream *fileStream = _resourceMan->getResource(filename); if (!fileStream) error("frameCount: Couldn't get bitmap resource '%s'", filename.c_str()); int tileCount = -1; DgdsChunkReader chunk(fileStream); while (chunk.readNextHeader(EX_BMP, filename)) { if (chunk.isContainer()) { continue; } chunk.readContent(_decompressor); Common::SeekableReadStream *stream = chunk.getContent(); if (chunk.isSection(ID_INF)) { tileCount = stream->readUint16LE(); } } return tileCount; } void Image::loadBitmap(const Common::String &filename) { DGDS_EX ex; Common::SeekableReadStream *fileStream = _resourceMan->getResource(filename); if (!fileStream) error("loadBitmap: Couldn't get bitmap resource '%s'", filename.c_str()); _frames.clear(); const char *dot; if ((dot = strrchr(filename.c_str(), '.'))) { ex = MKTAG24(toupper(dot[1]), toupper(dot[2]), toupper(dot[3])); } else { ex = 0; } if (ex != EX_BMP && ex != EX_CDS) { warning("Unknown bitmap extension: %d", ex); delete fileStream; return; } _filename = filename; if (DgdsEngine::getInstance()->getPlatform() == Common::kPlatformAmiga) { loadAmigaBitmap(fileStream, ex); } else { loadPCBitmap(fileStream, ex); } delete fileStream; } void Image::loadAmigaBitmap(Common::SeekableReadStream *fileStream, DGDS_EX ex) { uint16 nframes = fileStream->readUint16BE(); if (nframes > 1024) error("Image::loadAmigaBitmap: Unexpectedly large number of frames in image (%d)", nframes); _frames.resize(nframes); uint32 size = fileStream->readUint32BE(); // always the same as size below? for (int i = 0; i < nframes; i++) { uint16 width = fileStream->readUint16BE(); uint16 height = fileStream->readUint16BE(); _frames[i].reset(new Graphics::ManagedSurface(width, height, Graphics::PixelFormat::createFormatCLUT8())); } char idstr[13]; fileStream->read(idstr, 12); idstr[12] = '\0'; uint32 size2 = fileStream->readUint32BE(); if (size != size2) error("Image::loadAmigaBitmap: Expected sizes to match: %d != %d", size, size2); //uint32 insz = fileStream->size() - fileStream->pos(); byte row[200]; for (int i = 0; i < nframes; i++) { // round up int bytesPerRow = (_frames[i]->w * 5 + 4) / 8; for (int y = 0; y < _frames[i]->h; y++) { fileStream->read(row, bytesPerRow); for (int x = 0; x < _frames[i]->w; x++) { int offset = x * 5 / 8; uint16 val = READ_LE_INT16(row + offset); val = (val >> (11 - (x * 5 % 8))) & 0x1f; _frames[i]->setPixel(x, y, val); } } } } void Image::loadPCBitmap(Common::SeekableReadStream *fileStream, DGDS_EX ex) { int64 vqtpos = -1; int64 scnpos = -1; DgdsChunkReader chunk(fileStream); while (chunk.readNextHeader(ex, _filename)) { if (chunk.isContainer()) { continue; } chunk.readContent(_decompressor); Common::SeekableReadStream *stream = chunk.getContent(); if (chunk.isSection(ID_INF)) { Common::Array tileSizes; uint16 tileCount = stream->readUint16LE(); if (tileCount > 1024) error("Image::loadBitmap: Unexpectedly large number of tiles in image (%d)", tileCount); _frames.resize(tileCount); tileSizes.resize(tileCount); for (uint16 k = 0; k < tileCount; k++) { tileSizes[k].x = stream->readUint16LE(); } for (uint16 k = 0; k < tileCount; k++) { tileSizes[k].y = stream->readUint16LE(); } for (uint16 k = 0; k < tileCount; k++) { _frames[k].reset(new Graphics::ManagedSurface(tileSizes[k].x, tileSizes[k].y, Graphics::PixelFormat::createFormatCLUT8())); } } else if (chunk.isSection(ID_MTX)) { // Scroll offset _matrixX = stream->readUint16LE(); _matrixY = stream->readUint16LE(); uint32 mcount = (uint32)_matrixX * _matrixY; _tileMatrix.resize(mcount); debug(1, " %u x %u: mtx vals", _matrixX, _matrixY); for (uint32 k = 0; k < mcount; k++) { uint16 tile; tile = stream->readUint16LE(); _tileMatrix[k] = tile; } } else if (chunk.isSection(ID_BIN)) { for (auto & frame : _frames) { loadBitmap4(frame.get(), 0, stream, false, frame->w, frame->h); } } else if (chunk.isSection(ID_VGA)) { for (auto & frame : _frames) { loadBitmap4(frame.get(), 0, stream, true, frame->w, frame->h); } } else if (chunk.isSection(ID_VQT)) { // Postpone parsing this until we have the offsets, which come after. vqtpos = fileStream->pos(); } else if (chunk.isSection(ID_SCN)) { // Postpone parsing this until we have the offsets, which come after. scnpos = fileStream->pos(); } else if (chunk.isSection(ID_OFF)) { if (vqtpos == -1 && scnpos == -1) error("Expect VQT or SCN chunk before OFF chunk in BMP resource %s", _filename.c_str()); // 2 possibilities: A set of offsets (find the one which we need and use it) // or a single value of 0xffff. If it's only one tile, the offset is always // zero anyway. For subsequent images, round up to the next byte to start // reading. if (chunk.getSize() == 2) { assert(scnpos == -1); // don't support this mode for SCN? if (_frames.size() > 1) { uint16 val = stream->readUint16LE(); if (val != 0xffff) warning("Expected 0xffff in 2-byte offset list, got %04x", val); } uint32 nextOffset = 0; for (auto & frame : _frames) { fileStream->seek(vqtpos); nextOffset = loadVQT(frame.get(), nextOffset, fileStream); nextOffset = ((nextOffset + 7) / 8) * 8; } } else { Common::Array frameOffsets; for (uint i = 0; i < _frames.size(); i++) frameOffsets.push_back(stream->readUint32LE()); for (uint i = 0; i < _frames.size(); i++) { if (vqtpos != -1) { fileStream->seek(vqtpos + frameOffsets[i]); loadVQT(_frames[i].get(), 0, fileStream); } else { fileStream->seek(scnpos + frameOffsets[i]); loadSCN(_frames[i].get(), fileStream); } } } // NOTE: This was proably the last chunk, but we don't check - should have // the image data now. If we need to read another chunk we should fix up the // offset of fileStream because we just broke it. break; } } } void Image::drawBitmap(uint frameno, int x, int y, const Common::Rect &drawWin, Graphics::ManagedSurface &destSurf, ImageFlipMode flipMode, int dstWidth, int dstHeight) const { if (frameno >= _frames.size()) { warning("drawBitmap: Trying to draw frame %d from a %d frame image %s!", frameno, _frames.size(), _filename.c_str()); return; } const Common::SharedPtr srcFrame = _frames[frameno]; int srcWidth = srcFrame->w; int srcHeight = srcFrame->h; if (dstWidth == 0) dstWidth = srcWidth; if (dstHeight == 0) dstHeight = srcHeight; const Common::Rect destRect(Common::Point(x, y), dstWidth, dstHeight); Common::Rect clippedDestRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); clippedDestRect.clip(destRect); clippedDestRect.clip(drawWin); if (clippedDestRect.isEmpty()) return; const byte *src = (const byte *)srcFrame->getBasePtr(0, 0); // Note: this is not particularly optimized, written to be easier to understand byte *dst = (byte *)destSurf.getBasePtr(x, y); for (int i = 0; i < dstHeight; ++i) { const byte *srcRow = src; if (flipMode & kImageFlipV) srcRow += srcFrame->pitch * ((dstHeight - i - 1) * srcHeight / dstHeight); else srcRow += srcFrame->pitch * (i * srcHeight / dstHeight); for (int j = 0; j < dstWidth; ++j) { int srcX; if (flipMode & kImageFlipH) srcX = (dstWidth - j - 1) * srcWidth / dstWidth; else srcX = j * srcWidth / dstWidth; if (srcRow[srcX] && clippedDestRect.contains(j + x, i + y)) dst[j] = srcRow[srcX]; } dst += destSurf.pitch; } } void Image::drawScrollBitmap(int16 x, int16 y, int16 width, int16 height, int16 scrollX, int16 scrollY, const Common::Rect &drawWin, Graphics::ManagedSurface &dstSurf) const { if (_frames.empty()) error("Trying to draw scroll for empty image."); if (_tileMatrix.empty()) error("Trying to draw scroll with non-tiled image."); int tileW = _frames[0]->w; int tileH = _frames[0]->h; byte *dst = (byte *)dstSurf.getBasePtr(0, 0); int nXTiles = MIN((width + tileW - 1) / tileW, (int)_matrixX); int nYTiles = MIN((height + tileH - 1) / tileH, (int)_matrixY); for (int yTile = 0; yTile < nYTiles; yTile++) { int tileDstY = y + yTile * tileH; int tileRowIndex = (yTile + scrollY) % _matrixY; if (tileRowIndex < 0) tileRowIndex += _matrixY; assert(tileRowIndex >= 0 && tileRowIndex < _matrixY); if (tileDstY >= drawWin.bottom) continue; for (int xTile = 0; xTile < nXTiles; xTile++) { int tileDstX = x + xTile * tileW; Common::Rect tileDest(Common::Point(tileDstX, tileDstY), tileW, tileH); tileDest.clip(drawWin); if (tileDest.isEmpty()) continue; int tileColIndex = (xTile + scrollX) % _matrixX; if (tileColIndex < 0) tileColIndex += _matrixX; assert(tileColIndex >= 0 && tileColIndex < _matrixX); uint16 tileNo = _tileMatrix[tileRowIndex + tileColIndex * _matrixY]; Common::SharedPtr tile = _frames[tileNo]; const byte *src = (const byte *)tile->getBasePtr(0, 0); for (int dstY = tileDstY; dstY < tileDstY + tileH; dstY++) { for (int dstX = tileDstX; dstX < tileDstX + tileW; dstX++) { if (!tileDest.contains(dstX, dstY)) continue; dst[dstY * dstSurf.pitch + dstX] = src[(dstY - tileDstY) * tile->pitch + (dstX - tileDstX)]; } } } } } int16 Image::getFrameFromMatrix(int16 x, int16 y) { assert(x >= 0 && y >= 0 && x < _matrixX && y < _matrixY); return _tileMatrix[_matrixY * x + y]; } void Image::loadBitmap4(Graphics::ManagedSurface *surf, uint32 toffset, Common::SeekableReadStream *stream, bool highByte, uint16 tw, uint16 th) { assert(th != 0); byte *data = (byte *)surf->getPixels(); stream->skip(toffset >> 1); if (highByte) { for (uint32 i = 0; i < static_cast(tw * th); i += 2) { byte val = stream->readByte(); data[i + 0] |= val & 0xF0; data[i + 1] |= (val & 0x0F) << 4; } } else { for (uint32 i = 0; i < static_cast(tw * th); i += 2) { byte val = stream->readByte(); data[i + 0] |= (val & 0xF0) >> 4; data[i + 1] |= val & 0x0F; } } } void Image::loadBitmap8(Graphics::ManagedSurface *surf, uint32 toffset, Common::SeekableReadStream *stream, uint16 tw, uint16 th) { assert(th != 0); byte *data = (byte *)surf->getPixels(); stream->skip(toffset); stream->read(data, (uint32)tw * th); } struct VQTDecodeState { uint32 offset; const byte *srcPtr; byte *dstPtr; uint16 rowStarts[SCREEN_HEIGHT_FIXED]; }; static inline byte _getVqtBits(struct VQTDecodeState *state, uint16 nbits) { assert(nbits <= 8); const uint32 offset = state->offset; const uint32 index = offset >> 3; const uint32 shift = offset & 7; state->offset += nbits; return (READ_LE_UINT16(state->srcPtr + index) >> (shift)) & (byte)(0xff00 >> (16 - nbits)); } static void _doVqtDecode2(struct VQTDecodeState *state, const uint16 x, const uint16 y, const uint16 w, const uint16 h) { // Empty region -> nothing to do if (h == 0 || w == 0) return; // 1x1 region -> put the byte directly if (w == 1 && h == 1) { state->dstPtr[state->rowStarts[y] + x] = _getVqtBits(state, 8); return; } // this will always fit in uint16 but we multiply it later so need 32 bits. const uint32 losize = (w & 0xff) * (h & 0xff); byte bitcount1 = 8; if (losize < 256) { bitcount1 = 0; byte b = (byte)(losize - 1); do { bitcount1++; b >>= 1; } while (b != 0); } byte firstval = _getVqtBits(state, bitcount1); uint16 bitcount2 = 0; byte bval = (byte)firstval; while (firstval != 0) { bitcount2++; firstval >>= 1; } bval++; if (losize * 8 <= losize * bitcount2 + (uint32)bval * 8) { for (int xx = x; xx < x + w; xx++) { for (int yy = y; yy < y + h; yy++) { state->dstPtr[state->rowStarts[yy] + xx] = _getVqtBits(state, 8); } } return; } if (bval == 1) { const uint16 val = _getVqtBits(state, 8); for (int yy = y; yy < y + h; yy++) { for (int xx = x; xx < x + w; xx++) { state->dstPtr[state->rowStarts[yy] + xx] = val; } } return; } byte tmpbuf[255]; byte *ptmpbuf = tmpbuf; for (; bval != 0; bval--) { *ptmpbuf = _getVqtBits(state, 8); ptmpbuf++; } for (int xx = x; xx < x + w; xx++) { for (int yy = y; yy < y + h; yy++) { state->dstPtr[state->rowStarts[yy] + xx] = tmpbuf[_getVqtBits(state, bitcount2)]; } } } static void _doVqtDecode(struct VQTDecodeState *state, uint16 x, uint16 y, uint16 w, uint16 h) { if (!w && !h) return; const uint16 mask = _getVqtBits(state, 4); // Top left quadrant if (mask & 8) _doVqtDecode(state, x, y, w / 2, h / 2); else _doVqtDecode2(state, x, y, w / 2, h / 2); // Top right quadrant if (mask & 4) _doVqtDecode(state, x + (w / 2), y, (w + 1) / 2, h / 2); else _doVqtDecode2(state, x + (w / 2), y, (w + 1) / 2, h / 2); // Bottom left quadrant if (mask & 2) _doVqtDecode(state, x, y + (h / 2), w / 2, (h + 1) / 2); else _doVqtDecode2(state, x, y + (h / 2), w / 2, (h + 1) / 2); // Bottom right quadrant if (mask & 1) _doVqtDecode(state, x + (w / 2), y + (h / 2), (w + 1) / 2, (h + 1) / 2); else _doVqtDecode2(state, x + (w / 2), y + (h / 2), (w + 1) / 2, (h + 1) / 2); } uint32 Image::loadVQT(Graphics::ManagedSurface *surf, uint32 toffset, Common::SeekableReadStream *stream) { uint32 tw = surf->w; uint32 th = surf->h; assert(th != 0); assert(DgdsEngine::getInstance()->getGameId() != GID_CASTAWAY); if (th > (uint32)SCREEN_HEIGHT) error("Max VQT height supported is 200px"); VQTDecodeState state; state.dstPtr = (byte *)surf->getPixels(); state.offset = toffset; // FIXME: This sometimes reads more than it needs to.. uint64 nbytes = stream->size() - stream->pos(); byte *buf = (byte *)malloc(nbytes + 8); if (!buf) error("Image::loadVQT: Alloc failed"); memset(buf, 0, nbytes + 8); stream->read(buf, nbytes); state.srcPtr = buf; for (uint i = 0; i < th; i++) state.rowStarts[i] = tw * i; _doVqtDecode(&state, 0, 0, tw, th); free(buf); return state.offset; } // // SCN file parsing - eg, "WILLCRED.BMP" from Willy Beamish // Ref: https://moddingwiki.shikadi.net/wiki/The_Incredible_Machine_Image_Format // bool Image::loadSCN(Graphics::ManagedSurface *surf, Common::SeekableReadStream *stream) { int32 tw = surf->w; int32 th = surf->h; assert(th != 0); byte *dst = (byte *)surf->getPixels(); int32 y = 0; int32 x = 0; const byte addVal = stream->readByte(); byte lastcmd = 0xff; while (y < th && !stream->eos()) { byte val = stream->readByte(); byte cmd = val & 0xc0; // top 2 bits are the command val &= 0x3f; // bottom 6 bits are the value switch (cmd) { case 0x00: // CMD 00 - move cursor down and back if (lastcmd == 0x00 && val) { x -= (val << 6); } else { y++; x -= val; } if (x < 0) error("Image::loadSCN: Moved x too far back on line %d", y); break; case 0x40: // CMD 01 - skip if (!val) y = th; // end of image. else x += val; break; case 0x80: { // CMD 10 - repeat val byte color = stream->readByte(); for (uint16 i = 0; i < val; i++) dst[y * tw + x + i] = color + addVal; x += val; break; } case 0xc0: { // CMD 11 - direct read of `val` * 4-bit values for (uint16 i = 0; i < val; i += 2) { byte color = stream->readByte(); dst[y * tw + x + i] = (color >> 4) + addVal; if (x + i + 1 < tw && i < val - 1) dst[y * tw + x + i + 1] = (color & 0xf) + addVal; } x += val; break; } default: break; } lastcmd = cmd; if (x > tw) error("Image::loadSCN: Invalid data, x went off the end of the row"); } return !stream->err(); } int16 Image::width(uint frameno) const { if (frameno >= _frames.size()) error("Image::width: Invalid frameno %d requested from shape '%s'", frameno, _filename.c_str()); return _frames[frameno]->w; } int16 Image::height(uint frameno) const { if (frameno >= _frames.size()) error("Image::height: Invalid frameno %d requested from shape '%s'", frameno, _filename.c_str()); return _frames[frameno]->h; } Common::SharedPtr Image::getSurface(uint frameno) const { if (frameno >= _frames.size()) error("Image::getSurface: Invalid frameno %d requested from shape '%s'", frameno, _filename.c_str()); return _frames[frameno]; } } // End of namespace Dgds