Skip to content

Replace django-image-cropping with an in-repo Cropper.js admin widget (#1299) - #1305

Merged
jonfroehlich merged 4 commits into
masterfrom
1299-instant-image-crop
Jun 15, 2026
Merged

Replace django-image-cropping with an in-repo Cropper.js admin widget (#1299)#1305
jonfroehlich merged 4 commits into
masterfrom
1299-instant-image-crop

Conversation

@jonfroehlich

@jonfroehlich jonfroehlich commented Jun 15, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the EOL PyPI django-image-cropping (v1.7, Feb 2022; Jcrop + jQuery; classifiers stop at Django 4.0; "upload → Save and continue → scroll up → crop") with a small in-repo fork whose only material change is a modern Cropper.js admin widget that previews and crops the image client-side, before the first save.

Closes #1299. Advances #1269 (retires a fragile, unmaintained dependency that was a Django 6.1 upgrade risk). Follow-up enhancement filed as #1304.

Why this approach

The only thing tying us to the unmaintained dependency was the admin widget JSImageRatioField and the crop_corners processor are pure Django/Pillow. So instead of ripping the system out, we vendor image_cropping/ in-repo (treated as project source, exactly like sortedm2m_filter_horizontal_widget) and rewrite just the widget. There's no maintained drop-in that does instant client-side cropping plus a non-destructive box fed to easy_thumbnails (most modern widgets either only preview, or bake a destructive crop) — and our render pipeline (one stored box → rendered at every size on demand) is the valuable part worth keeping.

What changed

  • New top-level image_cropping/ package: ImageRatioField, crop_corners, ImageCroppingMixin, CropImageWidget, vendored Cropper.js v1.6.2 (MIT, no build step) + ml_cropper.{js,css} glue. See image_cropping/README.md.
  • Widget UX: instant FileReader preview on file select; live WYSIWYG preview of the cropped result; precise keyboard-accessible X/Y/W/H inputs tucked in a collapsed "Adjust crop precisely" disclosure.
  • requirements.txt: drop django-image-cropping (django-appconf falls away as a transitive dep). INSTALLED_APPS keeps 'image_cropping' (now the fork).
  • Templates: convert Banner's 2 cropped_thumbnail calls to the standard {% thumbnail … box=banner.cropping %} idiom; remove 19 now-dead {% load cropping %} lines; add {% load thumbnail %} to base.html.
  • Help texts: refresh the obsolete "Save and continue editing" instructions on Banner, News, Person, Photo, Project.
  • Docs: update CLAUDE.md image-handling section.

Migration safety

Deliberately migration-neutral: ImageRatioField stays a CharField whose deconstruct() returns image_cropping.fields.ImageRatioField, so the gitignored, per-environment migrations that import image_cropping.fields keep working and the DB column is untouched (makemigrations reports no change to any cropping field). Keeping the package name is what preserves those imports — important given deploys are push-only with no server shell. A regression test pins the deconstruct() path.

Testing

  • 14 new tests in website/tests/test_image_cropping.py (crop_corners parsing, the deconstruct() migration contract, widget Media uses Cropper not Jcrop, model crop metadata).
  • Full suite: 149 passing (--settings=makeabilitylab.settings_test).
  • Browser-verified end-to-end (Playwright + Chrome): instant preview on upload (no save), aspect-ratio lock, drag ↔ numeric sync, crop persists to the DB, reopen restores it, and the real easy_thumbnails pipeline applies the box. Found & fixed two real bugs this way: Cropper stalling on blob: URLs with checkOrientation:true, and the live preview going blank because the object URL was revoked too early.

Screenshots

Note: gh can't upload images via CLI — @jonfroehlich to drag in the before/after PNGs.

  • Before: old Jcrop flow — crop widget only appears after "Save and continue editing".
  • After: choose an image → cropper + live preview appear instantly; precise pixel inputs collapsed on one row.

Checklist

  • Tests added/passing
  • a11y considered (keyboard-accessible numeric controls; admin is not Pa11y-scanned)
  • Before/after screenshots attached

Screenshot after

image

🤖 Generated with Claude Code

jonfroehlich and others added 4 commits June 15, 2026 12:32
…1269)

Swap the EOL PyPI django-image-cropping (v1.7, Feb 2022; Jcrop+jQuery;
Django <=4.0; "save first, then crop") for an in-repo fork whose only
material change is a modern Cropper.js admin widget that previews and crops
client-side, before the first save. Closes the long-standing #1299 friction
and retires a Django-6.1-blocking dependency (#1269 sibling).

New top-level image_cropping/ package (project source, like
sortedm2m_filter_horizontal_widget): ImageRatioField, crop_corners,
ImageCroppingMixin, CropImageWidget, vendored cropper.min.{js,css} v1.6.2 +
ml_cropper.{js,css} glue (instant FileReader preview -> Cropper.js ->
"x1,y1,x2,y2" box; keyboard-accessible numeric X/Y/W/H inputs; no-JS
fallback).

Deliberately migration-neutral: ImageRatioField stays a CharField whose
deconstruct() returns "image_cropping.fields.ImageRatioField", so the
gitignored per-environment migrations that import image_cropping.fields keep
working and the DB column is untouched (pinned by a regression test). Keeping
the package name is what preserves those imports.

- requirements.txt: drop django-image-cropping (django-appconf falls away as
  a transitive dep); INSTALLED_APPS keeps 'image_cropping' (now the fork).
- Banner: convert the 2 cropped_thumbnail calls to the standard
  {% thumbnail box= %} idiom; remove 19 now-dead {% load cropping %} lines;
  add {% load thumbnail %} to base.html.
- Refresh the obsolete "Save and continue editing" help texts on Banner,
  News, Person, Photo, Project.
- Update CLAUDE.md image-handling docs; add image_cropping/README.md.

Tests: 14 new (crop_corners parsing, deconstruct migration contract, widget
Media uses Cropper not Jcrop, model crop metadata); full suite 149 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Editor-facing polish for the admin cropper. The raw "Crop region (pixels)"
X/Y/W/H fieldset read as dev internals; coordinates mean nothing to someone
cropping a photo.

- Add a live WYSIWYG preview (Cropper.js `preview`) showing exactly what the
  cropped thumbnail will look like, sized to the field's aspect ratio.
- Move the numeric inputs into a collapsed "Adjust crop precisely (pixels)"
  <details> disclosure — still keyboard/screen-reader accessible, just no
  longer cluttering the default drag-to-crop view.
- Fix: keep the instant-preview blob: object URL alive for the crop session.
  It was revoked on the main image's load, but Cropper builds its preview
  clone from the same URL afterward, leaving the preview blank (naturalWidth
  0). Now the previous URL is revoked only when a new file replaces it.

Browser-verified (Playwright + Chrome): preview renders the cropped pixels,
disclosure collapsed by default, numeric inputs still drive the crop when
expanded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The precise-pixel inputs wrapped (Height fell to a second line) because they're
rendered inside Django admin's aligned form, whose label styling
(width:160px, min-width:160px, float:left) was applied to each field's <label>
wrapper and forced it ~170px wide. Override that styling on the wrappers
(min-width:0 !important is the key — min-width floors width) and give the
number inputs a compact fixed width, so all four share a single flex row.

(The easter-egg image on Person already gets its own cropper + live preview;
both crop fields are handled identically by the widget. Verified in-browser.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jonfroehlich
jonfroehlich merged commit 99425d6 into master Jun 15, 2026
1 check passed
@jonfroehlich
jonfroehlich deleted the 1299-instant-image-crop branch June 22, 2026 20:36
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.

Would be so nice to get an instant preview of an image that you add for cropping

1 participant