diff --git a/README.md b/README.md index 7714e39..f863277 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,12 @@ jeff(options, function (error, stats, extractedData) { * Will sequentially parse and process the SWF files. * Will export JSON meta-data and images corresponding to the SWF files. +For a list of command line options: + +```shell +jeff -h +``` + ### Roadmap for unsupported features * Texts (Static/Dynamic) @@ -98,4 +104,4 @@ jeff(options, function (error, stats, extractedData) { * Option to extract shapes under vectorial format * Option to extract meta-data under keyframe based format (as opposed to per frame transformation matrix) -For contributors, see [SWF File Format Specifications](http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/swf/pdf/swf-file-format-spec.pdf) +For contributors, see [SWF File Format Specifications](https://wwwimages2.adobe.com/content/dam/acom/en/devnet/pdf/swf-file-format-spec.pdf) \ No newline at end of file diff --git a/bin/jeff b/bin/jeff index 231f6f0..f2d0857 100755 --- a/bin/jeff +++ b/bin/jeff @@ -36,6 +36,8 @@ program .option('-I, --ignoreImages ', 'Not to export images', 'false') .option('-F, --filtering ', 'Filtering that should be used when rendering the animation', 'linear') .option('-e, --outlineEmphasis ', 'Emphasis of outlines when rendering Flash vectorial drawings', '1') +.option('-V, --version ', 'If specified, force decompiler to attempt to decompile as if the swf was specified version', '-1') +.option('-L, --verbosity ', '(L)og Level. 1-10, 1 showing least, 10 showing the most. Default is 3', '3') .parse(process.argv); @@ -67,7 +69,9 @@ var exportParams = { ignoreData: JSON.parse(program.ignoreData), ignoreImages: JSON.parse(program.ignoreImages), filtering: program.filtering, - outlineEmphasis: JSON.parse(program.outlineEmphasis) + outlineEmphasis: JSON.parse(program.outlineEmphasis), + forceVersion: JSON.parse(program.version), + verbosity: JSON.parse(program.verbosity) }; // Creating a new Jeff diff --git a/package.json b/package.json index 652ac4f..a4ffb49 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jeff", "description": "Extracts Json meta-data and images from a SWF Flash file.", - "version": "0.2.10", + "version": "0.3.0", "private": false, "author": { "name": "Brice Chevalier", @@ -14,13 +14,13 @@ "dependencies": { "async": "0.2.10", "buffer-crc32": "^0.2.5", - "canvas": "1.1.6", + "canvas": "1.6.11", "commander": "2.2.0", "fs-extra": "0.8.1", "glob": "4.3.1", "image-optim": "0.3.0", "js-beautify": "1.4.2", - "node-pngquant-native": "0.0.12" + "node-pngquant-native": "2.0.1" }, "repository": { "type": "git", @@ -32,4 +32,4 @@ "url": "https://github.com/Wizcorp/jeff/blob/master/LICENSE" } ] -} \ No newline at end of file +} diff --git a/src/Gordon/base.js b/src/Gordon/base.js index dc608ae..1b483e7 100644 --- a/src/Gordon/base.js +++ b/src/Gordon/base.js @@ -17,7 +17,8 @@ var Base = { }, validSignatures: { SWF: 'FWS', - COMPRESSED_SWF: 'CWS' + COMPRESSED_ZLIB_SWF: 'CWS', + COMPRESSED_LZMA_SWF: 'ZWS' }, readyStates: { LOADING: 0, @@ -303,6 +304,22 @@ var Base = { 13: 'overlay', 14: 'hardlight' }, + soundFormats: { + UNCOMPRESSED_NATIVE_ENDIAN: 0, //swf 1 + ADPCM: 1, //swf 1 + MP3: 2, //swf 4 + UNCOMPRESSED_LITTLE_ENDIAN: 3, //swf 4 + NELLYMOSER16KHZ: 4, //swf 10 + NELLYMOSER8KHZ: 5, //swf 10 + NELLYMOSER: 6, //swf 6 + SPEEX: 11 //swf 10 + }, + soundRates: { + 0: 5500, + 1: 11000, + 2: 22000, + 3: 44000 + } }; (function () { diff --git a/src/Gordon/parser.js b/src/Gordon/parser.js index 4aa7805..d72a5a1 100644 --- a/src/Gordon/parser.js +++ b/src/Gordon/parser.js @@ -2,6 +2,7 @@ 'use strict'; +var util = require('util'); var Base = require('./base.js'); var Stream = require('./stream.js'); @@ -42,25 +43,41 @@ SwfParser.prototype = { // swfData is a buffer containing the SWF data // onData(object) will be called several times with the result of the parsing of 1 object // cb(error) will be called at the end or upon error - parse: function (swfName, swfData, onData, cb) { + parse: function (swfName, swfData, options, onData, cb) { + this.dataStorage = []; this.swfName = swfName; + this._options = options; this.onData = onData; - var stream = new Stream(swfData); + + var stream = new Stream(swfData, options); this._readSwlTable(stream); // Symbol table not used var sign = stream.readString(3); - this.version = stream.readUI8(); + var version = stream.readUI8(); this.fileLen = stream.readUI32(); + if(this._options.forceVersion == null || this._options.forceVersion == -1){ + this._options.version = version; + } + else{ + this._options.version = forceVersion; + } + + if(this._options.verbosity > 3){ + console.log('Attempting to decompile for SWF Version:', this._options.version); + } + var signatures = Base.validSignatures; - if (sign === signatures.COMPRESSED_SWF) { + if (sign === signatures.COMPRESSED_ZLIB_SWF) { var self = this; stream.decompressAsync(function () { self._parseSwf(stream, cb); }); } else if (sign === signatures.SWF) { this._parseSwf(stream, cb); + } else if (sign === signatures.COMPRESSED_LZMA_SWF) { + cb('No Support for LZMA Compressed SWF file: ' + swfName); } else { cb('Invalid SWF file: ' + swfName); } @@ -104,7 +121,7 @@ SwfParser.prototype = { //create a "header" object with file level info this.onData({ type: 'header', - version: this.version, + version: this._options.version, fileLength: this.fileLen, frameSize: stream.readRect(), frameRate: stream.readUI16() / 256 @@ -252,9 +269,8 @@ SwfParser.prototype = { _handleCsmTextSettings: function (stream, offset, len) { this._skipEndOfTag('CsmTextSettings', true, stream, offset, len); }, - _handleDefineBinaryData: function (stream, offset, len) { - this._skipEndOfTag('DefineBinaryData', true, stream, offset, len); - }, + + _handleDefineEditText: function (stream, offset, len) { var id = stream.readUI16(); var edit = { type: 'fakeEditText', id: id }; @@ -351,7 +367,26 @@ SwfParser.prototype = { if (code !== 0 || len !== 1) throw new Error('Unexpected value in undocumented1 tag: ' + code + ',' + len); }, //--------------------------------------------------------- + _handleDefineBinaryData: function (stream, offset, len) { + var headerSize = 6; + var info = { + binaryId: stream.readUI16(), //16bit id + reserved: stream.readUB(32) //reserved must be 0 + } + var data = stream.readBytes(len - headerSize); + + var fileToSave = { + id: info.binaryId, + type: 'binary', + fileName: info.binaryId+'.unknown', + info: info, + bytes: data + }; + + this.dataStorage.push(fileToSave); + this.onData(fileToSave); + }, _handleSymbolClass: function (stream, offset, len, frm) { var count = stream.readUI16(); while (count--) { @@ -902,11 +937,19 @@ SwfParser.prototype = { while (true) { var flags = stream.readUI8(); if (!flags) break; - var objId = stream.readUI16(), - depth = stream.readUI16(), + var objId = stream.readUI16(); + var pointingTo; + if(d[objId] != null) { + pointingTo = d[objId].id; + } + else { + console.warn('Button '+id+' is pointing to a non existing resource'); + } + + var depth = stream.readUI16(), state = 0x01, character = { - id: d[objId].id, + id: pointingTo, depth: depth, matrix: stream.readMatrix() }; @@ -943,6 +986,85 @@ SwfParser.prototype = { frm.bgcolor = stream.readRGB(); }, + _handleSoundStreamHead2: function (stream, offset, len) { + var f = Base.soundFormats; + var headerSize = 4;//7 bytes as defined next + if(len == 4) + { + //just the header... absolutly no data? how/why is this possible? + this._skipEndOfTag('SoundStreamHead2', true, stream, offset, len); + return; + } + + var info = { + reserved: stream.readUB(4), //not used, always 0 + playbackSoundRate: stream.readUB(2), //enum: soundRates + playbackSoundSize: stream.readUB(1), //0: '8Bit', 1: '16Bit' + playbackSoundType: stream.readUB(1), //0: 'Mono', 1: 'Stereo' + streamSoundFormat: stream.readUB(4), //enum: soundFormats + streamSoundRate: stream.readUB(2), //enum: soundRates + streamSoundSize: stream.readUB(1), //0: '8Bit', 1: '16Bit' + streamSoundType: stream.readUB(1), //0: 'Mono', 1: 'Stereo' + streamSoundSampleCount: stream.readUI16() //--- in a single frame, I'm assuming there can't be more than 16 bits worth of sample + }; + + if(info.streamSoundRate == f.MP3) + { + info.latencySeek = stream.readSI16(); //only used for mp3 + } + + var soundData = stream.readBytes(len - headerSize);//is only a tiny bit of the information, todo: needs to be stored and joined together with the other audios of this block + + console.warn('SoundStreamHead2 has '+ (len - headerSize) +' bytes of audio data which have not been handled.'); + }, + + _handleDefineSound: function (stream, offset, len) { + var f = Base.soundFormats; + var headerSize = 7;//7 bytes as defined next + + var info = { + soundId: stream.readUI16(), + soundFormat: stream.readUB(4), + soundRate: stream.readUB(2), //ignored for nellymoser and speex -- enums found in "soundRates" + soundSize: stream.readUB(1), //only matters for uncompressed, compressed always becomes 16bit. 0: '8Bit', 1: '16Bit' + soundType: stream.readUB(1), //Mono or stereo, ignored for Nellymoser and Speex, 0: 'Mono', 1: 'Stereo' + soundSampleCount: stream.readUI32() + }; + + var fileToSave; + + switch(info.soundFormat){ + case f.UNCOMPRESSED_NATIVE_ENDIAN: + case f.ADPCM: + case f.UNCOMPRESSED_LITTLE_ENDIAN: + case f.NELLYMOSER16KHZ: + case f.NELLYMOSER8KHZ: + case f.NELLYMOSER: + case f.SPEEX: + console.warn('Sound format: '+info.soundFormat+' not supported'); + break; + case f.MP3: + var soundData = stream.readBytes(len - headerSize); + fileToSave = { + id: info.soundId, + type: 'sound', + fileName: info.soundId+'.mp3', + info: info, + bytes: soundData + }; + break; + default: + throw new Error('Sound format not known: '+info.soundFormat); + } + + if(fileToSave != null) + { + this.dataStorage.push(fileToSave); + } + + this.onData(fileToSave); + }, + _handleDefineFont: function (stream) { var id = stream.readUI16(), numGlyphs = stream.readUI16() / 2, @@ -977,7 +1099,7 @@ SwfParser.prototype = { isUTF8: stream.readBool(), isItalic: stream.readBool(), isBold: stream.readBool(), - languageCode: (this.version > 5) ? stream.readLanguageCode() : 0, + languageCode: (this._options.version > 5) ? stream.readLanguageCode() : 0, name: stream.readUI8() && stream.readString() }; @@ -1022,7 +1144,7 @@ SwfParser.prototype = { _handleDefineFont3: function (stream, offset, len) { var startingOffset = stream.offset; -// console.error('version', this.version) +// console.error('version', this._options.version) // console.error('offset -1', stream.offset) var id = stream.readUI16(); // console.error('offset 0', stream.offset) @@ -1037,7 +1159,7 @@ SwfParser.prototype = { isUTF8: stream.readBool(), isItalic: stream.readBool(), isBold: stream.readBool(), - languageCode: (this.version > 5) ? stream.readLanguageCode() : 0, + languageCode: (this._options.version > 5) ? stream.readLanguageCode() : 0, name: stream.readUI8() && stream.readString() }; diff --git a/src/Gordon/stream.js b/src/Gordon/stream.js index 184ecb2..482efe8 100644 --- a/src/Gordon/stream.js +++ b/src/Gordon/stream.js @@ -3,11 +3,13 @@ var zlib = require('zlib'); -function Stream(data){ +function Stream(data, options){ this._buffer = new Buffer(data); this._nBytes = data.length; this.offset = 0; + this._options = options; + this._bitBuffer = null; this._bitOffset = 8; this._lastOffset = 0; @@ -217,14 +219,17 @@ module.exports = Stream; // We simply skip this useless zeros here, but verify it was actually zeros we skipped. // If one day we find something else than an all zero rect (e.g. a 0,0,1,1) we will // have to adapt some more here... - + // Edit: it looks like this hack is making the export fail on some swf // (may be those from the latest flash versions?) // So the bytes need to be tested in order to know whether the hack is necessary - if (this._buffer.readUInt16LE(this.offset) === 0) { - var empty = this.readBytes(16); //console.warn('numBits=1, rect:',rect) - for(var i = 0; i < 16; i++) { - if(empty[i] !== 0) { throw new Error('Unexpected rectangle data'); } + if(this._options.version < 30)//Not exactly sure which version makes this work/break + { + if (this._buffer.readUInt16LE(this.offset) === 0) { + var empty = this.readBytes(16); //console.warn('numBits=1, rect:',rect) + for(var i = 0; i < 16; i++) { + if(empty[i] !== 0) { throw new Error('Unexpected rectangle data'); } + } } } } diff --git a/src/index.js b/src/index.js index ed9c5ac..83b980d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ /* jshint camelcase: false */ //for js_beautify and indent_size 'use strict'; +var util = require('util'); var fs = require('fs-extra'); var glob = require('glob'); @@ -20,6 +21,11 @@ var JSON_WRITE_OPTIONS = { encoding:'binary' }; // Jeff's only API method function extractSwf(exportParams, cb) { + + if(exportParams.verbosity > 8){ + console.log('Options: '+util.inspect(exportParams)); + } + var jeff = new Jeff(); // Setting up Jeff ... @@ -104,6 +110,8 @@ function JeffOptions(params) { this.customWriteFile = params.customWriteFile; this.customReadFile = params.customReadFile; this.fixedSize = params.fixedSize; + this.forceVersion = params.forceVersion; + this.version = params.version; this.fallbackFrameRate = params.fallbackFrameRate || 25; // 1: minimal log level, 10: maximum log level. TODO: needs to be implemented on every console.warn/log/error this.verbosity = params.verbosity || 3; @@ -221,21 +229,33 @@ Jeff.prototype._parseFileGroup = function (fileGroup, nextGroupCb) { Jeff.prototype._parseFile = function (swfName, nextSwfCb) { if (this._options.verbosity >= 5) { - console.log('parsing: ' + swfName); + console.log('=================================');//to quickly find the start in a wall of text + console.log('Parsing: ' + swfName); + console.log('================================='); } var self = this; var swfObjects = []; function onFileRead(error, swfData) { if (error) return nextSwfCb(error); - self._parser.parse(swfName, swfData, - function (swfObject) { + self._parser.parse(swfName, swfData, self._options, + function (swfObject) {//include a .info on the object if you want to display more information var id = swfObject.id; swfObject._swfName = swfName; - // console.log('object', JSON.stringify(swfObject)); - // console.log('properties!!', swfObject.id); - // for (var p in swfObject) { - // console.log(p); - // } + + if(self._options.verbosity > 5) + { + console.log('\n--Object-- '); + console.log('Id', swfObject.id); + console.log('Type', swfObject.type); + //for (var p in swfObject) { + //console.log(p); + //} + //console.log('object', JSON.stringify(swfObject)); + if(swfObject.info != null) { + console.log(util.inspect(swfObject.info)); + } + } + if (id === undefined) { if (swfObject.type === 'scalingGrid') { @@ -337,6 +357,11 @@ Jeff.prototype._extractClassGroup = function (imageList, graphicProperties, next if (!this._options.ignoreData) { this._writeDataToDisk(data); } + for (var i = 0; i < this._parser.dataStorage.length; i += 1) { + + writeFile(this._fileGroupName + this._parser.dataStorage[i].fileName, this._parser.dataStorage[i].bytes); + } + } if (this._options.returnData) {