From ee4bfbcf27f80061bedf0a0a8a9caac9b3597848 Mon Sep 17 00:00:00 2001 From: CharlieDoesStuff Date: Mon, 15 Jun 2026 20:26:13 +0100 Subject: [PATCH 1/6] Fix nightly AppImage, make Adobe Animate the default UI, add in-app Discord/Website links, rewrite README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppImage (issue: missing from Linux nightly): - linuxdeploy-plugin-qt / -plugin-appimage were downloaded from the linuxdeploy/linuxdeploy repo, which 404s — each tool lives in its own repo. This silently set TOOLS_OK=0 and skipped AppImage generation on every build. Download each tool from its correct repo, with retries. - Stop swallowing linuxdeploy failures with `|| true`; emit ::warning:: so a missing AppImage is visible in CI instead of silent. - Publish a standalone Flare-x86_64.AppImage release asset (not only buried inside the tarball) and document it in the release notes. - Applied to both nightly.yml and workflow_linux.yml. Default UI = Adobe Animate (#2, part of #47): - The "Default" room set was a stripped single-room layout merely named "Adobe Animate". The real 4-room Animate workspace (Drawing/Animation/ Rigging/Compositing, with Flash import/export in its menubar) lived in a separate "Animate" folder users had to switch to manually. - Promote the real Animate workspace to Default and remove the duplicate Animate folder. The classic layout remains available as the OpenToonz preset. This also surfaces the Flash Open/Export menu items by default, addressing the room-layout half of #47. In-app community links (#5): - New Help menu items "Join us on Discord..." and "Flare Website..." (MI_OpenDiscord / MI_OpenWebsite) wired through mainwindow + menubar + Default and OpenToonz menubar templates. README (#5): rewritten from the shallow OpenToonz copy into a real intro with feature list, Flash support status table, download table, and prominent Discord + website links. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/nightly.yml | 62 ++++- .github/workflows/workflow_linux.yml | 27 +- README.md | 158 +++++++---- flare/sources/flare/mainwindow.cpp | 18 ++ flare/sources/flare/mainwindow.h | 2 + flare/sources/flare/menubar.cpp | 2 + flare/sources/flare/menubarcommandids.h | 2 + .../layouts/rooms/Animate/layouts.txt | 4 - .../rooms/Animate/menubar_template.xml | 262 ------------------ .../profiles/layouts/rooms/Animate/room1.ini | 32 --- .../layouts/rooms/Default/layouts.txt | 3 + .../rooms/Default/menubar_template.xml | 7 + .../profiles/layouts/rooms/Default/room1.ini | 2 +- .../rooms/{Animate => Default}/room2.ini | 0 .../rooms/{Animate => Default}/room3.ini | 0 .../rooms/{Animate => Default}/room4.ini | 0 .../rooms/OpenToonz/menubar_template.xml | 2 + 17 files changed, 199 insertions(+), 384 deletions(-) delete mode 100644 stuff/profiles/layouts/rooms/Animate/layouts.txt delete mode 100644 stuff/profiles/layouts/rooms/Animate/menubar_template.xml delete mode 100644 stuff/profiles/layouts/rooms/Animate/room1.ini rename stuff/profiles/layouts/rooms/{Animate => Default}/room2.ini (100%) rename stuff/profiles/layouts/rooms/{Animate => Default}/room3.ini (100%) rename stuff/profiles/layouts/rooms/{Animate => Default}/room4.ini (100%) 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..ba0ec6ac6 100644 --- a/README.md +++ b/README.md @@ -2,62 +2,108 @@ FlareBanner +**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. +[![Discord](https://img.shields.io/discord/1500316971802296430?label=Discord&logo=discord&logoColor=white&color=5865F2)](https://discord.com/invite/JpeScW8Awa) +[![Website](https://img.shields.io/badge/website-flare--animate-orange)](https://flare-animate.github.io/website/) +[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue)](./LICENSE.txt) -[![Discord Server](https://discord.com/api/guilds/1500316971802296430/widget.png?style=banner2)](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. +## Download -You can configure a build directory from the repository root with a command such as: +Pre-built nightly binaries for **Windows**, **Linux** (AppImage + tarball), and +**macOS** (DMG) are published automatically on every push to `master`: -```sh -cmake -S flare/sources -B build -G "Ninja" -DCMAKE_BUILD_TYPE=Release -``` +➡️ **[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) | -and then build (limit parallel jobs on low-memory systems): +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. + +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 +112,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/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/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_NewScene - MI_LoadScene - MI_SaveAll - MI_SaveScene - MI_SaveSceneAs - MI_OpenRecentScene - MI_RevertScene - - - MI_ImportFlashVector - MI_ExportFlash - - MI_LoadFolder - MI_LoadSubSceneFile - - MI_ConvertFileWithInput - - MI_LoadColorModel - - - MI_NewProject - MI_ProjectSettings - - MI_SaveDefaultSettings - - - - MI_ImportMagpieFile - MI_ImportOCA - MI_ImportFlashVector - - - MI_ExportCurrentScene - MI_SoundTrack - MI_ExportXDTS - MI_ExportSXF - MI_ExportOCA - MI_ExportFlash - MI_ExportTvpJson - MI_ExportXsheetPDF - MI_ExportCameraTrack - - - - MI_RunScript - MI_OpenScriptConsole - - - MI_Preferences - MI_ShortcutPopup - - MI_ClearCacheFolder - - MI_Quit - - - 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 From 29924fb183e24832a9757d2e6ef062114ce2365c Mon Sep 17 00:00:00 2001 From: CharlieDoesStuff Date: Mon, 15 Jun 2026 20:51:38 +0100 Subject: [PATCH 2/6] docs: add Next2Flash integration plan, link from FLASH_SUPPORT Assessment + roadmap for merging Next2Flash (MIT SWF round-trip editor with a native AS3 decompiler) into Flare. A direct merge is infeasible (C++/Qt vs Python/JS/Electron), so the plan ports the native-friendly SWF reader/writer pieces and bridges AS3 as an optional FFmpeg-style helper, preserving Flare's zero-runtime-dependency promise for the common FLA-import case. Co-Authored-By: Claude Opus 4.8 --- doc/FLASH_SUPPORT.md | 9 +++ doc/NEXT2FLASH_INTEGRATION.md | 109 ++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 doc/NEXT2FLASH_INTEGRATION.md diff --git a/doc/FLASH_SUPPORT.md b/doc/FLASH_SUPPORT.md index 6cbfcb4da..a8e2003e4 100644 --- a/doc/FLASH_SUPPORT.md +++ b/doc/FLASH_SUPPORT.md @@ -95,3 +95,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. From 353207dcada58dbfeaf8132f2148cabdad611c8e Mon Sep 17 00:00:00 2001 From: CharlieDoesStuff Date: Mon, 15 Jun 2026 21:01:19 +0100 Subject: [PATCH 3/6] flash: reject absolute paths in ZIP extraction (Zip Slip hardening) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extractZip() stripped a leading '/' from archive entries, silently relativizing absolute paths under the output dir. A well-formed FLA/XFL/SWC never contains absolute entries, so reject them outright instead — clearer and stricter. Updated the standalone test mirror to match; the Flash parser test suite now passes 22/22 (was 21/22, the failing case being exactly this absolute-path entry). Co-Authored-By: Claude Opus 4.8 --- flare/sources/flare/flashimport.cpp | 7 ++++--- flare/sources/flare/test_flashimport.cpp | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/flare/sources/flare/flashimport.cpp b/flare/sources/flare/flashimport.cpp index 509a2e372..16244afa7 100644 --- a/flare/sources/flare/flashimport.cpp +++ b/flare/sources/flare/flashimport.cpp @@ -116,10 +116,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("..") || 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; From 2417c811693d18bd1445fa23725174943eda77c9 Mon Sep 17 00:00:00 2001 From: CharlieDoesStuff Date: Mon, 15 Jun 2026 21:04:19 +0100 Subject: [PATCH 4/6] test: add Flash import fixtures + verifier for all 7 formats Deterministic minimal-but-valid sample files (FLA, XFL, SWF, SWC, FLV, F4V, AS) plus generate_fixtures.py and verify_fixtures.py. verify checks each fixture against the exact format contract flashimport.cpp/XFLReader rely on (11/11 pass). Complements the in-process C++ parser tests in test_flashimport.cpp (22/22 pass after the Zip Slip fix). Co-Authored-By: Claude Opus 4.8 --- tests/flash_fixtures/README.md | 33 +++++ tests/flash_fixtures/generate_fixtures.py | 125 ++++++++++++++++++ tests/flash_fixtures/sample.as | 6 + tests/flash_fixtures/sample.f4v | Bin 0 -> 20 bytes tests/flash_fixtures/sample.fla | Bin 0 -> 470 bytes tests/flash_fixtures/sample.flv | Bin 0 -> 13 bytes tests/flash_fixtures/sample.swc | Bin 0 -> 386 bytes tests/flash_fixtures/sample.swf | Bin 0 -> 21 bytes .../flash_fixtures/sample_xfl/DOMDocument.xml | 13 ++ tests/flash_fixtures/sample_xfl/sample.xfl | 1 + tests/flash_fixtures/verify_fixtures.py | 93 +++++++++++++ 11 files changed, 271 insertions(+) create mode 100644 tests/flash_fixtures/README.md create mode 100644 tests/flash_fixtures/generate_fixtures.py create mode 100644 tests/flash_fixtures/sample.as create mode 100644 tests/flash_fixtures/sample.f4v create mode 100644 tests/flash_fixtures/sample.fla create mode 100644 tests/flash_fixtures/sample.flv create mode 100644 tests/flash_fixtures/sample.swc create mode 100644 tests/flash_fixtures/sample.swf create mode 100644 tests/flash_fixtures/sample_xfl/DOMDocument.xml create mode 100644 tests/flash_fixtures/sample_xfl/sample.xfl create mode 100644 tests/flash_fixtures/verify_fixtures.py 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 0000000000000000000000000000000000000000..6e2f99de08a82ec14dc90498eb56c1d5988825f5 GIT binary patch literal 20 ZcmZQzU=T?wsVqn{DN|qog3RLlTmUQT1i%0Q literal 0 HcmV?d00001 diff --git a/tests/flash_fixtures/sample.fla b/tests/flash_fixtures/sample.fla new file mode 100644 index 0000000000000000000000000000000000000000..0923e8106b7bb6aa594b22fced0e3289ca8caf90 GIT binary patch literal 470 zcmWIWW@Zs#U|`^2$XIbcrmfmJ?FW!Imyv;iA4t3S`?};Om*%GCmFQLE=J*Dj&TBRh z*z;U_&D)(iokE(c3@lAftqfhd%|77+Q`p}X8!}Jczb=vS=<>ODXDZ6NFK3wiUU*MC z-7;shjKs}bo~hA%&KYw31zP4uf4=?t=GDOoygFKIo?mGF@V6zSR&B{@jn#7xZoIhT z^k-$J-=Rh&GJNeXZcNPDeIRkZ`-LxuF6`zwlDM5ctn=6Ew|5hsZoa(k`G*2e{!_fk zt@~%4%IxQ4N%he7u5%JR9x&g#`)0!1`=li!eSup3pN@6EsduF literal 0 HcmV?d00001 diff --git a/tests/flash_fixtures/sample.flv b/tests/flash_fixtures/sample.flv new file mode 100644 index 0000000000000000000000000000000000000000..c3b94371046bd36ed4089f65b4915c8f9bb9afa3 GIT binary patch literal 13 ScmZ?s31ehsU|`?`Vg>*UK>+vw literal 0 HcmV?d00001 diff --git a/tests/flash_fixtures/sample.swc b/tests/flash_fixtures/sample.swc new file mode 100644 index 0000000000000000000000000000000000000000..1053b5034c61f25900deb1f9c373edadeec4d1de GIT binary patch literal 386 zcmWIWW@Zs#U|`^2$XIbc=8iK{<3u3u91wE@adKiwVorX#UPW$BZm%`pAp-%I-*b|- z+8UNLym2|jm|@XaIHx~l{o*%l#WgakM5Y*jPIKEnU*f^r@Oi?Ld#J^u#6?5~aiKZqbtVl@cO;30bf8c<^!4nJ&0p5&EA`H0w4m1c18W=$oQb?d{ hM7J8Eje((oaTkz@0clY0|09HcMt#o literal 0 HcmV?d00001 diff --git a/tests/flash_fixtures/sample.swf b/tests/flash_fixtures/sample.swf new file mode 100644 index 0000000000000000000000000000000000000000..9c3d003e454d6fa4d7c7cdecf86fc3cacc3a8b3f GIT binary patch literal 21 ccmZ<@4`vl*U|^_VV2x*B;9tPNAi>B003!bauK)l5 literal 0 HcmV?d00001 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()) From 3047ca7a5eb6aa83d5254caabf22cafd21ad3a13 Mon Sep 17 00:00:00 2001 From: CharlieDoesStuff Date: Mon, 15 Jun 2026 21:07:19 +0100 Subject: [PATCH 5/6] docs: credit Next2Flash merge in README Co-Authored-By: Claude Opus 4.8 --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index ba0ec6ac6..6aa955733 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,13 @@ vector & raster drawing, the xsheet and timeline, plastic & skeleton rigging, the GTS scanning tools, the effects (FX) schematic, motion tracking, and the script console. +### 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). + ## Download Pre-built nightly binaries for **Windows**, **Linux** (AppImage + tarball), and From 027c0b13266dd447cbf46b67a74993223c7d7dec Mon Sep 17 00:00:00 2001 From: CharlieDoesStuff Date: Mon, 15 Jun 2026 21:19:51 +0100 Subject: [PATCH 6/6] Fix Linux AppImage desktop file + support legacy binary FLA import Build fix (Linux AppImage): - The AppImage download fix worked and exposed the next failure: the appimage plugin validates the .desktop file and rejected Categories=...;Animation; because "Animation" is not a registered freedesktop category. Use registered categories (Graphics;2DGraphics;RasterGraphics;VectorGraphics;) and move the animation tag to the allowed X- extension form (X-Animation;). Legacy binary FLA (issue #47): - The FLA reported in #47 is an OLE2 / Compound File Binary Format document (Flash CS4 and earlier), not a ZIP/XFL. extractZip() failed on it with a misleading "invalid/corrupt ZIP" error. - Detect the OLE2 magic (D0CF11E0A1B11AE1) and route legacy FLAs to a dedicated path that tells the user exactly what the file is and how to convert it (re-save as CS5+/XFL in Animate), instead of the cryptic ZIP error. - Best-effort bitmap recovery: carve embedded images and validate each with QImage so a false-positive marker in entropy data never yields a broken image. On the #47 sample this recovers 5 real bitmaps. - Full binary-FLA timeline/symbol parsing (CFBF stream reassembly) is documented as future work alongside the Next2Flash merge. Co-Authored-By: Claude Opus 4.8 --- doc/FLASH_SUPPORT.md | 3 +- flare/sources/flare/flashimport.cpp | 86 ++++++++++++++++++- .../sources/xdg-data/io.github.Flare.desktop | 2 +- 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/doc/FLASH_SUPPORT.md b/doc/FLASH_SUPPORT.md index a8e2003e4..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 | diff --git a/flare/sources/flare/flashimport.cpp b/flare/sources/flare/flashimport.cpp index 16244afa7..0b154c860 100644 --- a/flare/sources/flare/flashimport.cpp +++ b/flare/sources/flare/flashimport.cpp @@ -40,6 +40,7 @@ #include #include +#include #include #include #include @@ -405,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 // @@ -800,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/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;