XADMaster/XADSWFParser.m
2018-03-21 14:24:50 +02:00

718 lines
18 KiB
Objective-C

/*
* XADSWFParser.m
*
* Copyright (c) 2017-present, MacPaw Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
#import "XADSWFParser.h"
#import "XADSWFParser.h"
#import "XADPNGWriter.h"
#import "CSMemoryHandle.h"
#import "CSMultiHandle.h"
#import "CSZlibHandle.h"
@implementation XADSWFParser
+(int)requiredHeaderSize { return 6; }
+(BOOL)recognizeFileWithHandle:(CSHandle *)handle firstBytes:(NSData *)data name:(NSString *)name
{
const uint8_t *bytes=[data bytes];
int length=[data length];
if(length<4) return NO;
//NSLog(@"SWF version %d\n",bytes[3]);
if(bytes[3]>=32) return NO; // Assume the SWF format doesn't reach version 32 any time soon.
if(bytes[0]=='F'&&bytes[1]=='W'&&bytes[2]=='S') return YES;
if(bytes[0]=='C'&&bytes[1]=='W'&&bytes[2]=='S') return YES;
return NO;
}
-(id)init
{
if((self=[super init]))
{
parser=nil;
dataobjects=[NSMutableArray new];
}
return self;
}
-(void)dealloc
{
[parser release];
[dataobjects release];
[super dealloc];
}
-(void)parse
{
parser=[[XADSWFTagParser parserWithHandle:[self handle]] retain];
[parser parseHeader];
CSHandle *fh=[parser handle];
int numimages=0;
int numsounds=0;
int numstreams=0;
int laststreamframe=-1;
NSData *jpegtables=nil;
NSMutableData *mainstream=nil;
int tag;
while((tag=[parser nextTag]) && [self shouldKeepParsing])
switch(tag)
{
case SWFJPEGTables:
{
int length=[parser tagLength];
if(length>=2)
{
jpegtables=[fh readDataOfLength:length-2];
}
else
{
// Apparently sometimes Flash uses a zero-length table tag,
// and this implicitly means that the header should be a
// single SOI marker.
jpegtables=[NSData dataWithBytes:(uint8_t[2]){0xff,0xd8} length:2];
}
[dataobjects addObject:jpegtables];
}
break;
case SWFDefineBitsJPEGTag:
{
numimages++;
[fh skipBytes:4];
off_t offset=[fh offsetInFile];
off_t length=[parser tagBytesLeft];
if(!jpegtables) [XADException raiseIllegalDataException];
[self addEntryWithName:[NSString stringWithFormat:
@"Image %d at frame %d.jpg",numimages,[parser frame]]
data:jpegtables offset:offset length:length];
}
break;
case SWFDefineBitsJPEG2Tag:
case SWFDefineBitsJPEG3Tag:
case SWFDefineBitsJPEG4Tag:
{
numimages++;
[fh skipBytes:2];
int alphaoffs=0;
if(tag==SWFDefineBitsJPEG3Tag||tag==SWFDefineBitsJPEG4Tag)
{
alphaoffs=[fh readUInt32LE];
}
if(tag==SWFDefineBitsJPEG4Tag)
{
[fh skipBytes:2]; // Skip deblocking filter params.
}
off_t startoffs=[fh offsetInFile];
int first=[fh readUInt16BE];
if(first==0x8950)
{
// PNG image.
[self reportInterestingFileWithReason:@"Image with PNG data"];
[self addEntryWithName:[NSString stringWithFormat:
@"Image %d at frame %d.png",numimages,[parser frame]]
data:[NSData dataWithBytes:(uint8_t[2]){ 0x89,0x50 } length:2]
offset:[fh offsetInFile] length:[parser tagBytesLeft]];
}
else if(first==0x4749)
{
// GIF image.
[self reportInterestingFileWithReason:@"Image with GIF data"];
[self addEntryWithName:[NSString stringWithFormat:
@"Image %d at frame %d.gif",numimages,[parser frame]]
data:[NSData dataWithBytes:(uint8_t[2]){ 0x47,0x49 } length:2]
offset:[fh offsetInFile] length:[parser tagBytesLeft]];
}
else if(first==0xffd8||first==0xffd9)
{
// JPEG image.
// Check if there is an invalid EOI/SOI marker pair at
// the start of the image, and skip it.
if(first==0xffd9)
{
[fh skipBytes:4];
first=0xffd8;
}
// Parse JPEG markers to find image dimensions, and
// to remove possible later EOI/SOI pairs.
int width=0,height=0;
CSMemoryHandle *header=[CSMemoryHandle memoryHandleForWriting];
[header writeUInt16BE:first];
for(;;)
{
int marker=[fh readUInt16BE];
if(marker==0xffda)
{
[header writeUInt16BE:marker];
off_t imagestart=[fh offsetInFile];
int imagelength=[parser tagBytesLeft];
// Calculate alpha channel extent and adjust
// main image if needed.
off_t alphastart=0;
int alphalength=0;
if(alphaoffs)
{
alphastart=startoffs+alphaoffs;
alphalength=(int)(imagestart+imagelength-alphastart);
imagelength=(int)(alphastart-imagestart);
}
// Emit main image.
[self addEntryWithName:[NSString stringWithFormat:
@"Image %d at frame %d.jpg",numimages,[parser frame]]
data:[header data] offset:imagestart length:imagelength];
// Emit alpha channel image if one exists.
if(alphaoffs && width && height)
{
[self addEntryWithName:[NSString stringWithFormat:
@"Image %d alpha channel at frame %d.png",numimages,[parser frame]]
losslessFormat:-1 width:width height:height alpha:NO
offset:alphastart length:alphalength];
}
break;
}
else if(marker==0xffd9)
{
// EOI marker. This is garbage introduced by Flash.
// An SOI marker follows. Skip this.
[fh skipBytes:2];
}
else
{
// Other markers. Copy, and parse if known.
int len=[fh readUInt16BE];
[header writeUInt16BE:marker];
[header writeUInt16BE:len];
if(len>2)
{
uint8_t contents[len-2];
[fh readBytes:len-2 toBuffer:contents];
[header writeBytes:len-2 fromBuffer:contents];
if(marker>=0xffc0&&marker<=0xffc3)
{
// SOFx marker, extract width and height.
width=CSUInt16BE(&contents[3]);
height=CSUInt16BE(&contents[1]);
}
}
}
}
}
else NSLog(@"Error loading SWF file: invalid JPEG data in tag %d",[parser tag]);
}
break;
case SWFDefineBitsLosslessTag:
case SWFDefineBitsLossless2Tag:
{
numimages++;
[fh skipBytes:2];
int format=[fh readUInt8];
int width=[fh readUInt16LE];
int height=[fh readUInt16LE];
if(format==3||format==4||format==5)
{
[self addEntryWithName:[NSString stringWithFormat:
@"Image %d at frame %d.png",numimages,[parser frame]]
losslessFormat:format width:width height:height alpha:tag==SWFDefineBitsLossless2Tag
offset:[fh offsetInFile] length:[parser tagBytesLeft]];
}
else [self reportInterestingFileWithReason:@"Unsupported lossless format %d",format];
}
break;
case SWFDefineSoundTag:
{
numsounds++;
[fh skipBytes:2];
int flags=[fh readUInt8];
int format=flags>>4;
if(format==2)
{
// MP3 audio.
[fh skipBytes:4]; //uint32_t numsamples=[fh readUInt32LE];
[self addEntryWithName:[NSString stringWithFormat:
@"Sound %d at frame %d.mp3",numsounds,[parser frame]]
offset:[fh offsetInFile] length:[parser tagBytesLeft]];
}
else if(format==0||format==3)
{
// Uncompresed audio. Assumes format 0 is little-endian.
// Add a WAV header.
[fh skipBytes:4]; //uint32_t numsamples=[fh readUInt32LE];
int length=[parser tagBytesLeft];
[self addEntryWithName:[NSString stringWithFormat:
@"Sound %d at frame %d.wav",numsounds,[parser frame]]
data:[self createWAVHeaderForFlags:flags length:length]
offset:[fh offsetInFile] length:length];
}
else if(format==1) // ADPCM
{
// Not implemented yet.
}
else if(format==6) // Speex?
{
// Not implemented yet.
}
else [self reportInterestingFileWithReason:@"Unsupported sound format %d",format];
}
break;
case SWFSoundStreamHeadTag:
case SWFSoundStreamHead2Tag:
{
int flags=[fh readUInt8];
int format=(flags>>4)&0x0f;
if(format==2||format==0) // MP3 format - why is 0 MP3? Who knows!
{
mainstream=[NSMutableData data];
[dataobjects addObject:mainstream];
}
else if(format==4) // Nellymoser
{
// Not implemented yet.
}
else [self reportInterestingFileWithReason:@"Unsupported stream format %d",format];
}
break;
case SWFSoundStreamBlockTag:
{
if(laststreamframe!=[parser frame])
{
laststreamframe=[parser frame];
[fh skipBytes:4];
}
[mainstream appendData:[fh readDataOfLength:[parser tagBytesLeft]]];
}
break;
case SWFDefineSpriteTag:
{
[parser parseDefineSpriteTag];
NSMutableData *substream=nil;
int lastsubframe=-1;
int tag;
while((tag=[parser nextSubTag]) && [self shouldKeepParsing])
switch(tag)
{
case SWFSoundStreamHeadTag:
case SWFSoundStreamHead2Tag:
{
int flags=[fh readUInt8];
int format=(flags>>4)&0x0f;
if(format==2||format==0) // MP3 format - why is 0 MP3? Who knows!
{
substream=[NSMutableData data];
[dataobjects addObject:substream];
}
else if(format==4) // Nellymoser
{
// Not implemented yet.
}
else [self reportInterestingFileWithReason:@"Unsupported stream format %d",format];
}
break;
case SWFSoundStreamBlockTag:
{
if(lastsubframe!=[parser subFrame])
{
lastsubframe=[parser subFrame];
[fh skipBytes:4];
}
[substream appendData:[fh readDataOfLength:[parser subTagBytesLeft]]];
}
break;
}
if(substream && [substream length])
{
numstreams++;
[self addEntryWithName:[NSString stringWithFormat:
@"Stream %d at frame %d.mp3",numstreams,[parser frame]]
data:substream];
}
}
break;
}
if(mainstream && [mainstream length])
{
[self addEntryWithName:[NSString stringWithFormat:@"Stream.mp3"]
data:mainstream];
}
}
-(NSData *)createWAVHeaderForFlags:(int)flags length:(int)length
{
int samplerate,bitsperchannel,numchannels;
switch((flags>>2)&0x03)
{
default:
case 0: samplerate=5512; break; // 5.5125 kHz - what.
case 1: samplerate=11025; break;
case 2: samplerate=22050; break;
case 3: samplerate=44100; break;
}
if(flags&0x02) bitsperchannel=16;
else bitsperchannel=8;
if(flags&0x01) numchannels=2;
else numchannels=1;
uint8_t header[44]=
{
'R','I','F','F',0x00,0x00,0x00,0x00,
'W','A','V','E','f','m','t',' ',0x10,0x00,0x00,0x00,
0x01,0x00, 0x00,0x00, 0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00, 0x00,0x00, 0x00,0x00,
'd','a','t','a',0x00,0x00,0x00,0x00,
};
CSSetUInt32LE(&header[4],36+length);
CSSetUInt16LE(&header[22],numchannels);
CSSetUInt32LE(&header[24],samplerate);
CSSetUInt32LE(&header[28],samplerate*numchannels*bitsperchannel/8);
CSSetUInt16LE(&header[32],numchannels*bitsperchannel/8);
CSSetUInt16LE(&header[34],bitsperchannel);
CSSetUInt32LE(&header[40],length);
return [NSData dataWithBytes:header length:sizeof(header)];
}
-(void)addEntryWithName:(NSString *)name data:(NSData *)data
{
NSUInteger index=[dataobjects indexOfObjectIdenticalTo:data];
if(index==NSNotFound)
{
index=[dataobjects count];
[dataobjects addObject:data];
}
NSMutableDictionary *dict=[NSMutableDictionary dictionaryWithObjectsAndKeys:
[self XADPathWithString:name],XADFileNameKey,
[NSNumber numberWithLongLong:[data length]],XADFileSizeKey,
[self XADStringWithString:[parser isCompressed]?@"Zlib":@"None"],XADCompressionNameKey,
[NSNumber numberWithInt:index],@"SWFDataIndex",
nil];
[self addEntryWithDictionary:dict];
}
-(void)addEntryWithName:(NSString *)name
offset:(off_t)offset length:(off_t)length
{
NSMutableDictionary *dict=[NSMutableDictionary dictionaryWithObjectsAndKeys:
[self XADPathWithString:name],XADFileNameKey,
[NSNumber numberWithLongLong:length],XADFileSizeKey,
[NSNumber numberWithLongLong:length],@"SWFDataLength",
[NSNumber numberWithLongLong:offset],@"SWFDataOffset",
[self XADStringWithString:[parser isCompressed]?@"Zlib":@"None"],XADCompressionNameKey,
nil];
[self addEntryWithDictionary:dict];
}
-(void)addEntryWithName:(NSString *)name data:(NSData *)data
offset:(off_t)offset length:(off_t)length
{
NSUInteger index=[dataobjects indexOfObjectIdenticalTo:data];
if(index==NSNotFound)
{
index=[dataobjects count];
[dataobjects addObject:data];
}
NSMutableDictionary *dict=[NSMutableDictionary dictionaryWithObjectsAndKeys:
[self XADPathWithString:name],XADFileNameKey,
[NSNumber numberWithLongLong:length+[data length]],XADFileSizeKey,
[NSNumber numberWithLongLong:length],@"SWFDataLength",
[NSNumber numberWithLongLong:offset],@"SWFDataOffset",
[self XADStringWithString:[parser isCompressed]?@"Zlib":@"None"],XADCompressionNameKey,
[NSNumber numberWithInt:index],@"SWFDataIndex",
nil];
[self addEntryWithDictionary:dict];
}
-(void)addEntryWithName:(NSString *)name losslessFormat:(int)format
width:(int)width height:(int)height alpha:(BOOL)alpha
offset:(off_t)offset length:(off_t)length
{
NSMutableDictionary *dict=[NSMutableDictionary dictionaryWithObjectsAndKeys:
[self XADPathWithString:name],XADFileNameKey,
//[NSNumber numberWithLongLong:length],XADFileSizeKey,
[NSNumber numberWithLongLong:length],@"SWFDataLength",
[NSNumber numberWithLongLong:offset],@"SWFDataOffset",
[NSNumber numberWithInt:format],@"SWFLosslessFormat",
[NSNumber numberWithInt:width],@"SWFLosslessWidth",
[NSNumber numberWithInt:height],@"SWFLosslessHeight",
[NSNumber numberWithBool:alpha],@"SWFLosslessAlpha",
[self XADStringWithString:@"Zlib"],XADCompressionNameKey,
nil];
[self addEntryWithDictionary:dict];
}
-(CSHandle *)handleForEntryWithDictionary:(NSDictionary *)dict wantChecksum:(BOOL)checksum
{
CSHandle *handle=nil;
NSNumber *offsetnum=[dict objectForKey:@"SWFDataOffset"];
NSNumber *lengthnum=[dict objectForKey:@"SWFDataLength"];
if(offsetnum&&lengthnum)
{
handle=[[parser handle] nonCopiedSubHandleFrom:[offsetnum longLongValue]
length:[lengthnum longLongValue]];
NSNumber *formatnum=[dict objectForKey:@"SWFLosslessFormat"];
if(formatnum)
{
return [CSMemoryHandle memoryHandleForReadingData:
[self convertLosslessFormat:[formatnum intValue]
width:[[dict objectForKey:@"SWFLosslessWidth"] intValue]
height:[[dict objectForKey:@"SWFLosslessHeight"] intValue]
alpha:[[dict objectForKey:@"SWFLosslessAlpha"] boolValue]
handle:handle]];
}
}
CSHandle *datahandle=nil;
NSNumber *indexnum=[dict objectForKey:@"SWFDataIndex"];
if(indexnum)
{
datahandle=[CSMemoryHandle memoryHandleForReadingData:
[dataobjects objectAtIndex:[indexnum intValue]]];
}
if(handle&&datahandle)
{
return [CSMultiHandle handleWithHandles:datahandle,handle,nil];
}
else if(datahandle)
{
return datahandle;
}
else
{
return handle;
}
}
-(NSData *)convertLosslessFormat:(int)format width:(int)width height:(int)height
alpha:(BOOL)alpha handle:(CSHandle *)handle
{
XADPNGWriter *png=[XADPNGWriter PNGWriter];
switch(format)
{
case 3:
{
int numcolours=[handle readUInt8]+1;
[png addIHDRWithWidth:width height:height bitDepth:8 colourType:3];
CSZlibHandle *zh=[CSZlibHandle zlibHandleWithHandle:handle];
if(alpha)
{
uint8_t palette[4*numcolours];
[zh readBytes:4*numcolours toBuffer:palette];
uint8_t plte[3*numcolours];
uint8_t trns[numcolours];
for(int i=0;i<numcolours;i++)
{
plte[3*i+0]=palette[4*i+0];
plte[3*i+1]=palette[4*i+1];
plte[3*i+2]=palette[4*i+2];
trns[i]=palette[4*i+3];
}
[png addChunk:'PLTE' bytes:plte length:3*numcolours];
[png addChunk:'tRNS' bytes:trns length:numcolours];
}
else
{
uint8_t plte[3*numcolours];
[zh readBytes:3*numcolours toBuffer:plte];
[png addChunk:'PLTE' bytes:plte length:3*numcolours];
}
[png startIDAT];
int bytesperrow=(width+3)&~3;
for(int y=0;y<height;y++)
{
uint8_t row[bytesperrow];
[zh readBytes:bytesperrow toBuffer:row];
[png addIDATRow:row];
}
[png endIDAT];
}
break;
case 4:
{
CSZlibHandle *zh=[CSZlibHandle zlibHandleWithHandle:handle];
[png addIHDRWithWidth:width height:height bitDepth:8 colourType:2];
[png startIDAT];
int bytesperrow=(2*width+3)&~3;
for(int y=0;y<height;y++)
{
uint8_t row[bytesperrow];
uint8_t rgbrow[3*width];
[zh readBytes:bytesperrow toBuffer:row];
for(int x=0;x<width;x++)
{
uint16_t rgb=CSUInt16BE(&row[2*x]);
uint8_t r=(((rgb>>10)&0x1f)*0x21)>>2;
uint8_t g=(((rgb>>5)&0x1f)*0x21)>>2;
uint8_t b=(((rgb>>0)&0x1f)*0x21)>>2;
rgbrow[3*x+0]=r;
rgbrow[3*x+1]=g;
rgbrow[3*x+2]=b;
}
[png addIDATRow:rgbrow];
}
[png endIDAT];
}
break;
case 5:
{
CSZlibHandle *zh=[CSZlibHandle zlibHandleWithHandle:handle];
if(alpha) [png addIHDRWithWidth:width height:height bitDepth:8 colourType:6];
else [png addIHDRWithWidth:width height:height bitDepth:8 colourType:2];
[png startIDAT];
int bytesperrow=width*4;
for(int y=0;y<height;y++)
{
uint8_t row[bytesperrow];
[zh readBytes:bytesperrow toBuffer:row];
if(alpha)
{
for(int x=0;x<width;x++)
{
uint8_t a=row[4*x+0],r=row[4*x+1],g=row[4*x+2],b=row[4*x+3];
row[4*x+0]=r;
row[4*x+1]=g;
row[4*x+2]=b;
row[4*x+3]=a;
}
}
else
{
for(int x=0;x<width;x++)
{
uint8_t r=row[4*x+1],g=row[4*x+2],b=row[4*x+3];
row[3*x+0]=r;
row[3*x+1]=g;
row[3*x+2]=b;
}
}
[png addIDATRow:row];
}
[png endIDAT];
}
break;
case -1: // Special alpha channel format.
{
[png addIHDRWithWidth:width height:height bitDepth:8 colourType:0];
CSZlibHandle *zh=[CSZlibHandle zlibHandleWithHandle:handle];
[png startIDAT];
int bytesperrow=width;
for(int y=0;y<height;y++)
{
uint8_t row[bytesperrow];
// Kludge: Some alpha channels end (one byte) early.
// Just zero the buffer and accept short reads to work around it.
memset(row,0,sizeof(row));
[zh readAtMost:bytesperrow toBuffer:row];
[png addIDATRow:row];
}
[png endIDAT];
}
break;
}
[png addIEND];
return [png data];
}
-(NSString *)formatName { return @"SWF"; }
@end