From 4b42e06f6e7ebb210ae814acc85b3b0ebf174dc1 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Fri, 24 Oct 2014 14:29:14 -0700 Subject: [PATCH] Bug 1035170 - Implement lazy SWF parsing This is a monster-patch as it's the result of squashing together 81 individual patches. It contains a full refactoring of how SWF files are parsed and the results managed. In the new system, SWFs aren't deeply parsed and all symbols and other tags extracted. Instead, they're scanned through, detecting all symbols, frames, and code blocks. The player is notified of these tags, but they're only deeply parsed upon first usage. Even then, the `SWFFile` class that represents the loaded SWF doesn't hold on to the parsed tag. It is up to the playback system to decide whether that's required or not. For now, it does happen in most cases, but we might decide to hold most parsing results temporarily only. In the following, I tried to include the important commit messages for the original patches squashed down into this one. The original patches themselves are available in [a branch](https://github.com/tschneidereit/shumway/tree/parser-unsquashed) in @tschneidereit's fork. Bug fix in SoundStreamHead: StreamSoundSampleCount field is a UI16, not UI32 Disable usage of DOM Workers in fakeSyncWorker.ts for now This creates lots and lots of IO events, overwhelming browsers in some cases. Also, it most certainly imposes substantial overhead. Move initialization of loaded frames from Loader into MovieClip Null out the SWFFile's decompressor after loading is completed, to free its memory. Don't treat BMPs as eagerly-decoded images Increases height of profiler output panel to accomodate parser timeline Remove some somewhatImplemented warnings These got in the way of trace tests after some changes to how logging of warnings works. Add timeouts to the GNU parallel command. (Original patch by @mbebenita) Add simple DOM stubs for Image, URL and Blob to the shell testing harness. (Original patch by @mbebenita) Adds profiling option -o to the shell driver. This let's you dumps out timeline events to stdout when verbose mode is also on. (Original patch by @mbebenita) Optimize DataBuffer access by providing methods for writing multiple values at the same time. Also, lazify the way views are allocated since not all views are always needed. (Original patch by @mbebenita) Switches AVM1Utils.addEventListenerProxy to a native implementation Optimize parsing of AVM1 ClipEvents And moved their application to avm1lib. Before, it was partly in FrameDelta#_initialize, partly in the late Sprite#_initAVM1Bindings. This also fixes the mapping of some of the ClipEvents to equivalent AVM2 events. Some aren't trivially mapped, so more work has to be done there. Those will show a warning at least when they're used. Add new test for mouse events on nested AVM1 objects and reactivate some existing tests The existing tests where disabled before the nat branch merge and then forgotten about. Give PromiseWrapper a `then` method Delay committing Loader updates until the right point in the event loop Support initialization from Uint8Array in source instead of special-casing BinarySymbol in ByteArray initializer Change how resolved symbols get stored on ASClasses Instead of having the Symbol ctor do that, the symbolResolver now does it. Not only is that cleaner, it's also required to make embedded images work: for those, the symbol is associated with a subclass of `Bitmap`, but we create a `BitmapSymbol` for the actual data. This was then stored on `flash.display.BitmapData`. Not ideal. Fix a few assert guards to let the assert execute in non-release mode instead of release mode Don't take 3 screenshots of an unchanging reftest Add parser tracing output for eagerly parsed images and fonts --- Gruntfile.js | 8 +- examples/inspector/style/style.css | 2 +- src/TextContent.ts | 5 +- src/avm1/context.ts | 5 +- src/avm1/interpreter.ts | 32 +- src/avm1lib/AVM1Button.ts | 4 +- src/avm1lib/AVM1Globals.ts | 2 +- src/avm1lib/AVM1Utils.as | 56 +- src/avm1lib/AVM1Utils.ts | 132 ++- src/avm1lib/flash.d.ts | 6 +- src/avm2/bin/avm.js | 2 +- src/avm2/domain.ts | 6 + src/avm2/generated/avm1lib/avm1lib.abc | Bin 27213 -> 26336 bytes src/avm2/natives/byteArray.ts | 22 +- src/base/SWFTags.ts | 123 ++- src/base/ShapeData.ts | 7 +- src/base/dataBuffer.ts | 63 +- src/base/deflate.ts | 35 +- src/base/utilities.ts | 72 +- src/flash/avm1.d.ts | 3 +- src/flash/display/BitmapData.ts | 11 +- src/flash/display/DisplayObject.ts | 9 +- src/flash/display/Loader.ts | 819 ++++++----------- src/flash/display/LoaderInfo.ts | 142 ++- src/flash/display/MovieClip.ts | 56 +- src/flash/display/MovieClipSoundStream.ts | 24 +- src/flash/display/SimpleButton.ts | 8 +- src/flash/display/Sprite.ts | 64 +- src/flash/display/Stage.ts | 2 +- src/flash/events/EventDispatcher.ts | 4 +- src/flash/events/MouseEvent.ts | 2 - src/flash/text/Font.ts | 38 +- src/gfx/easel.ts | 3 +- src/gfx/remotingGfx.ts | 15 +- src/gfx/renderables/renderables.ts | 29 +- src/player/options.ts | 2 - src/player/player.ts | 15 +- src/player/remotingPlayer.ts | 18 +- src/player/test/fakeSyncWorker.ts | 37 +- src/player/timeline.ts | 337 ++++--- src/shell/domstubs.js | 32 +- src/shell/shell.ts | 145 ++- src/swf/FileLoader.ts | 409 ++------- src/swf/ImageFile.ts | 82 ++ src/swf/SWFFile.ts | 866 ++++++++++++++++++ src/swf/options.ts | 12 + src/swf/parser/bitmap.ts | 21 +- src/swf/parser/button.ts | 3 +- src/swf/parser/font.ts | 2 +- src/swf/parser/handlers.ts | 683 ++++++-------- src/swf/parser/image.ts | 36 +- src/swf/parser/label.ts | 80 +- src/swf/parser/parser.ts | 457 --------- src/swf/parser/references.ts | 1 - src/swf/parser/shape.ts | 73 +- src/swf/parser/sound.ts | 80 +- src/swf/parser/templates.ts | 13 - src/swf/parser/text.ts | 22 +- src/swf/references.ts | 5 +- src/swf/resourceLoader.ts | 449 --------- src/tools/profiler/timelineFrame.ts | 14 + .../nested-button/nested-button-click.fla | Bin 0 -> 6873 bytes .../nested-button/nested-button-click.stas | 13 + .../nested-button/nested-button-click.swf | Bin 0 -> 622 bytes .../nested-button-click.swf.trace | 1 + test/swfs/avm1/rollover/avm1-rollover.swd | Bin 582 -> 0 bytes test/test_manifest.json | 29 + test/test_manifest_bad.json | 32 +- test/unit/pass/MovieClip.js | 2 +- 69 files changed, 2782 insertions(+), 3000 deletions(-) create mode 100644 src/swf/ImageFile.ts create mode 100644 src/swf/SWFFile.ts create mode 100644 src/swf/options.ts delete mode 100644 src/swf/parser/parser.ts delete mode 100644 src/swf/resourceLoader.ts create mode 100644 test/swfs/avm1/nested-button/nested-button-click.fla create mode 100644 test/swfs/avm1/nested-button/nested-button-click.stas create mode 100644 test/swfs/avm1/nested-button/nested-button-click.swf create mode 100644 test/swfs/avm1/nested-button/nested-button-click.swf.trace delete mode 100644 test/swfs/avm1/rollover/avm1-rollover.swd diff --git a/Gruntfile.js b/Gruntfile.js index a6fef76763..5642cc11a1 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -126,20 +126,20 @@ module.exports = function(grunt) { }, smoke_parse_database: { maxBuffer: Infinity, - cmd: 'find -L test/swf -name "*.swf" | parallel -X -N100 utils/jsshell/js build/ts/shell.js -p -r -po {} >> data.json.txt' + cmd: 'find -L test/swf -name "*.swf" | parallel --no-notice -X -N50 --timeout 200% utils/jsshell/js build/ts/shell.js -p -r -po {} >> data.json.txt' }, smoke_play: { maxBuffer: Infinity, - cmd: 'find -L test/swf -name "*.swf" | parallel -X -N100 utils/jsshell/js build/ts/shell.js -x -md 1000 -v -tp 60 -v {}' + cmd: 'find -L test/swf -name "*.swf" | parallel --no-notice -X -N50 --timeout 200% utils/jsshell/js build/ts/shell.js -x -md 1000 -v -tp 60 -v {}' }, smoke_parse: { maxBuffer: Infinity, - cmd: 'find -L test/swf -name "*.swf" | parallel -X -N100 utils/jsshell/js build/ts/shell.js -p -r ' + + cmd: 'find -L test/swf -name "*.swf" | parallel --no-notice -X -N50 --timeout 200% utils/jsshell/js build/ts/shell.js -p -r ' + (grunt.option('verbose') ? '-v ' : '') + ' {}' }, smoke_parse_images: { maxBuffer: Infinity, - cmd: 'find -L test/swf -name "*.swf" | parallel -X -N100 utils/jsshell/js build/ts/shell.js -p -r -f "CODE_DEFINE_BITS,CODE_DEFINE_BITS_JPEG2,CODE_DEFINE_BITS_JPEG3,CODE_DEFINE_BITS_JPEG4,CODE_JPEG_TABLES,CODE_DEFINE_BITS_LOSSLESS,CODE_DEFINE_BITS_LOSSLESS2" {}' + cmd: 'find -L test/swf -name "*.swf" | parallel --no-notice -X -N50 --timeout 200% utils/jsshell/js build/ts/shell.js -p -r -f "CODE_DEFINE_BITS,CODE_DEFINE_BITS_JPEG2,CODE_DEFINE_BITS_JPEG3,CODE_DEFINE_BITS_JPEG4,CODE_JPEG_TABLES,CODE_DEFINE_BITS_LOSSLESS,CODE_DEFINE_BITS_LOSSLESS2" {}' }, closure: { // This needs a special build of closure that has SHUMWAY_OPTIMIZATIONS. diff --git a/examples/inspector/style/style.css b/examples/inspector/style/style.css index 1dfce9b702..59071a7c32 100644 --- a/examples/inspector/style/style.css +++ b/examples/inspector/style/style.css @@ -36,7 +36,7 @@ body.simple #easelContainer { right: 0; left: 0; width: auto; - height: 332px; + height: 380px; overflow: hidden; background-color: #000; } diff --git a/src/TextContent.ts b/src/TextContent.ts index 0fbe37c6d1..beedd5bcb9 100644 --- a/src/TextContent.ts +++ b/src/TextContent.ts @@ -411,10 +411,11 @@ module Shumway { textRunData.writeInt(size); var font = flash.text.Font.getByName(textFormat.font) || flash.text.Font.getDefaultFont(); + // TODO: ensure that font serialization really isn't required and clean this up. + textRunData.writeInt(0); if (font.fontType === flash.text.FontType.EMBEDDED) { - textRunData.writeInt(font._id); + textRunData.writeUTF('swffont' + font._id); } else { - textRunData.writeInt(0); textRunData.writeUTF(font._fontFamily); } textRunData.writeInt(font.ascent * size); diff --git a/src/avm1/context.ts b/src/avm1/context.ts index 98e03c1c2d..85f9cbbff5 100644 --- a/src/avm1/context.ts +++ b/src/avm1/context.ts @@ -36,15 +36,14 @@ module Shumway.AVM1 { export class AVM1Context { public static instance: AVM1Context = null; public root: AVM1MovieClip; - public swfVersion: number; + public loaderInfo: Shumway.AVM2.AS.flash.display.LoaderInfo; public globals: AVM1Globals; constructor() { this.root = null; - this.swfVersion = 0; this.globals = null; } - public static create: (swfVersion: number) => AVM1Context; + public static create: (loaderInfo: Shumway.AVM2.AS.flash.display.LoaderInfo) => AVM1Context; public flushPendingScripts() {} public addAsset(className: string, symbolId: number, symbolProps) {} diff --git a/src/avm1/interpreter.ts b/src/avm1/interpreter.ts index 01c347883e..a2ae84dd9d 100644 --- a/src/avm1/interpreter.ts +++ b/src/avm1/interpreter.ts @@ -132,7 +132,6 @@ module Shumway.AVM1 { } class AVM1ContextImpl extends AVM1Context { - swfVersion: number; initialScope: AVM1ScopeListItem; isActive: boolean; executionProhibited: boolean; @@ -149,16 +148,17 @@ module Shumway.AVM1 { private assetsSymbols: Array; private assetsClasses: Array; - constructor(swfVersion: number) { + constructor(loaderInfo: Shumway.AVM2.AS.flash.display.LoaderInfo) { super(); - this.swfVersion = swfVersion; + this.loaderInfo = loaderInfo; this.globals = new Shumway.AVM2.AS.avm1lib.AVM1Globals(); - if (swfVersion >= 8) { + if (loaderInfo.swfVersion >= 8) { this.globals.asSetPublicProperty("flash", Shumway.AVM2.AS.avm1lib.createFlashObject()); } this.initialScope = new AVM1ScopeListItem(this.globals, null); this.assets = {}; + // TODO: remove this list and always retrieve symbols from LoaderInfo. this.assetsSymbols = []; this.assetsClasses = []; this.isActive = false; @@ -172,9 +172,6 @@ module Shumway.AVM1 { } addAsset(className: string, symbolId: number, symbolProps) { this.assets[className] = symbolId; - if (this.assetsSymbols[symbolId]) { - Debug.warning('Symbol ' + symbolId + ' was exported already under different name'); - } this.assetsSymbols[symbolId] = symbolProps; } @@ -191,9 +188,18 @@ module Shumway.AVM1 { if (symbolId === undefined) { return undefined; } + var symbol = this.assetsSymbols[symbolId]; + if (!symbol) { + symbol = this.loaderInfo.getSymbolById(symbolId); + if (!symbol) { + Debug.warning("Symbol " + symbolId + " is not defined."); + return undefined; + } + this.assetsSymbols[symbolId] = symbol; + } return { symbolId: symbolId, - symbolProps: this.assetsSymbols[symbolId], + symbolProps: symbol, theClass: this.assetsClasses[symbolId] }; } @@ -241,8 +247,8 @@ module Shumway.AVM1 { } } - AVM1Context.create = function (swfVersion: number): AVM1Context { - return new AVM1ContextImpl(swfVersion); + AVM1Context.create = function(loaderInfo: Shumway.AVM2.AS.flash.display.LoaderInfo): AVM1Context { + return new AVM1ContextImpl(loaderInfo); }; class AVM1Error { @@ -280,7 +286,7 @@ module Shumway.AVM1 { } function as2GetCurrentSwfVersion() : number { - return ( AVM1Context.instance).swfVersion; + return AVM1Context.instance.loaderInfo.swfVersion; } function as2ToAddPrimitive(value) { @@ -2490,7 +2496,7 @@ module Shumway.AVM1 { var currentContext = AVM1Context.instance; if (!actionsData.ir) { - var stream = new ActionsDataStream(actionsData.bytes, currentContext.swfVersion); + var stream = new ActionsDataStream(actionsData.bytes, currentContext.loaderInfo.swfVersion); var parser = new ActionsDataParser(stream); parser.dataId = actionsData.id; var analyzer = new ActionsDataAnalyzer(); @@ -2509,7 +2515,7 @@ module Shumway.AVM1 { var compiled: Function = ( ir).compiled; var stack = []; - var isSwfVersion5 = currentContext.swfVersion >= 5; + var isSwfVersion5 = currentContext.loaderInfo.swfVersion >= 5; var actionTracer = ActionTracerFactory.get(); var scope = scopeContainer.scope; diff --git a/src/avm1lib/AVM1Button.ts b/src/avm1lib/AVM1Button.ts index eab858013f..df04c7bfc8 100644 --- a/src/avm1lib/AVM1Button.ts +++ b/src/avm1lib/AVM1Button.ts @@ -70,13 +70,13 @@ module Shumway.AVM2.AS.avm1lib { _init(nativeButton: flash.display.SimpleButton): any { this._nativeAS3Object = nativeButton; initDefaultListeners(this); - if (!nativeButton._symbol || !nativeButton._symbol.buttonActions) { + if (!nativeButton._symbol || !nativeButton._symbol.data.buttonActions) { return; } this._nativeAS3Object.addEventListener('addedToStage', this._addListeners.bind(this)); this._nativeAS3Object.addEventListener('removedFromStage', this._removeListeners.bind(this)); var requiredListeners = this._requiredListeners = Object.create(null); - var actions = this._actions = nativeButton._symbol.buttonActions; + var actions = this._actions = nativeButton._symbol.data.buttonActions; for (var i = 0; i < actions.length; i++) { var action = actions[i]; if (!action.actionsBlock) { diff --git a/src/avm1lib/AVM1Globals.ts b/src/avm1lib/AVM1Globals.ts index 65cc58cb4e..a93ab21a5f 100644 --- a/src/avm1lib/AVM1Globals.ts +++ b/src/avm1lib/AVM1Globals.ts @@ -159,7 +159,7 @@ module Shumway.AVM2.AS.avm1lib { */ escape(str: string): string { var result = encodeURIComponent(str); - return result.replace(/\!|'|\(|\)|\*|-|\.|_|~/g, function(char: string): string { + return result.replace(/!|'|\(|\)|\*|-|\.|_|~/g, function(char: string): string { switch (char) { case '*': return '%2A'; diff --git a/src/avm1lib/AVM1Utils.as b/src/avm1lib/AVM1Utils.as index f90693867e..83f2ac2ccf 100644 --- a/src/avm1lib/AVM1Utils.as +++ b/src/avm1lib/AVM1Utils.as @@ -67,10 +67,7 @@ package avm1lib { return path; } - public static function addEventHandlerProxy(obj: Object, propertyName: String, eventName: String, argsConverter: Function = null) - { - _addEventHandlerProxy(obj, propertyName, eventName, argsConverter); - } + public static native function addEventHandlerProxy(obj: Object, propertyName: String, eventName: String, argsConverter: Function = null); private static native function _installObjectMethods(); @@ -79,54 +76,3 @@ package avm1lib { } } } - -import avm1lib.AVM1Utils; - -AVM1Utils; - -function _addEventHandlerProxy(obj: Object, propertyName: String, eventName: String, argsConverter: Function) -{ - var currentHandler: Function = null; - var handlerRunner: Function = null; - AVM1Utils.addProperty(obj, propertyName, - function(): Function { - return currentHandler; - }, - function setter(newHandler: Function) { - if (!this._as3Object) { // prototype/class ? - var defaultListeners = this._as2DefaultListeners || - (this._as2DefaultListeners = []); - defaultListeners.push({setter: setter, value: newHandler}); - // see also initDefaultListeners() - return; - } - // AVM1 MovieClips don't receive roll/release events by default until they set one of the following properties. - // This behaviour gets triggered whenever those properties are set, despite of the actual value they are set to. - if (propertyName === 'onRelease' || - propertyName === 'onReleaseOutside' || - propertyName === 'onRollOut' || - propertyName === 'onRollOver') { - this._as3Object.mouseEnabled = true; - this._as3Object.buttonMode = true; - } - if (currentHandler === newHandler) { - return; - } - if (currentHandler != null) { - this._as3Object.removeEventListener(eventName, handlerRunner); - } - currentHandler = newHandler; - if (currentHandler != null) { - handlerRunner = (function (obj: Object, handler: Function) { - return function handlerRunner() { - var args = argsConverter != null ? argsConverter(arguments) : null; - return handler.apply(obj, args); - }; - })(this, currentHandler); - this._as3Object.addEventListener(eventName, handlerRunner); - } else { - handlerRunner = null; - } - }, - false); -} diff --git a/src/avm1lib/AVM1Utils.ts b/src/avm1lib/AVM1Utils.ts index e51d1a917a..b8f0aa9cdc 100644 --- a/src/avm1lib/AVM1Utils.ts +++ b/src/avm1lib/AVM1Utils.ts @@ -75,7 +75,7 @@ module Shumway.AVM2.AS.avm1lib { } static get swfVersion(): any { - return AVM1Context.instance.swfVersion; + return AVM1Context.instance.loaderInfo.swfVersion; } static getAVM1Object(as3Object) { @@ -111,6 +111,53 @@ module Shumway.AVM2.AS.avm1lib { configurable: false }); } + + static addEventHandlerProxy(obj: ASObject, propertyName: string, eventName: string, + argsConverter?: Function) { + + var currentHandler: Function = null; + var handlerRunner: Function = null; + + function getter(): Function { + return currentHandler; + } + + function setter(newHandler: Function) { + if (!this._as3Object) { // prototype/class ? + var defaultListeners = this._as2DefaultListeners || (this._as2DefaultListeners = []); + defaultListeners.push({setter: setter, value: newHandler}); + // see also initDefaultListeners() + return; + } + // AVM1 MovieClips are set to button mode if one of the button-related event listeners is + // set. This behaviour is triggered regardless of the actual value they are set to. + if (propertyName === 'onRelease' || + propertyName === 'onReleaseOutside' || + propertyName === 'onRollOut' || + propertyName === 'onRollOver') { + this._as3Object.buttonMode = true; + } + if (currentHandler === newHandler) { + return; + } + if (currentHandler != null) { + this._as3Object.removeEventListener(eventName, handlerRunner); + } + currentHandler = newHandler; + if (currentHandler != null) { + handlerRunner = (function (obj: Object, handler: Function) { + return function handlerRunner() { + var args = argsConverter != null ? argsConverter(arguments) : null; + return handler.apply(obj, args); + }; + })(this, currentHandler); + this._as3Object.addEventListener(eventName, handlerRunner); + } else { + handlerRunner = null; + } + } + AVM1Utils.addProperty(obj, propertyName, getter, setter, false); + } } export function initDefaultListeners(thisArg) { @@ -154,4 +201,87 @@ module Shumway.AVM2.AS.avm1lib { return null; } + + export declare class PlaceObjectState { + symbolId: number; + variableName: string; + events: any[]; + } + export function initializeAVM1Object(as3Object: any, state: PlaceObjectState) { + var instanceAVM1 = getAVM1Object(as3Object); + release || Debug.assert(instanceAVM1); + + if (state.variableName) { + instanceAVM1.asSetPublicProperty('variable', state.variableName); + } + + var events = state.events; + if (!events) { + return; + } + //var stageListeners = []; + for (var j = 0; j < events.length; j++) { + var swfEvent = events[j]; + var actionsData; + if (swfEvent.actionsData) { + actionsData = new AVM1.AVM1ActionsData(swfEvent.actionsData, + 's' + state.symbolId + 'e' + j); + swfEvent.actionsData = null; + swfEvent.compiled = actionsData; + } else { + actionsData = swfEvent.compiled; + } + release || Debug.assert(actionsData); + var handler = clipEventHandler.bind(null, actionsData, instanceAVM1); + var flags = swfEvent.flags; + for (var eventFlag in ClipEventMappings) { + if (!(flags & (eventFlag | 0))) { + continue; + } + var eventName = ClipEventMappings[eventFlag]; + //if (eventName === 'mouseDown' || eventName === 'mouseUp' || eventName === 'mouseMove') { + // // FIXME regressed, avm1 mouse events shall be received all the time. + // stageListeners.push({eventName: eventName, handler: handler}); + // as3Object.stage.addEventListener(eventName, handler); + //} else { + as3Object.addEventListener(eventName, handler); + //} + } + } + //if (stageListeners.length > 0) { + // as3Object.addEventListener('removedFromStage', function () { + // for (var i = 0; i < stageListeners.length; i++) { + // this.removeEventListener(stageListeners[i].eventName, stageListeners[i].fn, false); + // } + // }, false); + //} + } + + function clipEventHandler(actionsData: AVM1.AVM1ActionsData, + receiver: {_nativeAS3Object: flash.display.DisplayObject}) { + return receiver._nativeAS3Object.loaderInfo._avm1Context.executeActions(actionsData, receiver); + } + + import AVM1ClipEvents = SWF.Parser.AVM1ClipEvents; + var ClipEventMappings = Object.create(null); + ClipEventMappings[AVM1ClipEvents.Load] = 'load'; + // AVM1's enterFrame happens at the same point in the cycle as AVM2's frameConstructed. + ClipEventMappings[AVM1ClipEvents.EnterFrame] = 'frameConstructed'; + ClipEventMappings[AVM1ClipEvents.Unload] = 'unload'; + ClipEventMappings[AVM1ClipEvents.MouseMove] = 'mouseMove'; + ClipEventMappings[AVM1ClipEvents.MouseDown] = 'mouseDown'; + ClipEventMappings[AVM1ClipEvents.MouseUp] = 'mouseUp'; + ClipEventMappings[AVM1ClipEvents.KeyDown] = 'keyDown'; + ClipEventMappings[AVM1ClipEvents.KeyUp] = 'keyUp'; + ClipEventMappings[AVM1ClipEvents.Data] = {toString: function() {Debug.warning('Data ClipEvent not implemented');}}; + ClipEventMappings[AVM1ClipEvents.Initialize] = 'initialize'; + ClipEventMappings[AVM1ClipEvents.Press] = 'mouseDown'; + ClipEventMappings[AVM1ClipEvents.Release] = 'click'; + ClipEventMappings[AVM1ClipEvents.ReleaseOutside] = 'releaseOutside'; + ClipEventMappings[AVM1ClipEvents.RollOver] = 'mouseOver'; + ClipEventMappings[AVM1ClipEvents.RollOut] = 'mouseOut'; + ClipEventMappings[AVM1ClipEvents.DragOver] = {toString: function() {Debug.warning('DragOver ClipEvent not implemented');}}; + ClipEventMappings[AVM1ClipEvents.DragOut] = {toString: function() {Debug.warning('DragOut ClipEvent not implemented');}}; + ClipEventMappings[AVM1ClipEvents.KeyPress] = {toString: function() {Debug.warning('KeyPress ClipEvent not implemented');}}; + ClipEventMappings[AVM1ClipEvents.Construct] = 'construct'; } diff --git a/src/avm1lib/flash.d.ts b/src/avm1lib/flash.d.ts index 50fdb0dcdc..c4c199270d 100644 --- a/src/avm1lib/flash.d.ts +++ b/src/avm1lib/flash.d.ts @@ -47,6 +47,10 @@ declare module Shumway.AVM2.AS.flash { addFrameScript(frameIndex: number, script: (any?) => any /*, ...*/): void; } class Loader extends DisplayObject {} + class LoaderInfo { + swfVersion: number; + getSymbolById(id: number): any; + } class AVM1Movie extends DisplayObject {} class BitmapData extends ASNative {} class Bitmap extends DisplayObject { @@ -54,7 +58,7 @@ declare module Shumway.AVM2.AS.flash { } class SimpleButton extends DisplayObject { _symbol: { - buttonActions: Shumway.Timeline.AVM1ButtonAction[] + data: {buttonActions: Shumway.Timeline.AVM1ButtonAction[]} } } } diff --git a/src/avm2/bin/avm.js b/src/avm2/bin/avm.js index 50f94f684b..cf3bd2951c 100644 --- a/src/avm2/bin/avm.js +++ b/src/avm2/bin/avm.js @@ -175,7 +175,7 @@ for (var f = 0; f < files.length; f++) { load(tsBuildPath + "swf.js"); } var SWF_TAG_CODE_DO_ABC = Shumway.SWF.Parser.SwfTag.CODE_DO_ABC; - var SWF_TAG_CODE_DO_ABC_ = Shumway.SWF.Parser.SwfTag.CODE_DO_ABC_; + var SWF_TAG_CODE_DO_ABC_ = Shumway.SWF.Parser.SwfTag.CODE_DO_ABC_DEFINE; Shumway.SWF.Parser.parse(snarf(file, "binary"), { oncomplete: function(result) { diff --git a/src/avm2/domain.ts b/src/avm2/domain.ts index 0c7bc4fb45..be26bcc914 100644 --- a/src/avm2/domain.ts +++ b/src/avm2/domain.ts @@ -131,6 +131,7 @@ module Shumway.AVM2.Runtime { public exceptions: any []; public globals: Map; public builtinsLoaded: boolean; + public avm1Loaded: boolean; private _loadAVM1: (next) => void; private _loadAVM1Promise: Promise; @@ -149,6 +150,7 @@ module Shumway.AVM2.Runtime { this._loadAVM1 = loadAVM1; this._loadAVM1Promise = null; + this.avm1Loaded = false; /** * All runtime exceptions are boxed in this object to tag them as having @@ -198,10 +200,14 @@ module Shumway.AVM2.Runtime { var loadAVM1Callback = this._loadAVM1; release || assert(loadAVM1Callback); + var self = this; if (!this._loadAVM1Promise) { this._loadAVM1Promise = new Promise(function (resolve) { loadAVM1Callback(resolve); }); + this._loadAVM1Promise.then(function () { + self.avm1Loaded = true; + }) } return this._loadAVM1Promise; } diff --git a/src/avm2/generated/avm1lib/avm1lib.abc b/src/avm2/generated/avm1lib/avm1lib.abc index 852b6e9e668717e983f347a71d4165b6022f63dd..5b3766cc90e733c305b03f6be4def98fd4a84729 100644 GIT binary patch literal 26336 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 Svlaf$VtS9zfZA>tg#QHz;YG^; diff --git a/src/avm2/natives/byteArray.ts b/src/avm2/natives/byteArray.ts index 1ed808b979..7ba01bc3a4 100644 --- a/src/avm2/natives/byteArray.ts +++ b/src/avm2/natives/byteArray.ts @@ -105,23 +105,21 @@ module Shumway.AVM2.AS { static initializer = function (source: any) { var self: ByteArray = this; - // Unsafe reference to BinarySymbol, we don't want to create an unnecessary dependence just for - // this once use case. - var BinarySymbol = (Shumway).Timeline.BinarySymbol; - release || assert (BinarySymbol); - var buffer: ArrayBuffer; var length = 0; if (source) { if (source instanceof ArrayBuffer) { buffer = source.slice(); } else if (Array.isArray(source)) { - buffer = new Uint8Array(buffer).buffer; - } else if (source instanceof BinarySymbol) { - buffer = new Uint8Array(source.buffer).buffer.slice(); + buffer = new Uint8Array(source).buffer; } else if ('buffer' in source) { - release || assert (source.buffer instanceof ArrayBuffer); - buffer = source.buffer.slice(); + if (source.buffer instanceof Uint8Array) { + var begin = source.buffer.byteOffset; + buffer = source.buffer.buffer.slice(begin, begin + source.buffer.length); + } else { + release || assert(source.buffer instanceof ArrayBuffer); + buffer = source.buffer.slice(); + } } else { Debug.unexpected("Source type."); } @@ -132,7 +130,7 @@ module Shumway.AVM2.AS { self._buffer = buffer; self._length = length; self._position = 0; - self._updateViews(); + self._resetViews(); self._objectEncoding = ByteArray.defaultObjectEncoding; self._littleEndian = false; // AS3 is bigEndian by default. self._bitBuffer = 0; @@ -168,7 +166,7 @@ module Shumway.AVM2.AS { private _bitBuffer: number; private _bitLength: number; - private _updateViews: () => void; + private _resetViews: () => void; asGetNumericProperty: (name: number) => number; asSetNumericProperty: (name: number, value: number) => void; diff --git a/src/base/SWFTags.ts b/src/base/SWFTags.ts index 417fe21ea1..93791b7e87 100644 --- a/src/base/SWFTags.ts +++ b/src/base/SWFTags.ts @@ -63,12 +63,12 @@ module Shumway.SWF.Parser { CODE_DEFINE_BEHAVIOUR = 44, CODE_SOUND_STREAM_HEAD2 = 45, CODE_DEFINE_MORPH_SHAPE = 46, - CODE_FRAME_TAG = 47, + CODE_GENERATE_FRAME = 47, CODE_DEFINE_FONT2 = 48, CODE_GEN_COMMAND = 49, - CODE_DEFINE_COMMAND_OBJ = 50, + CODE_DEFINE_COMMAND_OBJECT = 50, CODE_CHARACTER_SET = 51, - CODE_FONT_REF = 52, + CODE_EXTERNAL_FONT = 52, CODE_DEFINE_FUNCTION = 53, CODE_PLACE_FUNCTION = 54, CODE_GEN_TAG_OBJECTS = 55, @@ -88,7 +88,7 @@ module Shumway.SWF.Parser { CODE_FILE_ATTRIBUTES = 69, CODE_PLACE_OBJECT3 = 70, CODE_IMPORT_ASSETS2 = 71, - CODE_DO_ABC_ = 72, + CODE_DO_ABC_DEFINE = 72, CODE_DEFINE_FONT_ALIGN_ZONES = 73, CODE_CSM_TEXT_SETTINGS = 74, CODE_DEFINE_FONT3 = 75, @@ -110,32 +110,97 @@ module Shumway.SWF.Parser { CODE_DEFINE_FONT4 = 91 } - export enum PlaceObjectFlags { - Reserved = 0x800, - OpaqueBackground = 0x400, - HasVisible = 0x200, - HasImage = 0x100, - HasClassName = 0x800, - HasCacheAsBitmap = 0x400, - HasBlendMode = 0x200, - HasFilterList = 0x100, - HasClipActions = 0x080, - HasClipDepth = 0x040, - HasName = 0x020, - HasRatio = 0x010, - HasColorTransform = 0x008, - HasMatrix = 0x004, - HasCharacter = 0x002, - Move = 0x001 + export enum DefinitionTags { + CODE_DEFINE_SHAPE = 2, + CODE_DEFINE_BITS = 6, + CODE_DEFINE_BUTTON = 7, + CODE_DEFINE_FONT = 10, + CODE_DEFINE_TEXT = 11, + CODE_DEFINE_SOUND = 14, + CODE_DEFINE_BITS_LOSSLESS = 20, + CODE_DEFINE_BITS_JPEG2 = 21, + CODE_DEFINE_SHAPE2 = 22, + CODE_DEFINE_SHAPE3 = 32, + CODE_DEFINE_TEXT2 = 33, + CODE_DEFINE_BUTTON2 = 34, + CODE_DEFINE_BITS_JPEG3 = 35, + CODE_DEFINE_BITS_LOSSLESS2 = 36, + CODE_DEFINE_EDIT_TEXT = 37, + CODE_DEFINE_SPRITE = 39, + CODE_DEFINE_MORPH_SHAPE = 46, + CODE_DEFINE_FONT2 = 48, + CODE_DEFINE_VIDEO_STREAM = 60, + CODE_DEFINE_FONT3 = 75, + CODE_DEFINE_SHAPE4 = 83, + CODE_DEFINE_MORPH_SHAPE2 = 84, + CODE_DEFINE_BINARY_DATA = 87, + CODE_DEFINE_BITS_JPEG4 = 90, + CODE_DEFINE_FONT4 = 91 + } + + export enum ImageDefinitionTags { + CODE_DEFINE_BITS = 6, + CODE_DEFINE_BITS_JPEG2 = 21, + CODE_DEFINE_BITS_JPEG3 = 35, + CODE_DEFINE_BITS_JPEG4 = 90 + } + + export enum FontDefinitionTags { + CODE_DEFINE_FONT = 10, + CODE_DEFINE_FONT2 = 48, + CODE_DEFINE_FONT3 = 75, + CODE_DEFINE_FONT4 = 91 } - export interface ISwfTagData { - code: SwfTag; - type?: string; - id?: number; - frameCount?: number; - repeat?: number; - tags?: Array; - finalTag?: boolean; + export enum ControlTags { + CODE_PLACE_OBJECT = 4, + CODE_PLACE_OBJECT2 = 26, + CODE_PLACE_OBJECT3 = 70, + CODE_REMOVE_OBJECT = 5, + CODE_REMOVE_OBJECT2 = 28, + CODE_START_SOUND = 15, + CODE_START_SOUND2 = 89, + CODE_VIDEO_FRAME = 61, + } + + export enum PlaceObjectFlags { + Move = 0x0001, + HasCharacter = 0x0002, + HasMatrix = 0x0004, + HasColorTransform = 0x0008, + HasRatio = 0x0010, + HasName = 0x0020, + HasClipDepth = 0x0040, + HasClipActions = 0x0080, + HasFilterList = 0x0100, + HasBlendMode = 0x0200, + HasCacheAsBitmap = 0x0400, + HasClassName = 0x0800, + HasImage = 0x1000, + HasVisible = 0x2000, + OpaqueBackground = 0x4000, + Reserved = 0x8000 + } + + export enum AVM1ClipEvents { + Load = 0x00001, + EnterFrame = 0x00002, + Unload = 0x00004, + MouseMove = 0x00008, + MouseDown = 0x00010, + MouseUp = 0x00020, + KeyDown = 0x00040, + KeyUp = 0x00080, + Data = 0x00100, + Initialize = 0x00200, + Press = 0x00400, + Release = 0x00800, + ReleaseOutside = 0x01000, + RollOver = 0x02000, + RollOut = 0x04000, + DragOver = 0x08000, + DragOut = 0x10000, + KeyPress = 0x20000, + Construct = 0x40000 } } diff --git a/src/base/ShapeData.ts b/src/base/ShapeData.ts index 6e7a1312a5..cb446b2512 100644 --- a/src/base/ShapeData.ts +++ b/src/base/ShapeData.ts @@ -466,12 +466,7 @@ module Shumway { private _writeStyleMatrix(matrix: ShapeMatrix, isMorph: boolean) { var styles: DataBuffer = isMorph ? this.morphStyles : this.styles; - styles.writeFloat(matrix.a); - styles.writeFloat(matrix.b); - styles.writeFloat(matrix.c); - styles.writeFloat(matrix.d); - styles.writeFloat(matrix.tx); - styles.writeFloat(matrix.ty); + styles.write6Floats(matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty); } private ensurePathCapacities(numCommands: number, numCoordinates: number) diff --git a/src/base/dataBuffer.ts b/src/base/dataBuffer.ts index 09c6ce6d21..c3588ae8d2 100644 --- a/src/base/dataBuffer.ts +++ b/src/base/dataBuffer.ts @@ -86,6 +86,12 @@ module Shumway.ArrayUtilities { bitMasks[i] = mask = (mask << 1) | 1; } + enum TypedArrayViewFlags { + U8 = 1, + I32 = 2, + F32 = 4 + } + export class DataBuffer implements IDataInput, IDataOutput { private static _nativeLittleEndian = new Int8Array(new Int32Array([1]).buffer)[0] === 1; @@ -115,7 +121,7 @@ module Shumway.ArrayUtilities { this._buffer = new ArrayBuffer(initialSize); this._length = 0; this._position = 0; - this._updateViews(); + this._resetViews(); this._littleEndian = DataBuffer._nativeLittleEndian; this._bitBuffer = 0; this._bitLength = 0; @@ -126,7 +132,7 @@ module Shumway.ArrayUtilities { dataBuffer._buffer = buffer; dataBuffer._length = length === -1 ? buffer.byteLength : length; dataBuffer._position = 0; - dataBuffer._updateViews(); + dataBuffer._resetViews(); dataBuffer._littleEndian = DataBuffer._nativeLittleEndian; dataBuffer._bitBuffer = 0; dataBuffer._bitLength = 0; @@ -143,11 +149,28 @@ module Shumway.ArrayUtilities { return new PlainObjectDataBuffer(this._buffer, this._length, this._littleEndian); } - private _updateViews() { + /** + * By default, we only have a byte view. All other views are |null|. + */ + private _resetViews() { this._u8 = new Uint8Array(this._buffer); + this._i32 = null; + this._f32 = null; + } + + /** + * We don't want to eagerly allocate views if we won't ever need them. You must call this method + * before using a view of a certain type to make sure it's available. Once a view is allocated, + * it is not re-allocated unless the view becomes |null| as a result of a call to |resetViews|. + */ + private _requestViews(flags: TypedArrayViewFlags) { if ((this._buffer.byteLength & 0x3) === 0) { - this._i32 = new Int32Array(this._buffer); - this._f32 = new Float32Array(this._buffer); + if (this._i32 === null && flags & TypedArrayViewFlags.I32) { + this._i32 = new Int32Array(this._buffer); + } + if (this._f32 === null && flags & TypedArrayViewFlags.F32) { + this._f32 = new Float32Array(this._buffer); + } } } @@ -165,7 +188,7 @@ module Shumway.ArrayUtilities { var newBuffer = DataBuffer._arrayBufferPool.acquire(newLength); var curentView = this._u8; this._buffer = newBuffer; - this._updateViews(); + this._resetViews(); this._u8.set(curentView); DataBuffer._arrayBufferPool.release(currentBuffer); } @@ -252,6 +275,7 @@ module Shumway.ArrayUtilities { throwError('EOFError', Errors.EOFError); } this._position = position + 4; + this._requestViews(TypedArrayViewFlags.F32); if (this._littleEndian && (position & 0x3) === 0 && this._f32) { return this._f32[position >> 2]; } else { @@ -378,6 +402,10 @@ module Shumway.ArrayUtilities { this.writeUnsignedInt(value); } + write2Ints(a: number, b: number): void { + this.write2UnsignedInts(a, b); + } + write4Ints(a: number, b: number, c: number, d: number): void { this.write4UnsignedInts(a, b, c, d); } @@ -385,6 +413,7 @@ module Shumway.ArrayUtilities { writeUnsignedInt(value: number /*uint*/): void { var position = this._position; this._ensureCapacity(position + 4); + this._requestViews(TypedArrayViewFlags.I32); if (this._littleEndian === DataBuffer._nativeLittleEndian && (position & 0x3) === 0 && this._i32) { this._i32[position >> 2] = value; } else { @@ -408,9 +437,28 @@ module Shumway.ArrayUtilities { } } + write2UnsignedInts(a: number, b: number): void { + var position = this._position; + this._ensureCapacity(position + 8); + this._requestViews(TypedArrayViewFlags.I32); + if (this._littleEndian === DataBuffer._nativeLittleEndian && (position & 0x3) === 0 && this._i32) { + this._i32[(position >> 2) + 0] = a; + this._i32[(position >> 2) + 1] = b; + position += 8; + this._position = position; + if (position > this._length) { + this._length = position; + } + } else { + this.writeUnsignedInt(a); + this.writeUnsignedInt(b); + } + } + write4UnsignedInts(a: number, b: number, c: number, d: number): void { var position = this._position; this._ensureCapacity(position + 16); + this._requestViews(TypedArrayViewFlags.I32); if (this._littleEndian === DataBuffer._nativeLittleEndian && (position & 0x3) === 0 && this._i32) { this._i32[(position >> 2) + 0] = a; this._i32[(position >> 2) + 1] = b; @@ -432,6 +480,7 @@ module Shumway.ArrayUtilities { writeFloat(value: number): void { var position = this._position; this._ensureCapacity(position + 4); + this._requestViews(TypedArrayViewFlags.F32); if (this._littleEndian === DataBuffer._nativeLittleEndian && (position & 0x3) === 0 && this._f32) { this._f32[position >> 2] = value; } else { @@ -460,6 +509,7 @@ module Shumway.ArrayUtilities { write6Floats(a: number, b: number, c: number, d: number, e: number, f: number): void { var position = this._position; this._ensureCapacity(position + 24); + this._requestViews(TypedArrayViewFlags.F32); if (this._littleEndian === DataBuffer._nativeLittleEndian && (position & 0x3) === 0 && this._f32) { this._f32[(position >> 2) + 0] = a; this._f32[(position >> 2) + 1] = b; @@ -577,6 +627,7 @@ module Shumway.ArrayUtilities { } get ints(): Int32Array { + this._requestViews(TypedArrayViewFlags.I32); return this._i32; } diff --git a/src/base/deflate.ts b/src/base/deflate.ts index c77865e2b0..e74aa57e05 100644 --- a/src/base/deflate.ts +++ b/src/base/deflate.ts @@ -76,7 +76,7 @@ module Shumway.ArrayUtilities { } constructor(verifyHeader: boolean) { - this._buffer = new Uint8Array(1024); + this._buffer = null; this._bufferSize = 0; this._bufferPosition = 0; this._bitBuffer = 0; @@ -103,13 +103,19 @@ module Shumway.ArrayUtilities { areTablesInitialized = true; } } - public push(data: Uint8Array) { - if (this._buffer.length < this._bufferSize + data.length) { - var newBuffer = new Uint8Array(this._bufferSize + data.length); - newBuffer.set(this._buffer); - this._buffer = newBuffer; + public push(data: Uint8Array, takeOwnership: boolean = false) { + if (takeOwnership && this._bufferSize === 0) { + this._buffer = data; + } else { + if (!this._buffer || this._buffer.length < this._bufferSize + data.length) { + var newBuffer = new Uint8Array(this._bufferSize + data.length); + if (this._buffer) { + newBuffer.set(this._buffer); + } + this._buffer = newBuffer; + } + this._buffer.set(data, this._bufferSize); } - this._buffer.set(data, this._bufferSize); this._bufferSize += data.length; this._bufferPosition = 0; @@ -153,15 +159,23 @@ module Shumway.ArrayUtilities { } if (this._windowPosition >= WINDOW_SHIFT_POSITION) { // shift window - this._window.set(this._window.subarray(this._windowPosition - WINDOW_SIZE, - this._windowPosition)); + if ('copyWithin' in this._buffer) { + this._window['copyWithin'](0, this._windowPosition - WINDOW_SIZE, this._windowPosition); + } else { + this._window.set(this._window.subarray(this._windowPosition - WINDOW_SIZE, + this._windowPosition)); + } this._windowPosition = WINDOW_SIZE; } } while (!incomplete && this._bufferPosition < this._bufferSize); if (this._bufferPosition < this._bufferSize) { // shift buffer - this._buffer.set(this._buffer.subarray(this._bufferPosition, this._bufferSize)); + if ('copyWithin' in this._buffer) { + this._buffer['copyWithin'](0, this._bufferPosition, this._bufferSize); + } else { + this._buffer.set(this._buffer.subarray(this._bufferPosition, this._bufferSize)); + } this._bufferSize -= this._bufferPosition; } else { this._bufferSize = 0; @@ -189,6 +203,7 @@ module Shumway.ArrayUtilities { } else { this._state = InflateState.INIT; } + return false; } private _decodeInitState(): boolean { if (this._isFinalBlock) { diff --git a/src/base/utilities.ts b/src/base/utilities.ts index ac88c9f245..27eae0895e 100644 --- a/src/base/utilities.ts +++ b/src/base/utilities.ts @@ -16,7 +16,8 @@ /// var jsGlobal = (function() { return this || (1, eval)('this'); })(); -var inBrowser = typeof console != "undefined"; +// Our polyfills for some DOM things make testing this slightly more onerous than it ought to be. +var inBrowser = typeof window !=='undefined' && 'document' in window && 'plugins' in window.document; declare var putstr; // declare var print; @@ -47,17 +48,8 @@ if (!jsGlobal.performance.now) { jsGlobal.performance.now = typeof dateNow !== 'undefined' ? dateNow : Date.now; } -function log(message?: any, ...optionalParams: any[]): void { - jsGlobal.print.apply(jsGlobal, arguments); -} - -function warn(message?: any, ...optionalParams: any[]): void { - if (inBrowser) { - console.warn.apply(console, arguments); - } else { - jsGlobal.print(Shumway.IndentingWriter.RED + message + Shumway.IndentingWriter.ENDC); - } -} +var log = console.log.bind(console); +var warn = console.warn.bind(console); interface String { padRight(c: string, n: number): string; @@ -261,11 +253,7 @@ module Shumway { } export function error(message: string) { - if (!inBrowser) { - warn(message + "\n\nStack Trace:\n" + Debug.backtrace()); - } else { - warn(message); - } + console.error(message); throw new Error(message); } @@ -274,7 +262,12 @@ module Shumway { condition = true; } if (!condition) { - Debug.error(message.toString()); + if (typeof console !== 'undefined' && 'assert' in console) { + console.assert(false, message); + throw new Error(message); + } else { + Debug.error(message.toString()); + } } } @@ -289,8 +282,8 @@ module Shumway { } } - export function warning(message: string) { - release || warn(message); + export function warning(...messages: any[]) { + release || warn.apply(window, messages); } export function notUsed(message: string) { @@ -327,10 +320,6 @@ module Shumway { export function unexpectedCase(message?: any) { Debug.assert(false, "Unexpected Case: " + message); } - - export function untested(message?: any) { - Debug.warning("Congratulations, you've found a code path for which we haven't found a test case. Please submit the test case: " + message); - } } export function getTicks(): number { @@ -1727,8 +1716,8 @@ module Shumway { public static logLevel: LogLevel = LogLevel.All; - private static _consoleOut = inBrowser ? console.info.bind(console) : print; - private static _consoleOutNoNewline = inBrowser ? console.info.bind(console) : putstr; + private static _consoleOut = console.info.bind(console); + private static _consoleOutNoNewline = console.info.bind(console); private _tab: string; private _padding: string; @@ -3354,21 +3343,30 @@ module Shumway { export var instance: IFileLoadingService; } - export function registerCSSFont(id: number, buffer: ArrayBuffer) { + export function registerCSSFont(id: number, buffer: ArrayBuffer, forceFontInit: boolean) { if (!inBrowser) { - console.warn('Cannot register CSS font outside the browser'); + Debug.warning('Cannot register CSS font outside the browser'); return; } var head = document.head; head.insertBefore(document.createElement('style'), head.firstChild); var style = document.styleSheets[0]; - style.insertRule( - '@font-face{' + - 'font-family:swffont' + id + ';' + - 'src:url(data:font/opentype;base64,' + - Shumway.StringUtilities.base64ArrayBuffer(buffer) + ')' + '}', - style.cssRules.length - ); + var rule = '@font-face{font-family:swffont' + id + ';src:url(data:font/opentype;base64,' + + Shumway.StringUtilities.base64ArrayBuffer(buffer) + ')' + '}'; + style.insertRule(rule, style.cssRules.length); + // In at least Chrome, the browser only decodes a font once it's used in the page at all. + // Because it still does so asynchronously, we create a with some text using the font, take + // some measurement from it (which will turn out wrong because the font isn't yet available), + // and then remove the node again. Then, magic happens. After a bit of time for said magic to + // take hold, the font is available for actual use on canvas. + if (forceFontInit) { + var node = document.createElement('div'); + node.style.fontFamily = 'swffont' + id; + node.innerHTML = 'hello'; + document.body.appendChild(node); + var dummyHeight = node.clientHeight; + document.body.removeChild(node); + } } export interface IExternalInterfaceService { @@ -3523,6 +3521,10 @@ module Shumway { public resolve: (result:T) => void; public reject: (reason) => void; + then(onFulfilled, onRejected) { + return this.promise.then(onFulfilled, onRejected); + } + constructor() { this.promise = new Promise(function (resolve, reject) { this.resolve = resolve; diff --git a/src/flash/avm1.d.ts b/src/flash/avm1.d.ts index a6580b703d..68bea3829d 100644 --- a/src/flash/avm1.d.ts +++ b/src/flash/avm1.d.ts @@ -30,6 +30,7 @@ declare module Shumway.AVM2.AS.avm1lib { export class AVM1Mouse extends ASClass {} export function getAVM1Object(as3Object: any): any; + export function initializeAVM1Object(as3Object: any, state: Shumway.Timeline.AnimationState): any; } declare module Shumway.AVM1 { @@ -37,7 +38,7 @@ declare module Shumway.AVM1 { constructor(actionsBlock: Uint8Array, name: string); } export class AVM1Context { - static create(swfVersion: number): AVM1Context; + static create(loaderInfo: Shumway.AVM2.AS.flash.display.LoaderInfo): AVM1Context; addAsset(className: string, symbolId: number, symbolProps); executeActions(actionsData: AVM1ActionsData, scopeObj); flushPendingScripts(); diff --git a/src/flash/display/BitmapData.ts b/src/flash/display/BitmapData.ts index 9e38bd3c90..09fbb7bbef 100644 --- a/src/flash/display/BitmapData.ts +++ b/src/flash/display/BitmapData.ts @@ -75,6 +75,13 @@ module Shumway.AVM2.AS.flash.display { this._transparent = !!transparent; this._rect = new Rectangle(0, 0, width, height); if (this._symbol) { + //var canvas: HTMLCanvasElement = document.createElement('canvas'); + //canvas.width = width; + //canvas.height = height; + //var context = canvas.getContext('2d'); + //context.drawImage(this._symbol.image, 0, 0); + //this._symbol.data = new Uint8Array(context.getImageData(0, 0, width, height).data; + //this._symbol.image = null; this._data = new Uint8Array(this._symbol.data); this._type = this._symbol.type; if (this._type === ImageType.PremultipliedAlphaARGB || @@ -107,13 +114,13 @@ module Shumway.AVM2.AS.flash.display { _addBitmapReferrer(bitmap: flash.display.Bitmap) { var index = indexOf(this._bitmapReferrers, bitmap); - release && assert(index < 0); + release || assert(index < 0); this._bitmapReferrers.push(bitmap); } _removeBitmapReferrer(bitmap: flash.display.Bitmap) { var index = indexOf(this._bitmapReferrers, bitmap); - release && assert(index >= 0); + release || assert(index >= 0); this._bitmapReferrers[index] = null; } diff --git a/src/flash/display/DisplayObject.ts b/src/flash/display/DisplayObject.ts index 29e1b528b7..d2a5aa0dd7 100644 --- a/src/flash/display/DisplayObject.ts +++ b/src/flash/display/DisplayObject.ts @@ -431,9 +431,14 @@ module Shumway.AVM2.AS.flash.display { * Calling its constructor is optional at this point, since that can happen in a later frame * phase. */ - static createAnimatedDisplayObject(state: Shumway.Timeline.AnimationState, - callConstructor: boolean): DisplayObject { + createAnimatedDisplayObject(state: Shumway.Timeline.AnimationState, + callConstructor: boolean): DisplayObject { var symbol = state.symbol; + if (!symbol) { + var ownSymbol = this._symbol; + symbol = ownSymbol.loaderInfo.getSymbolById(state.symbolId); + state.symbol = symbol; + } var symbolClass = symbol.symbolClass; var instance: DisplayObject; if (symbolClass.isSubtypeOf(flash.display.BitmapData)) { diff --git a/src/flash/display/Loader.ts b/src/flash/display/Loader.ts index d6fcecbee1..185b49bc72 100644 --- a/src/flash/display/Loader.ts +++ b/src/flash/display/Loader.ts @@ -15,28 +15,21 @@ */ module Shumway.AVM2.AS.flash.display { import assert = Shumway.Debug.assert; - import warning = Shumway.Debug.warning; import assertUnreachable = Shumway.Debug.assertUnreachable; - import notImplemented = Shumway.Debug.notImplemented; import throwError = Shumway.AVM2.Runtime.throwError; - import Telemetry = Shumway.Telemetry; - - import AVM2 = Shumway.AVM2.Runtime.AVM2; - import FileLoader = Shumway.FileLoader; - import ILoadListener = Shumway.ILoadListener; - import AbcFile = Shumway.AVM2.ABC.AbcFile; import asCoerceString = Shumway.AVM2.Runtime.asCoerceString; - import events = flash.events; import ActionScriptVersion = flash.display.ActionScriptVersion; + import AVM2 = Shumway.AVM2.Runtime.AVM2; import ApplicationDomain = flash.system.ApplicationDomain; import LoaderContext = flash.system.LoaderContext; + import events = flash.events; - import Bounds = Shumway.Bounds; - - declare var SHUMWAY_ROOT: string; - declare var LOADER_WORKER_PATH: string; + import FileLoader = Shumway.FileLoader; + import ILoadListener = Shumway.ILoadListener; + import AbcFile = Shumway.AVM2.ABC.AbcFile; + import SWFFile = Shumway.SWF.SWFFile; enum LoadStatus { Unloaded = 0, @@ -50,18 +43,13 @@ module Shumway.AVM2.AS.flash.display { Bytes = 1 } - function getPlayer(): any { - return AVM2.instance.globals['Shumway.Player.Utils']; - } - export class Loader extends flash.display.DisplayObjectContainer implements IAdvancable, ILoadListener { + static runtimeStartTime: number; private static _rootLoader: Loader; private static _loadQueue: Loader []; - private static _embeddedContentLoadCount: number = 0; - - private _writer: IndentingWriter; + private static _embeddedContentLoadCount: number; /** * Creates or returns the root Loader instance. The loader property of that instance's @@ -83,45 +71,41 @@ module Shumway.AVM2.AS.flash.display { } static reset() { - Loader._rootLoader = null; + Loader._loadQueue.forEach(loader => loader.unload()); + Loader.classInitializer(); } - // Called whenever the class is initialized. static classInitializer: any = function () { Loader._rootLoader = null; Loader._loadQueue = []; + Loader.runtimeStartTime = 0; + Loader._embeddedContentLoadCount = 0; }; - - // Called whenever an instance of the class is initialized. static initializer: any = function() { var self: Loader = this; DisplayObject._advancableInstances.push(self); }; - // List of static symbols to link. - static classSymbols: string [] = null; // []; - - // List of instance symbols to link. + static classSymbols: string [] = null; static instanceSymbols: string [] = null; - static WORKERS_AVAILABLE = typeof Worker !== 'undefined'; - static LOADER_PATH = 'swf/worker.js'; - - static runtimeStartTime: number = 0; - - private static _commitFrameQueue: {loader: Loader; data: any}[] = []; /** * Handles the load status and dispatches progress events. This gets manually triggered in the * event loop to ensure the correct order of operations. */ static progress() { - Loader.FlushCommittedFrames(); var queue = Loader._loadQueue; for (var i = 0; i < queue.length; i++) { var instance = queue[i]; + var queuedUpdates = instance._queuedLoadUpdates; + for (var j = 0; j < queuedUpdates.length; j++) { + instance._applyLoadUpdate(queuedUpdates[j]); + } + instance._queuedLoadUpdates.length = 0; + var loaderInfo = instance._contentLoaderInfo; - var bytesLoaded = loaderInfo._bytesLoaded; + var bytesLoaded = loaderInfo._newBytesLoaded; var bytesTotal = loaderInfo._bytesTotal; switch (instance._loadStatus) { case LoadStatus.Unloaded: @@ -133,72 +117,59 @@ module Shumway.AVM2.AS.flash.display { loaderInfo.dispatchEvent(events.Event.getInstance(events.Event.OPEN)); } - // The first time any progress is made at all, a progress event with bytesLoaded = 0 - // is dispatched. - loaderInfo.dispatchEvent(new events.ProgressEvent(events.ProgressEvent.PROGRESS, - false, false, 0, bytesTotal)); + // Except when loading the root SWF, the first time any progress is made at all, + // a progress event with bytesLoaded = 0 is dispatched. + if (instance !== Loader.getRootLoader()) { + loaderInfo.dispatchEvent(new events.ProgressEvent(events.ProgressEvent.PROGRESS, + false, false, 0, bytesTotal)); + } instance._loadStatus = LoadStatus.Opened; // Fallthrough case LoadStatus.Opened: - if (loaderInfo._bytesLoadedChanged) { - loaderInfo._bytesLoadedChanged = false; - loaderInfo.dispatchEvent(new events.ProgressEvent(events.ProgressEvent.PROGRESS, - false, false, bytesLoaded, - bytesTotal)); - } if (!(instance._content && instance._content._hasFlags(DisplayObjectFlags.Constructed))) { + if (loaderInfo._newBytesLoaded > loaderInfo._bytesLoaded) { + loaderInfo._bytesLoaded = loaderInfo._newBytesLoaded; + loaderInfo.dispatchEvent(new events.ProgressEvent(events.ProgressEvent.PROGRESS, + false, false, bytesLoaded, + bytesTotal)); + } break; } instance._loadStatus = LoadStatus.Initialized; loaderInfo.dispatchEvent(events.Event.getInstance(events.Event.INIT)); // Fallthrough case LoadStatus.Initialized: + if (loaderInfo._newBytesLoaded > loaderInfo._bytesLoaded) { + loaderInfo._bytesLoaded = loaderInfo._newBytesLoaded; + loaderInfo.dispatchEvent(new events.ProgressEvent(events.ProgressEvent.PROGRESS, + false, false, bytesLoaded, + bytesTotal)); + } if (bytesLoaded === bytesTotal) { instance._loadStatus = LoadStatus.Complete; + instance._queuedLoadUpdates = null; + queue.splice(i--, 1); loaderInfo.dispatchEvent(events.Event.getInstance(events.Event.COMPLETE)); } break; - case LoadStatus.Complete: - queue.splice(i--, 1); - break; default: assertUnreachable("Mustn't encounter unhandled status in Loader queue."); } } } - private static FlushCommittedFrames() { - var frames = Loader._commitFrameQueue; - for (var i = 0; i < frames.length; i++) { - frames[i].loader._commitFrame(frames[i].data); - } - frames.length = 0; - } - constructor () { false && super(); DisplayObjectContainer.instanceConstructorNoInitialize.call(this); - this._writer = new IndentingWriter(); this._content = null; this._contentLoaderInfo = new flash.display.LoaderInfo(); + this._contentLoaderInfo._loader = this; this._fileLoader = null; this._loadStatus = LoadStatus.Unloaded; - - this._contentLoaderInfo._loader = this; - - this._initialDataLoaded = new PromiseWrapper(); - this._waitForInitialData = true; - this._commitDataQueue = this._initialDataLoaded.promise; - - this._codeExecutionPromise = new PromiseWrapper(); - this._progressPromise = new PromiseWrapper(); - this._startPromise = Promise.all([ - this._codeExecutionPromise.promise, - this._progressPromise.promise - ]); + this._startPromise = new PromiseWrapper(); } _initFrame(advance: boolean) { @@ -242,27 +213,12 @@ module Shumway.AVM2.AS.flash.display { private _fileLoader: FileLoader; private _loadStatus: LoadStatus; private _loadingType: LoadingType; - - private _initialDataLoaded: PromiseWrapper; - private _waitForInitialData: boolean; - private _commitDataQueue: Promise; - private _frameAssetsQueue: Array>; - - /** - * Resolved when both |_progressPromise| and |_codeExecutionPromise| are resolved. - */ - _startPromise: Promise; + private _queuedLoadUpdates: LoadProgressUpdate[]; /** - * Resolved after the first progress event. This ensures that at least 64K of data have been - * parsed before playback begins. + * Resolved when the root Loader has loaded the first frame(s) of the main SWF. */ - private _progressPromise: PromiseWrapper; - - /** - * Resolved after AVM2 and AVM1 (if used) have been initialized. - */ - private _codeExecutionPromise: PromiseWrapper; + _startPromise: PromiseWrapper; /** * No way of knowing what's in |data|, so do a best effort to print out some meaninfgul debug info. @@ -275,377 +231,291 @@ module Shumway.AVM2.AS.flash.display { return "{" + keyValueParis.join(", ") + "}"; } - /** - * Chain a promise for |data| to the previous promise. - */ - private _commitData(data: any): void { - if (this._waitForInitialData) { - // 'progress' event usually fires after 64K, using this as a start to - // commit frame/symbols - var enoughData = data.command === 'progress' || - data.command === 'image' || - data.command === 'error'; - if (enoughData) { - this._waitForInitialData = false; - this._initialDataLoaded.resolve(undefined); - } + get content(): flash.display.DisplayObject { + if (this._loadStatus === LoadStatus.Unloaded) { + return null; } + return this._content; + } + + get contentLoaderInfo(): flash.display.LoaderInfo { + return this._contentLoaderInfo; + } - var nextPromise = this._commitDataQueue.then(this._commitQueuedData.bind(this, data)); - if (traceLoaderOption.value) { - this._writer.writeTimeLn("Making for: " + this._describeData(data)); + _getJPEGLoaderContextdeblockingfilter(context: flash.system.LoaderContext): number { + if (flash.system.JPEGLoaderContext.isType(context)) { + return (context).deblockingFilter; } - this._commitDataQueue = nextPromise; + return 0.0; } - /** - * Returns a promise for the requested |data|. Some of these resolve right away, returning |null| - * others return a promise that is suspended until some other thing is resolved, like font loading - * or image decoding. - */ - private _commitQueuedData(data: any): Promise { - var loaderInfo = this._contentLoaderInfo; - var command = data.command; - var suspendUntil: Promise = null; + get uncaughtErrorEvents(): events.UncaughtErrorEvents { + return this._uncaughtErrorEvents; + } - if (traceLoaderOption.value) { - this._writer.writeTimeLn("Executing Promise: " + this._describeData(data)); + load(request: flash.net.URLRequest, context?: LoaderContext): void { + this.close(); + // TODO: clean up contentloaderInfo. + this._contentLoaderInfo._url = request.url; + this._applyLoaderContext(context); + this._loadingType = LoadingType.External; + this._fileLoader = new FileLoader(this); + if (!release && traceLoaderOption.value) { + console.log("Load start: " + request.url); } + this._fileLoader.loadFile(request._toFileRequest()); - switch (command) { - case 'init': - var info = data.result; - - loaderInfo.bytesLoaded = info.bytesLoaded; - loaderInfo._bytesTotal = info.bytesTotal; - loaderInfo._swfVersion = info.swfVersion; - loaderInfo._frameRate = info.frameRate; - var bbox = info.bbox; - loaderInfo._width = bbox.xMax - bbox.xMin; - loaderInfo._height = bbox.yMax - bbox.yMin; - - var rootSymbol = new Timeline.SpriteSymbol(0, true); - rootSymbol.numFrames = info.frameCount; - loaderInfo.registerSymbol(rootSymbol); - - if (!info.fileAttributes || !info.fileAttributes.doAbc) { - loaderInfo._actionScriptVersion = ActionScriptVersion.ACTIONSCRIPT2; - suspendUntil = this._initAvm1(); - if (traceLoaderOption.value) { - this._writer.writeTimeLn("Suspending until AVM1 is initialized."); - } - } - break; - case 'progress': - var info = data.result; - var bytesLoaded = info.bytesLoaded; - var bytesTotal = info.bytesTotal; - release || assert (bytesLoaded <= bytesTotal, "Loaded bytes must not exceed total bytes."); - if (!loaderInfo._bytesTotal) { - loaderInfo._bytesTotal = bytesTotal; - } else { - release || assert (loaderInfo._bytesTotal === bytesTotal, "Total bytes must not change."); - } - // Content code might rely on specific values for bytesLoaded to assume embedded assets - // to be available. Hence, we delay updating the value until we can guarantee availability - // of decoded data for all preceding bytes. - if (this._frameAssetsQueue) { - suspendUntil = Promise.all(this._frameAssetsQueue).then(function () { - loaderInfo.bytesLoaded = bytesLoaded; - }); - } else { - loaderInfo.bytesLoaded = bytesLoaded; - } - this._progressPromise.resolve(undefined); - if (traceLoaderOption.value) { - this._writer.writeTimeLn("Resolving progress promise."); - } - break; - case 'complete': - if (data.stats) { - Telemetry.instance.reportTelemetry(data.stats); - } + // TODO: Only do this if a load wasn't in progress. + this._queuedLoadUpdates = []; + Loader._loadQueue.push(this); + } - this._fileLoader = null; - break; - case 'error': - this._contentLoaderInfo.dispatchEvent(new events.IOErrorEvent( - events.IOErrorEvent.IO_ERROR)); - break; - default: - //TODO: fix special-casing. Might have to move document class out of dictionary[0]. - if (data.id === 0) { - break; - } - if (data.isSymbol) { - this._commitAsset(data); - } else if (data.type === 'frame') { - if (this._frameAssetsQueue) { - if (traceLoaderOption.value) { - this._writer.writeTimeLn("Suspending frame execution until all of its assets are resolved."); - } - var self = this; - suspendUntil = Promise.all(this._frameAssetsQueue).then(function () { - self._enqueueFrame(data); - self._frameAssetsQueue = null; - }); - } else { - this._enqueueFrame(data); - } - } else if (data.type === 'image') { - this._commitImage(data); - } else if (data.type === 'abc') { - if (loaderInfo._allowCodeExecution) { - var appDomain = AVM2.instance.applicationDomain; - var abc = new AbcFile(data.data, data.name); - if (data.flags) { - // kDoAbcLazyInitializeFlag = 1 Indicates that the ABC block should not be executed - // immediately. - appDomain.loadAbc(abc); - } else { - if (this._frameAssetsQueue) { - suspendUntil = Promise.all(this._frameAssetsQueue).then(function () { - appDomain.executeAbc(abc); - }); - } else { - appDomain.executeAbc(abc); - } - } - } - } - break; - } - return suspendUntil; + loadBytes(data: flash.utils.ByteArray, context?: LoaderContext) { + this.close(); + // TODO: properly coerce object arguments to their types. + // In case this is the initial root loader, we won't have a loaderInfo object. That should + // only happen in the inspector when a file is loaded from a Blob, though. + this._contentLoaderInfo._url = (this.loaderInfo ? this.loaderInfo._url : '') + + '/[[DYNAMIC]]/' + (++Loader._embeddedContentLoadCount); + this._applyLoaderContext(context); + this._loadingType = LoadingType.Bytes; + this._fileLoader = new FileLoader(this); + this._queuedLoadUpdates = []; + // Just passing in the bytes won't do, because the buffer can contain slop at the end. + this._fileLoader.loadBytes(new Uint8Array((data).bytes, 0, data.length)); + + Loader._loadQueue.push(this); } - private _initAvm1(): Promise { - var contentLoaderInfo: LoaderInfo = this._contentLoaderInfo; - // Only the outermost AVM1 SWF gets an AVM1Context. SWFs loaded into it share that context. - if (this.loaderInfo && this.loaderInfo._avm1Context) { - contentLoaderInfo._avm1Context = this.loaderInfo._avm1Context; - return null; + close(): void { + if (!this._fileLoader) { + return; } - return AVM2.instance.loadAVM1().then(function() { - var swfVersion = contentLoaderInfo.swfVersion; - contentLoaderInfo._avm1Context = Shumway.AVM1.AVM1Context.create(swfVersion); - }); + this._fileLoader.abortLoad(); + this._fileLoader = null; } - private _commitAsset(data: any): void { - var loaderInfo = this._contentLoaderInfo; - var symbolId = data.id; - var symbol; - if (data.updates) { - var updates = data.updates; - symbol = loaderInfo.getSymbolById(symbolId); - if (updates.scale9Grid) { - symbol.scale9Grid = Bounds.FromUntyped(updates.scale9Grid); - } + _unload(stopExecution: boolean, gc: boolean): void { + if (this._loadStatus < LoadStatus.Initialized) { + this._loadStatus = LoadStatus.Unloaded; return; } - switch (data.type) { - case 'shape': - symbol = Timeline.ShapeSymbol.FromData(data, loaderInfo); - break; - case 'morphshape': - symbol = Timeline.MorphShapeSymbol.FromData(data, loaderInfo); - break; - case 'image': - var bitmapSymbol = symbol = Timeline.BitmapSymbol.FromData(data); - if (bitmapSymbol.type === ImageType.PNG || - bitmapSymbol.type === ImageType.GIF || - bitmapSymbol.type === ImageType.JPEG) { - if (!this._frameAssetsQueue) { - this._frameAssetsQueue = []; - } - this._frameAssetsQueue.push(new Promise(function (resolve) { - getPlayer().decodeImage(bitmapSymbol, resolve); - })); - } - break; - case 'label': - case 'text': - symbol = Timeline.TextSymbol.FromTextData(data); - break; - case 'button': - symbol = Timeline.ButtonSymbol.FromData(data, loaderInfo); - break; - case 'sprite': - symbol = Timeline.SpriteSymbol.FromData(data, loaderInfo); - break; - case 'font': - symbol = Timeline.FontSymbol.FromData(data); - var font = flash.text.Font.initializeFrom(symbol); - flash.text.Font.instanceConstructorNoInitialize.call(font); - - if (font.fontType === flash.text.FontType.DEVICE) { - break; - } - - getPlayer().registerFont(font); + this.close(); + this._content = null; + this._contentLoaderInfo._loader = null; + this._loadStatus = LoadStatus.Unloaded; + this.dispatchEvent(events.Event.getInstance(events.Event.UNLOAD)); + } + unload() { + this._unload(false, false); + } + unloadAndStop(gc: boolean) { + // TODO: remove all DisplayObjects originating from the unloaded SWF from all lists and stop + // them. + this._unload(true, !!gc); + } - // For non-Firefox browsers, we have to wait until font is "loaded" - if (typeof navigator !== 'undefined' && - navigator.userAgent.indexOf('Firefox') < 0) { - if (!this._frameAssetsQueue) { - this._frameAssetsQueue = []; - } - this._frameAssetsQueue.push(new Promise(function (resolve) { - setTimeout(resolve, 400 /* ms */); - })); + private _applyLoaderContext(context: LoaderContext) { + var parameters = {}; + if (context && context.parameters) { + var contextParameters = context.parameters; + for (var key in contextParameters) { + var value = contextParameters[key]; + if (!isString(value)) { + throwError('IllegalOperationError', Errors.ObjectWithStringsParamError, + 'LoaderContext.parameters'); } - break; - case 'sound': - symbol = Timeline.SoundSymbol.FromData(data); - break; - case 'binary': - symbol = Timeline.BinarySymbol.FromData(data); - break; + parameters[key] = value; + } + } + if (context && context.applicationDomain) { + var domain = new ApplicationDomain(ApplicationDomain.currentDomain); + this._contentLoaderInfo._applicationDomain = domain; + } + this._contentLoaderInfo._parameters = parameters; + } + + onLoadOpen(file: any) { + if (file instanceof SWFFile) { + this._contentLoaderInfo.setFile(file); + } else { + release || assert(file instanceof ImageFile); + this._contentLoaderInfo._bytesTotal = file.bytesTotal; } - release || assert (symbol, "Unknown symbol type."); - loaderInfo.registerSymbol(symbol); } - /** - * Enqueues a frame for addition to the target Sprite/MovieClip. - * - * Frames aren't immediately committed because doing so also enqueues execution of - * constructors of any contained timeline children. In order to preserve the correct - * order of their execution relative to when Loader events are dispatched and the - * frame event cycle is run, we just enqueue them here. - * - * The only exception is the first frame of the main root's timeline. That is committed - * immediately as it triggers all code execution in the first place: the event loop - * isn't run before. - */ - private _enqueueFrame(data: any): void { - if (this === Loader.getRootLoader()) { - var isFirstFrame = !this._content; - this._commitFrame(data); - - // TODO: the comment above says that only the root's first frame is committed eagerly. - // That, however, breaks content that has something like `nextFrame()` in its first frame. - // It's not entirely clear how to fix this issue, really. - if (isFirstFrame) { - Loader.runtimeStartTime = Date.now(); - this._codeExecutionPromise.resolve(undefined); + onLoadProgress(update: LoadProgressUpdate) { + var file = this._contentLoaderInfo._file; + if (file instanceof SWFFile) { + this._queuedLoadUpdates.push(update); + if (this._content || this !== Loader.getRootLoader()) { + return; + } + if (file.useAVM1 && !AVM2.instance.avm1Loaded) { + var self = this; + this._initAvm1().then(function () { + self.onFileStartupReady(); + }); + } else { + this.onFileStartupReady(); } } else { - Loader._commitFrameQueue.push({loader: this, data: data}); + this._contentLoaderInfo.bytesLoaded = update.bytesLoaded; } } - private _commitFrame(data: any) { + private _applyLoadUpdate(update: LoadProgressUpdate) { var loaderInfo = this._contentLoaderInfo; + var file = loaderInfo._file; - // HACK: Someone should figure out how to set the color on the stage better. - if (data.bgcolor !== undefined) { - loaderInfo._colorRGBA = data.bgcolor; - } - - if (data.symbolClasses) { - var symbolClasses = data.symbolClasses; + if (loaderInfo._allowCodeExecution) { var appDomain = AVM2.instance.applicationDomain; - for (var i = 0; i < symbolClasses.length; i++) { - var asset = symbolClasses[i]; - if (loaderInfo._allowCodeExecution) { - var symbol = loaderInfo.getSymbolById(asset.symbolId); - if (!symbol) { - warning ("Symbol " + asset.symbolId + " is not defined."); - continue; + + var abcBlocksLoaded = file.abcBlocks.length; + var abcBlocksLoadedDelta = abcBlocksLoaded - loaderInfo._abcBlocksLoaded; + if (abcBlocksLoadedDelta > 0) { + for (var i = loaderInfo._abcBlocksLoaded; i < abcBlocksLoaded; i++) { + var abcBlock = file.abcBlocks[i]; + var abc = new AbcFile(abcBlock.data, abcBlock.name); + if (abcBlock.flags) { + // kDoAbcLazyInitializeFlag = 1 Indicates that the ABC block should not be executed + // immediately. + appDomain.loadAbc(abc); + } else { + // TODO: probably delay execution until playhead reaches the frame. + appDomain.executeAbc(abc); } - var symbolClass = appDomain.getClass(asset.className); - symbolClass.defaultInitializerArgument = symbol; - symbol.symbolClass = symbolClass; } + loaderInfo._abcBlocksLoaded = abcBlocksLoaded; } - } - if (data.exports && loaderInfo._actionScriptVersion === ActionScriptVersion.ACTIONSCRIPT2) { - var exports = data.exports; - for (var i = 0; i < exports.length; i++) { - var asset = exports[i]; - var symbol = loaderInfo.getSymbolById(asset.symbolId); - if (!symbol) { - warning ("Symbol " + asset.symbolId + " is not defined."); - continue; + var mappedSymbolsLoaded = file.symbolClassesList.length; + var mappedSymbolsLoadedDelta = mappedSymbolsLoaded - loaderInfo._mappedSymbolsLoaded; + if (mappedSymbolsLoadedDelta > 0) { + for (var i = loaderInfo._mappedSymbolsLoaded; i < mappedSymbolsLoaded; i++) { + var symbolMapping = file.symbolClassesList[i]; + var symbolClass = appDomain.getClass(symbolMapping.className); + Object.defineProperty(symbolClass, "defaultInitializerArgument", + {get: loaderInfo.getSymbolResolver(symbolClass, symbolMapping.id), + configurable: true}); } - loaderInfo._avm1Context.addAsset(asset.className, asset.symbolId, symbol); + loaderInfo._mappedSymbolsLoaded = mappedSymbolsLoaded; } } - var rootSymbol = loaderInfo.getSymbolById(0); - var documentClass = rootSymbol.symbolClass; - var frames = rootSymbol.frames; - var frameIndex = frames.length; - - var frame = new Timeline.FrameDelta(loaderInfo, data.commands); - var repeat = data.repeat; - while (repeat--) { - frames.push(frame); + var fontsLoaded = file.fonts.length; + var fontsLoadedDelta = fontsLoaded - loaderInfo._fontsLoaded; + if (fontsLoadedDelta > 0) { + for (var i = loaderInfo._fontsLoaded; i < fontsLoaded; i++) { + flash.text.Font.registerLazyFont(file.fonts[i], loaderInfo); + } + loaderInfo._fontsLoaded = fontsLoaded; } + var rootSymbol = loaderInfo.getRootSymbol(); + loaderInfo.bytesLoaded = update.bytesLoaded; + var framesLoadedDelta = file.framesLoaded - rootSymbol.frames.length; + if (framesLoadedDelta === 0) { + return; + } var root = this._content; if (!root) { - root = documentClass.initializeFrom(rootSymbol); - // The root object gets a default of 'rootN', which doesn't use up a DisplayObject instance - // ID. - flash.display.DisplayObject._instanceID--; - root._name = 'root1'; // TODO: make this increment for subsequent roots. - - if (MovieClip.isType(root)) { - var mc = root; - if (data.sceneData) { - var scenes = data.sceneData.scenes; - for (var i = 0, n = scenes.length; i < n; i++) { - var sceneInfo = scenes[i]; - var offset = sceneInfo.offset; - var endFrame = i < n - 1 ? scenes[i + 1].offset : rootSymbol.numFrames; - mc.addScene(sceneInfo.name, [], offset, endFrame - offset); - } - var labels = data.sceneData.labels; - for (var i = 0; i < labels.length; i++) { - var labelInfo = labels[i]; - mc.addFrameLabel(labelInfo.name, labelInfo.frame + 1); - } - } else { - mc.addScene('Scene 1', [], 0, rootSymbol.numFrames); - } + if (Loader.getRootLoader() === this) { + // For the root loader, make sure that the real bytesLoaded value is set before any + // script runs. + loaderInfo._bytesLoaded = update.bytesLoaded; } - - root._loaderInfo = loaderInfo; - if (loaderInfo._actionScriptVersion === ActionScriptVersion.ACTIONSCRIPT2) { - root = this._content = this._initAvm1Root(root); - } else { - this._content = root; - } - this.addTimelineObjectAtDepth(this._content, 0); + root = this.createContentRoot(loaderInfo.getRootSymbol(), + file.sceneAndFrameLabelData); } - // For AVM1 SWFs directly loaded into AVM2 ones (or as the top-level SWF), unwrap the // contained MovieClip here to correctly initialize frame data. if (AVM1Movie.isType(root)) { root = root._children[0]; } + var rootSprite = root; + for (var i = 0; i < framesLoadedDelta; i++) { + var frameInfo = loaderInfo.getFrame(null, rootSymbol.frames.length); + rootSprite._addFrame(frameInfo); + } + } + onLoadComplete() { + var file = this._fileLoader._file; + if (file instanceof ImageFile) { + var bitmapData = flash.display.BitmapData.initializeFrom(file); + flash.display.BitmapData.instanceConstructorNoInitialize.call(bitmapData); + this._content = new flash.display.Bitmap(bitmapData); + this._contentLoaderInfo._width = this._content.width; + this._contentLoaderInfo._height = this._content.height; + this.addTimelineObjectAtDepth(this._content, 0); + } + } + onLoadError() { + // Go away, TSLint. + } - if (MovieClip.isType(root)) { - var rootMovie: MovieClip = root; - - if (data.labelName) { - rootMovie.addFrameLabel(data.labelName, frameIndex + 1); + private onFileStartupReady() { + // The first frames have been loaded, kick off event loop. + this._startPromise.resolve(null); + if (this === Loader.getRootLoader()) { + if (!release && traceLoaderOption.value) { + console.log("Initial frames loaded, starting main runtime event loop."); } + Loader.runtimeStartTime = Date.now(); + } + // The very first update is applied immediately, as that creates the root content, + // which the player expects to exist at this point. + this._applyLoadUpdate(this._queuedLoadUpdates.shift()); + } - if (loaderInfo._actionScriptVersion === ActionScriptVersion.ACTIONSCRIPT2) { - avm1lib.getAVM1Object(root).addFrameActionBlocks(frameIndex, data); - } + private createContentRoot(symbol: Timeline.SpriteSymbol, sceneData) { + var root = symbol.symbolClass.initializeFrom(symbol); + // The root object gets a default of 'rootN', which doesn't use up a DisplayObject instance + // ID. + flash.display.DisplayObject._instanceID--; + root._name = 'root1'; // TODO: make this increment for subsequent roots. - if (data.soundStream) { - rootMovie._initSoundStream(data.soundStream); - } - if (data.soundStreamBlock) { - rootMovie._addSoundStreamBlock(frameIndex + 1, data.soundStreamBlock); + if (MovieClip.isType(root)) { + var mc = root; + if (sceneData) { + var scenes = sceneData.scenes; + for (var i = 0, n = scenes.length; i < n; i++) { + var sceneInfo = scenes[i]; + var offset = sceneInfo.offset; + var endFrame = i < n - 1 ? scenes[i + 1].offset : symbol.numFrames; + mc.addScene(sceneInfo.name, [], offset, endFrame - offset); + } + var labels = sceneData.labels; + for (var i = 0; i < labels.length; i++) { + var labelInfo = labels[i]; + mc.addFrameLabel(labelInfo.name, labelInfo.frame + 1); + } + } else { + mc.addScene('Scene 1', [], 0, symbol.numFrames); } } + + var loaderInfo = this._contentLoaderInfo; + root._loaderInfo = loaderInfo; + if (loaderInfo._actionScriptVersion === ActionScriptVersion.ACTIONSCRIPT2) { + root = this._content = this._initAvm1Root(root); + } else { + this._content = root; + } + this.addTimelineObjectAtDepth(this._content, 0); + return root; + } + + private _initAvm1(): Promise { + var contentLoaderInfo: LoaderInfo = this._contentLoaderInfo; + // Only the outermost AVM1 SWF gets an AVM1Context. SWFs loaded into it share that context. + if (this.loaderInfo && this.loaderInfo._avm1Context) { + contentLoaderInfo._avm1Context = this.loaderInfo._avm1Context; + return null; + } + return AVM2.instance.loadAVM1().then(function() { + contentLoaderInfo._avm1Context = Shumway.AVM1.AVM1Context.create(contentLoaderInfo); + }); } /** @@ -665,7 +535,6 @@ module Shumway.AVM2.AS.flash.display { var avm1Context = this._contentLoaderInfo._avm1Context; avm1Context.root = as2Object; - as2Object.context = avm1Context; root.addEventListener('frameConstructed', avm1Context.flushPendingScripts.bind(avm1Context), false, @@ -685,141 +554,5 @@ module Shumway.AVM2.AS.flash.display { return avm1Movie; } - - private _commitImage(data: any): void { - var symbol = Timeline.BitmapSymbol.FromData(data.props); - var b = flash.display.BitmapData.initializeFrom(symbol); - flash.display.BitmapData.instanceConstructorNoInitialize.call(b); - - this._content = new flash.display.Bitmap(b); - this.addTimelineObjectAtDepth(this._content, 0); - - var loaderInfo = this._contentLoaderInfo; - loaderInfo._width = symbol.width; - loaderInfo._height = symbol.height; - - // Complete load process manually here to avoid any additional progress events to be fired. - this._loadStatus = LoadStatus.Initialized; - loaderInfo.dispatchEvent(events.Event.getInstance(events.Event.INIT)); - this._loadStatus = LoadStatus.Complete; - loaderInfo.dispatchEvent(events.Event.getInstance(events.Event.COMPLETE)); - this._loadStatus = LoadStatus.Complete; - } - - get content(): flash.display.DisplayObject { - if (this._loadStatus === LoadStatus.Unloaded) { - return null; - } - return this._content; - } - - get contentLoaderInfo(): flash.display.LoaderInfo { - return this._contentLoaderInfo; - } - - close(): void { - if (!this._fileLoader) { - return; - } - this._fileLoader.abortLoad(); - this._fileLoader = null; - } - - _unload(stopExecution: boolean, gc: boolean): void { - if (this._loadStatus < LoadStatus.Initialized) { - return; - } - this.close(); - this._content = null; - this._contentLoaderInfo._loader = null; - this._loadStatus = LoadStatus.Unloaded; - this.dispatchEvent(events.Event.getInstance(events.Event.UNLOAD)); - } - unload() { - this._unload(false, false); - } - unloadAndStop(gc: boolean) { - this._unload(true, !!gc); - } - - _getJPEGLoaderContextdeblockingfilter(context: flash.system.LoaderContext): number { - if (flash.system.JPEGLoaderContext.isType(context)) { - return (context).deblockingFilter; - } - return 0.0; - } - - get uncaughtErrorEvents(): events.UncaughtErrorEvents { - return this._uncaughtErrorEvents; - } - - load(request: flash.net.URLRequest, context?: LoaderContext): void { - this._contentLoaderInfo._url = request.url; - this._applyLoaderContext(context, request); - this._loadingType = LoadingType.External; - this.close(); - this._fileLoader = new FileLoader(this); - this._fileLoader.loadFile(request._toFileRequest()); - - Loader._loadQueue.push(this); - - if (this === Loader.getRootLoader()) { - if (!this._contentLoaderInfo._allowCodeExecution) { - this._codeExecutionPromise.reject('Disabled by _allowCodeExecution'); - } - if (!this._waitForInitialData) { - this._initialDataLoaded.resolve(undefined); - } - } - } - - onLoadOpen() { - // Go away, TSLint. - } - onLoadProgress(data) { - this._commitData(data); - } - onLoadComplete() { - // Go away, TSLint. - } - onLoadError() { - // Go away, TSLint. - } - - loadBytes(data: flash.utils.ByteArray, context?: LoaderContext): void - { - // TODO: properly coerce object arguments to their types. - // In case this is the initial root loader, we won't have a loaderInfo object. That should - // only happen in the inspector when a file is loaded from a Blob, though. - this._contentLoaderInfo._url = (this.loaderInfo ? this.loaderInfo._url : '') + - '/[[DYNAMIC]]/' + (++Loader._embeddedContentLoadCount); - this._applyLoaderContext(context, null); - this._loadingType = LoadingType.Bytes; - this.close(); - this._fileLoader = new FileLoader(this); - this._fileLoader.loadBytes((data).bytes); - - Loader._loadQueue.push(this); - } - - private _applyLoaderContext(context: LoaderContext, request: flash.net.URLRequest) { - var parameters = {}; - if (context && context.parameters) { - var contextParameters = context.parameters; - for (var key in contextParameters) { - var value = contextParameters[key]; - if (!isString(value)) { - throwError('IllegalOperationError', Errors.ObjectWithStringsParamError, - 'LoaderContext.parameters'); - } - parameters[key] = value; - } - } - if (context && context.applicationDomain) { - this._contentLoaderInfo._applicationDomain = - new ApplicationDomain(ApplicationDomain.currentDomain); - } - this._contentLoaderInfo._parameters = parameters; - } } } diff --git a/src/flash/display/LoaderInfo.ts b/src/flash/display/LoaderInfo.ts index 246c252ca7..27d8dc7722 100644 --- a/src/flash/display/LoaderInfo.ts +++ b/src/flash/display/LoaderInfo.ts @@ -21,6 +21,8 @@ module Shumway.AVM2.AS.flash.display { import asCoerceString = Shumway.AVM2.Runtime.asCoerceString; import ActionScriptVersion = flash.display.ActionScriptVersion; + import SWFFile = Shumway.SWF.SWFFile; + import SWFFrame = Shumway.SWF.SWFFrame; export class LoaderInfo extends flash.events.EventDispatcher { @@ -37,18 +39,18 @@ module Shumway.AVM2.AS.flash.display { static instanceSymbols: string [] = null; // ["parameters", "uncaughtErrorEvents", "dispatchEvent"]; constructor () { - false && super(undefined); + false && super(); flash.events.EventDispatcher.instanceConstructorNoInitialize.call(this); this._loaderURL = ''; this._url = ''; + this._file = null; this._isURLInaccessible = false; this._bytesLoaded = 0; - this._bytesLoadedChanged = false; + this._newBytesLoaded = 0; this._bytesTotal = 0; this._applicationDomain = null; this._swfVersion = 9; this._actionScriptVersion = ActionScriptVersion.ACTIONSCRIPT3; - release || assert (this._actionScriptVersion); this._frameRate = 24; this._parameters = null; this._width = 0; @@ -66,11 +68,31 @@ module Shumway.AVM2.AS.flash.display { this._uncaughtErrorEvents = null; this._allowCodeExecution = true; this._dictionary = []; + this._abcBlocksLoaded = 0; + this._mappedSymbolsLoaded = 0; + this._fontsLoaded = 0; this._avm1Context = null; this._colorRGBA = 0xFFFFFFFF; } + setFile(file: SWFFile) { + release || assert(!this._file); + this._file = file; + // TODO: remove these duplicated fields from LoaderInfo. + this._bytesTotal = file.bytesTotal; + this._swfVersion = file.swfVersion; + this._frameRate = file.frameRate; + var bbox = file.bounds; + this._width = bbox.xMax - bbox.xMin; + this._height = bbox.yMax - bbox.yMin; + this._colorRGBA = file.backgroundColor; + + if (!file.attributes || !file.attributes.doAbc) { + this._actionScriptVersion = ActionScriptVersion.ACTIONSCRIPT2; + } + } + uncaughtErrorEvents: flash.events.UncaughtErrorEvents; static getLoaderInfoByDefinition(object: Object): flash.display.LoaderInfo { @@ -80,9 +102,10 @@ module Shumway.AVM2.AS.flash.display { _loaderURL: string; _url: string; + _file: SWFFile; _isURLInaccessible: boolean; _bytesLoaded: number /*uint*/; - _bytesLoadedChanged: boolean; + _newBytesLoaded: number; _bytesTotal: number /*uint*/; _applicationDomain: flash.system.ApplicationDomain; _swfVersion: number /*uint*/; @@ -101,6 +124,9 @@ module Shumway.AVM2.AS.flash.display { _loader: flash.display.Loader; _content: flash.display.DisplayObject; _bytes: flash.utils.ByteArray; + _abcBlocksLoaded: number; + _mappedSymbolsLoaded: number; + _fontsLoaded: number; _uncaughtErrorEvents: flash.events.UncaughtErrorEvents; /** @@ -134,11 +160,10 @@ module Shumway.AVM2.AS.flash.display { return this._bytesLoaded; } set bytesLoaded(value: number /*uint*/) { - if (value === this._bytesLoaded) { + if (value === this._newBytesLoaded) { return; } - this._bytesLoaded = value; - this._bytesLoadedChanged = true; + this._newBytesLoaded = value; } get bytesTotal(): number /*uint*/ { @@ -238,12 +263,109 @@ module Shumway.AVM2.AS.flash.display { notImplemented("public flash.display.LoaderInfo::_setUncaughtErrorEvents"); return; } - registerSymbol(symbol: Shumway.Timeline.Symbol): void { - this._dictionary[symbol.id] = symbol; + getSymbolResolver(classDefinition: ASClass, symbolId: number): () => any { + return this.resolveClassSymbol.bind(this, classDefinition, symbolId); } getSymbolById(id: number): Shumway.Timeline.Symbol { - return this._dictionary[id] || null; + var symbol = this._dictionary[id]; + if (symbol) { + return symbol; + } + var data = this._file.getSymbol(id); + if (!data) { + // It's entirely valid not to have symbols defined. + Debug.warning("Unknown symbol requested: " + id); + return null; + } + // TODO: replace this switch with a table lookup. + switch (data.type) { + case 'shape': + symbol = Timeline.ShapeSymbol.FromData(data, this); + break; + case 'morphshape': + symbol = Timeline.MorphShapeSymbol.FromData(data, this); + break; + case 'image': + symbol = Timeline.BitmapSymbol.FromData(data.definition); + break; + case 'label': + symbol = Timeline.TextSymbol.FromLabelData(data, this); + break; + case 'text': + symbol = Timeline.TextSymbol.FromTextData(data, this); + break; + case 'button': + symbol = Timeline.ButtonSymbol.FromData(data, this); + break; + case 'sprite': + symbol = Timeline.SpriteSymbol.FromData(data, this); + break; + case 'font': + // Fonts are eagerly parsed and have their data in `definition`. + if (data.definition) { + data = data.definition; + } + symbol = Timeline.FontSymbol.FromData(data); + var font = flash.text.Font.initializeFrom(symbol); + flash.text.Font.instanceConstructorNoInitialize.call(font); + break; + case 'sound': + symbol = Timeline.SoundSymbol.FromData(data); + break; + case 'binary': + symbol = Timeline.BinarySymbol.FromData(data); + break; + } + release || assert(symbol, "Unknown symbol type " + data.type); + this._dictionary[id] = symbol; + return symbol; + } + + getRootSymbol(): Timeline.SpriteSymbol { + var symbol = this._dictionary[0]; + if (!symbol) { + symbol = new Timeline.SpriteSymbol({id: 0, className: this._file.symbolClassesMap[0]}, this); + symbol.isRoot = true; + if (this._actionScriptVersion === ActionScriptVersion.ACTIONSCRIPT2) { + symbol.isAVM1Object = true; + symbol.avm1Context = this._avm1Context; + } + symbol.numFrames = this._file.frameCount; + this._dictionary[0] = symbol; + } + return symbol; + } + + // TODO: deltas should be computed lazily when they're first needed, and this removed. + getFrame(sprite: {frames: SWFFrame[]}, index: number) { + var file = this._file; + if (!sprite) { + sprite = file; + } + var frame = sprite.frames[index]; + return { + labelName: frame.labelName, + soundStreamHead: frame.soundStreamHead, + soundStreamBlock: frame.soundStreamBlock, + actionBlocks: frame.actionBlocks, + initActionBlocks: frame.initActionBlocks, + exports: frame.exports, + frameDelta: new Timeline.FrameDelta(this, frame.displayListCommands) + }; + } + + // TODO: To prevent leaking LoaderInfo instances, those instances should be stored weakly, + // with support for retrieving the instances based on a numeric id, which would be passed here. + private resolveClassSymbol(classDefinition: ASClass, symbolId: number) { + var symbol = this.getSymbolById(symbolId); + if (!symbol) { + Debug.warning("Attempt to resolve symbol for AVM2 class failed: Symbol " + + symbolId + " not found."); + } else { + Object.defineProperty(classDefinition, "defaultInitializerArgument", {value: symbol}); + return symbol; + } } } } diff --git a/src/flash/display/MovieClip.ts b/src/flash/display/MovieClip.ts index fd7ce85340..4f75830b60 100644 --- a/src/flash/display/MovieClip.ts +++ b/src/flash/display/MovieClip.ts @@ -63,8 +63,10 @@ module Shumway.AVM2.AS.flash.display { this._soundStream = new MovieClipSoundStream(streamInfo, this._mc); } - addSoundStreamBlock(frameNum: number, streamBlock: any) { - this._soundStream.appendBlock(frameNum, streamBlock); + addSoundStreamBlock(frameNum: number, streamBlock: Uint8Array) { + if (this._soundStream) { + this._soundStream.appendBlock(frameNum, streamBlock); + } } private _startSounds(frameNum) { @@ -160,7 +162,6 @@ module Shumway.AVM2.AS.flash.display { } self._frames = symbol.frames; if (symbol.isAVM1Object) { - self._mouseEnabled = false; if (symbol.frameScripts) { var avm1MovieClip = avm1lib.getAVM1Object(this); avm1MovieClip.context = symbol.avm1Context; @@ -223,6 +224,36 @@ module Shumway.AVM2.AS.flash.display { Sprite.instanceConstructorNoInitialize.call(this); } + _addFrame(frameInfo: any) { + var spriteSymbol = this._symbol; + var frames = spriteSymbol.frames; + frames.push(frameInfo.frameDelta); + if (frameInfo.labelName) { + // Frame indices are 1-based, so use frames.length after pushing the frame. + this.addFrameLabel(frameInfo.labelName, frames.length); + } + if (frameInfo.soundStreamHead) { + this._initSoundStream(frameInfo.soundStreamHead); + } + if (frameInfo.soundStreamBlock) { + // Frame indices are 1-based, so use frames.length after pushing the frame. + this._addSoundStreamBlock(frames.length, frameInfo.soundStreamBlock); + } + if (spriteSymbol.isAVM1Object) { + avm1lib.getAVM1Object(this).addFrameActionBlocks(frames.length - 1, frameInfo); + if (frameInfo.exports) { + var exports = frameInfo.exports; + for (var i = 0; i < exports.length; i++) { + var asset = exports[i]; + spriteSymbol.avm1Context.addAsset(asset.className, asset.symbolId, null); + } + } + } + if (frames.length === 1) { + this._initializeChildren(frames[0]); + } + } + _initFrame(advance: boolean) { if (advance) { if (this.buttonMode) { @@ -387,10 +418,10 @@ module Shumway.AVM2.AS.flash.display { var legacyMode = MovieClip.frameNavigationModel === FrameNavigationModel.SWF1 || MovieClip.frameNavigationModel === FrameNavigationModel.SWF9; var label = scene.getLabelByName(frame, legacyMode); - if (!label && legacyMode) { - return; // noop for SWF9 and below - } if (!label) { + if (legacyMode) { + return; // noop for SWF9 and below + } throwError('ArgumentError', Errors.FrameLabelNotFoundError, frame, sceneName); } frameNum = label.frame; @@ -465,7 +496,7 @@ module Shumway.AVM2.AS.flash.display { var childDepth = child._depth; if (childDepth) { // We need to scan all past states to check if we can keep the child. - var state: Timeline.AnimationState = undefined; + var state: Timeline.AnimationState; for (var j = nextFrame - 1; j >= 0 && !state; j--) { state = frames[j].stateAtDepth[childDepth]; } @@ -490,6 +521,11 @@ module Shumway.AVM2.AS.flash.display { for (var depth in stateAtDepth) { var child = this.getTimelineObjectAtDepth(depth | 0); var state = stateAtDepth[depth]; + // Eagerly create the symbol here, because it's needed in the canBeAnimated check below. + if (state && state.symbolId > -1 && !state.symbol) { + var ownSymbol = this._symbol; + state.symbol = ownSymbol.loaderInfo.getSymbolById(state.symbolId); + } if (child) { if (state && state.canBeAnimated(child)) { if (state.symbol && !state.symbol.dynamic) { @@ -504,11 +540,11 @@ module Shumway.AVM2.AS.flash.display { } this._removeAnimatedChild(child); } - if (state && state.symbol) { - var character = DisplayObject.createAnimatedDisplayObject(state, false); + if (state && state.symbolId > -1) { + var character = this.createAnimatedDisplayObject(state, false); this.addTimelineObjectAtDepth(character, state.depth); if (state.symbol.isAVM1Object) { - this._initAvm1Bindings(character, state); + avm1lib.initializeAVM1Object(character, state); } } } diff --git a/src/flash/display/MovieClipSoundStream.ts b/src/flash/display/MovieClipSoundStream.ts index e3605c240d..0c36f8a42e 100644 --- a/src/flash/display/MovieClipSoundStream.ts +++ b/src/flash/display/MovieClipSoundStream.ts @@ -86,6 +86,7 @@ module Shumway.AVM2.AS.flash.display { private sourceBuffer; private rawFrames: Array; + private decode: (block: Uint8Array) => SWF.Parser.DecodedSound; private decoderPosition: number; private decoderSession: MP3DecoderSession; @@ -94,8 +95,9 @@ module Shumway.AVM2.AS.flash.display { private expectedFrame: number; private waitFor: number; - public constructor(streamInfo: any, movieClip: MovieClip) { + public constructor(streamInfo: SWF.Parser.SoundStream, movieClip: MovieClip) { this.movieClip = movieClip; + this.decode = streamInfo.decode; this.data = { sampleRate: streamInfo.sampleRate, channels: streamInfo.channels @@ -110,7 +112,8 @@ module Shumway.AVM2.AS.flash.display { syncTime(element, movieClip); if (element.canPlayType(MP3_MIME_TYPE)) { this.element = element; - if (typeof MediaSource !== 'undefined') { + if (typeof MediaSource !== 'undefined' && + (MediaSource).isTypeSupported(MP3_MIME_TYPE)) { var mediaSource = new MediaSource(); mediaSource.addEventListener('sourceopen', openMediaSource.bind(null, this, mediaSource)); @@ -140,27 +143,26 @@ module Shumway.AVM2.AS.flash.display { } } - public appendBlock(frameNum: number, streamBlock: any) { + public appendBlock(frameNum: number, streamBlock: Uint8Array) { + var decodedBlock = this.decode(streamBlock); var streamPosition = this.position; - this.seekIndex[frameNum] = streamPosition + - streamBlock.seek * this.data.channels; - this.position = streamPosition + - streamBlock.samplesCount * this.data.channels; + this.seekIndex[frameNum] = streamPosition + decodedBlock.seek * this.data.channels; + this.position = streamPosition + decodedBlock.samplesCount * this.data.channels; if (this.sourceBuffer) { - this.sourceBuffer.appendBuffer(streamBlock.data); + this.sourceBuffer.appendBuffer(decodedBlock.data); return; } if (this.rawFrames) { - this.rawFrames.push(streamBlock.data); + this.rawFrames.push(decodedBlock.data); return; } var decoderSession = this.decoderSession; if (decoderSession) { - decoderSession.pushAsync(streamBlock.data); + decoderSession.pushAsync(decodedBlock.data); } else { - this.data.pcm.set(streamBlock.pcm, streamPosition); + this.data.pcm.set(decodedBlock.pcm, streamPosition); } } diff --git a/src/flash/display/SimpleButton.ts b/src/flash/display/SimpleButton.ts index 8266b53428..fa5a058cf9 100644 --- a/src/flash/display/SimpleButton.ts +++ b/src/flash/display/SimpleButton.ts @@ -46,16 +46,16 @@ module Shumway.AVM2.AS.flash.display { if (symbol) { if (symbol.upState) { - self._upState = DisplayObject.createAnimatedDisplayObject(symbol.upState, true); + self._upState = self.createAnimatedDisplayObject(symbol.upState, true); } if (symbol.overState) { - self._overState = DisplayObject.createAnimatedDisplayObject(symbol.overState, true); + self._overState = self.createAnimatedDisplayObject(symbol.overState, true); } if (symbol.downState) { - self._downState = DisplayObject.createAnimatedDisplayObject(symbol.downState, true); + self._downState = self.createAnimatedDisplayObject(symbol.downState, true); } if (symbol.hitTestState) { - self._hitTestState = DisplayObject.createAnimatedDisplayObject(symbol.hitTestState, true); + self._hitTestState = self.createAnimatedDisplayObject(symbol.hitTestState, true); } } }; diff --git a/src/flash/display/Sprite.ts b/src/flash/display/Sprite.ts index fdf50dfc1e..362f568659 100644 --- a/src/flash/display/Sprite.ts +++ b/src/flash/display/Sprite.ts @@ -42,11 +42,10 @@ module Shumway.AVM2.AS.flash.display { if (symbol.isRoot) { self._root = self; } - if (symbol.numFrames) { - release || assert (symbol.frames.length >= 1, "Sprites have at least one frame."); - var frame = symbol.frames[0]; - release || assert (frame, "Initial frame is not defined."); - self._initializeChildren(frame); + if (symbol.numFrames && symbol.frames.length > 0) { + // For a SWF's root symbol, all frames are added after initialization, with + // _initializeChildren called after the first frame is added. + self._initializeChildren(symbol.frames[0]); } } }; @@ -75,52 +74,25 @@ module Shumway.AVM2.AS.flash.display { _hitTarget: flash.display.Sprite; - private _initializeChildren(frame: Timeline.FrameDelta): void { - for (var depth in frame.stateAtDepth) { - var state = frame.stateAtDepth[depth]; - if (state) { - var character = DisplayObject.createAnimatedDisplayObject(state, false); - this.addTimelineObjectAtDepth(character, state.depth); - if (state.symbol.isAVM1Object) { - this._initAvm1Bindings(character, state); - } - } + _addFrame(frameInfo: any) { + var frames = (this._symbol).frames; + frames.push(frameInfo.frameDelta); + if (frames.length === 1) { + this._initializeChildren(frames[0]); } } - _initAvm1Bindings(instance: DisplayObject, state: Shumway.Timeline.AnimationState) { - var instanceAVM1 = avm1lib.getAVM1Object(instance); - assert(instanceAVM1); - - if (state.variableName) { - instanceAVM1.asSetPublicProperty('variable', state.variableName); - } - - var events = state.events; - if (events) { - var eventsBound = []; - for (var i = 0; i < events.length; i++) { - var event = events[i]; - var eventNames = event.eventNames; - var fn = event.handler.bind(instance); - for (var j = 0; j < eventNames.length; j++) { - var eventName = eventNames[j]; - var avm2EventTarget = instance; - if (eventName === 'mouseDown' || eventName === 'mouseUp' || eventName === 'mouseMove') { - // FIXME regressed, avm1 mouse events shall be received all the time. - // avm2EventTarget = instance.stage; - } - avm2EventTarget.addEventListener(eventName, fn, false); - eventsBound.push({eventName: eventName, fn: fn, target: avm2EventTarget}); + _initializeChildren(frame: Timeline.FrameDelta): void { + var states = frame.stateAtDepth; + for (var depth in states) { + var state = states[depth]; + if (state) { + var character = this.createAnimatedDisplayObject(state, false); + this.addTimelineObjectAtDepth(character, state.depth); + if (state.symbol.isAVM1Object) { + avm1lib.initializeAVM1Object(character, state); } } - if (eventsBound.length > 0) { - instance.addEventListener('removed', function (eventsBound) { - for (var i = 0; i < eventsBound.length; i++) { - eventsBound[i].target.removeEventListener(eventsBound[i].eventName, eventsBound[i].fn, false); - } - }.bind(instance, eventsBound), false); - } } } diff --git a/src/flash/display/Stage.ts b/src/flash/display/Stage.ts index 0f925e2ad1..16b3399b3e 100644 --- a/src/flash/display/Stage.ts +++ b/src/flash/display/Stage.ts @@ -378,7 +378,7 @@ module Shumway.AVM2.AS.flash.display { notImplemented("public flash.display.Stage::isFocusInaccessible"); return; } requireOwnerPermissions(): void { - somewhatImplemented("public flash.display.Stage::requireOwnerPermissions"); return; + // TODO: implement requireOwnerPermissions } render(): void { diff --git a/src/flash/events/EventDispatcher.ts b/src/flash/events/EventDispatcher.ts index a0e8677022..07370ecfeb 100644 --- a/src/flash/events/EventDispatcher.ts +++ b/src/flash/events/EventDispatcher.ts @@ -436,8 +436,8 @@ module Shumway.AVM2.AS.flash.events { if (keepPropagating) { var list = this._getListenersForType(false, type); if (list) { - keepPropagating = EventDispatcher.callListeners(this._getListeners(false)[type], event, - target, target, EventPhase.AT_TARGET); + keepPropagating = EventDispatcher.callListeners(list, event, target, target, + EventPhase.AT_TARGET); } } diff --git a/src/flash/events/MouseEvent.ts b/src/flash/events/MouseEvent.ts index e5c0f1da64..5fd381f246 100644 --- a/src/flash/events/MouseEvent.ts +++ b/src/flash/events/MouseEvent.ts @@ -135,7 +135,6 @@ module Shumway.AVM2.AS.flash.events { } get movementX(): number { - somewhatImplemented("public flash.events.MouseEvent::set movementX"); return this._movementX || 0; } set movementX(value: number) { @@ -143,7 +142,6 @@ module Shumway.AVM2.AS.flash.events { } get movementY(): number { - somewhatImplemented("public flash.events.MouseEvent::set movementY"); return this._movementY || 0; } set movementY(value: number) { diff --git a/src/flash/text/Font.ts b/src/flash/text/Font.ts index 62675e987e..6ff89f1fc3 100644 --- a/src/flash/text/Font.ts +++ b/src/flash/text/Font.ts @@ -594,7 +594,8 @@ module Shumway.AVM2.AS.flash.text { static initializer: any = function (symbol: Shumway.Timeline.FontSymbol) { var self: Font = this; - self._id = flash.display.DisplayObject.getNextSyncID(); + // TODO: give fonts proper inter-SWF IDs, so multiple SWFs' fonts don't collide. + self._id = symbol.data.id; self._fontName = null; self._fontFamily = null; @@ -631,12 +632,16 @@ module Shumway.AVM2.AS.flash.text { } // Font symbols without any glyphs describe device fonts. - self._fontType = symbol.data ? FontType.EMBEDDED : FontType.DEVICE; - - if (!Font._fontsBySymbolId[symbol.id]) { - Font._fontsBySymbolId[symbol.id] = self; - Font._fontsByName[symbol.name.toLowerCase()] = self; - Font._fontsByName['swffont' + symbol.id] = self; + self._fontType = metrics ? FontType.EMBEDDED : FontType.DEVICE; + + var fontProp = Object.getOwnPropertyDescriptor(Font._fontsBySymbolId, symbol.id + ''); + // Define mapping or replace lazy getter with value. + if (!fontProp || !fontProp.value) { + Object.defineProperty(Font._fontsBySymbolId, symbol.id + '', {value: self}); + Object.defineProperty(Font._fontsByName, symbol.name.toLowerCase(), {value: self}); + if (self._fontType === FontType.EMBEDDED) { + Object.defineProperty(Font._fontsByName, 'swffont' + symbol.id, {value: self}); + } } } }; @@ -737,6 +742,25 @@ module Shumway.AVM2.AS.flash.text { somewhatImplemented('Font.registerFont'); } + /** + * Registers an embedded font as available in the system without it being decoded. + * + * Firefox decodes fonts synchronously, allowing us to do the decoding upon first actual use. + * All we need to do here is let the system know about the family name and ID, so that both + * TextFields/Labels referring to the font's symbol ID as well as HTML text specifying a font + * face can resolve the font. + */ + static registerLazyFont(fontMapping: {name: string; id: number}, + loaderInfo: flash.display.LoaderInfo): void { + var resolverProp = { + get: loaderInfo.getSymbolById.bind(loaderInfo, fontMapping.id), + configurable: true + }; + Object.defineProperty(Font._fontsByName, fontMapping.name.toLowerCase(), resolverProp); + Object.defineProperty(Font._fontsByName, 'swffont' + fontMapping.id, resolverProp); + Object.defineProperty(Font._fontsBySymbolId, fontMapping.id + '', resolverProp); + } + get fontName(): string { return this._fontName; } diff --git a/src/gfx/easel.ts b/src/gfx/easel.ts index 0e68fc1ac8..cb300734e2 100644 --- a/src/gfx/easel.ts +++ b/src/gfx/easel.ts @@ -272,7 +272,8 @@ module Shumway.GFX { disableHiDPI: boolean = false, backgroundColor: number = undefined ) { - release && assert(container && container.children.length === 0, "Easel container must be empty."); + release || assert(container && container.children.length === 0, + "Easel container must be empty."); this._container = container; this._stage = new Stage(512, 512, true); this._worldView = this._stage.content; diff --git a/src/gfx/remotingGfx.ts b/src/gfx/remotingGfx.ts index 17ccdfbf26..b1dfefb7ee 100644 --- a/src/gfx/remotingGfx.ts +++ b/src/gfx/remotingGfx.ts @@ -276,10 +276,6 @@ module Shumway.Remoting.GFX { data.updateNetStream ++; this._readUpdateNetStream(); break; - case MessageTag.RegisterFont: - data.registerFont ++; - this._readRegisterFont(); - break; case MessageTag.DrawToBitmap: data.drawToBitmap ++; this._readDrawToBitmap(); @@ -387,7 +383,7 @@ module Shumway.Remoting.GFX { renderable = new RenderableShape(id, pathData, textures, bounds); } for (var i = 0; i < textures.length; i++) { - textures[i].addRenderableParent(renderable); + textures[i] && textures[i].addRenderableParent(renderable); } context._registerAsset(id, symbolId, renderable); } @@ -599,15 +595,6 @@ module Shumway.Remoting.GFX { } } - private _readRegisterFont() { - var input = this.input; - var fontId = input.readInt(); - var bold = input.readBoolean(); - var italic = input.readBoolean(); - var data = this._readAsset(); - Shumway.registerCSSFont(fontId, data); - } - private _readDrawToBitmap() { var input = this.input; var context = this.context; diff --git a/src/gfx/renderables/renderables.ts b/src/gfx/renderables/renderables.ts index 5f1040c714..e6d5f6d47d 100644 --- a/src/gfx/renderables/renderables.ts +++ b/src/gfx/renderables/renderables.ts @@ -51,16 +51,16 @@ module Shumway.GFX { private _renderableParents: Renderable [] = []; public addParent(frame: Node) { - release && assert(frame); + release || assert(frame); var index = indexOf(this._parents, frame); - release && assert(index < 0); + release || assert(index < 0); this._parents.push(frame); } public addRenderableParent(renderable: Renderable) { - release && assert(renderable); + release || assert(renderable); var index = indexOf(this._renderableParents, renderable); - release && assert(index < 0); + release || assert(index < 0); this._renderableParents.push(renderable); } @@ -89,7 +89,7 @@ module Shumway.GFX { this._invalidateEventListeners = []; } var index = indexOf(this._invalidateEventListeners, listener); - release && assert(index < 0); + release || assert(index < 0); this._invalidateEventListeners.push(listener); } @@ -530,7 +530,7 @@ module Shumway.GFX { // Wait to deserialize paths until all textures have been loaded. var textures = this._textures; for (var i = 0; i < textures.length; i++) { - if (textures[i].hasFlags(NodeFlags.Loading)) { + if (textures[i] && textures[i].hasFlags(NodeFlags.Loading)) { return; } } @@ -582,7 +582,8 @@ module Shumway.GFX { leaveTimeline("RenderableShape.render"); } - protected _deserializePaths(data: ShapeData, context: CanvasRenderingContext2D, ratio: number): StyledPath[] { + protected _deserializePaths(data: ShapeData, context: CanvasRenderingContext2D, + ratio: number): StyledPath[] { release || assert(data ? !this._paths : this._paths); enterTimeline("RenderableShape.deserializePaths"); // TODO: Optimize path handling to use only one path if possible. @@ -766,9 +767,17 @@ module Shumway.GFX { var repeat = styles.readBoolean() ? 'repeat' : 'no-repeat'; var smooth = styles.readBoolean(); var texture = this._textures[textureIndex]; - release || assert(texture._canvas); - var fillStyle: CanvasPattern = context.createPattern(texture._canvas, repeat); - fillStyle.setTransform(fillTransform.toSVGMatrix()); + var fillStyle: CanvasPattern; + if (texture) { + fillStyle = context.createPattern(texture._canvas, repeat); + fillStyle.setTransform(fillTransform.toSVGMatrix()); + } else { + // TODO: Wire up initially-missing textures that become available later. + // An invalid SWF can have shape fills refer to images that occur later in the SWF. In that + // case, the image only becomes available once that frame is actually reached. Before that + // the fill isn't drawn; it is drawn once the image becomes available, though. + fillStyle = null; + } return {style: fillStyle, smoothImage: smooth}; } diff --git a/src/player/options.ts b/src/player/options.ts index 4aeb0917b6..f1a6e50cc1 100644 --- a/src/player/options.ts +++ b/src/player/options.ts @@ -17,8 +17,6 @@ module Shumway { import Option = Shumway.Options.Option; import OptionSet = Shumway.Options.OptionSet; - import StageScaleMode = Shumway.Remoting.StageScaleMode; - import StageAlignFlags = Shumway.Remoting.StageAlignFlags; import shumwayOptions = Shumway.Settings.shumwayOptions; diff --git a/src/player/player.ts b/src/player/player.ts index fd8b066d19..4b0efb67d3 100644 --- a/src/player/player.ts +++ b/src/player/player.ts @@ -198,7 +198,7 @@ module Shumway.Player { } self._enterLoops(); - }); + }, null); } var context = this.createLoaderContext(); if (buffer) { @@ -355,19 +355,6 @@ module Shumway.Player { }); } - public registerFont(font: flash.text.Font) { - // We register the font immediately and also send it over to the GFX process. That's required - // to have metrics for measuring text dimensions on the player side, too. - Shumway.registerCSSFont(font._id, font._symbol.data.buffer); - var updates = new DataBuffer(); - var assets = []; - var serializer = new Shumway.Remoting.Player.PlayerChannelSerializer(); - serializer.output = updates; - serializer.outputAssets = assets; - serializer.writeFont(font); - this.onSendUpdates(updates, assets); - } - public drawToBitmap(bitmapData: flash.display.BitmapData, source: Shumway.Remoting.IRemotable, matrix: flash.geom.Matrix = null, colorTransform: flash.geom.ColorTransform = null, blendMode: string = null, clipRect: flash.geom.Rectangle = null, smoothing: boolean = false) { var updates = new DataBuffer(); var assets = []; diff --git a/src/player/remotingPlayer.ts b/src/player/remotingPlayer.ts index 1dc548e8a5..ce2d3b4564 100644 --- a/src/player/remotingPlayer.ts +++ b/src/player/remotingPlayer.ts @@ -131,7 +131,7 @@ module Shumway.Remoting.Player { var textures = graphics.getUsedTextures(); var numTextures = textures.length; for (var i = 0; i < numTextures; i++) { - this.writeBitmapData(textures[i]); + textures[i] && this.writeBitmapData(textures[i]); } this.output.writeInt(MessageTag.UpdateGraphics); this.output.writeInt(graphics._id); @@ -140,7 +140,7 @@ module Shumway.Remoting.Player { this._writeAsset(graphics.getGraphicsData().toPlainObject()); this.output.writeInt(numTextures); for (var i = 0; i < numTextures; i++) { - this.output.writeInt(textures[i]._id); + this.output.writeInt(textures[i] ? textures[i]._id : -1); } graphics._isDirty = false; } @@ -200,20 +200,6 @@ module Shumway.Remoting.Player { } } - writeFont(font: flash.text.Font) { - // Device fonts can be skipped, they obviously should exist on the device. - if (font.fontType === 'embedded') { - writer && writer.writeLn("Sending Font: " + font._id); - var symbol = font._symbol; - release || assert(symbol); - this.output.writeInt(MessageTag.RegisterFont); - this.output.writeInt(font._id); - this.output.writeBoolean(symbol.bold); - this.output.writeBoolean(symbol.italic); - this._writeAsset(symbol.data); - } - } - /** * Writes the number of display objects this display object clips. */ diff --git a/src/player/test/fakeSyncWorker.ts b/src/player/test/fakeSyncWorker.ts index b382694f4b..7828255701 100644 --- a/src/player/test/fakeSyncWorker.ts +++ b/src/player/test/fakeSyncWorker.ts @@ -27,17 +27,20 @@ module Shumway.Player.Test { return FakeSyncWorker._singelton; } - private _worker: Worker; + //private _worker: Worker; + private _onmessageListeners: any[]; private _onsyncmessageListeners: any[]; constructor() { - this._worker = new Worker(FakeSyncWorker.WORKER_PATH); + //this._worker = new Worker(FakeSyncWorker.WORKER_PATH); + this._onmessageListeners = []; this._onsyncmessageListeners = []; } addEventListener(type: string, listener: any, useCapture?: boolean): void { + release || Debug.assert(type === 'syncmessage' || type === 'message'); if (type !== 'syncmessage') { - this._worker.addEventListener(type, listener, useCapture); + this._onmessageListeners.push(listener); } else { this._onsyncmessageListeners.push(listener); } @@ -51,12 +54,32 @@ module Shumway.Player.Test { } return; } - this._worker.removeEventListener(type, listener, useCapture); - + var i = this._onmessageListeners.indexOf(listener); + if (i >= 0) { + this._onmessageListeners.splice(i, 1); + } } - postMessage(message: any, ports?: any): void { - this._worker.postMessage(message, ports); + postMessage(message: any, ports?: any): any { + var result; + this._onmessageListeners.some(function (listener) { + var ev = { data: message, result: undefined, handled: false }; + try { + if (typeof listener === 'function') { + listener(ev); + } else { + listener.handleEvent(ev); + } + if (!ev.handled) { + return false; + } + } catch (ex) { + Debug.warning('Failure at postMessage: ' + ex.message); + } + result = ev.result; + return true; + }); + return result; } postSyncMessage(message: any, ports?: any): any { diff --git a/src/player/timeline.ts b/src/player/timeline.ts index a08a308530..397c50cc34 100644 --- a/src/player/timeline.ts +++ b/src/player/timeline.ts @@ -25,24 +25,41 @@ module Shumway.Timeline { import flash = Shumway.AVM2.AS.flash; import SwfTag = Shumway.SWF.Parser.SwfTag; import PlaceObjectFlags = Shumway.SWF.Parser.PlaceObjectFlags; + import SoundStream = Shumway.SWF.Parser.SoundStream; import ActionScriptVersion = flash.display.ActionScriptVersion; + export interface SymbolData {id: number; className: string} /** * TODO document */ export class Symbol { - id: number = -1; + data: any; isAVM1Object: boolean; avm1Context: Shumway.AVM1.AVM1Context; symbolClass: Shumway.AVM2.AS.ASClass; - constructor(id: number, symbolClass: Shumway.AVM2.AS.ASClass) { - release || assert (isInteger(id)); - this.id = id; - this.symbolClass = symbolClass; + constructor(data: SymbolData, symbolDefaultClass: Shumway.AVM2.AS.ASClass) { + release || assert (isInteger(data.id)); + this.data = data; + if (data.className) { + var appDomain = Shumway.AVM2.Runtime.AVM2.instance.applicationDomain; + try { + var symbolClass = appDomain.getClass(data.className); + this.symbolClass = symbolClass; + } catch (e) { + warning ("Symbol " + data.id + " bound to non-existing class " + data.className); + this.symbolClass = symbolDefaultClass; + } + } else { + this.symbolClass = symbolDefaultClass; + } this.isAVM1Object = false; } + + get id(): number { + return this.data.id; + } } export class DisplaySymbol extends Symbol { @@ -51,8 +68,8 @@ module Shumway.Timeline { scale9Grid: Bounds; dynamic: boolean; - constructor(id: number, symbolClass: Shumway.AVM2.AS.ASClass, dynamic: boolean = true) { - super(id, symbolClass); + constructor(data: SymbolData, symbolClass: Shumway.AVM2.AS.ASClass, dynamic: boolean) { + super(data, symbolClass); this.dynamic = dynamic; } @@ -68,15 +85,15 @@ module Shumway.Timeline { export class ShapeSymbol extends DisplaySymbol { graphics: flash.display.Graphics = null; - constructor(id: number, symbolClass: Shumway.AVM2.AS.ASClass = flash.display.Shape) { - super(id, symbolClass, false); + constructor(data: SymbolData, symbolClass: Shumway.AVM2.AS.ASClass) { + super(data, symbolClass, false); } - static FromData(data: any, loaderInfo: flash.display.LoaderInfo): ShapeSymbol { - var symbol = new ShapeSymbol(data.id); + static FromData(data: SymbolData, loaderInfo: flash.display.LoaderInfo): ShapeSymbol { + var symbol = new ShapeSymbol(data, flash.display.Shape); symbol._setBoundsFromData(data); symbol.graphics = flash.display.Graphics.FromData(data); - symbol.processRequires(data.require, loaderInfo); + symbol.processRequires((data).require, loaderInfo); return symbol; } @@ -87,6 +104,12 @@ module Shumway.Timeline { var textures = this.graphics.getUsedTextures(); for (var i = 0; i < dependencies.length; i++) { var symbol = loaderInfo.getSymbolById(dependencies[i]); + if (!symbol) { + warning("Bitmap symbol " + dependencies[i] + " required by shape, but not defined."); + textures.push(null); + // TODO: handle null-textures from invalid SWFs correctly. + continue; + } textures.push(symbol.getSharedInstance()); } } @@ -95,12 +118,12 @@ module Shumway.Timeline { export class MorphShapeSymbol extends ShapeSymbol { morphFillBounds: Bounds; morphLineBounds: Bounds; - constructor(id: number) { - super(id, flash.display.MorphShape); + constructor(data: SymbolData) { + super(data, flash.display.MorphShape); } static FromData(data: any, loaderInfo: flash.display.LoaderInfo): MorphShapeSymbol { - var symbol = new MorphShapeSymbol(data.id); + var symbol = new MorphShapeSymbol(data); symbol._setBoundsFromData(data); symbol.graphics = flash.display.Graphics.FromData(data); symbol.processRequires(data.require, loaderInfo); @@ -113,19 +136,21 @@ module Shumway.Timeline { export class BitmapSymbol extends DisplaySymbol { width: number; height: number; + image: any; // Image, but tsc doesn't like that. data: Uint8Array; type: ImageType; private sharedInstance: flash.display.BitmapData; - constructor(id: number) { - super(id, flash.display.BitmapData); + constructor(data: SymbolData) { + super(data, flash.display.BitmapData, false); } static FromData(data: any): BitmapSymbol { - var symbol = new BitmapSymbol(data.id); + var symbol = new BitmapSymbol(data); symbol.width = data.width; symbol.height = data.height; + symbol.image = data.image; symbol.data = data.data; switch (data.mimeType) { case "application/octet-stream": @@ -180,12 +205,12 @@ module Shumway.Timeline { variableName: string = null; textContent: Shumway.TextContent = null; - constructor(id: number) { - super(id, flash.text.TextField); + constructor(data: SymbolData) { + super(data, flash.text.TextField, true); } - static FromTextData(data: any): TextSymbol { - var symbol = new TextSymbol(data.id); + static FromTextData(data: any, loaderInfo: flash.display.LoaderInfo): TextSymbol { + var symbol = new TextSymbol(data); symbol._setBoundsFromData(data); var tag = data.tag; if (data.static) { @@ -205,13 +230,14 @@ module Shumway.Timeline { } if (tag.hasFont) { symbol.size = tag.fontHeight; + // Requesting the font symbol guarantees that it's loaded and initialized. + var fontSymbol = loaderInfo.getSymbolById(tag.fontId); var font = flash.text.Font.getBySymbolId(tag.fontId); - if (font) { + if (fontSymbol && font) { symbol.font = font.fontName; if (tag.fontClass) { var appDomain = Shumway.AVM2.Runtime.AVM2.instance.applicationDomain; - symbol.fontClass = - appDomain.getClass(tag.fontClass); + symbol.fontClass = appDomain.getClass(tag.fontClass); } } else { warning("Font " + tag.fontId + " is not defined."); @@ -243,42 +269,115 @@ module Shumway.Timeline { symbol.variableName = tag.variableName; return symbol; } + + /** + * Turns raw DefineLabel tag data into an object that's consumable as a text symbol and then + * passes that into `FromTextData`, returning the resulting TextSymbol. + * + * This has to be done outside the SWF parser because it relies on any used fonts being + * available as symbols, which isn't the case in the SWF parser. + */ + static FromLabelData(data: any, loaderInfo: flash.display.LoaderInfo): TextSymbol { + var bounds = data.fillBounds; + var records = data.records; + var coords = data.coords = []; + var htmlText = ''; + var size = 12; + var face = 'Times Roman'; + var color = 0; + var x = 0; + var y = 0; + var codes: number[]; + for (var i = 0; i < records.length; i++) { + var record = records[i]; + if (record.eot) { + break; + } + if (record.hasFont) { + var font = loaderInfo.getSymbolById(record.fontId); + font || Debug.warning('Label ' + data.id + 'refers to undefined font symbol ' + + record.fontId); + codes = font.codes; + size = record.fontHeight; + if (!font.originalSize) { + size /= 20; + } + face = 'swffont' + record.fontId; + } + if (record.hasColor) { + color = record.color >>> 8; + } + if (record.hasMoveX) { + x = record.moveX; + if (x < bounds.xMin) { + bounds.xMin = x; + } + } + if (record.hasMoveY) { + y = record.moveY; + if (y < bounds.yMin) { + bounds.yMin = y; + } + } + var text = ''; + var entries = record.entries; + var j = 0; + var entry; + while ((entry = entries[j++])) { + var code = codes[entry.glyphIndex]; + release || assert(code, 'undefined label glyph'); + var char = String.fromCharCode(code); + text += charEscapeMap[char] || char; + coords.push(x, y); + x += entry.advance; + } + htmlText += '' + text + ''; + } + data.tag.initialText = htmlText; + return TextSymbol.FromTextData(data, loaderInfo); + } } + var charEscapeMap = {'<': '<', '>': '>', '&' : '&'}; + export class ButtonSymbol extends DisplaySymbol { upState: AnimationState = null; overState: AnimationState = null; downState: AnimationState = null; hitTestState: AnimationState = null; - buttonActions: any[]; // Only relevant for AVM1, see AVM1Button. + loaderInfo: flash.display.LoaderInfo; - constructor(id: number) { - super(id, flash.display.SimpleButton); + constructor(data: SymbolData, loaderInfo: flash.display.LoaderInfo) { + super(data, flash.display.SimpleButton, false); + this.loaderInfo = loaderInfo; } static FromData(data: any, loaderInfo: flash.display.LoaderInfo): ButtonSymbol { - var symbol = new ButtonSymbol(data.id); + var symbol = new ButtonSymbol(data, loaderInfo); if (loaderInfo.actionScriptVersion === ActionScriptVersion.ACTIONSCRIPT2) { symbol.isAVM1Object = true; - symbol.buttonActions = data.buttonActions; } var states = data.states; - var character, matrix, colorTransform; + var character: SpriteSymbol = null; + var matrix: flash.geom.Matrix = null; + var colorTransform: flash.geom.ColorTransform = null; + var cmd; for (var stateName in states) { var commands = states[stateName]; if (commands.length === 1) { - var cmd = commands[0]; - character = loaderInfo.getSymbolById(cmd.symbolId); + cmd = commands[0]; matrix = flash.geom.Matrix.FromUntyped(cmd.matrix); if (cmd.cxform) { colorTransform = flash.geom.ColorTransform.FromCXForm(cmd.cxform); } } else { - character = new Timeline.SpriteSymbol(-1); + cmd = {symbolId: -1}; + character = new Timeline.SpriteSymbol({id: -1, className: null}, loaderInfo); character.frames.push(new FrameDelta(loaderInfo, commands)); } - symbol[stateName + 'State'] = - new Timeline.AnimationState(character, 0, matrix, colorTransform); + symbol[stateName + 'State'] = new Timeline.AnimationState(cmd.symbolId, character, 0, + matrix, colorTransform); } return symbol; } @@ -292,34 +391,37 @@ module Shumway.Timeline { isRoot: boolean; avm1Name: string; avm1SymbolClass; + loaderInfo: flash.display.LoaderInfo; - constructor(id: number, isRoot: boolean = false) { - super(id, flash.display.MovieClip); - this.isRoot = isRoot; + constructor(data: SymbolData, loaderInfo: flash.display.LoaderInfo) { + super(data, flash.display.MovieClip, true); + this.loaderInfo = loaderInfo; } static FromData(data: any, loaderInfo: flash.display.LoaderInfo): SpriteSymbol { - var symbol = new SpriteSymbol(data.id); + var symbol = new SpriteSymbol(data, loaderInfo); symbol.numFrames = data.frameCount; if (loaderInfo.actionScriptVersion === ActionScriptVersion.ACTIONSCRIPT2) { symbol.isAVM1Object = true; symbol.avm1Context = loaderInfo._avm1Context; } - symbol.frameScripts = data.frameScripts; + symbol.frameScripts = []; var frames = data.frames; - var frameNum = 1; for (var i = 0; i < frames.length; i++) { - var frameInfo = frames[i]; - var frame = new FrameDelta(loaderInfo, frameInfo.commands); - var repeat = frameInfo.repeat; - while (repeat--) { - symbol.frames.push(frame); + var frameInfo; + frameInfo = loaderInfo.getFrame(data, i); + if (frameInfo.actionBlocks) { + symbol.frameScripts.push(i); + symbol.frameScripts.push.apply(symbol.frameScripts, frameInfo.actionBlocks); } if (frameInfo.labelName) { - symbol.labels.push(new flash.display.FrameLabel(frameInfo.labelName, frameNum)); + symbol.labels.push(new flash.display.FrameLabel(frameInfo.labelName, i + 1)); + } + var frame = frameInfo.frameDelta; + var repeat = frameInfo.repeat || 1; + while (repeat--) { + symbol.frames.push(frame); } - - frameNum += frameInfo.repeat; } return symbol; } @@ -327,22 +429,27 @@ module Shumway.Timeline { // TODO: move this, and the other symbol classes, into better-suited files. export class FontSymbol extends Symbol { - name: string = ""; - bold: boolean = false; - italic: boolean = false; - data: Uint8Array; + name: string; + id: number; + bold: boolean; + italic: boolean; + codes: number[]; + originalSize: boolean; metrics: any; - constructor(id: number) { - super(id, flash.text.Font); + constructor(data: SymbolData) { + super(data, flash.text.Font); } static FromData(data: any): FontSymbol { - var symbol = new FontSymbol(data.id); + var symbol = new FontSymbol(data); symbol.name = data.name; + // No need to keep the original data baggage around. + symbol.data = {id: data.id}; symbol.bold = data.bold; symbol.italic = data.italic; - symbol.data = data.data; + symbol.originalSize = data.originalSize; + symbol.codes = data.codes; symbol.metrics = data.metrics; return symbol; } @@ -354,12 +461,12 @@ module Shumway.Timeline { pcm: Float32Array; packaged; - constructor(id: number) { - super(id, flash.media.Sound); + constructor(data: SymbolData) { + super(data, flash.media.Sound); } static FromData(data: any): SoundSymbol { - var symbol = new SoundSymbol(data.id); + var symbol = new SoundSymbol(data); symbol.channels = data.channels; symbol.sampleRate = data.sampleRate; symbol.pcm = data.pcm; @@ -372,12 +479,12 @@ module Shumway.Timeline { buffer: Uint8Array; byteLength: number; - constructor(id: number) { - super(id, flash.utils.ByteArray); + constructor(data: SymbolData) { + super(data, flash.utils.ByteArray); } static FromData(data: any): BinarySymbol { - var symbol = new BinarySymbol(data.id); + var symbol = new BinarySymbol(data); symbol.buffer = data.data; symbol.byteLength = data.data.byteLength; return symbol; @@ -388,7 +495,8 @@ module Shumway.Timeline { * TODO document */ export class AnimationState { - constructor(public symbol: DisplaySymbol = null, + constructor(public symbolId: number, + public symbol: DisplaySymbol = null, public depth: number = 0, public matrix: flash.geom.Matrix = null, public colorTransform: flash.geom.ColorTransform = null, @@ -445,50 +553,81 @@ module Shumway.Timeline { * TODO document */ export class FrameDelta { - _stateAtDepth: Shumway.Map; - _soundStarts: SoundStart[]; + private _stateAtDepth: Shumway.Map; + private _soundStarts: SoundStart[]; + private _soundStreamHead: SoundStream; + private _soundStreamBlock: {data: Uint8Array}; get stateAtDepth() { return this._stateAtDepth || this._initialize(); } get soundStarts() { - if (this._soundStarts === undefined) { + if (this.commands) { this._initialize(); } return this._soundStarts; } + // TODO: refactor streaming sound support to delay sound parsing until needed. + // These two fields aren't used for now, but will perhaps be helpful in the above. + get soundStreamHead() { + if (this.commands) { + this._initialize(); + } + return this._soundStreamHead; + } + + get soundStreamBlock() { + if (this.commands) { + this._initialize(); + } + return this._soundStreamBlock; + } + constructor(private loaderInfo: flash.display.LoaderInfo, private commands: any []) { this._stateAtDepth = null; - this._soundStarts = undefined; + this._soundStarts = null; + this._soundStreamHead = null; + this._soundStreamBlock = null; } private _initialize(): Shumway.Map { var states: Shumway.Map = this._stateAtDepth = Object.create(null); - var soundStarts : SoundStart[] = null; var commands = this.commands; + if (!commands) { + return states; + } var loaderInfo = this.loaderInfo; for (var i = 0; i < commands.length; i++) { - var cmd = commands[i]; + var cmd = 'depth' in commands[i] ? + commands[i] : + loaderInfo._file.getParsedTag(commands[i]); var depth = cmd.depth; switch (cmd.code) { - case 5: // SWF_TAG_CODE_REMOVE_OBJECT - case 28: // SWF_TAG_CODE_REMOVE_OBJECT2 + case SwfTag.CODE_REMOVE_OBJECT: + case SwfTag.CODE_REMOVE_OBJECT2: states[depth] = null; break; case SwfTag.CODE_START_SOUND: - if (!soundStarts) { - soundStarts = []; - } + var soundStarts = this._soundStarts || (this._soundStarts = []); soundStarts.push(new SoundStart(cmd.soundId, cmd.soundInfo)); break; - default: + case SwfTag.CODE_SOUND_STREAM_HEAD: + case SwfTag.CODE_SOUND_STREAM_HEAD2: + this._soundStreamHead = SoundStream.FromTag(cmd); + break; + case SwfTag.CODE_SOUND_STREAM_BLOCK: + this._soundStreamBlock = cmd; + break; + case SwfTag.CODE_PLACE_OBJECT: + case SwfTag.CODE_PLACE_OBJECT2: + case SwfTag.CODE_PLACE_OBJECT3: var symbol: DisplaySymbol = null; var matrix: flash.geom.Matrix = null; var colorTransform: flash.geom.ColorTransform = null; var filters: flash.filters.BitmapFilter[] = null; - var events: any[] = null; + var events: any[] = loaderInfo._allowCodeExecution ? cmd.events : 0; if (cmd.symbolId) { symbol = loaderInfo.getSymbolById(cmd.symbolId); if (!symbol) { @@ -522,44 +661,8 @@ module Shumway.Timeline { filters.push(filter); } } - if ((cmd.flags & PlaceObjectFlags.HasClipActions) && - loaderInfo._allowCodeExecution && - loaderInfo._actionScriptVersion === ActionScriptVersion.ACTIONSCRIPT2) { - var swfEvents = cmd.events; - events = []; - for (var j = 0; j < swfEvents.length; j++) { - var swfEvent = swfEvents[j]; - if (swfEvent.eoe) { - break; - } - var actionsData = new AVM1.AVM1ActionsData(swfEvent.actionsData, - 's' + cmd.symbolId + 'e' + j); - var fn = (function (actionsData, loaderInfo) { - return function() { - var avm1Context = loaderInfo._avm1Context; - var avm1Object = Shumway.AVM2.AS.avm1lib.getAVM1Object(this); - return avm1Context.executeActions(actionsData, avm1Object); - }; - })(actionsData, loaderInfo); - var eventNames = []; - for (var eventName in swfEvent) { - if (eventName.indexOf("on") !== 0 || !swfEvent[eventName]) { - continue; - } - var avm2EventName = eventName[2].toLowerCase() + eventName.substring(3); - if (avm2EventName === 'enterFrame') { - avm2EventName = 'frameConstructed'; - } - eventNames.push(avm2EventName); - } - events.push({ - eventNames: eventNames, - handler: fn, - keyPress: swfEvent.keyPress - }); - } - } var state = new Timeline.AnimationState ( + cmd.symbolId, symbol, depth, matrix, @@ -576,9 +679,11 @@ module Shumway.Timeline { ); states[depth] = state; break; + default: + console.warn("Unhandled timeline control tag: " + cmd.code + ": " + SwfTag[cmd.code]); } + } - this._soundStarts = soundStarts; this.commands = null; return states; } diff --git a/src/shell/domstubs.js b/src/shell/domstubs.js index 0c7d1b2332..eeeee8475e 100644 --- a/src/shell/domstubs.js +++ b/src/shell/domstubs.js @@ -18,8 +18,14 @@ if (typeof console === 'undefined') { console = { log: print, info: print, - error: print, - warn: print, + warn: function() { + print(Shumway.IndentingWriter.RED + [].join.call(arguments, ', ') + + Shumway.IndentingWriter.ENDC); + }, + error: function() { + print(Shumway.IndentingWriter.BOLD_RED + [].join.call(arguments, ', ') + + Shumway.IndentingWriter.ENDC + '\nstack:\n' + (new Error().stack)); + }, time: function () {}, timeEnd: function () {} }; @@ -84,6 +90,28 @@ var document = { } }; +var Image = function () {}; + +Image.prototype = { + +} + +var URL = function () {}; + +URL.prototype = { + +} + +URL.createObjectURL = function createObjectURL() { + return ""; +}; + +var Blob = function () {}; + +Blob.prototype = { + +} + var XMLHttpRequest = function () {}; XMLHttpRequest.prototype = { open: function (method, url, async) { diff --git a/src/shell/shell.ts b/src/shell/shell.ts index 4ea8cf9313..f925567adb 100644 --- a/src/shell/shell.ts +++ b/src/shell/shell.ts @@ -143,6 +143,7 @@ module Shumway.Shell { var parseForDatabaseOption: Option; var disassembleOption: Option; var verboseOption: Option; + var profileOption: Option; var releaseOption: Option; var executeOption: Option; var interpreterOption: Option; @@ -161,6 +162,7 @@ module Shumway.Shell { parseForDatabaseOption = shellOptions.register(new Option("po", "parseForDatabase", "boolean", false, "Parse File(s)")); disassembleOption = shellOptions.register(new Option("d", "disassemble", "boolean", false, "Disassemble File(s)")); verboseOption = shellOptions.register(new Option("v", "verbose", "boolean", false, "Verbose")); + profileOption = shellOptions.register(new Option("o", "profile", "boolean", false, "Profile")); releaseOption = shellOptions.register(new Option("r", "release", "boolean", false, "Release mode")); executeOption = shellOptions.register(new Option("x", "execute", "boolean", false, "Execute File(s)")); interpreterOption = shellOptions.register(new Option("i", "interpreter", "boolean", false, "Interpreter Only")); @@ -210,6 +212,7 @@ module Shumway.Shell { console.info = console.log = console.warn = console.error = function () {}; } + profile = profileOption.value; release = releaseOption.value; verbose = verboseOption.value; @@ -228,10 +231,12 @@ module Shumway.Shell { files.forEach(function (file) { var start = dateNow(); writer.debugLn("Parsing: " + file); + profile && SWF.timelineBuffer.reset(); parseFile(file, parseForDatabaseOption.value, symbolFilterOption.value.split(",")); var elapsed = dateNow() - start; if (verbose) { - verbose && writer.writeLn("Total Parse Time: " + (elapsed).toFixed(4)); + verbose && writer.writeLn("Total Parse Time: " + (elapsed).toFixed(2) + " ms."); + profile && SWF.timelineBuffer.createSnapshot().trace(writer); } }); } @@ -343,51 +348,6 @@ module Shumway.Shell { writer.outdent(); } - function parseSymbol(tag, symbols) { - var symbol; - switch (tag.code) { - case SwfTag.CODE_DEFINE_BITS: - case SwfTag.CODE_DEFINE_BITS_JPEG2: - case SwfTag.CODE_DEFINE_BITS_JPEG3: - case SwfTag.CODE_DEFINE_BITS_JPEG4: - case SwfTag.CODE_JPEG_TABLES: - symbol = Shumway.SWF.Parser.defineImage(tag, symbols); - break; - case SwfTag.CODE_DEFINE_BITS_LOSSLESS: - case SwfTag.CODE_DEFINE_BITS_LOSSLESS2: - symbol = Shumway.SWF.Parser.defineBitmap(tag); - break; - case SwfTag.CODE_DEFINE_BUTTON: - case SwfTag.CODE_DEFINE_BUTTON2: - // symbol = Shumway.SWF.Parser.defineButton(tag, symbols); - break; - case SwfTag.CODE_DEFINE_EDIT_TEXT: - symbol = Shumway.SWF.Parser.defineText(tag, symbols); - break; - case SwfTag.CODE_DEFINE_FONT: - case SwfTag.CODE_DEFINE_FONT2: - case SwfTag.CODE_DEFINE_FONT3: - case SwfTag.CODE_DEFINE_FONT4: - symbol = Shumway.SWF.Parser.defineFont(tag, symbols); - break; - case SwfTag.CODE_DEFINE_MORPH_SHAPE: - case SwfTag.CODE_DEFINE_MORPH_SHAPE2: - case SwfTag.CODE_DEFINE_SHAPE: - case SwfTag.CODE_DEFINE_SHAPE2: - case SwfTag.CODE_DEFINE_SHAPE3: - case SwfTag.CODE_DEFINE_SHAPE4: - symbol = Shumway.SWF.Parser.defineShape(tag, symbols); - break; - case SwfTag.CODE_DEFINE_SOUND: - symbol = Shumway.SWF.Parser.defineSound(tag, symbols); - break; - default: - // TODO: Handle all cases here. - break; - } - symbols[tag.id] = symbol; - } - function ignoreTag(code, symbolFilters) { if (symbolFilters[0].length === 0) { return false; @@ -413,51 +373,62 @@ module Shumway.Shell { if (file.endsWith(".swf")) { var fileNameWithoutExtension = fileName.substr(0, fileName.length - 4); var SWF_TAG_CODE_DO_ABC = SwfTag.CODE_DO_ABC; - var SWF_TAG_CODE_DO_ABC_ = SwfTag.CODE_DO_ABC_; + var SWF_TAG_CODE_DO_ABC_ = SwfTag.CODE_DO_ABC_DEFINE; try { var buffer = read(file, "binary"); var startSWF = dateNow(); - Shumway.SWF.Parser.parse(buffer, { - oncomplete: function(result) { - var symbols = {}; - var tags = result.tags; - var counter = new Metrics.Counter(true); - for (var i = 0; i < tags.length; i++) { - var tag = tags[i]; - assert(tag.code !== undefined); - if (ignoreTag(tag.code, symbolFilters)) { - continue; - } - var startTag = dateNow(); - if (!parseForDatabase) { - if (tag.code === SWF_TAG_CODE_DO_ABC || tag.code === SWF_TAG_CODE_DO_ABC_) { - parseABC(tag.data); - } else { - parseSymbol(tag, symbols); - } - } - var tagName = SwfTag[tag.code]; - if (tagName) { - tagName = tagName.substring("CODE_".length); - } else { - tagName = "TAG" + tag.code; - } - counter.count(tagName, 1, dateNow() - startTag); - } - if (parseForDatabase) { - writer.writeLn(JSON.stringify({ - size: buffer.byteLength, - time: dateNow() - startSWF, - name: fileNameWithoutExtension, - tags: counter.toJSON() - }, null, 0)); - } else if (verbose) { - writer.enter("Tag Frequency:"); - counter.traceSorted(writer); - writer.outdent(); - } + var swfFile: Shumway.SWF.SWFFile; + var loadListener: ILoadListener = { + onLoadOpen: function(file: Shumway.SWF.SWFFile) { + swfFile = file; + }, + onLoadProgress: function(update: LoadProgressUpdate) { + }, + onLoadError: function() { + }, + onLoadComplete: function() { + // TODO: re-enable all-tags parsing somehow. SWFFile isn't the right tool for that. + // var symbols = {}; + // var tags = result.tags; + // var counter = new Metrics.Counter(true); + // for (var i = 0; i < tags.length; i++) { + // var tag = tags[i]; + // assert(tag.code !== undefined); + // if (ignoreTag(tag.code, symbolFilters)) { + // continue; + // } + // var startTag = dateNow(); + // if (!parseForDatabase) { + // if (tag.code === SWF_TAG_CODE_DO_ABC || tag.code === SWF_TAG_CODE_DO_ABC_) { + // parseABC(tag.data); + // } else { + // parseSymbol(tag, symbols); + // } + // } + // var tagName = SwfTag[tag.code]; + // if (tagName) { + // tagName = tagName.substring("CODE_".length); + // } else { + // tagName = "TAG" + tag.code; + // } + // counter.count(tagName, 1, dateNow() - startTag); + // } + // if (parseForDatabase) { + // writer.writeLn(JSON.stringify({ + // size: buffer.byteLength, + // time: dateNow() - startSWF, + // name: fileNameWithoutExtension, + // tags: counter.toJSON() + // }, null, 0)); + // } else if (verbose) { + // writer.enter("Tag Frequency:"); + // counter.traceSorted(writer); + // writer.outdent(); + // } } - }); + }; + var loader = new Shumway.FileLoader(loadListener); + loader.loadBytes(buffer); } catch (x) { writer.redLn("Cannot parse: " + file + ", reason: " + x); if (verbose) { diff --git a/src/swf/FileLoader.ts b/src/swf/FileLoader.ts index db7dcbd4e6..617a774fae 100644 --- a/src/swf/FileLoader.ts +++ b/src/swf/FileLoader.ts @@ -16,30 +16,35 @@ module Shumway { import assert = Shumway.Debug.assert; - import Parser = Shumway.SWF.Parser; - import IPipe = Shumway.SWF.Parser.IPipe; - import SwfTag = Shumway.SWF.Parser.SwfTag; - import createSoundStream = Shumway.SWF.Parser.createSoundStream; + import SWFFile = Shumway.SWF.SWFFile; + export class LoadProgressUpdate { + constructor(public bytesLoaded: number) { + } + } export interface ILoadListener { - onLoadOpen: Function; - onLoadProgress: Function; - onLoadComplete: Function; - onLoadError: Function; + onLoadOpen: (any) => void; + onLoadProgress: (update: LoadProgressUpdate) => void; + onLoadComplete: () => void; + onLoadError: () => void; } export class FileLoader { + _file: any; // {SWFFile|ImageFile} private _listener: ILoadListener; private _loadingServiceSession: FileLoadingSession; + private _delayedUpdates: LoadProgressUpdate[]; + private _delayedUpdatesPromise: Promise; - private _parsingPipe: IPipe; constructor(listener: ILoadListener) { release || assert(listener); this._listener = listener; this._loadingServiceSession = null; - this._parsingPipe = null; + this._file = null; + this._delayedUpdates = null; + this._delayedUpdatesPromise = null; } // TODO: strongly type @@ -54,353 +59,79 @@ module Shumway { abortLoad() { // TODO: implement } - loadBytes(bytes: ArrayBuffer) { - Parser.parse(bytes, this.createParsingContext(this.commitData.bind(this))); + loadBytes(bytes: Uint8Array) { + var file = this._file = createFileInstanceForHeader(bytes, bytes.length); + this._listener.onLoadOpen(file); + this.processSWFFileUpdate(file); } - processLoadOpen() { - this._parsingPipe = Parser.parseAsync(this.createParsingContext(this.commitData.bind(this))); + release || assert(!this._file); } processNewData(data: Uint8Array, progressInfo: {bytesLoaded: number; bytesTotal: number}) { - this._parsingPipe.push(data, progressInfo); + var file = this._file; + if (!file) { + file = this._file = createFileInstanceForHeader(data, progressInfo.bytesTotal); + this._listener.onLoadOpen(file); + } else { + file.appendLoadedData(data); + } + if (file instanceof SWFFile) { + this.processSWFFileUpdate(file); + } else { + release || assert(file instanceof ImageFile); + this._listener.onLoadProgress(new LoadProgressUpdate(progressInfo.bytesLoaded)); + if (progressInfo.bytesLoaded === progressInfo.bytesTotal) { + file.decodingPromise.then(this._listener.onLoadComplete.bind(this._listener)); + } + } } processError(error) { + Debug.warning('Loading error encountered:', error); } processLoadClose() { - this._parsingPipe.close(); + if (this._file.bytesLoaded !== this._file.bytesTotal) { + Debug.warning("Not Implemented: processing loadClose when loading was aborted"); + } } - - commitData(data) { - this._listener.onLoadProgress(data); - } - - createParsingContext(commitData) { - var commands = []; - var symbols = {}; - var frame:any = { type: 'frame' }; - var tagsProcessed = 0; - var soundStream = null; - var bytesLoaded = 0; - - return { - onstart: function (result) { - commitData({command: 'init', result: result}); - }, - onimgprogress: function (bytesTotal) { - // image progress events are sent with 1K increments - while (bytesLoaded <= bytesTotal) { - commitData({command: 'progress', result: { - bytesLoaded: bytesLoaded, - bytesTotal: bytesTotal, - open: true - }}); - bytesLoaded += Math.min(bytesTotal - bytesLoaded || 1024, 1024); - } - }, - onprogress: function (result) { - // sending progress events with 64K increments - if (result.bytesLoaded - bytesLoaded >= 65536) { - while (bytesLoaded < result.bytesLoaded) { - if (bytesLoaded) { - commitData({command: 'progress', result: { - bytesLoaded: bytesLoaded, - bytesTotal: result.bytesTotal - }}); - } - bytesLoaded += 65536; - } - } - - var tags = result.tags; - for (var n = tags.length; tagsProcessed < n; tagsProcessed++) { - var tag = tags[tagsProcessed]; - if ('id' in tag) { - var symbol = defineSymbol(tag, symbols, commitData); - commitData(symbol, symbol.transferables); - continue; - } - - switch (tag.code) { - case SwfTag.CODE_DEFINE_SCENE_AND_FRAME_LABEL_DATA: - frame.sceneData = tag; - break; - case SwfTag.CODE_DEFINE_SCALING_GRID: - var symbolUpdate = { - isSymbol: true, - id: tag.symbolId, - updates: { - scale9Grid: tag.splitter - } - }; - commitData(symbolUpdate); - break; - case SwfTag.CODE_DO_ABC: - case SwfTag.CODE_DO_ABC_: - commitData({ - type: 'abc', - flags: tag.flags, - name: tag.name, - data: tag.data - }); - break; - case SwfTag.CODE_DO_ACTION: - var actionBlocks = frame.actionBlocks; - if (actionBlocks) - actionBlocks.push(tag.actionsData); - else - frame.actionBlocks = [tag.actionsData]; - break; - case SwfTag.CODE_DO_INIT_ACTION: - var initActionBlocks = frame.initActionBlocks || - (frame.initActionBlocks = []); - initActionBlocks.push({spriteId: tag.spriteId, actionsData: tag.actionsData}); - break; - case SwfTag.CODE_START_SOUND: - commands.push(tag); - break; - case SwfTag.CODE_SOUND_STREAM_HEAD: - try { - // TODO: make transferable - soundStream = createSoundStream(tag); - frame.soundStream = soundStream.info; - } catch (e) { - // ignoring if sound stream codec is not supported - // console.error('ERROR: ' + e.message); - } - break; - case SwfTag.CODE_SOUND_STREAM_BLOCK: - if (soundStream) { - frame.soundStreamBlock = soundStream.decode(tag.data); - } - break; - case SwfTag.CODE_EXPORT_ASSETS: - var exports = frame.exports; - if (exports) - frame.exports = exports.concat(tag.exports); - else - frame.exports = tag.exports.slice(0); - break; - case SwfTag.CODE_SYMBOL_CLASS: - var symbolClasses = frame.symbolClasses; - if (symbolClasses) - frame.symbolClasses = symbolClasses.concat(tag.exports); - else - frame.symbolClasses = tag.exports.slice(0); - break; - case SwfTag.CODE_FRAME_LABEL: - frame.labelName = tag.name; - break; - case SwfTag.CODE_PLACE_OBJECT: - case SwfTag.CODE_PLACE_OBJECT2: - case SwfTag.CODE_PLACE_OBJECT3: - commands.push(tag); - break; - case SwfTag.CODE_REMOVE_OBJECT: - case SwfTag.CODE_REMOVE_OBJECT2: - commands.push(tag); - break; - case SwfTag.CODE_SET_BACKGROUND_COLOR: - frame.bgcolor = tag.color; - break; - case SwfTag.CODE_SHOW_FRAME: - frame.repeat = tag.repeat; - frame.commands = commands; - frame.complete = !!tag.finalTag; - commitData(frame); - commands = []; - frame = { type: 'frame' }; - break; - default: - Debug.warning('Dropped tag during parsing. Code: ' + tag.code + ' (' + - SwfTag[tag.code] + ')'); - } - } - - if (result.bytesLoaded >= result.bytesTotal) { - commitData({command: 'progress', result: { - bytesLoaded: result.bytesLoaded, - bytesTotal: result.bytesTotal - }}); - } - }, - oncomplete: function (result) { - commitData(result); - - var stats; - if (typeof result.swfVersion === 'number') { - // Extracting stats from the context object - var bbox = result.bbox; - stats = { - topic: 'parseInfo', // HACK additional field for telemetry - parseTime: result.parseTime, - bytesTotal: result.bytesTotal, - swfVersion: result.swfVersion, - frameRate: result.frameRate, - width: (bbox.xMax - bbox.xMin) / 20, - height: (bbox.yMax - bbox.yMin) / 20, - isAvm2: !!result.fileAttributes.doAbc - }; - } - - commitData({command: 'complete', stats: stats}); - }, - onexception: function (e) { - commitData({type: 'exception', message: e.message, stack: e.stack}); + private processSWFFileUpdate(file: SWFFile) { + var update = new LoadProgressUpdate(file.bytesLoaded); + if (!(file.pendingSymbolsPromise || this._delayedUpdatesPromise)) { + this._listener.onLoadProgress(update); + return; + } + var promise = Promise.all([file.pendingSymbolsPromise, this._delayedUpdatesPromise]); + var self = this; + promise.then(function () { + self._listener.onLoadProgress(update); + if (self._delayedUpdatesPromise === promise) { + self._delayedUpdatesPromise = null; } - }; + }); } } - function defineSymbol(swfTag, symbols, commitData) { - var symbol; + function createFileInstanceForHeader(header: Uint8Array, fileLength: number): any { + var magic = (header[0] << 16) | (header[1] << 8) | header[2]; - switch (swfTag.code) { - case SwfTag.CODE_DEFINE_BITS: - case SwfTag.CODE_DEFINE_BITS_JPEG2: - case SwfTag.CODE_DEFINE_BITS_JPEG3: - case SwfTag.CODE_DEFINE_BITS_JPEG4: - case SwfTag.CODE_JPEG_TABLES: - symbol = Shumway.SWF.Parser.defineImage(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_BITS_LOSSLESS: - case SwfTag.CODE_DEFINE_BITS_LOSSLESS2: - symbol = Shumway.SWF.Parser.defineBitmap(swfTag); - break; - case SwfTag.CODE_DEFINE_BUTTON: - case SwfTag.CODE_DEFINE_BUTTON2: - symbol = Shumway.SWF.Parser.defineButton(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_EDIT_TEXT: - symbol = Shumway.SWF.Parser.defineText(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_FONT: - case SwfTag.CODE_DEFINE_FONT2: - case SwfTag.CODE_DEFINE_FONT3: - case SwfTag.CODE_DEFINE_FONT4: - symbol = Shumway.SWF.Parser.defineFont(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_MORPH_SHAPE: - case SwfTag.CODE_DEFINE_MORPH_SHAPE2: - case SwfTag.CODE_DEFINE_SHAPE: - case SwfTag.CODE_DEFINE_SHAPE2: - case SwfTag.CODE_DEFINE_SHAPE3: - case SwfTag.CODE_DEFINE_SHAPE4: - symbol = Shumway.SWF.Parser.defineShape(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_SOUND: - symbol = Shumway.SWF.Parser.defineSound(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_BINARY_DATA: - symbol = { - type: 'binary', - id: swfTag.id, - // TODO: make transferable - data: swfTag.data - }; - break; - case SwfTag.CODE_DEFINE_SPRITE: - var commands = []; - var frame:any = { type: 'frame' }; - var frames = []; - var tags = swfTag.tags; - var frameScripts = null; - var frameIndex = 0; - var soundStream = null; - for (var i = 0, n = tags.length; i < n; i++) { - var tag:any = tags[i]; - if ('id' in tag) { - // According to Chapter 13 of the SWF format spec, no nested definition tags are - // allowed within DefineSprite. However, they're added to the symbol dictionary - // anyway, and some tools produce them. Notably swfmill. - // We essentially treat them as though they came before the current sprite. That - // should be ok because it doesn't make sense for them to rely on their parent being - // fully defined - so they don't have to come after it -, and any control tags within - // the parent will just pick them up the moment they're defined, just as always. - var symbol = defineSymbol(tag, symbols, commitData); - commitData(symbol, symbol.transferables); - continue; - } - switch (tag.code) { - case SwfTag.CODE_DO_ACTION: - if (!frameScripts) - frameScripts = []; - frameScripts.push(frameIndex); - frameScripts.push(tag.actionsData); - break; - // case SwfTag.CODE_DO_INIT_ACTION: ?? - case SwfTag.CODE_START_SOUND: - commands.push(tag); - break; - case SwfTag.CODE_SOUND_STREAM_HEAD: - try { - // TODO: make transferable - soundStream = createSoundStream(tag); - frame.soundStream = soundStream.info; - } catch (e) { - // ignoring if sound stream codec is not supported - // console.error('ERROR: ' + e.message); - } - break; - case SwfTag.CODE_SOUND_STREAM_BLOCK: - if (soundStream) { - frame.soundStreamBlock = soundStream.decode(tag.data); - } - break; - case SwfTag.CODE_FRAME_LABEL: - frame.labelName = tag.name; - break; - case SwfTag.CODE_PLACE_OBJECT: - case SwfTag.CODE_PLACE_OBJECT2: - case SwfTag.CODE_PLACE_OBJECT3: - commands.push(tag); - break; - case SwfTag.CODE_REMOVE_OBJECT: - case SwfTag.CODE_REMOVE_OBJECT2: - commands.push(tag); - break; - case SwfTag.CODE_SHOW_FRAME: - frameIndex += tag.repeat; - frame.repeat = tag.repeat; - frame.commands = commands; - frames.push(frame); - commands = []; - frame = { type: 'frame' }; - break; - default: - Debug.warning('Dropped tag during parsing. Code: ' + tag.code + ' (' + - SwfTag[tag.code] + ')'); - } - } - if (frames.length === 0) { - // We need at least one frame - frame.repeat = 1; - frame.commands = commands; - frames.push(frame); - } - symbol = { - type: 'sprite', - id: swfTag.id, - frameCount: swfTag.frameCount, - frames: frames, - frameScripts: frameScripts - }; - break; - case SwfTag.CODE_DEFINE_TEXT: - case SwfTag.CODE_DEFINE_TEXT2: - symbol = Shumway.SWF.Parser.defineLabel(swfTag, symbols); - break; - default: - Debug.warning('Dropped tag during parsing. Code: ' + tag.code + ' (' + - SwfTag[tag.code] + ')'); + if ((magic & 0xffff) === FileTypeMagicHeaderBytes.SWF /* SWF */) { + return new SWFFile(header, fileLength); } - if (!symbol) { - return {command: 'error', message: 'unknown symbol type: ' + swfTag.code}; + if (magic === FileTypeMagicHeaderBytes.JPG) { + return new ImageFile(header, fileLength); + } + if (magic === FileTypeMagicHeaderBytes.PNG) { + return new ImageFile(header, fileLength); } - symbol.isSymbol = true; - symbols[swfTag.id] = symbol; - return symbol; + // TODO: throw instead of returning null? Perhaps? + return null; + } + + enum FileTypeMagicHeaderBytes { + SWF = 0x5753, + JPG = 0xffd8ff, + PNG = 0x89504e } } diff --git a/src/swf/ImageFile.ts b/src/swf/ImageFile.ts new file mode 100644 index 0000000000..0a594fcec6 --- /dev/null +++ b/src/swf/ImageFile.ts @@ -0,0 +1,82 @@ +/** + * Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module Shumway { + enum ImageTypeMagicHeaderBytes { + JPG = 0xffd8ff, + PNG = 0x89504e, + GIF = 0x474946 + } + + var mimetypesForHeaders = {}; + mimetypesForHeaders[ImageTypeMagicHeaderBytes.JPG] = 'image/jpeg'; + mimetypesForHeaders[ImageTypeMagicHeaderBytes.PNG] = 'image/png'; + mimetypesForHeaders[ImageTypeMagicHeaderBytes.GIF] = 'image/gif'; + + export class ImageFile { + data: Uint8Array; + bytesLoaded: number; + image: any; // Image + mimeType: string; + type: number = 4; + decodingPromise: Promise; + width: number; + height: number; + + constructor(header: Uint8Array, fileLength: number) { + this.bytesLoaded = header.length; + if (header.length === fileLength) { + this.data = header; + } else { + this.data = new Uint8Array(fileLength); + this.data.set(header); + } + this.setMimetype(); + this.processLoadedData(); + } + + get bytesTotal() { + return this.data.length; + } + + appendLoadedData(data: Uint8Array) { + this.data.set(data, this.bytesLoaded); + this.bytesLoaded += data.length; + this.processLoadedData(); + } + private processLoadedData() { + if (this.bytesLoaded === this.data.length) { + var image = this.image = new Image(); + image.src = URL.createObjectURL(new Blob([this.data], {type: this.mimeType})); + //this.data = null; + this.decodingPromise = new Promise(function(resolve, reject) { + image.onload = resolve; + image.onerror = resolve; + }).then(this.decodeImage.bind(this)); + } + } + + private setMimetype() { + var magic = (this.data[0] << 16) | (this.data[1] << 8) | this.data[2]; + this.mimeType = mimetypesForHeaders[magic]; + } + + private decodeImage() { + this.width = this.image.width; + this.height = this.image.height; + } + } +} diff --git a/src/swf/SWFFile.ts b/src/swf/SWFFile.ts new file mode 100644 index 0000000000..9e036d72b6 --- /dev/null +++ b/src/swf/SWFFile.ts @@ -0,0 +1,866 @@ +/** + * Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module Shumway.SWF { + import assert = Shumway.Debug.assert; + + import Parser = Shumway.SWF.Parser; + import Stream = SWF.Stream; + import Inflate = ArrayUtilities.Inflate; + + import SWFTag = Parser.SwfTag; + + import DefinitionTags = Parser.DefinitionTags; + import ImageDefinitionTags = Parser.ImageDefinitionTags; + import FontDefinitionTags = Parser.FontDefinitionTags; + import ImageDefinition = Parser.ImageDefinition; + import ControlTags = Parser.ControlTags; + + export class SWFFile { + isCompressed: boolean; + swfVersion: number; + useAVM1: boolean; + backgroundColor: number; + bounds: Bounds; + frameRate: number; + frameCount: number; + attributes: any; // TODO: type strongly + sceneAndFrameLabelData: any; // TODO: type strongly + + bytesLoaded: number; + bytesTotal: number; + framesLoaded: number; + + frames: SWFFrame[]; + abcBlocks: ABCBlock[]; + dictionary: DictionaryEntry[]; + fonts: {name: string; id: number}[]; + + symbolClassesMap: string[]; + symbolClassesList: {id: number; className: string}[]; + eagerlyParsedSymbols: EagerlyParsedDictionaryEntry[]; + pendingSymbolsPromise: Promise; + + private _uncompressedLength: number; + private _uncompressedLoadedLength: number; + private _data: Uint8Array; + private _dataView: DataView; + private _dataStream: Stream; + private _decompressor: Inflate; + private _jpegTables: any; + private _endTagEncountered: boolean; + + private _currentFrameLabel: string; + private _currentSoundStreamHead: Parser.SoundStream; + private _currentSoundStreamBlock: Uint8Array; + private _currentDisplayListCommands: UnparsedTag[]; + private _currentActionBlocks: Uint8Array[]; + private _currentInitActionBlocks: InitActionBlock[]; + private _currentExports: SymbolExport[]; + + constructor(initialBytes: Uint8Array, length: number) { + // TODO: cleanly abort loading/parsing instead of just asserting here. + release || assert(initialBytes[0] === 67 || initialBytes[0] === 70, + "Unsupported compression format: " + (initialBytes[0] === 90 ? + "LZMA" : + initialBytes[0] + '')); + release || assert(initialBytes[1] === 87); + release || assert(initialBytes[2] === 83); + release || assert(initialBytes.length >= 30, "At least the header must be complete here."); + + if (!release && SWF.traceLevel.value > 0) { + console.log('Create SWFFile'); + } + + this.isCompressed = false; + this.swfVersion = 0; + this.useAVM1 = true; + this.backgroundColor = 0xffffffff; + this.bounds = null; + this.frameRate = 0; + this.frameCount = 0; + this.attributes = null; + this.sceneAndFrameLabelData = null; + + this.bytesLoaded = 0; + this.bytesTotal = length; + this.framesLoaded = 0; + + this.frames = []; + this.abcBlocks = []; + this.dictionary = []; + this.fonts = []; + + this.symbolClassesMap = []; + this.symbolClassesList = []; + this.eagerlyParsedSymbols = []; + this.pendingSymbolsPromise = null; + this._jpegTables = null; + + this._currentFrameLabel = null; + this._currentSoundStreamHead = null; + this._currentSoundStreamBlock = null; + this._currentDisplayListCommands = null; + this._currentActionBlocks = null; + this._currentInitActionBlocks = null; + this._currentExports = null; + this._endTagEncountered = false; + this.readHeaderAndInitialize(initialBytes); + } + + appendLoadedData(bytes: Uint8Array) { + this.pendingSymbolsPromise = null; + // TODO: only report decoded or sync-decodable bytes as loaded. + this.bytesLoaded += bytes.length; + release || assert(this.bytesLoaded <= this.bytesTotal); + // Tags after the end tag are simply ignored, so we don't even have to scan them. + if (this._endTagEncountered) { + return; + } + if (this.isCompressed) { + this._decompressor.push(bytes, true); + } else { + this.processDecompressedData(bytes); + } + this.scanLoadedData(); + } + + getSymbol(id: number) { + if (this.eagerlyParsedSymbols[id]) { + return this.eagerlyParsedSymbols[id]; + } + var unparsed = this.dictionary[id]; + if (!unparsed) { + return null; + } + var symbol; + if (unparsed.tagCode === SWFTag.CODE_DEFINE_SPRITE) { + // TODO: replace this whole silly `type` business with tagCode checking. + symbol = this.parseSpriteTimeline(unparsed); + } else { + symbol = this.getParsedTag(unparsed); + } + symbol.className = this.symbolClassesMap[id] || null; + return symbol; + } + + getParsedTag(unparsed: UnparsedTag): any { + SWF.enterTimeline('Parse tag ' + SWFTag[unparsed.tagCode]); + this._dataStream.align(); + this._dataStream.pos = unparsed.byteOffset; + var tag: any = {code: unparsed.tagCode}; + var handler = Parser.LowLevel.tagHandlers[unparsed.tagCode]; + var tagEnd = unparsed.byteOffset + unparsed.byteLength; + handler(this._data, this._dataStream, tag, this.swfVersion, unparsed.tagCode, tagEnd); + var finalPos = this._dataStream.pos; + release || assert(finalPos <= tagEnd); + if (finalPos < tagEnd) { + var consumedBytes = finalPos - unparsed.byteOffset; + Debug.warning('Scanning ' + SWFTag[unparsed.tagCode] + ' at offset ' + + unparsed.byteOffset + ' consumed ' + consumedBytes + ' of ' + + unparsed.byteLength + ' bytes. (' + (tagEnd - finalPos) + ' left)'); + this._dataStream.pos = tagEnd; + } + var symbol = defineSymbol(tag, this.dictionary); + SWF.leaveTimeline(); + return symbol; + } + + private readHeaderAndInitialize(initialBytes: Uint8Array) { + SWF.enterTimeline('Initialize SWFFile'); + this.isCompressed = initialBytes[0] === 67; + this.swfVersion = initialBytes[3]; + this._uncompressedLength = readSWFLength(initialBytes); + // TODO: only report decoded or sync-decodable bytes as loaded. + this.bytesLoaded = initialBytes.length; + this._data = new Uint8Array(this._uncompressedLength); + this._dataStream = new Stream(this._data.buffer); + this._dataStream.pos = 8; + this._dataView = this._dataStream; + + if (this.isCompressed) { + this._data.set(initialBytes.subarray(0, 8)); + this._uncompressedLoadedLength = 8; + this._decompressor = new Inflate(true); + var self = this; + // Parts of the header are compressed. Get those out of the way before starting tag parsing. + this._decompressor.onData = function(data: Uint32Array) { + self._data.set(data, self._uncompressedLoadedLength); + self._uncompressedLoadedLength += data.length; + // TODO: clean up second part of header parsing. + var obj = Parser.LowLevel.readHeader(self._data, self._dataStream); + self.bounds = obj.bounds; + self.frameRate = obj.frameRate; + self.frameCount = obj.frameCount; + self._decompressor.onData = self.processDecompressedData.bind(self); + }; + this._decompressor.push(initialBytes.subarray(8), true); + } else { + this._data.set(initialBytes); + this._uncompressedLoadedLength = initialBytes.length; + this._decompressor = null; + // TODO: clean up second part of header parsing. + var obj = Parser.LowLevel.readHeader(this._data, this._dataStream); + this.bounds = obj.bounds; + this.frameRate = obj.frameRate; + this.frameCount = obj.frameCount; + } + SWF.leaveTimeline(); + this.scanLoadedData(); + } + + private processDecompressedData(data: Uint8Array) { + this._data.set(data, this._uncompressedLoadedLength); + this._uncompressedLoadedLength += data.length; + if (this._uncompressedLoadedLength === this._uncompressedLength) { + this._decompressor = null; + } + } + + private scanLoadedData() { + SWF.enterTimeline('Scan loaded SWF file tags'); + this.scanTagsToOffset(this._uncompressedLoadedLength, true); + SWF.leaveTimeline(); + } + + private scanTagsToOffset(endOffset: number, rootTimelineMode: boolean) { + // `parsePos` is always at the start of a tag at this point, because it only gets updated + // when a tag has been fully parsed. + var tempTag = new UnparsedTag(0, 0, 0); + while (this._dataStream.pos < endOffset - 1) { + this.parseNextTagHeader(tempTag); + if (tempTag.tagCode === SWFTag.CODE_END) { + if (rootTimelineMode) { + this._endTagEncountered = true; + } + return; + } + var tagEnd = tempTag.byteOffset + tempTag.byteLength; + if (tagEnd > endOffset) { + return; + } + this.scanTag(tempTag, rootTimelineMode); + release || assert(this._dataStream.pos <= tagEnd); + if (this._dataStream.pos < tagEnd) { + this.emitTagSlopWarning(tempTag, tagEnd); + } + } + } + + /** + * Parses tag header information at the current seek offset and stores it in the given object. + * + * Public so it can be used by tools to parse through entire SWFs. + */ + parseNextTagHeader(target: UnparsedTag) { + var position = this._dataStream.pos; + var tagCodeAndLength = this._dataView.getUint16(position, true); + position += 2; + target.tagCode = tagCodeAndLength >> 6; + var tagLength = tagCodeAndLength & 0x3f; + var extendedLength = tagLength === 0x3f; + if (extendedLength) { + if (position + 4 > this._uncompressedLoadedLength) { + return; + } + tagLength = this._dataView.getUint32(position, true); + position += 4; + } + this._dataStream.pos = position; + target.byteOffset = position; + target.byteLength = tagLength; + } + + private scanTag(tag: UnparsedTag, rootTimelineMode: boolean): void { + var stream: Stream = this._dataStream; + var byteOffset = stream.pos; + release || assert(byteOffset === tag.byteOffset); + var tagCode = tag.tagCode; + var tagLength = tag.byteLength; + if (!release && traceLevel.value > 1) { + console.info("Scanning tag " + SWFTag[tagCode] + " (start: " + byteOffset + + ", end: " + (byteOffset + tagLength) + ")"); + } + + if (tagCode === SWFTag.CODE_DEFINE_SPRITE) { + // According to Chapter 13 of the SWF format spec, no nested definition tags are + // allowed within DefineSprite. However, they're added to the symbol dictionary + // anyway, and some tools produce them. Notably swfmill. + // We essentially treat them as though they came before the current sprite. That + // should be ok because it doesn't make sense for them to rely on their parent being + // fully defined - so they don't have to come after it -, and any control tags within + // the parent will just pick them up the moment they're defined, just as always. + this.addLazySymbol(tagCode, byteOffset, tagLength); + var spriteTagEnd = byteOffset + tagLength; + stream.pos += 4; // Jump over symbol ID and frameCount. + this.scanTagsToOffset(spriteTagEnd, false); + if (this._dataStream.pos < tagEnd) { + this.emitTagSlopWarning(tag, tagEnd); + stream.pos = spriteTagEnd; + } + return; + } + if (ImageDefinitionTags[tagCode]) { + // Images are decoded asynchronously, so we have to deal with them ahead of time to + // ensure they're ready when used. + var unparsed = this.addLazySymbol(tagCode, byteOffset, tagLength); + this.decodeEmbeddedImage(unparsed); + return; + } + if (FontDefinitionTags[tagCode]) { + var unparsed = this.addLazySymbol(tagCode, byteOffset, tagLength); + if (!inFirefox) { + this.decodeEmbeddedFont(unparsed); + } else { + this.registerEmbeddedFont(unparsed); + } + return; + } + if (DefinitionTags[tagCode]) { + this.addLazySymbol(tagCode, byteOffset, tagLength); + this.jumpToNextTag(tagLength); + return; + } + + if (!rootTimelineMode && + !(tagCode === SWFTag.CODE_SYMBOL_CLASS || tagCode === SWFTag.CODE_EXPORT_ASSETS)) { + this.jumpToNextTag(tagLength); + return; + } + + if (ControlTags[tagCode]) { + this.addControlTag(tagCode, byteOffset, tagLength); + return; + } + + switch (tagCode) { + case SWFTag.CODE_FILE_ATTRIBUTES: + this.setFileAttributes(tagLength); + break; + case SWFTag.CODE_DEFINE_SCENE_AND_FRAME_LABEL_DATA: + this.setSceneAndFrameLabelData(tagLength); + break; + case SWFTag.CODE_SET_BACKGROUND_COLOR: + this.backgroundColor = Parser.LowLevel.rgb(this._data, this._dataStream); + break; + case SWFTag.CODE_JPEG_TABLES: + // Only use the first JpegTables tag, ignore any following. + if (!this._jpegTables) { + this._jpegTables = tagLength === 0 ? + new Uint8Array(0) : + this._data.subarray(stream.pos, stream.pos + tagLength - 2); + } + this.jumpToNextTag(tagLength); + break; + case SWFTag.CODE_DO_ABC: + case SWFTag.CODE_DO_ABC_DEFINE: + if (!this.useAVM1) { + var tagEnd = byteOffset + tagLength; + var abcBlock = new ABCBlock(); + if (tagCode === SWFTag.CODE_DO_ABC) { + abcBlock.flags = Parser.readUi32(this._data, stream); + abcBlock.name = Parser.readString(this._data, stream, 0); + } + else { + abcBlock.flags = 0; + abcBlock.name = ""; + } + abcBlock.data = this._data.subarray(stream.pos, tagEnd); + this.abcBlocks.push(abcBlock); + stream.pos = tagEnd; + } else { + this.jumpToNextTag(tagLength); + } + break; + case SWFTag.CODE_SYMBOL_CLASS: + var tagEnd = byteOffset + tagLength; + var symbolCount = Parser.readUi16(this._data, stream); + // TODO: check if symbols can be reassociated after instances have been created. + while (symbolCount--) { + var symbolId = Parser.readUi16(this._data, stream); + var symbolClassName = Parser.readString(this._data, stream, 0); + this.symbolClassesMap[symbolId] = symbolClassName; + this.symbolClassesList.push({id: symbolId, className: symbolClassName}); + } + // Make sure we move to end of tag even if the content is invalid. + stream.pos = tagEnd; + break; + case SWFTag.CODE_DO_INIT_ACTION: + if (this.useAVM1) { + var initActionBlocks = this._currentInitActionBlocks || + (this._currentInitActionBlocks = []); + var spriteId = this._dataView.getUint16(stream.pos, true); + var actionsData = this._data.subarray(byteOffset + 2, byteOffset + tagLength); + initActionBlocks.push({spriteId: spriteId, actionsData: actionsData}); + } + this.jumpToNextTag(tagLength); + break; + case SWFTag.CODE_DO_ACTION: + if (this.useAVM1) { + var actionBlocks = this._currentActionBlocks || (this._currentActionBlocks = []); + actionBlocks.push(this._data.subarray(stream.pos, stream.pos + tagLength)); + } + this.jumpToNextTag(tagLength); + break; + case SWFTag.CODE_SOUND_STREAM_HEAD: + case SWFTag.CODE_SOUND_STREAM_HEAD2: + var soundStreamTag = Parser.LowLevel.soundStreamHead(this._data, this._dataStream); + this._currentSoundStreamHead = Parser.SoundStream.FromTag(soundStreamTag); + break; + case SWFTag.CODE_SOUND_STREAM_BLOCK: + this._currentSoundStreamBlock = this._data.subarray(stream.pos, stream.pos += tagLength); + break; + case SWFTag.CODE_FRAME_LABEL: + var tagEnd = stream.pos + tagLength; + this._currentFrameLabel = Parser.readString(this._data, stream, 0); + // TODO: support SWF6+ anchors. + stream.pos = tagEnd; + break; + case SWFTag.CODE_SHOW_FRAME: + this.finishFrame(); + break; + case SWFTag.CODE_END: + return; + case SWFTag.CODE_EXPORT_ASSETS: + var tagEnd = stream.pos + tagLength; + var exportsCount = Parser.readUi16(this._data, stream); + var exports = this._currentExports || (this._currentExports = []); + while (exportsCount--) { + var symbolId = Parser.readUi16(this._data, stream); + var className = Parser.readString(this._data, stream, 0); + if (stream.pos > tagEnd) { + stream.pos = tagEnd; + break; + } + exports.push(new SymbolExport(symbolId, className)); + } + stream.pos = tagEnd; + break; + case SWFTag.CODE_DEFINE_BUTTON_CXFORM: + case SWFTag.CODE_DEFINE_BUTTON_SOUND: + case SWFTag.CODE_DEFINE_FONT_INFO: + case SWFTag.CODE_DEFINE_FONT_INFO2: + case SWFTag.CODE_DEFINE_SCALING_GRID: + case SWFTag.CODE_IMPORT_ASSETS: + case SWFTag.CODE_IMPORT_ASSETS2: + Debug.warning('Unsupported tag encountered ' + tagCode + ': ' + SWFTag[tagCode]); + this.jumpToNextTag(tagLength); + break; + // These tags should be supported at some point, but for now, we ignore them. + case SWFTag.CODE_CSM_TEXT_SETTINGS: + case SWFTag.CODE_DEFINE_FONT_ALIGN_ZONES: + case SWFTag.CODE_SCRIPT_LIMITS: + case SWFTag.CODE_SET_TAB_INDEX: + this.jumpToNextTag(tagLength); + break; + // These tags are used by the player, but not relevant to us. + case SWFTag.CODE_ENABLE_DEBUGGER: + case SWFTag.CODE_ENABLE_DEBUGGER2: + case SWFTag.CODE_DEBUG_ID: + case SWFTag.CODE_DEFINE_FONT_NAME: + case SWFTag.CODE_PRODUCT_INFO: + case SWFTag.CODE_METADATA: + case SWFTag.CODE_PROTECT: + this.jumpToNextTag(tagLength); + break; + // These tags aren't used in the player. + case SWFTag.CODE_CHARACTER_SET: + case SWFTag.CODE_DEFINE_BEHAVIOUR: + case SWFTag.CODE_DEFINE_COMMAND_OBJECT: + case SWFTag.CODE_DEFINE_FUNCTION: + case SWFTag.CODE_DEFINE_TEXT_FORMAT: + case SWFTag.CODE_DEFINE_VIDEO: + case SWFTag.CODE_EXTERNAL_FONT: + case SWFTag.CODE_FREE_CHARACTER: + case SWFTag.CODE_FREE_ALL: + case SWFTag.CODE_GENERATE_FRAME: + case SWFTag.CODE_STOP_SOUND: + case SWFTag.CODE_SYNC_FRAME: + console.info("Ignored tag (these shouldn't occur) " + tagCode + ': ' + SWFTag[tagCode]); + this.jumpToNextTag(tagLength); + break; + default: + Debug.warning('Tag not handled by the parser: ' + tagCode + ': ' + SWFTag[tagCode]); + this.jumpToNextTag(tagLength); + } + } + + parseSpriteTimeline(spriteTag: DictionaryEntry) { + SWF.enterTimeline("parseSpriteTimeline"); + var data = this._data; + var stream = this._dataStream; + var dataView = this._dataView; + var timeline: any = { + id: spriteTag.id, + type: 'sprite', + frames: [] + } + var spriteTagEnd = spriteTag.byteOffset + spriteTag.byteLength; + var frames = timeline.frames; + var label: string = null; + var commands: UnparsedTag[] = []; + var soundStreamHead: Parser.SoundStream = null; + var soundStreamBlock: Uint8Array = null; + var actionBlocks: Uint8Array[] = null; + var initActionBlocks: {spriteId: number; actionsData: Uint8Array}[] = null; + // Skip ID. + stream.pos = spriteTag.byteOffset + 2; + // TODO: check if numFrames or the real number of ShowFrame tags wins. (Probably the former.) + timeline.frameCount = dataView.getUint16(stream.pos, true); + stream.pos += 2; + var spriteContentTag = new UnparsedTag(0, 0, 0); + while (stream.pos < spriteTagEnd) { + this.parseNextTagHeader(spriteContentTag); + var tagLength = spriteContentTag.byteLength; + var tagCode = spriteContentTag.tagCode; + if (stream.pos + tagLength > spriteTagEnd) { + Debug.warning("DefineSprite child tags exceed DefineSprite tag length and are dropped"); + break; + } + + if (Parser.ControlTags[tagCode]) { + commands.push(new UnparsedTag(tagCode, stream.pos, tagLength)); + stream.pos += tagLength; + continue; + } + + switch (tagCode) { + case SWFTag.CODE_DO_ACTION: + if (this.useAVM1) { + if (!actionBlocks) { + actionBlocks = []; + } + actionBlocks.push(data.subarray(stream.pos, stream.pos + tagLength)); + } + break; + case SWFTag.CODE_DO_INIT_ACTION: + if (this.useAVM1) { + if (!initActionBlocks) { + initActionBlocks = []; + } + var spriteId = dataView.getUint16(stream.pos, true); + stream.pos += 2; + var actionsData = data.subarray(stream.pos, stream.pos + tagLength); + initActionBlocks.push({spriteId: spriteId, actionsData: actionsData}); + } + break; + case SWFTag.CODE_FRAME_LABEL: + var tagEnd = stream.pos + tagLength; + label = Parser.readString(data, stream, 0); + // TODO: support SWF6+ anchors. + stream.pos = tagEnd; + tagLength = 0; + break; + case SWFTag.CODE_SHOW_FRAME: + frames.push(new SWFFrame(label, commands, soundStreamHead, soundStreamBlock, + actionBlocks, initActionBlocks, null)); + label = null; + commands = []; + soundStreamHead = null; + soundStreamBlock = null; + actionBlocks = null; + initActionBlocks = null; + break; + case SWFTag.CODE_END: + stream.pos = spriteTagEnd; + tagLength = 0; + break; + default: + // Ignore other tags. + } + stream.pos += tagLength; + release || assert(stream.pos <= spriteTagEnd); + } + SWF.leaveTimeline(); + return timeline; + } + + private jumpToNextTag(currentTagLength: number) { + this._dataStream.pos += currentTagLength; + } + + private emitTagSlopWarning(tag: UnparsedTag, tagEnd: number) { + var consumedBytes = this._dataStream.pos - tag.byteOffset; + Debug.warning('Scanning ' + SWFTag[tag.tagCode] + ' at offset ' + tag.byteOffset + + ' consumed ' + consumedBytes + ' of ' + tag.byteLength + ' bytes. (' + + (tag.byteLength - consumedBytes) + ' left)'); + this._dataStream.pos = tagEnd; + } + + private finishFrame() { + if (this.framesLoaded === this.frames.length) { + this.framesLoaded++; + } + this.frames.push(new SWFFrame(this._currentFrameLabel, + this._currentDisplayListCommands, + this._currentSoundStreamHead, + this._currentSoundStreamBlock, + this._currentActionBlocks, + this._currentInitActionBlocks, + this._currentExports)); + this._currentFrameLabel = null; + this._currentDisplayListCommands = null; + this._currentSoundStreamHead = null; + this._currentSoundStreamBlock = null; + this._currentActionBlocks = null; + this._currentInitActionBlocks = null; + this._currentExports = null; + } + + private setFileAttributes(tagLength: number) { + // TODO: check what happens to attributes tags that aren't the first tag. + if (this.attributes) { + this.jumpToNextTag(tagLength); + } + var bits = this._data[this._dataStream.pos]; + this._dataStream.pos += 4; + this.attributes = { + network: bits & 0x1, + relativeUrls: bits & 0x2, + noCrossDomainCaching: bits & 0x4, + doAbc: bits & 0x8, + hasMetadata: bits & 0x10, + useGpu: bits & 0x20, + useDirectBlit : bits & 0x40 + }; + this.useAVM1 = !this.attributes.doAbc; + } + + private setSceneAndFrameLabelData(tagLength: number) { + if (this.sceneAndFrameLabelData) { + this.jumpToNextTag(tagLength); + } + this.sceneAndFrameLabelData = Parser.LowLevel.defineScene(this._data, this._dataStream, null); + } + + private addControlTag(tagCode: number, byteOffset: number, tagLength: number) { + var commands = this._currentDisplayListCommands || (this._currentDisplayListCommands = []); + commands.push(new UnparsedTag(tagCode, byteOffset, tagLength)); + this.jumpToNextTag(tagLength); + + } + private addLazySymbol(tagCode: number, byteOffset: number, tagLength: number) { + var id = this._dataStream.getUint16(this._dataStream.pos, true); + var symbol = new DictionaryEntry(id, tagCode, byteOffset, tagLength); + this.dictionary[id] = symbol; + if (!release && traceLevel.value > 0) { + console.info("Registering symbol " + id + " of type " + SWFTag[tagCode]); + } + return symbol; + } + + private decodeEmbeddedFont(unparsed: UnparsedTag) { + var definition = this.getParsedTag(unparsed); + var symbol = new EagerlyParsedDictionaryEntry(definition.id, unparsed, 'font', definition); + if (!release && traceLevel.value > 0) { + console.info("Decoding embedded font " + definition.id + " with name '" + + definition.name + "'", definition); + } + this.eagerlyParsedSymbols[symbol.id] = symbol; + // Nothing more to do for glyph-less fonts. + if (!definition.data) { + return; + } + this.fonts.push({name: definition.name, id: definition.id}); + // Firefox decodes fonts synchronously, so we don't need to delay other processing until it's + // done. For other browsers, delay for a time that should be enough in all cases. + var promise = new Promise((resolve, reject) => setTimeout(resolve, 400)); + promise.then(this.markSymbolAsDecoded.bind(this, symbol)); + var currentPromise = this.pendingSymbolsPromise; + this.pendingSymbolsPromise = currentPromise ? + Promise.all([currentPromise, promise]) : + promise; + } + + private registerEmbeddedFont(unparsed: UnparsedTag) { + // DefineFont only specifies a symbol ID, no font name, so we don't have to do anything here. + if (unparsed.tagCode === SWFTag.CODE_DEFINE_FONT) { + return; + } + var stream = this._dataStream; + var id = stream.getUint16(stream.pos, true); + // Skip flags and language code. + var nameLength = this._data[stream.pos + 4]; + stream.pos += 5; + var name = Parser.readString(this._data, stream, nameLength); + this.fonts.push({name: name, id: id}); + if (!release && traceLevel.value > 0) { + console.info("Registering embedded font " + id + " with name '" + name + "'"); + } + stream.pos = unparsed.byteOffset + unparsed.byteLength; + } + + private decodeEmbeddedImage(unparsed: UnparsedTag) { + var definition = this.getParsedTag(unparsed); + var symbol = new EagerlyParsedDictionaryEntry(definition.id, unparsed, 'image', definition); + if (!release && traceLevel.value > 0) { + console.info("Decoding embedded image " + definition.id + " of type " + + SWFTag[unparsed.tagCode] + " (start: " + unparsed.byteOffset + + ", end: " + (unparsed.byteOffset + unparsed.byteLength) + ")"); + } + this.eagerlyParsedSymbols[symbol.id] = symbol; + var promise = decodeImage(definition, this.markSymbolAsDecoded.bind(this, symbol)); + var currentPromise = this.pendingSymbolsPromise; + this.pendingSymbolsPromise = currentPromise ? + Promise.all([currentPromise, promise]) : + promise; + } + + private markSymbolAsDecoded(symbol: EagerlyParsedDictionaryEntry, event?: any) { + symbol.ready = true; + if (!release && traceLevel.value > 0) { + console.info("Marking symbol " + symbol.id + " as decoded.", symbol); + } + if (event && event.type === 'error') { + Debug.warning("Decoding of image symbol failed", symbol, event); + } + } + } + + function decodeImage(definition: ImageDefinition, oncomplete: (event: any) => void) { + var image = definition.image = new Image(); + image.src = URL.createObjectURL(new Blob([definition.data], {type: definition.mimeType})); + return new Promise(function(resolve, reject) { + image.onload = resolve; + image.onerror = resolve; + }).then(oncomplete); + } + + export class SWFFrame { + labelName: string; + displayListCommands: UnparsedTag[]; + soundStreamHead: Parser.SoundStream; + soundStreamBlock: Uint8Array; + actionBlocks: Uint8Array[]; + initActionBlocks: InitActionBlock[]; + exports: SymbolExport[]; + constructor(labelName: string, commands: UnparsedTag[], + soundStreamHead: Parser.SoundStream, + soundStreamBlock: Uint8Array, + actionBlocks: Uint8Array[], + initActionBlocks: InitActionBlock[], + exports: SymbolExport[]) { + this.labelName = labelName; + release || commands && Object.freeze(commands); + this.displayListCommands = commands; + release || actionBlocks && Object.freeze(actionBlocks); + this.soundStreamHead = soundStreamHead; + this.soundStreamBlock = soundStreamBlock; + this.actionBlocks = actionBlocks; + release || initActionBlocks && Object.freeze(initActionBlocks); + this.initActionBlocks = initActionBlocks; + release || exports && Object.freeze(exports); + this.exports = exports; + } + } + + export class ABCBlock { + name: string; + flags: number; + data: Uint8Array; + } + + export class InitActionBlock { + spriteId: number; + actionsData: Uint8Array; + } + + export class SymbolExport { + constructor(public symbolId: number, public className: string) {} + } + + export class UnparsedTag { + constructor(public tagCode: number, public byteOffset: number, public byteLength: number) {} + } + + export class DictionaryEntry extends UnparsedTag { + public id: number; + constructor(id: number, tagCode: number, byteOffset: number, byteLength: number) { + super(tagCode, byteOffset, byteLength); + this.id = id; + } + } + + export class EagerlyParsedDictionaryEntry extends DictionaryEntry { + type: string; + definition: Object; + ready: boolean; + constructor(id: number, unparsed: UnparsedTag, type: string, definition: any) { + super(id, unparsed.tagCode, unparsed.byteOffset, unparsed.byteLength); + this.type = type; + this.definition = definition; + this.ready = false; + } + } + + function readSWFLength(bytes: Uint8Array) { + // We read the length manually because creating a DataView just for that is silly. + return bytes[4] | bytes[5] << 8 | bytes[6] << 16 | bytes[7] << 24; + } + + var inFirefox = typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Firefox') >= 0; + + function defineSymbol(swfTag, symbols) { + switch (swfTag.code) { + case SWFTag.CODE_DEFINE_BITS: + case SWFTag.CODE_DEFINE_BITS_JPEG2: + case SWFTag.CODE_DEFINE_BITS_JPEG3: + case SWFTag.CODE_DEFINE_BITS_JPEG4: + return Shumway.SWF.Parser.defineImage(swfTag); + case SWFTag.CODE_DEFINE_BITS_LOSSLESS: + case SWFTag.CODE_DEFINE_BITS_LOSSLESS2: + return Shumway.SWF.Parser.defineBitmap(swfTag); + case SWFTag.CODE_DEFINE_BUTTON: + case SWFTag.CODE_DEFINE_BUTTON2: + return Shumway.SWF.Parser.defineButton(swfTag, symbols); + case SWFTag.CODE_DEFINE_EDIT_TEXT: + return Shumway.SWF.Parser.defineText(swfTag); + case SWFTag.CODE_DEFINE_FONT: + case SWFTag.CODE_DEFINE_FONT2: + case SWFTag.CODE_DEFINE_FONT3: + case SWFTag.CODE_DEFINE_FONT4: + var symbol = Shumway.SWF.Parser.defineFont(swfTag); + // Only register fonts with embedded glyphs. + if (symbol.data) { + Shumway.registerCSSFont(symbol.id, symbol.data, !inFirefox); + } + return symbol; + case SWFTag.CODE_DEFINE_MORPH_SHAPE: + case SWFTag.CODE_DEFINE_MORPH_SHAPE2: + case SWFTag.CODE_DEFINE_SHAPE: + case SWFTag.CODE_DEFINE_SHAPE2: + case SWFTag.CODE_DEFINE_SHAPE3: + case SWFTag.CODE_DEFINE_SHAPE4: + return Shumway.SWF.Parser.defineShape(swfTag); + case SWFTag.CODE_DEFINE_SOUND: + return Shumway.SWF.Parser.defineSound(swfTag); + case SWFTag.CODE_DEFINE_SPRITE: + // Sprites are fully defined at this point. + return swfTag; + case SWFTag.CODE_DEFINE_BINARY_DATA: + return { + type: 'binary', + id: swfTag.id, + data: swfTag.data + }; + case SWFTag.CODE_DEFINE_TEXT: + case SWFTag.CODE_DEFINE_TEXT2: + return Shumway.SWF.Parser.defineLabel(swfTag); + default: + return swfTag; + } + } +} diff --git a/src/swf/options.ts b/src/swf/options.ts new file mode 100644 index 0000000000..24fd49b35f --- /dev/null +++ b/src/swf/options.ts @@ -0,0 +1,12 @@ +module Shumway.SWF { + import Option = Shumway.Options.Option; + import OptionSet = Shumway.Options.OptionSet; + + import shumwayOptions = Shumway.Settings.shumwayOptions; + + export var parserOptions = shumwayOptions.register(new OptionSet("Parser Options")); + + export var traceLevel = parserOptions.register(new Option("parsertracelevel", + "Parser Trace Level", "number", 0, + "Parser Trace Level")); +} diff --git a/src/swf/parser/bitmap.ts b/src/swf/parser/bitmap.ts index ed3cd1f312..e07b9f332b 100644 --- a/src/swf/parser/bitmap.ts +++ b/src/swf/parser/bitmap.ts @@ -176,9 +176,8 @@ module Shumway.SWF.Parser { return null; } - export function defineBitmap(tag: any): ImageDefinition { + export function defineBitmap(tag: any): {definition: ImageDefinition; type: string} { enterTimeline("defineBitmap"); - var bmpData = tag.bmpData; var data: Uint32Array; var type = ImageType.None; switch (tag.format) { @@ -199,13 +198,17 @@ module Shumway.SWF.Parser { } leaveTimeline(); return { - type: 'image', - id: tag.id, - width: tag.width, - height: tag.height, - mimeType: 'application/octet-stream', - data: data, - dataType: type + definition: { + type: 'image', + id: tag.id, + width: tag.width, + height: tag.height, + mimeType: 'application/octet-stream', + data: data, + dataType: type, + image: null + }, + type: 'image' }; } } diff --git a/src/swf/parser/button.ts b/src/swf/parser/button.ts index 8c6531dcab..c9f05b93b7 100644 --- a/src/swf/parser/button.ts +++ b/src/swf/parser/button.ts @@ -31,9 +31,10 @@ module Shumway.SWF.Parser { if (character.eob) break; var characterItem = dictionary[character.symbolId]; - release || assert(characterItem, 'undefined character button'); + release || characterItem || Debug.warning('undefined character in button ' + tag.id); var cmd = { symbolId: characterItem.id, + code: SwfTag.CODE_PLACE_OBJECT, depth: character.depth, flags: character.matrix ? PlaceObjectFlags.HasMatrix : 0, matrix: character.matrix diff --git a/src/swf/parser/font.ts b/src/swf/parser/font.ts index 3afd35099e..226ac5cdd5 100644 --- a/src/swf/parser/font.ts +++ b/src/swf/parser/font.ts @@ -98,7 +98,7 @@ module Shumway.SWF.Parser { return maxDimension > 5000; } - export function defineFont(tag, dictionary) { + export function defineFont(tag) { var uniqueName = 'swf-font-' + tag.id; var fontName = tag.name || uniqueName; diff --git a/src/swf/parser/handlers.ts b/src/swf/parser/handlers.ts index 9f501a0664..1ed02bd426 100644 --- a/src/swf/parser/handlers.ts +++ b/src/swf/parser/handlers.ts @@ -15,7 +15,7 @@ */ /// -module Shumway.SWF.Parser { +module Shumway.SWF.Parser.LowLevel { function defineShape($bytes, $stream, output, swfVersion, tagCode) { output || (output = {}); output.id = readUi16($bytes, $stream); @@ -45,85 +45,90 @@ module Shumway.SWF.Parser { return output; } - function placeObject($bytes, $stream, $, swfVersion, tagCode) { + function placeObject($bytes, $stream, $, swfVersion, tagCode, tagEnd) { var flags; $ || ($ = {}); - if (tagCode > SwfTag.CODE_PLACE_OBJECT) { - flags = $.flags = tagCode > SwfTag.CODE_PLACE_OBJECT2 ? - readUi16($bytes, $stream) : - readUi8($bytes, $stream); + if (tagCode === SwfTag.CODE_PLACE_OBJECT) { + $.symbolId = readUi16($bytes, $stream); $.depth = readUi16($bytes, $stream); - if (flags & PlaceObjectFlags.HasClassName) { - $.className = readString($bytes, $stream, 0); - } - if (flags & PlaceObjectFlags.HasCharacter) { - $.symbolId = readUi16($bytes, $stream); - } - if (flags & PlaceObjectFlags.HasMatrix) { - var $0 = $.matrix = {}; - matrix($bytes, $stream, $0, swfVersion, tagCode); - } - if (flags & PlaceObjectFlags.HasColorTransform) { - var $1 = $.cxform = {}; - cxform($bytes, $stream, $1, swfVersion, tagCode); - } - if (flags & PlaceObjectFlags.HasRatio) { - $.ratio = readUi16($bytes, $stream); - } - if (flags & PlaceObjectFlags.HasName) { - $.name = readString($bytes, $stream, 0); - } - if (flags & PlaceObjectFlags.HasClipDepth) { - $.clipDepth = readUi16($bytes, $stream); + $.flags |= PlaceObjectFlags.HasMatrix; + $.matrix = matrix($bytes, $stream); + if ($stream.pos < tagEnd) { + $.flags |= PlaceObjectFlags.HasColorTransform; + var $31 = $.cxform = {}; + cxform($bytes, $stream, $31, tagCode); } - if (flags & PlaceObjectFlags.HasFilterList) { - var count = readUi8($bytes, $stream); - var $2 = $.filters = []; - var $3 = count; - while ($3--) { - var $4 = {}; - anyFilter($bytes, $stream, $4, swfVersion, tagCode); - $2.push($4); - } + return $; + } + flags = $.flags = tagCode > SwfTag.CODE_PLACE_OBJECT2 ? + readUi16($bytes, $stream) : + readUi8($bytes, $stream); + $.depth = readUi16($bytes, $stream); + if (flags & PlaceObjectFlags.HasClassName) { + $.className = readString($bytes, $stream, 0); + } + if (flags & PlaceObjectFlags.HasCharacter) { + $.symbolId = readUi16($bytes, $stream); + } + if (flags & PlaceObjectFlags.HasMatrix) { + $.matrix = matrix($bytes, $stream); + } + if (flags & PlaceObjectFlags.HasColorTransform) { + var $1 = $.cxform = {}; + cxform($bytes, $stream, $1, tagCode); + } + if (flags & PlaceObjectFlags.HasRatio) { + $.ratio = readUi16($bytes, $stream); + } + if (flags & PlaceObjectFlags.HasName) { + $.name = readString($bytes, $stream, 0); + } + if (flags & PlaceObjectFlags.HasClipDepth) { + $.clipDepth = readUi16($bytes, $stream); + } + if (flags & PlaceObjectFlags.HasFilterList) { + var count = readUi8($bytes, $stream); + var $2 = $.filters = []; + var $3 = count; + while ($3--) { + var $4 = {}; + anyFilter($bytes, $stream, $4); + $2.push($4); } - if (flags & PlaceObjectFlags.HasBlendMode) { - $.blendMode = readUi8($bytes, $stream); + } + if (flags & PlaceObjectFlags.HasBlendMode) { + $.blendMode = readUi8($bytes, $stream); + } + if (flags & PlaceObjectFlags.HasCacheAsBitmap) { + $.bmpCache = readUi8($bytes, $stream); + } + if (flags & PlaceObjectFlags.HasVisible) { + $.visibility = readUi8($bytes, $stream); + } + if (flags & PlaceObjectFlags.OpaqueBackground) { + $.backgroundColor = argb($bytes, $stream); + } + if (flags & PlaceObjectFlags.HasClipActions) { + var reserved = readUi16($bytes, $stream); + if (swfVersion >= 6) { + var allFlags = readUi32($bytes, $stream); } - if (flags & PlaceObjectFlags.HasCacheAsBitmap) { - $.bmpCache = readUi8($bytes, $stream); + else { + var allFlags = readUi16($bytes, $stream); } - if (flags & PlaceObjectFlags.HasClipActions) { - var reserved = readUi16($bytes, $stream); - if (swfVersion >= 6) { - var allFlags = readUi32($bytes, $stream); + var $28 = $.events = []; + do { + var $29 = {}; + if (events($bytes, $stream, $29, swfVersion)) { + break; } - else { - var allFlags = readUi16($bytes, $stream); + if ($stream.pos > tagEnd) { + Debug.warning('PlaceObject handler attempted to read clip events beyond tag end'); + $stream.pos = tagEnd; + break; } - var $28 = $.events = []; - do { - var $29 = {}; - var eoe = events($bytes, $stream, $29, swfVersion, tagCode); - $28.push($29); - } while (!eoe); - } - if (flags & PlaceObjectFlags.OpaqueBackground) { - $.backgroundColor = argb($bytes, $stream); - } - if (flags & PlaceObjectFlags.HasVisible) { - $.visibility = readUi8($bytes, $stream); - } - } else { - $.symbolId = readUi16($bytes, $stream); - $.depth = readUi16($bytes, $stream); - $.flags |= PlaceObjectFlags.HasMatrix; - var $30 = $.matrix = {}; - matrix($bytes, $stream, $30, swfVersion, tagCode); - if ($stream.remaining()) { - $.flags |= PlaceObjectFlags.HasColorTransform; - var $31 = $.cxform = {}; - cxform($bytes, $stream, $31, swfVersion, tagCode); - } + $28.push($29); + } while (true); } return $; } @@ -137,60 +142,63 @@ module Shumway.SWF.Parser { return $; } - function defineImage($bytes, $stream, $, swfVersion, tagCode) { + export function defineImage($bytes: Uint8Array, $stream: Stream, $, swfVersion, tagCode: number, + tagEnd: number, jpegTables: Uint8Array) { var imgData; - $ || ($ = {}); - $.id = readUi16($bytes, $stream); + var tag: any = $ || {}; + tag.id = readUi16($bytes, $stream); if (tagCode > 21) { var alphaDataOffset = readUi32($bytes, $stream); if (tagCode === 90) { - $.deblock = readFixed8($bytes, $stream); + tag.deblock = readFixed8($bytes, $stream); } - imgData = $.imgData = readBinary($bytes, $stream, alphaDataOffset, true); - $.alphaData = readBinary($bytes, $stream, 0, true); + alphaDataOffset += $stream.pos; + imgData = tag.imgData = $bytes.subarray($stream.pos, alphaDataOffset); + tag.alphaData = $bytes.subarray(alphaDataOffset, tagEnd); + $stream.pos = tagEnd; } else { - imgData = $.imgData = readBinary($bytes, $stream, 0, true); + imgData = tag.imgData = $bytes.subarray($stream.pos, tagEnd); + $stream.pos = tagEnd; } switch (imgData[0] << 8 | imgData[1]) { case 65496: case 65497: - $.mimeType = "image/jpeg"; + tag.mimeType = "image/jpeg"; break; case 35152: - $.mimeType = "image/png"; + tag.mimeType = "image/png"; break; case 18249: - $.mimeType = "image/gif"; + tag.mimeType = "image/gif"; break; default: - $.mimeType = "application/octet-stream"; - } - if (tagCode === 6) { - $.incomplete = 1; + tag.mimeType = "application/octet-stream"; } - return $; + tag.jpegTables = tagCode === 6 ? jpegTables : null; + return tag; } - function defineButton($bytes, $stream, $, swfVersion, tagCode) { + function defineButton($bytes, $stream, $, swfVersion, tagCode, tagEnd) { var eob: boolean; $ || ($ = {}); $.id = readUi16($bytes, $stream); - if (tagCode == 7) { - var $0 = $.characters = []; + if (tagCode == SwfTag.CODE_DEFINE_BUTTON) { + var characters = $.characters = []; do { var $1 = {}; var temp = button($bytes, $stream, $1, swfVersion, tagCode); eob = temp.eob; - $0.push($1); + characters.push($1); } while (!eob); - $.actionsData = readBinary($bytes, $stream, 0, false); + $.actionsData = $bytes.subarray($stream.pos, tagEnd); + $stream.pos = tagEnd; } else { var trackFlags = readUi8($bytes, $stream); $.trackAsMenu = trackFlags >> 7 & 1; var actionOffset = readUi16($bytes, $stream); - var $28 = $.characters = []; + var characters = $.characters = []; do { var $29: any = {}; var flags = readUi8($bytes, $stream); @@ -209,11 +217,10 @@ module Shumway.SWF.Parser { if (!eob) { $29.symbolId = readUi16($bytes, $stream); $29.depth = readUi16($bytes, $stream); - var $30 = $29.matrix = {}; - matrix($bytes, $stream, $30, swfVersion, tagCode); + $29.matrix = matrix($bytes, $stream); if (tagCode === 34) { var $31 = $29.cxform = {}; - cxform($bytes, $stream, $31, swfVersion, tagCode); + cxform($bytes, $stream, $31, tagCode); } if ($29.flags & PlaceObjectFlags.HasFilterList) { var count = readUi8($bytes, $stream); @@ -221,7 +228,7 @@ module Shumway.SWF.Parser { var $3 = count; while ($3--) { var $4 = {}; - anyFilter($bytes, $stream, $4, swfVersion, tagCode); + anyFilter($bytes, $stream, $4); $2.push($4); } } @@ -229,43 +236,33 @@ module Shumway.SWF.Parser { $29.blendMode = readUi8($bytes, $stream); } } - $28.push($29); + characters.push($29); } while (!eob); if (!!actionOffset) { var $56 = $.buttonActions = []; - do { - var $57 = {}; - buttonCondAction($bytes, $stream, $57, swfVersion, tagCode); - $56.push($57); - } while ($stream.remaining() > 0); + while ($stream.pos < tagEnd) { + var $57 = buttonCondAction($bytes, $stream, tagEnd); + // Ignore actions that exceed the tag length. + if ($stream.pos <= tagEnd) { + $56.push($57); + $stream.pos = tagEnd; + } + } } } return $; } - function defineJPEGTables($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); - $.id = 0; - $.imgData = readBinary($bytes, $stream, 0, false); - $.mimeType = "application/octet-stream"; - return $; - } - - function setBackgroundColor($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); - $.color = rgb($bytes, $stream); - return $; - } - - function defineBinaryData($bytes, $stream, $, swfVersion, tagCode) { + function defineBinaryData($bytes, $stream, $, swfVersion, tagCode, tagEnd) { $ || ($ = {}); $.id = readUi16($bytes, $stream); - var reserved = readUi32($bytes, $stream); - $.data = readBinary($bytes, $stream, 0, false); + readUi32($bytes, $stream); // Reserved + $.data = $bytes.subarray($stream.pos, tagEnd); + $stream.pos = tagEnd; return $; } - function defineFont($bytes, $stream, $, swfVersion, tagCode) { + export function defineFont($bytes, $stream, $, swfVersion, tagCode) { $ || ($ = {}); $.id = readUi16($bytes, $stream); var firstOffset = readUi16($bytes, $stream); @@ -292,8 +289,7 @@ module Shumway.SWF.Parser { $.id = readUi16($bytes, $stream); var $0 = $.bbox = {}; bbox($bytes, $stream, $0, swfVersion, tagCode); - var $1 = $.matrix = {}; - matrix($bytes, $stream, $1, swfVersion, tagCode); + $.matrix = matrix($bytes, $stream); var glyphBits = $.glyphBits = readUi8($bytes, $stream); var advanceBits = $.advanceBits = readUi8($bytes, $stream); var $2 = $.records = []; @@ -306,16 +302,7 @@ module Shumway.SWF.Parser { return $; } - function doAction($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); - if (tagCode === 59) { - $.spriteId = readUi16($bytes, $stream); - } - $.actionsData = readBinary($bytes, $stream, 0, false); - return $; - } - - function defineSound($bytes, $stream, $, swfVersion, tagCode) { + function defineSound($bytes, $stream, $, swfVersion, tagCode, tagEnd) { $ || ($ = {}); $.id = readUi16($bytes, $stream); var soundFlags = readUi8($bytes, $stream); @@ -324,7 +311,8 @@ module Shumway.SWF.Parser { $.soundSize = soundFlags >> 1 & 1; $.soundType = soundFlags & 1; $.samplesCount = readUi32($bytes, $stream); - $.soundData = readBinary($bytes, $stream, 0, false); + $.soundData = $bytes.subarray($stream.pos, tagEnd); + $stream.pos = tagEnd; return $; } @@ -336,13 +324,12 @@ module Shumway.SWF.Parser { if (tagCode == 89) { $.soundClassName = readString($bytes, $stream, 0); } - var $0 = $.soundInfo = {}; - soundInfo($bytes, $stream, $0, swfVersion, tagCode); + $.soundInfo = soundInfo($bytes, $stream); return $; } - function soundStreamHead($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); + export function soundStreamHead($bytes, $stream) { + var $: any = {}; var playbackFlags = readUi8($bytes, $stream); $.playbackRate = playbackFlags >> 2 & 3; $.playbackSize = playbackFlags >> 1 & 1; @@ -352,31 +339,26 @@ module Shumway.SWF.Parser { $.streamRate = streamFlags >> 2 & 3; $.streamSize = streamFlags >> 1 & 1; $.streamType = streamFlags & 1; - $.samplesCount = readUi32($bytes, $stream); + $.samplesCount = readUi16($bytes, $stream); if (streamCompression == 2) { $.latencySeek = readSi16($bytes, $stream); } return $; } - function soundStreamBlock($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); - $.data = readBinary($bytes, $stream, 0, false); - return $; - } - - function defineBitmap($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); - $.id = readUi16($bytes, $stream); - var format = $.format = readUi8($bytes, $stream); - $.width = readUi16($bytes, $stream); - $.height = readUi16($bytes, $stream); - $.hasAlpha = tagCode === 36; + export function defineBitmap(bytes, stream, $, swfVersion, tagCode: number, tagEnd: number) { + var tag: any = $ || {}; + tag.id = readUi16(bytes, stream); + var format = tag.format = readUi8(bytes, stream); + tag.width = readUi16(bytes, stream); + tag.height = readUi16(bytes, stream); + tag.hasAlpha = tagCode === 36; if (format === 3) { - $.colorTableSize = readUi8($bytes, $stream); + tag.colorTableSize = readUi8(bytes, stream); } - $.bmpData = readBinary($bytes, $stream, 0, false); - return $; + tag.bmpData = bytes.subarray(stream.pos, tagEnd); + stream.pos = tagEnd; + return tag; } function defineText($bytes, $stream, $, swfVersion, tagCode) { @@ -430,13 +412,7 @@ module Shumway.SWF.Parser { return $; } - function frameLabel($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); - $.name = readString($bytes, $stream, 0); - return $; - } - - function defineFont2($bytes, $stream, $, swfVersion, tagCode) { + export function defineFont2($bytes, $stream, $, swfVersion, tagCode) { $ || ($ = {}); $.id = readUi16($bytes, $stream); var hasLayout = $.hasLayout = readUb($bytes, $stream, 1); @@ -464,6 +440,10 @@ module Shumway.SWF.Parser { $.resolution = 20; } var glyphCount = $.glyphCount = readUi16($bytes, $stream); + // The SWF format docs don't say that, but the DefineFont{2,3} tag ends here for device fonts. + if (glyphCount === 0) { + return $; + } var startpos = $stream.pos; if (wideOffset) { var $0 = $.offsets = []; @@ -528,14 +508,14 @@ module Shumway.SWF.Parser { var $57 = kerningCount; while ($57--) { var $58 = {}; - kerning($bytes, $stream, $58, swfVersion, tagCode, wide); + kerning($bytes, $stream, $58, wide); $56.push($58); } } return $; } - function defineFont4($bytes, $stream, $, swfVersion, tagCode) { + export function defineFont4($bytes, $stream, $, swfVersion, tagCode, tagEnd) { $ || ($ = {}); $.id = readUi16($bytes, $stream); var reserved = readUb($bytes, $stream, 5); @@ -544,67 +524,8 @@ module Shumway.SWF.Parser { $.bold = readUb($bytes, $stream, 1); $.name = readString($bytes, $stream, 0); if (hasFontData) { - $.data = readBinary($bytes, $stream, 0, false); - } - return $; - } - - function fileAttributes($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); - var reserved = readUb($bytes, $stream, 1); - $.useDirectBlit = readUb($bytes, $stream, 1); - $.useGpu = readUb($bytes, $stream, 1); - $.hasMetadata = readUb($bytes, $stream, 1); - $.doAbc = readUb($bytes, $stream, 1); - $.noCrossDomainCaching = readUb($bytes, $stream, 1); - $.relativeUrls = readUb($bytes, $stream, 1); - $.network = readUb($bytes, $stream, 1); - var pad = readUb($bytes, $stream, 24); - return $; - } - - function doABC($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); - if (tagCode === 82) { - $.flags = readUi32($bytes, $stream); - } - else { - $.flags = 0; - } - if (tagCode === 82) { - $.name = readString($bytes, $stream, 0); - } - else { - $.name = ""; - } - $.data = readBinary($bytes, $stream, 0, false); - return $; - } - - function exportAssets($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); - var exportsCount = readUi16($bytes, $stream); - var $0 = $.exports = []; - var $1 = exportsCount; - while ($1--) { - var $2: any = {}; - $2.symbolId = readUi16($bytes, $stream); - $2.className = readString($bytes, $stream, 0); - $0.push($2); - } - return $; - } - - function symbolClass($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); - var symbolCount = readUi16($bytes, $stream); - var $0 = $.exports = []; - var $1 = symbolCount; - while ($1--) { - var $2: any = {}; - $2.symbolId = readUi16($bytes, $stream); - $2.className = readString($bytes, $stream, 0); - $0.push($2); + $.data = $bytes.subarray($stream.pos, tagEnd); + $stream.pos = tagEnd; } return $; } @@ -617,7 +538,7 @@ module Shumway.SWF.Parser { return $; } - function defineScene($bytes, $stream, $, swfVersion, tagCode) { + export function defineScene($bytes, $stream, $) { $ || ($ = {}); var sceneCount = readEncodedU32($bytes, $stream); var $0 = $.scenes = []; @@ -654,7 +575,7 @@ module Shumway.SWF.Parser { align($bytes, $stream); } - function rgb($bytes, $stream): number { + export function rgb($bytes, $stream): number { return ((readUi8($bytes, $stream) << 24) | (readUi8($bytes, $stream) <<16) | (readUi8($bytes, $stream) << 8) | 0xff) >>> 0; } @@ -669,19 +590,8 @@ module Shumway.SWF.Parser { (readUi8($bytes, $stream) << 16) | (readUi8($bytes, $stream) << 8); } - function fillSolid($bytes, $stream, $, swfVersion, tagCode, isMorph) { - if (tagCode > 22 || isMorph) { - $.color = rgba($bytes, $stream); - } - else { - $.color = rgb($bytes, $stream); - } - if (isMorph) { - $.colorMorph = rgba($bytes, $stream); - } - } - - function matrix($bytes, $stream, $, swfVersion, tagCode) { + function matrix($bytes, $stream) { + var $: any = {}; align($bytes, $stream); var hasScale = readUb($bytes, $stream, 1); if (hasScale) { @@ -709,9 +619,10 @@ module Shumway.SWF.Parser { $.tx = e; $.ty = f; align($bytes, $stream); + return $; } - function cxform($bytes, $stream, $, swfVersion, tagCode) { + function cxform($bytes, $stream, $, tagCode) { align($bytes, $stream); var hasOffsets = readUb($bytes, $stream, 1); var hasMultipliers = readUb($bytes, $stream, 1); @@ -751,16 +662,6 @@ module Shumway.SWF.Parser { align($bytes, $stream); } - function fillGradient($bytes, $stream, $, swfVersion, tagCode, isMorph, type) { - var $128 = $.matrix = {}; - matrix($bytes, $stream, $128, swfVersion, tagCode); - if (isMorph) { - var $129 = $.matrixMorph = {}; - matrix($bytes, $stream, $129, swfVersion, tagCode); - } - gradient($bytes, $stream, $, swfVersion, tagCode, isMorph, type); - } - function gradient($bytes, $stream, $, swfVersion, tagCode, isMorph, type) { if (tagCode === 83) { $.spreadMode = readUb($bytes, $stream, 2); @@ -774,7 +675,7 @@ module Shumway.SWF.Parser { var $131 = count; while ($131--) { var $132 = {}; - gradientRecord($bytes, $stream, $132, swfVersion, tagCode, isMorph); + gradientRecord($bytes, $stream, $132, tagCode, isMorph); $130.push($132); } if (type === 19) { @@ -785,7 +686,7 @@ module Shumway.SWF.Parser { } } - function gradientRecord($bytes, $stream, $, swfVersion, tagCode, isMorph) { + function gradientRecord($bytes, $stream, $, tagCode, isMorph) { $.ratio = readUi8($bytes, $stream); if (tagCode > 22) { $.color = rgba($bytes, $stream); @@ -804,34 +705,30 @@ module Shumway.SWF.Parser { temp = styles($bytes, $stream, $, swfVersion, tagCode, isMorph, hasStrokes); var lineBits = temp.lineBits; var fillBits = temp.fillBits; - var $160 = $.records = []; + var records = $.records = []; do { - var $161 = {}; - temp = shapeRecord($bytes, $stream, $161, swfVersion, tagCode, isMorph, - fillBits, lineBits, hasStrokes, bits); + var record = {}; + temp = shapeRecord($bytes, $stream, record, swfVersion, tagCode, isMorph, + fillBits, lineBits, hasStrokes, bits); eos = temp.eos; - var flags = temp.flags; - var type = temp.type; - var fillBits = temp.fillBits; - var lineBits = temp.lineBits; + fillBits = temp.fillBits; + lineBits = temp.lineBits; bits = temp.bits; - $160.push($161); + records.push(record); } while (!eos); - temp = styleBits($bytes, $stream, $, swfVersion, tagCode); + temp = styleBits($bytes, $stream); var fillBits = temp.fillBits; var lineBits = temp.lineBits; - var $162 = $.recordsMorph = []; + var recordsMorph = $.recordsMorph = []; do { - var $163 = {}; - temp = shapeRecord($bytes, $stream, $163, swfVersion, tagCode, isMorph, - fillBits, lineBits, hasStrokes, bits); + var morphRecord = {}; + temp = shapeRecord($bytes, $stream, morphRecord, swfVersion, tagCode, isMorph, + fillBits, lineBits, hasStrokes, bits); eos = temp.eos; - var flags = temp.flags; - var type = temp.type; - var fillBits = temp.fillBits; - var lineBits = temp.lineBits; + fillBits = temp.fillBits; + lineBits = temp.lineBits; bits = temp.bits; - $162.push($163); + recordsMorph.push(morphRecord); } while (!eos); } @@ -844,30 +741,29 @@ module Shumway.SWF.Parser { do { var $161 = {}; temp = shapeRecord($bytes, $stream, $161, swfVersion, tagCode, isMorph, - fillBits, lineBits, hasStrokes, bits); + fillBits, lineBits, hasStrokes, bits); eos = temp.eos; - var flags = temp.flags; - var type = temp.type; - var fillBits = temp.fillBits; - var lineBits = temp.lineBits; + fillBits = temp.fillBits; + lineBits = temp.lineBits; bits = temp.bits; $160.push($161); } while (!eos); } - function shapeRecord($bytes, $stream, $, swfVersion, tagCode, isMorph, fillBits, lineBits, hasStrokes, bits: number) { + function shapeRecord($bytes, $stream, $, swfVersion, tagCode, isMorph, fillBits, lineBits, + hasStrokes, bits: number) { var eos: boolean, temp: any; var type = $.type = readUb($bytes, $stream, 1); var flags = readUb($bytes, $stream, 5); eos = $.eos = !(type || flags); if (type) { - temp = shapeRecordEdge($bytes, $stream, $, swfVersion, tagCode, flags, bits); + temp = shapeRecordEdge($bytes, $stream, $, flags); bits = temp.bits; } else { temp = shapeRecordSetup($bytes, $stream, $, swfVersion, tagCode, flags, isMorph, fillBits, lineBits, hasStrokes, bits); - var fillBits = temp.fillBits; - var lineBits = temp.lineBits; + fillBits = temp.fillBits; + lineBits = temp.lineBits; bits = temp.bits; } return { @@ -880,18 +776,16 @@ module Shumway.SWF.Parser { }; } - function shapeRecordEdge($bytes, $stream, $, swfVersion, tagCode, flags, bits: number) { - var isStraight = 0, tmp = 0, bits = 0, isGeneral = 0, isVertical = 0; - isStraight = $.isStraight = flags >> 4; - tmp = flags & 0x0f; - bits = tmp + 2; + function shapeRecordEdge($bytes, $stream, $, flags) { + var bits = (flags & 0x0f) + 2; + var isStraight = $.isStraight = flags >> 4; if (isStraight) { - isGeneral = $.isGeneral = readUb($bytes, $stream, 1); + var isGeneral = $.isGeneral = readUb($bytes, $stream, 1); if (isGeneral) { $.deltaX = readSb($bytes, $stream, bits); $.deltaY = readSb($bytes, $stream, bits); } else { - isVertical = $.isVertical = readUb($bytes, $stream, 1); + var isVertical = $.isVertical = readUb($bytes, $stream, 1); if (isVertical) { $.deltaY = readSb($bytes, $stream, bits); } else { @@ -908,17 +802,11 @@ module Shumway.SWF.Parser { } function shapeRecordSetup($bytes, $stream, $, swfVersion, tagCode, flags, isMorph, fillBits: number, lineBits: number, hasStrokes, bits: number) { - var hasNewStyles = 0, hasLineStyle = 0, hasFillStyle1 = 0; - var hasFillStyle0 = 0, move = 0; - if (tagCode > 2) { - hasNewStyles = $.hasNewStyles = flags >> 4; - } else { - hasNewStyles = $.hasNewStyles = 0; - } - hasLineStyle = $.hasLineStyle = flags >> 3 & 1; - hasFillStyle1 = $.hasFillStyle1 = flags >> 2 & 1; - hasFillStyle0 = $.hasFillStyle0 = flags >> 1 & 1; - move = $.move = flags & 1; + var hasNewStyles = $.hasNewStyles = tagCode > 2 ? flags >> 4 : 0; + var hasLineStyle = $.hasLineStyle = flags >> 3 & 1; + var hasFillStyle1 = $.hasFillStyle1 = flags >> 2 & 1; + var hasFillStyle0 = $.hasFillStyle0 = flags >> 1 & 1; + var move = $.move = flags & 1; if (move) { bits = readUb($bytes, $stream, 5); $.moveX = readSb($bytes, $stream, bits); @@ -948,7 +836,7 @@ module Shumway.SWF.Parser { function styles($bytes, $stream, $, swfVersion, tagCode, isMorph, hasStrokes) { fillStyleArray($bytes, $stream, $, swfVersion, tagCode, isMorph); lineStyleArray($bytes, $stream, $, swfVersion, tagCode, isMorph, hasStrokes); - var temp = styleBits($bytes, $stream, $, swfVersion, tagCode); + var temp = styleBits($bytes, $stream); var fillBits = temp.fillBits; var lineBits = temp.lineBits; return {fillBits: fillBits, lineBits: lineBits}; @@ -989,7 +877,7 @@ module Shumway.SWF.Parser { } } - function styleBits($bytes, $stream, $, swfVersion, tagCode) { + function styleBits($bytes, $stream) { align($bytes, $stream); var fillBits = readUb($bytes, $stream, 4); var lineBits = readUb($bytes, $stream, 4); @@ -1003,18 +891,30 @@ module Shumway.SWF.Parser { var type = $.type = readUi8($bytes, $stream); switch (type) { case 0: - fillSolid($bytes, $stream, $, swfVersion, tagCode, isMorph); + $.color = tagCode > 22 || isMorph ? rgba($bytes, $stream) : rgb($bytes, $stream); + if (isMorph) { + $.colorMorph = rgba($bytes, $stream); + } break; case 16: case 18: case 19: - fillGradient($bytes, $stream, $, swfVersion, tagCode, isMorph, type); + $.matrix = matrix($bytes, $stream); + if (isMorph) { + $.matrixMorph = matrix($bytes, $stream); + } + gradient($bytes, $stream, $, swfVersion, tagCode, isMorph, type); break; case 64: case 65: case 66: case 67: - fillBitmap($bytes, $stream, $, swfVersion, tagCode, isMorph, type); + $.bitmapId = readUi16($bytes, $stream); + $.condition = type === 64 || type === 67; + $.matrix = matrix($bytes, $stream); + if (isMorph) { + $.matrixMorph = matrix($bytes, $stream); + } break; default: } @@ -1061,18 +961,7 @@ module Shumway.SWF.Parser { } } - function fillBitmap($bytes, $stream, $, swfVersion, tagCode, isMorph, type) { - $.bitmapId = readUi16($bytes, $stream); - var $18 = $.matrix = {}; - matrix($bytes, $stream, $18, swfVersion, tagCode); - if (isMorph) { - var $19 = $.matrixMorph = {}; - matrix($bytes, $stream, $19, swfVersion, tagCode); - } - $.condition = type === 64 || type === 67; - } - - function filterGlow($bytes, $stream, $, swfVersion, tagCode, type) { + function filterGlow($bytes, $stream, $, type) { var count; if (type === 4 || type === 7) { count = readUi8($bytes, $stream); @@ -1113,14 +1002,14 @@ module Shumway.SWF.Parser { } } - function filterBlur($bytes, $stream, $, swfVersion, tagCode) { + function filterBlur($bytes, $stream, $) { $.blurX = readFixed($bytes, $stream); $.blurY = readFixed($bytes, $stream); $.quality = readUb($bytes, $stream, 5); var reserved = readUb($bytes, $stream, 3); } - function filterConvolution($bytes, $stream, $, swfVersion, tagCode) { + function filterConvolution($bytes, $stream, $) { var matrixX = $.matrixX = readUi8($bytes, $stream); var matrixY = $.matrixY = readUi8($bytes, $stream); $.divisor = readFloat($bytes, $stream); @@ -1136,7 +1025,7 @@ module Shumway.SWF.Parser { $.preserveAlpha = readUb($bytes, $stream, 1); } - function filterColorMatrix($bytes, $stream, $, swfVersion, tagCode) { + function filterColorMatrix($bytes, $stream, $) { var $20 = $.matrix = []; var $21 = 20; while ($21--) { @@ -1144,7 +1033,7 @@ module Shumway.SWF.Parser { } } - function anyFilter($bytes, $stream, $, swfVersion, tagCode) { + function anyFilter($bytes, $stream, $) { var type = $.type = readUi8($bytes, $stream); switch (type) { case 0: @@ -1152,61 +1041,43 @@ module Shumway.SWF.Parser { case 3: case 4: case 7: - filterGlow($bytes, $stream, $, swfVersion, tagCode, type); + filterGlow($bytes, $stream, $, type); break; case 1: - filterBlur($bytes, $stream, $, swfVersion, tagCode); + filterBlur($bytes, $stream, $); break; case 5: - filterConvolution($bytes, $stream, $, swfVersion, tagCode); + filterConvolution($bytes, $stream, $); break; case 6: - filterColorMatrix($bytes, $stream, $, swfVersion, tagCode); + filterColorMatrix($bytes, $stream, $); break; default: } } - function events($bytes, $stream, $, swfVersion, tagCode) { - var flags = swfVersion >= 6 ? readUi32($bytes, $stream) : readUi16($bytes, $stream); - var eoe = $.eoe = !flags; - var keyPress = 0; - $.onKeyUp = flags >> 7 & 1; - $.onKeyDown = flags >> 6 & 1; - $.onMouseUp = flags >> 5 & 1; - $.onMouseDown = flags >> 4 & 1; - $.onMouseMove = flags >> 3 & 1; - $.onUnload = flags >> 2 & 1; - $.onEnterFrame = flags >> 1 & 1; - $.onLoad = flags & 1; - if (swfVersion >= 6) { - $.onDragOver = flags >> 15 & 1; - $.onRollOut = flags >> 14 & 1; - $.onRollOver = flags >> 13 & 1; - $.onReleaseOutside = flags >> 12 & 1; - $.onRelease = flags >> 11 & 1; - $.onPress = flags >> 10 & 1; - $.onInitialize = flags >> 9 & 1; - $.onData = flags >> 8 & 1; - if (swfVersion >= 7) { - $.onConstruct = flags >> 18 & 1; - } else { - $.onConstruct = 0; - } - keyPress = $.keyPress = flags >> 17 & 1; - $.onDragOut = flags >> 16 & 1; + function events($bytes, $stream, $, swfVersion) { + var flags = $.flags = swfVersion >= 6 ? readUi32($bytes, $stream) : readUi16($bytes, $stream); + if (!flags) { + // `true` means this is the EndOfEvents marker. + return true; } - if (!eoe) { - var length = $.length = readUi32($bytes, $stream); - if (keyPress) { - $.keyCode = readUi8($bytes, $stream); - } - $.actionsData = readBinary($bytes, $stream, length - +keyPress, false); + // The Construct event is only allowed in 7+. It can't be set in < 6, so mask it out for 6. + if (swfVersion === 6) { + flags = flags & ~AVM1ClipEvents.Construct; } - return eoe; + var length = $.length = readUi32($bytes, $stream); + if (flags & AVM1ClipEvents.KeyPress) { + $.keyCode = readUi8($bytes, $stream); + length--; + } + var end = $stream.pos + length; + $.actionsData = $bytes.subarray($stream.pos, end); + $stream.pos = end; + return false; } - function kerning($bytes, $stream, $, swfVersion, tagCode, wide) { + function kerning($bytes, $stream, $, wide) { if (wide) { $.code1 = readUi16($bytes, $stream); $.code2 = readUi16($bytes, $stream); @@ -1218,7 +1089,7 @@ module Shumway.SWF.Parser { $.adjustment = readUi16($bytes, $stream); } - function textEntry($bytes, $stream, $, swfVersion, tagCode, glyphBits, advanceBits) { + function textEntry($bytes, $stream, $, glyphBits, advanceBits) { $.glyphIndex = readUb($bytes, $stream, glyphBits); $.advance = readSb($bytes, $stream, advanceBits); } @@ -1266,20 +1137,21 @@ module Shumway.SWF.Parser { var $7 = glyphCount; while ($7--) { var $8 = {}; - textEntry($bytes, $stream, $8, swfVersion, tagCode, glyphBits, advanceBits); + textEntry($bytes, $stream, $8, glyphBits, advanceBits); $6.push($8); } } return {eot: eot}; } - function soundEnvelope($bytes, $stream, $, swfVersion, tagCode) { + function soundEnvelope($bytes, $stream, $) { $.pos44 = readUi32($bytes, $stream); $.volumeLeft = readUi16($bytes, $stream); $.volumeRight = readUi16($bytes, $stream); } - function soundInfo($bytes, $stream, $, swfVersion, tagCode) { + function soundInfo($bytes, $stream) { + var $: any = {}; var reserved = readUb($bytes, $stream, 2); $.stop = readUb($bytes, $stream, 1); $.noMultiple = readUb($bytes, $stream, 1); @@ -1302,10 +1174,11 @@ module Shumway.SWF.Parser { var $2 = envelopeCount; while ($2--) { var $3 = {}; - soundEnvelope($bytes, $stream, $3, swfVersion, tagCode); + soundEnvelope($bytes, $stream, $3); $1.push($3); } } + return $; } function button($bytes, $stream, $, swfVersion, tagCode) { @@ -1325,16 +1198,15 @@ module Shumway.SWF.Parser { if (!eob) { $.symbolId = readUi16($bytes, $stream); $.depth = readUi16($bytes, $stream); - var $2 = $.matrix = {}; - matrix($bytes, $stream, $2, swfVersion, tagCode); + $.matrix = matrix($bytes, $stream); if (tagCode === SwfTag.CODE_DEFINE_BUTTON2) { var $3 = $.cxform = {}; - cxform($bytes, $stream, $3, swfVersion, tagCode); + cxform($bytes, $stream, $3, tagCode); } if ($.flags & PlaceObjectFlags.HasFilterList) { $.filterCount = readUi8($bytes, $stream); var $4 = $.filters = {}; - anyFilter($bytes, $stream, $4, swfVersion, tagCode); + anyFilter($bytes, $stream, $4); } if ($.flags & PlaceObjectFlags.HasBlendMode) { $.blendMode = readUi8($bytes, $stream); @@ -1343,20 +1215,26 @@ module Shumway.SWF.Parser { return {eob: eob}; } - function buttonCondAction($bytes, $stream, $, swfVersion, tagCode) { + function buttonCondAction($bytes, $stream, tagEnd) { var tagSize = readUi16($bytes, $stream); + // If no tagSize is given, read to the tag's end. + var start = $stream.pos; + var end = tagSize ? start + tagSize : tagEnd; var conditions = readUi16($bytes, $stream); - // The 7 upper bits hold a key code the button should respond to. - $.keyCode = (conditions & 0xfe00) >> 9; - // The lower 9 bits hold state transition flags. See the enum in AVM1Button for details. - $.stateTransitionFlags = conditions & 0x1ff; - // If no tagSize is given, pass `0` to readBinary. - $.actionsData = readBinary($bytes, $stream, (tagSize || 4) - 4, false); + $stream.pos = end; + return { + // The 7 upper bits hold a key code the button should respond to. + keyCode: (conditions & 0xfe00) >> 9, + // The lower 9 bits hold state transition flags. See the enum in AVM1Button for details. + stateTransitionFlags: conditions & 0x1ff, + // If no tagSize is given, pass `0` to readBinary. + actionsData: $bytes.subarray(start, end) + }; } function shape($bytes, $stream, $, swfVersion, tagCode) { var eos: boolean, bits: number, temp: any; - temp = styleBits($bytes, $stream, $, swfVersion, tagCode); + temp = styleBits($bytes, $stream); var fillBits = temp.fillBits; var lineBits = temp.lineBits; var $4 = $.records = []; @@ -1367,14 +1245,14 @@ module Shumway.SWF.Parser { temp = shapeRecord($bytes, $stream, $5, swfVersion, tagCode, isMorph, fillBits, lineBits, hasStrokes, bits); eos = temp.eos; - var fillBits = temp.fillBits; - var lineBits = temp.lineBits; - bits = bits; + fillBits = temp.fillBits; + lineBits = temp.lineBits; + bits = temp.bits; $4.push($5); } while (!eos); } - export var tagHandler:any = { + export var tagHandlers: any = { /* End */ 0: undefined, /* ShowFrame */ 1: undefined, /* DefineShape */ 2: defineShape, @@ -1382,17 +1260,17 @@ module Shumway.SWF.Parser { /* RemoveObject */ 5: removeObject, /* DefineBits */ 6: defineImage, /* DefineButton */ 7: defineButton, - /* JPEGTables */ 8: defineJPEGTables, - /* SetBackgroundColor */ 9: setBackgroundColor, + /* JPEGTables */ 8: undefined, + /* SetBackgroundColor */ 9: undefined, /* DefineFont */ 10: defineFont, /* DefineText */ 11: defineLabel, - /* DoAction */ 12: doAction, + /* DoAction */ 12: undefined, /* DefineFontInfo */ 13: undefined, /* DefineSound */ 14: defineSound, /* StartSound */ 15: startSound, /* DefineButtonSound */ 17: undefined, - /* SoundStreamHead */ 18: soundStreamHead, - /* SoundStreamBlock */ 19: soundStreamBlock, + /* SoundStreamHead */ 18: undefined, + /* SoundStreamBlock */ 19: undefined, /* DefineBitsLossless */ 20: defineBitmap, /* DefineBitsJPEG2 */ 21: defineImage, /* DefineShape2 */ 22: defineShape, @@ -1407,31 +1285,31 @@ module Shumway.SWF.Parser { /* DefineBitsLossless2 */ 36: defineBitmap, /* DefineEditText */ 37: defineText, /* DefineSprite */ 39: undefined, - /* FrameLabel */ 43: frameLabel, - /* SoundStreamHead2 */ 45: soundStreamHead, + /* FrameLabel */ 43: undefined, + /* SoundStreamHead2 */ 45: undefined, /* DefineMorphShape */ 46: defineShape, /* DefineFont2 */ 48: defineFont2, - /* ExportAssets */ 56: exportAssets, + /* ExportAssets */ 56: undefined, /* ImportAssets */ 57: undefined, /* EnableDebugger */ 58: undefined, - /* DoInitAction */ 59: doAction, + /* DoInitAction */ 59: undefined, /* DefineVideoStream */ 60: undefined, /* VideoFrame */ 61: undefined, /* DefineFontInfo2 */ 62: undefined, /* EnableDebugger2 */ 64: undefined, /* ScriptLimits */ 65: undefined, /* SetTabIndex */ 66: undefined, - /* FileAttributes */ 69: fileAttributes, + /* FileAttributes */ 69: undefined, /* PlaceObject3 */ 70: placeObject, /* ImportAssets2 */ 71: undefined, - /* DoABC (undoc) */ 72: doABC, + /* DoABC (undoc) */ 72: undefined, /* DefineFontAlignZones */ 73: undefined, /* CSMTextSettings */ 74: undefined, /* DefineFont3 */ 75: defineFont2, - /* SymbolClass */ 76: symbolClass, + /* SymbolClass */ 76: undefined, /* Metadata */ 77: undefined, /* DefineScalingGrid */ 78: defineScalingGrid, - /* DoABC */ 82: doABC, + /* DoABC */ 82: undefined, /* DefineShape4 */ 83: defineShape, /* DefineMorphShape2 */ 84: defineShape, /* DefineSceneAndFrameLabelData */ 86: defineScene, @@ -1443,23 +1321,20 @@ module Shumway.SWF.Parser { }; - export function readHeader($bytes, $stream, $, swfVersion, tagCode) { - $ || ($ = {}); - var $0: any = $.bbox = {}; - align($bytes, $stream); + export function readHeader($bytes, $stream) { var bits = readUb($bytes, $stream, 5); var xMin = readSb($bytes, $stream, bits); var xMax = readSb($bytes, $stream, bits); var yMin = readSb($bytes, $stream, bits); var yMax = readSb($bytes, $stream, bits); - $0.xMin = xMin; - $0.xMax = xMax; - $0.yMin = yMin; - $0.yMax = yMax; align($bytes, $stream); var frameRateFraction = readUi8($bytes, $stream); - $.frameRate = readUi8($bytes, $stream) + frameRateFraction / 256; - $.frameCount = readUi16($bytes, $stream); - return $; + var frameRate = readUi8($bytes, $stream) + frameRateFraction / 256; + var frameCount = readUi16($bytes, $stream); + return { + frameRate: frameRate, + frameCount: frameCount, + bounds: new Shumway.Bounds(xMin, yMin, xMax, yMax) + }; } } diff --git a/src/swf/parser/image.ts b/src/swf/parser/image.ts index 2842cf5207..5c13a0f81e 100644 --- a/src/swf/parser/image.ts +++ b/src/swf/parser/image.ts @@ -37,13 +37,12 @@ module Shumway.SWF.Parser { /** * Parses JPEG chunks and reads image width and height information. JPEG data - * is SWFs is encoded in chunks and is not directly decodable by the JPEG - * parser. + * in SWFs is encoded in chunks and not directly decodable by the JPEG parser. */ - export function parseJpegChunks(image: any, bytes:Uint8Array): Uint8Array [] { + export function parseJpegChunks(image: any, bytes:Uint8Array, + chunks: Uint8Array[]): Uint8Array [] { var i = 0; var n = bytes.length; - var chunks = []; var code; do { var begin = i; @@ -69,7 +68,9 @@ module Shumway.SWF.Parser { } chunks.push(bytes.subarray(begin, i)); } while (i < n); - release || assert(image.width && image.height, 'bad jpeg image'); + if (!release && !(image.width && image.height)) { + Debug.warning('bad jpeg image'); + } return chunks; } @@ -114,6 +115,7 @@ module Shumway.SWF.Parser { mimeType: string; data: Uint8Array; dataType?: ImageType; + image: any; // For some reason, tsc doesn't like us using the DOM Image definition here. } export interface DefineImageTag { @@ -121,10 +123,10 @@ module Shumway.SWF.Parser { imgData: Uint8Array; mimeType: string; alphaData: boolean; - incomplete: boolean; + jpegTables: Uint8Array; } - export function defineImage(tag: DefineImageTag, dictionary: any): ImageDefinition { + export function defineImage(tag: DefineImageTag): ImageDefinition { enterTimeline("defineImage"); var image: any = { type: 'image', @@ -132,13 +134,12 @@ module Shumway.SWF.Parser { mimeType: tag.mimeType }; var imgData = tag.imgData; - var chunks; if (tag.mimeType === 'image/jpeg') { var alphaData: Uint8Array = tag.alphaData; if (alphaData) { var jpegImage = new Shumway.JPEG.JpegImage(); - jpegImage.parse(joinChunks(parseJpegChunks(image, imgData))); + jpegImage.parse(joinChunks(parseJpegChunks(image, imgData, []))); release || assert(image.width === jpegImage.width); release || assert(image.height === jpegImage.height); var width = image.width; @@ -155,17 +156,16 @@ module Shumway.SWF.Parser { image.mimeType = 'application/octet-stream'; image.dataType = ImageType.StraightAlphaRGBA; } else { - chunks = parseJpegChunks(image, imgData); + var chunks = []; + if (tag.jpegTables) { + chunks.push(tag.jpegTables); + } + parseJpegChunks(image, imgData, chunks); - if (tag.incomplete) { - var tables = dictionary[0]; - release || assert(tables, 'missing jpeg tables'); - var header = tables.data; - if (header && header.size) { - chunks[0] = chunks[0].subarray(2); - chunks.unshift(header.slice(0, header.size - 2)); - } + if (tag.jpegTables && tag.jpegTables.byteLength > 0) { + chunks[1] = chunks[1].subarray(2); } + image.data = joinChunks(chunks); image.dataType = ImageType.JPEG; } diff --git a/src/swf/parser/label.ts b/src/swf/parser/label.ts index 59fbcb4a40..c0c1f48687 100644 --- a/src/swf/parser/label.ts +++ b/src/swf/parser/label.ts @@ -16,91 +16,23 @@ /// module Shumway.SWF.Parser { - import assert = Shumway.Debug.assert; - import ColorUtilities = Shumway.ColorUtilities; - - export function defineLabel(tag: any, dictionary: any) { - var records = tag.records; - var bbox = tag.bbox; - var htmlText = ''; - var coords = []; - var dependencies = []; - var size = 12; - var face = 'Times Roman'; - var color = 0; - var x = 0; - var y = 0; - var i = 0; - var record; - var codes; - var font; - var fontAttributes; - while ((record = records[i++])) { - if (record.eot) { - break; - } - if (record.hasFont) { - font = dictionary[record.fontId]; - release || assert(font, 'undefined label font'); - codes = font.codes; - dependencies.push(font.id); - size = record.fontHeight; - if (!font.originalSize) { - size = size / 20; - } - face = 'swffont' + font.id; - } - if (record.hasColor) { - color = record.color >>> 8; - } - if (record.hasMoveX) { - x = record.moveX; - if (x < bbox.xMin) { - bbox.xMin = x; - } - } - if (record.hasMoveY) { - y = record.moveY; - if (y < bbox.yMin) { - bbox.yMin = y; - } - } - var text = ''; - var entries = record.entries; - var j = 0; - var entry; - while ((entry = entries[j++])) { - var code = codes[entry.glyphIndex]; - release || assert(code, 'undefined label glyph'); - text += String.fromCharCode(code); - coords.push(x, y); - x += entry.advance; - } - htmlText += '' + - text.replace(/[<>&]/g, function(s: string) { - return s === '<' ? '<' : (s === '>' ? '>' : '&'); - }) + - ''; - } + export function defineLabel(tag: any) { var label = { - type: 'text', + type: 'label', id: tag.id, - fillBounds: bbox, + fillBounds: tag.bbox, matrix: tag.matrix, tag: { hasText: true, - initialText: htmlText, + initialText: '', html: true, readonly: true }, - coords: coords, + records: tag.records, + coords: null, static: true, require: null }; - if (dependencies.length) { - label.require = dependencies; - } return label; } } diff --git a/src/swf/parser/parser.ts b/src/swf/parser/parser.ts deleted file mode 100644 index e3a399b400..0000000000 --- a/src/swf/parser/parser.ts +++ /dev/null @@ -1,457 +0,0 @@ -/* - * Copyright 2013 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/// -module Shumway.SWF.Parser { - import Inflate = Shumway.ArrayUtilities.Inflate; - - function readTags(context, stream, swfVersion, final, onprogress, onexception): boolean { - function rollback(): boolean { - // recovering the stream state - stream.pos = lastSuccessfulPosition; - context._readTag = tag; - return false; - } - - var tags = context.tags; - var bytes = stream.bytes; - var lastSuccessfulPosition; - - var tag: ISwfTagData = null; - if (context._readTag) { - tag = context._readTag; - context._readTag = null; - } - - try { - while (stream.pos < stream.end) { - // this loop can be interrupted at any moment by `return rollback();` - // saving last data/position for the rollback method. - lastSuccessfulPosition = stream.pos; - - if (stream.pos + 2 > stream.end) { - return rollback(); - } - - var tagCodeAndLength = readUi16(bytes, stream); - if (!tagCodeAndLength) { - // end of tags - final = true; - break; - } - - var tagCode = tagCodeAndLength >> 6; - var length = tagCodeAndLength & 0x3f; - if (length === 0x3f) { - if (stream.pos + 4 > stream.end) { - return rollback(); - } - - length = readUi32(bytes, stream); - } - - if (tag) { - if (tagCode === 1 && tag.code === 1) { - // counting ShowFrame - tag.repeat++; - stream.pos += length; - continue; - } - tags.push(tag); - if (onprogress && tag.id !== undefined) { - context.bytesLoaded = (context.bytesTotal * stream.pos / stream.end) | 0; - onprogress(context); - } - tag = null; - } - - if (stream.pos + length > stream.end) { - return rollback(); - } - - var substream = stream.substream(stream.pos, stream.pos += length); - var subbytes = substream.bytes; - var nextTag: ISwfTagData = { code: tagCode }; - - if (tagCode === SwfTag.CODE_DEFINE_SPRITE) { - nextTag.type = 'sprite'; - nextTag.id = readUi16(subbytes, substream); - nextTag.frameCount = readUi16(subbytes, substream); - nextTag.tags = []; - var isEnoughData = readTags(nextTag, substream, swfVersion, true, null, null); - if (!isEnoughData) { - Debug.error('Invalid SWF tag structure'); - } - } else if (tagCode === 1) { - nextTag.repeat = 1; - } else { - var handler = tagHandler[tagCode]; - if (handler) { - handler(subbytes, substream, nextTag, swfVersion, tagCode); - } - } - - tag = nextTag; - } - if ((tag && final) || (stream.pos >= stream.end)) { - if (tag) { - tag.finalTag = true; // note: 'eot' is reserved by handlers - tags.push(tag); - } - if (onprogress) { - context.bytesLoaded = context.bytesTotal; - onprogress(context); - } - } else { - context._readTag = tag; - } - } catch (e) { - onexception && onexception(e); - throw e; - } - return true; - } - - class HeadTailBuffer { - private _bufferSize: number; - private _buffer: Uint8Array; - private _pos: number; - - constructor(defaultSize:number = 16) { - this._bufferSize = defaultSize; - this._buffer = new Uint8Array(this._bufferSize); - this._pos = 0; - } - - push(data: Uint8Array, need?: number) { - var bufferLengthNeed = this._pos + data.length; - if (this._bufferSize < bufferLengthNeed) { - var newBufferSize = this._bufferSize; - while (newBufferSize < bufferLengthNeed) { - newBufferSize <<= 1; - } - var newBuffer = new Uint8Array(newBufferSize); - if (this._bufferSize > 0) { - newBuffer.set(this._buffer); - } - this._buffer = newBuffer; - this._bufferSize = newBufferSize; - } - this._buffer.set(data, this._pos); - this._pos += data.length; - if (need) { - return this._pos >= need; - } - } - - getHead(size: number) { - return this._buffer.subarray(0, size); - } - - getTail(offset: number) { - return this._buffer.subarray(offset, this._pos); - } - - removeHead(size: number) { - var tail = this.getTail(size); - this._buffer = new Uint8Array(this._bufferSize); - this._buffer.set(tail); - this._pos = tail.length; - } - - get arrayBuffer() { - return this._buffer.buffer; - } - - get length() { - return this._pos; - } - - getBytes(): Uint8Array { - return this._buffer.subarray(0, this._pos); - } - - createStream() { - return new Stream(this.arrayBuffer, 0, this.length); - } - } - - export interface ProgressInfo { - bytesLoaded: number; - bytesTotal: number; - } - - export interface IPipe { - push(data: Uint8Array, progressInfo: ProgressInfo); - close(); - } - - class CompressedPipe implements IPipe { - private _inflate: Inflate; - private _progressInfo: ProgressInfo; - - constructor (target) { - this._inflate = new Inflate(true); - this._inflate.onData = function (data: Uint8Array) { - target.push(data, this._progressInfo); - }.bind(this); - } - - push(data: Uint8Array, progressInfo: ProgressInfo) { - this._progressInfo = progressInfo; - this._inflate.push(data); - } - - close() { - this._inflate = null; - } - } - - interface SwfInfo { - swfVersion: number; - parseTime: number; - bytesLoaded: number; - bytesTotal: number; - fileAttributes: any; - tags: any[] - } - - class BodyParser implements IPipe { - swf: SwfInfo; - - _buffer: HeadTailBuffer; - _initialize: boolean; - _totalRead: number; - _length: number; - _options: any; - - constructor(swfVersion: number, length: number, options: any) { - this.swf = { - swfVersion: swfVersion, - parseTime: 0, - bytesLoaded: undefined, - bytesTotal: undefined, - fileAttributes: undefined, - tags: undefined - }; - this._buffer = new HeadTailBuffer(32768); - this._initialize = true; - this._totalRead = 0; - this._length = length; - this._options = options; - } - - push(data: Uint8Array, progressInfo: ProgressInfo) { - if (data.length === 0) { - return; - } - - var swf = this.swf; - var swfVersion = swf.swfVersion; - var buffer = this._buffer; - var options = this._options; - var stream; - - var finalBlock = false; - if (progressInfo) { - swf.bytesLoaded = progressInfo.bytesLoaded; - swf.bytesTotal = progressInfo.bytesTotal; - finalBlock = progressInfo.bytesLoaded >= progressInfo.bytesTotal; - } - - if (this._initialize) { - var PREFETCH_SIZE = 17 /* RECT */ + - 4 /* Frames rate and count */ + - 6 /* FileAttributes */; - if (!buffer.push(data, PREFETCH_SIZE)) - return; - - stream = buffer.createStream(); - var bytes = stream.bytes; - readHeader(bytes, stream, swf, null, null); - - // reading FileAttributes tag, this tag shall be first in the file - var nextTagHeader = readUi16(bytes, stream); - var FILE_ATTRIBUTES_LENGTH = 4; - if (nextTagHeader == ((SwfTag.CODE_FILE_ATTRIBUTES << 6) | FILE_ATTRIBUTES_LENGTH)) { - if (stream.pos + FILE_ATTRIBUTES_LENGTH > stream.end) { - Debug.error('Not enough data.'); - } - - var substream = stream.substream(stream.pos, stream.pos += FILE_ATTRIBUTES_LENGTH); - var handler = tagHandler[SwfTag.CODE_FILE_ATTRIBUTES]; - var fileAttributesTag = {code: SwfTag.CODE_FILE_ATTRIBUTES}; - handler(substream.bytes, substream, fileAttributesTag, swfVersion, SwfTag.CODE_FILE_ATTRIBUTES); - swf.fileAttributes = fileAttributesTag; - } else { - stream.pos -= 2; // FileAttributes tag was not found -- re-winding - swf.fileAttributes = {}; // using empty object here, defaults all attributes to false - } - - if (options.onstart) - options.onstart(swf); - - swf.tags = []; - - this._initialize = false; - } else { - buffer.push(data); - stream = buffer.createStream(); - } - - var readStartTime = performance.now(); - readTags(swf, stream, swfVersion, finalBlock, options.onprogress, options.onexception); - swf.parseTime += performance.now() - readStartTime; - - var read = stream.pos; - buffer.removeHead(read); - this._totalRead += read; - - if (options.oncomplete && swf.tags[swf.tags.length - 1].finalTag) { - options.oncomplete(swf); - } - } - - close() {} - } - - export function parseAsync(options) { - var buffer = new HeadTailBuffer(); - var target: IPipe = null; - - var pipe: IPipe = { - push: function (data: Uint8Array, progressInfo: ProgressInfo) { - if (target !== null) { - return target.push(data, progressInfo); - } - if (!buffer.push(data, 8)) { - return null; - } - var bytes = buffer.getHead(8); - var magic1 = bytes[0]; - var magic2 = bytes[1]; - var magic3 = bytes[2]; - - // check for SWF - if (magic2 === 87 && magic3 === 83) { - Shumway.Debug.assert (magic1 === 70 || magic1 === 67, "Unsupported compression format: " + (magic1 === 90 ? "LZMA" : String(magic1))); - var swfVersion = bytes[3]; - var compressed = magic1 === 67; - parseSWF(compressed, swfVersion, progressInfo); - buffer = null; - return; - } - - var isImage = false; - var imageType; - - // check for JPG - if (magic1 === 0xff && magic2 === 0xd8 && magic3 === 0xff) { - isImage = true; - imageType = 'image/jpeg'; - } else if (magic1 === 0x89 && magic2 === 0x50 && magic3 === 0x4e) { - isImage = true; - imageType = 'image/png'; - } - - if (isImage) { - parseImage(data, progressInfo.bytesTotal, imageType); - } - buffer = null; - }, - close: function () { - if (buffer) { - // buffer was closed: none or few bytes were received - var symbol = { - command: 'empty', - data: buffer.getBytes() - }; - options.oncomplete && options.oncomplete(symbol); - } - if (target && target.close) { - target.close(); - } - } - }; - - function parseSWF(compressed, swfVersion, progressInfo) { - var stream = buffer.createStream(); - stream.pos += 4; - var fileLength = readUi32(null, stream); - var bodyLength = fileLength - 8; - - target = new BodyParser(swfVersion, bodyLength, options); - if (compressed) { - target = new CompressedPipe(target); - } - target.push(buffer.getTail(8), progressInfo); - } - - function parseImage(data, bytesTotal, type) { - var buffer = new Uint8Array(bytesTotal); - buffer.set(data); - var bufferPos = data.length; - - target = { - push: function (data: Uint8Array, progressInfo: ProgressInfo) { - buffer.set(data, bufferPos); - bufferPos += data.length; - }, - close: function () { - var props: any = {}; - var chunks; - if (type == 'image/jpeg') { - chunks = parseJpegChunks(props, buffer); - } else { - parsePngHeaders(props, buffer); - chunks = [buffer.subarray(0, bufferPos)]; - } - var length = 0, pos = 0; - chunks.forEach(function (chunk) { - length += chunk.byteLength; - }); - var data = new Uint8Array(length); - chunks.forEach(function (chunk) { - data.set(chunk, pos); - pos += chunk.byteLength; - }); - props.id = 0; - props.data = data; - props.mimeType = type; - var symbol = { - command: 'image', - type: 'image', - props: props - }; - options.oncomplete && options.oncomplete(symbol); - } - }; - - options.onimgprogress(bytesTotal); - } - - return pipe; - } - - export function parse(buffer, options = {}) { - var pipe = parseAsync(options); - var bytes = new Uint8Array(buffer); - var progressInfo: ProgressInfo = { bytesLoaded: bytes.length, bytesTotal: bytes.length }; - pipe.push(bytes, progressInfo); - pipe.close(); - } -} - diff --git a/src/swf/parser/references.ts b/src/swf/parser/references.ts index 751e8e96a7..cefdb95c82 100644 --- a/src/swf/parser/references.ts +++ b/src/swf/parser/references.ts @@ -17,7 +17,6 @@ /// /// /// -/// /// /// diff --git a/src/swf/parser/shape.ts b/src/swf/parser/shape.ts index 99d713a56d..d6e7c3323a 100644 --- a/src/swf/parser/shape.ts +++ b/src/swf/parser/shape.ts @@ -83,7 +83,7 @@ module Shumway.SWF.Parser { * http://wahlers.com.br/claus/blog/hacking-swf-1-shapes-in-flash/ for details. */ function convertRecordsToShapeData(records, fillPaths: SegmentedPath[], - linePaths: SegmentedPath[], dictionary, dependencies, + linePaths: SegmentedPath[], dependencies: number[], recordsMorph): ShapeData { var isMorph = recordsMorph !== null; @@ -124,9 +124,9 @@ module Shumway.SWF.Parser { allPaths = []; } push.apply(allPaths, fillPaths); - fillPaths = createPathsList(record.fillStyles, false, isMorph, dictionary, dependencies); + fillPaths = createPathsList(record.fillStyles, false, isMorph, dependencies); push.apply(allPaths, linePaths); - linePaths = createPathsList(record.lineStyles, true, isMorph, dictionary, dependencies); + linePaths = createPathsList(record.lineStyles, true, isMorph, dependencies); if (defaultPath) { allPaths.push(defaultPath); defaultPath = null; @@ -190,8 +190,7 @@ module Shumway.SWF.Parser { if (!segment) { if (!defaultPath) { var style = {color: {red: 0, green: 0, blue: 0, alpha: 0}, width: 20}; - defaultPath = new SegmentedPath(null, processStyle(style, true, isMorph, - dictionary, dependencies)); + defaultPath = new SegmentedPath(null, processStyle(style, true, isMorph, dependencies)); } segment = PathSegment.FromDefaults(isMorph); defaultPath.addSegment(segment); @@ -317,16 +316,15 @@ module Shumway.SWF.Parser { var IDENTITY_MATRIX: ShapeMatrix = {a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0}; function processStyle(style, isLineStyle: boolean, isMorph: boolean, - dictionary, dependencies): ShapeStyle { + dependencies: number[]): ShapeStyle { var shapeStyle: ShapeStyle = style; if (isMorph) { - shapeStyle.morph = processMorphStyle(style, isLineStyle, dictionary, dependencies); + shapeStyle.morph = processMorphStyle(style, isLineStyle, dependencies); } if (isLineStyle) { shapeStyle.miterLimit = (style.miterLimitFactor || 1.5) * 2; if (!style.color && style.hasFill) { - var fillStyle = processStyle(style.fillStyle, false, false, - dictionary, dependencies); + var fillStyle = processStyle(style.fillStyle, false, false, dependencies); shapeStyle.type = fillStyle.type; shapeStyle.transform = fillStyle.transform; shapeStyle.colors = fillStyle.colors; @@ -368,16 +366,12 @@ module Shumway.SWF.Parser { style.type !== FillType.NonsmoothedClippedBitmap; shapeStyle.repeat = style.type !== FillType.ClippedBitmap && style.type !== FillType.NonsmoothedClippedBitmap; - if (dictionary[style.bitmapId]) { - shapeStyle.bitmapIndex = dependencies.length; - dependencies.push(style.bitmapId); - scale = 0.05; - } else { - shapeStyle.bitmapIndex = -1; - } + shapeStyle.bitmapIndex = dependencies.length; + dependencies.push(style.bitmapId); + scale = 0.05; break; default: - release || assertUnreachable('shape parser encountered invalid fill style'); + Debug.warning('shape parser encountered invalid fill style ' + style.type); } if (!style.matrix) { shapeStyle.transform = IDENTITY_MATRIX; @@ -397,12 +391,12 @@ module Shumway.SWF.Parser { return shapeStyle; } - function processMorphStyle(style, isLineStyle: boolean, dictionary, dependencies): ShapeStyle { + function processMorphStyle(style, isLineStyle: boolean, dependencies: number[]): ShapeStyle { var morphStyle: ShapeStyle = Object.create(style); if (isLineStyle) { morphStyle.width = style.widthMorph; if (!style.color && style.hasFill) { - var fillStyle = processMorphStyle(style.fillStyle, false, dictionary, dependencies); + var fillStyle = processMorphStyle(style.fillStyle, false, dependencies); morphStyle.transform = fillStyle.transform; morphStyle.colors = fillStyle.colors; morphStyle.ratios = fillStyle.ratios; @@ -464,11 +458,11 @@ module Shumway.SWF.Parser { * all the paths for a certain fill or line style. */ function createPathsList(styles: any[], isLineStyle: boolean, isMorph: boolean, - dictionary: any, dependencies: any): SegmentedPath[] + dependencies: number[]): SegmentedPath[] { var paths: SegmentedPath[] = []; for (var i = 0; i < styles.length; i++) { - var style = processStyle(styles[i], isLineStyle, isMorph, dictionary, dependencies); + var style = processStyle(styles[i], isLineStyle, isMorph, dependencies); if (!isLineStyle) { paths[i] = new SegmentedPath(style, null); } else { @@ -478,14 +472,12 @@ module Shumway.SWF.Parser { return paths; } - export function defineShape(tag, dictionary) { + export function defineShape(tag) { var dependencies = []; - var fillPaths = createPathsList(tag.fillStyles, false, !!tag.recordsMorph, - dictionary, dependencies); - var linePaths = createPathsList(tag.lineStyles, true, !!tag.recordsMorph, - dictionary, dependencies); + var fillPaths = createPathsList(tag.fillStyles, false, !!tag.recordsMorph, dependencies); + var linePaths = createPathsList(tag.lineStyles, true, !!tag.recordsMorph, dependencies); var shape = convertRecordsToShapeData(tag.records, fillPaths, linePaths, - dictionary, dependencies, tag.recordsMorph || null); + dependencies, tag.recordsMorph || null); return { type: tag.isMorph ? 'morphshape' : 'shape', id: tag.id, @@ -526,44 +518,34 @@ module Shumway.SWF.Parser { moveTo(x: number, y: number) { this.commands.writeUnsignedByte(PathCommand.MoveTo); - this.data.writeInt(x); - this.data.writeInt(y); + this.data.write2Ints(x, y); } morphMoveTo(x: number, y: number, mx: number, my: number) { this.moveTo(x, y); - this.morphData.writeInt(mx); - this.morphData.writeInt(my); + this.morphData.write2Ints(mx, my); } lineTo(x: number, y: number) { this.commands.writeUnsignedByte(PathCommand.LineTo); - this.data.writeInt(x); - this.data.writeInt(y); + this.data.write2Ints(x, y); } morphLineTo(x: number, y: number, mx: number, my: number) { this.lineTo(x, y); - this.morphData.writeInt(mx); - this.morphData.writeInt(my); + this.morphData.write2Ints(mx, my); } curveTo(cpx: number, cpy: number, x: number, y: number) { this.commands.writeUnsignedByte(PathCommand.CurveTo); - this.data.writeInt(cpx); - this.data.writeInt(cpy); - this.data.writeInt(x); - this.data.writeInt(y); + this.data.write4Ints(cpx, cpy, x, y); } morphCurveTo(cpx: number, cpy: number, x: number, y: number, mcpx: number, mcpy: number, mx: number, my: number) { this.curveTo(cpx, cpy, x, y); - this.morphData.writeInt(mcpx); - this.morphData.writeInt(mcpy); - this.morphData.writeInt(mx); - this.morphData.writeInt(my); + this.morphData.write4Ints(mcpx, mcpy, mx, my); } /** @@ -843,7 +825,6 @@ module Shumway.SWF.Parser { case FillType.RepeatingBitmap: case FillType.NonsmoothedClippedBitmap: case FillType.NonsmoothedRepeatingBitmap: - release || assert(style.bitmapIndex > -1); writeBitmap(PathCommand.BeginBitmapFill, style, shape); if (morph) { writeMorphBitmap(morph, shape); @@ -877,7 +858,6 @@ module Shumway.SWF.Parser { case FillType.RepeatingBitmap: case FillType.NonsmoothedClippedBitmap: case FillType.NonsmoothedRepeatingBitmap: - release || assert(style.bitmapIndex > -1); writeLineStyle(style, shape); writeBitmap(PathCommand.LineStyleBitmap, style, shape); if (morph) { @@ -937,8 +917,7 @@ module Shumway.SWF.Parser { } function writeBitmap(command: PathCommand, style: ShapeStyle, shape: ShapeData): void { - shape.beginBitmap(command, style.bitmapIndex, style.transform, - style.repeat, style.smooth); + shape.beginBitmap(command, style.bitmapIndex, style.transform, style.repeat, style.smooth); } function writeMorphBitmap(style: ShapeStyle, shape: ShapeData) { diff --git a/src/swf/parser/sound.ts b/src/swf/parser/sound.ts index 8f11cb50ea..a1f98d2c6b 100644 --- a/src/swf/parser/sound.ts +++ b/src/swf/parser/sound.ts @@ -67,7 +67,7 @@ module Shumway.SWF.Parser { }; } - export function defineSound(tag, dictionary) { + export function defineSound(tag) { var channels = tag.soundType == SOUND_TYPE_STEREO ? 2 : 1; var samplesCount = tag.samplesCount; var sampleRate = SOUND_RATES[tag.soundRate]; @@ -124,7 +124,7 @@ module Shumway.SWF.Parser { sampleRate: sampleRate, channels: channels, pcm: pcm, - packaged: undefined + packaged: null }; if (packaged) { sound.packaged = packaged; @@ -210,7 +210,7 @@ module Shumway.SWF.Parser { seek?: number; } - export class SwfSoundStream { + export class SoundStream { streamId: number; samplesCount: number; sampleRate: number; @@ -218,7 +218,7 @@ module Shumway.SWF.Parser { format: any; currentSample: number; - decode: (data) => DecodedSound; + decode: (block: Uint8Array) => DecodedSound; constructor(samplesCount, sampleRate, channels) { this.streamId = (nextSoundStreamId++); @@ -229,14 +229,34 @@ module Shumway.SWF.Parser { this.currentSample = 0; } - get info() { - return { - samplesCount: this.samplesCount, - sampleRate: this.sampleRate, - channels: this.channels, - format: this.format, - streamId: this.streamId - }; + static FromTag(tag): SoundStream { + var channels = tag.streamType == SOUND_TYPE_STEREO ? 2 : 1; + var samplesCount = tag.samplesCount; + var sampleRate = SOUND_RATES[tag.streamRate]; + var stream = new SoundStream(samplesCount, sampleRate, channels); + + switch (tag.streamCompression) { + case SOUND_FORMAT_PCM_BE: + case SOUND_FORMAT_PCM_LE: + stream.format = 'wave'; + if (tag.soundSize == SOUND_SIZE_16_BIT) { + stream.decode = tag.streamCompression === SOUND_FORMAT_PCM_BE ? + SwfSoundStream_decode_PCM_be : + SwfSoundStream_decode_PCM_le; + } else { + stream.decode = SwfSoundStream_decode_PCM; + } + break; + case SOUND_FORMAT_MP3: + stream.format = 'mp3'; + stream.decode = SwfSoundStream_decode_MP3; + break; + default: + Debug.warning('Unsupported audio format: ' + tag.soundFormat); + return null; + } + + return stream; } } @@ -287,38 +307,4 @@ module Shumway.SWF.Parser { seek: seek }; } - - export function createSoundStream(tag): SwfSoundStream { - var channels = tag.streamType == SOUND_TYPE_STEREO ? 2 : 1; - var samplesCount = tag.samplesCount; - var sampleRate = SOUND_RATES[tag.streamRate]; - var stream = new SwfSoundStream(samplesCount, sampleRate, channels); - - switch (tag.streamCompression) { - case SOUND_FORMAT_PCM_BE: - stream.format = 'wave'; - if (tag.soundSize == SOUND_SIZE_16_BIT) { - stream.decode = SwfSoundStream_decode_PCM_be; - } else { - stream.decode = SwfSoundStream_decode_PCM; - } - break; - case SOUND_FORMAT_PCM_LE: - stream.format = 'wave'; - if (tag.soundSize == SOUND_SIZE_16_BIT) { - stream.decode = SwfSoundStream_decode_PCM_le; - } else { - stream.decode = SwfSoundStream_decode_PCM; - } - break; - case SOUND_FORMAT_MP3: - stream.format = 'mp3'; - stream.decode = SwfSoundStream_decode_MP3; - break; - default: - throw new Error('Unsupported audio format: ' + tag.soundFormat); - } - - return stream; - } -} \ No newline at end of file +} diff --git a/src/swf/parser/templates.ts b/src/swf/parser/templates.ts index ef43e33e42..e7e78b1055 100644 --- a/src/swf/parser/templates.ts +++ b/src/swf/parser/templates.ts @@ -147,17 +147,4 @@ module Shumway.SWF.Parser { } return str; } - - export function readBinary($bytes, $stream, size, temporaryUsage: boolean): Uint8Array { - if (!size) { - size = $stream.end - $stream.pos; - } - var subArray = $bytes.subarray($stream.pos, $stream.pos = ($stream.pos + size)); - if (temporaryUsage) { - return subArray; - } - var result = new Uint8Array(size); - result.set(subArray); - return result; - } } diff --git a/src/swf/parser/text.ts b/src/swf/parser/text.ts index e10d7a8852..8825b7dc43 100644 --- a/src/swf/parser/text.ts +++ b/src/swf/parser/text.ts @@ -16,34 +16,18 @@ /// module Shumway.SWF.Parser { - export function defineText(tag, dictionary) { - var dependencies = []; + export function defineText(tag) { var bold = false; var italic = false; - if (tag.hasFont) { - var font = dictionary[tag.fontId]; - if (font) { - dependencies.push(font.id); - bold = font.bold; - italic = font.italic; - } else { - Shumway.Debug.warning("Font is not defined."); - } - } - var props = { + return { type: 'text', id: tag.id, fillBounds: tag.bbox, variableName: tag.variableName, // for AVM1 tag: tag, bold: bold, - italic: italic, - require: undefined + italic: italic }; - if (dependencies.length) { - props.require = dependencies; - } - return props; } } diff --git a/src/swf/references.ts b/src/swf/references.ts index 0209c429a8..ace248e638 100644 --- a/src/swf/references.ts +++ b/src/swf/references.ts @@ -17,8 +17,11 @@ /// /// -/// /// +/// +/// /// /// +/// +/// /// diff --git a/src/swf/resourceLoader.ts b/src/swf/resourceLoader.ts deleted file mode 100644 index 53a2afbe1f..0000000000 --- a/src/swf/resourceLoader.ts +++ /dev/null @@ -1,449 +0,0 @@ -/* - * Copyright 2013 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -module Shumway.SWF { - import SwfTag = Shumway.SWF.Parser.SwfTag; - import createSoundStream = Shumway.SWF.Parser.createSoundStream; - - declare class FileReaderSync { - readAsArrayBuffer(request):ArrayBuffer; - } - - function defineSymbol(swfTag, symbols, commitData) { - var symbol; - - switch (swfTag.code) { - case SwfTag.CODE_DEFINE_BITS: - case SwfTag.CODE_DEFINE_BITS_JPEG2: - case SwfTag.CODE_DEFINE_BITS_JPEG3: - case SwfTag.CODE_DEFINE_BITS_JPEG4: - case SwfTag.CODE_JPEG_TABLES: - symbol = Shumway.SWF.Parser.defineImage(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_BITS_LOSSLESS: - case SwfTag.CODE_DEFINE_BITS_LOSSLESS2: - symbol = Shumway.SWF.Parser.defineBitmap(swfTag); - break; - case SwfTag.CODE_DEFINE_BUTTON: - case SwfTag.CODE_DEFINE_BUTTON2: - symbol = Shumway.SWF.Parser.defineButton(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_EDIT_TEXT: - symbol = Shumway.SWF.Parser.defineText(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_FONT: - case SwfTag.CODE_DEFINE_FONT2: - case SwfTag.CODE_DEFINE_FONT3: - case SwfTag.CODE_DEFINE_FONT4: - symbol = Shumway.SWF.Parser.defineFont(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_MORPH_SHAPE: - case SwfTag.CODE_DEFINE_MORPH_SHAPE2: - case SwfTag.CODE_DEFINE_SHAPE: - case SwfTag.CODE_DEFINE_SHAPE2: - case SwfTag.CODE_DEFINE_SHAPE3: - case SwfTag.CODE_DEFINE_SHAPE4: - symbol = Shumway.SWF.Parser.defineShape(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_SOUND: - symbol = Shumway.SWF.Parser.defineSound(swfTag, symbols); - break; - case SwfTag.CODE_DEFINE_BINARY_DATA: - symbol = { - type: 'binary', - id: swfTag.id, - // TODO: make transferable - data: swfTag.data - }; - break; - case SwfTag.CODE_DEFINE_SPRITE: - var commands = []; - var frame:any = { type: 'frame' }; - var frames = []; - var tags = swfTag.tags; - var frameScripts = null; - var frameIndex = 0; - var soundStream = null; - for (var i = 0, n = tags.length; i < n; i++) { - var tag:any = tags[i]; - if ('id' in tag) { - // According to Chapter 13 of the SWF format spec, no nested definition tags are - // allowed within DefineSprite. However, they're added to the symbol dictionary - // anyway, and some tools produce them. Notably swfmill. - // We essentially treat them as though they came before the current sprite. That - // should be ok because it doesn't make sense for them to rely on their parent being - // fully defined - so they don't have to come after it -, and any control tags within - // the parent will just pick them up the moment they're defined, just as always. - var symbol = defineSymbol(tag, symbols, commitData); - commitData(symbol, symbol.transferables); - continue; - } - switch (tag.code) { - case SwfTag.CODE_DO_ACTION: - if (!frameScripts) - frameScripts = []; - frameScripts.push(frameIndex); - frameScripts.push(tag.actionsData); - break; - // case SwfTag.CODE_DO_INIT_ACTION: ?? - case SwfTag.CODE_START_SOUND: - commands.push(tag); - break; - case SwfTag.CODE_SOUND_STREAM_HEAD: - try { - // TODO: make transferable - soundStream = createSoundStream(tag); - frame.soundStream = soundStream.info; - } catch (e) { - // ignoring if sound stream codec is not supported - // console.error('ERROR: ' + e.message); - } - break; - case SwfTag.CODE_SOUND_STREAM_BLOCK: - if (soundStream) { - frame.soundStreamBlock = soundStream.decode(tag.data); - } - break; - case SwfTag.CODE_FRAME_LABEL: - frame.labelName = tag.name; - break; - case SwfTag.CODE_PLACE_OBJECT: - case SwfTag.CODE_PLACE_OBJECT2: - case SwfTag.CODE_PLACE_OBJECT3: - commands.push(tag); - break; - case SwfTag.CODE_REMOVE_OBJECT: - case SwfTag.CODE_REMOVE_OBJECT2: - commands.push(tag); - break; - case SwfTag.CODE_SHOW_FRAME: - frameIndex += tag.repeat; - frame.repeat = tag.repeat; - frame.commands = commands; - frames.push(frame); - commands = []; - frame = { type: 'frame' }; - break; - default: - Debug.warning('Dropped tag during parsing. Code: ' + tag.code); - } - } - if (frames.length === 0) { - // We need at least one frame - frame.repeat = 1; - frame.commands = commands; - frames.push(frame); - } - symbol = { - type: 'sprite', - id: swfTag.id, - frameCount: swfTag.frameCount, - frames: frames, - frameScripts: frameScripts - }; - break; - case SwfTag.CODE_DEFINE_TEXT: - case SwfTag.CODE_DEFINE_TEXT2: - symbol = Shumway.SWF.Parser.defineLabel(swfTag, symbols); - break; - default: - Debug.warning('Dropped tag during parsing. Code: ' + tag.code); - } - - if (!symbol) { - return {command: 'error', message: 'unknown symbol type: ' + swfTag.code}; - } - - symbol.isSymbol = true; - symbols[swfTag.id] = symbol; - return symbol; - } - - function createParsingContext(commitData) { - var commands = []; - var symbols = {}; - var frame:any = { type: 'frame' }; - var tagsProcessed = 0; - var soundStream = null; - var bytesLoaded = 0; - - return { - onstart: function (result) { - commitData({command: 'init', result: result}); - }, - onimgprogress: function (bytesTotal) { - // image progress events are sent with 1K increments - while (bytesLoaded <= bytesTotal) { - commitData({command: 'progress', result: { - bytesLoaded: bytesLoaded, - bytesTotal: bytesTotal, - open: true - }}); - bytesLoaded += Math.min(bytesTotal - bytesLoaded || 1024, 1024); - } - }, - onprogress: function (result) { - // sending progress events with 64K increments - if (result.bytesLoaded - bytesLoaded >= 65536) { - while (bytesLoaded < result.bytesLoaded) { - if (bytesLoaded) { - commitData({command: 'progress', result: { - bytesLoaded: bytesLoaded, - bytesTotal: result.bytesTotal - }}); - } - bytesLoaded += 65536; - } - } - - var tags = result.tags; - for (var n = tags.length; tagsProcessed < n; tagsProcessed++) { - var tag = tags[tagsProcessed]; - if ('id' in tag) { - var symbol = defineSymbol(tag, symbols, commitData); - commitData(symbol, symbol.transferables); - continue; - } - - switch (tag.code) { - case SwfTag.CODE_DEFINE_SCENE_AND_FRAME_LABEL_DATA: - frame.sceneData = tag; - break; - case SwfTag.CODE_DEFINE_SCALING_GRID: - var symbolUpdate = { - isSymbol: true, - id: tag.symbolId, - updates: { - scale9Grid: tag.splitter - } - }; - commitData(symbolUpdate); - break; - case SwfTag.CODE_DO_ABC: - case SwfTag.CODE_DO_ABC_: - commitData({ - type: 'abc', - flags: tag.flags, - name: tag.name, - data: tag.data - }); - break; - case SwfTag.CODE_DO_ACTION: - var actionBlocks = frame.actionBlocks; - if (actionBlocks) - actionBlocks.push(tag.actionsData); - else - frame.actionBlocks = [tag.actionsData]; - break; - case SwfTag.CODE_DO_INIT_ACTION: - var initActionBlocks = frame.initActionBlocks || - (frame.initActionBlocks = []); - initActionBlocks.push({spriteId: tag.spriteId, actionsData: tag.actionsData}); - break; - case SwfTag.CODE_START_SOUND: - commands.push(tag); - break; - case SwfTag.CODE_SOUND_STREAM_HEAD: - try { - // TODO: make transferable - soundStream = createSoundStream(tag); - frame.soundStream = soundStream.info; - } catch (e) { - // ignoring if sound stream codec is not supported - // console.error('ERROR: ' + e.message); - } - break; - case SwfTag.CODE_SOUND_STREAM_BLOCK: - if (soundStream) { - frame.soundStreamBlock = soundStream.decode(tag.data); - } - break; - case SwfTag.CODE_EXPORT_ASSETS: - var exports = frame.exports; - if (exports) - frame.exports = exports.concat(tag.exports); - else - frame.exports = tag.exports.slice(0); - break; - case SwfTag.CODE_SYMBOL_CLASS: - var symbolClasses = frame.symbolClasses; - if (symbolClasses) - frame.symbolClasses = symbolClasses.concat(tag.exports); - else - frame.symbolClasses = tag.exports.slice(0); - break; - case SwfTag.CODE_FRAME_LABEL: - frame.labelName = tag.name; - break; - case SwfTag.CODE_PLACE_OBJECT: - case SwfTag.CODE_PLACE_OBJECT2: - case SwfTag.CODE_PLACE_OBJECT3: - commands.push(tag); - break; - case SwfTag.CODE_REMOVE_OBJECT: - case SwfTag.CODE_REMOVE_OBJECT2: - commands.push(tag); - break; - case SwfTag.CODE_SET_BACKGROUND_COLOR: - frame.bgcolor = tag.color; - break; - case SwfTag.CODE_SHOW_FRAME: - frame.repeat = tag.repeat; - frame.commands = commands; - frame.complete = !!tag.finalTag; - commitData(frame); - commands = []; - frame = { type: 'frame' }; - break; - default: - Debug.warning('Dropped tag during parsing. Code: ' + tag.code); - } - } - - if (result.bytesLoaded >= result.bytesTotal) { - commitData({command: 'progress', result: { - bytesLoaded: result.bytesLoaded, - bytesTotal: result.bytesTotal - }}); - } - }, - oncomplete: function (result) { - commitData(result); - - var stats; - if (typeof result.swfVersion === 'number') { - // Extracting stats from the context object - var bbox = result.bbox; - stats = { - topic: 'parseInfo', // HACK additional field for telemetry - parseTime: result.parseTime, - bytesTotal: result.bytesTotal, - swfVersion: result.swfVersion, - frameRate: result.frameRate, - width: (bbox.xMax - bbox.xMin) / 20, - height: (bbox.yMax - bbox.yMin) / 20, - isAvm2: !!result.fileAttributes.doAbc - }; - } - - commitData({command: 'complete', stats: stats}); - }, - onexception: function (e) { - commitData({type: 'exception', message: e.message, stack: e.stack}); - } - }; - } - - function parseBytes(bytes, commitData) { - Shumway.SWF.Parser.parse(bytes, createParsingContext(commitData)); - } - - interface IPostMessageCapable { - postMessage(data: any, transferables?: Array); - } - - export class ResourceLoader { - private _subscription: any; - private _messenger: IPostMessageCapable; - - constructor(scope, isWorker) { - this._subscription = null; - - var self = this; - if (!isWorker) { - this._messenger = { - postMessage: function (data) { - self.onmessage({data: data}); - } - }; - } else { - this._messenger = scope; - scope.onmessage = function (event) { - self.listener(event.data); - }; - } - } - - terminate() { - this._messenger = null; - this.listener = null; - } - - onmessage(event) { - this.listener(event.data); - } - - postMessage(data) { - this.listener && this.listener(data); - } - - listener(data) { - if (this._subscription) { - this._subscription.callback(data.data, data.progress); - } else if (data === 'pipe:') { - // progressive data loading is requested, replacing onmessage handler - // for the following messages - this._subscription = { - subscribe: function (callback) { - this.callback = callback; - } - }; - this.parseLoadedData(this._messenger, this._subscription); - } else { - this.parseLoadedData(this._messenger, data); - } - } - - private parseLoadedData(loader: IPostMessageCapable, request) { - function commitData(data, transferables) { - try { - loader.postMessage(data, transferables); - } catch (ex) { - // Attempting to fix IE10/IE11 transferables by retrying without - // Transferables. - if (ex != 'DataCloneError') { - throw ex; - } - loader.postMessage(data); - } - } - - if (request instanceof ArrayBuffer) { - parseBytes(request, commitData); - } else if ('subscribe' in request) { - var pipe = Shumway.SWF.Parser.parseAsync(createParsingContext(commitData)); - request.subscribe(function (data, progress) { - if (data) { - pipe.push(data, progress); - } else { - pipe.close(); - } - }); - } else if (typeof FileReaderSync !== 'undefined') { - var readerSync = new FileReaderSync(); - var buffer = readerSync.readAsArrayBuffer(request); - parseBytes(buffer, commitData); - } else { - var reader = new FileReader(); - reader.onload = function () { - parseBytes(this.result, commitData); - }; - reader.readAsArrayBuffer(request); - } - } - } -} diff --git a/src/tools/profiler/timelineFrame.ts b/src/tools/profiler/timelineFrame.ts index 6b364ddbf4..dbb3befaf5 100644 --- a/src/tools/profiler/timelineFrame.ts +++ b/src/tools/profiler/timelineFrame.ts @@ -233,6 +233,20 @@ module Shumway.Tools.Profiler { } visit(this); } + + public trace(writer: IndentingWriter) { + var s = (this.kind ? this.kind.name + ": " : "Profile: ") + + (this.endTime - this.startTime).toFixed(2); + if (this.children && this.children.length) { + writer.enter(s); + for (var i = 0; i < this.children.length; i++) { + this.children[i].trace(writer); + } + writer.outdent(); + } else { + writer.writeLn(s); + } + } } export class TimelineBufferSnapshot extends TimelineFrame { diff --git a/test/swfs/avm1/nested-button/nested-button-click.fla b/test/swfs/avm1/nested-button/nested-button-click.fla new file mode 100644 index 0000000000000000000000000000000000000000..4dfa73e04c2df85284740a756cc002ba9a17bd74 GIT binary patch literal 6873 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 Mu!?Nzo}hsL2iuU%7ytkO literal 0 HcmV?d00001 diff --git a/test/swfs/avm1/nested-button/nested-button-click.stas b/test/swfs/avm1/nested-button/nested-button-click.stas new file mode 100644 index 0000000000..012080ed98 --- /dev/null +++ b/test/swfs/avm1/nested-button/nested-button-click.stas @@ -0,0 +1,13 @@ +run_test = function (t, file) { + print ("Testing " + file); + t.reset (file); + var expected = Buffer.load (file + ".trace"); + t.mouse_press (50, 40); + var diff = t.trace.diff (expected); +}; + +t = new Test (); +for (var i = 0; i < filenames.length; i++) { + run_test (t, filenames[i]); +} + diff --git a/test/swfs/avm1/nested-button/nested-button-click.swf b/test/swfs/avm1/nested-button/nested-button-click.swf new file mode 100644 index 0000000000000000000000000000000000000000..47ea60100591d57c860a6176c9b37884d830d4eb GIT binary patch literal 622 zcmV-!0+IbgS5pyX1pokeoTZdKZ__{+$IopV=mJzNf(cov#DbE$bKb4QDvA@SLj;Pt zqlTHMq|h`9VA;MG{h#06^S_r7 z*m?%Q?RNm&SAe;52LSM5_f-mjT4?w5j@eR2p6hd6w(6O_5P=Tia5&5j^EnnCLtL#^ zA=03xWu+p^$G#w=tk3r|h8h!0%B|1|gu{F_YLhM-hdPV;OzD>RM6O%$Y-x0Zzct>pReytX!EQO z?rwmsgEK_|Z+rQPvIpX4vQzx<65ROy{=t`vAKxY)pQ>Q89o=agGWI;3R%8JVBH^|n z|C_RTkw&t1u1Lw}=U+edBh1a|w=0V`w*cr4gpjsaGM^LhFdk^ih{&mgygBWS>#1q| I7o3QnNd(tAYXATM literal 0 HcmV?d00001 diff --git a/test/swfs/avm1/nested-button/nested-button-click.swf.trace b/test/swfs/avm1/nested-button/nested-button-click.swf.trace new file mode 100644 index 0000000000..0a2e1d136d --- /dev/null +++ b/test/swfs/avm1/nested-button/nested-button-click.swf.trace @@ -0,0 +1 @@ +button click diff --git a/test/swfs/avm1/rollover/avm1-rollover.swd b/test/swfs/avm1/rollover/avm1-rollover.swd deleted file mode 100644 index a02b3e17b3b2cd198a59fc042805b8f5282d3da7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 582 zcmZ<@cVTB{U|_gq&zXE=d7|%nn|%gP+b2B$3Nh@fVPIec(vHa`nfZCe3TgR83c<;# zd8rD9Rtj!KiMgo?Mhf|93O$gqph$i@9h%!euUs zj|@RRf;vbqKQAaXCpED+Rl!yvtuzlDSPB}N3e{YkP)%?ZAZ@k4u+Z060E*@3