mirror of
https://github.com/Olde-Skuul/spaceaceiigs.git
synced 2024-10-31 18:04:43 +00:00
10ea4b36ea
Updated the Mac and PC tools to convert IIgs video files back into animated GIF files.
610 lines
15 KiB
C++
610 lines
15 KiB
C++
/***************************************
|
|
|
|
Tool to pre-process video data for Space Ace IIgs
|
|
|
|
Copyright (c) 1995-2015 by Rebecca Ann Heineman <becky@burgerbecky.com>
|
|
|
|
It is released under an MIT Open Source license. Please see LICENSE
|
|
for license details. Yes, you can use it in a
|
|
commercial title without paying anything, just give me a credit.
|
|
Please? It's not like I'm asking you for money!
|
|
|
|
***************************************/
|
|
|
|
#include "packvideo.h"
|
|
|
|
/***************************************
|
|
|
|
Convert the IIgs palette to RGBAWord8_t
|
|
|
|
***************************************/
|
|
|
|
static void BURGER_API ConvertPalette(RGBAWord8_t *pOutput,const Word8 *pInput)
|
|
{
|
|
Word uIndex = 0;
|
|
do {
|
|
pOutput->m_uRed = Renderer::RGB4ToRGB8Table[pInput[1]&0xFU];
|
|
pOutput->m_uGreen = Renderer::RGB4ToRGB8Table[pInput[0]>>4U];
|
|
pOutput->m_uBlue = Renderer::RGB4ToRGB8Table[pInput[0]&0xFU];
|
|
pOutput->m_uAlpha = 0xFF;
|
|
pInput+=2;
|
|
++pOutput;
|
|
} while (++uIndex<16);
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Convert the RGBAWord8_t palette to IIgs
|
|
|
|
***************************************/
|
|
|
|
static void BURGER_API ConvertPalette(Word8 *pOutput,const RGBAWord8_t *pInput)
|
|
{
|
|
Word uIndex = 0;
|
|
do {
|
|
Word uTemp = pInput->m_uGreen&0xF0U;
|
|
uTemp |= (pInput->m_uBlue>>4U);
|
|
pOutput[0] = static_cast<Word8>(uTemp);
|
|
pOutput[1] = static_cast<Word8>(pInput->m_uRed>>4U);
|
|
++pInput;
|
|
pOutput+=2;
|
|
} while (++uIndex<16);
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Test if the IIgs palette has changed
|
|
|
|
***************************************/
|
|
|
|
static Word BURGER_API ComparePalette(const Word8 *pInput1,const Word8 *pInput2)
|
|
{
|
|
return MemoryCompare(pInput1,pInput2,32);
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Convert bitmap to IIgs format
|
|
|
|
8 bits per pixel converted to 4 bits per pixel
|
|
|
|
***************************************/
|
|
|
|
static void BURGER_API ConvertPixelsToIIgs(Word8 *pOutput,const Image *pInput)
|
|
{
|
|
WordPtr uStride = pInput->GetStride()-320;
|
|
const Word8 *pPixels = pInput->GetImage();
|
|
WordPtr j=200;
|
|
do {
|
|
WordPtr i=320/2;
|
|
do {
|
|
Word uTemp = pPixels[0]<<4U;
|
|
uTemp |= (pPixels[1]&0xFU);
|
|
pOutput[0] = static_cast<Word8>(uTemp);
|
|
pPixels+=2;
|
|
++pOutput;
|
|
} while (--i);
|
|
pPixels+=uStride;
|
|
} while (--j);
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Compress a IIgs keyframe
|
|
|
|
The compression only packs runs of a minimum of 3 matching
|
|
bytes to reduce mode switching during decompression
|
|
|
|
***************************************/
|
|
|
|
static void BURGER_API CompressKeyFrame(OutputMemoryStream *pOutput,const Word8 *pInput)
|
|
{
|
|
// Number of bytes to process
|
|
WordPtr uInputLength = 320*200/2;
|
|
|
|
do {
|
|
// If two bytes or less, just store and exit
|
|
if (uInputLength<3) {
|
|
pOutput->Append(static_cast<Word8>(uInputLength));
|
|
pOutput->Append(pInput,uInputLength);
|
|
break;
|
|
}
|
|
|
|
// Check for repeater of at LEAST three bytes
|
|
Word uMatchTest = pInput[0];
|
|
|
|
// Is there a run?
|
|
if ((pInput[1] == uMatchTest) && (pInput[2] == uMatchTest)) {
|
|
|
|
// Maximum length of a matched run
|
|
WordPtr uMaximumRun = 127;
|
|
if (uInputLength<127) {
|
|
uMaximumRun = uInputLength; // 1-127
|
|
}
|
|
WordPtr uRun = 3-1;
|
|
while (++uRun<uMaximumRun) {
|
|
// Find end of repeater
|
|
if (pInput[uRun]!=uMatchTest) {
|
|
break;
|
|
}
|
|
}
|
|
// Encode 128-255 for 0-127 run
|
|
pOutput->Append(static_cast<Word8>(0x80|uRun));
|
|
pOutput->Append(static_cast<Word8>(uMatchTest));
|
|
uInputLength-=uRun;
|
|
pInput += uRun;
|
|
} else {
|
|
// Raw run, minimum size of 2 bytes
|
|
WordPtr uMaximumRun = 127;
|
|
if (uInputLength<127) {
|
|
uMaximumRun = uInputLength;
|
|
}
|
|
// Preload the next byte
|
|
uMatchTest = pInput[1];
|
|
WordPtr uRun = 2-1;
|
|
while (++uRun<uMaximumRun) {
|
|
// Scan for next repeater
|
|
if (pInput[uRun]==uMatchTest && (pInput[uRun+1]==uMatchTest)) {
|
|
// Remove from the run
|
|
--uRun;
|
|
break;
|
|
}
|
|
// Get the next byte
|
|
uMatchTest = pInput[uRun];
|
|
}
|
|
// Perform a raw data transfer
|
|
// Run 1-128
|
|
// Encode 0-127
|
|
pOutput->Append(static_cast<Word8>(uRun));
|
|
pOutput->Append(pInput,uRun);
|
|
uInputLength-=uRun;
|
|
pInput += uRun;
|
|
}
|
|
} while (uInputLength);
|
|
|
|
// Mark the end of compressed data
|
|
pOutput->Append(static_cast<Word8>(0));
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Compress a IIgs animation frame
|
|
|
|
***************************************/
|
|
|
|
static void BURGER_API CompressAnimFrame(OutputMemoryStream *pOutput,const Word8 *pPreviousFrame,const Word8 *pCurrentFrame)
|
|
{
|
|
// Number of bytes to process
|
|
WordPtr uInputLength = 320*200/2;
|
|
do {
|
|
// Check if there were any differences between the frames to create a skip token
|
|
WordPtr uMaximumRun = 127; // Skip token maximum value
|
|
if (uInputLength<uMaximumRun) {
|
|
uMaximumRun = uInputLength;
|
|
}
|
|
|
|
// Test from the previous frame to the current frame
|
|
WordPtr uRun = 0;
|
|
do {
|
|
if (pPreviousFrame[uRun]!=pCurrentFrame[uRun]) {
|
|
break;
|
|
}
|
|
} while (++uRun<uMaximumRun);
|
|
|
|
// If the run is at least 2 bytes or end of the data, use it as is
|
|
|
|
if ((uRun==uInputLength) || (uRun>=3)) {
|
|
|
|
// Output a "skip" data token
|
|
pOutput->Append(static_cast<Word8>(uRun));
|
|
pPreviousFrame+=uRun;
|
|
pCurrentFrame+=uRun;
|
|
uInputLength-=uRun;
|
|
|
|
} else {
|
|
|
|
// Maximum length of a matched run
|
|
uMaximumRun = 255;
|
|
if (uInputLength<255) {
|
|
uMaximumRun = uInputLength; // 1-255
|
|
}
|
|
|
|
Word uMatchTest = pCurrentFrame[0];
|
|
uRun = 1;
|
|
while (uRun<uMaximumRun) {
|
|
// Find end of repeater
|
|
if (pCurrentFrame[uRun]!=uMatchTest) {
|
|
break;
|
|
}
|
|
++uRun;
|
|
}
|
|
|
|
// Is there a run of 4 or greater?
|
|
if (uRun>=4) {
|
|
// Encode the run length
|
|
pOutput->Append(static_cast<Word8>(0));
|
|
pOutput->Append(static_cast<Word8>(uRun));
|
|
pOutput->Append(static_cast<Word8>(uMatchTest));
|
|
uInputLength -= uRun;
|
|
pCurrentFrame += uRun;
|
|
pPreviousFrame += uRun;
|
|
|
|
} else {
|
|
|
|
// Raw run
|
|
uMaximumRun = 127;
|
|
if (uInputLength<127) {
|
|
uMaximumRun = uInputLength;
|
|
}
|
|
uRun = 0;
|
|
while (++uRun<uMaximumRun) {
|
|
// Scan for next repeater
|
|
if (pCurrentFrame[uRun]==uMatchTest && (pCurrentFrame[uRun+1]==uMatchTest) && (pCurrentFrame[uRun+2]==uMatchTest)) {
|
|
// Remove from the run
|
|
--uRun;
|
|
break;
|
|
}
|
|
if ((pCurrentFrame[uRun]==pPreviousFrame[uRun]) &&
|
|
(pCurrentFrame[uRun+1]==pPreviousFrame[uRun+1]) &&
|
|
(pCurrentFrame[uRun+2]==pPreviousFrame[uRun+2])) {
|
|
break;
|
|
}
|
|
// Get the next byte
|
|
uMatchTest = pCurrentFrame[uRun];
|
|
}
|
|
|
|
// Handle some data optimizations
|
|
|
|
// If it's only a single byte run and it's the same as the previous
|
|
// frame? Just skip
|
|
if ((uRun==1) && (pCurrentFrame[0]==pPreviousFrame[0])) {
|
|
pOutput->Append(static_cast<Word8>(1));
|
|
--uInputLength;
|
|
++pCurrentFrame;
|
|
++pPreviousFrame;
|
|
|
|
// If it's only a single byte run and it's the same as the previous
|
|
// frame? Just skip
|
|
} else if ((uRun==2) &&
|
|
(pCurrentFrame[0]==pPreviousFrame[0]) &&
|
|
(pCurrentFrame[1]==pPreviousFrame[1])) {
|
|
pOutput->Append(static_cast<Word8>(2));
|
|
uInputLength-=2;
|
|
pCurrentFrame+=2;
|
|
pPreviousFrame+=2;
|
|
|
|
} else {
|
|
// Perform a raw data transfer
|
|
// Run 1-128
|
|
pOutput->Append(static_cast<Word8>(0x80|uRun));
|
|
pOutput->Append(pCurrentFrame,uRun);
|
|
uInputLength-=uRun;
|
|
pCurrentFrame += uRun;
|
|
pPreviousFrame+=uRun;
|
|
}
|
|
}
|
|
}
|
|
} while (uInputLength>=2);
|
|
|
|
// Simple check if there's only one byte left
|
|
|
|
if (uInputLength==1) {
|
|
if (pCurrentFrame[0]==pPreviousFrame[0]) {
|
|
pOutput->Append(static_cast<Word8>(1));
|
|
} else {
|
|
pOutput->Append(static_cast<Word8>(0x81));
|
|
pOutput->Append(pCurrentFrame[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************
|
|
|
|
Process a video file into space ace format
|
|
|
|
***************************************/
|
|
|
|
static Word ExtractVideo(OutputMemoryStream *pOutput,const Word8 *pInput,WordPtr uInputLength)
|
|
{
|
|
InputMemoryStream InputMem(pInput,uInputLength,TRUE);
|
|
Image MyImage;
|
|
FileGIF Giffy;
|
|
Word8 IIgsPalette[32];
|
|
Word8 NewIIgsPalette[32];
|
|
Word uResult = 10;
|
|
if (!Giffy.Load(&MyImage,&InputMem)) {
|
|
|
|
if ((MyImage.GetWidth()!=320) || (MyImage.GetHeight()!=200)) {
|
|
printf("Input file is not 320 x 200");
|
|
} else {
|
|
// Initialize the IIgs palette to invalid values
|
|
MemoryFill(IIgsPalette,255,sizeof(IIgsPalette));
|
|
Word8 *pCurrentFrame = static_cast<Word8 *>(Alloc(320*200/2));
|
|
Word8 *pPreviousFrame = static_cast<Word8 *>(Alloc(320*200/2));
|
|
int i = 1;
|
|
do {
|
|
|
|
// Process a frame
|
|
|
|
// Save space for the chunk size
|
|
WordPtr uOutputMark = pOutput->GetSize();
|
|
pOutput->Append(static_cast<Word16>(0));
|
|
|
|
// Convert the palette to IIgs format
|
|
ConvertPalette(NewIIgsPalette,Giffy.GetPalette());
|
|
|
|
// Set the default chunk type
|
|
|
|
Word8 uTypeFlag = 0x01;
|
|
|
|
// Is there a palette update?
|
|
if (ComparePalette(NewIIgsPalette,IIgsPalette)) {
|
|
MemoryCopy(IIgsPalette,NewIIgsPalette,sizeof(IIgsPalette));
|
|
uTypeFlag |= 0x80U;
|
|
}
|
|
|
|
// Initial frame?
|
|
if (i==1) {
|
|
uTypeFlag |= 0x60;
|
|
}
|
|
|
|
// Send the data type byte
|
|
pOutput->Append(static_cast<Word8>(uTypeFlag));
|
|
|
|
if (uTypeFlag&0x80U) {
|
|
pOutput->Append(IIgsPalette,32);
|
|
}
|
|
|
|
ConvertPixelsToIIgs(pCurrentFrame,&MyImage);
|
|
|
|
if (uTypeFlag&0x40) {
|
|
CompressKeyFrame(pOutput,pCurrentFrame);
|
|
} else {
|
|
CompressAnimFrame(pOutput,pPreviousFrame,pCurrentFrame);
|
|
}
|
|
MemoryCopy(pPreviousFrame,pCurrentFrame,320*200/2);
|
|
|
|
// Update the chunk size
|
|
Word16 uChuckShort;
|
|
LittleEndian::Store(&uChuckShort,static_cast<Word16>(pOutput->GetSize()-uOutputMark));
|
|
pOutput->Overwrite(&uChuckShort,2,uOutputMark);
|
|
++i;
|
|
} while (!Giffy.LoadNextFrame(&MyImage,&InputMem));
|
|
Free(pCurrentFrame);
|
|
Free(pPreviousFrame);
|
|
// Append an "End of data" marker
|
|
pOutput->Append(static_cast<Word16>(0xFF00U));
|
|
uResult = 0;
|
|
}
|
|
} else {
|
|
printf("Gif input file error!\n");
|
|
}
|
|
return uResult;
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Convert a Space Ace file to an animated GIF file
|
|
|
|
***************************************/
|
|
|
|
static char Name[] = "filexxx.gif";
|
|
|
|
static Word EncapsulateToGIF(OutputMemoryStream *pOutput,const Word8 *pInput,WordPtr uInputLength)
|
|
{
|
|
// Too small?
|
|
if (uInputLength<2) {
|
|
return 10;
|
|
}
|
|
|
|
FileGIF GIF;
|
|
Image MyImage;
|
|
|
|
// Create an initial image
|
|
MyImage.Init(320,200,Image::PIXELTYPE8BIT);
|
|
MyImage.ClearBitmap();
|
|
MemoryClear(GIF.GetPalette(),sizeof(GIF.GetPalette()[0])*256);
|
|
|
|
//
|
|
// Decompress a chunk
|
|
//
|
|
Word uFrame = 0;
|
|
for (;;) {
|
|
Word uChunkSize = LittleEndian::LoadAny(reinterpret_cast<const Word16 *>(pInput));
|
|
if (uChunkSize>=0xFF00) {
|
|
// printf("End of data, frames = %u\n",uFrame);
|
|
break;
|
|
}
|
|
++uFrame;
|
|
if (uChunkSize>uInputLength) {
|
|
printf("Premature end of data\n");
|
|
return 10;
|
|
}
|
|
if (uChunkSize<2) {
|
|
printf("Chunk size too small\n");
|
|
return 10;
|
|
}
|
|
// printf("Chunk is %u bytes\n",uChunkSize);
|
|
const Word8 *pWork = pInput+2;
|
|
uInputLength -= uChunkSize;
|
|
pInput+= uChunkSize;
|
|
uChunkSize-=2;
|
|
|
|
// Get the palette token
|
|
|
|
if (uChunkSize) {
|
|
Word uType = pWork[0];
|
|
++pWork;
|
|
--uChunkSize;
|
|
// printf("Token = 0x%02X\n",uType);
|
|
|
|
if (uType&0x80) {
|
|
|
|
// Clear out the palette
|
|
MemoryClear(GIF.GetPalette(),sizeof(GIF.GetPalette()[0])*256);
|
|
ConvertPalette(GIF.GetPalette(),pWork);
|
|
pWork+=32;
|
|
uChunkSize-=32;
|
|
}
|
|
|
|
// Full image or animation frame?
|
|
if (uType&0x40) {
|
|
Word uTemp;
|
|
Word8 *pDest = MyImage.GetImage();
|
|
|
|
for (;;) {
|
|
uTemp = pWork[0];
|
|
++pWork;
|
|
if (!uTemp) {
|
|
break;
|
|
}
|
|
if (uTemp&0x80) {
|
|
uTemp&=0x7f;
|
|
if (uTemp) {
|
|
// Run length compressed loop
|
|
Word uSecond = pWork[0];
|
|
++pWork;
|
|
Word uFirst = uSecond>>4U;
|
|
uSecond&=0xF;
|
|
do {
|
|
pDest[0] = static_cast<Word8>(uFirst);
|
|
pDest[1] = static_cast<Word8>(uSecond);
|
|
pDest+=2;
|
|
} while (--uTemp);
|
|
}
|
|
} else {
|
|
// Uncompressed loop
|
|
do {
|
|
Word uColor = pWork[0];
|
|
++pWork;
|
|
pDest[0] = static_cast<Word8>(uColor>>4U);
|
|
pDest[1] = static_cast<Word8>(uColor&0xF);
|
|
pDest+=2;
|
|
} while (--uTemp);
|
|
}
|
|
}
|
|
} else {
|
|
|
|
Word uTemp;
|
|
Word8 *pDest = MyImage.GetImage();
|
|
Word8 *pEnd = pDest+(320*200);
|
|
do {
|
|
uTemp = pWork[0];
|
|
++pWork;
|
|
if (!uTemp) {
|
|
uTemp = pWork[0];
|
|
++pWork;
|
|
if (uTemp) {
|
|
// Run length compressed loop
|
|
Word uSecond = pWork[0];
|
|
++pWork;
|
|
Word uFirst = uSecond>>4U;
|
|
uSecond&=0xF;
|
|
do {
|
|
pDest[0] = static_cast<Word8>(uFirst);
|
|
pDest[1] = static_cast<Word8>(uSecond);
|
|
pDest+=2;
|
|
} while (--uTemp);
|
|
}
|
|
} else if (uTemp&0x80) {
|
|
uTemp&=0x7f;
|
|
if (uTemp) {
|
|
// Uncompressed loop
|
|
do {
|
|
Word uColor = pWork[0];
|
|
++pWork;
|
|
pDest[0] = static_cast<Word8>(uColor>>4U);
|
|
pDest[1] = static_cast<Word8>(uColor&0xF);
|
|
pDest+=2;
|
|
} while (--uTemp);
|
|
}
|
|
} else {
|
|
pDest+=(uTemp*2);
|
|
}
|
|
} while (pDest<pEnd);
|
|
}
|
|
}
|
|
if (uFrame==1) {
|
|
GIF.AnimationSaveStart(pOutput,&MyImage);
|
|
}
|
|
GIF.AnimationSaveFrame(pOutput,&MyImage,(100U/8U));
|
|
}
|
|
GIF.AnimationSaveFinish(pOutput);
|
|
return 0;
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Main dispatcher
|
|
|
|
***************************************/
|
|
|
|
int BURGER_ANSIAPI main(int argc,const char **argv)
|
|
{
|
|
ConsoleApp MyApp(argc,argv);
|
|
CommandParameterBooleanTrue DoVideo("Process Video","v");
|
|
CommandParameterBooleanTrue ConvertToGIF("Convert to GIF","g");
|
|
const CommandParameter *MyParms[] = {
|
|
&DoVideo,
|
|
&ConvertToGIF
|
|
};
|
|
argc = MyApp.GetArgc();
|
|
argv = MyApp.GetArgv();
|
|
argc = CommandParameter::Process(argc,argv,MyParms,sizeof(MyParms)/sizeof(MyParms[0]),
|
|
"Usage: packvideo InputFile OutputFile\n\n"
|
|
"Preprocess video data for Space Ace IIgs.\nCopyright by Rebecca Ann Heineman\n",3);
|
|
if (argc<0) {
|
|
Globals::SetErrorCode(10);
|
|
} else {
|
|
MyApp.SetArgc(argc);
|
|
|
|
Filename InputName;
|
|
InputName.SetFromNative(argv[1]);
|
|
|
|
WordPtr uInputLength;
|
|
Word8 *pInput = static_cast<Word8 *>(FileManager::LoadFile(&InputName,&uInputLength));
|
|
if (!pInput) {
|
|
printf("Can't open %s!\n",argv[1]);
|
|
Globals::SetErrorCode(10);
|
|
} else {
|
|
|
|
// Convert gif to data
|
|
if (DoVideo.GetValue()) {
|
|
OutputMemoryStream Output;
|
|
if (ExtractVideo(&Output,pInput,uInputLength)) {
|
|
printf("Can't convert %s!\n",argv[1]);
|
|
Globals::SetErrorCode(10);
|
|
} else {
|
|
Filename OutputName;
|
|
OutputName.SetFromNative(argv[2]);
|
|
if (Output.SaveFile(&OutputName)) {
|
|
printf("Can't save %s!\n",argv[2]);
|
|
Globals::SetErrorCode(10);
|
|
}
|
|
}
|
|
|
|
// Convert raw video to GIF
|
|
} else if (ConvertToGIF.GetValue()) {
|
|
OutputMemoryStream Output;
|
|
if (EncapsulateToGIF(&Output,pInput,uInputLength)) {
|
|
printf("Can't convert %s!\n",argv[1]);
|
|
Globals::SetErrorCode(10);
|
|
} else {
|
|
Filename OutputName;
|
|
OutputName.SetFromNative(argv[2]);
|
|
if (Output.SaveFile(&OutputName)) {
|
|
printf("Can't save %s!\n",argv[2]);
|
|
Globals::SetErrorCode(10);
|
|
}
|
|
}
|
|
} else {
|
|
printf("No conversion selected for %s!\n",argv[1]);
|
|
Globals::SetErrorCode(10);
|
|
}
|
|
Free(pInput);
|
|
}
|
|
}
|
|
return Globals::GetErrorCode();
|
|
}
|