# How to Add AI-Powered Image Resizing to Your App — Copy-Paste Prompts Included

- Tool: App Features
- Last updated: July 2026

## TL;DR

AI-powered image resizing uses smart crop to keep faces and focal objects centered across multiple output sizes — not just a center crop. You need three pieces: an image picker, a Cloudinary or AWS Rekognition smart crop API called from a Supabase Edge Function, and a before/after comparison UI. With FlutterFlow you can ship a working build in 3–5 hours. Costs run $0–25/month at 100 users on Cloudinary's free tier; $89–115/month at 1,000 users on the Plus plan.

## What AI-Powered Image Resizing Actually Is

Standard image resizing crops from the center — which works for landscapes but cuts off faces in portrait photos and misses the subject in product shots. AI-powered smart crop uses computer vision to detect the most important region (faces, primary objects) and crops around it, so the output looks intentional rather than accidental. Cloudinary's g_auto transformation handles face detection and focal point selection server-side with a single URL parameter. AWS Rekognition returns face bounding boxes you use to calculate the crop coordinates in sharp.js. The real engineering work is the pipeline: image picker → user-scoped Supabase Storage upload → smart crop API call → status tracking in an image_jobs table → before/after comparison UI → signed URL download.

## Anatomy of the Feature

Seven components across UI, backend, and service layers. AI tools correctly generate the file picker and storage upload. The smart crop API routing and the before/after comparison widget are where first builds fail.

- **Image Picker** (ui): Flutter image_picker package on mobile (FlutterFlow native action) or HTML input[type=file] with accept='image/*' on web (Lovable). Accepts JPEG, PNG, and WEBP. Should apply imageQuality: 80 and maxWidth: 2048 constraints before upload to prevent out-of-memory crashes on high-resolution devices.
- **Upload Layer** (backend): Supabase Storage bucket receives the original image at path originals/{user_id}/{uuid}.jpg using a signed upload URL pattern. The client uploads directly to Storage; the Edge Function receives only the storage path, not the image bytes — this avoids the 6MB Edge Function request body limit.
- **Smart Crop API** (service): Cloudinary AI crop via the g_auto:face or g_auto:subject transformation URL parameter. The transformation URL is constructed server-side: https://res.cloudinary.com/{cloud_name}/image/upload/c_fill,g_auto:face,w_{width},h_{height}/{public_id}. Alternatively, AWS Rekognition DetectFaces returns bounding box coordinates that you pass to sharp.js for precise crop math.
- **Transformation Queue** (backend): A Supabase Edge Function orchestrates the pipeline: receives the storage path and output dimensions, constructs the Cloudinary transformation URL or calls AWS Rekognition, stores the result URL in image_jobs, and returns the processed image URL. Job status (pending/processing/done/failed) is tracked per row.
- **Before/After Slider** (ui): react-compare-slider on web displays the original and processed images side by side with a draggable divider. On Flutter, a custom Stack widget with a GestureDetector handles horizontal drag to reveal the processed image using a ClipRect with varying width.
- **Output Size Presets** (ui): A toggle button group with four presets — Square 1:1 (1080×1080), Story 9:16 (1080×1920), Banner 16:9 (1920×1080), Thumbnail 4:3 (800×600). The selected preset passes target width and height to the Transformation Queue Edge Function as query parameters.
- **Download/Share Button** (ui): On web, triggers a Supabase Storage createSignedUrl() (3600-second expiry) for the processed image path, then uses an anchor[download] tag to initiate the browser download. On Flutter, uses the flutter_share package to share the signed URL or saves to photo gallery via gallery_saver.

## Data model

One table tracks each image processing job with status and output paths. Run this in the Supabase SQL editor:

```sql
create table public.image_jobs (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  original_path text not null,
  output_path text,
  status text not null default 'pending'
    check (status in ('pending', 'processing', 'done', 'failed')),
  width int,
  height int,
  crop_mode text not null default 'g_auto:face',
  error_message text,
  created_at timestamptz not null default now()
);

alter table public.image_jobs enable row level security;

create policy "Users see own jobs"
  on public.image_jobs for select
  using (auth.uid() = user_id);

create policy "Users insert own jobs"
  on public.image_jobs for insert
  with check (auth.uid() = user_id);

create policy "Users update own jobs"
  on public.image_jobs for update
  using (auth.uid() = user_id);

create index image_jobs_user_status_idx
  on public.image_jobs (user_id, status, created_at desc);
```

