diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index c5b6afd19..e98ebc97e 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -400,15 +400,23 @@ jobs:
rmdir appdir/usr/share/flare 2>/dev/null || true
fi
- base_url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous"
+ # Each linuxdeploy tool lives in its OWN GitHub repo. Downloading the
+ # qt / appimage plugins from linuxdeploy/linuxdeploy returns 404, which
+ # is exactly why the AppImage was silently missing from nightly builds.
TOOLS_OK=1
- for tool in linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage linuxdeploy-plugin-appimage-x86_64.AppImage; do
- if ! (wget -q "${base_url}/${tool}" || wget -q "${base_url}/${tool}" || wget -q "${base_url}/${tool}"); then
- echo "Warning: failed to download ${tool}; skipping AppImage"
- TOOLS_OK=0; break
- fi
- chmod +x "${tool}"
- done
+ download_tool() {
+ repo="$1"; file="$2"
+ url="https://github.com/${repo}/releases/download/continuous/${file}"
+ for attempt in 1 2 3; do
+ if wget -q "${url}" -O "${file}"; then chmod +x "${file}"; return 0; fi
+ sleep 3
+ done
+ echo "::warning::failed to download ${file} from ${repo}"
+ return 1
+ }
+ download_tool linuxdeploy/linuxdeploy linuxdeploy-x86_64.AppImage || TOOLS_OK=0
+ download_tool linuxdeploy/linuxdeploy-plugin-qt linuxdeploy-plugin-qt-x86_64.AppImage || TOOLS_OK=0
+ download_tool linuxdeploy/linuxdeploy-plugin-appimage linuxdeploy-plugin-appimage-x86_64.AppImage || TOOLS_OK=0
if [ -d appdir/usr/lib/flare ]; then
export LD_LIBRARY_PATH="appdir/usr/lib/flare:${LD_LIBRARY_PATH:-}"
@@ -422,10 +430,21 @@ jobs:
./linuxdeploy-x86_64.AppImage --appdir=appdir \
--desktop-file=appdir/io.github.Flare.desktop \
--icon-file=appdir/io.github.Flare.png \
- --plugin=qt --output=appimage --custom-apprun=apprun.sh || true
+ --plugin=qt --output=appimage --custom-apprun=apprun.sh \
+ || echo "::warning::linuxdeploy failed; AppImage will be absent for this build"
+ else
+ echo "::warning::linuxdeploy tools unavailable; skipping AppImage generation"
fi
- if ls Flare*.AppImage >/dev/null 2>&1; then mv Flare*.AppImage artifact/Flare.AppImage; fi
+ # Expose the AppImage both inside the tarball AND as a standalone asset
+ if ls Flare*.AppImage >/dev/null 2>&1; then
+ APPIMAGE_FILE=$(ls Flare*.AppImage | head -n1)
+ cp "${APPIMAGE_FILE}" "${GITHUB_WORKSPACE}/Flare-x86_64.AppImage"
+ mv "${APPIMAGE_FILE}" artifact/Flare.AppImage
+ echo "AppImage created: ${APPIMAGE_FILE}"
+ else
+ echo "::warning::No AppImage was produced for this build (see download/linuxdeploy warnings above)"
+ fi
ARTIFACT_NAME=Flare-Linux-nightly
mv artifact ${ARTIFACT_NAME} || true
tar zcf ${ARTIFACT_NAME}.tar.gz ${ARTIFACT_NAME} || true
@@ -467,6 +486,13 @@ jobs:
path: flare/build/Flare-Linux-nightly.tar.gz
if-no-files-found: ignore
+ - name: Upload standalone AppImage
+ uses: actions/upload-artifact@v4
+ with:
+ name: Flare-Linux-AppImage
+ path: Flare-x86_64.AppImage
+ if-no-files-found: warn
+
- name: Upload smoke test log
if: always()
uses: actions/upload-artifact@v4
@@ -684,7 +710,7 @@ jobs:
)
fi
- find dist -type f \( -name '*.tar.gz' -o -name '*.dmg' \) -exec cp -f {} release-assets/ \;
+ find dist -type f \( -name '*.tar.gz' -o -name '*.dmg' -o -name '*.AppImage' \) -exec cp -f {} release-assets/ \;
find release-assets -type f | sort
- name: Publish / update rolling nightly release
@@ -706,7 +732,7 @@ jobs:
| Platform | Package | Requirements |
|----------|---------|--------------|
| **Windows** | `Flare-Windows-Portable-nightly.zip` | Windows 10/11 **64-bit (x86_64/AMD64)** — ARM is not supported |
- | **Linux** | `Flare-Linux-nightly.tar.gz` | x86_64 Linux, glibc 2.35+ (Ubuntu 22.04 or newer) |
+ | **Linux** | `Flare-x86_64.AppImage` (standalone) or `Flare-Linux-nightly.tar.gz` | x86_64 Linux, glibc 2.35+ (Ubuntu 22.04 or newer) |
| **macOS** | `Flare.dmg` | macOS 13+ Intel (Apple Silicon via Rosetta 2) |
## Windows usage
@@ -727,8 +753,16 @@ jobs:
## Linux usage
- 1. Extract: `tar xzf Flare-Linux-nightly.tar.gz`
- 2. Inside the `Flare-Linux-nightly/` folder run `./Flare.AppImage` (or the `Flare` binary if AppImage creation was skipped).
+ **Easiest:** download `Flare-x86_64.AppImage`, then:
+ ```
+ chmod +x Flare-x86_64.AppImage
+ ./Flare-x86_64.AppImage
+ ```
+
+ **Or the tarball:** extract `tar xzf Flare-Linux-nightly.tar.gz`, then inside the
+ `Flare-Linux-nightly/` folder run `./Flare.AppImage` (or the `Flare` binary if
+ AppImage creation was skipped). If the standalone AppImage asset is absent, the
+ build's AppImage step failed — check the [CI run](https://github.com/Flare-Animate/Flare/actions/runs/${{ github.run_id }}) for `::warning::` lines.
See the [CI run](https://github.com/Flare-Animate/Flare/actions/runs/${{ github.run_id }}) for build logs and smoke test results.
files: |
diff --git a/.github/workflows/workflow_linux.yml b/.github/workflows/workflow_linux.yml
index 9c82366cf..0a5dc4d93 100644
--- a/.github/workflows/workflow_linux.yml
+++ b/.github/workflows/workflow_linux.yml
@@ -187,18 +187,23 @@ jobs:
rmdir appdir/usr/share/flare 2>/dev/null || true
fi
- base_url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous"
+ # Each linuxdeploy tool lives in its OWN GitHub repo; the qt / appimage
+ # plugins are NOT in linuxdeploy/linuxdeploy (downloading them from there
+ # 404s and silently skips AppImage generation).
APPIMAGE_TOOLS_OK=1
- for tool in linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage linuxdeploy-plugin-appimage-x86_64.AppImage; do
- if ! (wget -q --show-progress "${base_url}/${tool}" || \
- wget -q --show-progress "${base_url}/${tool}" || \
- wget -q --show-progress "${base_url}/${tool}"); then
- echo "Warning: failed to download ${tool}; skipping AppImage generation for this run."
- APPIMAGE_TOOLS_OK=0
- break
- fi
- chmod +x "${tool}"
- done
+ download_tool() {
+ repo="$1"; file="$2"
+ url="https://github.com/${repo}/releases/download/continuous/${file}"
+ for attempt in 1 2 3; do
+ if wget -q "${url}" -O "${file}"; then chmod +x "${file}"; return 0; fi
+ sleep 3
+ done
+ echo "::warning::failed to download ${file} from ${repo}"
+ return 1
+ }
+ download_tool linuxdeploy/linuxdeploy linuxdeploy-x86_64.AppImage || APPIMAGE_TOOLS_OK=0
+ download_tool linuxdeploy/linuxdeploy-plugin-qt linuxdeploy-plugin-qt-x86_64.AppImage || APPIMAGE_TOOLS_OK=0
+ download_tool linuxdeploy/linuxdeploy-plugin-appimage linuxdeploy-plugin-appimage-x86_64.AppImage || APPIMAGE_TOOLS_OK=0
if [ -d appdir/usr/lib/flare ]; then
export LD_LIBRARY_PATH="appdir/usr/lib/flare:${LD_LIBRARY_PATH:-}"
diff --git a/README.md b/README.md
index 3e4e75186..6aa955733 100644
--- a/README.md
+++ b/README.md
@@ -2,62 +2,115 @@
+**Flare** is a free, open-source 2D animation studio — a community-driven fork of
+[OpenToonz](https://opentoonz.github.io/) reworked to feel like **Adobe Animate**
+and to open the Flash/Animate file formats Adobe left behind (`.fla`, `.xfl`,
+`.swf`, and friends).
-A fork of OpenToonz rebranded as Flare — focused on providing an Adobe Animate-like
-user experience and improving interoperability with Flash assets (.swf/.fla).
+The goal is simple: give the Flash/Animate community a modern, actively
+maintained, cross-platform home that reads their existing projects — without a
+subscription and without a dead runtime.
-This repository is a fork of OpenToonz and retains the original licensing and
-attribution. See the Licensing section below for details.
+[](https://discord.com/invite/JpeScW8Awa)
+[](https://flare-animate.github.io/website/)
+[](./LICENSE.txt)
-[](https://discord.gg/JpeScW8Awa)
+➡️ **Website:** https://flare-animate.github.io/website/ · **Discord:** https://discord.com/invite/JpeScW8Awa
+[日本語](./doc/README_ja.md) · [简体中文](./doc/README_chs.md)
-[日本語](./doc/README_ja.md) [简体中文](./doc/README_chs.md)
+---
-## What is Flare?
+## Why Flare?
-Flare is a community-driven fork of OpenToonz that ships a revamped UI layout
-inspired by Adobe Animate and adds built-in Flash ecosystem import support for
-FLA/XFL/SWC/SWF/FLV/F4V/AS workflows.
+Adobe Animate is proprietary, subscription-only, and Flash is end-of-life — yet
+huge amounts of animation, games, and educational content still live in `.fla`
+and `.swf` files. OpenToonz is a powerful production tool but its UI is
+unfamiliar to Flash/Animate artists. Flare bridges the two:
-For the original Flare project and its history, see the Flare website:
-https://flare-animate.github.io/website
+- **Familiar UI** — the default workspace is laid out like Adobe Animate
+ (Drawing / Animation / Rigging / Compositing rooms), with an Adobe-style theme.
+- **Open your old work** — built-in import for the Flash/Animate file family.
+- **OpenToonz power underneath** — vector + raster levels, a full xsheet/timeline,
+ plastic/skeleton rigging, FX, and the OpenToonz rendering pipeline.
-## Program Requirements
+## Features
-To enable SWF import features, install FFmpeg and ensure it is available on your PATH.
+### Adobe Animate-style workspace (default)
+Flare ships with the **Adobe Animate** workspace selected out of the box — no
+configuration needed. Prefer the classic layout? Open **Preferences → Interface →
+Rooms** and switch to **OpenToonz** (or **StudioGhibli**).
-Flare uses CMake as its build system. You must have CMake (version 3.10 or later)
-installed and available on your PATH before attempting to configure or build the
-project. On Windows you can install CMake via the official installer or
-[Chocolatey](https://chocolatey.org/). On macOS use Homebrew (`brew install cmake`),
-and on Linux use your distribution's package manager (`apt`, `dnf`, etc.).
+### Flash / Adobe Animate file support *(work in progress)*
+Built-in, native C++ import — no Java, JPEXS, or external runtime required.
-## Installation
+| Format | Extension | Status |
+|--------|-----------|--------|
+| Flash project (ZIP) | `.fla` | Import (XFL extraction + parse) |
+| XFL project | `.xfl` | Import (directory or ZIP) |
+| Compiled Flash | `.swf` | Header + embedded bitmap extraction |
+| Component library | `.swc` | Catalog + embedded bitmaps |
+| Flash Video | `.flv` / `.f4v` | Raster level via FFmpeg |
+| ActionScript | `.as` | Imported as reference text |
-Please see the `doc/` folder for platform-specific build and installation
-instructions.
+> ⚠️ Flash import is actively being developed and **not yet production-ready** —
+> complex documents will not round-trip cleanly. See [`doc/FLASH_SUPPORT.md`](./doc/FLASH_SUPPORT.md)
+> for the architecture and current limitations, and please file bugs with a sample
+> file. Tracking issues: [#16](https://github.com/Flare-Animate/Flare/issues/16),
+> [#47](https://github.com/Flare-Animate/Flare/issues/47).
-## How to Build Locally
+### Inherited from OpenToonz / Tahoma2D
+Flare keeps the OpenToonz feature set and continues to pull improvements from
+OpenToonz and the [Tahoma2D](https://tahoma2d.org/) community fork:
+vector & raster drawing, the xsheet and timeline, plastic & skeleton rigging,
+the GTS scanning tools, the effects (FX) schematic, motion tracking, and the
+script console.
-⚠️ **IMPORTANT:** Building Flare is memory-intensive. On systems with limited RAM (< 8GB), limit parallel jobs to avoid system freezes. See platform-specific guides below for details.
+### Merging with Next2Flash
+The Flare-Animate org is consolidating its Flash tooling by merging
+[Next2Flash](https://github.com/SSF2-Mods-Official/Next2Flash) — an MIT-licensed
+SWF round-trip editor with a native AS3 decompiler — into Flare. The native SWF
+reader/writer pieces are being ported, while AS3 round-tripping is bridged as an
+optional helper. See [`doc/NEXT2FLASH_INTEGRATION.md`](./doc/NEXT2FLASH_INTEGRATION.md).
-You can configure a build directory from the repository root with a command such as:
+## Download
-```sh
-cmake -S flare/sources -B build -G "Ninja" -DCMAKE_BUILD_TYPE=Release
-```
+Pre-built nightly binaries for **Windows**, **Linux** (AppImage + tarball), and
+**macOS** (DMG) are published automatically on every push to `master`:
+
+➡️ **[Latest nightly release](https://github.com/Flare-Animate/Flare/releases/tag/nightly)**
+
+| Platform | Asset | Requirements |
+|----------|-------|--------------|
+| Windows | `Flare-Windows-Portable-nightly.zip` | Windows 10/11, 64-bit |
+| Linux | `Flare-x86_64.AppImage` | x86_64, glibc 2.35+ (Ubuntu 22.04+) |
+| macOS | `Flare.dmg` | macOS 13+ Intel (Apple Silicon via Rosetta 2) |
+
+These are pre-release builds and may be unstable.
+
+## Program Requirements
+
+To enable FFmpeg-based `.swf`/`.flv`/`.f4v` playback, install **FFmpeg** and make
+sure it is on your `PATH`. The Flash *import* commands themselves need no external
+tools.
-and then build (limit parallel jobs on low-memory systems):
+Building requires **CMake 3.10+** on your `PATH`. Install it via the official
+installer or [Chocolatey](https://chocolatey.org/) on Windows, Homebrew
+(`brew install cmake`) on macOS, or your package manager on Linux.
+
+## How to Build Locally
+
+> ⚠️ Building Flare is memory-intensive. On systems with < 8 GB RAM, limit
+> parallel jobs to avoid freezes.
```sh
-# For systems with < 4GB RAM, use: cmake --build build -j1
-# For systems with 4-8GB RAM, use: cmake --build build -j2
-# For systems with > 8GB RAM, use: cmake --build build --parallel
+cmake -S flare/sources -B build -G "Ninja" -DCMAKE_BUILD_TYPE=Release
+
+# < 4GB RAM: -j1 | 4-8GB RAM: -j2 | > 8GB RAM: --parallel
cmake --build build -j2
```
-For more detailed, platform‑specific guidance follow the links below:
+Platform-specific guides:
- [Windows](./doc/how_to_build_win.md)
- [macOS](./doc/how_to_build_macosx.md)
@@ -66,42 +119,34 @@ For more detailed, platform‑specific guidance follow the links below:
## Community & Contribution
-This fork aims to stay compatible with OpenToonz where possible while
-introducing new features. When contributing, please keep the original project's
+- 💬 **Discord:** https://discord.com/invite/JpeScW8Awa — the fastest way to ask
+ questions, report bugs, and follow development.
+- 🌐 **Website:** https://flare-animate.github.io/website/
+- 🗳️ **Discussions:** [GitHub Discussions](https://github.com/orgs/Flare-Animate/discussions)
+ for proposals and ideas.
+- 🐛 **Issues:** [GitHub Issues](https://github.com/Flare-Animate/Flare/issues).
+
+Contributions are welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md). Flare aims to
+stay compatible with OpenToonz where possible; please keep the original project's
licensing and attribution in mind.
## Licensing
-Files outside of the `thirdparty` and `stuff/library/mypaint brushes`
-directories are based on the Modified BSD License.
-- [modified BSD license](./LICENSE.txt).
+Files outside `thirdparty/` and `stuff/library/mypaint brushes/` are under the
+[Modified BSD License](./LICENSE.txt). Third-party components retain their
+original licenses — see `thirdparty/` and
+`stuff/library/mypaint brushes/Licenses.txt`.
-Third-party components retain their original licenses. See the relevant
-documentation in `thirdparty/` and `stuff/library/mypaint brushes/Licenses.txt`.
-
-### Adobe Animate-style Workspace & Theme
-
-Flare includes an "Adobe Animate" workspace and an Adobe-like color theme by
-default. To switch to the Adobe Animate workspace, open the Room (Workspace)
-menu and choose "Adobe Animate".
-
-### Importing SWF files
-
-Flare provides a built-in Flash import workflow for `.fla`, `.xfl`, `.swc`,
-`.swf`, `.flv`, `.f4v`, and `.as`. Native metadata/asset extraction works
-without external helper tools; when FFmpeg exposes direct readers for
-`.swf`/`.flv`/`.f4v` on your system, Flare can also load those containers as
-scene levels automatically.
+Flare is a fork of [OpenToonz](https://github.com/opentoonz/opentoonz) (© DWANGO,
+based on Toonz © Digital Video / Studio Ghibli) and retains its licensing and
+attribution.
## Development helper scripts
-To make it easier to follow build and test output you can run the included
-`log_watcher.py` script. It watches `*.log` files underneath the build
-directory and prints the last few lines whenever they are modified. This is
-also the script that the autonomous chat mode will launch automatically:
+`scripts/log_watcher.py` watches `*.log` files under the build directory and
+prints the tail whenever they change (also wired to the "watch logs" task in
+VS Code, `Ctrl+Shift+B`):
```sh
python scripts/log_watcher.py # defaults to flare/build
```
-
-You can run the same command via the "watch logs" task in VS Code (`Ctrl+Shift+B`).
diff --git a/doc/FLASH_SUPPORT.md b/doc/FLASH_SUPPORT.md
index 6cbfcb4da..4a43428c8 100644
--- a/doc/FLASH_SUPPORT.md
+++ b/doc/FLASH_SUPPORT.md
@@ -10,7 +10,8 @@ video playback if it is installed).
| Format | Extension | Support |
|--------|-----------|---------|
-| Flash project (ZIP) | `.fla` | Extract with minizip → parse XFLReader |
+| Flash project (XFL-based, CS5+) | `.fla` | Extract with minizip → parse XFLReader |
+| Flash project (legacy binary, CS4-) | `.fla` | OLE2 compound document — detected, embedded bitmaps recovered (QImage-validated); full timeline import not yet supported (re-save as CS5+/XFL) |
| XFL project | `.xfl` | Directory or ZIP → parse XFLReader |
| Compiled Flash | `.swf` | Header + embedded bitmap extraction |
| Component library | `.swc` | ZIP + catalog.xml + library.swf bitmaps |
@@ -95,3 +96,12 @@ flash.writeMovie(fp);
| [Apache Flex SDK](https://github.com/apache/flex-sdk) | SWC `catalog.xml` schema | Apache 2.0 |
| [lifeart/fla-viewer](https://github.com/lifeart/fla-viewer) | XFL DOMDocument.xml structure | MIT |
| FLV / ISO BMFF public spec | FLV 9-byte header, `ftyp` box layout | Public spec |
+
+## Roadmap: Next2Flash merge
+
+The Flare-Animate org is consolidating its Flash tooling by merging
+[Next2Flash](https://github.com/SSF2-Mods-Official/Next2Flash) (an MIT-licensed
+SWF round-trip editor with a native AS3 decompiler) into Flare. Because the two
+projects use different stacks (C++/Qt vs Python/JS/Electron), the merge ports the
+native-friendly pieces and bridges AS3 as an optional helper. See
+[`NEXT2FLASH_INTEGRATION.md`](./NEXT2FLASH_INTEGRATION.md) for the full plan.
diff --git a/doc/NEXT2FLASH_INTEGRATION.md b/doc/NEXT2FLASH_INTEGRATION.md
new file mode 100644
index 000000000..68a094b80
--- /dev/null
+++ b/doc/NEXT2FLASH_INTEGRATION.md
@@ -0,0 +1,109 @@
+# Merging Next2Flash into Flare — Integration Plan
+
+Status: **assessment / roadmap** (no code merged yet)
+
+[Next2Flash](https://github.com/SSF2-Mods-Official/Next2Flash) and Flare are being
+brought together as the Flare-Animate org consolidates its Flash tooling. This
+document records what Next2Flash actually is, why a naïve "copy the files in"
+merge will not work, and the concrete path that will.
+
+## What Next2Flash is
+
+A desktop **SWF round-trip editor** — "import a SWF, edit it visually, export it
+back to a working SWF." MIT-licensed, built on the open-source Next2D engine.
+
+| Layer | Tech | Role |
+|-------|------|------|
+| Shell | Electron | Desktop window, native menus/dialogs |
+| Editor | Next2D (JS / HTML / CSS) | Visual timeline, stage, library |
+| Backend | Python + Flask | SWF parse, conversion, **AS3 (de)compilation** |
+| Toolchain | Flex SDK (bundled) | Compiles AS3 back into the SWF |
+
+The genuinely valuable, hard-to-replicate parts live in `app/`:
+
+- `app/as3_decompiler/` — a from-scratch **ABC/AS3 bytecode** parser, decompiler,
+ and patcher (`abc_parser.py`, `method_decompiler.py`, `opcodes.py`,
+ `swf_reader.py`, `swf_patcher.py`). This is the crown jewel — native AS3
+ round-tripping without JPEXS.
+- `bitmap_converter.py`, `char_id_allocator.py`, `cycle_detector.py`,
+ `conversion_service.py`, `compilation_pipeline.py` — the SWF asset/ID pipeline.
+- `app/assets/js/swf-parse-worker.js`, `swf-timeline-importer.js` — JS-side SWF
+ tag parsing and timeline reconstruction.
+
+## Why a direct merge does not work
+
+Flare is **C++ / Qt**. Next2Flash is **Python + JavaScript + Electron**. There is
+no shared language, build system, or object model:
+
+- Flare's `doc/FLASH_SUPPORT.md` deliberately moved *away* from external runtimes
+ (it dropped the JPEXS/Python approach for native C++) for licensing and
+ zero-dependency reasons. Pulling a Python+Flask+Flex-SDK stack back in reverses
+ that decision and adds a heavy runtime to every install.
+- Electron/Next2D's renderer cannot be embedded in a Qt app.
+
+So "merge the codebase" has to mean **merge the capabilities**, choosing per
+component between *porting* and *bridging*.
+
+## Component-by-component mapping
+
+| Next2Flash capability | Flare today | Recommended path |
+|-----------------------|-------------|------------------|
+| SWF tag parsing | `common/flash/FSWFStream`, `FDT*`, `flashimport.cpp::readSwfHeader/extractSwfBitmaps` | **Port** — extend Flare's native tag reader using N2F's tag handling as reference (both MIT/BSD-compatible) |
+| FLA/XFL parsing | `common/flash/XFLReader` | Keep Flare's; cross-check against N2F timeline importer |
+| **AS3 / ABC (de)compile** | `common/flash/FAction.*` (stubs only) | **Bridge first, port later** — see below |
+| SWF *export* / round-trip | `common/flash/tflash` (writer) | Port N2F's `swf_patcher` ID-preservation approach |
+| Visual timeline editor | Flare xsheet/timeline (native) | Already covered by Flare — no port needed |
+
+## Recommended approach
+
+**Two tracks, in order:**
+
+### Track 1 — Port the native-friendly pieces (no new runtime)
+Improve Flare's existing C++ SWF reader/writer using Next2Flash's parsing logic as
+a reference implementation. Targets, in priority order:
+
+1. **Character-ID preservation on export** — port the `char_id_allocator` /
+ `swf_patcher` strategy into `tflash` so re-exported SWFs keep working.
+2. **Fuller SWF tag coverage** in `extractSwfBitmaps` / `FDT*` (shapes, sprites,
+ placements) cross-referenced against `swf-parse-worker.js`.
+3. **Bitmap pass-through** (`bitmap_converter.py`) so embedded images survive a
+ round-trip byte-for-byte.
+
+These are pure C++ changes, keep the zero-dependency promise, and directly move
+the needle on issues #16 and #47.
+
+### Track 2 — AS3 round-tripping as an *optional* bridge
+A native C++ ABC decompiler is a large project. Next2Flash already has a working
+Python one. Rather than reverse the no-runtime decision globally, expose AS3
+features through an **optional, auto-detected helper**, exactly like Flare already
+treats FFmpeg:
+
+- Ship the `app/as3_decompiler/` Python package as an optional `flare-as3` sidecar.
+- Flare calls it over a small CLI/JSON boundary (it already has a `cli.py`).
+- If the helper isn't present, AS3 import/export is simply greyed out — core FLA
+ import still works with no Python at all.
+
+This gives users N2F's AS3 power immediately without forcing the dependency, and
+buys time to port the decompiler to C++ later if desired.
+
+## Licensing
+
+Next2Flash is **MIT**; Flare is **Modified BSD (3-Clause)**. MIT code can be
+incorporated into a BSD project provided the MIT copyright notice is preserved.
+Any ported file or bundled sidecar must keep Next2Flash's `LICENSE` and a header
+note. The bundled **Flex SDK** is Apache-2.0 and must ship with its own NOTICE.
+
+## Concrete first steps
+
+- [ ] Add Next2Flash as a git submodule under `thirdparty/next2flash/` (or vendored
+ `tools/flash/next2flash/`) so the Python helper can be packaged optionally.
+- [ ] Define the `flare-as3` CLI contract (stdin/stdout JSON: `decompile`,
+ `compile`, `patch`).
+- [ ] Wire an optional "ActionScript (via Next2Flash)" path into
+ `flashimport.cpp`, detected like FFmpeg.
+- [ ] Begin Track 1.1: port char-ID preservation into `common/flash/tflash`.
+- [ ] Credit Next2Flash in `README.md` and `doc/FLASH_SUPPORT.md`.
+
+> This plan keeps Flare installable with zero extra runtime for the common case
+> (open a FLA, get the art), while still delivering Next2Flash's AS3 superpowers to
+> users who opt in — and leaves a clean path to fully native C++ later.
diff --git a/flare/sources/flare/flashimport.cpp b/flare/sources/flare/flashimport.cpp
index 509a2e372..0b154c860 100644
--- a/flare/sources/flare/flashimport.cpp
+++ b/flare/sources/flare/flashimport.cpp
@@ -40,6 +40,7 @@
#include
#include
+#include
#include
#include
#include
@@ -116,10 +117,11 @@ static bool extractZip(const QString &zipPath, const QString &outDir) {
entryStr.replace('\\', '/');
while (entryStr.startsWith("./"))
entryStr = entryStr.mid(2);
- while (entryStr.startsWith('/'))
- entryStr = entryStr.mid(1);
- // Zip Slip protection: reject absolute paths and path traversal
+ // Zip Slip protection: reject absolute paths and path traversal.
+ // Absolute entries (leading '/' or a drive letter) are rejected rather
+ // than silently relativized — a well-formed FLA/XFL/SWC never contains
+ // them, so their presence indicates a malformed or malicious archive.
if (entryStr.startsWith('/') || entryStr.startsWith('\\') ||
entryStr.contains("../") || entryStr.contains("..\\") ||
entryStr.endsWith("..") ||
@@ -404,6 +406,66 @@ static QStringList extractFLABinaryMedia(const QString &outDir) {
return extracted;
}
+// ---------------------------------------------------------------------------
+// Legacy binary FLA support (Flash CS4 and earlier)
+//
+// Pre-CS5 .fla files are not ZIP/XFL archives — they are OLE2 / Compound File
+// Binary Format (CFBF) documents, identified by the 8-byte magic
+// D0 CF 11 E0 A1 B1 1A E1. Flare's ZIP-based importer cannot open them, which
+// previously surfaced as a misleading "invalid/corrupt ZIP" error (issue #47).
+//
+// Full timeline/symbol reconstruction from the binary format is a large effort
+// (tracked with the Next2Flash merge). As a first step we (a) detect the format
+// and tell the user exactly what it is and how to convert it, and (b) recover
+// whatever embedded bitmaps we can, validating each candidate with QImage so a
+// false-positive marker in entropy data never produces a broken image.
+// ---------------------------------------------------------------------------
+static bool isOle2CompoundFile(const QString &path) {
+ QFile f(path);
+ if (!f.open(QIODevice::ReadOnly)) return false;
+ QByteArray magic = f.read(8);
+ f.close();
+ static const unsigned char kOle2[8] = {0xD0, 0xCF, 0x11, 0xE0,
+ 0xA1, 0xB1, 0x1A, 0xE1};
+ return magic.size() == 8 &&
+ std::memcmp(magic.constData(), kOle2, 8) == 0;
+}
+
+static QStringList extractLegacyFlaBitmaps(const QByteArray &data,
+ const QString &outDir) {
+ QStringList extracted;
+ int idx = 0;
+
+ // Carve candidates by image signature; decode each with QImage (which stops
+ // at the real end of the image and rejects invalid candidates) and re-save
+ // as PNG. Signatures are scanned independently; the window for each is up to
+ // the next signature of the same type.
+ auto carve = [&](const QByteArray &sig, const char *qtFormat) {
+ int pos = 0;
+ int found = 0;
+ while (pos < data.size() && found < 4096) {
+ int start = data.indexOf(sig, pos);
+ if (start < 0) break;
+ int next = data.indexOf(sig, start + sig.size());
+ int end = (next < 0) ? data.size() : next;
+ QByteArray chunk = data.mid(start, end - start);
+ pos = start + sig.size();
+ ++found;
+ QImage img;
+ if (img.loadFromData(chunk, qtFormat) && !img.isNull() &&
+ img.width() >= 2 && img.height() >= 2) {
+ QString fname =
+ QString("media_%1.png").arg(idx++, 4, 10, QChar('0'));
+ if (img.save(outDir + "/" + fname, "PNG")) extracted << fname;
+ }
+ }
+ };
+
+ carve(QByteArray("\xFF\xD8\xFF", 3), "JPG");
+ carve(QByteArray("\x89PNG\r\n\x1A\n", 8), "PNG");
+ return extracted;
+}
+
// ---------------------------------------------------------------------------
// SWF bitmap extractor
//
@@ -799,8 +861,31 @@ void ImportFlashVectorCommand::execute() {
QStringList exported;
QString info;
+ // ---- Legacy binary FLA (Flash CS4 and earlier; OLE2 compound document) ----
+ if (ext == "fla" && isOle2CompoundFile(srcPath)) {
+ QFile flaFile(srcPath);
+ QStringList bitmaps;
+ if (flaFile.open(QIODevice::ReadOnly)) {
+ QByteArray flaData = flaFile.readAll();
+ flaFile.close();
+ bitmaps = extractLegacyFlaBitmaps(flaData, outPath);
+ }
+ exported += bitmaps;
+ info = QObject::tr(
+ "Legacy binary FLA detected (Adobe Flash CS4 or earlier).\n"
+ "Flare natively imports XFL-based FLAs (Animate / Flash CS5 and "
+ "newer). Full timeline and symbol import for the older binary format "
+ "is not supported yet — to import everything, open this file in Adobe "
+ "Animate and re-save it as a CS5+ FLA or an uncompressed XFL.");
+ if (!bitmaps.isEmpty())
+ info += QObject::tr("\n Recovered %1 embedded bitmap(s) from the file.")
+ .arg(bitmaps.size());
+ else
+ info += QObject::tr("\n No embedded bitmaps could be recovered.");
+
// ---- FLA / XFL / SWC : ZIP-based container ----
- if (ext == "fla" || ext == "swc" || (ext == "xfl" && XFL::isFLAZipBased(fp))) {
+ } else if (ext == "fla" || ext == "swc" ||
+ (ext == "xfl" && XFL::isFLAZipBased(fp))) {
if (!extractZip(srcPath, outPath)) {
DVGui::error(QObject::tr("Failed to extract archive (invalid/corrupt ZIP): %1").arg(srcPath));
diff --git a/flare/sources/flare/mainwindow.cpp b/flare/sources/flare/mainwindow.cpp
index 4a4da7dc0..bf66d1e94 100644
--- a/flare/sources/flare/mainwindow.cpp
+++ b/flare/sources/flare/mainwindow.cpp
@@ -506,6 +506,8 @@ centralWidget->setLayout(centralWidgetLayout);*/
setCommandHandler(MI_OpenWhatsNew, this, &MainWindow::onOpenWhatsNew);
setCommandHandler(MI_OpenCommunityForum, this,
&MainWindow::onOpenCommunityForum);
+ setCommandHandler(MI_OpenDiscord, this, &MainWindow::onOpenDiscord);
+ setCommandHandler(MI_OpenWebsite, this, &MainWindow::onOpenWebsite);
setCommandHandler(MI_OpenReportABug, this, &MainWindow::onOpenReportABug);
setCommandHandler(MI_MaximizePanel, this, &MainWindow::maximizePanel);
@@ -1109,6 +1111,18 @@ void MainWindow::onOpenCommunityForum() {
//-----------------------------------------------------------------------------
+void MainWindow::onOpenDiscord() {
+ QDesktopServices::openUrl(QUrl("https://discord.com/invite/JpeScW8Awa"));
+}
+
+//-----------------------------------------------------------------------------
+
+void MainWindow::onOpenWebsite() {
+ QDesktopServices::openUrl(QUrl("https://flare-animate.github.io/website/"));
+}
+
+//-----------------------------------------------------------------------------
+
void MainWindow::onOpenReportABug() {
QString str = QString(
tr("To report a bug, click on the button below to open a web browser "
@@ -2401,6 +2415,10 @@ void MainWindow::defineActions() {
"web");
createMenuHelpAction(MI_OpenCommunityForum, QT_TR_NOOP("&Community Forum..."),
"", "web");
+ createMenuHelpAction(MI_OpenDiscord, QT_TR_NOOP("Join us on &Discord..."), "",
+ "web");
+ createMenuHelpAction(MI_OpenWebsite, QT_TR_NOOP("Flare &Website..."), "",
+ "web");
createMenuHelpAction(MI_OpenReportABug, QT_TR_NOOP("&Report a Bug..."), "",
"web");
diff --git a/flare/sources/flare/mainwindow.h b/flare/sources/flare/mainwindow.h
index e15f8c7be..820edd724 100644
--- a/flare/sources/flare/mainwindow.h
+++ b/flare/sources/flare/mainwindow.h
@@ -109,6 +109,8 @@ class MainWindow final : public QMainWindow {
void onOpenOnlineManual();
void onOpenWhatsNew();
void onOpenCommunityForum();
+ void onOpenDiscord();
+ void onOpenWebsite();
void onOpenReportABug();
void checkForUpdates();
int getRoomCount() const;
diff --git a/flare/sources/flare/menubar.cpp b/flare/sources/flare/menubar.cpp
index 28bb67407..329e3dd3b 100644
--- a/flare/sources/flare/menubar.cpp
+++ b/flare/sources/flare/menubar.cpp
@@ -1489,6 +1489,8 @@ QMenuBar *StackedMenuBar::createFullMenuBar() {
addMenuItem(helpMenu, MI_OpenOnlineManual);
addMenuItem(helpMenu, MI_OpenWhatsNew);
addMenuItem(helpMenu, MI_OpenCommunityForum);
+ addMenuItem(helpMenu, MI_OpenDiscord);
+ addMenuItem(helpMenu, MI_OpenWebsite);
helpMenu->addSeparator();
addMenuItem(helpMenu, MI_OpenReportABug);
helpMenu->addSeparator();
diff --git a/flare/sources/flare/menubarcommandids.h b/flare/sources/flare/menubarcommandids.h
index 4c7a321e3..9556e04bd 100644
--- a/flare/sources/flare/menubarcommandids.h
+++ b/flare/sources/flare/menubarcommandids.h
@@ -458,6 +458,8 @@
#define MI_OpenOnlineManual "MI_OpenOnlineManual"
#define MI_OpenWhatsNew "MI_OpenWhatsNew"
#define MI_OpenCommunityForum "MI_OpenCommunityForum"
+#define MI_OpenDiscord "MI_OpenDiscord"
+#define MI_OpenWebsite "MI_OpenWebsite"
#define MI_OpenReportABug "MI_OpenReportABug"
#define MI_ClearCacheFolder "MI_ClearCacheFolder"
diff --git a/flare/sources/flare/test_flashimport.cpp b/flare/sources/flare/test_flashimport.cpp
index f983016d0..6e16bff1e 100644
--- a/flare/sources/flare/test_flashimport.cpp
+++ b/flare/sources/flare/test_flashimport.cpp
@@ -555,8 +555,8 @@ static bool isPathSafeForExtraction(const QString &outDir, const QString &entryN
QString entry = entryName;
entry.replace('\\', '/');
while (entry.startsWith("./")) entry = entry.mid(2);
- while (entry.startsWith('/')) entry = entry.mid(1);
if (entry.isEmpty()) return false;
+ // Absolute paths are rejected outright (not relativized) — matches extractZip().
if (entry.startsWith('/')) return false;
// Reject any remaining backslash after normalisation
if (entry.contains('\\')) return false;
diff --git a/flare/sources/xdg-data/io.github.Flare.desktop b/flare/sources/xdg-data/io.github.Flare.desktop
index 67519e616..00fbb252d 100644
--- a/flare/sources/xdg-data/io.github.Flare.desktop
+++ b/flare/sources/xdg-data/io.github.Flare.desktop
@@ -5,6 +5,6 @@ Exec=flare
Icon=io.github.Flare
Terminal=false
Type=Application
-Categories=Graphics;RasterGraphics;2DGraphics;Animation;
+Categories=Graphics;2DGraphics;RasterGraphics;VectorGraphics;X-Animation;
StartupWMClass=Flare
MimeType=application/x-toonz-tnz;application/x-shockwave-flash;application/x-xfl;
diff --git a/stuff/profiles/layouts/rooms/Animate/layouts.txt b/stuff/profiles/layouts/rooms/Animate/layouts.txt
deleted file mode 100644
index 191370cd4..000000000
--- a/stuff/profiles/layouts/rooms/Animate/layouts.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-room1.ini
-room2.ini
-room3.ini
-room4.ini
diff --git a/stuff/profiles/layouts/rooms/Animate/menubar_template.xml b/stuff/profiles/layouts/rooms/Animate/menubar_template.xml
deleted file mode 100644
index d28bc95cc..000000000
--- a/stuff/profiles/layouts/rooms/Animate/menubar_template.xml
+++ /dev/null
@@ -1,262 +0,0 @@
-
-
-
- MI_Undo
- MI_Redo
-
- MI_Cut
- MI_Copy
- MI_Paste
- MI_PasteAbove
- MI_PasteInto
- MI_PasteDuplicate
- MI_Insert
- MI_InsertAbove
- MI_Clear
-
- MI_SelectAll
- MI_InvertSelection
-
-
- MI_Group
- MI_Ungroup
- MI_EnterGroup
- MI_ExitGroup
-
-
-
- MI_BringToFront
- MI_BringForward
- MI_SendBackward
- MI_SendBack
-
-
-
-
- MI_NewLevel
-
- MI_NewToonzRasterLevel
- MI_NewVectorLevel
- MI_NewRasterLevel
- MI_NewNoteLevel
-
- MI_LoadLevel
- MI_SaveLevel
- MI_SaveLevelAs
- MI_SaveAllLevels
- MI_OpenRecentLevel
- MI_ExportLevel
-
- MI_AddFrames
- MI_Renumber
- MI_ReplaceLevel
- MI_RevertToLastSaved
-
-
- MI_BrightnessAndContrast
- MI_AdjustLevels
- MI_AdjustThickness
- MI_Antialias
- MI_Binarize
- MI_LinesFade
-
-
- MI_ExposeResource
- MI_EditLevel
-
- MI_CanvasSize
- MI_LevelSettings
- MI_FileInfo
-
- MI_RemoveUnused
-
-
- MI_SceneSettings
- MI_CameraSettings
-
- MI_OpenChild
- MI_CloseChild
- MI_SaveSubxsheetAs
- MI_Collapse
- MI_Resequence
- MI_ExplodeChild
-
- MI_MergeColumns
-
- MI_InsertFx
- MI_NewOutputFx
-
- MI_InsertSceneFrame
- MI_RemoveSceneFrame
-
- MI_LipSyncPopup
-
- MI_RemoveEmptyColumns
-
-
- MI_Reverse
- MI_Swing
- MI_Random
- MI_Increment
- MI_Dup
-
-
- MI_Reframe1
- MI_Reframe2
- MI_Reframe3
- MI_Reframe4
-
- MI_Rollup
- MI_Rolldown
- MI_TimeStretch
-
- MI_CreateBlankDrawing
- MI_Duplicate
- MI_MergeFrames
- MI_CloneLevel
-
-
- MI_Play
- MI_Pause
- MI_Loop
-
- MI_FirstFrame
- MI_LastFrame
- MI_PrevFrame
- MI_NextFrame
-
- MI_PrevDrawing
- MI_NextDrawing
-
-
- MI_PreviewSettings
- MI_Preview
- MI_SavePreviewedFrames
-
- MI_OutputSettings
- MI_Render
-
- MI_FastRender
-
-
- MI_ViewTable
- MI_ViewCamera
- MI_ViewColorcard
- MI_ViewBBox
-
- MI_SafeArea
- MI_FieldGuide
- MI_ViewRuler
- MI_ViewGuide
-
- MI_TCheck
- MI_ICheck
- MI_PCheck
- MI_BCheck
- MI_GCheck
- MI_ACheck
-
-
-
- MI_DockingCheck
-
- MI_ResetRoomLayout
-
-
- MI_OpenCommandToolbar
- MI_OpenToolbar
- MI_OpenToolOptionBar
-
- MI_OpenStyleControl
- MI_OpenPalette
- MI_OpenStudioPalette
- MI_OpenColorModel
-
- MI_OpenComboViewer
- MI_OpenLevelView
-
- MI_OpenXshView
- MI_OpenTimelineView
- MI_OpenFunctionEditor
- MI_OpenSchematic
- MI_FxParamEditor
- MI_OpenFilmStrip
-
- MI_OpenFileBrowser
-
- MI_OpenTasks
- MI_OpenTMessage
- MI_OpenHistoryPanel
- MI_StartupPopup
-
- MI_MaximizePanel
- MI_FullScreenWindow
-
-
- MI_OpenOnlineManual
- MI_OpenWhatsNew
- MI_OpenCommunityForum
-
- MI_OpenReportABug
-
-
- MI_FlashGuide
-
- MI_About
-
-
diff --git a/stuff/profiles/layouts/rooms/Animate/room1.ini b/stuff/profiles/layouts/rooms/Animate/room1.ini
deleted file mode 100644
index b5cf86b9a..000000000
--- a/stuff/profiles/layouts/rooms/Animate/room1.ini
+++ /dev/null
@@ -1,32 +0,0 @@
-[room]
-pane_0\name=ToolBar
-pane_0\geometry=@Rect(0 60 34 927)
-pane_1\name=FilmStrip
-pane_1\geometry=@Rect(1798 60 122 800)
-pane_1\vertical=1
-pane_1\showCombo=1
-pane_1\navigator=1
-pane_2\name=ToolOptions
-pane_2\geometry=@Rect(0 30 1920 26)
-pane_3\name=LevelPalette
-pane_3\geometry=@Rect(38 420 220 440)
-pane_3\toolbarOnTop=0
-pane_3\toolbarVisibleMsk=3
-pane_3\variableWidth=1
-pane_3\viewtype=1
-pane_4\name=StyleEditor
-pane_4\geometry=@Rect(38 60 220 360)
-pane_4\isVertical=true
-pane_4\visibleParts=7
-pane_4\splitterState=@ByteArray(\0\0\0\xff\0\0\0\1\0\0\0\2\0\0\0\xd2\0\0\0\34\1\xff\xff\ff\xff\1\0\0\0\2\0)
-pane_5\name=Timeline
-pane_5\geometry=@Rect(0 860 1920 217)
-pane_5\orientation=LeftToRight
-pane_5\frameZoomFactor=60
-pane_6\name=SceneViewer
-pane_6\geometry=@Rect(264 60 1656 800)
-pane_6\viewerVisibleParts=3
-hierarchy="-1 1 [ 7 2 [ 0 [ 4 3 ] 6 5 1 ] ] "
-name=Drawing
-pane_7\name=CommandBar
-pane_7\geometry=@Rect(0 0 1920 26)
diff --git a/stuff/profiles/layouts/rooms/Default/layouts.txt b/stuff/profiles/layouts/rooms/Default/layouts.txt
index 1414e9aae..191370cd4 100644
--- a/stuff/profiles/layouts/rooms/Default/layouts.txt
+++ b/stuff/profiles/layouts/rooms/Default/layouts.txt
@@ -1 +1,4 @@
room1.ini
+room2.ini
+room3.ini
+room4.ini
diff --git a/stuff/profiles/layouts/rooms/Default/menubar_template.xml b/stuff/profiles/layouts/rooms/Default/menubar_template.xml
index 2ba56cc99..af9eb0fd7 100644
--- a/stuff/profiles/layouts/rooms/Default/menubar_template.xml
+++ b/stuff/profiles/layouts/rooms/Default/menubar_template.xml
@@ -8,6 +8,10 @@
MI_OpenRecentScene
MI_RevertScene
+
+ MI_ImportFlashVector
+ MI_ExportFlash
+
MI_LoadFolder
MI_LoadSubSceneFile
@@ -247,9 +251,12 @@
MI_OpenOnlineManual
MI_OpenWhatsNew
MI_OpenCommunityForum
+ MI_OpenDiscord
+ MI_OpenWebsite
MI_OpenReportABug
+
MI_FlashGuide
MI_About
diff --git a/stuff/profiles/layouts/rooms/Default/room1.ini b/stuff/profiles/layouts/rooms/Default/room1.ini
index aa7a565a2..b5cf86b9a 100644
--- a/stuff/profiles/layouts/rooms/Default/room1.ini
+++ b/stuff/profiles/layouts/rooms/Default/room1.ini
@@ -27,6 +27,6 @@ pane_6\name=SceneViewer
pane_6\geometry=@Rect(264 60 1656 800)
pane_6\viewerVisibleParts=3
hierarchy="-1 1 [ 7 2 [ 0 [ 4 3 ] 6 5 1 ] ] "
-name=Adobe Animate
+name=Drawing
pane_7\name=CommandBar
pane_7\geometry=@Rect(0 0 1920 26)
diff --git a/stuff/profiles/layouts/rooms/Animate/room2.ini b/stuff/profiles/layouts/rooms/Default/room2.ini
similarity index 100%
rename from stuff/profiles/layouts/rooms/Animate/room2.ini
rename to stuff/profiles/layouts/rooms/Default/room2.ini
diff --git a/stuff/profiles/layouts/rooms/Animate/room3.ini b/stuff/profiles/layouts/rooms/Default/room3.ini
similarity index 100%
rename from stuff/profiles/layouts/rooms/Animate/room3.ini
rename to stuff/profiles/layouts/rooms/Default/room3.ini
diff --git a/stuff/profiles/layouts/rooms/Animate/room4.ini b/stuff/profiles/layouts/rooms/Default/room4.ini
similarity index 100%
rename from stuff/profiles/layouts/rooms/Animate/room4.ini
rename to stuff/profiles/layouts/rooms/Default/room4.ini
diff --git a/stuff/profiles/layouts/rooms/OpenToonz/menubar_template.xml b/stuff/profiles/layouts/rooms/OpenToonz/menubar_template.xml
index a35f9d5c1..acf7a474a 100644
--- a/stuff/profiles/layouts/rooms/OpenToonz/menubar_template.xml
+++ b/stuff/profiles/layouts/rooms/OpenToonz/menubar_template.xml
@@ -341,6 +341,8 @@
MI_OpenOnlineManual
MI_OpenWhatsNew
MI_OpenCommunityForum
+ MI_OpenDiscord
+ MI_OpenWebsite
MI_OpenReportABug
diff --git a/tests/flash_fixtures/README.md b/tests/flash_fixtures/README.md
new file mode 100644
index 000000000..ba6fcab38
--- /dev/null
+++ b/tests/flash_fixtures/README.md
@@ -0,0 +1,33 @@
+# Flash import test fixtures
+
+Minimal-but-valid sample files for every Flash/Animate format Flare imports, plus
+two checkers. These are deterministic inputs for exercising the Flash import
+pipeline (`flare/sources/flare/flashimport.cpp`, `common/flash/XFLReader`).
+
+| Fixture | Format | What it proves |
+|---------|--------|----------------|
+| `sample.swf` | Uncompressed FWS SWF (550×400 @ 24fps) | `readSwfHeader()` RECT/frameRate decode |
+| `sample.flv` | FLV header (video+audio) | `readFlvHeader()` magic + flags |
+| `sample.f4v` | ISO-BMFF `ftyp` box (`f4v `) | `readF4vHeader()` brand detection |
+| `sample.as` | ActionScript 3 source | `.as` text import path |
+| `sample_xfl/` | XFL directory + `DOMDocument.xml` | `XFLReader` directory parse |
+| `sample.fla` | ZIP wrapping the XFL document | FLA = ZIP → `extractZip` → XFL parse |
+| `sample.swc` | ZIP (`catalog.xml` + `library.swf`) | SWC catalog + embedded-SWF bitmap path |
+
+## Running the checks
+
+```sh
+# (re)generate the fixtures
+python generate_fixtures.py
+
+# verify each fixture meets the importer's format contract (exit 0 = all pass)
+python verify_fixtures.py
+```
+
+The matching **C++ parser unit tests** live in
+`flare/sources/flare/test_flashimport.cpp` (compile standalone against Qt5Core);
+they cover the same header/RECT/XML/ZipSlip/JSFL logic in-process.
+
+> These fixtures are intentionally tiny. They validate that each format is
+> recognised and its metadata parsed — not full visual fidelity, which is the
+> ongoing Flash-import work (see `doc/FLASH_SUPPORT.md`).
diff --git a/tests/flash_fixtures/generate_fixtures.py b/tests/flash_fixtures/generate_fixtures.py
new file mode 100644
index 000000000..7d216b5ea
--- /dev/null
+++ b/tests/flash_fixtures/generate_fixtures.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+"""Generate minimal-but-valid sample files for every Flash/Animate format Flare
+imports. These are deterministic test fixtures for the Flash import pipeline
+(flashimport.cpp / XFLReader). Run: python generate_fixtures.py
+
+Formats covered: FLA, XFL, SWF (uncompressed FWS), SWC, FLV, F4V, AS.
+"""
+import os, struct, zipfile, shutil
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+
+
+def build_uncompressed_swf(version=5, w_px=550, h_px=400, fps=24, frames=1) -> bytes:
+ """Minimal uncompressed FWS SWF: header + RECT + frameRate + frameCount.
+ Matches the layout flashimport.cpp::readSwfHeader expects."""
+ xmax, ymax = w_px * 20, h_px * 20 # twips
+ nbits = 1
+ while (1 << (nbits - 1)) <= xmax:
+ nbits += 1
+ total_bits = 5 + 4 * nbits
+ rect = bytearray((total_bits + 7) // 8)
+ bitpos = 0
+
+ def wbits(val, n):
+ nonlocal bitpos
+ for b in range(n - 1, -1, -1):
+ if (val >> b) & 1:
+ rect[bitpos // 8] |= 1 << (7 - (bitpos % 8))
+ bitpos += 1
+
+ wbits(nbits, 5)
+ wbits(0, nbits); wbits(xmax, nbits); wbits(0, nbits); wbits(ymax, nbits)
+ tail = bytes([0, fps, frames & 0xFF, (frames >> 8) & 0xFF])
+ body = bytes(rect) + tail
+ file_len = 8 + len(body)
+ hdr = b'FWS' + bytes([version]) + struct.pack('\n'
+ '\n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ '\n'
+)
+
+CATALOG_XML = (
+ '\n'
+ '\n'
+ ' \n'
+ ' \n'
+ ' \n'
+ '\n'
+)
+
+AS_SRC = (
+ 'package {\n'
+ ' import flash.display.Sprite;\n'
+ ' public class Main extends Sprite {\n'
+ ' public function Main() { trace("Hello from Flare fixture"); }\n'
+ ' }\n'
+ '}\n'
+)
+
+
+def main():
+ swf = build_uncompressed_swf()
+
+ # SWF
+ with open(os.path.join(HERE, 'sample.swf'), 'wb') as f:
+ f.write(swf)
+
+ # FLV: 9-byte header (version 1, video+audio) + PreviousTagSize0
+ with open(os.path.join(HERE, 'sample.flv'), 'wb') as f:
+ f.write(b'FLV' + bytes([1, 0x05, 0, 0, 0, 9]) + struct.pack('>I', 0))
+
+ # F4V: ftyp box (major brand 'f4v ', compat 'isom')
+ with open(os.path.join(HERE, 'sample.f4v'), 'wb') as f:
+ f.write(struct.pack('>I', 20) + b'ftyp' + b'f4v ' + struct.pack('>I', 0) + b'isom')
+
+ # AS
+ with open(os.path.join(HERE, 'sample.as'), 'w', encoding='utf-8') as f:
+ f.write(AS_SRC)
+
+ # XFL directory: DOMDocument.xml + LIBRARY/
+ xfl_dir = os.path.join(HERE, 'sample_xfl')
+ if os.path.isdir(xfl_dir):
+ shutil.rmtree(xfl_dir)
+ os.makedirs(os.path.join(xfl_dir, 'LIBRARY'))
+ with open(os.path.join(xfl_dir, 'DOMDocument.xml'), 'w', encoding='utf-8') as f:
+ f.write(DOMDOCUMENT)
+ # XFL marker file (Adobe writes a .xfl stub at the project root)
+ with open(os.path.join(xfl_dir, 'sample.xfl'), 'w', encoding='utf-8') as f:
+ f.write('PROXY-CS5\n')
+
+ # FLA: ZIP archive containing the XFL document at the root
+ with zipfile.ZipFile(os.path.join(HERE, 'sample.fla'), 'w', zipfile.ZIP_DEFLATED) as z:
+ z.writestr('DOMDocument.xml', DOMDOCUMENT)
+ z.writestr('LIBRARY/', '')
+
+ # SWC: ZIP with catalog.xml + library.swf
+ with zipfile.ZipFile(os.path.join(HERE, 'sample.swc'), 'w', zipfile.ZIP_DEFLATED) as z:
+ z.writestr('catalog.xml', CATALOG_XML)
+ z.writestr('library.swf', swf)
+
+ print('Generated fixtures in', HERE)
+ for name in sorted(os.listdir(HERE)):
+ p = os.path.join(HERE, name)
+ kind = 'dir ' if os.path.isdir(p) else 'file'
+ size = '' if os.path.isdir(p) else f'{os.path.getsize(p)} bytes'
+ print(f' [{kind}] {name} {size}')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/flash_fixtures/sample.as b/tests/flash_fixtures/sample.as
new file mode 100644
index 000000000..9e320c993
--- /dev/null
+++ b/tests/flash_fixtures/sample.as
@@ -0,0 +1,6 @@
+package {
+ import flash.display.Sprite;
+ public class Main extends Sprite {
+ public function Main() { trace("Hello from Flare fixture"); }
+ }
+}
diff --git a/tests/flash_fixtures/sample.f4v b/tests/flash_fixtures/sample.f4v
new file mode 100644
index 000000000..6e2f99de0
Binary files /dev/null and b/tests/flash_fixtures/sample.f4v differ
diff --git a/tests/flash_fixtures/sample.fla b/tests/flash_fixtures/sample.fla
new file mode 100644
index 000000000..0923e8106
Binary files /dev/null and b/tests/flash_fixtures/sample.fla differ
diff --git a/tests/flash_fixtures/sample.flv b/tests/flash_fixtures/sample.flv
new file mode 100644
index 000000000..c3b943710
Binary files /dev/null and b/tests/flash_fixtures/sample.flv differ
diff --git a/tests/flash_fixtures/sample.swc b/tests/flash_fixtures/sample.swc
new file mode 100644
index 000000000..1053b5034
Binary files /dev/null and b/tests/flash_fixtures/sample.swc differ
diff --git a/tests/flash_fixtures/sample.swf b/tests/flash_fixtures/sample.swf
new file mode 100644
index 000000000..9c3d003e4
Binary files /dev/null and b/tests/flash_fixtures/sample.swf differ
diff --git a/tests/flash_fixtures/sample_xfl/DOMDocument.xml b/tests/flash_fixtures/sample_xfl/DOMDocument.xml
new file mode 100644
index 000000000..d7829f767
--- /dev/null
+++ b/tests/flash_fixtures/sample_xfl/DOMDocument.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/flash_fixtures/sample_xfl/sample.xfl b/tests/flash_fixtures/sample_xfl/sample.xfl
new file mode 100644
index 000000000..c7a73e26a
--- /dev/null
+++ b/tests/flash_fixtures/sample_xfl/sample.xfl
@@ -0,0 +1 @@
+PROXY-CS5
diff --git a/tests/flash_fixtures/verify_fixtures.py b/tests/flash_fixtures/verify_fixtures.py
new file mode 100644
index 000000000..bf7f75159
--- /dev/null
+++ b/tests/flash_fixtures/verify_fixtures.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+"""Verify every Flash fixture is a valid input for Flare's importer, checking the
+exact contract flashimport.cpp / XFLReader rely on for each format.
+Run: python verify_fixtures.py (exit 0 = all pass)
+"""
+import os, struct, zipfile, sys
+import xml.etree.ElementTree as ET
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+passed = failed = 0
+
+
+def check(name, cond, detail=''):
+ global passed, failed
+ if cond:
+ passed += 1; print(f' PASS {name} {detail}')
+ else:
+ failed += 1; print(f' FAIL {name} {detail}')
+
+
+def read_swf_dims(data):
+ """Mirror readSwfHeader(): FWS/CWS/ZWS sig, RECT decode for uncompressed."""
+ if len(data) < 9 or data[0:1] not in (b'F', b'C', b'Z') or data[1:3] != b'WS':
+ return None
+ if data[0:1] != b'F':
+ return ('compressed', data[3])
+ first = data[8]; nbits = first >> 3
+ bitpos = 8 * 8 + 5
+ def rd(n):
+ nonlocal bitpos
+ v = 0
+ for _ in range(n):
+ v = (v << 1) | ((data[bitpos // 8] >> (7 - bitpos % 8)) & 1); bitpos += 1
+ return v
+ def rds(n):
+ v = rd(n)
+ return v - (1 << n) if v & (1 << (n - 1)) else v
+ xmin, xmax, ymin, ymax = rds(nbits), rds(nbits), rds(nbits), rds(nbits)
+ return (data[3], (xmax - xmin) // 20, (ymax - ymin) // 20)
+
+
+def main():
+ # SWF
+ swf = open(os.path.join(HERE, 'sample.swf'), 'rb').read()
+ dims = read_swf_dims(swf)
+ check('SWF FWS header + RECT', dims == (5, 550, 400), f'ver/w/h={dims}')
+
+ # FLV
+ flv = open(os.path.join(HERE, 'sample.flv'), 'rb').read()
+ check('FLV magic + flags', flv[0:3] == b'FLV' and (flv[4] & 0x05) == 0x05,
+ f'flags=0x{flv[4]:02x}')
+
+ # F4V
+ f4v = open(os.path.join(HERE, 'sample.f4v'), 'rb').read()
+ check('F4V ftyp box', f4v[4:8] == b'ftyp' and f4v[8:12] == b'f4v ',
+ f'brand={f4v[8:12]!r}')
+
+ # AS
+ src = open(os.path.join(HERE, 'sample.as'), encoding='utf-8').read()
+ check('AS source readable', 'class Main' in src and 'trace' in src)
+
+ # XFL directory
+ dom = os.path.join(HERE, 'sample_xfl', 'DOMDocument.xml')
+ check('XFL has DOMDocument.xml', os.path.isfile(dom))
+ root = ET.parse(dom).getroot()
+ check('XFL DOMDocument attrs', root.get('width') == '550' and
+ root.get('height') == '400' and root.get('frameRate') == '24',
+ f"{root.get('width')}x{root.get('height')}@{root.get('frameRate')}")
+
+ # FLA (ZIP)
+ with zipfile.ZipFile(os.path.join(HERE, 'sample.fla')) as z:
+ names = z.namelist()
+ check('FLA is ZIP w/ DOMDocument.xml', 'DOMDocument.xml' in names, str(names))
+ d2 = ET.fromstring(z.read('DOMDocument.xml'))
+ check('FLA DOMDocument parses', d2.get('width') == '550')
+
+ # SWC (ZIP)
+ with zipfile.ZipFile(os.path.join(HERE, 'sample.swc')) as z:
+ names = z.namelist()
+ check('SWC has catalog.xml + library.swf',
+ 'catalog.xml' in names and 'library.swf' in names, str(names))
+ cat = ET.fromstring(z.read('catalog.xml'))
+ comps = [e for e in cat.iter() if e.tag.endswith('component')]
+ check('SWC catalog lists component(s)', len(comps) >= 1, f'{len(comps)} comp')
+ lib = read_swf_dims(z.read('library.swf'))
+ check('SWC library.swf is valid SWF', lib == (5, 550, 400), f'{lib}')
+
+ print(f'\nResults: {passed} passed, {failed} failed')
+ return 1 if failed else 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())