# Image Compression Pipeline Design

## Context

Currently, Building and Unit photo uploads flow through `app/Traits/HandlesMediaPhotos.php` into spatie/laravel-medialibrary `documents` collections. Hard limits are enforced by `app/Rules/PhotoFileRules.php` (`max:10240`, `dimensions()->maxWidth(5000)->maxHeight(5000)`), reused by Building/Unit create/update requests and `addPhotos` endpoints. Models also restrict mime types to `image/jpeg`, `image/png`, `image/heif`, `image/heic`. The reservation on-arrival ID photo in `ReservationRequest.php` and `OnArrivalValidateRequest.php` has its own `max:10240` rule.

There is no server-side compression or optimization. Originals are stored as-uploaded, so the only way to keep storage reasonable is to reject large files.

## Goal

Accept any image upload regardless of file size or pixel dimensions, then compress/optimize it on ingest so persisted media is bounded in bytes. Apply the same treatment to Building photos, Unit photos, and the reservation on-arrival ID photo.

## Non-goals

- Client-side compression (browsers may still pre-compress, but the server no longer relies on it).
- New UI/admin screens.
- Changing the public API response shape for media URLs.
- Adding thumbnails or responsive image sets in this change.

## Decision log

| Question | Assumption/decision | Rationale |
|---|---|---|
| Keep raw originals? | No. Store only the compressed file. | Requirement says stored originals must be bounded in bytes without rejecting uploads. |
| HEIF/HEIC handling? | Decode and re-encode to WebP/JPEG. | Browsers generally cannot display HEIF; convert for compatibility. |
| Reservation ID photo? | Use the same compression. | Single consistent pipeline for all uploaded images. |
| Output format? | WebP at 85 % quality; JPEG fallback. | WebP gives the best size/quality ratio and is universally supported by modern clients. |
| Max output dimension? | 2560 px on the longest edge. | Large enough for gallery/detail views, small enough to bound file size. |
| Infra upload limits? | Document raising PHP/nginx to 50 MB minimum. | Removing the app cap only helps if the web server and PHP also allow large bodies. |

## Architecture

```
┌─────────────────┐     ┌──────────────────────┐     ┌─────────────────────────┐
│ Request photo   │────▶│ Validation (relaxed) │────▶│ ImageCompressor service │
│ (any size/type) │     │ image + mimes only   │     │ resize + encode + temp  │
└─────────────────┘     └──────────────────────┘     └─────────────────────────┘
                                                                │
                                                                ▼
                                          ┌───────────────────────────────────┐
                                          │ HandlesMediaPhotos::storePhotos() │
                                          │ addMedia(tempFile) → documents    │
                                          └───────────────────────────────────┘
```

### Components

1. **`App\Services\ImageCompressor`**
   - Public method: `compress(UploadedFile $file): UploadedFile`
   - Uses `intervention/image` v3.
   - Steps:
     1. Open source image.
     2. Auto-orient from EXIF.
     3. Downscale to max 2560 px on longest edge (preserve aspect ratio).
     4. Encode to WebP quality 85, or JPEG quality 85 if WebP fails.
     5. Write to a temp file and return a new `UploadedFile` wrapper.
   - On any failure (unsupported format, corrupt file, memory exhaustion), fall back to the original uploaded file and log a warning.

2. **`App\Rules\PhotoFileRules::forSinglePhoto()`**
   - Remove `max:10240`.
   - Remove `Rule::dimensions()->maxWidth(5000)->maxHeight(5000)`.
   - Update `mimes` to `jpg,jpeg,png,webp,heif,heic`.

3. **`App\Traits\HandlesMediaPhotos::storePhotos()`**
   - Inject `ImageCompressor` via constructor/service container.
   - For each `$photo['photo']`, call `compress()` before `addMedia()`.
   - Clean up temp files after the media is attached.

4. **`App\Models\Building` and `App\Models\Unit`**
   - Add `image/webp` to `acceptsMimeTypes(...)` in `registerMediaCollections()`.

