Skip to content

New feature: Image management (crop sizes + modern format optimization) - #289

Open
Castellon-ACM wants to merge 28 commits into
trunkfrom
image-management
Open

New feature: Image management (crop sizes + modern format optimization)#289
Castellon-ACM wants to merge 28 commits into
trunkfrom
image-management

Conversation

@Castellon-ACM

@Castellon-ACM Castellon-ACM commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #258.

Adds Image Management: control over which image sizes WordPress generates and at what dimensions, and automatic WebP/AVIF delivery on the frontend. Entirely a free-plugin feature, no PRO gating.

Size / crop management

  • Admin screen listing every registered image size (core + theme + plugins): width, height, crop mode, source.
  • Enable/disable individual sizes so disabled ones stop generating on upload.
  • Edit width/height/crop of existing sizes, including WordPress's built-in ones.
  • Define new custom sizes from the settings screen.
  • Max upload dimension (large-image downscaling), overriding core's big_image_size_threshold default, configurable or disable-able.
  • Estimated disk usage per size.

Modern format delivery

  • WebP/AVIF variant generation on upload, gated by runtime wp_image_editor_supports() capability detection per format — an unsupported format is hidden from the settings UI rather than offered broken.
  • Frontend <img>/srcset URL rewriting (no <picture> restructuring): WebP applied unconditionally when a variant exists; AVIF only when the visitor's Accept header confirms support.
  • Per-format quality setting.

Background bulk jobs (Action Scheduler)

