// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.resource.video;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.Arrays;
import org.infinity.resource.graphics.ColorConvert;
import org.infinity.resource.video.MveDecoder.MveInfo;
import org.infinity.resource.video.MveDecoder.MveSegment;
import org.infinity.util.Misc;
/**
* Decodes a single 8x8 pixel block of video data. (Internally used by MveDecoder)
*/
public class MveVideoDecoder
{
private final MveInfo info; // the currently used MVE info structure
private final int[] tmpBlock; // pre-allocated 8x8 pixel block usable in block copy routines
private final int[] tmpData; // pre-allocated working buffer of 32 elements for general use
private final Palette palette; // palette used in indexed color mode
private final BasicVideoBuffer workingBuffer; // internally used video buffer
private BufferedImage curBuffer; // points to the current working buffer
private BufferedImage prevBuffer; // points to the previous working buffer
private BufferedImage blackImage; // used when no video buffer has been updated in the current video chunk
private MveSegment codeSegment; // contains the code map of the last MVE_OC_CODE_MAP segment
private boolean isVideoDrawn; // set when the current video buffer has been updated
private boolean isVideoInit; // set when video initialization occured in the current chunk
/**
* Returns a new MveVideoDecoder object, asociated with the specified MveDecoder.
* @param mve The parent MveDecoder object
* @return A new MveVideoDecoder object associated with the specified MveDecoder or null on error.
*/
public static MveVideoDecoder createDecoder(MveInfo info)
{
if (info != null) {
return new MveVideoDecoder(info);
} else {
return null;
}
}
/**
* Processes video specific segments.
* @param segment The current segment to process.
* @return {@code true} if the segment has been processed successfully,
* {@code false} if the segment did not fit into the video category.
* @throws Exception On error.
*/
public boolean processVideo(MveSegment segment) throws Exception
{
if (info == null || segment == null)
throw new NullPointerException();
switch (segment.getOpcode()) {
case MveDecoder.MVE_OC_CREATE_TIMER: // sets a stable timer that can be used throughout the whole video
processVideoTimer(segment);
break;
case MveDecoder.MVE_OC_VIDEO_BUFFERS: // initializes video properties
processInitVideo(segment);
break;
case MveDecoder.MVE_OC_PLAY_VIDEO: // presents the completed frame
processOutputFrame(segment);
break;
case MveDecoder.MVE_OC_VIDEO_MODE: // unused
break;
case MveDecoder.MVE_OC_CREATE_GRADIENT: // creates a gradient palette (unused)
processGradient(segment);
break;
case MveDecoder.MVE_OC_PALETTE: // sets a palette
processPalette(segment);
break;
case MveDecoder.MVE_OC_PALETTE_PACKED: // sets palette entries
processPalettePacked(segment);
break;
case MveDecoder.MVE_OC_CODE_MAP: // prepares video code stream for this frame
processCodeMap(segment);
break;
case MveDecoder.MVE_OC_VIDEO_DATA: // decodes a frame (used in conjunction with MVE_OC_CODE_MAP)
processVideoFrame(segment);
break;
case MveDecoder.MVE_OC_END_OF_CHUNK: // do some temporary clean up
cleanUp();
break;
case MveDecoder.MVE_OC_END_OF_STREAM: // do final clean up
shutDown();
break;
default:
return false;
}
return true;
}
/**
* Properly releases temporary resources.
*/
public void close()
{
shutDown();
}
// Cannot be constructed directly.
private MveVideoDecoder(MveInfo info)
{
if (info == null)
throw new NullPointerException();
this.info = info;
workingBuffer = new BasicVideoBuffer();
curBuffer = (BufferedImage)workingBuffer.frontBuffer();
prevBuffer = (BufferedImage)workingBuffer.backBuffer();
palette = new MveVideoDecoder.Palette();
tmpBlock = new int[8*8];
tmpData = new int[32];
codeSegment = null;
isVideoDrawn = false;
blackImage = null;
}
// cleans up temporary data
private void cleanUp()
{
info.videoInitialized = isVideoInit;
isVideoInit = false;
isVideoDrawn = false;
codeSegment = null;
}
// cleans up all MVE specific
private void shutDown()
{
workingBuffer.release();
}
// sets the stable timer
private void processVideoTimer(MveSegment segment) throws Exception
{
int rate = segment.getBits(32);
int factor = segment.getBits(16);
info.frameDelay = rate * factor;
info.isFrameDelayStable = true;
}
// initializes video properties
private void processInitVideo(MveSegment segment) throws Exception
{
isVideoInit = true;
int width, height;
boolean isPalette;
switch (segment.getVersion()) {
case 0:
width = segment.getBits(16);
height = segment.getBits(16);
isPalette = true;
break;
case 1:
width = segment.getBits(16);
height = segment.getBits(16);
segment.getBits(16); // count: not needed
isPalette = true;
break;
case 2:
width = segment.getBits(16);
height = segment.getBits(16);
segment.getBits(16); // count: not needed
isPalette = (segment.getBits(16) == 0);
break;
default:
throw new Exception("Unsupported version of video initialization segment: " + segment.getVersion());
}
info.width = width << 3;
info.height = height << 3;
info.isPalette = isPalette;
palette.clearPalette();
workingBuffer.create(2, info.width, info.height, false);
curBuffer = (BufferedImage)workingBuffer.frontBuffer();
prevBuffer = (BufferedImage)workingBuffer.backBuffer();
blackImage = ColorConvert.createCompatibleImage(info.width, info.height, false);
}
// Presents the completed frame
private void processOutputFrame(MveSegment segment) throws Exception
{
if (info.videoOutput != null) {
// image is drawn centered
Image dstImage = info.videoOutput.backBuffer();
Image srcImage = null;
if (isVideoDrawn) {
srcImage = workingBuffer.frontBuffer();
} else if (blackImage != null) {
srcImage = blackImage;
} else {
srcImage = ColorConvert.createCompatibleImage(info.width, info.height, false);
}
Graphics2D g = (Graphics2D)dstImage.getGraphics();
int x = (dstImage.getWidth(null) - srcImage.getWidth(null)) / 2;
int y = (dstImage.getHeight(null) - srcImage.getHeight(null)) / 2;
g.drawImage(srcImage, x, y, null);
g.dispose();
info.videoOutput.flipBuffers();
srcImage = null;
dstImage = null;
}
workingBuffer.flipBuffers();
curBuffer = (BufferedImage)workingBuffer.frontBuffer();
prevBuffer = (BufferedImage)workingBuffer.backBuffer();
}
// reads decoding map from stream
private void processCodeMap(MveSegment segment) throws Exception
{
codeSegment = segment;
}
// creates a gradient palette
private void processGradient(MveSegment segment) throws Exception
{
// Color ranges: [0..63] for red and [0..39] for green/blue
int baseRB = segment.getBits(8);
int numR_RB = segment.getBits(8);
int numB_RB = segment.getBits(8);
int baseRG = segment.getBits(8);
int numR_RG = segment.getBits(8);
int numG_RG = segment.getBits(8);
if (baseRB > 0 && numR_RB > 0 && numB_RB > 0) {
int idx = baseRB;
for (int y = 0; y < numR_RB; y++) {
for (int x = 0; x < numB_RB; x++) {
palette.setColor(idx++, (byte)((63*y)/(numR_RB-1)), (byte)0, (byte)((36*x)/(numB_RB-1)));
}
}
}
if (baseRG > 0 && numR_RG > 0 && numG_RG > 0) {
int idx = baseRG;
for (int y = 0; y < numR_RG; y++) {
for (int x = 0; x < numG_RG; x++) {
palette.setColor(idx++, (byte)((63*y)/(numR_RG-1)), (byte)((36*x)/(numG_RG-1)), (byte)0);
}
}
}
}
// sets a new palette
private void processPalette(MveSegment segment) throws Exception
{
int palStart = segment.getBits(16);
int palCount = segment.getBits(16);
palette.setPalette(palStart, palCount, segment.getData(), 4);
}
// sets specific palette entries as defined in the segment data
private void processPalettePacked(MveSegment segment) throws Exception
{
for (int i = 0; i < (256 >>> 3); i++) {
int mask = segment.getBits(8);
if (mask != 0) {
for (int j = 0; j < 8; j++) {
if ((mask & (1 << j)) != 0) {
int idx = (i << 3) + j; // palette index
byte r = (byte)segment.getBits(8); // red
byte g = (byte)segment.getBits(8); // green
byte b = (byte)segment.getBits(8); // blue
palette.setColor(idx, r, g, b);
}
}
}
}
}
// entry point for the main video decoding routine
private void processVideoFrame(MveSegment segment) throws Exception
{
if (codeSegment == null)
throw new Exception("No code map available");
isVideoDrawn = true;
info.currentFrame = segment.getBits(16);
segment.getBits(16);
segment.getBits(16); // x offset: always 0 (maybe used for panning?)
segment.getBits(16); // y offset: always 0 (maybe used for panning?)
int sizeX = segment.getBits(16); // width in 8x8 blocks
int sizeY = segment.getBits(16); // height in 8x8 blocks
segment.getBits(16); // flags: bit 0 = delta frame (usually all frames but the first)
// using separate decoding functions for direct color and indexed color modes
int idx = 0, count = sizeX * sizeY;
if (info.isPalette) {
while (idx < count) {
int w = (idx % sizeX) << 3; // start x of the current 8x8 pixel block
int h = (idx / sizeX) << 3; // start y of the current 8x8 pixel block
byte code = (byte)codeSegment.getBits(4);
switch (code) {
case 0x00: decode8_00(w, h, segment); break;
case 0x01: decode8_01(w, h, segment); break;
case 0x02: decode8_02(w, h, segment); break;
case 0x03: decode8_03(w, h, segment); break;
case 0x04: decode8_04(w, h, segment); break;
case 0x05: decode8_05(w, h, segment); break;
case 0x06: decode8_06(w, h, segment); idx++; codeSegment.getBits(4); break;
case 0x07: decode8_07(w, h, segment); break;
case 0x08: decode8_08(w, h, segment); break;
case 0x09: decode8_09(w, h, segment); break;
case 0x0a: decode8_0a(w, h, segment); break;
case 0x0b: decode8_0b(w, h, segment); break;
case 0x0c: decode8_0c(w, h, segment); break;
case 0x0d: decode8_0d(w, h, segment); break;
case 0x0e: decode8_0e(w, h, segment); break;
case 0x0f: decode8_0f(w, h, segment); break;
}
idx++;
}
} else {
// separate data block used in codes 0x02, 0x03 and 0x04
int ofs = segment.getOffset();
int ofsExtra = segment.getBits(16);
segment.setOffsetExtra(ofs + ofsExtra);
while (idx < count) {
int w = (idx % sizeX) << 3; // start x of the current 8x8 pixel block
int h = (idx / sizeX) << 3; // start y of the current 8x8 pixel block
byte code = (byte)codeSegment.getBits(4);
switch (code) {
case 0x00: decode16_00(w, h, segment); break;
case 0x01: decode16_01(w, h, segment); break;
case 0x02: decode16_02(w, h, segment); break;
case 0x03: decode16_03(w, h, segment); break;
case 0x04: decode16_04(w, h, segment); break;
case 0x05: decode16_05(w, h, segment); break;
case 0x06: decode16_06(w, h, segment); idx++; codeSegment.getBits(4); break;
case 0x07: decode16_07(w, h, segment); break;
case 0x08: decode16_08(w, h, segment); break;
case 0x09: decode16_09(w, h, segment); break;
case 0x0a: decode16_0a(w, h, segment); break;
case 0x0b: decode16_0b(w, h, segment); break;
case 0x0c: decode16_0c(w, h, segment); break;
case 0x0d: decode16_0d(w, h, segment); break;
case 0x0e: decode16_0e(w, h, segment); break;
case 0x0f: decode16_0f(w, h, segment); break;
}
idx++;
}
}
}
//--------------- indexed color decoding routines ---------------
private void decode8_00(int startX, int startY, MveSegment segment)
{
// copy block from previous buffer
copyBlock8x8(prevBuffer, startX, startY, startX, startY);
}
private void decode8_01(int startX, int startY, MveSegment segment)
{
// block has same content as two buffers ago -> no change in double buffered chain
}
private void decode8_02(int startX, int startY, MveSegment segment)
{
// block is copied from below/right of current buffer
int v = segment.getBits(8);
int x, y;
if (v < 56) {
x = 8 + (v % 7);
y = v / 7;
} else {
x = -14 + ((v - 56) % 29);
y = 8 + ((v - 56) / 29);
}
copyBlock8x8(curBuffer, startX + x, startY + y, startX, startY);
}
private void decode8_03(int startX, int startY, MveSegment segment)
{
// block is copied from above/left of current buffer
int v = segment.getBits(8);
int x, y;
if (v < 56) {
x = -(8 + (v % 7));
y = -(v / 7);
} else {
x = -(-14 + (v - 56) % 29);
y = -(8 + ((v - 56) / 29));
}
copyBlock8x8(curBuffer, startX + x, startY + y, startX, startY);
}
private void decode8_04(int startX, int startY, MveSegment segment)
{
// block is copied from nearby (all directions) of previous buffer - short ranged
int x = -8 + segment.getBits(4);
int y = -8 + segment.getBits(4);
copyBlock8x8(prevBuffer, startX + x, startY + y, startX, startY);
}
private void decode8_05(int startX, int startY, MveSegment segment)
{
// block is copied from nearby (all directions) of previous buffer - long ranged
int x = Misc.signExtend(segment.getBits(8), 8);
int y = Misc.signExtend(segment.getBits(8), 8);
copyBlock8x8(prevBuffer, startX + x, startY + y, startX, startY);
}
private void decode8_06(int startX, int startY, MveSegment segment)
{
// indicates to skip this and the next pixel block (??? to be confirmed ???)
}
private void decode8_07(int startX, int startY, MveSegment segment)
{
// create a patterned 8x8 block (version 1)
int p0 = segment.getBits(8); tmpData[0] = palette.data[p0];
int p1 = segment.getBits(8); tmpData[1] = palette.data[p1];
int ofs = 0;
if (p0 <= p1) {
// mask bits (from left=7 to right=0, for each pixel): clear=c0, set=c1
for (int y = 0; y < 8; y++) {
int m = segment.getBits(8); // mask
for (int bit = 0; bit < 8; bit++, ofs++) {
tmpBlock[ofs] = tmpData[(m >>> bit) & 1];
}
}
} else {
// mask bits (from top-left=15, to bottom-right=0, for each 2x2 pixel block): clear=c1, set=c0
int m = segment.getBits(16);
for (int y = 0, bit = 0; y < 4; y++, ofs+=8) {
for (int x = 0; x < 4; x++, bit++, ofs+=2) {
int v = tmpData[(m >>> bit) & 1];
tmpBlock[ofs] = v; tmpBlock[ofs+1] = v;
tmpBlock[ofs+8] = v; tmpBlock[ofs+9] = v;
}
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode8_08(int startX, int startY, MveSegment segment)
{
// create a patterned 8x8 block (version 2)
int p0 = segment.getBits(8); tmpData[0] = palette.data[p0];
int p1 = segment.getBits(8); tmpData[1] = palette.data[p1];
tmpData[2] = segment.getBits(16);
int ofs = 0;
if (p0 <= p1) {
// pattern: c0, c1, mask(16) for each quadrant
for (int i = 3; i < 12; i+=3) {
tmpData[i] = palette.data[segment.getBits(8)];
tmpData[i+1] = palette.data[segment.getBits(8)];
tmpData[i+2] = segment.getBits(16);
}
for (int y = 0; y < 4; y++, ofs+=4) {
for (int x = 0; x < 4; x++, ofs++) {
int bit = (y << 2) + x;
// quadrant top-left [c0=0, c1=1, m=2]
tmpBlock[ofs] = tmpData[(tmpData[2] >>> bit) & 1];
// quadrant bottom-left [c0=3, c1=4, m=5]
tmpBlock[32+ofs] = tmpData[3 + ((tmpData[5] >>> bit) & 1)];
// quadrant top-right [c0=6, c1=7, m=8]
tmpBlock[4+ofs] = tmpData[6 + ((tmpData[8] >>> bit) & 1)];
// quadrant bottom-right [c0=9, c1=10, m=11]
tmpBlock[36+ofs] = tmpData[9 + ((tmpData[11] >>> bit) & 1)];
}
}
} else {
// pattern: c0, c1, mask(32) for either left/right or top/bottom halves
tmpData[2] |= segment.getBits(8) << 16; tmpData[2] |= segment.getBits(8) << 24;
int p2 = segment.getBits(8); tmpData[3] = palette.data[p2];
int p3 = segment.getBits(8); tmpData[4] = palette.data[p3];
tmpData[5] = segment.getBits(32);
if (p2 <= p3) {
// left/right halves
for (int y = 0; y < 8; y++, ofs+=4) {
for (int x = 0; x < 4; x++, ofs++) {
int bit = (y << 2) + x;
// left half: [c0=0, c1=1, m=2]
tmpBlock[ofs] = tmpData[(tmpData[2] >>> bit) & 1];
// right half: [c0=3, c1=4, m=5]
tmpBlock[4+ofs] = tmpData[3 + ((tmpData[5] >>> bit) & 1)];
}
}
} else {
// top/bottom halves
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 8; x++, ofs++) {
int bit = (y << 3) + x;
// top half: [c0=0, c1=1, m=2]
tmpBlock[ofs] = tmpData[(tmpData[2] >>> bit) & 1];
// bottom half: [c0=3, c1=4, m=5]
tmpBlock[32+ofs] = tmpData[3 + ((tmpData[5] >>> bit) & 1)];
}
}
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode8_09(int startX, int startY, MveSegment segment)
{
// create a patterned 8x8 block (version 3)
int p0 = segment.getBits(8); tmpData[0] = palette.data[p0];
int p1 = segment.getBits(8); tmpData[1] = palette.data[p1];
int p2 = segment.getBits(8); tmpData[2] = palette.data[p2];
int p3 = segment.getBits(8); tmpData[3] = palette.data[p3];
int ofs = 0, m = 0;
if (p0 <= p1) {
if (p2 <= p3) {
// two bits per pixel define which color [c0..c3] to take
for (int y = 0, bit = 0; y < 8; y++) {
if ((y & 1) == 0)
m = segment.getBits(32);
for (int x = 0; x < 8; x++, ofs++, bit+=2) {
tmpBlock[ofs] = tmpData[(m >>> (bit & 31)) & 3];
}
}
} else {
m = segment.getBits(32);
// two bits per 2x2 pixel block define which color [c0..c3] to take
for (int y = 0, bit = 0; y < 4; y++, ofs+=8) {
for (int x = 0; x < 4; x++, ofs+=2, bit+=2) {
tmpBlock[ofs] = tmpData[(m >>> bit) & 3];
tmpBlock[ofs+1] = tmpData[(m >>> bit) & 3];
tmpBlock[ofs+8] = tmpData[(m >>> bit) & 3];
tmpBlock[ofs+9] = tmpData[(m >>> bit) & 3];
}
}
}
} else {
if (p2 <= p3) {
// two bits per 2x1 pixel block define which color [c0..c3] to take
for (int y = 0, bit = 0; y < 8; y++) {
if ((y & 3) == 0)
m = segment.getBits(32);
for (int x = 0; x < 4; x++, ofs+=2, bit+=2) {
tmpBlock[ofs] = tmpData[(m >>> (bit & 31)) & 3];
tmpBlock[ofs+1] = tmpData[(m >>> (bit & 31)) & 3];
}
}
} else {
// two bits per 1x2 pixel block define which color [c0..c3] to take
for (int y = 0, bit = 0; y < 4; y++, ofs+=8) {
if ((y & 1) == 0)
m = segment.getBits(32);
for (int x = 0; x < 8; x++, ofs++, bit+=2) {
tmpBlock[ofs] = tmpData[(m >>> (bit & 31)) & 3];
tmpBlock[ofs+8] = tmpData[(m >>> (bit & 31)) & 3];
}
}
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode8_0a(int startX, int startY, MveSegment segment)
{
// create a patterned 8x8 block (version 4)
int p0 = segment.getBits(8); tmpData[0] = palette.data[p0];
int p1 = segment.getBits(8); tmpData[1] = palette.data[p1];
int p2 = segment.getBits(8); tmpData[2] = palette.data[p2];
int p3 = segment.getBits(8); tmpData[3] = palette.data[p3];
tmpData[4] = segment.getBits(32);
int ofs = 0;
if (p0 <= p1) {
// 4 quadrants, two bits per pixel to address one of 4 colors
for (int i = 5; i < 20; i+=5) {
tmpData[i] = palette.data[segment.getBits(8)];
tmpData[i+1] = palette.data[segment.getBits(8)];
tmpData[i+2] = palette.data[segment.getBits(8)];
tmpData[i+3] = palette.data[segment.getBits(8)];
tmpData[i+4] = segment.getBits(32);
}
for (int y = 0, bit = 0; y < 4; y++, ofs+=4) {
for (int x = 0; x < 4; x++, ofs++, bit+=2) {
// Quadrant top-left [0..4]
tmpBlock[ofs] = tmpData[(tmpData[4] >>> bit) & 3];
// Quadrant bottom-left [5..9]
tmpBlock[ofs+32] = tmpData[5 + ((tmpData[9] >>> bit) & 3)];
// Quadrant top-right [10..14]
tmpBlock[ofs+4] = tmpData[10 + ((tmpData[14] >>> bit) & 3)];
// Quadrant bottom-right [15..19]
tmpBlock[ofs+36] = tmpData[15 + ((tmpData[19] >>> bit) & 3)];
}
}
} else {
tmpData[5] = segment.getBits(32);
int p4 = segment.getBits(8); tmpData[6] = palette.data[p4];
int p5 = segment.getBits(8); tmpData[7] = palette.data[p5];
int p6 = segment.getBits(8); tmpData[8] = palette.data[p6];
int p7 = segment.getBits(8); tmpData[9] = palette.data[p7];
tmpData[10] = segment.getBits(32);
tmpData[11] = segment.getBits(32);
if (p4 <= p5) {
// left/right halves
for (int y = 0, bit = 0; y < 8; y++, ofs+=4) {
for (int x = 0; x < 4; x++, ofs++, bit+=2) {
// left half [0..5]
tmpBlock[ofs] = tmpData[(tmpData[4 + (bit >>> 5)] >>> (bit & 31)) & 3];
// right half [6..11]
tmpBlock[4+ofs] = tmpData[6 + ((tmpData[10 + (bit >>> 5)] >>> (bit & 31)) & 3)];
}
}
} else {
// top/bottom halves
for (int y = 0, bit = 0; y < 4; y++) {
for (int x = 0; x < 8; x++, ofs++, bit+=2) {
// top half [0..5]
tmpBlock[ofs] = tmpData[(tmpData[4 + (bit >>> 5)] >>> (bit & 30)) & 3];
// bottom half [6..11]
tmpBlock[32+ofs] = tmpData[6 + ((tmpData[10 + (bit >>> 5)] >>> (bit & 30)) & 3)];
}
}
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode8_0b(int startX, int startY, MveSegment segment)
{
// copy raw pixel data from segment data block (1 byte per pixel)
for (int i = 0; i < 64; i++) {
tmpBlock[i] = palette.data[segment.getBits(8)];
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode8_0c(int startX, int startY, MveSegment segment)
{
// copy raw pixel data from segment data block (1 byte per 2x2 pixel block)
int ofs = 0;
for (int y = 0; y < 4; y++, ofs+=8) {
for (int x = 0; x < 4; x++, ofs+=2) {
int c = palette.data[segment.getBits(8)];
tmpBlock[ofs] = c;
tmpBlock[ofs+1] = c;
tmpBlock[ofs+8] = c;
tmpBlock[ofs+9] = c;
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode8_0d(int startX, int startY, MveSegment segment)
{
// copy raw pixel data from segment data block (1 byte per 4x4 pixel block)
tmpData[0] = palette.data[segment.getBits(8)];
tmpData[1] = palette.data[segment.getBits(8)];
tmpData[2] = palette.data[segment.getBits(8)];
tmpData[3] = palette.data[segment.getBits(8)];
int ofs = 0;
for (int y = 0; y < 4; y++, ofs+=4) {
for (int x = 0; x < 4; x++, ofs++) {
tmpBlock[ofs] = tmpData[0];
tmpBlock[4+ofs] = tmpData[1];
tmpBlock[32+ofs] = tmpData[2];
tmpBlock[36+ofs] = tmpData[3];
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode8_0e(int startX, int startY, MveSegment segment)
{
// copy raw pixel data from segment data block (1 byte for the whole 8x8 pixel block)
int c = palette.data[segment.getBits(8)];
Arrays.fill(tmpBlock, c);
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode8_0f(int startX, int startY, MveSegment segment)
{
// create dithered pixel block by alternating two pixels from segment data block
tmpData[0] = palette.data[segment.getBits(8)];
tmpData[1] = palette.data[segment.getBits(8)];
int ofs = 0;
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++, ofs++) {
tmpBlock[ofs] = tmpData[(y+x) & 1];
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
// --------------- direct color decoding routines ---------------
private void decode16_00(int startX, int startY, MveSegment segment)
{
// copy block from previous buffer
copyBlock8x8(prevBuffer, startX, startY, startX, startY);
}
private void decode16_01(int startX, int startY, MveSegment segment)
{
// block has same content as two buffers ago -> no change in double buffered chain
}
private void decode16_02(int startX, int startY, MveSegment segment)
{
// block is copied from below/right of current buffer
int v = segment.getBitsExtra(8);
int x, y;
if (v < 56) {
x = 8 + (v % 7);
y = v / 7;
} else {
x = -14 + ((v - 56) % 29);
y = 8 + ((v - 56) / 29);
}
copyBlock8x8(curBuffer, startX + x, startY + y, startX, startY);
}
private void decode16_03(int startX, int startY, MveSegment segment)
{
// block is copied from above/left of current buffer
int v = segment.getBitsExtra(8);
int x, y;
if (v < 56) {
x = -(8 + (v % 7));
y = -(v / 7);
} else {
x = -(-14 + (v - 56) % 29);
y = -(8 + ((v - 56) / 29));
}
copyBlock8x8(curBuffer, startX + x, startY + y, startX, startY);
}
private void decode16_04(int startX, int startY, MveSegment segment)
{
// block is copied from nearby (all directions) of previous buffer - short ranged
int x = -8 + segment.getBitsExtra(4);
int y = -8 + segment.getBitsExtra(4);
copyBlock8x8(prevBuffer, startX + x, startY + y, startX, startY);
}
private void decode16_05(int startX, int startY, MveSegment segment)
{
// block is copied from nearby (all directions) of previous buffer - long ranged
int x = Misc.signExtend(segment.getBits(8), 8);
int y = Misc.signExtend(segment.getBits(8), 8);
copyBlock8x8(prevBuffer, startX + x, startY + y, startX, startY);
}
private void decode16_06(int startX, int startY, MveSegment segment)
{
// indicates to skip this and the next pixel block (??? to be confirmed ???)
}
private void decode16_07(int startX, int startY, MveSegment segment)
{
// create a patterned 8x8 block (version 1)
int p0 = segment.getBits(16); tmpData[0] = pixelToColor(p0);
int p1 = segment.getBits(16); tmpData[1] = pixelToColor(p1);
int ofs = 0;
if ((p0 & 0x8000) == 0) {
// mask bits (from left=7 to right=0, for each pixel): clear=c0, set=c1
for (int y = 0; y < 8; y++) {
int m = segment.getBits(8); // mask
for (int bit = 0; bit < 8; bit++, ofs++) {
tmpBlock[ofs] = tmpData[(m >>> bit) & 1];
}
}
} else {
// mask bits (from top-left=15, to bottom-right=0, for each 2x2 pixel block): clear=c1, set=c0
int m = segment.getBits(16);
for (int y = 0, bit = 0; y < 4; y++, ofs+=8) {
for (int x = 0; x < 4; x++, bit++, ofs+=2) {
int v = tmpData[(m >>> bit) & 1];
tmpBlock[ofs] = v; tmpBlock[ofs+1] = v;
tmpBlock[ofs+8] = v; tmpBlock[ofs+9] = v;
}
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode16_08(int startX, int startY, MveSegment segment)
{
// create a patterned 8x8 block (version 2)
int p0 = segment.getBits(16); tmpData[0] = pixelToColor(p0);
int p1 = segment.getBits(16); tmpData[1] = pixelToColor(p1);
tmpData[2] = segment.getBits(16);
int ofs = 0;
if ((p0 & 0x8000) == 0) {
// pattern: c0, c1, mask(16) for each quadrant
for (int i = 3; i < 12; i+=3) {
tmpData[i] = pixelToColor(segment.getBits(16));
tmpData[i+1] = pixelToColor(segment.getBits(16));
tmpData[i+2] = segment.getBits(16);
}
for (int y = 0; y < 4; y++, ofs+=4) {
for (int x = 0; x < 4; x++, ofs++) {
int bit = (y << 2) + x;
// quadrant top-left [c0=0, c1=1, m=2]
tmpBlock[ofs] = tmpData[(tmpData[2] >>> bit) & 1];
// quadrant bottom-left [c0=3, c1=4, m=5]
tmpBlock[32+ofs] = tmpData[3 + ((tmpData[5] >>> bit) & 1)];
// quadrant top-right [c0=6, c1=7, m=8]
tmpBlock[4+ofs] = tmpData[6 + ((tmpData[8] >>> bit) & 1)];
// quadrant bottom-right [c0=9, c1=10, m=11]
tmpBlock[36+ofs] = tmpData[9 + ((tmpData[11] >>> bit) & 1)];
}
}
} else {
// pattern: c0, c1, mask(32) for either left/right or top/bottom halves
tmpData[2] |= segment.getBits(8) << 16; tmpData[2] |= segment.getBits(8) << 24;
int p2 = segment.getBits(16); tmpData[3] = pixelToColor(p2);
int p3 = segment.getBits(16); tmpData[4] = pixelToColor(p3);
tmpData[5] = segment.getBits(32);
if ((p2 & 0x8000) == 0) {
// left/right halves
for (int y = 0; y < 8; y++, ofs+=4) {
for (int x = 0; x < 4; x++, ofs++) {
int bit = (y << 2) + x;
// left half: [c0=0, c1=1, m=2]
tmpBlock[ofs] = tmpData[(tmpData[2] >>> bit) & 1];
// right half: [c0=3, c1=4, m=5]
tmpBlock[4+ofs] = tmpData[3 + ((tmpData[5] >>> bit) & 1)];
}
}
} else {
// top/bottom halves
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 8; x++, ofs++) {
int bit = (y << 3) + x;
// top half: [c0=0, c1=1, m=2]
tmpBlock[ofs] = tmpData[(tmpData[2] >>> bit) & 1];
// bottom half: [c0=3, c1=4, m=5]
tmpBlock[32+ofs] = tmpData[3 + ((tmpData[5] >>> bit) & 1)];
}
}
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode16_09(int startX, int startY, MveSegment segment)
{
// create a patterned 8x8 block (version 3)
int p0 = segment.getBits(16); tmpData[0] = pixelToColor(p0);
int p1 = segment.getBits(16); tmpData[1] = pixelToColor(p1);
int p2 = segment.getBits(16); tmpData[2] = pixelToColor(p2);
int p3 = segment.getBits(16); tmpData[3] = pixelToColor(p3);
int ofs = 0, m = 0;
if ((p0 & 0x8000) == 0) {
if ((p2 & 0x8000) == 0) {
// two bits per pixel define which color [c0..c3] to take
for (int y = 0, bit = 0; y < 8; y++) {
if ((y & 1) == 0)
m = segment.getBits(32);
for (int x = 0; x < 8; x++, ofs++, bit+=2) {
tmpBlock[ofs] = tmpData[(m >>> (bit & 31)) & 3];
}
}
} else {
m = segment.getBits(32);
// two bits per 2x2 pixel block define which color [c0..c3] to take
for (int y = 0, bit = 0; y < 4; y++, ofs+=8) {
for (int x = 0; x < 4; x++, ofs+=2, bit+=2) {
tmpBlock[ofs] = tmpData[(m >>> bit) & 3];
tmpBlock[ofs+1] = tmpData[(m >>> bit) & 3];
tmpBlock[ofs+8] = tmpData[(m >>> bit) & 3];
tmpBlock[ofs+9] = tmpData[(m >>> bit) & 3];
}
}
}
} else {
if ((p2 & 0x8000) == 0) {
// two bits per 2x1 pixel block define which color [c0..c3] to take
for (int y = 0, bit = 0; y < 8; y++) {
if ((y & 3) == 0)
m = segment.getBits(32);
for (int x = 0; x < 4; x++, ofs+=2, bit+=2) {
tmpBlock[ofs] = tmpData[(m >>> (bit & 31)) & 3];
tmpBlock[ofs+1] = tmpData[(m >>> (bit & 31)) & 3];
}
}
} else {
// two bits per 1x2 pixel block define which color [c0..c3] to take
for (int y = 0, bit = 0; y < 4; y++, ofs+=8) {
if ((y & 1) == 0)
m = segment.getBits(32);
for (int x = 0; x < 8; x++, ofs++, bit+=2) {
tmpBlock[ofs] = tmpData[(m >>> (bit & 31)) & 3];
tmpBlock[ofs+8] = tmpData[(m >>> (bit & 31)) & 3];
}
}
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode16_0a(int startX, int startY, MveSegment segment)
{
// create a patterned 8x8 block (version 4)
int p0 = segment.getBits(16); tmpData[0] = pixelToColor(p0);
int p1 = segment.getBits(16); tmpData[1] = pixelToColor(p1);
int p2 = segment.getBits(16); tmpData[2] = pixelToColor(p2);
int p3 = segment.getBits(16); tmpData[3] = pixelToColor(p3);
tmpData[4] = segment.getBits(32);
int ofs = 0;
if ((p0 & 0x8000) == 0) {
// 4 quadrants, two bits per pixel to address one of 4 colors
for (int i = 5; i < 20; i+=5) {
tmpData[i] = pixelToColor(segment.getBits(16));
tmpData[i+1] = pixelToColor(segment.getBits(16));
tmpData[i+2] = pixelToColor(segment.getBits(16));
tmpData[i+3] = pixelToColor(segment.getBits(16));
tmpData[i+4] = segment.getBits(32);
}
for (int y = 0, bit = 0; y < 4; y++, ofs+=4) {
for (int x = 0; x < 4; x++, ofs++, bit+=2) {
// Quadrant top-left [0..4]
tmpBlock[ofs] = tmpData[(tmpData[4] >>> bit) & 3];
// Quadrant bottom-left [5..9]
tmpBlock[ofs+32] = tmpData[5 + ((tmpData[9] >>> bit) & 3)];
// Quadrant top-right [10..14]
tmpBlock[ofs+4] = tmpData[10 + ((tmpData[14] >>> bit) & 3)];
// Quadrant bottom-right [15..19]
tmpBlock[ofs+36] = tmpData[15 + ((tmpData[19] >>> bit) & 3)];
}
}
} else {
tmpData[5] = segment.getBits(32);
int p4 = segment.getBits(16); tmpData[6] = pixelToColor(p4);
int p5 = segment.getBits(16); tmpData[7] = pixelToColor(p5);
int p6 = segment.getBits(16); tmpData[8] = pixelToColor(p6);
int p7 = segment.getBits(16); tmpData[9] = pixelToColor(p7);
tmpData[10] = segment.getBits(32);
tmpData[11] = segment.getBits(32);
if ((p4 & 0x8000) == 0) {
// left/right halves
for (int y = 0, bit = 0; y < 8; y++, ofs+=4) {
for (int x = 0; x < 4; x++, ofs++, bit+=2) {
// left half [0..5]
tmpBlock[ofs] = tmpData[(tmpData[4 + (bit >>> 5)] >>> (bit & 31)) & 3];
// right half [6..11]
tmpBlock[4+ofs] = tmpData[6 + ((tmpData[10 + (bit >>> 5)] >>> (bit & 31)) & 3)];
}
}
} else {
// top/bottom halves
for (int y = 0, bit = 0; y < 4; y++) {
for (int x = 0; x < 8; x++, ofs++, bit+=2) {
// top half [0..5]
tmpBlock[ofs] = tmpData[(tmpData[4 + (bit >>> 5)] >>> (bit & 30)) & 3];
// bottom half [6..11]
tmpBlock[32+ofs] = tmpData[6 + ((tmpData[10 + (bit >>> 5)] >>> (bit & 30)) & 3)];
}
}
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode16_0b(int startX, int startY, MveSegment segment)
{
// copy raw pixel data from segment data block (1 byte per pixel)
for (int i = 0; i < 64; i++) {
tmpBlock[i] = pixelToColor(segment.getBits(16));
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode16_0c(int startX, int startY, MveSegment segment)
{
// copy raw pixel data from segment data block (1 byte per 2x2 pixel block)
int ofs = 0;
for (int y = 0; y < 4; y++, ofs+=8) {
for (int x = 0; x < 4; x++, ofs+=2) {
int c = pixelToColor(segment.getBits(16));
tmpBlock[ofs] = c;
tmpBlock[ofs+1] = c;
tmpBlock[ofs+8] = c;
tmpBlock[ofs+9] = c;
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode16_0d(int startX, int startY, MveSegment segment)
{
// copy raw pixel data from segment data block (1 byte per 4x4 pixel block)
tmpData[0] = pixelToColor(segment.getBits(16)); tmpData[1] = pixelToColor(segment.getBits(16));
tmpData[2] = pixelToColor(segment.getBits(16)); tmpData[3] = pixelToColor(segment.getBits(16));
int ofs = 0;
for (int y = 0; y < 4; y++, ofs+=4) {
for (int x = 0; x < 4; x++, ofs++) {
tmpBlock[ofs] = tmpData[0];
tmpBlock[4+ofs] = tmpData[1];
tmpBlock[32+ofs] = tmpData[2];
tmpBlock[36+ofs] = tmpData[3];
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode16_0e(int startX, int startY, MveSegment segment)
{
// copy raw pixel data from segment data block (1 byte for the whole 8x8 pixel block)
int c = pixelToColor(segment.getBits(16));
Arrays.fill(tmpBlock, c);
writeBlock8x8(tmpBlock, startX, startY);
}
private void decode16_0f(int startX, int startY, MveSegment segment)
{
// create dithered pixel block by alternating two pixels from segment data block
tmpData[0] = pixelToColor(segment.getBits(16));
tmpData[1] = pixelToColor(segment.getBits(16));
int ofs = 0;
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++, ofs++) {
tmpBlock[ofs] = tmpData[(y+x) & 1];
}
}
writeBlock8x8(tmpBlock, startX, startY);
}
// converts a 16-bit R5G5B5 color into a 32-bit A8R8G8B8 color
private int pixelToColor(int pixel)
{
return 0xff000000 | ((pixel & 0x7c00) << 9) | ((pixel & 0x03e0) << 6) | ((pixel & 0x1f) << 3);
}
// copy 8x8 pixel block from imgSrc to current buffer, using specified coordinates
private void copyBlock8x8(BufferedImage src, int srcX, int srcY, int dstX, int dstY)
{
if (srcX >= 0 && srcX + 8 <= src.getWidth() && srcY >= 0 && srcY + 8 <= src.getHeight()) {
int[] srcBuf = ((DataBufferInt)src.getRaster().getDataBuffer()).getData();
int[] dstBuf = ((DataBufferInt)curBuffer.getRaster().getDataBuffer()).getData();
int srcOfs = srcY*src.getWidth() + srcX;
int dstOfs = dstY*curBuffer.getWidth() + dstX;
if (srcOfs < 0) {
System.err.println("Debug: copyBlock8x8(src, " + srcX + ", " + srcY + ", " + dstX + ", " + dstY + ")");
return;
}
for (int y = 0; y < 8; y++, srcOfs+=src.getWidth(), dstOfs+=curBuffer.getWidth()) {
System.arraycopy(srcBuf, srcOfs, dstBuf, dstOfs, 8);
}
}
}
// write a 8x8 pixel block to the current buffer at the specified coordinates
private void writeBlock8x8(int[] block, int dstX, int dstY)
{
int[] dstBuf = ((DataBufferInt)curBuffer.getRaster().getDataBuffer()).getData();
int dstOfs = dstY*curBuffer.getWidth() + dstX;
for (int y = 0; y < 8; y++, dstOfs+=curBuffer.getWidth()) {
System.arraycopy(block, y << 3, dstBuf, dstOfs, 8);
}
}
//----------------------------- INNER CLASSES -----------------------------
/**
* Stores a color palette of variable size. The color component's range is always assumed [0..63].
*/
public static class Palette
{
/**
* Palette of 256 entries in format ARGB
*/
private final int[] data;
public Palette()
{
data = new int[256];
clearPalette();
}
/**
* Returns the specified palette entry.
* @param index The palette index (range: 0..255).
* @return The palette entry in A8R8G8B8 format.
*/
public int getColor(int index)
{
index &= 0xff;
return data[index];
}
/**
* Sets a specific palette entry to the specified RGB color.
* @param index The palette entry.
* @param r The red component
* @param g The green component
* @param b The blue component
*/
public void setColor(int index, byte r, byte g, byte b)
{
index &= 0xff;
data[index] = 0xff000000 | ((r & 0x3f) << 18) | ((g & 0x3f) << 10) | ((b & 0x3f) << 2);
}
/**
* Provides access to the palette.
* @return The palette as integer array.
*/
public int[] getPalette()
{
return data;
}
/**
* Sets a range of palette entries
* @param startIndex First palette entry to set.
* @param count Number of palette entries to set.
* @param rgbData RGB triplet data buffer to get the new palette entries from.
* @param rgbOfs Start offset in rgbData
*/
public void setPalette(int startIndex, int count, byte[] rgbData, int rgbOfs)
{
if (rgbData != null) {
startIndex &= 0xff;
if (startIndex + count > 256)
count = 256 - startIndex;
if (rgbData.length - rgbOfs < count * 3)
count = (rgbData.length - rgbOfs) / 3;
for (int i = 0; i < count; i++) {
data[startIndex+i] = 0xff000000 |
((rgbData[rgbOfs+i*3] & 0x3f) << 18) |
((rgbData[rgbOfs+i*3+1] & 0x3f) << 10) |
((rgbData[rgbOfs+i*3+2] & 0x3f) << 2);
}
}
}
/**
* Sets all color entries to black.
*/
public void clearPalette()
{
Arrays.fill(data, 0xff000000);
}
}
}