IMAGE: Add support for Xan codec videos (Crusader, Wing Commander)

This commit is contained in:
Matthew Duggan 2021-02-17 20:38:30 +09:00 committed by Eugene Sandulenko
parent 1947eae043
commit 017a801c96
4 changed files with 595 additions and 0 deletions

View File

@ -43,6 +43,7 @@
#include "image/codecs/smc.h"
#include "image/codecs/svq1.h"
#include "image/codecs/truemotion1.h"
#include "image/codecs/xan.h"
#include "common/endian.h"
#include "common/textconsole.h"
@ -224,6 +225,8 @@ Codec *createBitmapCodec(uint32 tag, uint32 streamTag, int width, int height, in
return new Indeo4Decoder(width, height, bitsPerPixel);
case MKTAG('I', 'V', '5', '0'):
return new Indeo5Decoder(width, height, bitsPerPixel);
case MKTAG('X', 'x', 'a', 'n'):
return new XanDecoder(width, height, bitsPerPixel);
#ifdef IMAGE_CODECS_TRUEMOTION1_H
case MKTAG('D','U','C','K'):
case MKTAG('d','u','c','k'):

502
image/codecs/xan.cpp Normal file
View File

@ -0,0 +1,502 @@
/* 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.
*/
/**
* Xan image decoder. (fourcc Xxan)
*
* Used by Crusader: No Regret AVI files
*
* This code was created based on the multimedia wiki:
* https://wiki.multimedia.cx/index.php/Origin_Xan_Codec
* and ffmpeg's libavcodec/xxan.c.
* The ffmpeg code is LGPL2 licensed and Copyright (C) 2011
* Konstantin Shishkov based on work by Mike Melanson.
*
* A similar format is used in Wing Commander III (although not in an AVI
* container) and IV.
*/
#include "image/codecs/xan.h"
#include "common/stream.h"
#include "common/bitstream.h"
#include "common/memstream.h"
#include "common/util.h"
#include "common/textconsole.h"
#include "common/system.h"
#include "common/debug.h"
#include "common/rect.h"
#include "graphics/surface.h"
#include "graphics/yuv_to_rgb.h"
namespace Image {
static const int SCRATCH_SPARE = 256;
XanDecoder::XanDecoder(int width, int height, int bitsPerPixel) : Codec(),
_width(width), _height(height), _wc4Mode(false) {
assert(bitsPerPixel == 16);
if (bitsPerPixel != 16)
error("XanDecoder: BPP must be 16 not %d", bitsPerPixel);
if (width % 2)
error("XanDecoder: width must be even, not %d", width);
_surface.create(_width, _height, getPixelFormat());
_scratchbuf = new uint8[_width * _height + SCRATCH_SPARE];
_lumabuf = new uint8[_width * _height];
_ybuf = new uint8[_width * _height];
_ubuf = new uint8[_width * _height / 2];
_vbuf = new uint8[_width * _height / 2];
memset(_scratchbuf, 0, _width * _height + SCRATCH_SPARE);
memset(_lumabuf, 0, _width * _height);
memset(_ybuf, 0, _width * _height);
memset(_ubuf, 127, _width * _height / 2);
memset(_vbuf, 127, _width * _height / 2);
}
XanDecoder::~XanDecoder() {
_surface.free();
delete [] _scratchbuf;
delete [] _lumabuf;
delete [] _ybuf;
delete [] _ubuf;
delete [] _vbuf;
}
const Graphics::Surface *XanDecoder::decodeFrame(Common::SeekableReadStream &stream) {
uint32 frametype = stream.readUint32LE();
if (frametype > 1) {
error("Xan frametype should be 0 or 1, got %d", frametype);
}
if (frametype == 0) {
decodeFrameType0(stream);
} else {
decodeFrameType1(stream);
}
return &_surface;
}
// An unoptimized version of the one from libavutil, but works fine
static void _memcpy_backptr(uint8 *dst, int back, int cnt) {
if (cnt <= 0 || back <= 0)
return;
const uint8 *src = &dst[-back];
if (back == 1) {
uint8 val = *src;
memset(dst, val, cnt);
} else {
int blocklen = back;
while (cnt > blocklen) {
memcpy(dst, src, blocklen);
dst += blocklen;
cnt -= blocklen;
blocklen <<= 1;
}
memcpy(dst, src, cnt);
}
}
// Unpack using the WC3 algorithm
static int _unpack(Common::SeekableReadStream &stream, uint8 *dest, const int dest_len) {
const uint8 *orig_dest = dest;
const uint8 *dest_end = dest + dest_len;
memset(dest, 0, dest_len);
while (dest < dest_end) {
if (stream.eos())
return -1;
const uint8 opcode = stream.readByte();
if (opcode < 0xe0) {
int readsize, copysize, back;
if ((opcode & 0x80) == 0) {
readsize = opcode & 3;
back = ((opcode & 0x60) << 3) + stream.readByte() + 1;
copysize = ((opcode & 0x1c) >> 2) + 3;
} else if ((opcode & 0x40) == 0) {
const uint8 b = stream.readByte();
readsize = b >> 6;
back = ((b & 0x3f) << 8) + stream.readByte() + 1;
copysize = (opcode & 0x3f) + 4;
} else {
readsize = opcode & 3;
back = ((opcode & 0x10) << 12) + stream.readUint16BE() + 1;
copysize = ((opcode & 0x0c) << 6) + stream.readByte() + 5;
if (readsize + copysize > dest_end - dest)
break;
}
if (dest + readsize + copysize > dest_end ||
dest - orig_dest + readsize < back)
return -1;
stream.read(dest, readsize);
dest += readsize;
_memcpy_backptr(dest, back, copysize);
dest += copysize;
} else {
bool finish = (opcode >= 0xfc);
int readsize = finish ? opcode & 3 : ((opcode & 0x1f) * 4) + 4;
if (dest + readsize > dest_end)
return -1;
stream.read(dest, readsize);
dest += readsize;
if (finish)
break;
}
}
return dest - orig_dest;
}
bool XanDecoder::decodeChroma(Common::SeekableReadStream &stream, const int chroma_off) {
if (!chroma_off)
return 0;
if (chroma_off + 4 >= stream.size() - stream.pos()) {
warning("Invalid chroma block position");
return false;
}
stream.seek(chroma_off + 4);
const int mode = stream.readUint16LE();
const int table_start = stream.pos();
// 2 bytes per table entry
const int table_size = stream.readUint16LE() * 2;
if (mode > 1) {
warning("Unexpected chroma mode %d", mode);
return false;
}
if (table_size >= stream.size() - stream.pos()) {
warning("Invalid chroma block offset");
return false;
}
stream.skip(table_size);
const int dec_size = _unpack(stream, _scratchbuf, _width * _height);
if (dec_size < 0) {
warning("Chroma unpacking failed");
return false;
}
const int pitch = _width / 2;
uint8 *U = _ubuf;
uint8 *V = _vbuf;
const uint8 *src = _scratchbuf;
const uint8 *src_end = src + dec_size;
if (mode) {
// YUV420 frame
for (int y = 0; y < _height / 2; y++) {
for (int x = 0; x < pitch; x++) {
if (src >= src_end)
return true;
int toff = *src++ * 2;
if (toff) {
if (toff > table_size)
return false;
const int pos = stream.pos();
stream.seek(table_start + toff);
uint8 uval, vval;
if (_wc4Mode) {
uint16 val = stream.readUint16LE();
uval = (val >> 3) & 0xF8;
vval = (val >> 8) & 0xF8;
} else {
uval = stream.readByte();
vval = stream.readByte();
}
uval = uval | uval >> 5;
vval = vval | vval >> 5;
stream.seek(pos);
U[x] = uval;
V[x] = vval;
}
}
U += pitch;
V += pitch;
}
if (_height % 1) {
memcpy(U, U - pitch, pitch);
memcpy(V, V - pitch, pitch);
}
} else {
// YUV410 frame - expand out U and V components
uint8 *U2 = U + pitch;
uint8 *V2 = V + pitch;
for (int y = 0; y < _height / 4; y++) {
for (int x = 0; x < pitch; x += 2) {
if (src >= src_end)
return true;
int toff = *src++ * 2;
if (toff) {
if (toff > table_size)
return false;
const int pos = stream.pos();
stream.seek(table_start + toff);
uint8 uval, vval;
if (_wc4Mode) {
uint16 val = stream.readUint16LE();
uval = (val >> 3) & 0xF8;
vval = (val >> 8) & 0xF8;
} else {
uval = stream.readByte();
vval = stream.readByte();
}
uval = uval | uval >> 5;
vval = vval | vval >> 5;
stream.seek(pos);
U[x] = U[x + 1] = U2[x] = U2[x + 1] = uval;
V[x] = V[x + 1] = V2[x] = V2[x + 1] = vval;
}
}
U += pitch * 2;
V += pitch * 2;
U2 += pitch * 2;
V2 += pitch * 2;
}
if (_height % 4) {
int lines = ((_height + 1) / 2) - (_height / 4) * 2;
memcpy(U, U - lines * pitch, lines * pitch);
memcpy(V, V - lines * pitch, lines * pitch);
}
}
return true;
}
void XanDecoder::decodeFrameType0(Common::SeekableReadStream &stream) {
const uint32 chroma_offset = stream.readUint32LE();
const uint32 refines_offset = stream.readUint32LE();
const uint32 luma_offset = stream.pos();
const uint32 nbytes = static_cast<uint32>(stream.size());
if (chroma_offset > nbytes || refines_offset > nbytes) {
error("invalid frame type 0 offsets");
}
if (!decodeChroma(stream, chroma_offset)) {
warning("xxan chrome decode failed frame type 0");
return;
}
stream.seek(luma_offset);
decompressLuma(stream);
//
// Expand out the decompressed luma data. For type 0 frames:
// * luma vals are 5-bit diffs, where
// * top row values are diffs on the last value
// * and the remaining rows are deltas on the value above.
// * every second pixel in x is linearly interpolated from its horizontal neighbours.
// * output values are clipped to 6 bits.
// * a set of refinements values can adjust luma of interpolated pixels
//
const uint8 *lumadecomp = _scratchbuf;
uint8 *lumarow = _lumabuf;
int last = *lumadecomp++;
lumarow[0] = last * 2;
int x;
// The top row uses only the left value for prediction
for (x = 1; x < _surface.w - 1; x += 2) {
int cur = (last + *lumadecomp++) & 0x1F;
lumarow[x] = last + cur;
lumarow[x + 1] = cur * 2;
last = cur;
}
lumarow[x] = last * 2;
uint8 const *last_lumarow = lumarow;
lumarow += _surface.w;
// The remaining rows
for (int y = 1; y < _surface.h; y++) {
last = ((last_lumarow[0] / 2) + *lumadecomp++) & 0x1F;
lumarow[0] = last * 2;
for (x = 1; x < _surface.w - 1; x += 2) {
int cur = ((last_lumarow[x + 1] / 2) + *lumadecomp++) & 0x1F;
lumarow[x] = last + cur;
lumarow[x + 1] = cur * 2;
last = cur;
}
lumarow[x] = last * 2;
last_lumarow = lumarow;
lumarow += _surface.w;
}
if (refines_offset) {
stream.seek(refines_offset + 8);
int dec_size = _unpack(stream, _scratchbuf, _width * _height / 2);
if (dec_size < 0) {
warning("luma refine unpacking failed!");
dec_size = 0;
} else {
dec_size = MIN(dec_size, _width * _height / 2 - 1);
}
for (int i = 0; i < dec_size; i++)
_lumabuf[i * 2 + 1] = (_lumabuf[i * 2 + 1] + _scratchbuf[i] * 2) & 0x3F;
}
convertYUVtoRGBSurface();
}
void XanDecoder::decodeFrameType1(Common::SeekableReadStream &stream) {
const uint32 chroma_offset = stream.readUint32LE();
const uint32 refines_offset = stream.readUint32LE();
const uint32 refine2_offset = stream.readUint32LE();
const uint32 luma_offset = stream.pos();
const uint32 nbytes = static_cast<uint32>(stream.size());
if (chroma_offset > nbytes || refines_offset > nbytes || refine2_offset > nbytes) {
error("invalid frame type 1 offset");
}
if (!decodeChroma(stream, chroma_offset)) {
warning("xxan chrome decode failed frame type 1");
return;
}
stream.seek(luma_offset);
decompressLuma(stream);
//
// Expand out the decompressed luma data. For type 1 frames:
// * luma vals are 5-bit diffs on the previous frame's values
// * every second pixel in x is linearly interpolated from its horizontal neighbours.
// * output values are clipped to 6 bits.
// * a set of refinements values can adjust luma of interpolated pixels
//
const uint8 *lumadecomp = _scratchbuf;
uint8 *lumarow = _lumabuf;
for (int y = 0; y < _surface.h; y++) {
int x;
int last = (lumarow[0] + (*lumadecomp++ * 2)) & 0x3F;
lumarow[0] = last;
for (x = 1; x < _surface.w - 1; x += 2) {
int cur = (lumarow[x + 1] + (*lumadecomp++ * 2)) & 0x3F;
lumarow[x] = (last + cur) / 2;
lumarow[x + 1] = cur;
last = cur;
}
lumarow[x] = last;
lumarow += _surface.w;
}
if (refines_offset) {
stream.seek(refines_offset + 8);
int dec_size = _unpack(stream, _scratchbuf, _width * _height / 2);
if (dec_size < 0) {
warning("luma refine unpacking failed!");
dec_size = 0;
} else {
dec_size = MIN(dec_size, _width * _height / 2 - 1);
}
int dec2_size = 0;
uint8 *scratch2 = _scratchbuf + _width * _height / 2;
if (refine2_offset) {
stream.seek(refine2_offset + 8);
dec2_size = _unpack(stream, scratch2, _width * _height / 2);
if (dec2_size < 0) {
warning("luma refine2 unpacking failed!");
dec2_size = 0;
} else {
dec2_size = MIN(dec_size, _width * _height / 2 - 1);
}
}
for (int i = 0; i < dec_size; i++) {
int adjust = _scratchbuf[i] * 2;
if (dec2_size)
adjust += scratch2[i] * 2;
_lumabuf[i * 2 + 1] = (_lumabuf[i * 2 + 1] + adjust) & 0x3F;
}
}
convertYUVtoRGBSurface();
}
void XanDecoder::decompressLuma(Common::SeekableReadStream &stream) {
const int32 startpos = stream.pos();
const int nsymbols = stream.readByte();
const int eofsymbol = stream.readByte();
const int root = nsymbols + eofsymbol;
const uint8 *lumaend = _scratchbuf + _surface.w * _surface.h;
stream.skip(nsymbols * 2);
uint8 *luma = _scratchbuf;
int node = root;
int bits = stream.readByte();
int mask = 0x80;
while (!stream.eos()) {
const int bit = ((bits & mask) ? 1 : 0);
mask >>= 1;
const int32 nextbitspos = stream.pos();
stream.seek(startpos + node * 2 + bit - eofsymbol * 2);
node = stream.readByte();
stream.seek(nextbitspos);
if (node == eofsymbol)
break;
if (node < eofsymbol) {
*luma++ = node;
if (luma >= lumaend)
break;
node = root;
}
if (!mask) {
if (stream.eos())
break;
bits = stream.readByte();
mask = 0x80;
}
}
}
void XanDecoder::convertYUVtoRGBSurface() {
// Expand luma from 6-bit to 8-bit.
for (int i = 0; i < _width * _height; i++)
_ybuf[i] = _lumabuf[i] << 2 | _lumabuf[i] >> 4;
YUVToRGBMan.convert420(&_surface, Graphics::YUVToRGBManager::kScaleFull,
_ybuf, _ubuf, _vbuf, _width, (_height / 2) * 2, _width, _width / 2);
}
Graphics::PixelFormat XanDecoder::getPixelFormat() const {
return Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24);
}
} // End of namespace Image

