getgrav / getgrav/grav-plugin-api

Feature: optional server-side resize of oversized raster uploads

Open
#42 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
PHP
Stars
5
Forks
8
Avg merge
3d 15h
Merged PRs (30d)
2

Description

Split out of #41, which was closed once the immediate gap (admin-next ignoring the classic admin `pagemedia` resizer) was fixed.

Grav resizes oversized uploads in the browser, in two places: classic admin does it in Dropzone, and admin-next now does the same in Uppy, both reading `plugins.admin.pagemedia.resize_width` / `resize_height` / `resize_quality`. The form plugin has its own version for frontend forms via FilePond.

All of those are client-side, which makes them a convenience rather than a policy. Nothing resizes on the server, so a full-size original still lands in `user/pages` whenever the upload does not come from one of those widgets: a direct API call, grav-mcp, a sync or import script, a mobile client, or anything else talking to `/api/v1`. On a Git Sync site that original is then in the repo and every clone, forever.

### Proposal

An optional maximum edge applied in `HandlesMediaUploads::processUploadedFile()`, after the SVG sanitise and before it returns. That single point sits ahead of `onAdminAfterAddMedia` on all three upload paths (page media, site media, flex-object media) at once. **Off by default**, with 2560 pre-filled so enabling it is one toggle.

Off by default is deliberate rather than a hedge. Unlike WordPress, Grav never serves the file that was uploaded, since the theme resizes on demand, so an on-by-default maximum edge is a lossy, irreversible rewrite of someone's original that buys nothing back on the serving side. Sites where storage or a Git Sync repo is the constraint turn it on and get exactly the benefit.

### Constraints worth writing down up front

- It cannot run before `moveTo()`. PSR-7 gives no path to the temp file, so there is nothing on disk to open yet.
- Core's `ImageFile` provides the image library and `fixOrientation()`. Orientation must be applied before the resize, so the cap lands on the long edge of the upright image rather than of a phone photo on its side.
- GIF is excluded and will not be configurable: both the GD and Imagick adapters decode and write a single frame, so resizing an animated GIF would silently discard the animation. Animated WebP is excluded for the same reason.
- A 6000px decode is well over 100MB in GD, so dimensions need checking against the remaining `memory_limit` before anything is opened; if it will not fit, store as-is.
- If the resize fails for any reason (CMYK JPEG, missing GD, anything at all) the original is kept and the failure is logged. It must never turn into a failed upload: the file passed every security check and is already on disk, and an image library having a bad day is not a reason to throw it away.
- If the re-encode comes out larger than the original, keep the original.

Once this exists the browser-side resizes become a pure bandwidth optimisation rather than the only line of defence, which is the right division of labour.

Originally reported by @onetrev in #41.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start at HandlesMediaUploads::processUploadedFile() and trace moveTo(), SVG sanitisation, Core's ImageFile, and the onAdminAfterAddMedia flow across page, site, and flex-object uploads. Done means an opt-in maximum edge resizes eligible raster images safely, preserves originals on memory or re-encode failure, excludes GIF and animated WebP, and logs failures without rejecting uploads.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
api, backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.