Files
scummvm-cursorfix/engines/dgds/image.cpp
2026-02-02 04:50:13 +01:00

681 lines
20 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#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<Common::Point> 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<uint32> 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<Graphics::ManagedSurface> 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<Graphics::ManagedSurface> 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<uint32>(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<uint32>(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<Graphics::ManagedSurface> 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