diff --git a/README.md b/README.md index f937b9d..13895cf 100644 --- a/README.md +++ b/README.md @@ -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. + +```java +imageCrop.getStyle().set("max-height", "70vh"); +``` + * Get cropped image ```java diff --git a/src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java b/src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java index 3181f1e..6b25b40 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java +++ b/src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java @@ -39,7 +39,15 @@ * react-image-crop * library. * This component allows users to define and manipulate crop areas on images. - * + * + *

+ * 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. + *

+ * * @author Paola De Bartolo / Flowing Code */ @NpmPackage(value = "react-image-crop", version = "11.0.6") @@ -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"; @@ -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 diff --git a/src/main/resources/META-INF/frontend/src/image-crop.tsx b/src/main/resources/META-INF/frontend/src/image-crop.tsx index cb484a2..793aa61 100644 --- a/src/main/resources/META-INF/frontend/src/image-crop.tsx +++ b/src/main/resources/META-INF/frontend/src/image-crop.tsx @@ -144,6 +144,40 @@ class ImageCropElement extends ReactAdapterElement { ) : 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(".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); + if (img.getBoundingClientRect().height > available + 0.5) { + cropElement.style.maxHeight = `${available}px`; + } + }; + + // 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 @@ -156,6 +190,7 @@ class ImageCropElement extends ReactAdapterElement { * below). setCrop's Javadoc documents the difference. */ const onImageLoad = () => { + fitImageHeight(); const img = imgRef.current; if (!img || !crop) { return; diff --git a/src/main/resources/META-INF/frontend/styles/image-crop-styles.css b/src/main/resources/META-INF/frontend/styles/image-crop-styles.css index 79463c2..549c8b2 100644 --- a/src/main/resources/META-INF/frontend/styles/image-crop-styles.css +++ b/src/main/resources/META-INF/frontend/styles/image-crop-styles.css @@ -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; } diff --git a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/UploadImageCropDemo.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/UploadImageCropDemo.java index cb91d8e..c83d1cc 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/UploadImageCropDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/UploadImageCropDemo.java @@ -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();