├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src ├── avm1.rs ├── bin │ └── flashback.rs ├── bitmap.rs ├── button.rs ├── dictionary.rs ├── export │ ├── js │ │ ├── avm1.rs │ │ ├── mod.rs │ │ ├── sound.rs │ │ └── timeline.rs │ ├── mod.rs │ └── svg │ │ ├── animate.rs │ │ ├── mod.rs │ │ └── runtime.js ├── lib.rs ├── shape.rs ├── sound.rs └── timeline.rs └── web ├── Cargo.toml ├── index.html └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | /.vscode 5 | 6 | # HACK(eddyb) to facilitate testing 7 | *.swf 8 | *.svg 9 | *.svgz 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | cache: cargo 7 | script: cargo test --all 8 | branches: 9 | only: 10 | - master 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flashback" 3 | version = "0.0.1" 4 | authors = ["Eduard-Mihai Burtescu "] 5 | edition = "2018" 6 | repository = "https://github.com/lykenware/flashback" 7 | license = "MIT/Apache-2.0" 8 | keywords = ["Flash", "SWF"] 9 | readme = "README.md" 10 | description = "Adobe Flash / SWF preservation tools." 11 | 12 | [dependencies] 13 | avm1-parser = "0.2.0" 14 | avm1-tree = "0.2.1" 15 | swf-parser = "0.11.0" 16 | swf-types = "0.11.0" 17 | svg = "0.5.12" 18 | image = "0.20.1" 19 | inflate = "0.4.4" 20 | base64 = "0.10.0" 21 | structopt = "0.3" 22 | 23 | [lib] 24 | doctest = false 25 | test = false 26 | 27 | [workspace] 28 | members = [ 29 | "web", 30 | ] 31 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 FlashBack Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adobe Flash / SWF preservation tools 2 | 3 | [![Build Status](https://travis-ci.com/lykenware/flashback.svg?branch=master)](https://travis-ci.com/lykenware/flashback) 4 | [![Latest Version](https://img.shields.io/crates/v/flashback.svg)](https://crates.io/crates/flashback) 5 | [![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/flashback) 6 | 7 | The goal is to convert SWF files to more durable technologies (SVG, WASM, etc.), 8 | avoiding on-the-fly emulation as much as possible. 9 | 10 | ## Status 11 | 12 | *This is an **experimental** project, with no progress/completion guarantees.* 13 | 14 | Feel free to try it out and report issues, but keep in mind that errors 15 | explicitly mentioning `swf-parser` or `Unknown {...}` tags are caused 16 | by limitations in the [Open Flash] components (also see relevant section). 17 | 18 | ### Conversion to SVG+JS 19 | 20 | This is now the default mode. As much as possible (paths, bitmaps, etc.) is 21 | statically present in the SVG, while animations and actions are driven by JS. 22 | 23 | Note that relying on JS means `` tags in HTML can't display these SVGs, 24 | and you need embed them, either directly, or via `` or ``. 25 | 26 | `cargo run foo.swf` will output a `foo.svg` file, which hopefully resembles 27 | the original, at least partially. It can also process multiple files, so you 28 | can use `cargo run your-flash-stash/*.swf` to get a representative sample. 29 | 30 | ### Conversion to animated SVGs 31 | 32 | While currently not exposed via the CLI (see `src/bin/flashback.rs`), there 33 | is support for producing an animated SVG, which works with ``. 34 | 35 | This uses `` and ``, and supports all of the 36 | static resources (e.g. paths and bitmaps) that the SVG+JS mode does. 37 | 38 | There doesn't seem to be an easy way to handle sprites' independent animation, 39 | and right now multiple instances of the same sprite are always in sync. 40 | 41 | AVM1 (AS1/AS2) actions are not supported in this mode, although it might be 42 | possible to handle some cases which result in deterministic animations. 43 | 44 | SVG animations also appear to have some sort of event support, could be usable. 45 | 46 | ## Is this another "Open Source/Third-Party Flash Player" project? 47 | 48 | Ideally, no. The main difference is the split between the conversion step 49 | ("recompilation" in a sense) and the runtime required to view/interact with 50 | the result of the conversion. A separate conversion step can afford to take 51 | longer than a player, in order to reduce the burden on the runtime, which 52 | can be smaller, or even non-existent (e.g. the animated SVG mode). 53 | 54 | It's not clear yet how well this model scales to the full Flash featureset, 55 | specifically the possibility of dynamically manipulating everything. 56 | This is why this is primarily an experiment, and may grow several 57 | different backends, to deal with as many usecases as possible. 58 | 59 | ## Relation to the [Open Flash] project 60 | 61 | [Open Flash]'s goals align well with this project, and its components will be 62 | used where they suffice, to avoid wasting time reinventing the wheel. 63 | 64 | It's possible that contributions will be made to [Open Flash] components if 65 | necessary, and this project might even be merged into [Open Flash] 66 | (if it gets past the experimental stage). 67 | 68 | See also their ["Related projects"](https://github.com/open-flash/open-flash#related-projects) section. 69 | 70 | [Open Flash]: https://github.com/open-flash/open-flash#open-flash 71 | 72 | ## License 73 | 74 | Licensed under either of 75 | 76 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 77 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 78 | 79 | at your option. 80 | 81 | ### Contribution 82 | 83 | Unless you explicitly state otherwise, any contribution intentionally submitted 84 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 85 | be dual licensed as above, without any additional terms or conditions. 86 | -------------------------------------------------------------------------------- /src/avm1.rs: -------------------------------------------------------------------------------- 1 | use crate::timeline::Frame; 2 | 3 | #[derive(Clone, Debug)] 4 | pub enum Value { 5 | Undefined, 6 | Null, 7 | Bool(bool), 8 | I32(i32), 9 | F32(f32), 10 | F64(f64), 11 | Str(String), 12 | 13 | OpRes(usize), 14 | } 15 | 16 | impl Value { 17 | pub fn as_i32(&self) -> Option { 18 | match *self { 19 | Value::I32(x) => Some(x), 20 | Value::F32(x) if x == (x as i32 as f32) => Some(x as i32), 21 | Value::F64(x) if x == (x as i32 as f64) => Some(x as i32), 22 | _ => None, 23 | } 24 | } 25 | 26 | pub fn as_str(&self) -> Option<&str> { 27 | match self { 28 | Value::Str(s) => Some(s), 29 | _ => None, 30 | } 31 | } 32 | } 33 | 34 | #[derive(Debug)] 35 | pub enum Op { 36 | Play, 37 | Stop, 38 | GotoFrame(Frame), 39 | // FIXME(eddyb) can we statically resolve this? 40 | GotoLabel(String), 41 | GetUrl(String, String), 42 | 43 | GetVar(String), 44 | SetVar(String, Value), 45 | 46 | Call(Value, Vec), 47 | // FIXME(eddyb) integrate with GetMember. 48 | CallMethod(Value, String, Vec), 49 | } 50 | 51 | #[derive(Debug)] 52 | pub struct Code { 53 | pub ops: Vec, 54 | } 55 | 56 | impl Code { 57 | pub fn parse_and_compile(mut data: &[u8]) -> Self { 58 | let mut actions = vec![]; 59 | while data[0] != 0 { 60 | let (rest, action) = avm1_parser::parse_action(data).unwrap(); 61 | data = rest; 62 | actions.push(action); 63 | } 64 | assert_eq!(data, [0]); 65 | 66 | Code::compile(actions) 67 | } 68 | 69 | pub fn compile(actions: Vec) -> Self { 70 | let mut consts = vec![]; 71 | let mut regs = vec![]; 72 | let mut stack = vec![]; 73 | let mut ops = vec![]; 74 | 75 | // HACK(eddyb) this hides the warnings / inference errors about `regs`. 76 | // FIXME(eddyb) remove after register writes are implemented. 77 | regs.push(Value::Undefined); 78 | regs.pop(); 79 | 80 | for action in actions { 81 | match action { 82 | avm1_tree::Action::Play => ops.push(Op::Play), 83 | avm1_tree::Action::Stop => ops.push(Op::Stop), 84 | avm1_tree::Action::GotoFrame(goto) => { 85 | ops.push(Op::GotoFrame(Frame(goto.frame as u16))); 86 | } 87 | avm1_tree::Action::GotoLabel(goto) => { 88 | ops.push(Op::GotoLabel(goto.label)); 89 | } 90 | avm1_tree::Action::GetUrl(get_url) => { 91 | ops.push(Op::GetUrl(get_url.url, get_url.target)); 92 | } 93 | 94 | // All of frames are loaded ahead of time, no waiting needed. 95 | avm1_tree::Action::WaitForFrame(_) => {} 96 | avm1_tree::Action::WaitForFrame2(_) => { 97 | stack.pop(); 98 | } 99 | 100 | avm1_tree::Action::ConstantPool(pool) => { 101 | consts = pool.constant_pool; 102 | } 103 | avm1_tree::Action::Push(push) => { 104 | stack.extend(push.values.into_iter().map(|value| match value { 105 | avm1_tree::Value::Undefined => Value::Undefined, 106 | avm1_tree::Value::Null => Value::Null, 107 | avm1_tree::Value::Boolean(x) => Value::Bool(x), 108 | avm1_tree::Value::Sint32(x) => Value::I32(x), 109 | avm1_tree::Value::Float32(x) => Value::F32(x), 110 | avm1_tree::Value::Float64(x) => Value::F64(x), 111 | avm1_tree::Value::String(s) => Value::Str(s), 112 | 113 | // FIXME(eddyb) avoid per-use cloning. 114 | avm1_tree::Value::Constant(i) => Value::Str(consts[i as usize].to_string()), 115 | avm1_tree::Value::Register(i) => regs[i as usize].clone(), 116 | })); 117 | } 118 | avm1_tree::Action::Pop => { 119 | stack.pop(); 120 | } 121 | avm1_tree::Action::GetVariable => match stack.pop().unwrap() { 122 | Value::Str(name) => { 123 | ops.push(Op::GetVar(name)); 124 | stack.push(Value::OpRes(ops.len() - 1)); 125 | } 126 | name => { 127 | eprintln!("avm1: too dynamic GetVar({:?})", name); 128 | break; 129 | } 130 | }, 131 | avm1_tree::Action::SetVariable => { 132 | let value = stack.pop().unwrap(); 133 | match stack.pop().unwrap() { 134 | Value::Str(name) => { 135 | ops.push(Op::SetVar(name, value)); 136 | stack.push(Value::OpRes(ops.len() - 1)); 137 | } 138 | name => { 139 | eprintln!("avm1: too dynamic SetVar({:?}, {:?})", name, value); 140 | break; 141 | } 142 | } 143 | } 144 | avm1_tree::Action::CallFunction => { 145 | let name = stack.pop().unwrap(); 146 | let arg_count = stack.pop().unwrap(); 147 | match (name, arg_count.as_i32()) { 148 | (Value::Str(name), Some(arg_count)) => { 149 | let args = (0..arg_count).map(|_| stack.pop().unwrap()).collect(); 150 | ops.push(Op::GetVar(name)); 151 | ops.push(Op::Call(Value::OpRes(ops.len() - 1), args)); 152 | stack.push(Value::OpRes(ops.len() - 1)); 153 | } 154 | (name, _) => { 155 | eprintln!( 156 | "avm1: too dynamic CallFunction({:?}, {:?})", 157 | name, arg_count 158 | ); 159 | break; 160 | } 161 | } 162 | } 163 | avm1_tree::Action::CallMethod => { 164 | let mut name = stack.pop().unwrap(); 165 | let this = stack.pop().unwrap(); 166 | let arg_count = stack.pop().unwrap(); 167 | 168 | if let Value::Str(s) = &name { 169 | if s.is_empty() { 170 | name = Value::Undefined; 171 | } 172 | } 173 | 174 | match (name, arg_count.as_i32()) { 175 | (Value::Undefined, Some(arg_count)) => { 176 | let args = (0..arg_count).map(|_| stack.pop().unwrap()).collect(); 177 | ops.push(Op::Call(this, args)); 178 | stack.push(Value::OpRes(ops.len() - 1)); 179 | } 180 | (Value::Str(name), Some(arg_count)) => { 181 | let args = (0..arg_count).map(|_| stack.pop().unwrap()).collect(); 182 | ops.push(Op::CallMethod(this, name, args)); 183 | stack.push(Value::OpRes(ops.len() - 1)); 184 | } 185 | (name, _) => { 186 | eprintln!("avm1: too dynamic CallMethod({:?}, {:?})", name, arg_count); 187 | break; 188 | } 189 | } 190 | } 191 | _ => { 192 | eprintln!("unknown action: {:?}", action); 193 | break; 194 | } 195 | } 196 | } 197 | 198 | Code { ops } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/bin/flashback.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | use structopt::StructOpt; 3 | 4 | #[derive(Debug,StructOpt)] 5 | struct Opt{ 6 | #[structopt(long)] 7 | use_js:bool, 8 | #[structopt(required = true)] 9 | files: Vec, 10 | } 11 | 12 | fn main() { 13 | let opt = Opt::from_args(); 14 | for path in opt.files { 15 | let data = fs::read(&path).unwrap(); 16 | eprint!("{}:", path.display()); 17 | match swf_parser::parse_swf(&data) { 18 | Ok(movie) => { 19 | // println!("{:#?}", movie); 20 | let document = flashback::export::svg::export( 21 | &movie, 22 | flashback::export::svg::Config { 23 | use_js: opt.use_js, 24 | }, 25 | ); 26 | svg::save(path.with_extension("svg"), &document).unwrap(); 27 | } 28 | Err(e) => { 29 | eprintln!("swf-parser errored: {:?}", e); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/bitmap.rs: -------------------------------------------------------------------------------- 1 | use image::{DynamicImage, Rgb, RgbImage, Rgba, RgbaImage}; 2 | use swf_types as swf; 3 | 4 | pub struct Bitmap { 5 | pub image: DynamicImage, 6 | } 7 | 8 | impl<'a> From<&'a swf::tags::DefineBitmap> for Bitmap { 9 | fn from(bitmap: &swf::tags::DefineBitmap) -> Self { 10 | let has_alpha = match bitmap.media_type { 11 | swf::ImageType::SwfLossless1 => false, 12 | swf::ImageType::SwfLossless2 => true, 13 | _ => { 14 | eprintln!("Bitmap::from: unsupported type: {:?}", bitmap.media_type); 15 | 16 | return Bitmap { 17 | image: DynamicImage::ImageRgb8(RgbImage::new( 18 | bitmap.width as u32, 19 | bitmap.height as u32, 20 | )), 21 | }; 22 | } 23 | }; 24 | 25 | let format = bitmap.data[0]; 26 | let width = u16::from_le_bytes([bitmap.data[1], bitmap.data[2]]); 27 | let height = u16::from_le_bytes([bitmap.data[3], bitmap.data[4]]); 28 | 29 | let (color_table_len, compressed_data) = if format == 3 { 30 | (bitmap.data[5] as usize + 1, &bitmap.data[6..]) 31 | } else { 32 | (0, &bitmap.data[5..]) 33 | }; 34 | 35 | let data = inflate::inflate_bytes_zlib(compressed_data).unwrap(); 36 | 37 | let (color_table, data) = data.split_at(color_table_len * (3 + has_alpha as usize)); 38 | 39 | // FIXME(eddyb) this is probably really inefficient. 40 | let rgb_px = |px: &[u8]| { 41 | let px = match format { 42 | 3 => { 43 | let i = px[0] as usize * 3; 44 | 45 | &color_table[i..i + 3] 46 | } 47 | 4 => { 48 | let rgb = u16::from_be_bytes([px[0], px[1]]); 49 | let (r, g, b) = (rgb >> 10, (rgb >> 5) & 0x1f, rgb & 0x1f); 50 | 51 | // Uniformly map a 5-bit channel to a 8-bit one by repeating 52 | // the top 3 bits below the original 5 bits, to turn e.g. 53 | // 0x00 into 0x00, 0x10 into 0x84 and 0x1f into 0xff. 54 | let extend = |x| ((x << 3) | (x >> 2)) as u8; 55 | 56 | return Rgb([extend(r), extend(g), extend(b)]); 57 | } 58 | 5 => px, 59 | _ => unreachable!(), 60 | }; 61 | Rgb([px[0], px[1], px[2]]) 62 | }; 63 | let rgba_px = |px: &[u8]| { 64 | let px = match format { 65 | 3 => { 66 | let i = px[0] as usize * 4; 67 | &color_table[i..i + 4] 68 | } 69 | 5 => px, 70 | _ => unreachable!(), 71 | }; 72 | Rgba([px[0], px[1], px[2], px[3]]) 73 | }; 74 | 75 | let px_bytes = match format { 76 | 3 => 1, 77 | 4 => 2, 78 | 5 => 4, 79 | _ => { 80 | eprintln!("Bitmap::from: unsupported bitmap format {}", format); 81 | 82 | return Bitmap { 83 | image: DynamicImage::ImageRgb8(RgbImage::new( 84 | bitmap.width as u32, 85 | bitmap.height as u32, 86 | )), 87 | }; 88 | } 89 | }; 90 | let row_len = (width as usize * px_bytes + 3) / 4 * 4; 91 | let image = if has_alpha { 92 | // FIXME(eddyb) figure out how to deduplicate all of this. 93 | DynamicImage::ImageRgba8(RgbaImage::from_fn(width as u32, height as u32, |x, y| { 94 | let i = y as usize * row_len + x as usize * px_bytes; 95 | rgba_px(&data[i..i + px_bytes]) 96 | })) 97 | } else { 98 | DynamicImage::ImageRgb8(RgbImage::from_fn(width as u32, height as u32, |x, y| { 99 | let i = y as usize * row_len + x as usize * px_bytes; 100 | rgb_px(&data[i..i + px_bytes]) 101 | })) 102 | }; 103 | 104 | Bitmap { image } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/button.rs: -------------------------------------------------------------------------------- 1 | use crate::dictionary::CharacterId; 2 | use crate::timeline::{Depth, Object}; 3 | use std::collections::BTreeMap; 4 | use swf_types as swf; 5 | 6 | #[derive(Clone, Debug, Default)] 7 | pub struct PerState { 8 | pub up: T, 9 | pub over: T, 10 | pub down: T, 11 | pub hit_test: T, 12 | } 13 | 14 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 15 | pub enum Event { 16 | // Keyboard events. 17 | KeyPress(u8), 18 | 19 | // Mouse events. 20 | HoverIn, 21 | HoverOut, 22 | Down, 23 | Up, 24 | 25 | // Push button mouse events. 26 | DragOut, 27 | DragIn, 28 | UpOut, 29 | 30 | // Menu button mouse events. 31 | DownIn, 32 | DownOut, 33 | } 34 | 35 | #[derive(Debug)] 36 | pub struct EventHandler { 37 | pub on: Vec, 38 | pub actions: crate::avm1::Code, 39 | } 40 | 41 | pub struct Button { 42 | pub objects: PerState>>, 43 | pub handlers: Vec, 44 | } 45 | 46 | impl<'a> From<&'a swf::tags::DefineButton> for Button { 47 | fn from(button: &swf::tags::DefineButton) -> Self { 48 | let mut objects = PerState::>::default(); 49 | for record in &button.characters { 50 | if !record.filters.is_empty() || record.blend_mode != swf::BlendMode::Normal { 51 | eprintln!("Button::from: unsupported features in {:?}", record); 52 | } 53 | 54 | let depth = Depth(record.depth); 55 | 56 | let object = Object { 57 | character: CharacterId(record.character_id), 58 | matrix: record.matrix, 59 | name: None, 60 | color_transform: record.color_transform.unwrap_or_default(), 61 | ratio: None, 62 | }; 63 | 64 | if record.state_up { 65 | objects.up.insert(depth, object); 66 | } 67 | if record.state_over { 68 | objects.over.insert(depth, object); 69 | } 70 | if record.state_down { 71 | objects.down.insert(depth, object); 72 | } 73 | if record.state_hit_test { 74 | objects.hit_test.insert(depth, object); 75 | } 76 | } 77 | 78 | let handlers = button 79 | .actions 80 | .iter() 81 | .map(|cond_actions| { 82 | let cond = cond_actions 83 | .conditions 84 | .expect("ButtonCondAction missing conditions"); 85 | let on = [ 86 | (Event::HoverIn, cond.idle_to_over_up), 87 | (Event::HoverOut, cond.over_up_to_idle), 88 | (Event::Down, cond.over_up_to_over_down), 89 | (Event::Up, cond.over_down_to_over_up), 90 | (Event::DragOut, cond.over_down_to_out_down), 91 | (Event::DragIn, cond.out_down_to_over_down), 92 | (Event::UpOut, cond.out_down_to_idle), 93 | (Event::DownIn, cond.idle_to_over_down), 94 | (Event::DownOut, cond.over_down_to_idle), 95 | ] 96 | .iter() 97 | .filter(|&&(_, cond)| cond) 98 | .map(|&(ev, _)| ev) 99 | .chain(cond.key_press.map(|key| Event::KeyPress(key as u8))) 100 | .collect(); 101 | 102 | let actions = crate::avm1::Code::parse_and_compile(&cond_actions.actions); 103 | 104 | EventHandler { on, actions } 105 | }) 106 | .collect(); 107 | 108 | Button { objects, handlers } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/dictionary.rs: -------------------------------------------------------------------------------- 1 | use crate::bitmap::Bitmap; 2 | use crate::button::Button; 3 | use crate::shape::Shape; 4 | use crate::sound::Sound; 5 | use crate::timeline::Timeline; 6 | use std::collections::BTreeMap; 7 | use swf_types as swf; 8 | 9 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 10 | pub struct CharacterId(pub u16); 11 | 12 | pub enum Character<'a> { 13 | Shape(Shape<'a>), 14 | Bitmap(Bitmap), 15 | Sound(Sound<'a>), 16 | 17 | Sprite(Timeline<'a>), 18 | Button(Button), 19 | DynamicText(&'a swf::tags::DefineDynamicText), 20 | } 21 | 22 | #[derive(Default)] 23 | pub struct Dictionary<'a> { 24 | pub characters: BTreeMap>, 25 | } 26 | 27 | impl<'a> Dictionary<'a> { 28 | pub fn define(&mut self, id: CharacterId, character: Character<'a>) { 29 | assert!( 30 | self.characters.insert(id, character).is_none(), 31 | "Dictionary::define: ID {} is already taken" 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/export/js/avm1.rs: -------------------------------------------------------------------------------- 1 | use crate::avm1; 2 | use crate::export::js; 3 | 4 | impl avm1::Value { 5 | fn to_js(&self) -> js::Code { 6 | match self { 7 | avm1::Value::Undefined => js::code! { "undefined" }, 8 | avm1::Value::Null => js::code! { "null" }, 9 | avm1::Value::Bool(false) => js::code! { "false" }, 10 | avm1::Value::Bool(true) => js::code! { "true" }, 11 | avm1::Value::I32(x) => js::code! { x }, 12 | avm1::Value::F32(x) => js::code! { x }, 13 | avm1::Value::F64(x) => js::code! { x }, 14 | avm1::Value::Str(s) => js::string(s), 15 | 16 | avm1::Value::OpRes(i) => js::code! { "_", i }, 17 | } 18 | } 19 | } 20 | 21 | pub fn export<'a>(codes: impl IntoIterator) -> js::Code { 22 | let mut js_body = js::code! {}; 23 | 24 | fn this_call(name: &str, args: impl IntoIterator) -> js::Code { 25 | js::call(js::code! { "local.this.", name }, args) 26 | } 27 | 28 | for code in codes { 29 | for (i, op) in code.ops.iter().enumerate() { 30 | let assign = |value| js::code! { "var _", i, " = ", value }; 31 | js_body += js::code! { "\n" }; 32 | js_body += match op { 33 | avm1::Op::Play => this_call("play", vec![]), 34 | avm1::Op::Stop => this_call("stop", vec![]), 35 | avm1::Op::GotoFrame(frame) => this_call("goto", vec![js::code! { frame.0 }]), 36 | avm1::Op::GotoLabel(name) => this_call("goto", vec![js::string(name)]), 37 | avm1::Op::GetUrl(url, target) => { 38 | this_call("getURL", vec![js::string(url), js::string(target)]) 39 | } 40 | 41 | avm1::Op::GetVar(name) => assign(js::code! { 42 | "(", js::string(name), " in local) ? ", 43 | "local[", js::string(name), "] : ", 44 | "global[", js::string(name), "]" 45 | }), 46 | avm1::Op::SetVar(name, value) => js::code! { 47 | "local[", js::string(name), "] = ", value.to_js() 48 | }, 49 | 50 | avm1::Op::Call(callee, args) => { 51 | assign(js::call(callee.to_js(), args.iter().map(|arg| arg.to_js()))) 52 | } 53 | avm1::Op::CallMethod(receiver, name, args) => assign(js::call( 54 | js::code! { receiver.to_js(), ".", name }, 55 | args.iter().map(|arg| arg.to_js()), 56 | )), 57 | }; 58 | js_body += js::code! { ";" }; 59 | } 60 | } 61 | 62 | js::code! { "function(global, local) {", js_body.indent(), "\n}" } 63 | } 64 | -------------------------------------------------------------------------------- /src/export/js/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::ops::AddAssign; 3 | 4 | pub mod avm1; 5 | pub mod sound; 6 | pub mod timeline; 7 | 8 | pub use crate::__mod_hack__js_code as code; 9 | #[macro_export] 10 | // HACK(eddyb) quick quasi-quoting hack. 11 | macro_rules! __mod_hack__js_code { 12 | ($($x:expr),*) => {{ 13 | #[allow(unused_imports)] 14 | use std::fmt::Write; 15 | 16 | let mut _code = String::new(); 17 | $(write!(_code, "{}", $x).unwrap();)* 18 | crate::export::js::Code(_code) 19 | }} 20 | } 21 | 22 | #[derive(Clone, Debug)] 23 | pub struct Code(pub String); 24 | 25 | impl fmt::Display for Code { 26 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 27 | self.0.fmt(f) 28 | } 29 | } 30 | 31 | impl AddAssign for Code { 32 | fn add_assign(&mut self, other: Self) { 33 | self.0.push_str(&other.0); 34 | } 35 | } 36 | 37 | impl Code { 38 | pub fn indent(&self) -> Self { 39 | Code(self.0.replace("\n", "\n ")) 40 | } 41 | } 42 | 43 | pub fn string(s: &str) -> Code { 44 | code! { format!("{:?}", s) } 45 | } 46 | 47 | pub fn call(callee: Code, args: impl IntoIterator) -> Code { 48 | let mut code = code! { callee, "(" }; 49 | for (i, arg) in args.into_iter().enumerate() { 50 | if i > 0 { 51 | code += code! { ", " }; 52 | } 53 | code += arg; 54 | } 55 | code += code! { ")" }; 56 | code 57 | } 58 | 59 | pub fn array(elems: impl IntoIterator) -> Code { 60 | let mut code = code! { "[" }; 61 | for elem in elems { 62 | if elem.0.is_empty() { 63 | code += code! { "," }; 64 | } else { 65 | code += code! { "\n ", elem.indent(), "," }; 66 | } 67 | } 68 | code += code! { "\n]" }; 69 | code 70 | } 71 | 72 | pub fn object(props: impl IntoIterator) -> Code { 73 | let mut code = code! { "{\n" }; 74 | for (name, value) in props { 75 | code += code! { " ", name, ": ", value.indent(), ",\n" }; 76 | } 77 | code += code! { "}" }; 78 | code 79 | } 80 | -------------------------------------------------------------------------------- /src/export/js/sound.rs: -------------------------------------------------------------------------------- 1 | use crate::export::js; 2 | 3 | // TODO(eddyb) figure out a way to avoid copying the data URL around. 4 | pub fn export_mp3(mp3: &[u8]) -> js::Code { 5 | let mut code = js::code! { "new Audio('data:audio/mpeg;base64," }; 6 | base64::encode_config_buf(mp3, base64::STANDARD, &mut code.0); 7 | code += js::code! { "')" }; 8 | code 9 | } 10 | -------------------------------------------------------------------------------- /src/export/js/timeline.rs: -------------------------------------------------------------------------------- 1 | use crate::export::js; 2 | use crate::timeline::{Depth, Frame, Timeline}; 3 | use swf_types as swf; 4 | 5 | #[rustfmt::skip] 6 | pub fn export_matrix(m: &swf::Matrix) -> js::Code { 7 | if *m == swf::Matrix::default() { 8 | return js::code! { "null" }; 9 | } 10 | js::array( 11 | [ 12 | m.scale_x, m.rotate_skew0, 13 | m.rotate_skew1, m.scale_y, 14 | ] 15 | .iter() 16 | .map(|&x| js::code! { f64::from(x) }) 17 | .chain( 18 | [m.translate_x, m.translate_y] 19 | .iter() 20 | .map(|x| js::code! { x }), 21 | ), 22 | ) 23 | } 24 | 25 | #[rustfmt::skip] 26 | pub fn export_color_transform(c: &swf::ColorTransformWithAlpha) -> js::Code { 27 | if *c == swf::ColorTransformWithAlpha::default() { 28 | return js::code! { "null" }; 29 | } 30 | 31 | js::array( 32 | [ 33 | f32::from(c.red_mult) as f64, 0.0, 0.0, 0.0, 34 | c.red_add as f64 / 255.0, 35 | 36 | 0.0, f32::from(c.green_mult) as f64, 0.0, 0.0, 37 | c.green_add as f64 / 255.0, 38 | 39 | 0.0, 0.0, f32::from(c.blue_mult) as f64, 0.0, 40 | c.blue_add as f64 / 255.0, 41 | 42 | 0.0, 0.0, 0.0, f32::from(c.alpha_mult) as f64, 43 | c.alpha_add as f64 / 255.0, 44 | ] 45 | .iter() 46 | .map(|x| js::code! { x }), 47 | ) 48 | } 49 | 50 | pub fn export(timeline: &Timeline) -> js::Code { 51 | let max_depth = timeline 52 | .layers 53 | .keys() 54 | .cloned() 55 | .rev() 56 | .next() 57 | .unwrap_or(Depth(0)); 58 | js::object(vec![ 59 | ( 60 | "layers", 61 | js::array((0..=max_depth.0).map(Depth).map( 62 | |depth| match timeline.layers.get(&depth) { 63 | Some(layer) => { 64 | let last_frame = layer 65 | .frames 66 | .keys() 67 | .cloned() 68 | .rev() 69 | .next() 70 | .unwrap_or(Frame(0)); 71 | js::array((0..=last_frame.0).map(Frame).map(|frame| { 72 | match layer.frames.get(&frame) { 73 | Some(Some(obj)) => js::object(vec![ 74 | ("character", js::code! { obj.character.0 }), 75 | ("matrix", export_matrix(&obj.matrix)), 76 | ( 77 | "name", 78 | match obj.name { 79 | Some(s) => js::string(s), 80 | None => js::code! { "null" }, 81 | }, 82 | ), 83 | ( 84 | "color_transform", 85 | export_color_transform(&obj.color_transform), 86 | ), 87 | ( 88 | "ratio", 89 | match obj.ratio { 90 | Some(x) => js::code! { x }, 91 | None => js::code! { "null" }, 92 | }, 93 | ), 94 | ]), 95 | Some(None) => js::code! { "null" }, 96 | None => js::code! {}, 97 | } 98 | })) 99 | } 100 | None => js::code! {}, 101 | }, 102 | )), 103 | ), 104 | ("actions", { 105 | let last_frame = timeline 106 | .actions 107 | .keys() 108 | .cloned() 109 | .rev() 110 | .next() 111 | .unwrap_or(Frame(0)); 112 | js::array((0..=last_frame.0).map(Frame).map( 113 | |frame| match timeline.actions.get(&frame) { 114 | Some(codes) => js::avm1::export(codes), 115 | None => js::code! {}, 116 | }, 117 | )) 118 | }), 119 | ( 120 | "labels", 121 | js::object( 122 | timeline 123 | .labels 124 | .iter() 125 | .map(|(name, frame)| (js::string(name), js::code! { frame.0 })), 126 | ), 127 | ), 128 | ("sounds", { 129 | let last_frame = timeline 130 | .sounds 131 | .keys() 132 | .cloned() 133 | .rev() 134 | .next() 135 | .unwrap_or(Frame(0)); 136 | js::array((0..=last_frame.0).map(Frame).map( 137 | |frame| match timeline.sounds.get(&frame) { 138 | Some(sounds) => js::array(sounds.iter().map(|sound| { 139 | js::object(vec![ 140 | ("character", js::code! { sound.sound_id }), 141 | ( 142 | "no_restart", 143 | js::code! { sound.sound_info.sync_no_multiple }, 144 | ), 145 | ( 146 | "loops", 147 | match sound.sound_info.loop_count { 148 | Some(c) => js::code! { c }, 149 | None => js::code! { "null" }, 150 | }, 151 | ), 152 | ]) 153 | })), 154 | None => js::code! {}, 155 | }, 156 | )) 157 | }), 158 | ( 159 | "sound_stream", 160 | match &timeline.sound_stream { 161 | Some(stream) => js::object(vec![ 162 | ("start", js::code! { stream.start.0 }), 163 | ("sound", js::sound::export_mp3(&stream.mp3)), 164 | ]), 165 | None => js::code! { "null" }, 166 | }, 167 | ), 168 | ("frame_count", js::code! { timeline.frame_count.0 }), 169 | ]) 170 | } 171 | -------------------------------------------------------------------------------- /src/export/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod js; 2 | pub mod svg; 3 | -------------------------------------------------------------------------------- /src/export/svg/animate.rs: -------------------------------------------------------------------------------- 1 | use crate::dictionary::CharacterId; 2 | use crate::timeline::{Frame, Object}; 3 | use std::f64::consts::PI; 4 | use std::fmt::Write; 5 | use svg::node::element::{Animate, AnimateTransform, Element, Filter, Group, Use}; 6 | use svg::Node; 7 | use swf_types as swf; 8 | 9 | struct Animation { 10 | frame_count: Frame, 11 | movie_duration: f64, 12 | 13 | key_times: String, 14 | values: String, 15 | current_value: T, 16 | } 17 | 18 | impl> Animation { 19 | fn new(frame_count: Frame, movie_duration: f64, initial_value: T) -> Self { 20 | Animation { 21 | frame_count, 22 | movie_duration, 23 | key_times: String::new(), 24 | values: String::new(), 25 | current_value: initial_value, 26 | } 27 | } 28 | 29 | fn add(&mut self, frame: Frame, value: T) { 30 | if self.current_value == value { 31 | return; 32 | } 33 | if frame != Frame(0) && self.key_times.is_empty() { 34 | self.add_without_checking(Frame(0), self.current_value); 35 | } 36 | self.add_without_checking(frame, value); 37 | } 38 | 39 | fn add_without_checking(&mut self, frame: Frame, value: T) { 40 | let t = frame.0 as f64 / self.frame_count.0 as f64; 41 | if !self.key_times.is_empty() { 42 | self.key_times.push(';'); 43 | self.values.push(';'); 44 | } 45 | let _ = write!(self.key_times, "{}", t); 46 | let _ = write!(self.values, "{}", Into::::into(value)); 47 | self.current_value = value; 48 | } 49 | 50 | fn animate(self, mut node: U, attr: &str) -> U { 51 | match &self.key_times[..] { 52 | "" => {} 53 | "0" => node.assign(attr, self.values), 54 | _ => node.append( 55 | Animate::new() 56 | .set("attributeName", attr) 57 | .set("keyTimes", self.key_times) 58 | .set("values", self.values) 59 | .set("calcMode", "discrete") 60 | .set("repeatCount", "indefinite") 61 | .set("dur", self.movie_duration), 62 | ), 63 | } 64 | node 65 | } 66 | 67 | fn animate_transform(self, g: Group, ty: &str) -> Group { 68 | match &self.key_times[..] { 69 | "" => g, 70 | // HACK(eddyb) perhaps there's a way to avoid having 71 | // one `` nesting per transform, but right now it's 72 | // the only way I can compe up with to compose them. 73 | // 74 | // NB: if the transforms were grouped then they could use 75 | // one "transform" attribute for everything instead. 76 | "0" => Group::new() 77 | .add(g) 78 | .set("transform", format!("{}({})", ty, self.values)), 79 | _ => Group::new().add(g).add( 80 | AnimateTransform::new() 81 | .set("attributeName", "transform") 82 | .set("type", ty) 83 | .set("keyTimes", self.key_times) 84 | .set("values", self.values) 85 | .set("calcMode", "discrete") 86 | .set("repeatCount", "indefinite") 87 | .set("dur", self.movie_duration), 88 | ), 89 | } 90 | } 91 | } 92 | 93 | #[derive(Copy, Clone)] 94 | struct Transform { 95 | scale: (f64, f64), 96 | skew_y: f64, 97 | rotate: f64, 98 | translate: (i32, i32), 99 | } 100 | 101 | impl<'a> From<&'a swf::Matrix> for Transform { 102 | fn from(matrix: &swf::Matrix) -> Self { 103 | let a = f64::from(matrix.scale_x); 104 | let b = f64::from(matrix.rotate_skew0); 105 | let c = f64::from(matrix.rotate_skew1); 106 | let d = f64::from(matrix.scale_y); 107 | 108 | let rotate = b.atan2(a); 109 | let skew_y = d.atan2(c) - PI / 2.0 - rotate; 110 | 111 | let sx = (a * a + b * b).sqrt(); 112 | let sy = (c * c + d * d).sqrt() * skew_y.cos(); 113 | 114 | Transform { 115 | scale: (sx, sy), 116 | skew_y: skew_y * 180.0 / PI, 117 | rotate: rotate * 180.0 / PI, 118 | translate: (matrix.translate_x, matrix.translate_y), 119 | } 120 | } 121 | } 122 | 123 | impl Into for Transform { 124 | fn into(self) -> svg::node::Value { 125 | let (tx, ty) = self.translate; 126 | let (sx, sy) = self.scale; 127 | format!( 128 | "translate({} {}) rotate({}) skewY({}) scale({} {})", 129 | tx, ty, self.rotate, self.skew_y, sx, sy, 130 | ) 131 | .into() 132 | } 133 | } 134 | 135 | // TODO(eddyb) consider using linear . 136 | #[derive(Copy, Clone, PartialEq)] 137 | struct ColorMatrix { 138 | mul: [f32; 4], 139 | add: [i16; 4], 140 | } 141 | 142 | impl<'a> From<&'a swf::ColorTransformWithAlpha> for ColorMatrix { 143 | fn from(color_transform: &swf::ColorTransformWithAlpha) -> Self { 144 | ColorMatrix { 145 | mul: [ 146 | f32::from(color_transform.red_mult), 147 | f32::from(color_transform.green_mult), 148 | f32::from(color_transform.blue_mult), 149 | f32::from(color_transform.alpha_mult), 150 | ], 151 | add: [ 152 | color_transform.red_add, 153 | color_transform.green_add, 154 | color_transform.blue_add, 155 | color_transform.alpha_add, 156 | ], 157 | } 158 | } 159 | } 160 | 161 | impl Into for ColorMatrix { 162 | fn into(self) -> svg::node::Value { 163 | format!( 164 | concat!( 165 | "{r} 0 0 0 {r_add} ", 166 | "0 {g} 0 0 {g_add} ", 167 | "0 0 {b} 0 {b_add} ", 168 | "0 0 0 {a} {a_add}", 169 | ), 170 | r = self.mul[0], 171 | g = self.mul[1], 172 | b = self.mul[2], 173 | a = self.mul[3], 174 | r_add = self.add[0] as f64 / 255.0, 175 | g_add = self.add[1] as f64 / 255.0, 176 | b_add = self.add[2] as f64 / 255.0, 177 | a_add = self.add[3] as f64 / 255.0, 178 | ) 179 | .into() 180 | } 181 | } 182 | 183 | #[derive(Copy, Clone, PartialEq)] 184 | struct CharacterUseHref(Option); 185 | 186 | impl Into for CharacterUseHref { 187 | fn into(self) -> svg::node::Value { 188 | match self.0 { 189 | Some(id) => format!("#c_{}", id.0).into(), 190 | None => "#".into(), 191 | } 192 | } 193 | } 194 | 195 | pub struct ObjectAnimation { 196 | id_prefix: String, 197 | 198 | character: Animation, 199 | 200 | scale: Animation<(f64, f64)>, 201 | skew_y: Animation, 202 | rotate: Animation, 203 | translate: Animation<(i32, i32)>, 204 | 205 | color_matrix: Animation, 206 | } 207 | 208 | impl ObjectAnimation { 209 | pub fn new(id_prefix: String, frame_count: Frame, movie_duration: f64) -> Self { 210 | ObjectAnimation { 211 | id_prefix, 212 | 213 | character: Animation::new(frame_count, movie_duration, CharacterUseHref(None)), 214 | 215 | scale: Animation::new(frame_count, movie_duration, (1.0, 1.0)), 216 | skew_y: Animation::new(frame_count, movie_duration, 0.0), 217 | rotate: Animation::new(frame_count, movie_duration, 0.0), 218 | translate: Animation::new(frame_count, movie_duration, (0, 0)), 219 | 220 | color_matrix: Animation::new( 221 | frame_count, 222 | movie_duration, 223 | ColorMatrix { 224 | mul: [1.0; 4], 225 | add: [0; 4], 226 | }, 227 | ), 228 | } 229 | } 230 | 231 | pub fn add(&mut self, frame: Frame, obj: Option<&Object>) { 232 | let obj = match obj { 233 | None => { 234 | self.character.add(frame, CharacterUseHref(None)); 235 | return; 236 | } 237 | Some(obj) => obj, 238 | }; 239 | self.character 240 | .add(frame, CharacterUseHref(Some(obj.character))); 241 | 242 | let transform = Transform::from(&obj.matrix); 243 | 244 | self.scale.add(frame, transform.scale); 245 | self.skew_y.add(frame, transform.skew_y); 246 | self.rotate.add(frame, transform.rotate); 247 | self.translate.add(frame, transform.translate); 248 | 249 | self.color_matrix 250 | .add(frame, ColorMatrix::from(&obj.color_transform)); 251 | } 252 | 253 | pub fn to_svg(self) -> Group { 254 | // FIXME(eddyb) try to get rid of the redundant `` here. 255 | let mut g = Group::new(); 256 | 257 | // FIXME(eddyb) `xlink:href` probably needs to go through `attributeNS`. 258 | let mut obj = self.character.animate(Use::new(), "xlink:href"); 259 | 260 | if !self.color_matrix.key_times.is_empty() { 261 | let filter_id = format!("{}filter", self.id_prefix); 262 | obj = obj.set("filter", format!("url(#{})", filter_id)); 263 | 264 | g = g.add( 265 | Filter::new() 266 | .set("id", filter_id) 267 | .set("x", 0) 268 | .set("y", 0) 269 | .set("width", 1) 270 | .set("height", 1) 271 | .add( 272 | self.color_matrix 273 | .animate(Element::new("feColorMatrix"), "values"), 274 | ), 275 | ); 276 | } 277 | 278 | g = g.add(obj); 279 | 280 | g = self.scale.animate_transform(g, "scale"); 281 | g = self.skew_y.animate_transform(g, "skewY"); 282 | g = self.rotate.animate_transform(g, "rotate"); 283 | g = self.translate.animate_transform(g, "translate"); 284 | 285 | g 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/export/svg/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::bitmap::Bitmap; 2 | use crate::button::{self, Button}; 3 | use crate::dictionary::{Character, CharacterId, Dictionary}; 4 | use crate::export::js; 5 | use crate::shape::{Line, Shape}; 6 | use crate::sound::Sound; 7 | use crate::timeline::{Frame, Timeline, TimelineBuilder}; 8 | use image::GenericImageView; 9 | use std::collections::BTreeMap; 10 | use svg::node::element::{ 11 | path, ClipPath, Definitions, Group, Image, LinearGradient, Path, Pattern, RadialGradient, 12 | Rectangle, Script, Stop, 13 | }; 14 | use swf_types as swf; 15 | 16 | mod animate; 17 | 18 | #[derive(Default)] 19 | pub struct Config { 20 | pub use_js: bool, 21 | } 22 | 23 | pub fn export(movie: &swf::Movie, config: Config) -> svg::Document { 24 | let mut dictionary = Dictionary::default(); 25 | 26 | let mut bg = [0, 0, 0]; 27 | let mut timeline_builder = TimelineBuilder::default(); 28 | for tag in &movie.tags { 29 | match tag { 30 | swf::Tag::SetBackgroundColor(set_bg) => { 31 | let c = &set_bg.color; 32 | bg = [c.r, c.g, c.b]; 33 | } 34 | swf::Tag::DefineShape(def) => { 35 | dictionary.define(CharacterId(def.id), Character::Shape(Shape::from(def))) 36 | } 37 | // FIXME(eddyb) deduplicate this. 38 | swf::Tag::DefineSprite(def) => { 39 | let mut timeline_builder = TimelineBuilder::default(); 40 | for tag in &def.tags { 41 | match tag { 42 | swf::Tag::FrameLabel(label) => timeline_builder.frame_label(label), 43 | swf::Tag::PlaceObject(place) => timeline_builder.place_object(place), 44 | swf::Tag::RemoveObject(remove) => timeline_builder.remove_object(remove), 45 | swf::Tag::DoAction(do_action) => timeline_builder.do_action(do_action), 46 | swf::Tag::StartSound(sound) => timeline_builder.start_sound(sound), 47 | swf::Tag::SoundStreamHead(head) => timeline_builder.sound_stream_head(head), 48 | swf::Tag::SoundStreamBlock(block) => { 49 | timeline_builder.sound_stream_block(block) 50 | } 51 | swf::Tag::ShowFrame => timeline_builder.advance_frame(), 52 | _ => eprintln!("unknown sprite tag: {:?}", tag), 53 | } 54 | } 55 | let timeline = timeline_builder.finish(Frame(def.frame_count as u16)); 56 | dictionary.define(CharacterId(def.id), Character::Sprite(timeline)) 57 | } 58 | swf::Tag::DefineDynamicText(def) => { 59 | dictionary.define(CharacterId(def.id), Character::DynamicText(def)) 60 | } 61 | swf::Tag::DefineSound(def) => { 62 | dictionary.define(CharacterId(def.id), Character::Sound(Sound::from(def))); 63 | } 64 | swf::Tag::DefineBitmap(def) => { 65 | dictionary.define(CharacterId(def.id), Character::Bitmap(Bitmap::from(def))); 66 | } 67 | swf::Tag::DefineButton(def) => { 68 | dictionary.define(CharacterId(def.id), Character::Button(Button::from(def))); 69 | } 70 | swf::Tag::FrameLabel(label) => timeline_builder.frame_label(label), 71 | swf::Tag::PlaceObject(place) => timeline_builder.place_object(place), 72 | swf::Tag::RemoveObject(remove) => timeline_builder.remove_object(remove), 73 | swf::Tag::DoAction(do_action) => timeline_builder.do_action(do_action), 74 | swf::Tag::StartSound(sound) => timeline_builder.start_sound(sound), 75 | swf::Tag::SoundStreamHead(head) => timeline_builder.sound_stream_head(head), 76 | swf::Tag::SoundStreamBlock(block) => timeline_builder.sound_stream_block(block), 77 | swf::Tag::ShowFrame => timeline_builder.advance_frame(), 78 | _ => eprintln!("unknown tag: {:?}", tag), 79 | } 80 | } 81 | let timeline = timeline_builder.finish(Frame(movie.header.frame_count)); 82 | 83 | let view_box = { 84 | let r = &movie.header.frame_size; 85 | (r.x_min, r.y_min, r.x_max - r.x_min, r.y_max - r.y_min) 86 | }; 87 | 88 | let mut cx = Context { 89 | config, 90 | frame_rate: f32::from(movie.header.frame_rate) as f64, 91 | 92 | svg_defs: Definitions::new(), 93 | js_defs: js::code! {}, 94 | next_gradient_id: 0, 95 | }; 96 | 97 | for (&id, character) in &dictionary.characters { 98 | cx.export_character(id, character); 99 | } 100 | 101 | let mut svg_document = svg::Document::new() 102 | .set("xmlns:xlink", "http://www.w3.org/1999/xlink") 103 | .set("viewBox", view_box) 104 | .set("style", "background: black") 105 | .add( 106 | Rectangle::new() 107 | .set("id", "bg") 108 | .set("width", "100%") 109 | .set("height", "100%") 110 | .set("fill", format!("#{:02x}{:02x}{:02x}", bg[0], bg[1], bg[2])), 111 | ); 112 | 113 | cx.add_svg_def( 114 | ClipPath::new().set("id", "viewBox_clip").add( 115 | Rectangle::new() 116 | .set("x", view_box.0) 117 | .set("y", view_box.1) 118 | .set("width", view_box.2) 119 | .set("height", view_box.3), 120 | ), 121 | ); 122 | 123 | if !cx.config.use_js { 124 | let svg_body = cx 125 | .export_timeline(None, &timeline) 126 | .set("clip-path", "url(#viewBox_clip)"); 127 | svg_document = svg_document.add(cx.svg_defs).add(svg_body); 128 | } else { 129 | svg_document = svg_document 130 | .add(cx.svg_defs) 131 | .add( 132 | Group::new() 133 | .set("id", "body") 134 | .set("clip-path", "url(#viewBox_clip)"), 135 | ) 136 | .add( 137 | js::code! { 138 | "var timeline = ", js::timeline::export(&timeline), ";\n", 139 | "var sounds = [];\n", 140 | "var sprites = [];\n", 141 | "var buttons = [];\n", 142 | cx.js_defs, 143 | "var frame_rate = ", cx.frame_rate, ";\n\n", 144 | include_str!("runtime.js") 145 | } 146 | .to_svg(), 147 | ); 148 | } 149 | 150 | svg_document 151 | } 152 | 153 | impl js::Code { 154 | fn to_svg(self) -> Script { 155 | Script::new( 156 | js::code! { 157 | "// \n" 160 | } 161 | .0, 162 | ) 163 | } 164 | } 165 | 166 | struct Context { 167 | config: Config, 168 | frame_rate: f64, 169 | 170 | svg_defs: Definitions, 171 | js_defs: js::Code, 172 | next_gradient_id: usize, 173 | } 174 | 175 | impl Context { 176 | fn add_svg_def(&mut self, node: impl svg::Node) { 177 | self.svg_defs = std::mem::replace(&mut self.svg_defs, Definitions::new()).add(node); 178 | } 179 | 180 | fn rgba_to_svg(&self, c: &swf::StraightSRgba8) -> String { 181 | if c.a == 0xff { 182 | format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b) 183 | } else { 184 | format!("rgba({}, {}, {}, {})", c.r, c.g, c.b, c.a) 185 | } 186 | } 187 | 188 | fn fill_to_svg(&mut self, style: &swf::FillStyle) -> String { 189 | match style { 190 | swf::FillStyle::Solid(solid) => self.rgba_to_svg(&solid.color), 191 | // FIXME(eddyb) don't ignore the gradient transformation matrix. 192 | // TODO(eddyb) cache identical gradients. 193 | swf::FillStyle::LinearGradient(gradient) => { 194 | let mut svg_gradient = LinearGradient::new(); 195 | for stop in &gradient.gradient.colors { 196 | svg_gradient = svg_gradient.add( 197 | Stop::new() 198 | .set( 199 | "offset", 200 | format!("{}%", (stop.ratio as f64 / 255.0) * 100.0), 201 | ) 202 | .set("stop-color", self.rgba_to_svg(&stop.color)), 203 | ); 204 | } 205 | 206 | let id = self.next_gradient_id; 207 | self.next_gradient_id += 1; 208 | 209 | self.add_svg_def(svg_gradient.set("id", format!("grad_{}", id))); 210 | 211 | format!("url(#grad_{})", id) 212 | } 213 | swf::FillStyle::RadialGradient(gradient) => { 214 | // FIXME(eddyb) remove duplication between linear and radial gradients. 215 | let mut svg_gradient = RadialGradient::new(); 216 | for stop in &gradient.gradient.colors { 217 | svg_gradient = svg_gradient.add( 218 | Stop::new() 219 | .set( 220 | "offset", 221 | format!("{}%", (stop.ratio as f64 / 255.0) * 100.0), 222 | ) 223 | .set("stop-color", self.rgba_to_svg(&stop.color)), 224 | ); 225 | } 226 | 227 | let id = self.next_gradient_id; 228 | self.next_gradient_id += 1; 229 | 230 | self.add_svg_def(svg_gradient.set("id", format!("grad_{}", id))); 231 | 232 | format!("url(#grad_{})", id) 233 | } 234 | // FIXME(eddyb) don't ignore the bitmap transformation matrix, 235 | // and the `repeating` and `smoothed` options. 236 | swf::FillStyle::Bitmap(bitmap) => format!("url(#pat_{})", bitmap.bitmap_id), 237 | _ => { 238 | eprintln!("unsupported fill: {:?}", style); 239 | // TODO(eddyb) implement focal gradient support. 240 | "#ff00ff".to_string() 241 | } 242 | } 243 | } 244 | 245 | fn export_character(&mut self, id: CharacterId, character: &Character) { 246 | let svg_id = format!("c_{}", id.0); 247 | let mut g = Group::new(); 248 | match character { 249 | Character::Shape(shape) => { 250 | // TODO(eddyb) do the transforms need to take `shape.center` into account? 251 | 252 | let path_data = |path: &[Line]| { 253 | let start = path.first()?.from; 254 | 255 | let mut data = path::Data::new().move_to(start.x_y()); 256 | let mut pos = start; 257 | 258 | for line in path { 259 | if line.from != pos { 260 | data = data.move_to(line.from.x_y()); 261 | } 262 | 263 | if let Some(control) = line.bezier_control { 264 | data = data 265 | .quadratic_curve_to((control.x, control.y, line.to.x, line.to.y)); 266 | } else { 267 | data = data.line_to(line.to.x_y()); 268 | } 269 | 270 | pos = line.to; 271 | } 272 | 273 | Some((start, data, pos)) 274 | }; 275 | 276 | for fill in &shape.fill { 277 | if let Some((start, mut data, end)) = path_data(&fill.path) { 278 | if start == end { 279 | data = data.close(); 280 | } 281 | 282 | g = g.add( 283 | Path::new() 284 | .set("fill", self.fill_to_svg(fill.style)) 285 | // TODO(eddyb) confirm/infirm the correctness of this. 286 | .set("fill-rule", "evenodd") 287 | .set("d", data), 288 | ); 289 | } 290 | } 291 | 292 | for stroke in &shape.stroke { 293 | if let Some((start, mut data, end)) = path_data(&stroke.path) { 294 | if !stroke.style.no_close && start == end { 295 | data = data.close(); 296 | } 297 | 298 | // TODO(eddyb) implement cap/join support. 299 | 300 | g = g.add( 301 | Path::new() 302 | .set("fill", "none") 303 | .set("stroke", self.fill_to_svg(&stroke.style.fill)) 304 | .set("stroke-width", stroke.style.width) 305 | .set("d", data), 306 | ); 307 | } 308 | } 309 | } 310 | 311 | Character::Bitmap(Bitmap { image }) => { 312 | let mut data_url = "data:image/png;base64,".to_string(); 313 | { 314 | let mut png = vec![]; 315 | image.write_to(&mut png, image::PNG).unwrap(); 316 | base64::encode_config_buf(&png, base64::STANDARD, &mut data_url); 317 | } 318 | g = g.add( 319 | Pattern::new() 320 | .set("id", format!("pat_{}", id.0)) 321 | .set("width", 1) 322 | .set("height", 1) 323 | .add( 324 | Image::new() 325 | .set("xlink:href", data_url) 326 | .set("width", image.width() * 20) 327 | .set("height", image.height() * 20), 328 | ), 329 | ); 330 | } 331 | 332 | Character::Sound(sound) => { 333 | if self.config.use_js { 334 | if let Some(mp3) = sound.mp3 { 335 | self.js_defs += js::code! { 336 | "sounds[", id.0, "] = ", js::sound::export_mp3(mp3.data), ";\n" 337 | }; 338 | return; 339 | } 340 | } 341 | 342 | // TODO(eddyb) try to make this work for animated SVGs as well. 343 | } 344 | 345 | // TODO(eddyb) figure out if there's anything to be done here 346 | // wrt synchronizing the animiation timelines of sprites. 347 | Character::Sprite(timeline) => { 348 | if self.config.use_js { 349 | self.js_defs += js::code! { 350 | "sprites[", id.0, "] = ", js::timeline::export(timeline), ";\n" 351 | }; 352 | return; 353 | } 354 | g = self.export_timeline(Some(id), timeline); 355 | } 356 | 357 | Character::Button(button) => { 358 | let states = [ 359 | ("", &button.objects.up), 360 | ("_over", &button.objects.over), 361 | ("_down", &button.objects.down), 362 | ("_hit_test", &button.objects.hit_test), 363 | ]; 364 | for &(suffix, objects) in &states { 365 | let mut g = Group::new(); 366 | let svg_id = format!("{}{}", svg_id, suffix); 367 | for (&depth, obj) in objects { 368 | let id_prefix = format!("{}_d_{}_", svg_id, depth.0); 369 | let mut animation = animate::ObjectAnimation::new(id_prefix, Frame(1), 1.0); 370 | animation.add(Frame(0), Some(obj)); 371 | g = g.add(animation.to_svg()); 372 | } 373 | self.add_svg_def(g.set("id", svg_id)); 374 | } 375 | 376 | if self.config.use_js { 377 | let js_button = js::code! { "buttons[", id.0, "]" }; 378 | self.js_defs += js::code! { 379 | js_button, " = ", js::object(vec![ 380 | ("mouse", js::code! { "{}" }), 381 | ("keyPress", js::array(vec![])), 382 | ]), ";\n" 383 | }; 384 | 385 | // Try to reuse functions as much as possible, 386 | // while having only one function per `Event`. 387 | // Note that this will still result in code 388 | // being duplicated, but hopefully not too much. 389 | let mut event_to_handlerset = BTreeMap::new(); 390 | for (i, handler) in button.handlers.iter().enumerate() { 391 | for &event in &handler.on { 392 | event_to_handlerset.entry(event).or_insert(vec![]).push(i); 393 | } 394 | } 395 | let mut handlerset_to_events = BTreeMap::new(); 396 | for (event, handlers) in event_to_handlerset { 397 | handlerset_to_events 398 | .entry(handlers) 399 | .or_insert(vec![]) 400 | .push(event); 401 | } 402 | 403 | for (handlers, events) in handlerset_to_events { 404 | // Generate `buttons[x].mouse.foo = buttons[x].mouse.bar = ...;`. 405 | for event in events { 406 | self.js_defs += js::code! { js_button, "." }; 407 | let mouse_event = match event { 408 | button::Event::KeyPress(c) => { 409 | self.js_defs += js::code! { "keyPress[", c, "] = " }; 410 | continue; 411 | } 412 | 413 | button::Event::HoverIn => "hoverIn", 414 | button::Event::HoverOut => "hoverOut", 415 | button::Event::Down => "down", 416 | button::Event::Up => "up", 417 | button::Event::DragOut => "dragOut", 418 | button::Event::DragIn => "dragIn", 419 | button::Event::UpOut => "upOut", 420 | button::Event::DownIn => "downIn", 421 | button::Event::DownOut => "downOut", 422 | }; 423 | self.js_defs += js::code! { "mouse.", mouse_event, " = " }; 424 | } 425 | 426 | self.js_defs += 427 | js::avm1::export(handlers.iter().map(|&i| &button.handlers[i].actions)); 428 | 429 | self.js_defs += js::code! { ";\n" }; 430 | } 431 | } 432 | 433 | return; 434 | } 435 | 436 | Character::DynamicText(def) => { 437 | let mut text = svg::node::element::Text::new().add(svg::node::Text::new( 438 | // HACK(eddyb) this only handles escaping `<`, should either 439 | // fix the `svg` crate, or switch to one which does escape, 440 | // like `svgdom`. 441 | def.text 442 | .as_ref() 443 | .map_or("", |s| &s[..]) 444 | .replace("<", "<"), 445 | )); 446 | 447 | if let Some(size) = def.font_size { 448 | text = text.set("font-size", size); 449 | } 450 | 451 | if let Some(color) = &def.color { 452 | text = text.set("fill", self.rgba_to_svg(color)); 453 | } 454 | 455 | g = g.add(text); 456 | } 457 | } 458 | 459 | self.add_svg_def(g.set("id", svg_id)); 460 | } 461 | 462 | fn export_timeline(&self, id: Option, timeline: &Timeline) -> Group { 463 | let frame_duration = 1.0 / self.frame_rate; 464 | let movie_duration = timeline.frame_count.0 as f64 * frame_duration; 465 | 466 | let mut g = Group::new(); 467 | if self.config.use_js { 468 | return g; 469 | } 470 | let id_prefix = id.map_or(String::new(), |id| format!("c_{}_", id.0)); 471 | for (&depth, layer) in &timeline.layers { 472 | let id_prefix = format!("{}d_{}_", id_prefix, depth.0); 473 | let mut animation = 474 | animate::ObjectAnimation::new(id_prefix, timeline.frame_count, movie_duration); 475 | for (&frame, obj) in &layer.frames { 476 | animation.add(frame, obj.as_ref()); 477 | } 478 | g = g.add(animation.to_svg()); 479 | } 480 | g 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /src/export/svg/runtime.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function int(x) { 3 | return x | 0; 4 | } 5 | 6 | function svg_element(tag) { 7 | return document.createElementNS('http://www.w3.org/2000/svg', tag); 8 | } 9 | 10 | var rt = {}; 11 | rt.mkGlobalScope = function() { 12 | var o = Object.create(null); 13 | function def(name, x) { 14 | Object.defineProperty(o, name, { value: x }); 15 | } 16 | def('hasOwnProperty', function(o, x) { 17 | return Object.prototype.hasOwnProperty.call(o, x); 18 | }); 19 | // HACK(eddyb) trap writes. 20 | if(Object.freeze) 21 | return Object.freeze(o); 22 | return o; 23 | }; 24 | rt.mkLocalScope = function(_this) { 25 | var o = Object.create(_this); 26 | function def(name, x) { 27 | Object.defineProperty(o, name, { value: x }); 28 | } 29 | def('this', _this); 30 | // HACK(eddyb) trap writes. 31 | if(Object.freeze) 32 | return Object.freeze(o); 33 | return o; 34 | }; 35 | rt.mkMovieClip = function(timeline) { 36 | var o = Object.create(null); 37 | function def_get(name, f) { 38 | Object.defineProperty(o, name, { get: f }); 39 | } 40 | function def(name, x) { 41 | Object.defineProperty(o, name, { value: x }); 42 | } 43 | def('play', function() { 44 | timeline.paused = false; 45 | }); 46 | def('stop', function() { 47 | timeline.paused = true; 48 | }); 49 | // HACK(eddyb) support for Goto{Frame,Label}. 50 | def('goto', function(frame) { 51 | if(typeof frame === 'string') 52 | frame = timeline.labels[frame]; 53 | timeline.frame = frame; 54 | }); 55 | def('gotoAndPlay', function(frame) { 56 | this.goto(frame); 57 | timeline.paused = false; 58 | }); 59 | // HACK(eddyb) these are usually only used as the 60 | // `getBytesLoaded() / getBytesTotal()` ratio. 61 | def('getBytesLoaded', function() { 62 | return 1; 63 | }); 64 | def('getBytesTotal', function() { 65 | return 1; 66 | }); 67 | def('getURL', function(url, target) { 68 | window.open(url, target); 69 | }); 70 | def_get('_root', rt.mkMovieClip.bind(null, timeline.root)); 71 | if(timeline.parent) 72 | def_get('_parent', rt.mkMovieClip.bind(null, timeline.parent)); 73 | for(var name in timeline.named) { 74 | var layer = timeline.layers[timeline.named[name]]; 75 | if(layer && layer.sprite) 76 | def_get(name, rt.mkMovieClip.bind(null, layer.sprite)); 77 | } 78 | // HACK(eddyb) trap writes. 79 | if(Object.freeze) 80 | return Object.freeze(o); 81 | return api; 82 | }; 83 | 84 | function Timeline(data, container, id_prefix) { 85 | if(!(this instanceof Timeline)) 86 | return new Timeline(data); 87 | 88 | id_prefix = id_prefix || ''; 89 | this.frame_count = data.frame_count; 90 | this.named = Object.create(null); 91 | this.actions = data.actions; 92 | this.labels = data.labels; 93 | this.sounds = data.sounds; 94 | this.sound_stream = data.sound_stream; 95 | this.activeSounds = []; 96 | this.layers = data.layers.map(function(frames, depth) { 97 | var container = svg_element('g'); 98 | var use = svg_element('use'); 99 | container.appendChild(use); 100 | 101 | var filter = svg_element('filter'); 102 | filter.setAttribute('id', id_prefix + 'd_' + depth + '_filter'); 103 | filter.setAttribute('x', 0); 104 | filter.setAttribute('y', 0); 105 | filter.setAttribute('width', 1); 106 | filter.setAttribute('height', 1); 107 | var feColorMatrix = svg_element('feColorMatrix'); 108 | filter.appendChild(feColorMatrix); 109 | container.appendChild(filter); 110 | 111 | return { 112 | frames: frames, 113 | container: container, 114 | use: use, 115 | filter: filter, 116 | feColorMatrix: feColorMatrix, 117 | 118 | ratio: null, 119 | 120 | isHover: function() { 121 | // FIXME(eddyb) figure out how much this needs polyfill. 122 | return this.container.matches(':hover'); 123 | }, 124 | 125 | updateUseHref: function() { 126 | if(this.character > 0) { 127 | var href = '#c_' + this.character; 128 | if(this.button && this.button.state != 'up') 129 | href += '_' + this.button.state; 130 | if(href != this.useHref) 131 | this.use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', href); 132 | this.useHref = href; 133 | } else { 134 | this.use.removeAttributeNS('http://www.w3.org/1999/xlink', 'href'); 135 | this.useHref = null; 136 | } 137 | } 138 | }; 139 | }); 140 | this.root = this; 141 | this.container = container; 142 | this.id_prefix = id_prefix; 143 | this.attachLayers(); 144 | } 145 | Timeline.prototype.paused = false; 146 | Timeline.prototype.frame = 0; 147 | Timeline.prototype.renderedFrame = -1; 148 | Timeline.prototype.attachLayers = function() { 149 | var container = this.container; 150 | this.layers.forEach(function(layer) { 151 | container.appendChild(layer.container); 152 | }); 153 | }; 154 | Timeline.prototype.detachLayers = function() { 155 | this.layers.forEach(function(layer) { 156 | layer.container.remove(); 157 | }); 158 | }; 159 | Timeline.prototype.showFrame = function() { 160 | if(this.paused && this.renderedFrame == this.frame) { 161 | // Update sprites and buttons even when paused. 162 | this.layers.forEach(function(layer) { 163 | if(layer.sprite) 164 | layer.sprite.showFrame(); 165 | if(layer.button) 166 | layer.button.showFrame(); 167 | }); 168 | return; 169 | } 170 | 171 | var mkMovieClip = rt.mkMovieClip.bind(null, this); 172 | var frame = this.frame; 173 | var renderedFrame = this.renderedFrame; 174 | var named = this.named; 175 | var id_prefix = this.id_prefix; 176 | 177 | if(renderedFrame > frame) 178 | renderedFrame = -1; 179 | 180 | this.layers.forEach(function(layer, depth) { 181 | var obj, i; 182 | for(i = frame; i > renderedFrame && !obj && obj !== null; i--) 183 | obj = layer.frames[i]; 184 | 185 | // Fully remove anything not present yet. 186 | var removeOld = renderedFrame == -1 || obj === null; 187 | 188 | // TODO(eddyb) this might need to take SWF's `is_move` into account. 189 | // HACK(eddyb) there's the issue of what `ratio` does, see also 190 | // http://wahlers.com.br/claus/blog/hacking-swf-2-placeobject-and-ratio/. 191 | 192 | // Remove the old character if necessary. 193 | if(removeOld || (obj && (layer.character != obj.character || layer.ratio !== obj.ratio))) { 194 | layer.character = -1; 195 | layer.ratio = null; 196 | if(layer.sprite) { 197 | layer.sprite.detachLayers(); 198 | layer.sprite.parent = null; 199 | layer.sprite.root = null; 200 | layer.sprite = null; 201 | } 202 | if(layer.button) { 203 | layer.button = null; 204 | } 205 | } 206 | 207 | // Remove the old name if necessary. 208 | if(layer.name && (removeOld || (obj && layer.name != obj.name))) { 209 | named[layer.name] = null; 210 | layer.name = null; 211 | } 212 | 213 | if(obj) { 214 | if(layer.character != obj.character || layer.ratio !== obj.ratio) { 215 | layer.character = obj.character; 216 | layer.ratio = obj.ratio; 217 | 218 | var sprite_data = sprites[obj.character]; 219 | if(sprite_data) { 220 | layer.sprite = new Timeline( 221 | sprite_data, 222 | layer.container, 223 | id_prefix + 'd_' + depth + '_', 224 | ); 225 | layer.sprite.parent = this; 226 | layer.sprite.root = this.root; 227 | } 228 | var button_data = buttons[obj.character]; 229 | if(button_data) { 230 | var button = layer.button = { 231 | state: 'up', 232 | attachListeners: function() { 233 | layer.use.addEventListener('mouseover', this.mouse_over_out_up); 234 | layer.use.addEventListener('mouseout', this.mouse_over_out_up); 235 | layer.use.addEventListener('mouseup', this.mouse_over_out_up); 236 | layer.use.addEventListener('mousedown', this.mouse_down); 237 | }, 238 | detachListeners: function() { 239 | layer.use.removeEventListener('mouseover', this.mouse_over_out_up); 240 | layer.use.removeEventListener('mouseout', this.mouse_over_out_up); 241 | layer.use.removeEventListener('mouseup', this.mouse_over_out_up); 242 | layer.use.removeEventListener('mousedown', this.mouse_down); 243 | }, 244 | mouse_over_out_up: function(ev) { 245 | button.transition(layer.isHover() ? 'over' : 'up'); 246 | }, 247 | mouse_down: function() { 248 | button.transition('down'); 249 | }, 250 | showFrame: function() { 251 | if(layer.button !== this) 252 | return; 253 | if(!layer.isHover()) 254 | this.transition('up'); 255 | }, 256 | transition: function(to) { 257 | if(layer.button !== this) 258 | return; 259 | if(this.state == to) 260 | return; 261 | var event; 262 | if(this.state == 'up' && to == 'over') { 263 | event = 'hoverIn'; 264 | } else if(this.state == 'over' && to == 'up') { 265 | event = 'hoverOut'; 266 | } else if(this.state == 'over' && to == 'down') { 267 | event = 'down'; 268 | } else if(this.state == 'down' && to == 'over') { 269 | event = 'up'; 270 | } 271 | this.state = to; 272 | var handler = event && button_data.mouse[event]; 273 | if(handler) 274 | handler(rt.mkGlobalScope(), rt.mkLocalScope(mkMovieClip())); 275 | }, 276 | }; 277 | button.attachListeners(); 278 | } 279 | } 280 | if(obj.matrix) { 281 | layer.container.setAttribute('transform', 'matrix(' + obj.matrix.join(' ') + ')'); 282 | } else { 283 | layer.container.removeAttribute('transform'); 284 | } 285 | if(obj.color_transform) { 286 | layer.feColorMatrix.setAttribute('values', obj.color_transform.join(' ')); 287 | layer.container.setAttribute('filter', 'url(#' + layer.filter.id + ')'); 288 | } else { 289 | layer.container.removeAttribute('filter'); 290 | } 291 | if(layer.name != obj.name) { 292 | layer.name = obj.name; 293 | if(layer.name) 294 | named[layer.name] = depth; 295 | } 296 | } 297 | 298 | // Update the sprite or button if it exists. 299 | if(layer.sprite) 300 | layer.sprite.showFrame(); 301 | if(layer.button) 302 | layer.button.showFrame(); 303 | 304 | // Update the element. 305 | layer.updateUseHref(); 306 | }); 307 | 308 | if(renderedFrame == -1) { 309 | var activeSounds = this.activeSounds; 310 | 311 | // Remove sounds that start right away from the 312 | // active set, to avoid them getting stopped. 313 | var play_sounds_at_start = this.sounds[0]; 314 | if(play_sounds_at_start) 315 | play_sounds_at_start.forEach(function(sound) { 316 | activeSounds[sound.character] = null; 317 | }); 318 | if(this.sound_stream && this.sound_stream.start == 0) 319 | activeSounds[0] = null; 320 | 321 | activeSounds.forEach(function(sound) { 322 | if(sound) { 323 | sound.userTimeline = null; 324 | sound.pause(); 325 | } 326 | }); 327 | this.activeSounds = []; 328 | } 329 | 330 | for(var i = renderedFrame + 1; i <= frame; i++) { 331 | var timeline = this; 332 | 333 | var play_sounds = this.sounds[i]; 334 | if(play_sounds) 335 | play_sounds.forEach(function(sound) { 336 | sounds[sound.character].userTimeline = timeline; 337 | }); 338 | if(this.sound_stream && this.sound_stream.start == i) 339 | this.sound_stream.sound.userTimeline = timeline; 340 | } 341 | 342 | for(var i = renderedFrame + 1; i <= frame; i++) { 343 | var timeline = this; 344 | 345 | function playSound(sound_data, sound) { 346 | if(sound.userTimeline && sound.userTimeline != timeline) 347 | return console.error('sound already in use by', sound.userTimeline); 348 | sound.loop = sound_data.loops !== null; 349 | if(!(sound_data.no_restart && sound.userTimeline == timeline)) { 350 | // FIXME(eddyb) couple this with `requestAnimationFrame` 351 | // (currently calling `showFrame` in a loop doesn't do the right thing). 352 | sound.currentTime = (frame - i) / frame_rate; 353 | } 354 | var promise = sound.play(); 355 | if(promise && promise.catch) 356 | promise.catch(function(e) { 357 | console.error('failed to play sound: ' + e.toString()); 358 | }); 359 | sound.userTimeline = timeline; 360 | timeline.activeSounds[sound_data.character] = sound; 361 | } 362 | 363 | var play_sounds = this.sounds[i]; 364 | if(play_sounds) 365 | play_sounds.forEach(function(sound) { 366 | playSound(sound, sounds[sound.character]); 367 | }); 368 | if(this.sound_stream && this.sound_stream.start == i) 369 | playSound({ character: 0 }, this.sound_stream.sound); 370 | } 371 | 372 | this.renderedFrame = frame; 373 | 374 | var action = this.actions[frame]; 375 | if(action) 376 | action(rt.mkGlobalScope(), rt.mkLocalScope(mkMovieClip())); 377 | 378 | // HACK(eddyb) no idea what the interaction here should be. 379 | if(!this.paused) 380 | this.frame = (frame + 1) % this.frame_count; 381 | }; 382 | 383 | timeline = new Timeline(timeline, document.getElementById('body')); 384 | 385 | var start; 386 | var last_frame = 0; 387 | function update(now) { 388 | window.requestAnimationFrame(update); 389 | 390 | if(!start) start = now; 391 | // TODO(eddyb) figure out how to avoid absolute values. 392 | var frame = int((now - start) * frame_rate / 1000); 393 | 394 | for(; last_frame < frame; last_frame++) 395 | timeline.showFrame(); 396 | } 397 | 398 | // HACK(eddyb) work around unreasonable autoplay policies in Chrome. 399 | // See https://goo.gl/xX8pDD for their one-sided description of it. 400 | var anySounds = false; 401 | function forEachSound(f) { 402 | timeline.sound_stream && f(timeline.sound_stream.sound); 403 | sprites.forEach(function(sprite) { 404 | sprite.sound_stream && f(sprite.sound_stream.sound); 405 | }); 406 | sounds.forEach(f); 407 | } 408 | forEachSound(function() { anySounds = true; }) 409 | if(anySounds) { 410 | var viewBox = timeline.container.parentNode.getAttribute('viewBox') 411 | .split(' ') 412 | .map(function(x) { return +x; }); 413 | 414 | var bgRect = document.getElementById('bg'); 415 | var bgOriginalFill = bgRect.getAttribute('fill'); 416 | bgRect.setAttribute('fill', 'white'); 417 | 418 | var playButton = svg_element('path'); 419 | var x = viewBox[0] + viewBox[2] / 2; 420 | var y = viewBox[1] + viewBox[3] / 2; 421 | var size = Math.min(viewBox[2], viewBox[3]) / 2; 422 | var x0 = x - size / 3; 423 | var x1 = x + size / 3 * 2; 424 | var y0 = y - size / 2; 425 | var y1 = y + size / 2; 426 | playButton.setAttribute('d', 'M' + x1 + ',' + y + ' L' + x0 + ',' + y0 + ' L' + x0 + ',' + y1 + ' Z'); 427 | playButton.setAttribute('fill', 'black'); 428 | playButton.style.cursor = 'pointer'; 429 | var clicked = false; 430 | playButton.addEventListener('click', function() { 431 | if(clicked) 432 | return; 433 | clicked = true; 434 | 435 | // HACK(eddyb) Safari is even worse, requires loading media 436 | // as a result of a user interaction to enable "autoplay" later. 437 | forEachSound(function(sound) { sound.load(); }); 438 | 439 | playButton.remove(); 440 | bgRect.setAttribute('fill', bgOriginalFill); 441 | window.requestAnimationFrame(update); 442 | }); 443 | timeline.container.appendChild(playButton); 444 | } else 445 | window.requestAnimationFrame(update); 446 | })() 447 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | pub mod avm1; 4 | pub mod bitmap; 5 | pub mod button; 6 | pub mod dictionary; 7 | pub mod export; 8 | pub mod shape; 9 | pub mod sound; 10 | pub mod timeline; 11 | -------------------------------------------------------------------------------- /src/shape.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ops::{Add, Sub}; 3 | use swf_types as swf; 4 | 5 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] 6 | pub struct Point { 7 | pub x: i32, 8 | pub y: i32, 9 | } 10 | 11 | impl From for Point { 12 | fn from(v: swf::Vector2D) -> Self { 13 | Point { x: v.x, y: v.y } 14 | } 15 | } 16 | 17 | impl Add for Point { 18 | type Output = Self; 19 | fn add(self, other: Self) -> Self { 20 | Point { 21 | x: self.x + other.x, 22 | y: self.y + other.y, 23 | } 24 | } 25 | } 26 | 27 | impl Sub for Point { 28 | type Output = Self; 29 | fn sub(self, other: Self) -> Self { 30 | Point { 31 | x: self.x - other.x, 32 | y: self.y - other.y, 33 | } 34 | } 35 | } 36 | 37 | impl Point { 38 | pub fn x_y(self) -> (i32, i32) { 39 | (self.x, self.y) 40 | } 41 | } 42 | 43 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 44 | pub struct Line { 45 | pub from: Point, 46 | pub bezier_control: Option, 47 | pub to: Point, 48 | } 49 | 50 | impl Line { 51 | pub fn flip_direction(self) -> Self { 52 | Line { 53 | from: self.to, 54 | bezier_control: self.bezier_control, 55 | to: self.from, 56 | } 57 | } 58 | 59 | pub fn map_points(self, mut f: impl FnMut(Point) -> Point) -> Self { 60 | Line { 61 | from: f(self.from), 62 | bezier_control: self.bezier_control.map(&mut f), 63 | to: f(self.to), 64 | } 65 | } 66 | } 67 | 68 | #[derive(Clone, Debug)] 69 | pub struct StyledPath { 70 | pub style: S, 71 | pub path: Vec, 72 | } 73 | 74 | impl StyledPath { 75 | pub fn new(style: S) -> Self { 76 | StyledPath { 77 | style, 78 | path: vec![], 79 | } 80 | } 81 | 82 | // TODO(eddyb) confirm/infirm the correctness of this. 83 | // Suspected test case would involve self-overlapping paths. 84 | // 85 | // http://wahlers.com.br/claus/blog/hacking-swf-1-shapes-in-flash/ 86 | // has some examples, but no SWF's to download. 87 | fn untangle_path(&mut self) { 88 | if self.path.is_empty() { 89 | return; 90 | } 91 | 92 | // FIXME(eddyb) optimize this with a bitset. 93 | let mut used: Vec = vec![false; self.path.len()]; 94 | // TODO(eddyb) consider using a bitset instead of `Vec`. 95 | let mut lines_from: HashMap> = HashMap::new(); 96 | for (i, &line) in self.path.iter().enumerate() { 97 | lines_from.entry(line.from).or_default().push(i); 98 | } 99 | 100 | let mut new_path = Vec::with_capacity(self.path.len()); 101 | 102 | let mut i = 0; 103 | loop { 104 | assert!(!used[i]); 105 | 106 | used[i] = true; 107 | let line = self.path[i]; 108 | new_path.push(line); 109 | 110 | if new_path.len() == self.path.len() { 111 | break; 112 | } 113 | 114 | // Prefer continuing lines in the original order. 115 | let preferred_start = i; 116 | 117 | // Pick one of the remaining continuation lines from the map. 118 | let mut line_indices = lines_from 119 | .get(&line.to) 120 | .map_or(&[][..], |v| &v[..]) 121 | .iter() 122 | .cloned() 123 | .filter(|&j| !used[j]); 124 | 125 | i = line_indices.next().unwrap_or_else(|| { 126 | // No remaining lines, start another path. 127 | used.iter().position(|&x| x == false).unwrap() 128 | }); 129 | 130 | // FIXME(eddyb) speed this up with binary search and/or bitsets. 131 | if i < preferred_start { 132 | if let Some(j) = line_indices.find(|&j| j > preferred_start) { 133 | i = j; 134 | } 135 | } 136 | } 137 | 138 | self.path = new_path; 139 | } 140 | } 141 | 142 | #[derive(Clone, Debug)] 143 | pub struct Shape<'a> { 144 | pub center: Point, 145 | pub fill: Vec>, 146 | pub stroke: Vec>, 147 | } 148 | 149 | impl<'a> From<&'a swf::tags::DefineShape> for Shape<'a> { 150 | fn from(def: &'a swf::tags::DefineShape) -> Self { 151 | #[derive(Copy, Clone, Default)] 152 | struct Style { 153 | start: usize, 154 | current: Option, 155 | } 156 | 157 | impl Style { 158 | fn set_from_swf(&mut self, i: usize) { 159 | self.current = i.checked_sub(1).map(|i| i + self.start); 160 | } 161 | } 162 | 163 | #[derive(Copy, Clone, Default)] 164 | struct Styles { 165 | fill0: Style, 166 | fill1: Style, 167 | stroke: Style, 168 | } 169 | 170 | impl<'a> Shape<'a> { 171 | fn add_path(&mut self, path: &[Line], styles: Styles) { 172 | if let Some(fill0) = styles.fill0.current { 173 | self.fill[fill0] 174 | .path 175 | .extend(path.iter().rev().map(|line| line.flip_direction())); 176 | } 177 | if let Some(fill1) = styles.fill1.current { 178 | self.fill[fill1].path.extend(path); 179 | } 180 | if let Some(stroke) = styles.stroke.current { 181 | self.stroke[stroke].path.extend(path); 182 | } 183 | } 184 | } 185 | 186 | let mut shape = Shape { 187 | center: Point { 188 | x: (def.bounds.x_min as i32 + def.bounds.x_max as i32) / 2, 189 | y: (def.bounds.y_min as i32 + def.bounds.y_max as i32) / 2, 190 | }, 191 | fill: def 192 | .shape 193 | .initial_styles 194 | .fill 195 | .iter() 196 | .map(StyledPath::new) 197 | .collect(), 198 | stroke: def 199 | .shape 200 | .initial_styles 201 | .line 202 | .iter() 203 | .map(StyledPath::new) 204 | .collect(), 205 | }; 206 | 207 | let mut pos = Point::default(); 208 | let mut styles = Styles::default(); 209 | 210 | let mut path = vec![]; 211 | for record in &def.shape.records { 212 | match record { 213 | swf::ShapeRecord::StyleChange(change) => { 214 | match change { 215 | // Moving without changing styles stays within a path. 216 | swf::shape_records::StyleChange { 217 | left_fill: None, 218 | right_fill: None, 219 | line_style: None, 220 | .. 221 | } => {} 222 | 223 | // If we do have a style change, switch paths. 224 | _ => { 225 | shape.add_path(&path, styles); 226 | path.clear(); 227 | } 228 | } 229 | 230 | // Process new style definitions first, so that 231 | // style updates can refer to the new styles. 232 | if let Some(new_styles) = &change.new_styles { 233 | styles.fill0.start = shape.fill.len(); 234 | styles.fill1.start = shape.fill.len(); 235 | shape 236 | .fill 237 | .extend(new_styles.fill.iter().map(StyledPath::new)); 238 | styles.stroke.start = shape.stroke.len(); 239 | shape 240 | .stroke 241 | .extend(new_styles.line.iter().map(StyledPath::new)); 242 | } 243 | 244 | if let Some(move_to) = change.move_to.map(Point::from) { 245 | pos = move_to; 246 | } 247 | if let Some(left_fill) = change.left_fill { 248 | styles.fill0.set_from_swf(left_fill); 249 | } 250 | if let Some(right_fill) = change.right_fill { 251 | styles.fill1.set_from_swf(right_fill); 252 | } 253 | if let Some(line_style) = change.line_style { 254 | styles.stroke.set_from_swf(line_style); 255 | } 256 | } 257 | swf::ShapeRecord::Edge(edge) => { 258 | let line = Line { 259 | from: Point::default(), 260 | bezier_control: edge.control_delta.map(Point::from), 261 | to: Point::from(edge.delta), 262 | }; 263 | let line = line.map_points(|p| pos + p); 264 | path.push(line); 265 | pos = line.to; 266 | } 267 | }; 268 | } 269 | 270 | shape.add_path(&path, styles); 271 | 272 | for fill in &mut shape.fill { 273 | fill.untangle_path(); 274 | } 275 | for stroke in &mut shape.stroke { 276 | stroke.untangle_path(); 277 | } 278 | 279 | shape 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/sound.rs: -------------------------------------------------------------------------------- 1 | use swf_types as swf; 2 | 3 | #[derive(Copy, Clone)] 4 | pub struct Mp3<'a> { 5 | pub seek_samples: u16, 6 | pub data: &'a [u8], 7 | } 8 | 9 | impl<'a> Mp3<'a> { 10 | fn parse(mut data: &'a [u8]) -> Self { 11 | // FIXME(eddyb) process all the mp3 frames correctly. 12 | let seek_samples = u16::from_le_bytes([data[0], data[1]]); 13 | data = &data[2..]; 14 | Mp3 { seek_samples, data } 15 | } 16 | } 17 | 18 | pub struct Mp3StreamBlock<'a> { 19 | pub samples: u16, 20 | pub mp3: Mp3<'a>, 21 | } 22 | 23 | impl<'a> From<&'a swf::tags::SoundStreamBlock> for Mp3StreamBlock<'a> { 24 | fn from(block: &'a swf::tags::SoundStreamBlock) -> Self { 25 | let mut data = &block.data[..]; 26 | let samples = u16::from_le_bytes([data[0], data[1]]); 27 | data = &data[2..]; 28 | Mp3StreamBlock { 29 | samples, 30 | mp3: Mp3::parse(data), 31 | } 32 | } 33 | } 34 | 35 | pub struct Sound<'a> { 36 | pub sample_rate: swf::SoundRate, 37 | pub stereo: bool, 38 | pub samples: u32, 39 | 40 | pub mp3: Option>, 41 | } 42 | 43 | impl<'a> From<&'a swf::tags::DefineSound> for Sound<'a> { 44 | fn from(sound: &'a swf::tags::DefineSound) -> Self { 45 | let mp3 = match sound.format { 46 | swf::AudioCodingFormat::Mp3 => Some(Mp3::parse(&sound.data)), 47 | _ => { 48 | eprintln!("Sound::from: unsupported format: {:?}", sound.format); 49 | None 50 | } 51 | }; 52 | 53 | Sound { 54 | sample_rate: sound.sound_rate, 55 | stereo: sound.sound_type == swf::SoundType::Stereo, 56 | samples: sound.sample_count, 57 | 58 | mp3, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/timeline.rs: -------------------------------------------------------------------------------- 1 | use crate::avm1; 2 | use crate::dictionary::CharacterId; 3 | use crate::sound; 4 | use std::collections::BTreeMap; 5 | use std::ops::Add; 6 | use std::str; 7 | use swf_types as swf; 8 | 9 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 10 | pub struct Depth(pub u16); 11 | 12 | #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 13 | pub struct Frame(pub u16); 14 | 15 | impl Add for Frame { 16 | type Output = Self; 17 | fn add(self, other: Self) -> Self { 18 | Frame(self.0 + other.0) 19 | } 20 | } 21 | 22 | #[derive(Copy, Clone, Debug)] 23 | pub struct Object<'a> { 24 | pub character: CharacterId, 25 | pub matrix: swf::Matrix, 26 | pub name: Option<&'a str>, 27 | pub color_transform: swf::ColorTransformWithAlpha, 28 | pub ratio: Option, 29 | } 30 | 31 | impl<'a> Object<'a> { 32 | pub fn new(character: CharacterId) -> Self { 33 | Object { 34 | character, 35 | matrix: swf::Matrix::default(), 36 | name: None, 37 | color_transform: swf::ColorTransformWithAlpha::default(), 38 | ratio: None, 39 | } 40 | } 41 | } 42 | 43 | #[derive(Default, Debug)] 44 | pub struct Layer<'a> { 45 | pub frames: BTreeMap>>, 46 | } 47 | 48 | #[derive(Debug)] 49 | pub struct SoundStream { 50 | pub start: Frame, 51 | pub format: swf::AudioCodingFormat, 52 | // FIXME(eddyb) support multiple formats. 53 | pub mp3: Vec, 54 | } 55 | 56 | #[derive(Default, Debug)] 57 | pub struct Timeline<'a> { 58 | pub layers: BTreeMap>, 59 | pub actions: BTreeMap>, 60 | pub labels: BTreeMap<&'a str, Frame>, 61 | pub sounds: BTreeMap>, 62 | pub sound_stream: Option, 63 | pub frame_count: Frame, 64 | } 65 | 66 | #[derive(Default)] 67 | pub struct TimelineBuilder<'a> { 68 | timeline: Timeline<'a>, 69 | current_frame: Frame, 70 | } 71 | 72 | impl<'a> TimelineBuilder<'a> { 73 | pub fn place_object(&mut self, place: &'a swf::tags::PlaceObject) { 74 | let layer = self.timeline.layers.entry(Depth(place.depth)).or_default(); 75 | 76 | // Find the last changed frame for this object, if it's not 77 | // the current one, and copy its state of the object. 78 | let prev_obj = match layer.frames.range(..=self.current_frame).rev().next() { 79 | Some((&frame, &obj)) if frame != self.current_frame => obj, 80 | _ => None, 81 | }; 82 | 83 | let obj = layer 84 | .frames 85 | .entry(self.current_frame) 86 | .or_insert(prev_obj) 87 | .get_or_insert_with(|| { 88 | Object::new( 89 | place 90 | .character_id 91 | .map(CharacterId) 92 | .expect("TimelineBuilder::place_object: missing `character_id`"), 93 | ) 94 | }); 95 | 96 | if let Some(character) = place.character_id.map(CharacterId) { 97 | if place.is_update { 98 | *obj = Object::new(character); 99 | } else { 100 | assert_eq!(obj.character, character); 101 | } 102 | } 103 | if let Some(matrix) = place.matrix { 104 | obj.matrix = matrix; 105 | } 106 | if let Some(name) = &place.name { 107 | obj.name = Some(name); 108 | } 109 | if let Some(color_transform) = place.color_transform { 110 | obj.color_transform = color_transform; 111 | } 112 | if let Some(ratio) = place.ratio { 113 | obj.ratio = Some(ratio); 114 | } 115 | 116 | if place.class_name.is_some() 117 | || place.clip_depth.is_some() 118 | || place.filters.is_some() 119 | || place.blend_mode.is_some() 120 | || place.visible.is_some() 121 | || place.background_color.is_some() 122 | || place.clip_actions.is_some() 123 | { 124 | eprintln!( 125 | "TimelineBuilder::place_object: unsupported features in {:?}", 126 | place 127 | ); 128 | } 129 | } 130 | 131 | pub fn remove_object(&mut self, remove: &swf::tags::RemoveObject) { 132 | self.timeline 133 | .layers 134 | .get_mut(&Depth(remove.depth)) 135 | .unwrap() 136 | .frames 137 | .insert(self.current_frame, None); 138 | } 139 | 140 | pub fn do_action(&mut self, do_action: &'a swf::tags::DoAction) { 141 | self.timeline 142 | .actions 143 | .entry(self.current_frame) 144 | .or_default() 145 | .push(avm1::Code::parse_and_compile(&do_action.actions)) 146 | } 147 | 148 | pub fn frame_label(&mut self, label: &'a swf::tags::FrameLabel) { 149 | self.timeline.labels.insert(&label.name, self.current_frame); 150 | } 151 | 152 | pub fn start_sound(&mut self, sound: &'a swf::tags::StartSound) { 153 | if sound.sound_info.envelope_records.is_some() 154 | || sound.sound_info.in_point.is_some() 155 | || sound.sound_info.out_point.is_some() 156 | || sound.sound_info.sync_stop 157 | { 158 | eprintln!( 159 | "TimelineBuilder::start_sound: unsupported SoundInfo: {:?}", 160 | sound 161 | ); 162 | } 163 | self.timeline 164 | .sounds 165 | .entry(self.current_frame) 166 | .or_default() 167 | .push(sound); 168 | } 169 | 170 | pub fn sound_stream_head(&mut self, head: &swf::tags::SoundStreamHead) { 171 | assert!(self.timeline.sound_stream.is_none()); 172 | self.timeline.sound_stream = Some(SoundStream { 173 | start: self.current_frame, 174 | format: head.stream_format, 175 | mp3: vec![], 176 | }); 177 | } 178 | 179 | pub fn sound_stream_block(&mut self, block: &swf::tags::SoundStreamBlock) { 180 | match &mut self.timeline.sound_stream { 181 | Some(stream) => { 182 | let mp3 = match stream.format { 183 | swf::AudioCodingFormat::Mp3 => sound::Mp3StreamBlock::from(block).mp3, 184 | _ => { 185 | eprintln!( 186 | "TimelineBuilder::sound_stream_block: unsupported format: {:?}", 187 | stream.format, 188 | ); 189 | return; 190 | } 191 | }; 192 | stream.mp3.extend(mp3.data); 193 | } 194 | None => { 195 | eprintln!( 196 | "TimelineBuilder::sound_stream_block: unsupported {:?}", 197 | block, 198 | ); 199 | } 200 | } 201 | } 202 | 203 | pub fn advance_frame(&mut self) { 204 | self.current_frame = self.current_frame + Frame(1); 205 | } 206 | 207 | pub fn finish(mut self, frame_count: Frame) -> Timeline<'a> { 208 | // HACK(eddyb) this should be an error but it happens during testing. 209 | if self.current_frame != frame_count { 210 | eprintln!( 211 | "TimelineBuilder::finish: expected {} frames, found {}", 212 | frame_count.0, self.current_frame.0, 213 | ); 214 | } 215 | self.timeline.frame_count = frame_count; 216 | 217 | self.timeline 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flashback-web" 3 | version = "0.0.1" 4 | authors = ["Eduard-Mihai Burtescu "] 5 | edition = "2018" 6 | repository = "https://github.com/lykenware/flashback" 7 | license = "MIT/Apache-2.0" 8 | keywords = ["Flash", "SWF"] 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | 13 | [dependencies.flashback] 14 | path = ".." 15 | version = "0.0.1" 16 | 17 | [dependencies] 18 | futures = "0.1.26" 19 | js-sys = "0.3.17" 20 | swf-parser = "0.11.0" 21 | wasm-bindgen = "0.2.40" 22 | wasm-bindgen-futures = "0.3.17" 23 | 24 | [dependencies.web-sys] 25 | version = "0.3.4" 26 | features = [ 27 | 'console', 28 | 'Document', 29 | 'Element', 30 | 'EventTarget', 31 | 'HtmlElement', 32 | 'Location', 33 | 'Node', 34 | 'Request', 35 | 'RequestInit', 36 | 'RequestMode', 37 | 'Response', 38 | 'SvgScriptElement', 39 | 'Window', 40 | ] 41 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /web/src/lib.rs: -------------------------------------------------------------------------------- 1 | use futures::Future; 2 | use js_sys::{Promise, Uint8Array}; 3 | use wasm_bindgen::prelude::*; 4 | use wasm_bindgen::JsCast; 5 | use wasm_bindgen_futures::{future_to_promise, JsFuture}; 6 | use web_sys::{console, Element, Request, RequestInit, RequestMode, Response, SvgScriptElement}; 7 | 8 | fn convert_swf(swf: &[u8]) -> String { 9 | match swf_parser::parse_swf(&swf) { 10 | Ok(movie) => { 11 | flashback::export::svg::export(&movie, flashback::export::svg::Config { use_js: true }) 12 | .to_string() 13 | } 14 | Err(e) => format!("swf-parser errored: {:?}", e), 15 | } 16 | } 17 | 18 | fn request_animation_frame(f: impl FnOnce() + 'static) { 19 | let window = web_sys::window().unwrap(); 20 | let mut f = Some(f); 21 | let closure = Closure::wrap(Box::new(move || f.take().unwrap()()) as Box); 22 | window 23 | .request_animation_frame(closure.as_ref().unchecked_ref()) 24 | .unwrap(); 25 | // FIXME(eddyb) memory management? 26 | closure.forget(); 27 | } 28 | 29 | fn load_swf_from_url(container: Element, url: String) { 30 | container.set_inner_html(&format!("Downloading `{}`...", url)); 31 | 32 | let mut opts = RequestInit::new(); 33 | opts.mode(RequestMode::Cors); 34 | 35 | let request = Request::new_with_str_and_init( 36 | &format!("https://cors-anywhere.herokuapp.com/{}", url), 37 | &opts, 38 | ) 39 | .unwrap(); 40 | 41 | let window = web_sys::window().unwrap(); 42 | future_to_promise( 43 | JsFuture::from(window.fetch_with_request(&request)) 44 | .and_then(|resp_value| { 45 | assert!(resp_value.is_instance_of::()); 46 | let resp: Response = resp_value.dyn_into().unwrap(); 47 | resp.array_buffer() 48 | }) 49 | .and_then(|buffer_value: Promise| JsFuture::from(buffer_value)) 50 | .map(move |buffer_value| { 51 | let buffer = Uint8Array::new(&buffer_value); 52 | let mut data = vec![0; buffer.length() as usize]; 53 | buffer.copy_to(&mut data); 54 | 55 | container.set_inner_html(&format!( 56 | "Converting `{}` ({:.2}kB) to SVG...", 57 | url, 58 | buffer.length() as f64 / 1000.0, 59 | )); 60 | 61 | request_animation_frame(move || { 62 | request_animation_frame(move || { 63 | container.set_inner_html(&convert_swf(&data)); 64 | 65 | // HACK(eddyb) manually evaluate script sources; 66 | if let Some(script) = container.query_selector("script").unwrap() { 67 | if let Ok(script) = script.dyn_into::() { 68 | js_sys::eval(&script.text_content().unwrap()).unwrap(); 69 | } 70 | } 71 | }) 72 | }); 73 | 74 | JsValue::undefined() 75 | }), 76 | ); 77 | } 78 | 79 | fn load_swf_from_hash(container: Element) { 80 | let window = web_sys::window().unwrap(); 81 | let location = window.location(); 82 | let hash = location.hash().unwrap(); 83 | if !hash.starts_with("#") { 84 | container.set_inner_html(&format!( 85 | "Please navigate to {}#foo.com/path/to/flash/file.swf", 86 | location.href().unwrap() 87 | )); 88 | return; 89 | } 90 | 91 | let url = &hash[1..]; 92 | let url = if url.starts_with("hs:") { 93 | format!( 94 | "cdn.mspaintadventures.com/storyfiles/hs2/{0}/{0}.swf", 95 | &url[3..] 96 | ) 97 | } else if url.starts_with("z0r:") { 98 | format!("z0r.de/L/z0r-de_{}.swf", &url[4..]) 99 | } else { 100 | url.to_string() 101 | }; 102 | load_swf_from_url(container, url); 103 | } 104 | 105 | #[wasm_bindgen(start)] 106 | pub fn main() -> Result<(), JsValue> { 107 | let window = web_sys::window().unwrap(); 108 | let document = window.document().unwrap(); 109 | let body = document.body().unwrap(); 110 | let container = document.create_element("pre")?; 111 | body.append_child(&container)?; 112 | 113 | load_swf_from_hash(container.clone()); 114 | 115 | { 116 | let closure = Closure::wrap(Box::new(move || { 117 | load_swf_from_hash(container.clone()); 118 | }) as Box); 119 | let window_et: web_sys::EventTarget = window.into(); 120 | window_et 121 | .add_event_listener_with_callback("hashchange", closure.as_ref().unchecked_ref())?; 122 | closure.forget(); 123 | } 124 | 125 | Ok(()) 126 | } 127 | --------------------------------------------------------------------------------