5. **`App\Http\Requests\ReservationRequest` and `App\Http\Requests\OnArrivalValidateRequest`**
   - Drop `max:10240`.
   - Broaden `mimes` to `jpg,jpeg,png,webp,heif,heic`.

## Compression settings

| Setting | Value |
|---|---|
| Max long edge | 2560 px |
| Quality | 85 % |
| Output format | WebP (JPEG fallback) |
| EXIF orientation | Auto-apply and strip orientation data |
| Typical output size | 200 KB – 1.5 MB per image |

These values are centralized as constants in `ImageCompressor` so they can be tuned later without touching multiple files.

## Validation changes summary

- `PhotoFileRules::forSinglePhoto()`: `['required', 'image', 'mimes:jpg,jpeg,png,webp,heif,heic']`
- `ReservationRequest::photo`: `['required', 'image', 'mimes:jpg,jpeg,png,webp,heif,heic']`
- `OnArrivalValidateRequest::photo`: `['nullable', 'image', 'mimes:jpg,jpeg,png,webp,heif,heic']`
- `Building::registerMediaCollections()` and `Unit::registerMediaCollections()`: accept `['image/jpeg', 'image/png', 'image/heif', 'image/heic', 'image/webp']`

## Infra / deployment notes

Removing the application-level cap is necessary but not sufficient. The web server and PHP limits must also be raised:

- `php.ini` / pool config:
  ```ini
  upload_max_filesize = 50M
  post_max_size = 50M
  max_execution_time = 120
  memory_limit = 512M
  ```
- nginx:
  ```nginx
  client_max_body_size 50M;
  ```
- Laravel Forge / Ploi / Cloudways: update the site’s PHP upload limits and nginx config, then reload both services.
- For very large batches or slow connections, consider queueing compression or increasing `max_input_time`.
- HEIF/HEIC conversion requires the `imagick` PHP extension with libheif support. If only GD is available, HEIF uploads will fall back to the original file and a warning will be logged.

## Error handling

- Malformed images that pass the `image` validator but fail Intervention parsing are stored as-is and a warning is logged.
- Memory or timeout errors during compression fall back to the original file to avoid upload failures.
- Non-image files are still rejected at validation.

## Testing plan

### Unit tests

- `ImageCompressorTest` with generated fixtures:
  - Large JPEG (15 MB, 6000×4000) → output smaller, max edge ≤ 2560, mime `image/webp`.
  - Large PNG with transparency → output smaller, max edge ≤ 2560.
  - HEIF fixture (if available) or mock → converted to WebP/JPEG.
  - Corrupt image → falls back to original and logs warning.

### Feature tests

- `BuildingPhotoUploadTest`: upload a 15 MB / 8000 px image, assert 201 and media stored.
- `UnitPhotoUploadTest`: same.
- `BuildingAddPhotosTest`: same via the dedicated `addPhotos` endpoint.
- `ReservationOnArrivalPhotoTest`: upload a large ID photo and assert success.
- Assert existing small-image uploads still work unchanged.

## Files to change

- `composer.json` — add `intervention/image`.
- `app/Services/ImageCompressor.php` — new service.
- `app/Traits/HandlesMediaPhotos.php` — compress before `addMedia`.
- `app/Rules/PhotoFileRules.php` — relax rules.
- `app/Models/Building.php` — accept `image/webp`.
- `app/Models/Unit.php` — accept `image/webp`.
- `app/Http/Requests/ReservationRequest.php` — relax `photo` rule.
- `app/Http/Requests/OnArrivalValidateRequest.php` — relax `photo` rule.
- `docs/operations/image-upload-limits.md` — new deployment/infra note.
- `tests/Unit/Services/ImageCompressorTest.php` — new unit tests.
- `tests/Feature/*Photo*` — update/add feature tests as needed.

## Out of scope

- Thumbnails, responsive conversions, or CDN integration.
- Changing the public media URL format.
- Client-side pre-compression.
- Video uploads.