The storage bucket policies should also be configured: originals bucket — authenticated users read and write their own path prefix ({user_id}/); processed bucket — authenticated users read their own path prefix only. Set up these bucket policies in the Supabase Dashboard under Storage → Policies.

## Build paths

### Lovable — fit 3/10, 4–6 hours

Lovable handles the Supabase Storage upload and Edge Function calling Cloudinary well for web. Best if your app is web-first and image uploads come from a desktop or camera on mobile web.

1. Create a new Lovable project, connect Lovable Cloud, and add your CLOUDINARY_CLOUD_NAME and CLOUDINARY_API_SECRET to Cloud tab → Secrets
2. Run the image_jobs data model SQL in Cloud tab → Database → SQL Editor
3. Paste the prompt below; Agent Mode will build the file input, Edge Function, before/after slider, and download button
4. Test file upload on the published URL — the Lovable preview iframe blocks file input interactions and Supabase Storage uploads
5. Verify the before/after slider loads both original and processed images before the divider becomes interactive

Starter prompt:

```
Build an AI image resizing feature. Use HTML file input (accept=image/*) for image selection. On file select: upload the image directly to Supabase Storage bucket 'originals' at path {user_id}/{uuid}.jpg using a signed upload URL (call the Edge Function to get the signed URL, then upload directly from client — do not send image bytes through the Edge Function). Then call an Edge Function 'process-image' with {storage_path, width, height, crop_mode}. The Edge Function should: insert a row into image_jobs (status: pending), update status to processing, construct a Cloudinary transformation URL: https://res.cloudinary.com/{cloud_name}/image/upload/c_fill,g_auto:faces,w_{width},h_{height},f_auto/{public_id} where public_id is derived from the storage path, fetch the transformed image from Cloudinary to verify it exists, store the Cloudinary URL as output_path in the image_jobs row, update status to done, and return {job_id, output_url, original_url}. Show a before/after comparison using react-compare-slider with the original Supabase signed URL on the left and the Cloudinary output URL on the right. Add four preset buttons: Square (1080x1080), Story (1080x1920), Banner (1920x1080), Thumbnail (800x600). Show a spinner while status is processing. Add a Download button that opens the processed image in a new tab. Handle errors: if Cloudinary returns non-200, update job status to failed and show an error message. Use the CLOUDINARY_CLOUD_NAME and CLOUDINARY_API_SECRET from Secrets in the Edge Function.
```

Limitations:

- Lovable is web-only (Vite/React); native mobile camera and gallery access requires iOS/Android builds unavailable in the browser
- Test file upload on the published URL only — the Lovable preview iframe blocks native browser file inputs and Supabase Storage uploads
- Cloudinary free tier grants 25 credits per month; each unique transformation consumes credits, so 25+ users testing multiple sizes will exhaust the free tier quickly

### Flutterflow — fit 4/10, 3–5 hours

The right mobile path: image_picker gives native camera and gallery access with quality controls, and Supabase Storage upload is a built-in FlutterFlow action. The smart crop AI call routes through a Supabase Edge Function.

1. Add a 'Select Image' button using FlutterFlow's built-in Upload Media action (set to ImagePicker) — configure imageQuality: 80 and maxWidth: 2048 to prevent OOM crashes on high-resolution photos
2. After image selection, add a Supabase Storage Upload action to the 'originals' bucket at path {current_user_uid}/{current_time_epoch}.jpg; store the returned path in a Page State variable 'originalPath'
3. Add a Custom Action 'ProcessImage' that makes an authenticated POST request to your Supabase Edge Function 'process-image' with {storage_path: originalPath, width: selectedWidth, height: selectedHeight}; store the returned output_url in Page State variable 'processedUrl'
4. Display the before/after comparison with a Custom Widget — a Stack with two Image widgets and a GestureDetector that adjusts a ClipRect width based on horizontal drag position
5. Add a Download button Custom Action using the url_launcher package to open the signed Supabase Storage URL; on mobile, use gallery_saver to save directly to photo library

