Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/ci-linux-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Linux Real-Environment Test Suite (CI)

on:
push:
branches: [ "**" ]
pull_request:
branches: [ "**" ]
workflow_dispatch:

jobs:
test_linux:
name: Run Pytest Suite on Real Linux (Ubuntu)
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
lfs: true

- name: Install System Dependencies for Qt6 & Multimedia
run: |
sudo apt-get update
ALSA_PKG="libasound2t64"
if ! apt-cache show libasound2t64 >/dev/null 2>&1; then
ALSA_PKG="libasound2"
fi
sudo apt-get install -y \
libgl1-mesa-dev \
libegl1 \
libxkbcommon-x11-0 \
libxcb-cursor0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-randr0 \
libxcb-render-util0 \
libxcb-shape0 \
libxcb-xfixes0 \
libxcb-xinerama0 \
$ALSA_PKG \
libpulse0 \
ffmpeg

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install Dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install pytest pytest-qt

- name: Run Pytest Matrix in Offscreen Mode
env:
QT_QPA_PLATFORM: offscreen
run: |
pytest -v tests/
Comment on lines +12 to +59
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,44 @@ This project adheres to Semantic Versioning (`MAJOR.MINOR.PATCH`).

---

## [1.7.12] - 2026-10-23

### Studio-Grade Visual Quality, Cross-Platform POSIX Paths, Project Persistence Engine & Portable Distribution

This release introduces high-fidelity canvas ingestion preserving native multi-megapixel asset resolution, studio-grade FFmpeg video rendering with visually lossless encoding, complete cross-platform path centralization and POSIX execution permissions for Linux, a triple-layer self-healing project persistence architecture that permanently eliminates image loss and engine freezing, and a standalone portable diagnostic packaging pipeline.

#### Studio-Grade Visual Fidelity & Video Export Engine
* **High-Fidelity Canvas Ingestion (4096px Safety Ceiling):** Eliminated aggressive downscaling of raw assets to 1080p upon canvas insertion in `AnimatedImageItem`. Native resolution is now preserved up to an industrial 4096px safety ceiling, allowing standard 1080p, 2K, and 4K assets to maintain pristine sharpness during zoom and transformation.
* **Studio-Grade FFmpeg Encoding:** Upgraded video export parameters in `FFmpegManager` from default high compression to visually lossless studio settings (`-crf 17`, `-tune animation`, `-preset medium`, `-pix_fmt yuv420p`, `-color_range pc`), eliminating banding artifacts, muddy gradients, and compression blur on animated vector strokes.
* **Premultiplied Alpha Anti-Aliasing:** Enforced `Format_ARGB32_Premultiplied` across compositing and scribble mask buffers, permanently fixing jagged pixelated borders on transparent PNG graphics.

#### Cross-Platform Architecture & POSIX Permissions (Linux/Windows)
* **Centralized Path Resolution (`utils/paths.py`):** Unified AppData and XDG Base Directory specification compliance (`~/.local/share/drawmotion` on Linux, `%LOCALAPPDATA%\DrawMotion` on Windows), removing ad-hoc hardcoded directory lookups across 5 core modules (`ffmpeg_manager.py`, `project_manager.py`, `library_widget.py`, `recovery_dialog.py`, `lifecycle_tracker.py`).
* **POSIX Execution Permissions (`+x`):** Enforced `stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH` on extracted FFmpeg binaries under POSIX/Linux environments, preventing `Permission denied` (`errno 13`) export crashes on Linux desktops and AppImage containers.
* **GitHub Actions CI Linux Workflow:** Introduced `.github/workflows/ci-linux-tests.yml` running headless pytest validation under Ubuntu 24.04 with Xvfb, auditing cross-platform paths, static asset case sensitivity against ext4 filesystems, and POSIX permissions.

