/* 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 2 * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "sherlock/image_file.h" #include "sherlock/screen.h" #include "sherlock/sherlock.h" #include "common/debug.h" #include "common/memstream.h" namespace Sherlock { SherlockEngine *ImageFile::_vm; void ImageFile::setVm(SherlockEngine *vm) { _vm = vm; } ImageFile::ImageFile() { } ImageFile::ImageFile(const Common::String &name, bool skipPal, bool animImages) { Common::SeekableReadStream *stream = _vm->_res->load(name); Common::fill(&_palette[0], &_palette[PALETTE_SIZE], 0); load(*stream, skipPal, animImages); delete stream; } ImageFile::ImageFile(Common::SeekableReadStream &stream, bool skipPal) { Common::fill(&_palette[0], &_palette[PALETTE_SIZE], 0); load(stream, skipPal, false); } ImageFile::~ImageFile() { for (uint idx = 0; idx < size(); ++idx) (*this)[idx]._frame.free(); } void ImageFile::load(Common::SeekableReadStream &stream, bool skipPalette, bool animImages) { loadPalette(stream); int streamSize = stream.size(); while (stream.pos() < streamSize) { ImageFrame frame; frame._width = stream.readUint16LE() + 1; frame._height = stream.readUint16LE() + 1; frame._paletteBase = stream.readByte(); if (animImages) { // Animation cutscene image files use a 16-bit x offset frame._offset.x = stream.readUint16LE(); frame._rleEncoded = (frame._offset.x & 0xff) == 1; frame._offset.y = stream.readByte(); } else { // Standard image files have a separate byte for the RLE flag, and an 8-bit X offset frame._rleEncoded = stream.readByte() == 1; frame._offset.x = stream.readByte(); frame._offset.y = stream.readByte(); } frame._rleEncoded = !skipPalette && frame._rleEncoded; if (frame._paletteBase) { // Nibble packed frame data frame._size = (frame._width * frame._height) / 2; } else if (frame._rleEncoded) { // This size includes the header size, which we subtract frame._size = stream.readUint16LE() - 11; frame._rleMarker = stream.readByte(); } else { // Uncompressed data frame._size = frame._width * frame._height; } // Load data for frame and decompress it byte *data = new byte[frame._size + 4]; stream.read(data, frame._size); Common::fill(data + frame._size, data + frame._size + 4, 0); frame.decompressFrame(data, IS_ROSE_TATTOO); delete[] data; push_back(frame); } } void ImageFile::loadPalette(Common::SeekableReadStream &stream) { // Check for palette uint16 width = stream.readUint16LE() + 1; uint16 height = stream.readUint16LE() + 1; byte paletteBase = stream.readByte(); byte rleEncoded = stream.readByte(); byte offsetX = stream.readByte(); byte offsetY = stream.readByte(); uint32 palSignature = 0; if ((width == 390) && (height == 2) && (!paletteBase) && (!rleEncoded) && (!offsetX) && (!offsetY)) { // We check for these specific values // We can't do "width * height", because at least the first German+Spanish menu bar is 60 x 13 // which is 780, which is the size of the palette. We obviously don't want to detect it as palette. // As another security measure, we also check for the signature text palSignature = stream.readUint32BE(); if (palSignature != MKTAG('V', 'G', 'A', ' ')) { // signature mismatch, rewind stream.seek(-12, SEEK_CUR); return; } // Found palette, so read it in stream.seek(8, SEEK_CUR); // Skip over the rest of the signature text "VGA palette" for (int idx = 0; idx < PALETTE_SIZE; ++idx) _palette[idx] = VGA_COLOR_TRANS(stream.readByte()); } else { // Not a palette, so rewind to start of frame data for normal frame processing stream.seek(-8, SEEK_CUR); } } void ImageFrame::decompressFrame(const byte *src, bool isRoseTattoo) { _frame.create(_width, _height, Graphics::PixelFormat::createFormatCLUT8()); byte *dest = (byte *)_frame.getPixels(); Common::fill(dest, dest + _width * _height, 0xff); if (_paletteBase) { // Nibble-packed for (uint idx = 0; idx < _size; ++idx, ++src) { *dest++ = *src & 0xF; *dest++ = (*src >> 4); } } else if (_rleEncoded && isRoseTattoo) { // Rose Tattoo run length encoding doesn't use the RLE marker byte for (int yp = 0; yp < _height; ++yp) { int xSize = _width; while (xSize > 0) { // Skip a given number of pixels byte skip = *src++; dest += skip; xSize -= skip; if (!xSize) break; // Get a run length, and copy the following number of pixels int rleCount = *src++; xSize -= rleCount; while (rleCount-- > 0) *dest++ = *src++; } assert(xSize == 0); } } else if (_rleEncoded) { // RLE encoded int frameSize = _width * _height; while (frameSize > 0) { if (*src == _rleMarker) { byte rleColor = src[1]; byte rleCount = src[2]; src += 3; frameSize -= rleCount; while (rleCount--) *dest++ = rleColor; } else { *dest++ = *src++; --frameSize; } } assert(frameSize == 0); } else { // Uncompressed frame Common::copy(src, src + _width * _height, dest); } } /*----------------------------------------------------------------*/ int ImageFrame::sDrawXSize(int scaleVal) const { int width = _width; int scale = scaleVal == 0 ? 1 : scaleVal; if (scaleVal >= SCALE_THRESHOLD) --width; int result = width * SCALE_THRESHOLD / scale; if (scaleVal >= SCALE_THRESHOLD) ++result; return result; } int ImageFrame::sDrawYSize(int scaleVal) const { int height = _height; int scale = scaleVal == 0 ? 1 : scaleVal; if (scaleVal >= SCALE_THRESHOLD) --height; int result = height * SCALE_THRESHOLD / scale; if (scaleVal >= SCALE_THRESHOLD) ++result; return result; } int ImageFrame::sDrawXOffset(int scaleVal) const { if (scaleVal == SCALE_THRESHOLD) return _offset.x; int width = _offset.x; int scale = scaleVal == 0 ? 1 : scaleVal; if (scaleVal >= SCALE_THRESHOLD) --width; int result = width * SCALE_THRESHOLD / scale; if (scaleVal > SCALE_THRESHOLD) ++result; return result; } int ImageFrame::sDrawYOffset(int scaleVal) const { if (scaleVal == SCALE_THRESHOLD) return _offset.y; int height = _offset.y; int scale = scaleVal == 0 ? 1 : scaleVal; if (scaleVal >= SCALE_THRESHOLD) --height; int result = height * SCALE_THRESHOLD / scale; if (scaleVal > SCALE_THRESHOLD) ++result; return result; } // ******************************************************* /*----------------------------------------------------------------*/ SherlockEngine *ImageFile3DO::_vm; void ImageFile3DO::setVm(SherlockEngine *vm) { _vm = vm; } ImageFile3DO::ImageFile3DO(const Common::String &name, ImageFile3DOType imageFile3DOType) { #if 0 Common::File *dataStream = new Common::File(); if (!dataStream->open(name)) { error("unable to open %s\n", name.c_str()); } #endif Common::SeekableReadStream *dataStream = _vm->_res->load(name); switch(imageFile3DOType) { case kImageFile3DOType_Animation: loadAnimationFile(*dataStream); break; case kImageFile3DOType_Cel: case kImageFile3DOType_CelAnimation: load3DOCelFile(*dataStream); break; case kImageFile3DOType_RoomFormat: load3DOCelRoomData(*dataStream); break; case kImageFile3DOType_Font: loadFont(*dataStream); break; default: error("unknown Imagefile-3DO-Type"); break; } delete dataStream; } ImageFile3DO::ImageFile3DO(Common::SeekableReadStream &stream, bool isRoomData) { if (!isRoomData) { load(stream, isRoomData); } else { load3DOCelRoomData(stream); } } ImageFile3DO::~ImageFile3DO() { // already done in ImageFile destructor //for (uint idx = 0; idx < size(); ++idx) // (*this)[idx]._frame.free(); } void ImageFile3DO::load(Common::SeekableReadStream &stream, bool isRoomData) { uint32 headerId = 0; if (isRoomData) { load3DOCelRoomData(stream); return; } headerId = stream.readUint32BE(); assert(!stream.eos()); // Seek back to the start stream.seek(-4, SEEK_CUR); // Identify type of file switch (headerId) { case MKTAG('C', 'C', 'B', ' '): case MKTAG('A', 'N', 'I', 'M'): case MKTAG('O', 'F', 'S', 'T'): // 3DOSplash.cel // 3DO .cel (title1a.cel, etc.) or animation file (walk.anim) load3DOCelFile(stream); break; default: // Sherlock animation file (.3da files) loadAnimationFile(stream); break; } } // 3DO uses RGB555, we use RGB565 internally so that more platforms are able to run us inline uint16 ImageFile3DO::convertPixel(uint16 pixel3DO) { byte red = (pixel3DO >> 10) & 0x1F; byte green = (pixel3DO >> 5) & 0x1F; byte blue = pixel3DO & 0x1F; return ((red << 11) | (green << 6) | (blue)); } void ImageFile3DO::loadAnimationFile(Common::SeekableReadStream &stream) { uint32 streamLeft = stream.size() - stream.pos(); uint32 celDataSize = 0; while (streamLeft > 0) { ImageFrame frame; // We expect a basic header of 8 bytes if (streamLeft < 8) error("load3DOAnimationFile: expected animation header, not enough bytes"); celDataSize = stream.readUint16BE(); frame._width = stream.readUint16BE() + 1; // 2 bytes BE width frame._height = stream.readByte() + 1; // 1 byte BE height frame._paletteBase = 0; frame._rleEncoded = true; // always compressed if (frame._width & 0x8000) { frame._width &= 0x7FFF; celDataSize += 0x10000; } frame._offset.x = stream.readUint16BE(); frame._offset.y = stream.readByte(); frame._size = 0; // Got header streamLeft -= 8; // cel data follows if (streamLeft < celDataSize) error("load3DOAnimationFile: expected cel data, not enough bytes"); // // Load data for frame and decompress it byte *data = new byte[celDataSize]; stream.read(data, celDataSize); streamLeft -= celDataSize; // always 16 bits per pixel (RGB555) decompress3DOCelFrame(frame, data, celDataSize, 16, NULL); delete[] data; push_back(frame); } } static byte imagefile3DO_cel_bitsPerPixelLookupTable[8] = { 0, 1, 2, 4, 6, 8, 16, 0 }; // Reads a 3DO .cel/.anim file void ImageFile3DO::load3DOCelFile(Common::SeekableReadStream &stream) { int32 streamSize = stream.size(); int32 chunkStartPos = 0; uint32 chunkTag = 0; uint32 chunkSize = 0; byte *chunkDataPtr = NULL; // ANIM chunk (animation header for animation files) bool animFound = false; uint32 animVersion = 0; uint32 animType = 0; uint32 animFrameCount = 1; // we expect 1 frame without an ANIM header // CCB chunk (cel control block) bool ccbFound = false; uint32 ccbVersion = 0; uint32 ccbFlags = 0; bool ccbFlags_compressed = false; uint16 ccbPPMP0 = 0; uint16 ccbPPMP1 = 0; uint32 ccbPRE0 = 0; uint16 ccbPRE0_height = 0; byte ccbPRE0_bitsPerPixel = 0; uint32 ccbPRE1 = 0; uint16 ccbPRE1_width = 0; uint32 ccbWidth = 0; uint32 ccbHeight = 0; // pixel lookup table bool plutFound = false; uint32 plutCount = 0; ImageFile3DOPixelLookupTable plutRGBlookupTable; memset(&plutRGBlookupTable, 0, sizeof(plutRGBlookupTable)); while (!stream.err() && (stream.pos() < streamSize)) { chunkStartPos = stream.pos(); chunkTag = stream.readUint32BE(); chunkSize = stream.readUint32BE(); if (stream.eos() || stream.err()) break; if (chunkSize < 8) error("load3DOCelFile: Invalid chunk size"); uint32 dataSize = chunkSize - 8; switch (chunkTag) { case MKTAG('A', 'N', 'I', 'M'): // animation header assert(dataSize >= 24); if (animFound) error("load3DOCelFile: multiple ANIM chunks not supported"); animFound = true; animVersion = stream.readUint32BE(); animType = stream.readUint32BE(); animFrameCount = stream.readUint32BE(); // UINT32 - framerate (0x2000 in walk.anim???) // UINT32 - starting frame (0 for walk.anim) // UINT32 - number of loops (0 for walk.anim) if (animVersion != 0) error("load3DOCelFile: Unsupported animation file version"); if (animType != 1) error("load3DOCelFile: Only single CCB animation files are supported"); break; case MKTAG('C', 'C', 'B', ' '): // CEL control block assert(dataSize >= 72); if (ccbFound) error("load3DOCelFile: multiple CCB chunks not supported"); ccbFound = true; ccbVersion = stream.readUint32BE(); ccbFlags = stream.readUint32BE(); stream.skip(3 * 4); // skip over 3 pointer fields, which are used in memory only by 3DO hardware stream.skip(8 * 4); // skip over 8 offset fields ccbPPMP0 = stream.readUint16BE(); ccbPPMP1 = stream.readUint16BE(); ccbPRE0 = stream.readUint32BE(); ccbPRE1 = stream.readUint32BE(); ccbWidth = stream.readUint32BE(); ccbHeight = stream.readUint32BE(); if (ccbVersion != 0) error("load3DOCelFile: Unsupported CCB version"); if (ccbFlags & 0x200) // bit 9 ccbFlags_compressed = true; // bit 5 of ccbFlags defines how RGB-black (0, 0, 0) will get treated // = false -> RGB-black is treated as transparent // = true -> RGB-black is treated as actual black // atm we are always treating it as transparent // it seems this bit is not set for any data of Sherlock Holmes // PRE0 first 3 bits define how many bits per encoded pixel are used ccbPRE0_bitsPerPixel = imagefile3DO_cel_bitsPerPixelLookupTable[ccbPRE0 & 0x07]; if (!ccbPRE0_bitsPerPixel) error("load3DOCelFile: Invalid CCB PRE0 bits per pixel"); ccbPRE0_height = ((ccbPRE0 >> 6) & 0x03FF) + 1; ccbPRE1_width = (ccbPRE1 & 0x03FF) + 1; assert(ccbPRE0_height == ccbHeight); assert(ccbPRE1_width == ccbWidth); break; case MKTAG('P', 'L', 'U', 'T'): // pixel lookup table // optional, not required for at least 16-bit pixel data assert(dataSize >= 6); if (!ccbFound) error("load3DOCelFile: PLUT chunk found without CCB chunk"); if (plutFound) error("load3DOCelFile: multiple PLUT chunks currently not supported"); plutFound = true; plutCount = stream.readUint32BE(); // table follows, each entry is 16bit RGB555 assert(dataSize >= 4 + (plutCount * 2)); // security check assert(plutCount <= 256); // security check assert(plutCount <= 32); // PLUT should never contain more than 32 entries for (uint32 plutColorNr = 0; plutColorNr < plutCount; plutColorNr++) { plutRGBlookupTable.pixelColor[plutColorNr] = stream.readUint16BE(); } if (ccbPRE0_bitsPerPixel == 8) { // In case we are getting 8-bits per pixel, we calculate the shades accordingly // I'm not 100% sure if the calculation is correct. It's difficult to find information // on this topic. // The map uses this type of cel assert(plutCount == 32); // And we expect 32 entries inside PLUT chunk uint16 plutColorRGB = 0; for (uint32 plutColorNr = 0; plutColorNr < plutCount; plutColorNr++) { plutColorRGB = plutRGBlookupTable.pixelColor[plutColorNr]; // Extract RGB values byte plutColorRed = (plutColorRGB >> 10) & 0x1F; byte plutColorGreen = (plutColorRGB >> 5) & 0x1F; byte plutColorBlue = plutColorRGB & 0x1F; byte shadeMultiplier = 2; for (uint32 plutShadeNr = 1; plutShadeNr < 8; plutShadeNr++) { uint16 shadedColorRGB; byte shadedColorRed = (plutColorRed * shadeMultiplier) >> 3; byte shadedColorGreen = (plutColorGreen * shadeMultiplier) >> 3; byte shadedColorBlue = (plutColorBlue * shadeMultiplier) >> 3; shadedColorRed = CLIP(shadedColorRed, 0, 0x1F); shadedColorGreen = CLIP(shadedColorGreen, 0, 0x1F); shadedColorBlue = CLIP(shadedColorBlue, 0, 0x1F); shadedColorRGB = (shadedColorRed << 10) | (shadedColorGreen << 5) | shadedColorBlue; plutRGBlookupTable.pixelColor[plutColorNr + (plutShadeNr << 5)] = shadedColorRGB; shadeMultiplier++; } } } break; case MKTAG('X', 'T', 'R', 'A'): // Unknown contents, occurs right before PDAT break; case MKTAG('P', 'D', 'A', 'T'): { // pixel data for one frame // may be compressed or uncompressed pixels if (ccbPRE0_bitsPerPixel != 16) { // We require a pixel lookup table in case bits-per-pixel is lower than 16 if (!plutFound) error("load3DOCelFile: bits per pixel < 16, but no pixel lookup table was found"); } else { // But we don't like it in case bits-per-pixel is 16 and we find one if (plutFound) error("load3DOCelFile: bits per pixel == 16, but pixel lookup table was found as well"); } // read data into memory chunkDataPtr = new byte[dataSize]; stream.read(chunkDataPtr, dataSize); // Set up frame ImageFrame imageFrame; imageFrame._width = ccbWidth; imageFrame._height = ccbHeight; imageFrame._paletteBase = 0; imageFrame._offset.x = 0; imageFrame._offset.y = 0; imageFrame._rleEncoded = ccbFlags_compressed; imageFrame._size = 0; // Decompress/copy this frame if (!plutFound) { decompress3DOCelFrame(imageFrame, chunkDataPtr, dataSize, ccbPRE0_bitsPerPixel, NULL); } else { decompress3DOCelFrame(imageFrame, chunkDataPtr, dataSize, ccbPRE0_bitsPerPixel, &plutRGBlookupTable); } delete[] chunkDataPtr; push_back(imageFrame); break; } case MKTAG('O', 'F', 'S', 'T'): // 3DOSplash.cel // unknown contents break; default: error("Unsupported '%s' chunk in 3DO cel file", tag2str(chunkTag)); } // Seek to end of chunk stream.seek(chunkStartPos + chunkSize); } // Warning below being used to silence unused variable warnings for now warning("TODO: Remove %d %d %d", animFrameCount, ccbPPMP0, ccbPPMP1); } // Reads 3DO .cel data (room file format) void ImageFile3DO::load3DOCelRoomData(Common::SeekableReadStream &stream) { uint32 streamLeft = stream.size() - stream.pos(); uint16 roomDataHeader_size = 0; byte roomDataHeader_offsetX = 0; byte roomDataHeader_offsetY = 0; // CCB chunk (cel control block) uint32 ccbFlags = 0; bool ccbFlags_compressed = false; uint16 ccbPPMP0 = 0; uint16 ccbPPMP1 = 0; uint32 ccbPRE0 = 0; uint16 ccbPRE0_height = 0; byte ccbPRE0_bitsPerPixel = 0; uint32 ccbPRE1 = 0; uint16 ccbPRE1_width = 0; uint32 ccbWidth = 0; uint32 ccbHeight = 0; // cel data uint32 celDataSize = 0; while (streamLeft > 0) { // We expect at least 8 bytes basic header if (streamLeft < 8) error("load3DOCelRoomData: expected room data header, not enough bytes"); // 3DO sherlock holmes room data header stream.skip(4); // Possibly UINT16 width, UINT16 height?!?! roomDataHeader_size = stream.readUint16BE(); roomDataHeader_offsetX = stream.readByte(); roomDataHeader_offsetY = stream.readByte(); streamLeft -= 8; // We expect the header size specified in the basic header to be at least a raw CCB if (roomDataHeader_size < 68) error("load3DOCelRoomData: header size is too small"); // Check, that enough bytes for CCB are available if (streamLeft < 68) error("load3DOCelRoomData: expected raw cel control block, not enough bytes"); // 3DO raw cel control block ccbFlags = stream.readUint32BE(); stream.skip(3 * 4); // skip over 3 pointer fields, which are used in memory only by 3DO hardware stream.skip(8 * 4); // skip over 8 offset fields ccbPPMP0 = stream.readUint16BE(); ccbPPMP1 = stream.readUint16BE(); ccbPRE0 = stream.readUint32BE(); ccbPRE1 = stream.readUint32BE(); ccbWidth = stream.readUint32BE(); ccbHeight = stream.readUint32BE(); if (ccbFlags & 0x200) // bit 9 ccbFlags_compressed = true; // PRE0 first 3 bits define how many bits per encoded pixel are used ccbPRE0_bitsPerPixel = imagefile3DO_cel_bitsPerPixelLookupTable[ccbPRE0 & 0x07]; if (!ccbPRE0_bitsPerPixel) error("load3DOCelRoomData: Invalid CCB PRE0 bits per pixel"); ccbPRE0_height = ((ccbPRE0 >> 6) & 0x03FF) + 1; ccbPRE1_width = (ccbPRE1 & 0x03FF) + 1; assert(ccbPRE0_height == ccbHeight); assert(ccbPRE1_width == ccbWidth); if (ccbPRE0_bitsPerPixel != 16) { // We currently support 16-bits per pixel in here error("load3DOCelRoomData: bits per pixel < 16?!?!?"); } // Got the raw CCB streamLeft -= 68; // cel data follows // size field does not include the 8 byte header celDataSize = roomDataHeader_size - 68; if (streamLeft < celDataSize) error("load3DOCelRoomData: expected cel data, not enough bytes"); // read data into memory byte *celDataPtr = new byte[celDataSize]; stream.read(celDataPtr, celDataSize); streamLeft -= celDataSize; // Set up frame { ImageFrame imageFrame; imageFrame._width = ccbWidth; imageFrame._height = ccbHeight; imageFrame._paletteBase = 0; imageFrame._offset.x = roomDataHeader_offsetX; imageFrame._offset.y = roomDataHeader_offsetY; imageFrame._rleEncoded = ccbFlags_compressed; imageFrame._size = 0; // Decompress/copy this frame decompress3DOCelFrame(imageFrame, celDataPtr, celDataSize, ccbPRE0_bitsPerPixel, NULL); delete[] celDataPtr; push_back(imageFrame); } } // Suppress compiler warning warning("ccbPPMP0 = %d, ccbPPMP1 = %d", ccbPPMP0, ccbPPMP1); } static uint16 imagefile3DO_cel_bitsMask[17] = { 0, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF }; // gets [bitCount] bits from dataPtr, going from MSB to LSB inline uint16 ImageFile3DO::celGetBits(const byte *&dataPtr, byte bitCount, byte &dataBitsLeft) { byte resultBitsLeft = bitCount; uint16 result = 0; byte currentByte = *dataPtr; // Get bits of current byte while (resultBitsLeft) { if (resultBitsLeft < dataBitsLeft) { // we need less than we have left result |= (currentByte >> (dataBitsLeft - resultBitsLeft)) & imagefile3DO_cel_bitsMask[resultBitsLeft]; dataBitsLeft -= resultBitsLeft; resultBitsLeft = 0; } else { // we need as much as we have left or more resultBitsLeft -= dataBitsLeft; result |= (currentByte & imagefile3DO_cel_bitsMask[dataBitsLeft]) << resultBitsLeft; // Go to next byte dataPtr++; dataBitsLeft = 8; if (resultBitsLeft) { currentByte = *dataPtr; } } } return result; } // decompress/copy 3DO cel data void ImageFile3DO::decompress3DOCelFrame(ImageFrame &frame, const byte *dataPtr, uint32 dataSize, byte bitsPerPixel, ImageFile3DOPixelLookupTable *pixelLookupTable) { frame._frame.create(frame._width, frame._height, Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0)); uint16 *dest = (uint16 *)frame._frame.getPixels(); Common::fill(dest, dest + frame._width * frame._height, 0); int frameHeightLeft = frame._height; int frameWidthLeft = frame._width; uint16 pixelCount = 0; uint16 pixel = 0; const byte *srcLineStart = dataPtr; const byte *srcLineData = dataPtr; byte srcLineDataBitsLeft = 0; uint16 lineDWordSize = 0; uint16 lineByteSize = 0; if (bitsPerPixel == 16) { // Must not use pixel lookup table on 16-bits-per-pixel data assert(!pixelLookupTable); } if (frame._rleEncoded) { // compressed byte compressionType = 0; byte compressionPixels = 0; while (frameHeightLeft > 0) { frameWidthLeft = frame._width; if (bitsPerPixel >= 8) { lineDWordSize = READ_BE_UINT16(srcLineStart); srcLineData = srcLineStart + 2; } else { lineDWordSize = *srcLineStart; srcLineData = srcLineStart + 1; } srcLineDataBitsLeft = 8; lineDWordSize += 2; lineByteSize = lineDWordSize * 4; // calculate compressed data size in bytes for current line // debug //warning("offset %d: decoding line, size %d, bytesize %d", srcSeeker - src, dwordSize, lineByteSize); while (frameWidthLeft > 0) { // get 2 bits -> compressionType // get 6 bits -> pixel count (0 = 1 pixel) compressionType = celGetBits(srcLineData, 2, srcLineDataBitsLeft); // 6 bits == length (0 = 1 pixel) compressionPixels = celGetBits(srcLineData, 6, srcLineDataBitsLeft) + 1; if (!compressionType) // end of line break; switch(compressionType) { case 1: // simple copy for (pixelCount = 0; pixelCount < compressionPixels; pixelCount++) { pixel = celGetBits(srcLineData, bitsPerPixel, srcLineDataBitsLeft); if (pixelLookupTable) { pixel = pixelLookupTable->pixelColor[pixel]; } *dest++ = convertPixel(pixel); } break; case 2: // transparent for (pixelCount = 0; pixelCount < compressionPixels; pixelCount++) { *dest++ = 0; } break; case 3: // duplicate pixels pixel = celGetBits(srcLineData, bitsPerPixel, srcLineDataBitsLeft); if (pixelLookupTable) { pixel = pixelLookupTable->pixelColor[pixel]; } pixel = convertPixel(pixel); for (pixelCount = 0; pixelCount < compressionPixels; pixelCount++) { *dest++ = pixel; } break; default: break; } frameWidthLeft -= compressionPixels; } assert(frameWidthLeft >= 0); if (frameWidthLeft > 0) { // still pixels left? skip them dest += frameWidthLeft; } frameHeightLeft--; // Seek to next line start srcLineStart += lineByteSize; } } else { // uncompressed srcLineDataBitsLeft = 8; lineDWordSize = ((frame._width * bitsPerPixel) + 31) >> 5; lineByteSize = lineDWordSize * 4; uint32 totalExpectedSize = lineByteSize * frame._height; assert(totalExpectedSize <= dataSize); // security check while (frameHeightLeft > 0) { srcLineData = srcLineStart; frameWidthLeft = frame._width; while (frameWidthLeft > 0) { pixel = celGetBits(srcLineData, bitsPerPixel, srcLineDataBitsLeft); if (pixelLookupTable) { pixel = pixelLookupTable->pixelColor[pixel]; } *dest++ = convertPixel(pixel); frameWidthLeft--; } frameHeightLeft--; // Seek to next line start srcLineStart += lineByteSize; } } } // Reads Sherlock Holmes 3DO font file void ImageFile3DO::loadFont(Common::SeekableReadStream &stream) { uint32 streamSize = stream.size(); uint32 header_offsetWidthTable = 0; uint32 header_offsetBitsTable = 0; uint32 header_fontHeight = 0; uint32 header_bytesPerLine = 0; uint32 header_maxChar = 0; uint32 header_charCount = 0; byte *widthTablePtr = NULL; uint32 bitsTableSize = 0; byte *bitsTablePtr = NULL; stream.skip(2); // Unknown bytes stream.skip(2); // Unknown bytes (0x000E) header_offsetWidthTable = stream.readUint32BE(); header_offsetBitsTable = stream.readUint32BE(); stream.skip(4); // Unknown bytes (0x00000004) header_fontHeight = stream.readUint32BE(); header_bytesPerLine = stream.readUint32BE(); header_maxChar = stream.readUint32BE(); assert(header_maxChar <= 255); header_charCount = header_maxChar + 1; // Allocate memory for width table widthTablePtr = new byte[header_charCount]; stream.seek(header_offsetWidthTable); stream.read(widthTablePtr, header_charCount); // Allocate memory for the bits assert(header_offsetBitsTable < streamSize); // Security check bitsTableSize = streamSize - header_offsetBitsTable; bitsTablePtr = new byte[bitsTableSize]; stream.read(bitsTablePtr, bitsTableSize); // Now extract all characters uint16 curChar = 0; const byte *curBitsLinePtr = bitsTablePtr; const byte *curBitsPtr = NULL; byte curBitsLeft = 0; uint32 curCharHeightLeft = 0; uint32 curCharWidthLeft = 0; byte curBits = 0; byte curBitsReversed = 0; byte curPosX = 0; assert(bitsTableSize >= (header_maxChar * header_fontHeight * header_bytesPerLine)); // Security // first frame needs to be "!" (33 decimal) // our font code is subtracting 33 from the actual character code curBitsLinePtr += (33 * (header_fontHeight * header_bytesPerLine)); for (curChar = 33; curChar < header_charCount; curChar++) { // create frame ImageFrame imageFrame; imageFrame._width = widthTablePtr[curChar]; imageFrame._height = header_fontHeight; imageFrame._paletteBase = 0; imageFrame._offset.x = 0; imageFrame._offset.y = 0; imageFrame._rleEncoded = false; imageFrame._size = 0; // Extract pixels imageFrame._frame.create(imageFrame._width, imageFrame._height, Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0)); uint16 *dest = (uint16 *)imageFrame._frame.getPixels(); Common::fill(dest, dest + imageFrame._width * imageFrame._height, 0); curCharHeightLeft = header_fontHeight; while (curCharHeightLeft) { curCharWidthLeft = widthTablePtr[curChar]; curBitsPtr = curBitsLinePtr; curBitsLeft = 8; curPosX = 0; while (curCharWidthLeft) { if (!(curPosX & 1)) { curBits = *curBitsPtr >> 4; } else { curBits = *curBitsPtr & 0x0F; curBitsPtr++; } // doing this properly is complicated // the 3DO has built-in anti-aliasing // this here at least results in somewhat readable text // TODO: make it better if (curBits) { curBitsReversed = (curBits >> 3) | ((curBits & 0x04) >> 1) | ((curBits & 0x02) << 1) | ((curBits & 0x01) << 3); curBits = 20 - curBits; } byte curIntensity = curBits; *dest = (curIntensity << 11) | (curIntensity << 6) | curIntensity; dest++; curCharWidthLeft--; curPosX++; } curCharHeightLeft--; curBitsLinePtr += header_bytesPerLine; } push_back(imageFrame); } // Warning below being used to silence unused variable warnings for now warning("TODO: Remove %d %d", curBitsLeft, curBitsReversed); delete[] bitsTablePtr; delete[] widthTablePtr; } /*----------------------------------------------------------------*/ StreamingImageFile::StreamingImageFile() { _frameNumber = 0; _stream = nullptr; _flags = 0; _scaleVal = 0; _zPlacement = 0; _compressed = false; _active = false; } StreamingImageFile::~StreamingImageFile() { close(); } void StreamingImageFile::load(Common::SeekableReadStream *stream, bool compressed) { _stream = stream; _compressed = compressed; _frameNumber = -1; _active = true; } void StreamingImageFile::close() { delete _stream; _stream = nullptr; _frameNumber = -1; _active = false; _imageFrame._frame.free(); } bool StreamingImageFile::getNextFrame() { // Don't proceed if we're already at the end of the stream assert(_stream); if (_stream->pos() >= _stream->size()) { _active = false; return false; } // Increment frame number ++_frameNumber; // If necessary, decompress the next frame Common::SeekableReadStream *frameStream = _stream; if (_compressed) { uint32 inSize = _stream->readUint32LE(); Resources::decompressLZ(*_stream, _buffer, STREAMING_BUFFER_SIZE, inSize); frameStream = new Common::MemoryReadStream(_buffer, 11, DisposeAfterUse::NO); } // Load the data for the frame _imageFrame._width = frameStream->readUint16LE() + 1; _imageFrame._height = frameStream->readUint16LE() + 1; _imageFrame._paletteBase = frameStream->readByte(); _imageFrame._rleEncoded = frameStream->readByte() == 1; _imageFrame._offset.x = frameStream->readByte(); _imageFrame._offset.y = frameStream->readByte(); _imageFrame._size = frameStream->readUint16LE() - 11; _imageFrame._rleMarker = frameStream->readByte(); // Free the previous frame _imageFrame._frame.free(); // Decode the frame if (_compressed) { delete frameStream; _imageFrame.decompressFrame(_buffer + 11, true); } else { byte *data = new byte[_imageFrame._size]; _stream->read(data, _imageFrame._size); _imageFrame.decompressFrame(_buffer + 11, true); delete[] data; } return true; } } // End of namespace Sherlock