# BW WebP — Specification

**Version:** 0.1.0 | **Last Updated:** 2026-04-30

## Purpose

Generate `.webp` versions of every image in the WordPress media library, and serve them automatically to browsers that support WebP, **without modifying or breaking the original files**. Optimised for speed on large libraries (10k+ images), where the existing free option (WebP Express) takes hours.

Audience: site owners and developers who want WebP delivery on existing sites with minimal disruption and a one-click bulk conversion that finishes in minutes, not hours.

## Requirements

### Functional

- **R1** — Bulk-convert every supported image (`.jpg`, `.jpeg`, `.png`, `.gif`) under `wp-content/uploads/` to `image.ext.webp` next to the original ("mingled" mode). Originals are never modified or deleted.
- **R2** — Serve `.webp` automatically when the browser sends `Accept: image/webp`, via `.htaccess` rewrite (Apache/LiteSpeed). Falls back transparently to the original on unsupported browsers.
- **R3** — Bulk conversion runs in parallel: N concurrent workers (default = `max(1, nproc - 1)`, capped at 8), via Action Scheduler with batch sizes of 50 images per scheduled action.
- **R4** — Auto-detect the best available converter at activation in priority order: `cwebp` binary → Imagick (with WebP support) → GD. The plugin records which converter is in use and exposes it on the admin page.
- **R5** — Maintain a manifest table (`{prefix}bw_webp_jobs`) with one row per source image: source path, source mtime, source size, status (`pending|converting|done|failed|skipped`), error message, conversion timestamp. The bulk scanner only queues files whose source mtime is newer than the last successful conversion (or absent from the manifest).
- **R6** — Convert new uploads automatically. Hook `wp_handle_upload` and `wp_generate_attachment_metadata` to enqueue conversion of the original file and every registered intermediate size.
- **R7** — Admin page under **Settings → BW WebP** with: detected converter, total/done/pending/failed counts, "Convert all" button, per-batch progress, quality slider (default 82), and a "Clear conversions" destructive action.
- **R8** — REST endpoints under `/wp-json/bw-webp/v1/`: `GET /status` (counts and current converter), `POST /scan` (rebuild queue), `GET /progress` (poll while bulk job runs). All require `manage_options` capability and a nonce.
- **R9** — WP-CLI command: `wp bw-webp convert [--all] [--workers=N] [--quality=Q]`, `wp bw-webp status`, `wp bw-webp clear [--yes]`. Bypasses PHP-FPM, so safe for very large libraries via SSH.
- **R10** — On deactivation: remove the `.htaccess` rewrite rules. On uninstall (`uninstall.php`): drop the manifest table and (opt-in via setting) delete generated `.webp` files.

### Non-functional

- WordPress 6.0+, PHP 7.4+.
- Must follow WordPress security best practices: prepared SQL, nonces on state-changing actions, capability checks, escaped output, sanitised input.
- Translatable (i18n): text domain `bw-webp`, `load_plugin_textdomain` on `plugins_loaded`.
- No hard dependency on other plugins. Action Scheduler is bundled (most major plugins ship it; we use it via `as_enqueue_async_action` only if available, otherwise fall back to WP-Cron events).
- Must not interfere with WebP Express, EWWW, ShortPixel, or other image plugins. On activation, detect them and refuse to install rewrite rules until the user disables them.
- `.htaccess` edits use WordPress's own `# BEGIN BW WebP` / `# END BW WebP` block markers (`insert_with_markers`) so they're cleanly removable.
- All converter binary invocations use `proc_open` with explicit argv (no shell expansion); paths are validated to exist under `wp-content/uploads/` before being passed.

## Acceptance Criteria

| Requirement | Acceptance | Status |
|---|---|---|
| R1 | Run bulk-convert on a 10k-image fixture; every `.jpg`/`.png`/`.gif` has a sibling `.webp`. Originals' mtimes unchanged. | ☐ |
| R2 | `curl -H "Accept: image/webp" https://site/wp-content/uploads/2024/01/foo.jpg` returns `Content-Type: image/webp`. Without the header it returns the original. | ☐ |
| R3 | On an 8-core box, 1k images convert in under 60s with default settings (vs. ~10 minutes on WebP Express). | ☐ |
| R4 | Activation log records detected converter. Forcing `cwebp` unavailable falls back to Imagick. Forcing both unavailable falls back to GD. | ☐ |
| R5 | Re-running bulk on a fully-converted library does zero conversion work (manifest hits). Touching one source file's mtime requeues only that file. | ☐ |
| R6 | Uploading a new image via Media Library produces a `.webp` next to the original within 30s. | ☐ |
| R7 | Admin page shows live progress during a bulk job; numbers match REST `/status` output. | ☐ |
| R8 | REST endpoints return 401 without nonce, 403 without `manage_options`, 200 with both. | ☐ |
| R9 | `wp bw-webp convert --all` on the dev site exits 0 and matches the admin page's counts. | ☐ |
| R10 | After uninstall with the cleanup option enabled, no `.webp` files and no manifest table remain. | ☐ |

## Out of scope (Phase 1)

- AVIF support (Phase 3).
- `<picture>` element rewriting in post content (Nginx/IIS users will need a server-config snippet).
- CDN integration (R2/S3/Cloudflare Images).
- Per-attachment regeneration UI.
- Lazy "WebP on demand" runtime conversion (we always do upfront conversion).
- Lossless / near-lossless modes (single quality slider for Phase 1).