89
image/codecs/xan.h Normal file
View File

@ -0,0 +1,89 @@
/* 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.
*
*/
#ifndef IMAGE_CODECS_XAN_H
#define IMAGE_CODECS_XAN_H
#include "image/codecs/codec.h"
namespace Image {
/**
* Xan image decoder. (fourcc Xxan)
*
* Used by Crusader: No Regret AVI files
*
* This code was created based on the multimedia wiki:
* https://wiki.multimedia.cx/index.php/Origin_Xan_Codec
* and ffmpeg's libavcodec/xxan.c.
* The ffmpeg code is LGPL2 licensed and Copyright (C) 2011
* Konstantin Shishkov based on work by Mike Melanson.
*
* A similar format is used in Wing Commander III (although not in an AVI
* container) and IV.
*/
class XanDecoder : public Codec {
public:
XanDecoder (int width, int height, int bitsPerPixel);
~XanDecoder();
const Graphics::Surface *decodeFrame(Common::SeekableReadStream &stream) override;
Graphics::PixelFormat getPixelFormat() const override;
private:
void decodeFrameType0(Common::SeekableReadStream &stream);
void decodeFrameType1(Common::SeekableReadStream &stream);
/** Decompress the huffman table for base luma data */
void decompressLuma(Common::SeekableReadStream &stream);
bool decodeChroma(Common::SeekableReadStream &stream, int chroma_off);
/** convert the internally expanded YUV to the output RGBA surface */
void convertYUVtoRGBSurface();
/** A buffer to hold the final frame in RGBA */
Graphics::Surface _surface;
/** Dest surface width and height */
int _width, _height;
/** If true, decode chroma vals in Wing Commander 4 style (false = No Regret style) */
bool _wc4Mode;
/** A buffer to hold scratch data. Either chroma data in progress, or
* decompressed delta luma values (5-bit). Interpretation depends on frame type. */
uint8 *_scratchbuf;
/** A buffer to hold expanded/interpolated absolute luma values (6-bit) from the values in _scratchbuf.
* These still need to be multiplied out to make 8-bit values. */
uint8 *_lumabuf;
/** a buffer for uncompressed and multiplied out "y" values (of yuv) */
uint8 *_ybuf;
/** a buffer for uncompressed "u" values (of yuv) */
uint8 *_ubuf;
/** a buffer for uncompressed "v" values (of yuv) */
uint8 *_vbuf;
};
} // End of namespace Image
#endif

View File

@ -26,6 +26,7 @@ MODULE_OBJS := \
codecs/smc.o \
codecs/svq1.o \
codecs/truemotion1.o \
codecs/xan.o \
codecs/indeo/indeo.o \
codecs/indeo/indeo_dsp.o \
codecs/indeo/mem.o \