Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ ImageCrop imageCrop = new ImageCrop(DownloadHandler.forClassResource(getClass(),
add(imageCrop);
```

* Constrain the size of large images

The image is scaled down to fit the component's size, whether it is set with `height` or `max-height` (in any unit) or given by the layout.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This only holds when something limits the component's height. In a container that doesn't size its children, the component grows to the image's height, so nothing is clamped: with new Div(imageCrop) and div.setHeight("400px"), a 1000×4000 image renders at 4000px and overflows the Div. Percentages also need a parent with a defined height. Since HasSize was removed, setSizeFull() isn't available either.

Suggested change
The image is scaled down to fit the component's size, whether it is set with `height` or `max-height` (in any unit) or given by the layout.
The image is scaled down, keeping its aspect ratio, to fit the component's height when that height is limited: by a `height` or `max-height` set on the component, or by a flex layout such as `VerticalLayout` that sizes it. Percentages are resolved against the parent's height, so the parent needs a defined height. In a container that doesn't size its children, such as a `Div`, the component grows to the image's height; set a height on it, for instance `getStyle().set("height", "100%")`, to constrain the image.


```java
imageCrop.getStyle().set("max-height", "70vh");
```

* Get cropped image

```java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,15 @@
* <a href="https://www.npmjs.com/package/react-image-crop">react-image-crop</a>
* library.
* This component allows users to define and manipulate crop areas on images.
*
*
* <p>
* The image is scaled down, keeping its aspect ratio, to fit the component's width
* and height, whether they are set on the component (a {@code height} or a
* {@code max-height} in any unit) or given by the layout it is in. For instance,
* {@code getStyle().set("max-height", "100vh")} keeps a large image within the
* viewport. Images smaller than these limits are shown at their natural size.
Comment on lines +44 to +48

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as the README:

Suggested change
* The image is scaled down, keeping its aspect ratio, to fit the component's width
* and height, whether they are set on the component (a {@code height} or a
* {@code max-height} in any unit) or given by the layout it is in. For instance,
* {@code getStyle().set("max-height", "100vh")} keeps a large image within the
* viewport. Images smaller than these limits are shown at their natural size.
* The image is scaled down, keeping its aspect ratio, to fit the component's width
* and height. The height is limited by a {@code height} or {@code max-height} set on
* the component, or by a flex layout that sizes it, such as a {@code VerticalLayout}.
* Percentages are resolved against the parent's height, so they only take effect when
* the parent has a defined height. In a container that does not size its children,
* such as a {@code Div}, the component grows to the image's height unless a height is
* set on it. For instance, {@code getStyle().set("max-height", "100vh")} keeps a large
* image within the viewport. Images smaller than these limits are shown at their
* natural size.

* </p>
*
* @author Paola De Bartolo / Flowing Code
*/
@NpmPackage(value = "react-image-crop", version = "11.0.6")
Expand All @@ -48,8 +56,6 @@
@CssImport("react-image-crop/dist/ReactCrop.css")
@CssImport("./styles/image-crop-styles.css")
public class ImageCrop extends ReactAdapterComponent {

private static final String IMG_FULL_HEIGHT_CLASS_NAME = "img-full-height";

private static final String IMG_SRC_RESOURCE_ATTRIBUTE = "img-src";

Expand Down Expand Up @@ -497,21 +503,6 @@ public String getCroppedImageDataUri() {
return this.croppedImageDataUri;
}

/**
* Sets the image to occupy the full viewport height when enabled.
* If {@code fullHeight} is {@code true}, applies a CSS class that
* sets the image height to 100vh. If {@code false}, removes the class
* to revert to the default height.
*
* @param fullHeight whether the image should fill the viewport height
*/
public void setImageFullHeight(Boolean fullHeight) {
if (fullHeight)
this.addClassName(IMG_FULL_HEIGHT_CLASS_NAME);
else
this.removeClassName(IMG_FULL_HEIGHT_CLASS_NAME);
}

/**
* Decodes the cropped image data URI and returns it as a byte array. If the image data URI is not
* in the format "data:image/*;base64,", it will be decoded assuming it is a Base64 encoded
Expand Down
35 changes: 35 additions & 0 deletions src/main/resources/META-INF/frontend/src/image-crop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,40 @@
)
: percentCrop;

/**
* Fits the image to the height the component actually gets, however it is
* defined: a height, a max-height in any unit, or the layout. CSS alone can't
* do it, since react-image-crop inherits max-height as written, so a
* percentage would resolve against an auto-height parent and be ignored.
*
* The limit is cleared before measuring, so the component is measured without
* it, and is set in pixels only when the image is taller than the available
* space. Smaller images are never upscaled.
*/
const fitImageHeight = () => {
const cropElement = this.querySelector<HTMLElement>(".ReactCrop");
const img = imgRef.current;
if (!cropElement || !img) {
return;
}
cropElement.style.maxHeight = "";
const style = getComputedStyle(this);
const available = this.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);

Check warning on line 165 in src/main/resources/META-INF/frontend/src/image-crop.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_ImageCrop&issues=AaFayBqBEdop_KTz_I3g&open=AaFayBqBEdop_KTz_I3g&pullRequest=42

Check warning on line 165 in src/main/resources/META-INF/frontend/src/image-crop.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_ImageCrop&issues=AaFayBqBEdop_KTz_I3h&open=AaFayBqBEdop_KTz_I3h&pullRequest=42
if (img.getBoundingClientRect().height > available + 0.5) {
Comment on lines +165 to +166

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

getBoundingClientRect() includes CSS transforms, but clientHeight doesn't, and a transform change doesn't trigger the ResizeObserver. In a Dialog, the image is measured during the opening animation, while the overlay is scaled to 0.95. With max-height: 70vh at a 905px viewport (633.5px) and a 1000×650 image, the image measures 618px, so it isn't clamped. When the animation ends, it renders at 650px in a 634px component, overflowing by 17px, and nothing refits it. The same happens inside any container with a scale below 1. Comparing img.offsetHeight, which isn't affected by transforms, avoids this.

cropElement.style.maxHeight = `${available}px`;
Comment on lines +163 to +167

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Measuring with the clamp cleared assumes the clamp doesn't change the space the layout gives the component. In a flex container where a sibling can also shrink, it does: the space each item loses is weighted by its flex basis, and the clamp lowers the component's basis. In a 500px-tall flex column with an image-crop (1000×3000 image) and a sibling with flex: 0 1 200px, the unclamped component gets 469px, so the image is clamped to 469px. With that smaller basis the component then gets 351px, and the image overflows it by 118px. It stays that way: at 250px the overflow is 98px, at 700px it is 119px.

}
};

// Refit when the component or its container is resized.
useEffect(() => {
const observer = new ResizeObserver(() => fitImageHeight());
observer.observe(this);
if (this.parentElement) {
observer.observe(this.parentElement);
}
return () => observer.disconnect();
}, []);

/**
* 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
Expand All @@ -156,6 +190,7 @@
* below). setCrop's Javadoc documents the difference.
*/
const onImageLoad = () => {
fitImageHeight();
const img = imgRef.current;
if (!img || !crop) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
* #L%
*/

.img-full-height img {
height: 100vh;
/* min-width/min-height let the component shrink in flex layouts, so the image
is fitted to the space the layout gives it (see fitImageHeight). */
image-crop {
display: inline-block;
max-width: 100%;
min-width: 0;
min-height: 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ private void openCropDialog(ByteArrayOutputStream outputStream, String mimeType)
imageCrop.setCircularCrop(true);
imageCrop.setCrop(new Crop("%", 25, 25, 50, 50)); // centered crop
imageCrop.setKeepSelection(true);
// keep large images within the dialog
imageCrop.getStyle().set("max-height", "70vh");

cropButton.addClickListener(event -> {
newCroppedPicture = imageCrop.getCroppedImageBase64();
Expand Down
Loading