#### Robust Project Persistence & Fail-Safe Restore Engine
* **Triple-Layer Asset Packaging Fallback:** Overhauled `ProjectManager._pack_and_save()` to eliminate asset dropouts when moving or saving projects:
- *Level 1 (Direct Disk Path):* Copies verified asset from disk.
- *Level 2 (Recursive Asset Library Scan):* Automatically searches library subdirectories (e.g., `task2/`) by filename if the source was relocated.
- *Level 3 (In-Memory Live Pixmap Extraction):* Captures the full-resolution uncorrupted `_pixmap` directly from RAM (`canvas_item._pixmap.save()`) if the original file was deleted from disk, making missing image assets mathematically impossible.
* **Smart Asset Resolver & Self-Healing Loader:** Upgraded `ProjectManager.load_project_file()` with `_resolve_asset_path()` to transparently repair legacy project archives referencing absolute or moved paths.
* **Zero-Wait Visual Display:** Reconstructed canvas items immediately display their native pixmap (`set_progress(1.0)`), eliminating blank/white screens on project open.
* **Non-Blocking Background Plan Calculation (60 FPS):** Wired `plan_started` and `plan_ready` signals and triggered asynchronous background vector calculations via `build_plan_async()` on a dedicated `QThread`, preventing UI/timeline freezing upon project loading.
* **Windows File-Lock Resilience (`[WinError 32]`):** Protected project cache extraction by catching locked file exceptions and dynamically isolating concurrent sessions into dedicated `ActiveProject_<uuid>` directories.

#### Diagnostic Standalone Distribution & Portable Packaging
* **Zero-Installer Portable Packaging:** Automated Nuitka standalone compilation producing `DrawMotion-Diagnostic-v1.7.12-Portable.zip` (~184 MB), bundled with all required PySide6 plugins, C++ runtimes, and documentation.
* **Bundled FFmpeg & Security Binaries:** Guaranteed inclusion of `bin/ffmpeg.exe` and `core/security_engine/bin/scanner.exe` directly inside the portable distribution root.
* **Interactive Diagnostic Launcher (`Debug_DrawMotion.bat`):** Integrated real-time log tailing displaying the last 15 lifecycle events from `startup_lifecycle.log` directly in the command prompt upon exit.
* **Offline-Resilient Updater:** Verified that `core/updater.py` and `SilentUpdateChecker` fail gracefully without crashes or modal blocks when running offline.

#### Automated Testing Suite
* **Comprehensive Automated Tests (`tests/test_project_assets_persistence.py`):** Added 5 dedicated test cases verifying direct disk packaging, library subfolder recovery, in-memory RAM pixmap extraction, instant visual rendering, and legacy project self-healing.
* **Complete Pytest Suite Validation:** All 23 test cases in the test suite pass cleanly (22 passed, 1 skipped for Linux POSIX permissions on Windows).

---

## [1.7.11] - 2026-10-19

### Microsoft Store Publishing Alignment, Open-Source Purge & Author Governance
Expand Down
84 changes: 58 additions & 26 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,75 @@
# Contributor Covenant Code of Conduct
# Contributing to DrawMotion

## Our Pledge
Thank you for your interest in improving DrawMotion. This guide explains how to contribute code, bug reports, and ideas, and what we expect from contributions.

We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
## Ways to Contribute

We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
- Report bugs
- Suggest features or improvements
- Fix bugs or implement features
- Improve documentation and translations
- Test pre-release builds and share feedback

## Our Standards
## Before You Start

Examples of behavior that contributes to a positive environment for our community include:
1. Check the existing issues and pull requests to make sure your idea or bug has not already been reported or fixed.
2. For anything larger than a small fix, open an issue first and describe what you want to change. This avoids duplicated effort and wasted work.
3. Read the README for how to build and run the project locally.

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
## Reporting Bugs

Examples of unacceptable behavior include:
Open an issue and include:

* The use of sexualized language or imagery, and sexual attention or advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
- A clear, short title
- Steps to reproduce the problem
- What you expected to happen and what actually happened
- Your app version and Windows version
- Screenshots, a screen recording, or a sample project file if possible

## Enforcement Responsibilities
## Suggesting Features

Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Open an issue that explains the problem you want to solve, not only the solution. Describe who benefits and give examples of how it would be used.

Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Submitting Code

