{
  "slug": "bw-dev",
  "name": "BW Dev",
  "version": "1.12.0",
  "download_url": "https://plugins.bowden.works/wp-content/uploads/plugin-updates/bw-dev-1.12.0.zip",
  "download_hash": "sha256:9fb372973794bf064706738bfe2c5f94a0053c3c9db3c7d5543aa52d70bd19a8",
  "download_size": 444361,
  "requires": "6.0",
  "tested": "",
  "requires_php": "7.4",
  "last_updated": "2026-05-27",
  "homepage": "https://plugins.bowden.works/bw-dev/",
  "author": "Bowden Works",
  "description": "Bowden Works dev toolkit — admin columns, favicon, sticky elements, post-link blocks, YouTube embed, menu visibility, SVG uploads, admin notes, and Flywheel auto-update support in one actively-maintained plugin. Required for several site features. See Settings → BW Dev → About.",
  "changelog": "## [1.12.0] - 2026-05-27\n\n### Added\n- **Image Optimizer module** (`image_optimizer`, group `editor_admin`) — Phase 1: resize + compression. Hooks `wp_handle_upload_prefilter` so image uploads are processed BEFORE WordPress saves them, preventing 8-10MB AI-generated PNGs (Gemini, Midjourney, etc.) from filling client Media Libraries at the source. Pipeline: edge crops → resize (fit-to-bounds or cover-with-9-anchor) → JPEG compression (Imagick-first with `wp_get_image_editor` fallback). PNG transparency detection via `Imagick::getImageChannelRange( CHANNEL_ALPHA )` — real alpha keeps PNG output, decorative alpha (channel enabled but every pixel opaque) flattens to JPEG. Filename suffix `_bw` (never doubled). Skip conditions: non-image uploads, SVGs (handled by SVG Upload module), JPEGs already smaller than 512 KB AND within profile dimensions. Source replacement only happens if the processed output is actually smaller than the original. Master \"process new uploads\" toggle defaults OFF (separate from the Modules-tab enable) so module deployment to existing client sites doesn't surprise-mutate uploads. Bundled \"Web 1920\" default profile: 1920×1080 fit-to-bounds, JPEG q82, no edge crops, watermark disabled. Single-profile in v1; multi-profile is a v1.2.0 feature. Lifetime stats counter (images processed, bytes saved) shown in the settings tab with optional reset-on-save. Watermark removal (the crop.bowden.works Gemini-watermark trick) is stubbed in the data model but lands as Phase 2 in a follow-up release.\n- **Per-user admin-bar widget for Image Optimizer**: editors who can `upload_files` see an \"Optimizer\" admin-bar dropdown with three states — Active (full pipeline including watermark removal once Phase 2 ships), Resize only (skip watermark for real-photo uploads), Off (pass-through, for cases like \"I need this image pristine\"). State is sticky per-user via user meta `_bw_dev_image_optimizer_mode`. Widget is hidden when the feature is globally disabled. State change goes through a nonce-gated `admin-post.php?action=bw_dev_image_optimizer_set_mode` handler with `upload_files` capability check. Per-upload opt-out is \"set widget to Off → upload → set back\" — no separate per-upload UI in v1.\n\n### Fixed\n- **Image Optimizer: also hook `wp_handle_sideload_prefilter`** so programmatic upload paths (`wp media import` via wp-cli, remote-URL imports, theme demo importers, block-editor \"insert from URL\" flows) get the same optimization treatment as wp-admin Media Library uploads. Caught during dev-site smoke test when `wp media import` of an 18 MB PNG bypassed processing entirely. Both filters share the same `$file` shape and the same callback.\n- **Image Optimizer: stats counter now reliably ticks** on every processed upload. `record_stats()` was reading via the cached settings layer, which could be clobbered by a stale snapshot held by the BW_Dev_Plugin singleton in edge cases. Rewrote to go straight to `get_option()` → mutate → `update_option()`, then sync the in-memory cache.\n\n### Changed (Image Optimizer Phase 3 — multi-profile + UI refinement)\n- **Live preview now runs the full pipeline.** The preview previously showed only the watermark-removal step on the source image. It now applies (in order) watermark removal → edge crops → resize (fit/cover with anchor) → JPEG compression at the profile's quality — exactly matching the upload pipeline. So edge crops and resize-mode changes now show up in the After panel. Preview-side downscaling to 900 px max width is only applied as a final transport step *after* the profile's own resize, so smaller profile targets (Square 1080, etc.) render at their actual final dimensions instead of being further shrunk for the preview. Auto-refresh listener broadened to all profile fields (was watermark-only).\n- **Multi-profile model.** Replaced the single-profile design with named profiles. Each profile is a self-contained bundle (label, dimensions, fit/cover, anchor, quality, edge crops, watermark sub-block with its own enable). Editors pick which profile to apply from the admin-bar widget on a per-upload basis.\n- **Admin-bar widget rewritten** for dynamic profile lists. The 3-state (Active / Resize-only / Off) trichotomy is gone — replaced by \"Off\" + one entry per saved profile. The \"resize only\" use case is now expressed as \"create a profile with watermark disabled and pick that.\" Each user has a sticky per-session choice (`_bw_dev_image_optimizer_profile` user meta — replaces `_mode`). Falls back to the site-wide default profile when no per-user pick exists. Auto-migrates pre-multi-profile `_mode` values (`active`/`resize_only` → default profile; `off` stays off).\n- **Settings tab — tabbed profile editor with 2-column sticky preview.** Profiles live in a tab strip at the top of the section; clicking a tab switches the editor pane below. A `+ Add profile` button creates a new pane (named \"New profile N\" by default) — JS assigns a temp `_new_*` slug and the sanitizer reslugs from the label on save with collision handling. Per-pane: \"Use as default profile\" radio (all profiles share the same name so it's an exclusive selection), and a \"Delete this profile\" button (disabled when only one profile remains — at-least-one invariant). The Live Preview column lives in a sticky right-hand column (`position: sticky; top: 42px`) inside the active pane and JS-moves between panes on tab switch, so it's always in view while you adjust watermark fields on the active profile.\n- **Sample image is shared across profiles.** One picker at the top of the preview column; the chosen attachment is used for whichever profile you're currently editing.\n- **`active_profile` setting renamed `default_profile`** to better reflect its role: the profile applied to editors who haven't made a per-user pick yet (not \"the one always being applied\").\n- **Sanitize hardened**: handles add/edit/delete in a single form submit via `__deleted_profiles[]` hidden inputs; resolves `_new_*` temp slugs to label-derived slugs with `_2`, `_3`, ... collision suffixes; enforces at-least-one invariant by re-seeding the default profile if a submission would result in zero profiles; sanitizes `default_profile` to always reference a slug that survives the form save.\n\n### Added (Image Optimizer Phase 2 — watermark removal + live preview)\n- **Watermark removal step** in the upload pipeline. Algorithm ported from `crop.bowden.works` (`/srv/apps/opti/app.py`): covers a configurable bottom-right rectangle with a fill color sampled from outside or inside the box, optionally textured with bilinear-resized random noise, blended into the surrounding image via a corner-distance gradient mask (`1 - sqrt((1-u)² + (1-v)²)` feather pattern). Runs in 4-7 ms on a 1920×1047 image via Imagick (`getImageChannelRange()`-aware build).\n- **Per-profile watermark config** in `bw_dev_settings[image_optimizer][profiles][web_1920][watermark]`: `enabled` (default false), `w_pct` (1-50, default 11), `h_pct` (1-50, default 11), `sample` (`outside` / `inside`, default `outside`), `noise` (0-100, default 0).\n- **Settings tab — Watermark removal section** with enable checkbox, width/height percentage inputs, sample-from radio, and noise slider. Below it: a Live Preview area with a Media Library picker for choosing a sample image (typically a previously-uploaded watermarked image), and a side-by-side Before / After preview that **auto-re-renders within 300ms of any watermark-field change** — no save needed to iterate.\n- **AJAX endpoint** `bw_dev_image_optimizer_preview` (nonce + `manage_options` cap) that runs the watermark-removal algorithm on the chosen sample attachment with current form values, downscales to 900px max width for fast transport, and returns the result as a base64 data URL — keeps temp files off disk, no cleanup required.\n- **Admin-bar \"Active\" vs \"Resize only\" modes now functionally diverge.** Until Phase 2 they did the same thing because watermark removal didn't exist yet. Now: Active runs the full pipeline (resize + compress + watermark removal when the active profile has it on); Resize only runs resize + compress but skips the watermark step — for editors uploading real photos where the bottom-right may legitimately contain content they want to keep."
}