Limitations:

- Smart crop API call must go through a Supabase Edge Function — FlutterFlow cannot call Cloudinary transformation directly with CLOUDINARY_API_SECRET in the request header
- The before/after comparison slider requires a custom Flutter widget; FlutterFlow's visual builder has no native component for interactive image comparison
- FlutterFlow preview (web mode) cannot test the native image_picker — always test on a real device via TestFlight or APK sideload

### Custom — fit 5/10, 1–2 weeks

The right choice for batch processing, on-device inference, or integrating into an existing CDN or Digital Asset Management pipeline. Eliminates per-image API cost with a self-hosted sharp.js + face-api.js pipeline.

1. Build the image picker → Supabase Storage upload pipeline with client-side image compression (canvas drawImage + toBlob at 0.85 quality) before upload
2. Deploy a Node.js server (or Supabase Edge Function with WASM sharp.js build) that runs: face-api.js DetectAllFaces for bounding boxes → calculate optimal crop rectangle → sharp.js resize + crop to target dimensions
3. Implement batch processing: accept an array of storage paths, process sequentially, update image_jobs status per item, and emit real-time progress via Supabase Realtime channel
4. Integrate with CDN: after processing, push the result to Cloudflare R2 or Cloudinary via their upload API; return the CDN URL for fast global delivery

Limitations:

- Running sharp.js in Deno Edge Functions requires a WASM build of libvips; for high-volume processing, a dedicated Node.js server is more reliable
- face-api.js models are 5–30MB each — loading them on each Edge Function cold start adds significant latency; a persistent Node.js server with models pre-loaded is required for sub-second processing

## Gotchas

- **Image upload works in browser but fails silently in FlutterFlow preview** — FlutterFlow's browser-based preview runs as a web app, which means the image_picker package falls back to HTML file input. The native iOS/Android image picker (with camera access, gallery browsing, and quality controls) only activates in a real device build. Users testing in the browser preview see a broken or unresponsive picker that appears to work but doesn't produce a usable image. Fix: Test image picker functionality on a real device via TestFlight (iOS) or an APK install (Android), never in the FlutterFlow browser preview. Use the FlutterFlow mobile test app on your phone as a quick first check.
- **Cloudinary returns cropped image with faces cut off** — The g_auto:face parameter (singular) detects a single primary face. In group photos or images where a face is partially obscured, Cloudinary may fall back to center gravity silently without indicating that face detection failed. The result is a crop that looks like a default center crop — which is exactly the behavior the user wanted to avoid. Fix: Use g_auto:faces (plural) to detect multiple faces. Add an explicit fallback gravity: gravity_center so the fallback behavior is predictable. Show the before/after comparison slider before confirming the save, so users can see if the smart crop failed and choose a different preset or manual crop.
- **Large image uploads fail with 413 error from Edge Function** — Supabase Edge Functions have a 6MB request body limit. A 10MB RAW photo uploaded directly as base64 or multipart to the Edge Function exceeds this limit. The 413 error often appears as a generic network error in the client rather than a clear message, making it hard to diagnose. Fix: Use the signed upload URL pattern: the Edge Function generates a Supabase Storage signed upload URL and returns it to the client. The client uploads directly to Supabase Storage, bypassing the Edge Function entirely. The Edge Function only receives the storage path once the upload completes.
- **Processed image URL is publicly accessible to anyone with the link** — If the Supabase Storage bucket containing processed images is set to public, anyone who obtains the URL (from browser history, network logs, or a shared screenshot) can access any user's processed images permanently. This is a privacy issue for apps where users process personal photos. Fix: Set the processed images bucket to private. Generate short-lived signed URLs using supabase.storage.from('processed').createSignedUrl(path, 3600) — the URL expires after 1 hour. For downloads, generate a fresh signed URL at the moment the user clicks Download.
- **FlutterFlow app crashes when processing a 10MB photo** — The image_picker package returns the full-resolution image from the device gallery. A 10MB+ photo loaded entirely into memory on the device causes an OutOfMemoryError on phones with limited RAM, particularly older Android devices. The app crashes without a useful error message. Fix: Configure image_picker with imageQuality: 80 and maxWidth: 2048 (or maxHeight for portrait). These constraints compress and resize the image before it enters app memory. For most smart crop use cases, a 2048px wide image is more than sufficient quality and weighs 200–500KB rather than 10MB.