## Scope
1. **Fork** the repository and create a branch from `main`. Use a descriptive name, such as `fix/export-crash` or `feature/layer-groups`.
2. **Keep changes focused.** One pull request should address one issue or feature. Avoid unrelated formatting or refactoring changes.
3. **Follow the existing code style** and structure of the project. Use clear names and add comments where the intent is not obvious.
4. **Test your changes.** Make sure the app builds and runs, existing features still work, and you have verified the change manually or with tests where they exist.
5. **Write clear commit messages.** Use a short summary line in the imperative (for example, "Fix crash when exporting empty project"), followed by details if needed.
6. **Open a pull request** against `main` and fill in the description:
- What the change does and why
- The issue it relates to (for example, `Closes #123`)
- How you tested it
- Screenshots or recordings for UI changes

This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces.
## Review Process

## Enforcement
- A maintainer will review your pull request and may ask for changes.
- Reviews focus on correctness, quality, maintainability, performance, and fit with the project's direction.
- Please respond to feedback and update your branch. Pull requests with no activity for a long time may be closed, and can be reopened later.
- Maintainers have the final decision on what is merged.

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project team at `conduct@easysoft.dev` or via the repository maintainers. All complaints will be reviewed and investigated promptly and fairly.
## Licensing and Ownership

## Attribution
- DrawMotion is released under the **GNU General Public License v3.0 (GPLv3)**. By submitting a contribution, you agree that it will be licensed under the same license.
- Only submit work that you wrote yourself or that you have the right to submit under a compatible license.
- Do not remove or change license headers or attribution notices.
- If you add third-party libraries, fonts, images, icons, or audio, state their source and license in the pull request. They must be compatible with GPLv3.

This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
## Content Guidelines

Everything added to the project, including code, comments, commit messages, documentation, and sample assets, should be professional and suitable for a general audience. Please avoid offensive, explicit, or deceptive content, and any code that is malicious or collects data without clear disclosure.

## Communication

- Keep discussions focused on the project and its technical details.
- Be respectful and constructive. Critique the work, not the person.
- Politics, religion, and other off-topic debates are not for project spaces.
- Be patient. Maintainers and contributors are often volunteers.

If someone's behavior is disrupting the project, please report it privately to `conduct@easysoft.dev`. Maintainers may edit or remove comments and contributions, and may limit participation, when needed to keep the project productive.

## Questions

If something here is unclear, open an issue with your question and we will help.
30 changes: 30 additions & 0 deletions asset_library/engine/svg_canvas_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ def __init__(self, filepath: str):
self._layer_name = "SVG Shape"
self.drawing_style = "Solid Reveal"
self.anim_type = "Draw"
self.entrance_mode = "draw" # "draw" or "motion"
self.motion_settings = {
"style": "hand_drag",
"direction": "left",
"duration": 1.5,
"easing": "smooth"
}

self._qimage = None
self._scribble_buffer = None
Expand Down Expand Up @@ -669,6 +676,11 @@ def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
if not getattr(self, "_hover_handle", None):
self.setCursor(Qt.CursorShape.OpenHandCursor)
# Capture live transform for motion graphics so enlarged scale / pos is preserved
self._motion_target_pos = QPointF(self.pos().x(), self.pos().y())
self._motion_target_scale = float(self.scale())
self._motion_target_rotation = float(self.rotation())
self._motion_target_opacity = float(self.opacity())

def to_dict(self):
return {
Expand All @@ -685,6 +697,13 @@ def to_dict(self):
"anim_type": self.anim_type,
"sfx_name": getattr(self, "sfx_name", "None"),
"sfx_volume": getattr(self, "sfx_volume", 100),
"entrance_mode": getattr(self, "entrance_mode", "draw"),
"motion_settings": getattr(self, "motion_settings", {
"style": "hand_drag",
"direction": "left",
"duration": 1.5,
"easing": "smooth"
}),
"svg_filepath": self.property("svg_filepath")
}

Expand All @@ -702,8 +721,19 @@ def from_dict(cls, data):
item._layer_name = data.get("layer_name", "SVG Shape")
item.drawing_style = data.get("drawing_style", "Solid Reveal")
item.anim_type = data.get("anim_type", "Draw")
item.entrance_mode = data.get("entrance_mode", "draw")
item.motion_settings = data.get("motion_settings", {
"style": "hand_drag",
"direction": "left",
"duration": 1.5,
"easing": "smooth"
})
item.sfx_name = data.get("sfx_name", "None")
item.sfx_volume = data.get("sfx_volume", 100)
item._motion_target_pos = QPointF(item.pos().x(), item.pos().y())
item._motion_target_scale = float(item.scale())
item._motion_target_rotation = float(item.rotation())
item._motion_target_opacity = float(item.opacity())

