Skip to content
Merged
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
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ or as a Base64 encoded byte array by using `getCroppedImageBase64` method.

## Supported versions

Supported versions: Vaadin 24-25 (add-on version 1.x)
Supported versions:
- Vaadin 24.8+ and 25: add-on version 2.x
- Vaadin 24 and 25: add-on version 1.x (without `DownloadHandler` support)

## Online demo

Expand Down Expand Up @@ -112,8 +114,16 @@ Image Crop Add-on is written by Flowing Code S.A.
* Basic use

```java
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);
```

* Image served by a `DownloadHandler`

```java
ImageCrop imageCrop = new ImageCrop(DownloadHandler.forClassResource(getClass(),
"/META-INF/resources/images/empty-plant.png", "empty-plant.png").inline());
add(imageCrop);
```

Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

<groupId>com.flowingcode.vaadin.addons</groupId>
<artifactId>image-crop-addon</artifactId>
<version>1.3.1-SNAPSHOT</version>
<version>2.0.0-SNAPSHOT</version>
<name>Image Crop Add-on</name>
<description>Image Crop Add-on for Vaadin Flow</description>
<url>https://www.flowingcode.com/en/open-source/</url>

<properties>
<vaadin.version>24.4.6</vaadin.version>
<vaadin.version>24.8.4</vaadin.version>
<selenium.version>4.10.0</selenium.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* #%L
* Image Crop Add-on
* %%
* Copyright (C) 2024-2025 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.
Expand All @@ -22,14 +22,16 @@

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.react.ReactAdapterComponent;
import com.vaadin.flow.server.streams.DownloadHandler;
import com.vaadin.flow.shared.Registration;

/**
Expand All @@ -49,6 +51,8 @@

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

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

private String croppedImageDataUri;

/**
Expand All @@ -63,16 +67,16 @@
}

/**
* Constructs an ImageCrop component with the given image.
* Constructs an ImageCrop component whose image is served by the given
* {@link DownloadHandler}. See {@link #setImageSrc(DownloadHandler)} for the
* conditions under which the image is served.
*
* @param image the image to be cropped
* @deprecated This constructor only preserves the image URL and {@linkplain #setImageAlt(String)
* alternate text}. Use {@link #ImageCrop(String)} instead.
* @param downloadHandler the handler that serves the image to be cropped
* @see #setImageSrc(DownloadHandler)
*/
@Deprecated(forRemoval = true, since = "1.2.0")
public ImageCrop(Image image) {
this(image.getSrc());
image.getAlt().ifPresent(a -> this.setImageAlt(a));
public ImageCrop(DownloadHandler downloadHandler) {
this.setImageSrc(downloadHandler);
this.addCroppedImageListener(this::updateCroppedImage);
}

/**
Expand All @@ -97,18 +101,67 @@
this.croppedImageDataUri = event.getCroppedImageDataUri();
}

@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
// the resource URL is regenerated on attach (e.g. with a new UI id after a
// refresh with @PreserveOnRefresh), so the image source must follow it
if (getElement().hasAttribute(IMG_SRC_RESOURCE_ATTRIBUTE)) {
setState("imgSrc", getElement().getAttribute(IMG_SRC_RESOURCE_ATTRIBUTE));

Check failure on line 110 in src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "imgSrc" 4 times.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_ImageCrop&issues=AaE6-FfCoukKg12_78HT&open=AaE6-FfCoukKg12_78HT&pullRequest=37
}
}

/**
* Sets the source of the image to be cropped.
*
* @param imageSrc the image source
*/
public void setImageSrc(String imageSrc) {
getElement().removeAttribute(IMG_SRC_RESOURCE_ATTRIBUTE);
Comment thread
javier-godoy marked this conversation as resolved.
Resolved
setState("imgSrc", imageSrc);
}

/**
* Sets the source of the image to be cropped to a resource served by the given
* {@link DownloadHandler}.
*
* <p>
* The handler is registered to this component, so the image is served while this
Comment thread
javier-godoy marked this conversation as resolved.
Resolved
* 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()}.
*
* <p>
* 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}.
*
* <p>
* 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);
Comment thread
javier-godoy marked this conversation as resolved.
Resolved
setState("imgSrc", getElement().getAttribute(IMG_SRC_RESOURCE_ATTRIBUTE));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment thread
javier-godoy marked this conversation as resolved.
Resolved
croppedImageDataUri = null;
}

/**
* Gets the source of the image being cropped.
*
* <p>
* 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() {
Expand Down Expand Up @@ -434,7 +487,11 @@
/**
* Returns the cropped image data URI.
*
* @return the cropped image data URI
* <p>
* 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -37,6 +37,7 @@ public ImageCropDemoView() {
addDemo(BasicImageCropDemo.class);
addDemo(UploadImageCropDemo.class);
addDemo(OutputFormatImageCropDemo.class);
addDemo(DownloadHandlerImageCropDemo.class);
setSizeFull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand All @@ -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);
Expand Down
Loading
Loading