+ * The handler is registered to this component, so the image is served while this + * component is attached and visible, independently of any other component. Requests + * are also rejected while this component is disabled or inert (for instance, behind a + * modal dialog), unless the handler overrides + * {@link DownloadHandler#getDisabledUpdateMode()} or + * {@link DownloadHandler#isAllowInert()}. + * + *
+ * Use an {@code inline()} handler, since the image is displayed rather than + * downloaded. When the {@linkplain #setOutputMimeType(String) output MIME type} is not + * set, it is detected from the file extension of the handler's + * {@linkplain DownloadHandler#getUrlPostfix() URL postfix}; handlers without one (such + * as {@code DownloadHandler.fromInputStream(...)}) fall back to {@code image/png}. + * + *
+ * As with any {@code DownloadHandler} set on an element, this method must be called + * while a {@link com.vaadin.flow.component.UI UI} is current. + * + * @param downloadHandler the handler that serves the image to be cropped + */ + public void setImageSrc(DownloadHandler downloadHandler) { + Objects.requireNonNull(downloadHandler, "downloadHandler must not be null"); + getElement().setAttribute(IMG_SRC_RESOURCE_ATTRIBUTE, downloadHandler); + setState("imgSrc", getElement().getAttribute(IMG_SRC_RESOURCE_ATTRIBUTE)); + croppedImageDataUri = null; + } + /** * Gets the source of the image being cropped. * + *
+ * When the source was set with a {@link DownloadHandler}, this returns the internal + * URL of the registered resource. Passing that URL back to + * {@link #setImageSrc(String)} unregisters the handler, so the URL stops being + * served. + * * @return the image source */ public String getImageSrc() { @@ -434,7 +487,11 @@ public double getOutputQuality() { /** * Returns the cropped image data URI. * - * @return the cropped image data URI + *
+ * When the component is created with a {@link DownloadHandler}, this is {@code null} + * until the image is cropped for the first time. + * + * @return the cropped image data URI, or {@code null} if not available yet */ public String getCroppedImageDataUri() { return this.croppedImageDataUri; diff --git a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/BasicImageCropDemo.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/BasicImageCropDemo.java index c25566f..d689c95 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/BasicImageCropDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/BasicImageCropDemo.java @@ -2,7 +2,7 @@ * #%L * Image Crop Add-on * %% - * Copyright (C) 2024 Flowing Code + * Copyright (C) 2024-2026 Flowing Code * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,15 +40,13 @@ public class BasicImageCropDemo extends VerticalLayout { public BasicImageCropDemo() { add(new Span("Select a portion of the picture to crop: ")); - Image image = new Image("images/empty-plant.png", "image to crop"); - ImageCrop imageCrop = new ImageCrop(image); + ImageCrop imageCrop = new ImageCrop("images/empty-plant.png"); + imageCrop.setImageAlt("image to crop"); add(imageCrop); Button getCropButton = new Button("Get Cropped Image"); croppedResultDiv.setId("result-cropped-image-div"); - croppedResultDiv.setWidth(image.getWidth()); - croppedResultDiv.setHeight(image.getHeight()); getCropButton.addClickListener(e -> { croppedResultDiv.removeAll(); diff --git a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/DownloadHandlerImageCropDemo.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/DownloadHandlerImageCropDemo.java new file mode 100644 index 0000000..cbb38b6 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/DownloadHandlerImageCropDemo.java @@ -0,0 +1,66 @@ +/*- + * #%L + * Image Crop Add-on + * %% + * Copyright (C) 2024-2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +package com.flowingcode.vaadin.addons.imagecrop; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Image; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.streams.DownloadHandler; + +@DemoSource +@PageTitle("Image Crop with DownloadHandler") +@SuppressWarnings("serial") +@Route(value = "image-crop/download-handler", layout = ImageCropDemoView.class) +public class DownloadHandlerImageCropDemo extends VerticalLayout { + + private Div croppedResultDiv = new Div(); + + public DownloadHandlerImageCropDemo() { + add(new Span("Select a portion of the picture to crop: ")); + + // The image is served by a DownloadHandler registered to the ImageCrop itself + DownloadHandler handler = DownloadHandler.forClassResource(getClass(), + "/META-INF/resources/images/empty-plant.png", "empty-plant.png").inline(); + ImageCrop imageCrop = new ImageCrop(handler); + imageCrop.setImageAlt("image to crop"); + add(imageCrop); + + Button getCropButton = new Button("Get Cropped Image"); + + croppedResultDiv.setId("result-cropped-image-div"); + + getCropButton.addClickListener(e -> { + croppedResultDiv.removeAll(); + // there is no cropped image until the user selects an area + if (imageCrop.getCroppedImageDataUri() != null) { + croppedResultDiv.add(new Image(imageCrop.getCroppedImageDataUri(), "cropped image")); + } + }); + + add(getCropButton, new Span("Crop Result:"), croppedResultDiv); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/ImageCropDemoView.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/ImageCropDemoView.java index 4acd8f8..20ce13c 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/ImageCropDemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/ImageCropDemoView.java @@ -2,7 +2,7 @@ * #%L * Image Crop Add-on * %% - * Copyright (C) 2024 Flowing Code + * Copyright (C) 2024-2026 Flowing Code * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ public ImageCropDemoView() { addDemo(BasicImageCropDemo.class); addDemo(UploadImageCropDemo.class); addDemo(OutputFormatImageCropDemo.class); + addDemo(DownloadHandlerImageCropDemo.class); setSizeFull(); } } diff --git a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/ImageCropTest.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/ImageCropTest.java index 7c3314d..faac7c7 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/ImageCropTest.java +++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/ImageCropTest.java @@ -3,25 +3,36 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; import java.util.Base64; +import org.junit.After; import org.junit.Before; import org.junit.Test; import com.flowingcode.vaadin.addons.imagecrop.Crop; import com.flowingcode.vaadin.addons.imagecrop.CroppedImageEvent; import com.flowingcode.vaadin.addons.imagecrop.ImageCrop; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.streams.DownloadHandler; +import com.vaadin.flow.server.streams.DownloadResponse; public class ImageCropTest { private ImageCrop imageCrop; + // keeps the current UI strongly reachable during the test + private UI ui; + @Before public void setUp() { + ui = new UI(); + UI.setCurrent(ui); imageCrop = new ImageCrop("dummyImageSrc"); } @@ -32,6 +43,45 @@ public void testSetAndGetImageSrc() { assertEquals(expectedSrc, imageCrop.getImageSrc()); } + @Test + public void testSetImageSrcWithDownloadHandler() { + imageCrop.setImageSrc(imageHandler()); + assertNotNull(imageCrop.getImageSrc()); + assertEquals(imageCrop.getImageSrc(), imageCrop.getElement().getAttribute("img-src")); + } + + @Test + public void testSetImageSrcWithStringReleasesDownloadHandler() { + imageCrop.setImageSrc(imageHandler()); + imageCrop.setImageSrc("newImageSrc"); + assertEquals("newImageSrc", imageCrop.getImageSrc()); + assertFalse(imageCrop.getElement().hasAttribute("img-src")); + } + + @Test + public void testSetImageSrcWithDownloadHandlerResetsCroppedImage() { + imageCrop.setImageSrc(imageHandler()); + assertNull(imageCrop.getCroppedImageDataUri()); + } + + @Test + public void testDownloadHandlerConstructorHasNoCroppedImage() { + imageCrop = new ImageCrop(imageHandler()); + assertNotNull(imageCrop.getImageSrc()); + assertNull(imageCrop.getCroppedImageDataUri()); + assertNull(imageCrop.getCroppedImageBase64()); + } + + @After + public void tearDown() { + UI.setCurrent(null); + } + + private static DownloadHandler imageHandler() { + return DownloadHandler.fromInputStream(event -> new DownloadResponse( + new ByteArrayInputStream(new byte[0]), "image.png", "image/png", 0)).inline(); + } + @Test public void testSetAndGetCrop() { Crop expectedCrop = new Crop("%", 10, 10, 200, 200); diff --git a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/SerializationTest.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/SerializationTest.java index 37fafe2..f2d0b01 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/SerializationTest.java +++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/SerializationTest.java @@ -2,7 +2,7 @@ * #%L * Image Crop Add-on * %% - * Copyright (C) 2024 Flowing Code + * Copyright (C) 2024-2026 Flowing Code * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,9 @@ import org.junit.Test; import com.flowingcode.vaadin.addons.imagecrop.ImageCrop; -import com.vaadin.flow.component.html.Image; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.streams.DownloadHandler; +import com.vaadin.flow.server.streams.DownloadResponse; public class SerializationTest { @@ -46,9 +48,25 @@ private void testSerializationOf(Object obj) throws IOException, ClassNotFoundEx @Test public void testSerialization() throws ClassNotFoundException, IOException { try { - testSerializationOf(new ImageCrop(new Image())); + testSerializationOf(new ImageCrop("")); } catch (Exception e) { Assert.fail("Problem while testing serialization: " + e.getMessage()); } } + + @Test + public void testSerializationWithDownloadHandler() throws ClassNotFoundException, IOException { + UI ui = new UI(); + UI.setCurrent(ui); + try { + byte[] content = new byte[0]; + // unattached, so the pending resource registration is serialized too + testSerializationOf(new ImageCrop(DownloadHandler.fromInputStream(event -> new DownloadResponse( + new ByteArrayInputStream(content), "image.png", "image/png", content.length)).inline())); + } catch (Exception e) { + Assert.fail("Problem while testing serialization: " + e.getMessage()); + } finally { + UI.setCurrent(null); + } + } }