Skip to content

fix: make px crops export at a deterministic size - #35

Open
paodb wants to merge 1 commit into
masterfrom
issue-33
Open

fix: make px crops export at a deterministic size#35
paodb wants to merge 1 commit into
masterfrom
issue-33

Conversation

@paodb

@paodb paodb commented Jul 20, 2026

Copy link
Copy Markdown
Member

Configured px crops mapped to the exported image via rendered (on-screen) pixels, so the output size varied with how the browser scaled the image at crop time (e.g. a 500×500 px crop could export at ~500 or ~667 px). This makes the mapping deterministic.

Changes

  • Store the crop as % (resolution-independent): onChange/onComplete now use react-image-crop's percentCrop.
  • onImageLoad normalizes the configured crop against the image's natural size; a px crop is interpreted as source (natural) pixels.
  • _updateCroppedImage maps the crop with convertToPixelCrop against naturalWidth/naturalHeight, dropping the rendered→natural rescaling.
  • Remove the now-redundant ResizeObserver/resizeCrop workaround (a % crop needs no rescaling on layout changes) and guard makeAspectCrop against an unset aspect.
  • Document that crop units are source pixels while the min/max crop constraints remain in rendered pixels.

Close #33

Summary by CodeRabbit

  • Bug Fixes

    • Improved crop stability across responsive layouts by keeping crop selections consistent through layout changes.
    • Ensured crop rendering and generated output are mapped using the image’s natural dimensions.
    • Improved handling when setting crop programmatically, including correct percent/% vs pixel conversions and aspect-ratio enforcement.
  • Documentation

    • Clarified meaning of % (resolution-independent) versus px (based on the image’s natural/source pixels).
    • Documented how crop min/max constraints are applied in rendered/on-screen pixels.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Crop normalization

Layer / File(s) Summary
Crop unit semantics
src/main/java/com/flowingcode/vaadin/addons/imagecrop/Crop.java, src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java
Javadocs clarify that % crops are resolution-independent, px crops use natural image pixels, and min/max constraints use rendered pixels.
Percent crop rendering
src/main/resources/META-INF/resources/frontend/src/image-crop.tsx
Crop initialization and ReactCrop state now use percentages, while canvas rendering converts the crop directly to natural-pixel coordinates.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: javier-godoy

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The code and docs implement issue #33 by using percent crops, natural dimensions, and removing the resize workaround.
Out of Scope Changes check ✅ Passed The changes stay within the issue scope and only add related crop-handling and documentation updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: making px crop exports deterministic.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-33

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/resources/META-INF/resources/frontend/src/image-crop.tsx`:
- Around line 144-157: Update the onImageLoad normalization to preserve
explicitly supplied x and y coordinates by removing the unconditional
makeAspectCrop and centerCrop calls. Move px-to-% conversion into a useEffect
that observes the crop state and image dimensions, converting programmatic
server updates before passing the crop to ReactCrop while leaving percentage
crops unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44225dc7-6409-410f-952f-89ba91e1c764

📥 Commits

Reviewing files that changed from the base of the PR and between c9c2cc8 and a7c450c.

📒 Files selected for processing (3)
  • src/main/java/com/flowingcode/vaadin/addons/imagecrop/Crop.java
  • src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java
  • src/main/resources/META-INF/resources/frontend/src/image-crop.tsx

Comment thread src/main/resources/META-INF/resources/frontend/src/image-crop.tsx
@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/resources/META-INF/resources/frontend/src/image-crop.tsx (1)

271-278: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Prevent IndexSizeError crash on zero-dimension crops.

If the crop region has a width or height of zero (e.g., from an uninitialized state or programmatic update), outWidth or outHeight will evaluate to 0. Calling drawImage with a zero source width or height throws an IndexSizeError (or InvalidStateError) in browsers, which crashes the script execution and prevents subsequent logic from running. Consider adding an early return to handle this gracefully.

🛡️ Proposed fix
 				const outWidth = Math.round(ccrop.width);
 				const outHeight = Math.round(ccrop.height);
 
+				if (outWidth <= 0 || outHeight <= 0) {
+					return;
+				}
+
 				// Setting canvas dimensions resets the 2D context, so it must happen
 				// before any drawing/clipping state is configured below.
 				canvas.width = outWidth;
 				canvas.height = outHeight;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/resources/META-INF/resources/frontend/src/image-crop.tsx` around