## Best practices

- Apply imageQuality: 80 and maxWidth: 2048 in image_picker before upload — prevents OOM crashes and reduces storage costs without visible quality loss
- Use the signed upload URL pattern to bypass the 6MB Edge Function body limit for all file uploads
- Set processed images bucket to private with short-lived signed URLs; never expose user photo processing results publicly
- Use g_auto:faces (plural) with explicit g_center fallback so group photos and non-face images degrade gracefully
- Show a before/after comparison slider before the user confirms the save — let them see the smart crop result and choose a different preset if the AI missed the subject
- Track every processing job in the image_jobs table with status so users can resume failed batch jobs without re-uploading
- Cache Cloudinary transformation URLs by storing the output_url in image_jobs — the same transformation on the same image is free on delivery from Cloudinary's CDN

## Frequently asked questions

### What is smart cropping versus regular cropping?

Regular cropping cuts a rectangular region from a fixed position — typically the center of the image. Smart cropping uses computer vision to detect the most important region (faces, primary objects, focal points) and crops around that instead. A portrait photo center-cropped to 1:1 might cut off the top of the head; smart crop detects the face and adjusts the crop window to keep it centered and fully visible.

### Does Cloudinary work with FlutterFlow?

Not directly. FlutterFlow cannot call Cloudinary's transformation API with authentication credentials from the client side without exposing your API secret. The correct pattern is a Supabase Edge Function that constructs the Cloudinary transformation URL using the API secret from Secrets, then returns the result URL to the FlutterFlow app. The Edge Function acts as a secure proxy.

### How do I keep user images private?

Set your Supabase Storage buckets (both originals and processed) to private. Use Supabase RLS storage policies that restrict access to the uploading user's own path prefix (user_id/). Generate signed URLs with short expiry (1 hour) for downloads. Never put processed image URLs in a public-readable database column without access control.

### What file sizes are supported?

Supabase Storage accepts files up to 50MB on the free tier. However, the Edge Function body limit is 6MB, so always use the signed upload URL pattern to upload directly from the client to Storage. For practical purposes, apply imageQuality: 80 and maxWidth: 2048 before upload — this keeps files under 2MB without noticeable quality loss for smart crop use cases.

### Can I process images without uploading them to a server?

For basic resize without smart crop: yes. The canvas API on web can resize images in the browser using drawImage() with target dimensions. On Flutter, the image package's copyResize() function works on-device. Smart crop with AI face detection requires a server-side call to Cloudinary or AWS Rekognition — these models don't run in the browser without a WASM build, which is a significant engineering effort.

### How do I add batch image resizing?

The image_jobs table already supports batch: submit multiple jobs with individual rows, each tracking their own status. The Edge Function can accept an array of storage paths and process them sequentially, updating status per row. On the client, poll the image_jobs table (or use a Supabase Realtime subscription) to show progress per image. At scale, consider a Supabase Database Function triggered by new image_jobs rows to avoid the Edge Function timeout limit.

### What happens if no face is detected?

Cloudinary's g_auto:face falls back to center gravity silently when no face is detected. To make the fallback explicit, always include a gravity parameter: set gravity_center as the fallback. The before/after comparison slider lets users see the result before confirming — if the AI chose a poor crop, they can switch to a manual preset. Alternatively, g_auto:subject (not g_auto:face) detects non-face focal points like products and landmarks.

### How much does Cloudinary cost at scale?

Cloudinary charges per transformation credit: one credit = one unique transformation (unique image + unique output dimensions). The free tier is 25 credits per month. Critically, Cloudinary caches every transformation — so if 1,000 users view the same processed image, that's 1 credit, not 1,000. At 1,000 active users generating unique crops, the Plus plan at $89/month (1,000 credits) is the right tier. At 10,000 users the Advanced tier (~$224/mo) or a self-hosted sharp.js pipeline becomes more economical.

---

Source: https://www.rapidevelopers.com/app-features/ai-powered-image-resizing
© RapidDev — https://www.rapidevelopers.com/app-features/ai-powered-image-resizing
