diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..5bd3c36 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,34 @@ +name: Deploy Docs + +on: + push: + branches: [main] + paths: ['docs/**', 'mkdocs.yml'] + workflow_dispatch: + +permissions: + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip install mkdocs-material + - run: mkdocs build + - uses: actions/upload-pages-artifact@v3 + with: + path: site + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 2f36114..79f8ce3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ .env .AFFINITY_PATH +# mkdocs build output +site/ + # User-specific files *.rsuser *.suo diff --git a/README.md b/README.md index 14d9083..b81c2a9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # Affinity Plugin Loader -> [!NOTE] -> *Information on WineFix has moved, see [WineFix/](WineFix#winefix)* - - A managed code plugin loader & injector hook for Affinity by Canva (Affinity v3). APL gives you a simple method to load custom code into Affinity and perform custom patches at runtime using the Harmony library. No more patching DLL files on disk. @@ -12,123 +8,35 @@ APL supports Windows and Linux (Wine). MacOS support is not planned at this time image -## Install - -1. Download `affinitypluginloader-vX.X.X-amd64.zip` from the [latest release](https://github.com/noahc3/AffinityPluginLoader/releases). -2. Extract the contents of the archive into your Affinity install directory - - This is `C:/Program Files/Affinity/Affinity/` by default on Windows, or - `path/to/wineprefix/drive_c/Program Files/Affinity/Affinity/` on Linux. - -That's it. On Windows, just launch AffinityHook.exe instead of Affinity.exe. On Linux, launch AffinityHook.exe -with Wine instead of Affinity.exe. - -Alternatively if you want your existing shortcuts to work without any changes, you can: +> [!TIP] +> 📖 **Full documentation is available at [apl.ncuroe.dev](https://apl.ncuroe.dev)** -- Rename `Affinity.exe` to `Affinity.real.exe` -- Rename `AffinityHook.exe` to `Affinity.exe` +## Quick Start -However, doing this means updates to Affinity may not work correctly or Affinity Hook may be removed on updates. -As such it's recommended you update your existing shortcuts or create new shortcuts for `AffinityHook.exe`. +1. Download `affinitypluginloader-vX.X.X-amd64.zip` from the [latest release](https://github.com/noahc3/AffinityPluginLoader/releases). +2. Extract into your Affinity install directory. +3. Launch `AffinityHook.exe` instead of `Affinity.exe`. +For detailed installation instructions, see the [Installation Guide](https://apl.ncuroe.dev/guide/installation/). ## Developing Plugins -Plugins extend `AffinityPlugin` and override stage methods to hook into Affinity's loading pipeline. APL calls your plugin at each stage as Affinity starts up: - -| Stage | Method | What's available | -|---|---|---| -| 0 – Load | `OnLoad` | Plugin discovered, settings initialized. No Affinity types yet. | -| 1 – Patch | `OnPatch` | Serif assemblies loaded. Apply Harmony patches here. | -| 2 – ServicesReady | `OnServicesReady` | Affinity's DI container and services initialized. | -| 3 – Ready | `OnReady` | Full runtime including native engine, tools, effects. | -| 4 – UiReady | `OnUiReady` | Main window loaded. Full UI tree available. | -| 5 – StartupComplete | `OnStartupComplete` | Splash hidden, app idle. Safe to show dialogs. | - -Each stage method receives an `IPluginContext` with: -- `Harmony` — shared Harmony instance for patching -- `Settings` — your plugin's settings store (if you defined settings) -- `Patch(description, action)` — apply a patch with automatic deferral if dependencies aren't loaded yet -- `Log` / `LogWarning` / `LogError` — logging helpers - -### Settings API - -Override `DefineSettings()` to declare configuration options. APL auto-generates a preferences page in Affinity's preferences dialog using native Affinity controls. - -```csharp -public override PluginSettingsDefinition DefineSettings() -{ - return new PluginSettingsDefinition("myplugin") - .AddSection("General") - .AddBool("my_toggle", "Enable feature", defaultValue: true, - description: "Description shown below the toggle.") - .AddEnum("my_choice", "Pick one", new List - { - new EnumOption("a", "Option A"), - new EnumOption("b", "Option B"), - }) - .AddSlider("my_slider", "Amount", 0, 100, defaultValue: 50); -} -``` - -Supported setting types: `Bool`, `String`, `Enum`, `Slider`, `DropdownSlider`. Layout elements like `AddSection`, `AddInlineText`, `AddInlineMutedText`, and `AddInlineXaml` are also available. Settings descriptions support basic markdown formatting. - -Settings are persisted as TOML in the `apl/config/` directory and can be overridden via environment variables (`APL__PLUGINID__KEY`). - -### Minimal Plugin Example - -```csharp -using HarmonyLib; -using AffinityPluginLoader; -using AffinityPluginLoader.Settings; - -public class MyPlugin : AffinityPlugin -{ - public override PluginSettingsDefinition DefineSettings() - { - return new PluginSettingsDefinition("myplugin") - .AddBool("enabled", "Enable patch", defaultValue: true); - } - - public override void OnPatch(Harmony harmony, IPluginContext context) - { - if (context.Settings.GetEffectiveValue("enabled")) - { - context.Patch("My patch", h => - { - // Use reflection to find target types, then apply Harmony patches - }); - } - } -} -``` - -See [WineFix/](WineFix/) for a full working example with multiple patches, settings, and deferred patching. - -### Building - -Build scripts are provided for Windows and Linux. +Plugins extend `AffinityPlugin` and use [Harmony](https://github.com/pardeike/Harmony) to patch Affinity methods at runtime. See the [Creating a Plugin](https://apl.ncuroe.dev/dev/creating-a-plugin/) guide for the full walkthrough, including the plugin lifecycle, settings API, and build instructions. -> [!TIP] -> Developing on Linux? Use `./docker-build.sh` to build inside a Docker container with all dependencies pre-configured. See `docker/Dockerfile` for the full list of build dependencies if you prefer to build on your host system. -> -> APL fully supports building on Linux via mingw-w64 and the dotnet SDK, you'll just need to grab a Windows SDK header and library from Wine. - -Use `./deploy.sh` to build and deploy directly to your Affinity install directory for testing. +## WineFix +WineFix is an APL plugin that fixes Wine-specific Affinity bugs. See [WineFix/](WineFix/) for an overview, or the [WineFix docs](https://apl.ncuroe.dev/winefix/) for full details. ## Licensing -Affinity Hook, Affinity Bootstrap, Affinity Plugin Loader, and the project solution configuration files in the root -directory of the repository are licensed under the MIT License except for the exemption noted below. You can -find a copy of the license in the LICENSE file under the directories for each respective project. +APL (AffinityHook, AffinityBootstrap, AffinityPluginLoader) is licensed under the **MIT License**. See the LICENSE file under each project directory. > [!WARNING] -> WineFix is offered under a different license. See [WineFix#Licensing](WineFix#Licensing) for information. - +> WineFix is offered under a different license. See [WineFix#Licensing](WineFix#licensing) for information. ### License Exemption -[Canva](https://github.com/canva) and it's subsidiaries are exempt from MIT licensing and may (at its option) instead license any source code authored for the Affinity Hook, Affinity Bootstrap, and Affinity Plugin Loader projects under the Zero-Clause BSD license. +[Canva](https://github.com/canva) and its subsidiaries are exempt from MIT licensing and may (at its option) instead license any source code authored for the Affinity Hook, Affinity Bootstrap, and Affinity Plugin Loader projects under the Zero-Clause BSD license. # Credits diff --git a/WineFix/README.md b/WineFix/README.md index 4f69c93..6cc8288 100644 --- a/WineFix/README.md +++ b/WineFix/README.md @@ -1,59 +1,45 @@ # WineFix -WineFix is an APL plugin which aims to patch bugs encountered when running Affinity on Linux under Wine using -runtime code patches. +WineFix is an APL plugin which patches bugs encountered when running Affinity on Linux under Wine using runtime code patches. -## Install +> [!TIP] +> 📖 **Full documentation at [apl.ncuroe.dev/winefix](https://apl.ncuroe.dev/winefix/)** -To install WineFix, download `apl-winefix-vX.X.X.zip` from the [latest release](https://github.com/noahc3/AffinityPluginLoader/releases) and extract the `apl/` directory and `d2d1.dll` file to your Affinity install directory. That is, `d2d1.dll` should be in the Affinity install directory next to your Affinity.exe, and `WineFix.dll` should be inside the `apl/plugins` folder in your Affinity directory after extraction. +## Install -## Included Patches +1. [Install APL](https://apl.ncuroe.dev/guide/installation/) first. +2. Download `winefix-vX.X.X.zip` from the [latest release](https://github.com/noahc3/AffinityPluginLoader/releases) and extract into your Affinity install directory. -As of now WineFix fixes the following bugs: +For detailed instructions, see the [WineFix Installation Guide](https://apl.ncuroe.dev/winefix/installation/). -- Fixed: Preferences fail to save on application exit -- Fixed: Vector path preview lines are drawn incorrectly and don't match the underlying stroke path -- Fixed: Color picker zoom preview displays a black image on Wayland (auto-detected; configurable) -- Fixed: Intermittent startup crash caused by parallel font enumeration (force-disabled by default) +## Included Patches -> [!WARNING] -> WineFix currently patches out the Canva sign-in dialog prompt when launching Affinity. This is temporary and the sign-in dialog prompt will be restored as soon as we have a known consistent way to fix the sign-in browser redirect and Affinity protocol handler. +- Preferences fail to save on application exit +- Vector path preview lines drawn incorrectly +- Color picker zoom preview displays a black image on Wayland +- Intermittent startup crash from parallel font enumeration +- Canva sign-in dialog patched out (temporary; pending protocol handler fix) ## Configuration -WineFix settings are configurable from Affinity's preferences dialog (under the WineFix tab) or via environment variables. - -| Setting | Default | Description | -|---|---|---| -| Color picker magnifier fix | Auto | Wayland zoom preview fix. Auto enables only on Wayland/XWayland. | -| Color picker sampling mode | Native | Native uses Affinity's color space; Exact picks the literal pixel color (sRGB). | -| Force synchronous font enumeration | Enabled | Disables parallel font loading to reduce startup crashes. May increase startup time. | - -Environment variable overrides use the format `APL__WINEFIX__KEY` (e.g. `APL__WINEFIX__COLOR_PICKER_MAGNIFIER_FIX=disabled`). +WineFix is configurable from Affinity's preferences dialog, TOML files, or environment variables. See the [Configuration Reference](https://apl.ncuroe.dev/winefix/configuration/) for all settings and options. ## Known Open Bugs -We are currently researching potential solutions for the following bugs: - - Accepting crash reporting causes permanent crash until prefs are cleared -- Embedded SVG document editor causes crash after being open for some amount of time +- Embedded SVG document editor crashes after being open for some time -We are open to resolving any Wine-specific bugs. Feel free to open an issue requesting a patch for any -particular bug you encounter. Just please keep in mind these bugs take time to research and develop patches for, -especially if the bug needs to be patched in native code. +We are open to resolving any Wine-specific bugs. Feel free to [open an issue](https://github.com/noahc3/AffinityPluginLoader/issues) requesting a patch. ## Licensing -WineFix is licensed under the terms of the GPLv2 except for the exclusion and exemption noted below. You can find a copy of the license in the LICENSE file. - -### License Exclusion +WineFix is licensed under **GPLv2**. See the [LICENSE](LICENSE) file. -WineFix includes source code from the Wine project for d2d1.dll under `WineFix/lib/d2d1`. In accordance with the original project, the code in this directory is instead licensed under the LGPLv2.1. A copy of this license can be found at `WineFix/lib/d2d1/LICENSE`. Changes have been applied to the d2d1 source code to implement a recursive cubic bezier subdivision algorithm to correct cubic bezier rendering in Affinity, and to allow building d2d1.dll standalone from the full Wine source code repository. +The bundled `d2d1.dll` source (under `WineFix/lib/d2d1/`) is licensed under **LGPLv2.1** per upstream Wine licensing. Changes have been applied to implement a cubic bezier subdivision algorithm and to allow building d2d1.dll standalone. ### License Exemption -[Canva](https://github.com/canva) and it's subsidiaries are exempt from GPLv2 licensing and may (at its option) instead license any source code authored for the WineFix project under the Zero-Clause BSD license. -- Due to requirements of the upstream Wine licensing, this exemption **does not apply** to the d2d1.dll implementation source code, ie. all code under `WineFix/lib/d2d1/` is excluded from this exemption. +[Canva](https://github.com/canva) and its subsidiaries are exempt from GPLv2 licensing and may (at its option) instead license any source code authored for the WineFix project under the Zero-Clause BSD license. This exemption **does not apply** to the d2d1.dll source code under `WineFix/lib/d2d1/`. # Credits diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..0efd760 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +apl.ncuroe.dev diff --git a/docs/dev/creating-a-plugin.md b/docs/dev/creating-a-plugin.md new file mode 100644 index 0000000..cf751d9 --- /dev/null +++ b/docs/dev/creating-a-plugin.md @@ -0,0 +1,254 @@ +# Creating a Plugin + +This guide walks through creating an APL plugin from scratch. + +!!! warning "Unstable API" + APL is experimental. While APL is still v0, expect breaking changes to the plugin API between updates. + + +## Prerequisites + +- .NET Framework 4.8 SDK (or the Docker build environment — see [Building](#building)) +- A reference to `AffinityPluginLoader.dll` and `0Harmony.dll` from an APL release + +## Project Setup + +Create a .NET Framework 4.8 class library. Add references to `AffinityPluginLoader.dll` and `0Harmony.dll`. + +## Minimal Plugin + +Plugins extend the `AffinityPlugin` base class. At minimum, override `OnPatch` to apply Harmony patches when Affinity's assemblies are loaded: + +```csharp +using HarmonyLib; +using AffinityPluginLoader; + +public class MyPlugin : AffinityPlugin +{ + public override void OnPatch(Harmony harmony, IPluginContext context) + { + context.Patch("My patch", h => + { + // Find and patch target methods here + }); + } +} +``` + +Place the compiled DLL in `apl/plugins/` and launch Affinity through `AffinityHook.exe`. + +## Plugin Metadata + +APL reads standard .NET assembly attributes to display plugin info in the preferences dialog: + +```xml + + My Plugin + 1.0.0 + Your Name + A short description of what this plugin does. + +``` + +The plugin ID is derived from the `AssemblyProduct` (or `AssemblyTitle`) attribute, lowercased with spaces replaced by hyphens. + +## Plugin Lifecycle + +APL loads plugins through a staged pipeline that mirrors Affinity's own startup sequence. Each stage corresponds to a virtual method on `AffinityPlugin` that you can override: + +| Stage | Method | What's Available | +|---|---|---| +| 0 – Load | `OnLoad` | Plugin discovered, settings initialized. No Affinity types yet. | +| 1 – Patch | `OnPatch` | Serif assemblies loaded. Apply Harmony patches here. | +| 2 – ServicesReady | `OnServicesReady` | Affinity's DI container and services initialized. | +| 3 – Ready | `OnReady` | Full runtime including native engine, tools, effects. | +| 4 – UiReady | `OnUiReady` | Main window loaded. Full UI tree available. | +| 5 – StartupComplete | `OnStartupComplete` | Splash hidden, app idle. Safe to show dialogs. | + +You only need to override the stages you use. Most plugins only need `OnPatch`. + +### Stage Details + +**Stage 0 – Load:** Called immediately after plugin discovery. Settings are already loaded from TOML. Use this for early setup that doesn't require any Affinity types. + +**Stage 1 – Patch:** The main patching stage. Serif assemblies (`Serif.Affinity`, `Serif.Interop.Persona`, etc.) are loaded. Apply Harmony patches here. Use `context.Patch()` for automatic deferral if a dependency isn't loaded yet (see [Patching with Harmony](#patching-with-harmony)). + +**Stages 2–5:** Triggered by Harmony postfixes on Affinity's internal lifecycle methods (`InitialiseServices`, `OnServicesInitialised`, `OnMainWindowLoaded`, `PostLoad`). Use these for work that depends on Affinity's runtime being progressively more initialized. + +### IPluginContext + +Every stage method receives an `IPluginContext` with: + +- `Harmony` — shared Harmony instance for patching +- `Settings` — your plugin's `SettingsStore` (null if you didn't define settings) +- `CurrentStage` — the current `LoadStage` enum value +- `Patch(description, action)` — apply a patch with automatic deferral (see below) +- `Log()` / `LogWarning()` / `LogError()` — logging helpers that tag output with your plugin ID + +## Patching with Harmony + +### Finding Patch Targets + +Use reflection to find types and methods in Affinity's assemblies at runtime: + +```csharp +public override void OnPatch(Harmony harmony, IPluginContext context) +{ + context.Patch("Fix something", h => + { + var assembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "Serif.Affinity"); + + var targetType = assembly?.GetType("Some.Namespace.TargetClass"); + var targetMethod = targetType?.GetMethod("TargetMethod", + BindingFlags.Public | BindingFlags.Instance); + + h.Patch(targetMethod, + prefix: new HarmonyMethod(typeof(MyPlugin), nameof(MyPrefix))); + }); +} + +static bool MyPrefix() => false; // Skip original method +``` + +### Automatic Patch Deferral + +Use `context.Patch()` instead of calling `harmony.Patch()` directly. If your patch throws a `TypeLoadException` (because a transitive dependency isn't loaded yet), APL automatically defers it and retries when new assemblies are loaded: + +```csharp +context.Patch("My deferred patch", h => +{ + // This is safe even if the target type's dependencies + // aren't loaded yet — APL will retry automatically + h.Patch(targetMethod, prefix: new HarmonyMethod(...)); +}); +``` + +### Patch Types + +Harmony supports several patch types. The most common ones are: + +- **Prefix** — runs before the original method. Can skip the original by returning `false`. +- **Postfix** — runs after the original method. Can modify the return value. +- **Transpiler** — rewrites the IL instructions of the original method. + +See the [Harmony patching documentation](https://harmony.pardeike.net/articles/patching.html) for the full list of patch types and detailed usage. + +## Settings API + +Override `DefineSettings()` to declare configuration options for your plugin. APL auto-generates a preferences page in Affinity's preferences dialog and persists values to a TOML file. + +```csharp +public override PluginSettingsDefinition DefineSettings() +{ + return new PluginSettingsDefinition("myplugin") + .AddSection("General") + .AddBool("my_toggle", "Enable feature", + defaultValue: true, + description: "Description shown below the toggle.") + .AddEnum("my_choice", "Pick one", new List + { + new EnumOption("a", "Option A"), + new EnumOption("b", "Option B"), + }) + .AddSlider("my_slider", "Amount", 0, 100, defaultValue: 50); +} +``` + +### Setting Types + +| Method | Control | Value Type | +|---|---|---| +| `AddBool` | Toggle switch | `bool` | +| `AddString` | Text input | `string` | +| `AddEnum` | Dropdown | `string` (one of the option values) | +| `AddSlider` | Slider | `double` | +| `AddDropdownSlider` | Dropdown with slider | `double` | + +### Setting Options + +All setting types accept these common parameters: + +| Parameter | Description | +|---|---| +| `key` | Unique key used in TOML and environment variables | +| `displayName` | Label shown in the preferences UI | +| `defaultValue` | Value used when no config file or override exists | +| `description` | Help text shown below the control. Supports basic markdown. | +| `restartRequired` | When `true`, shows a restart notice when the value changes | +| `infoMessage` | Tooltip text shown on an (i) icon next to the setting name | + +Slider types additionally accept `minimum`, `maximum`, and `precision` (decimal places). + +### Layout Elements + +You can add non-setting elements to organize the preferences page: + +| Method | Description | +|---|---| +| `AddSection(title)` | Section header. Also groups settings under a TOML table. | +| `AddInlineText(text)` | Plain text paragraph | +| `AddInlineMutedText(text)` | Dimmed/secondary text | +| `AddInlineXaml(xaml, dataContext)` | Custom XAML content | + +### Reading Settings + +Use the `SettingsStore` on `context.Settings` to read values: + +```csharp +// Get the effective value (respects environment variable overrides) +bool enabled = context.Settings.GetEffectiveValue("my_toggle"); + +// Get the stored value only (ignores env overrides) +string choice = context.Settings.GetValue("my_choice"); + +// Check if a setting is overridden by an environment variable +bool isOverridden = context.Settings.IsOverriddenByEnv("my_toggle"); +``` + +Use `GetEffectiveValue()` in most cases — it checks for environment variable overrides first, then falls back to the TOML/GUI value. + +### Environment Variable Overrides + +Any plugin setting can be overridden via environment variables: + +``` +APL____= +``` + +For example, a plugin with ID `myplugin` and setting key `my_toggle`: + +```bash +APL__MYPLUGIN__MY_TOGGLE=true +``` + +### Custom Preferences XAML + +For advanced cases, override `GetCustomPreferencesXaml()` to provide custom XAML for your plugin's preferences tab instead of the auto-generated UI. + +## Building + +### Linux + +Use the provided Docker build script for a reproducible build environment: + +```bash +./docker-build.sh +``` + +To build and deploy directly to your Affinity install directory for testing: + +```bash +./deploy.sh --set-affinity-path /path/to/affinity # one-time setup +./deploy.sh # build and deploy +``` + +### Windows + +```bash +build.bat +``` + +## Example + +See the [WineFix source code](https://github.com/noahc3/AffinityPluginLoader/tree/main/WineFix) for a complete working plugin with multiple patches, settings with sections, conditional patching based on settings, and deferred patch application. diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md new file mode 100644 index 0000000..c7c58b4 --- /dev/null +++ b/docs/guide/configuration.md @@ -0,0 +1,77 @@ +# Configuration + +APL and its plugins are configured through a preferences page inside Affinity's preferences dialog, TOML files on disk, or environment variables. + +## Preferences GUI + +APL injects a preferences page into Affinity's built-in preferences dialog (Edit → Preferences). Each plugin that defines settings gets its own tab. Changes made in the GUI are saved when the preferences dialog is closed. + +## TOML Files + +Settings are stored as TOML files in the `apl/config/` directory inside your Affinity install folder. Each plugin gets its own file named `.toml`. APL auto-generates these files with defaults and comments on first launch. + +For example, APL's own settings are in `apl/config/apl.toml`: + +```toml +# Logging + +# Enable logging to file +# Write APL and plugin log output to apl/logs/apl.latest.log. +# Values: true, false +file_logging = false + +# Log level +# Minimum severity level for log messages. +# Values: DEBUG, INFO, WARNING, ERROR, NONE +log_level = "INFO" + +# Advanced + +# Force WPF fallback controls +# Use standard WPF controls instead of Affinity's built-in controls for plugin preferences pages. +# Values: true, false +force_wpf_controls = false +``` + +You can edit these files while Affinity is closed. Changes take effect on next launch. + +## Environment Variables + +Any setting can be overridden via environment variables using the format: + +``` +APL____= +``` + +All parts are uppercase. For example: + +| Setting | Environment Variable | +|---|---| +| APL log level | `APL__APL__LOG_LEVEL=DEBUG` | +| APL file logging | `APL__APL__FILE_LOGGING=true` | + +Environment variable overrides take priority over both the GUI and TOML values. They are temporary — the override only applies while the variable is set. The underlying TOML/GUI value is not modified, so removing the environment variable restores the original setting on next launch. + +Boolean values accept `true`/`false` or `1`/`0`. + +## APL Settings Reference + +These are APL's own built-in settings (plugin ID: `apl`): + +| Key | Type | Default | Description | +|---|---|---|---| +| `file_logging` | bool | `false` | Write log output to `apl/logs/apl.latest.log`. Up to 5 rotated log files are kept. | +| `log_level` | enum | `INFO` | Minimum log severity. Values: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `NONE`. | +| `force_wpf_controls` | bool | `false` | Use standard WPF controls instead of Affinity's native controls for plugin preferences pages. | + +## Logging + +When launched from a terminal, APL writes log output to the console. Enable `file_logging` to also write to `apl/logs/apl.latest.log`. Log files are rotated automatically — up to 5 previous logs are kept (`apl.1.log` through `apl.5.log`). + +Log output includes timestamps, severity levels, and the source plugin: + +``` +[14:32:01] [INFO] [APL/Core] APL logging initialized +[14:32:01] [INFO] [APL/Core] Stage 0 complete: 2 plugins discovered, settings loaded +[14:32:02] [INFO] [APL/WineFix] Skipping ColorPicker Wayland fix (setting: auto) +``` diff --git a/docs/guide/installation.md b/docs/guide/installation.md new file mode 100644 index 0000000..241a0b6 --- /dev/null +++ b/docs/guide/installation.md @@ -0,0 +1,40 @@ +# Installation + +## Download + +Download `affinitypluginloader-vX.X.X-amd64.zip` from the [latest release](https://github.com/noahc3/AffinityPluginLoader/releases). + +## Install + +Extract the contents of the archive into your Affinity install directory: + +- **Windows:** `C:\Program Files\Affinity\Affinity\` +- **Linux (Wine):** `/drive_c/Program Files/Affinity/Affinity/` + +## Launch + +Launch `AffinityHook.exe` instead of `Affinity.exe`. On Linux, launch it with Wine the same way you would launch Affinity. + +!!! tip "Keep existing shortcuts working" + You can rename `Affinity.exe` to `Affinity.real.exe` and rename `AffinityHook.exe` to `Affinity.exe`. However, Affinity updates may overwrite or remove the hook if you do this. It's recommended to update your shortcuts to point to `AffinityHook.exe` instead. + +## Installing Plugins + +Place plugin DLLs in the `apl/plugins/` directory inside your Affinity install folder. APL discovers and loads them automatically on launch. + +## Directory Layout + +After installation, your Affinity directory will look like this: + +``` +Affinity/ +├── AffinityHook.exe # APL entry point +├── AffinityBootstrap.dll # Native bootstrap +├── AffinityPluginLoader.dll # Core plugin loader +├── 0Harmony.dll # Harmony library +├── Affinity.exe # Original Affinity executable +└── apl/ + ├── plugins/ # Place plugin DLLs here + ├── config/ # TOML settings files (auto-generated) + └── logs/ # Log files (when file logging is enabled) +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..01bee3b --- /dev/null +++ b/docs/index.md @@ -0,0 +1,12 @@ +# Affinity Plugin Loader + +A managed code plugin loader & injector hook for [Affinity](https://affinity.serif.com/) v3. Load custom code into Affinity and apply runtime patches using the [Harmony](https://github.com/pardeike/Harmony) library — no more patching DLL files on disk. + +Supports Windows and Linux (Wine). + +## Quick Links + +- [Installation](guide/installation.md) — Install APL and run it for the first time +- [Configuration](guide/configuration.md) — APL settings and how to configure them +- [Creating a Plugin](dev/creating-a-plugin.md) — Developer guide for building APL plugins +- [WineFix](winefix/index.md) — Plugin that fixes Wine-specific Affinity bugs diff --git a/docs/winefix/configuration.md b/docs/winefix/configuration.md new file mode 100644 index 0000000..a673e5c --- /dev/null +++ b/docs/winefix/configuration.md @@ -0,0 +1,56 @@ +# WineFix Configuration + +WineFix settings are configurable from Affinity's preferences dialog (under the WineFix tab), by editing the TOML file on disk, or via environment variables. + +For general information about how APL settings work (GUI, TOML files, environment variables), see [APL Configuration](../guide/configuration.md). + +## Settings Reference + +WineFix uses plugin ID `winefix`. Settings are stored in `apl/config/winefix.toml`. + +### Patches + +| Key | Type | Default | Restart Required | Description | +|---|---|---|---|---| +| `color_picker_magnifier_fix` | enum | `auto` | Yes | Wayland zoom preview fix. Replaces `CopyFromScreen` (which returns black on Wayland) with a `BitBlt` from the canvas window. | +| `color_picker_sampling_mode` | enum | `native` | No | Controls how the color picker samples color values. See [Sampling Modes](index.md#color-picker-sampling-modes). | + +#### `color_picker_magnifier_fix` + +| Value | Behavior | +|---|---| +| `auto` | Apply the fix only if Wayland or XWayland is detected (checks `WAYLAND_DISPLAY`). | +| `enabled` | Always apply the fix. | +| `disabled` | Never apply the fix. | + +!!! note + Enabling this on X11 desktop environments will prevent the zoom preview from displaying content outside the bounds of the canvas window. + +#### `color_picker_sampling_mode` + +| Value | Behavior | +|---|---| +| `native` | Use Affinity's built-in color sampling. Colors within the canvas use the document's native color space. The highlighted pixel in the zoom preview may differ slightly from the sampled value. | +| `exact` | Pick the exact color of the highlighted pixel in the zoom preview. Samples from a screen capture in sRGB. Not recommended for CMYK or wide-gamut documents. | + +### Crash Fixes + +| Key | Type | Default | Restart Required | Description | +|---|---|---|---|---| +| `force_sync_font_enum` | bool | `true` | Yes | Disable parallel font enumeration to reduce startup crashes. May increase startup time on systems with many fonts. | + +## Environment Variable Overrides + +Any WineFix setting can be overridden via environment variables using the format: + +``` +APL__WINEFIX__= +``` + +| Setting | Environment Variable | Example | +|---|---|---| +| Color picker magnifier fix | `APL__WINEFIX__COLOR_PICKER_MAGNIFIER_FIX` | `APL__WINEFIX__COLOR_PICKER_MAGNIFIER_FIX=disabled` | +| Color picker sampling mode | `APL__WINEFIX__COLOR_PICKER_SAMPLING_MODE` | `APL__WINEFIX__COLOR_PICKER_SAMPLING_MODE=exact` | +| Force synchronous font enumeration | `APL__WINEFIX__FORCE_SYNC_FONT_ENUM` | `APL__WINEFIX__FORCE_SYNC_FONT_ENUM=false` | + +Environment variable overrides take priority over both the GUI and TOML values. They are temporary — the override only applies while the variable is set. diff --git a/docs/winefix/index.md b/docs/winefix/index.md new file mode 100644 index 0000000..83a328a --- /dev/null +++ b/docs/winefix/index.md @@ -0,0 +1,45 @@ +# WineFix + +WineFix is an APL plugin that patches Wine-specific bugs in Affinity using runtime code patches. It applies [Harmony](https://github.com/pardeike/Harmony) IL transpilers and prefixes to fix issues at the .NET level, and ships a patched `d2d1.dll` to fix native rendering bugs. + +## Fixes + +- **Preferences fail to save on exit** — Transpiler replaces `HasPreviousPackageInstalled()` with `false`, which otherwise blocks the preferences save path under Wine. +- **Vector path preview lines drawn incorrectly** — Fixed via a patched `d2d1.dll` built from Wine 10.18 source with a cubic bezier subdivision algorithm that approximates cubic beziers using multiple quadratic beziers. +- **Color picker zoom preview shows a black image on Wayland** — Replaces `CopyFromScreen` (which returns black under Wayland) with a `BitBlt` from the canvas window. Auto-detected by default; [configurable](configuration.md). +- **Intermittent startup crash from parallel font enumeration** — Forces synchronous font loading to avoid an access violation in `libkernel.dll` during startup. Enabled by default; [configurable](configuration.md). + +!!! warning + WineFix currently patches out the Canva sign-in dialog prompt. This is temporary and will be restored once there is a consistent fix for the sign-in browser redirect and Affinity protocol handler. + +## Color Picker Sampling Modes + +The color picker has two sampling modes, configurable in [settings](configuration.md): + +- **Native** (default) — Uses Affinity's built-in color sampling pipeline. Colors are sampled in the document's native color space (sRGB, CMYK, wide-gamut, etc.). The highlighted pixel in the zoom preview may differ slightly from the actual sampled color value due to differences in how the zoom preview and the native picker resolve coordinates. +- **Exact** — Picks the literal pixel color shown in the zoom preview center. Samples from a screen capture in sRGB. The picked color always matches what you see in the zoom, but does not use the document's native color space. + +Use Native for color-accurate work (especially CMYK or wide-gamut documents). Use Exact if you want the picked color to always match the zoom preview. + + + +## Known Open Bugs + +These are under investigation and not yet patched: + +- Accepting crash reporting causes a permanent crash until preferences are cleared +- Embedded SVG document editor crashes after being open for some time + +We are open to resolving any Wine-specific bugs. Feel free to [open an issue](https://github.com/noahc3/AffinityPluginLoader/issues) requesting a patch — just keep in mind these bugs take time to research and develop patches for, especially when native code is involved. + +## Licensing + +WineFix is licensed under **GPLv2**. The bundled `d2d1.dll` source (under `WineFix/lib/d2d1/`) is licensed under **LGPLv2.1** per upstream Wine licensing. + +This is a different license from the rest of APL (MIT). See the [LICENSE](https://github.com/noahc3/AffinityPluginLoader/blob/main/WineFix/LICENSE) file for details. diff --git a/docs/winefix/installation.md b/docs/winefix/installation.md new file mode 100644 index 0000000..8c5c128 --- /dev/null +++ b/docs/winefix/installation.md @@ -0,0 +1,42 @@ +# WineFix Installation + +## Prerequisites + +[APL must be installed](../guide/installation.md) before installing WineFix. + +## Download + +Download `winefix-vX.X.X.zip` from the [latest release](https://github.com/noahc3/AffinityPluginLoader/releases), or download the combined `affinitypluginloader-plus-winefix.tar.xz` bundle which includes both APL and WineFix. + +## Install + +Extract the WineFix archive into your Affinity install directory. After extraction, the following files should be present: + +``` +Affinity/ +├── d2d1.dll # Patched Wine d2d1 (next to Affinity.exe) +└── apl/ + └── plugins/ + └── WineFix.dll # WineFix plugin +``` + +WineFix will be loaded automatically the next time you launch Affinity through AffinityHook. + +## Included Files + +For reference (e.g. if you need to uninstall WineFix manually), the release archive contains: + +| File | Location | Purpose | +|---|---|---| +| `WineFix.dll` | `apl/plugins/WineFix.dll` | WineFix plugin (Harmony patches) | +| `d2d1.dll` | `d2d1.dll` (next to `Affinity.exe`) | Patched Wine d2d1 library (cubic bezier fix) | + +Additionally, WineFix generates the following files on first launch: + +| File | Location | Purpose | +|---|---|---| +| `winefix.toml` | `apl/config/winefix.toml` | Settings file (auto-generated with defaults) | + +## Uninstall + +Delete the files listed above from your Affinity install directory. If you also want to remove the auto-generated config, delete `apl/config/winefix.toml`. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..2b36742 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,43 @@ +site_name: Affinity Plugin Loader +site_url: https://noahc3.github.io/AffinityPluginLoader/ +repo_url: https://github.com/noahc3/AffinityPluginLoader +repo_name: noahc3/AffinityPluginLoader + +theme: + name: material + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.sections + - navigation.expand + - content.code.copy + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - attr_list + +nav: + - Home: index.md + - User Guide: + - Installation: guide/installation.md + - Configuration: guide/configuration.md + - Developer Guide: + - Creating a Plugin: dev/creating-a-plugin.md + - WineFix: + - Overview: winefix/index.md + - Installation: winefix/installation.md + - Configuration: winefix/configuration.md