lines 271 - 278, Add an early return in the crop-rendering flow after computing
outWidth and outHeight, before assigning canvas dimensions or calling drawImage,
when either dimension is zero or otherwise non-positive. Preserve the existing
rendering path for positive dimensions and allow subsequent logic to continue
without throwing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/main/resources/META-INF/resources/frontend/src/image-crop.tsx`:
- Around line 271-278: Add an early return in the crop-rendering flow after
computing outWidth and outHeight, before assigning canvas dimensions or calling
drawImage, when either dimension is zero or otherwise non-positive. Preserve the
existing rendering path for positive dimensions and allow subsequent logic to
continue without throwing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 808376d8-5c44-4867-9897-a49725681424

📥 Commits

Reviewing files that changed from the base of the PR and between a7c450c and 1464c6a.

📒 Files selected for processing (3)
  • src/main/java/com/flowingcode/vaadin/addons/imagecrop/Crop.java
  • src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java
  • src/main/resources/META-INF/resources/frontend/src/image-crop.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java

@paodb
paodb marked this pull request as ready for review July 20, 2026 21:01
@paodb
paodb requested review from javier-godoy and scardanzan July 20, 2026 21:01

@javier-godoy javier-godoy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review of the changes introduced by this PR (px/percent crop normalization rework).

/**
* Adjusts the crop size proportionally when the image is resized.
* Normalizes the configured crop when the image loads. The crop is kept as a
* percentage of the image's natural size, so both the on-screen selection and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toPercent returns 0 when dimension is falsy:

const toPercent = (value: number, dimension: number) =>
    dimension ? (value / dimension) * 100 : 0;

For an image whose naturalWidth/naturalHeight is 0 even after load fires (e.g. an SVG source without width/height/viewBox), all four toPercent calls in onImageLoad collapse to 0, producing a zero-size crop that gets fed into _updateCroppedImage — an empty/invalid exported image instead of the configured crop, with no error surfaced.

/**
* Adjusts the crop size proportionally when the image is resized.
* Normalizes the configured crop when the image loads. The crop is kept as a
* percentage of the image's natural size, so both the on-screen selection and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This px→percent conversion is hand-rolled here and duplicated again in the useEffect below (normalizing a late setCrop). Consider sharing one helper (or using react-image-crop's own convertToPercentCrop, already available alongside convertToPixelCrop) so the two normalization paths can't drift out of sync — which is exactly what happened with the missing aspect-ratio step noted in the other comment.

* has loaded. onImageLoad only runs on the initial load, so without this a
* later setCrop("px", ...) would be rendered by ReactCrop as on-screen pixels
* and the selection box would diverge from the natural-pixel export (issue
* #33). The configured x/y are preserved (no centering) since the crop is

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike onImageLoad (which calls makeAspectCrop before centerCrop), this effect never re-applies the configured aspect when normalizing a late programmatic px crop.

Repro: configure aspect={1} with the image already loaded, then call setCrop(new Crop("px", 10, 10, 200, 50)) (non-square). onImageLoad won't re-run since the image is already loaded, so only this effect fires — it converts x/y/width/height to percent verbatim with no aspect enforcement, leaving the crop box (and exported image) non-square despite aspect=1, until the user manually drags a handle.

* has loaded. onImageLoad only runs on the initial load, so without this a
* later setCrop("px", ...) would be rendered by ReactCrop as on-screen pixels
* and the selection box would diverge from the natural-pixel export (issue
* #33). The configured x/y are preserved (no centering) since the crop is

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This normalization always rewrites the crop's unit to "%", so getCrop() on the Java side can now return a different unit and different numeric values than what was passed to setCrop().

On master, onImageLoad preserved crop.unit verbatim, so a px crop stayed px after load. With this change, setCrop(new Crop("px", 100, 100, 300, 300)) followed by getCrop() (after load or any interaction) returns unit % with fractional values instead of the original px values — silently breaking any caller code that persists getCrop() and re-applies it later, or that branches on crop.unit().equals("px").

@javier-godoy javier-godoy moved this from To Do to In Progress in Flowing Code Addons Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

Configured px crops don't map deterministically to the output size

2 participants