The three bulk actions (Regenerate Thumbnails, Convert to modern formats, Clean Up Disabled Sizes) run on a real Action Scheduler background queue — one scheduled action per attachment — instead of a client-driven admin-ajax batch loop. This means:

  • Processing survives closing the browser tab (previously it didn't — navigating away aborted the whole run).
  • No bulk-timeout risk on large libraries (each attachment is its own unit of work).
  • Reopening the settings page resumes showing progress for a job still running (tracked in localStorage).

Found and fixed via live testing: a critical bug where the bulk-job callback was only registered inside an is_admin() guard, so Action Scheduler's real execution contexts (WP-Cron, WP-CLI) — none of which are is_admin() — never had the callback registered. Every single scheduled action failed silently with "no callbacks are registered", leaving the progress bar hung forever with no visible error. Fixed by loading that module unconditionally; re-verified live that jobs now actually reach complete status via real WP-Cron execution, with file-level proof (deleted thumbnail files reappearing with fresh timestamps after a real cron-driven run).

Cleanup behavior

  • Deleting an attachment deletes all its generated variants (crop sizes + WebP/AVIF).
  • Disabling a size/format only stops future generation — an explicit manual "clean up unused variants" bulk action reclaims existing disk space instead of auto-deleting on toggle.

Requirements bump

WordPress minimum raised to 5.8 (required for the image-format-output filters), reflected in AGENTS.md/frontblocks.php/readme.txt.

Test plan

  • 494 tests, 983 assertions passing (up from prior branch's baseline). New/updated coverage: menu_icon-equivalent size CRUD, format capability downgrade (all 4 support combinations), full sanitize_settings surface, and the Action Scheduler job system — starting a job schedules one action per image attachment (never non-images), job-status reports progress and cleans up on completion, and process_bulk_item() for all three job types including progress-counter correctness.
  • composer lint (phpcs) clean.
  • Live-verified in a real browser end to end, twice: once found the is_admin() bug via real WP-Cron execution (44 actions, 100% failure), then re-verified after the fix — 28 actions across two job runs, 0 failed, 0 pending, progress bar reaching "Done", and file-level proof of real work (deleted thumbnail/variant files reappearing with fresh timestamps after a cron-driven run).
Open WordPress Playground Preview

Castellon-ACM and others added 28 commits August 24, 2026 11:08
The bulk regenerate/convert/list AJAX endpoints required manage_options,
but the settings page itself only requires edit_theme_options. A user
able to reach the page (e.g. an editor with theme-options access) would
always get a 403 on the bulk-action buttons.
WordPress core has no knowledge of the modern-format variants this
module generates, so without explicit cleanup they were left orphaned
on disk in two cases: when their attachment is deleted, and when
variants are regenerated after a format/size change (e.g. switching
from "both" to "webp" left the old .avif files behind).
Disabling a size only stopped future generation — existing files for
that size (and any of its WebP/AVIF variants) stuck around forever, so
the disk-usage estimate in the sizes table never actually translated
into reclaimed space. Adds a third bulk action, following the same
batched-AJAX pattern as regenerate/convert, that deletes those files
and updates the attachment metadata.
Size names (including admin-editable custom size names) were
concatenated directly into innerHTML, so a name containing quotes or
angle brackets could break the row markup or inject arbitrary HTML.
Saved custom sizes are registered via add_image_size(), which puts
them into \$_wp_additional_image_sizes — the same global the main
table reads to list theme/plugin sizes. Once saved, a custom size
showed up both there (as a plain, non-editable row) and again in its
own editable "custom" row.
wp_content_img_tag only covers the_content/the_excerpt, so featured
images rendered via the_post_thumbnail() (a very common case for hero
images and archive cards) never got the <picture> rewrite. Reuses the
same filter_content_img_tag() logic via the post_thumbnail_html filter.
- Store generated variants per size, keyed by full MIME type, with each
  variant's file size recorded alongside its filename — a more
  self-describing structure than the previous short format-key map, and
  it captures the data needed to report real compression savings later.
- Default frontend delivery now rewrites the <img> src/srcset in place
  (AVIF preferred over WebP when both exist) instead of always wrapping
  in a <picture> element, since modern-format browser support is high
  enough that the extra markup usually isn't needed. srcset rewriting
  now covers every candidate URL, not just the one matching the
  filter's own src.
- The <picture>-element wrap is kept as an opt-in mode (via a new
  image_format_use_picture setting) for sites that want an explicit,
  guaranteed fallback.
- Original files are never modified or deleted by this module in
  either mode — only additional variant files are ever generated.
- New "Use a <picture> element for delivery" toggle, matching the
  Frontend rework: off (default) rewrites the img tag directly, on
  wraps it with an explicit fallback.
- Custom sizes can now be given a Label and flagged "Show in picker" so
  they appear, under that label, in the image-size dropdown shown when
  inserting or editing an image — the same dropdown WordPress core uses
  to list Thumbnail/Medium/Large/Full Size.
…eader

The issue explicitly rejects <picture>-element markup as a non-goal in
favor of direct src/srcset rewriting, but the delivery code supported
both and offered picture wrapping as an opt-in setting. Removed the
setting, the wrapping method, its CSS asset, and all related UI/docs.

Also fixed the modern-format selection logic to actually honor the
issue's stated behavior: WebP is rewritten unconditionally, AVIF only
when the visitor's Accept header confirms support for it. Previously
AVIF was always preferred over WebP whenever both variants existed,
with no request-time negotiation at all.
# Conflicts:
#	includes/Plugin_Main.php
#	readme.md
Image Management's format-output delivery relies on filters that
aren't reliable before 5.8. Updates the plugin header, readme.txt, and
AGENTS.md, which all still claimed 5.0.
…them

The 'Modern format' select still offered WebP/AVIF/Both even when the
server's image library couldn't produce one of them, only showing a
warning text below — meaning it was possible to pick and save a
broken option. The select now only lists options the server can
actually produce, and sanitize_settings() downgrades a saved target
that requires an unsupported format instead of trusting it blindly,
in case support changed since the settings page was last loaded.
The issue asks for a quality/compression setting per format; there was
only one shared image_format_quality slider. Replaced it with
image_format_quality_webp and image_format_quality_avif, each with
its own default (82 for WebP, 60 for AVIF — the same numeric quality
produces a noticeably smaller AVIF file at a visually comparable
result, so a shared default made AVIF either over-compressed or WebP
under-compressed). Updated generation code, settings UI, sanitization,
and docs accordingly.
Full requirement from the issue that had no implementation at all: on
upload, downscale the stored original if it exceeds a configured
width/height, so oversized source images don't bloat storage or slow
down thumbnail generation. Filters big_image_size_threshold, enabled
by default at 2048px (overriding core's own 2560px default),
configurable to a different value or disabled entirely to keep
full-size originals untouched.
Settings::render_section() only renders a section whose ID is
explicitly wired to a tab panel — there's no wildcard fallback. The
Image Management admin section (its own settings section, fields,
sizes table, and bulk actions) was registered via the normal
frontblocks_register_settings hook but no tab panel ever referenced
frontblocks_section_image_management, so the entire feature — despite
being fully implemented — was unreachable from the actual settings
screen. Added its own 'Image Management' tab and wired its panel to
the section, and added the section id to the full-width-card render
path alongside Maintenance/Cookie Notice/CPT (it has a custom sizes
table and bulk-action UI, not a simple toggle grid).

Verified in-browser: the tab now appears, renders the sizes table,
and the max-upload-dimension and per-format quality fields added in
prior commits are visible and functional.
…arget

The in_array() check correctly defaulted to 'none' via ?? when
image_format_target is absent from $posted, but the ternary's
true-branch then read $posted['image_format_target'] directly without
the same fallback — an undefined-array-key access whenever the target
key is missing from the request entirely (not just an unchecked
select, which always sends a value). Found while writing
AdminImageManagementTest.php.
Adds woocommerce/action-scheduler as a runtime Composer dependency
and requires its bootstrap file before plugins_loaded, matching its
own standard shared-library loading pattern: the bootstrap
self-registers a versioned instance via ActionScheduler_Versions and
only initializes the highest version found across every plugin that
bundles it, so multiple plugins can each vendor their own copy
without conflict.

vendor/ stays gitignored for development as before; the release
build (composer install --no-dev) already vendors and ships whatever
is in composer.json's require section, so this is bundled in the
shipped plugin the same way as any other runtime dependency would be.
Regenerate/convert/cleanup used to run as a client-driven admin-ajax
loop: the browser fetched all attachment IDs, then looped POSTing
batches of 5, updating a progress bar between calls. That meant
processing only continued while the tab stayed open — closing it,
navigating away, or a slow request timing out aborted the whole run
with no way to resume.

Bulk actions now schedule one Action Scheduler action per image
attachment (ajax_start_bulk_job()), each processed independently by
process_bulk_item() regardless of whether anyone still has the
settings page open. Progress is tracked in a small per-job option
(total/done), polled via ajax_bulk_job_status() and cleaned up once
the job completes.
Replaces the client-side batch loop with: start the job (schedules
everything server-side), then poll its status every 1.5s until done.
The active job is also tracked in localStorage, so reopening the
settings page (after closing the tab, or a simple reload) resumes
showing its progress — the job itself was never actually lost, only
the visible progress bar was, since processing runs independently on
the Action Scheduler queue either way.
Replaces the old per-batch AJAX tests with coverage for: starting a
job schedules one Action Scheduler action per image attachment
(never for non-image attachments) and seeds its progress option;
job-status reports progress and cleans up the option once complete;
an unknown job id and invalid job type are rejected; and
process_bulk_item() itself for each of the three job types
(regenerate/convert/cleanup), including that its progress counter
increments correctly, never exceeds the total, and silently no-ops
for an unknown job id.
Admin\ImageManagement was only instantiated inside the is_admin()
block, so its process_bulk_item() callback for
frontblocks_image_management_process_item was never registered in
the contexts Action Scheduler actually runs scheduled actions in —
WP-Cron and WP-CLI, neither of which is_admin(). Every single bulk
job action was permanently failing with "no callbacks are
registered", silently: the progress bar just hung forever at 0%
with no error surfaced.

Found via live testing: 44 real scheduled actions all failed
identically after triggering WP-Cron, confirmed via
wp_get_attachment_metadata() staying byte-identical before/after a
"regenerate thumbnails" run that appeared to run to completion in
the UI.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New feature: Image management (crop sizes + modern format optimization)

2 participants