return item

Expand Down
3 changes: 2 additions & 1 deletion asset_library/ui/library_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ def import_svg(self):

# Copy to AppData
filename = os.path.basename(filepath)
appdata_dir = os.path.join(os.environ.get('APPDATA', ''), 'DrawMotion', 'assets', 'svg')
from utils.paths import get_app_data_dir
appdata_dir = os.path.join(get_app_data_dir(), 'assets', 'svg')
os.makedirs(appdata_dir, exist_ok=True)

target_path = os.path.join(appdata_dir, filename)
Expand Down
Binary file removed assets/drawing-hand.png
Binary file not shown.
Binary file removed assets/hand-mask.png
Binary file not shown.
Binary file added assets/hand_drag.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions build_assets/Debug_DrawMotion.bat
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,10 @@ echo %%LOCALAPPDATA%%\DrawMotion\Logs\startup_lifecycle.log
echo %%LOCALAPPDATA%%\DrawMotion\Logs\qt_messages.log
echo %%LOCALAPPDATA%%\DrawMotion\Logs\crash_faulthandler.log
echo ========================================================
if exist "%LOCALAPPDATA%\DrawMotion\Logs\startup_lifecycle.log" (
echo.
echo --- Last Startup Lifecycle Events ---
powershell -NoProfile -Command "Get-Content '%LOCALAPPDATA%\DrawMotion\Logs\startup_lifecycle.log' -Tail 15"
echo.
)
pause
2 changes: 1 addition & 1 deletion build_assets/msstore/AppxManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Identity Name="DrawMotion.DrawMotion"
ProcessorArchitecture="x64"
Publisher="CN=5FC00DE5-158E-431A-9660-1C8F36A4FEB0"
Version="1.7.11.0" />
Version="1.7.12.0" />
<Properties>
<DisplayName>DrawMotion</DisplayName>
<PublisherDisplayName>DrawMotion</PublisherDisplayName>
Expand Down
2 changes: 1 addition & 1 deletion build_assets/setup_script_github.iss
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[Setup]
SourceDir=..
AppName=DrawMotion
AppVersion=1.7.11
AppVersion=1.7.12
AppPublisher=EASY SOFT TEAM (Mohamed Fadl)
AppPublisherURL=https://github.com/DMotionX/DrawMotion
AppSupportURL=https://drawmotion-web.vercel.app/
Expand Down
17 changes: 12 additions & 5 deletions components/canvas/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ def __init__(self, workspace, scene, parent=None):
if self.viewport():
self.viewport().setMouseTracking(True)

# Enable high-quality rendering to prevent pixelation on small screens
# Enable high-quality rendering to prevent pixelation on small screens and high-DPI displays
from PySide6.QtGui import QPainter
self.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform)
hints = QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform
if hasattr(QPainter.RenderHint, 'LosslessImageRendering'):
hints |= QPainter.RenderHint.LosslessImageRendering
self.setRenderHints(hints)

def drawForeground(self, painter, rect):
super().drawForeground(painter, rect)
Expand Down Expand Up @@ -231,9 +234,13 @@ def _on_context_replace(self):
from PySide6.QtGui import QPixmap
path, _ = QFileDialog.getOpenFileName(self, "Replace Image", "", "Images (*.png *.jpg *.jpeg)")
if path:
self._context_item.replace_image(QPixmap(path))
self._context_item.filepath = path
self.workspace.items_changed.emit(self.workspace.user_items)
pix = QPixmap(path)
if not pix.isNull():
ok = self._context_item.replace_image(pix)
if ok:
self._context_item.image_path = path
self._context_item.filepath = path
self.workspace.items_changed.emit(self.workspace.user_items)

def _on_context_cut(self):
if hasattr(self, '_context_item') and self._context_item:
Expand Down
Loading
Loading