- 0
+ 0
jar
diff --git a/src/main/java/com/flowingcode/vaadin/addons/imagecrop/Crop.java b/src/main/java/com/flowingcode/vaadin/addons/imagecrop/Crop.java
new file mode 100644
index 0000000..ccb2b65
--- /dev/null
+++ b/src/main/java/com/flowingcode/vaadin/addons/imagecrop/Crop.java
@@ -0,0 +1,50 @@
+/*-
+ * #%L
+ * Image Crop Add-on
+ * %%
+ * Copyright (C) 2024 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;
+
+/**
+ * Represents crop dimensions.
+ *
+ * The crop dimensions are defined by the unit, x and y coordinates, width, and
+ * height.
+ *
+ * @param unit the unit of the crop dimensions, can be 'px' (pixels) or '%'
+ * (percentage).
+ * @param x the x-coordinate of the cropped area.
+ * @param y the y-coordinate of the cropped area.
+ * @param width the width of the cropped area
+ * @param height the height of the cropped area
+ */
+public record Crop(String unit, int x, int y, int width, int height) {
+
+ /**
+ * Returns a string representation of the Crop object.
+ *
+ * @return A string representing the crop dimensions in the format:
+ * "{ unit: %s, x: %s, y: %s, width: %s, height: %s }"
+ * where %s is replaced by the corresponding value.
+ */
+ @Override
+ public final String toString() {
+ return "{ unit: %s, x: %s, y: %s, width: %s, height: %s }".formatted(unit, x, y, width, height);
+ }
+
+}
diff --git a/src/main/java/com/flowingcode/vaadin/addons/imagecrop/CroppedImageEvent.java b/src/main/java/com/flowingcode/vaadin/addons/imagecrop/CroppedImageEvent.java
new file mode 100644
index 0000000..6f49b54
--- /dev/null
+++ b/src/main/java/com/flowingcode/vaadin/addons/imagecrop/CroppedImageEvent.java
@@ -0,0 +1,57 @@
+/*-
+ * #%L
+ * Image Crop Add-on
+ * %%
+ * Copyright (C) 2024 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.vaadin.flow.component.ComponentEvent;
+import com.vaadin.flow.component.DomEvent;
+import com.vaadin.flow.component.EventData;
+
+/**
+ * Represents an event triggered when an image is cropped and encoded.
+ */
+@DomEvent("cropped-image")
+public class CroppedImageEvent extends ComponentEvent {
+
+ private String croppedImageDataUri;
+
+ /**
+ * Constructs a new CroppedImageEvent.
+ *
+ * @param source the source of the event
+ * @param fromClient true
if the event originated from the client-side,
+ * false
otherwise
+ * @param croppedImageDataUri the data URL of the cropped image
+ */
+ public CroppedImageEvent(ImageCrop source, boolean fromClient,
+ @EventData("event.detail.croppedImageDataUri") String croppedImageDataUri) {
+ super(source, fromClient);
+ this.croppedImageDataUri = croppedImageDataUri;
+ }
+
+ /**
+ * Returns the cropped image data URL.
+ *
+ * @return the cropped image data URL
+ */
+ public String getCroppedImageDataUri() {
+ return this.croppedImageDataUri;
+ }
+}
diff --git a/src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java b/src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java
new file mode 100644
index 0000000..10a0a80
--- /dev/null
+++ b/src/main/java/com/flowingcode/vaadin/addons/imagecrop/ImageCrop.java
@@ -0,0 +1,372 @@
+/*-
+ * #%L
+ * Image Crop Add-on
+ * %%
+ * Copyright (C) 2024 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 java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import org.apache.commons.lang3.StringUtils;
+
+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.shared.Registration;
+
+/**
+ * Component for cropping images based on
+ * react-image-crop
+ * library.
+ * This component allows users to define and manipulate crop areas on images.
+ *
+ * @author Paola De Bartolo / Flowing Code
+ */
+@NpmPackage(value = "react-image-crop", version = "11.0.6")
+@JsModule("./src/image-crop.tsx")
+@Tag("image-crop")
+@CssImport("react-image-crop/dist/ReactCrop.css")
+public class ImageCrop extends ReactAdapterComponent {
+
+ private String croppedImageDataUri;
+
+ /**
+ * Constructs an ImageCrop component with the given image URL.
+ *
+ * @param src the URL of the image to be cropped
+ */
+ public ImageCrop(String src) {
+ this.setImageSrc(src);
+ this.addCroppedImageListener(this::updateCroppedImage);
+ this.croppedImageDataUri = src;
+ }
+
+ /**
+ * Constructs an ImageCrop component with the given image.
+ *
+ * @param image the image to be cropped
+ */
+ public ImageCrop(Image image) {
+ this(image.getSrc());
+ image.getAlt().ifPresent(a -> this.setImageAlt(a));
+ }
+
+ /**
+ * Adds a listener for the {@link CroppedImageEvent} fired when the
+ * cropped image is updated.
+ *
+ * @param listener the listener to be added
+ * @return a registration for the listener, which can be used to remove the
+ * listener
+ */
+ protected Registration addCroppedImageListener(
+ ComponentEventListener listener) {
+ return this.addListener(CroppedImageEvent.class, listener);
+ }
+
+ /**
+ * Updates the cropped image data URI based on the event data.
+ *
+ * @param event the event containing the new cropped image data URI
+ */
+ private void updateCroppedImage(CroppedImageEvent event) {
+ this.croppedImageDataUri = event.getCroppedImageDataUri();
+ }
+
+ /**
+ * Sets the source of the image to be cropped.
+ *
+ * @param imageSrc the image source
+ */
+ public void setImageSrc(String imageSrc) {
+ setState("imgSrc", imageSrc);
+ }
+
+ /**
+ * Gets the source of the image being cropped.
+ *
+ * @return the image source
+ */
+ public String getImageSrc() {
+ return getState("imgSrc", String.class);
+ }
+
+ /**
+ * Sets the alternative information of the image to be cropped.
+ *
+ * @param imageAlt the image alternative information
+ */
+ public void setImageAlt(String imageAlt) {
+ setState("imgAlt", imageAlt);
+ }
+
+ /**
+ * Gets the alternative information of the image being cropped.
+ *
+ * @return the image alternative information
+ */
+ public String getImageAlt() {
+ return getState("imgAlt", String.class);
+ }
+
+ /**
+ * Defines the crop dimensions.
+ *
+ * @param crop the crop dimensions
+ */
+ public void setCrop(Crop crop) {
+ setState("crop", crop);
+ }
+
+ /**
+ * Gets the crop dimensions.
+ *
+ * @param crop the current crop dimensions
+ */
+ public Crop getCrop() {
+ return getState("crop", Crop.class);
+ }
+
+ /**
+ * Sets the aspect ratio of the crop.
+ * For example, 1 for a square or 16/9 for landscape.
+ *
+ * @param aspect the aspect ratio of the crop
+ */
+ public void setAspect(double aspect) {
+ setState("aspect", aspect);
+ }
+
+ /**
+ * Gets the aspect ratio of the crop.
+ *
+ * @return the aspect ratio
+ */
+ public double getAspect() {
+ return getState("aspect", Double.class);
+ }
+
+ /**
+ * Sets whether the crop area should be shown as a circle.
+ * If the aspect ratio is not 1, the circle will be warped into an oval shape.
+ * Defaults to false.
+ *
+ * @param circularCrop true to show the crop area as a circle, false otherwise
+ */
+ public void setCircularCrop(boolean circularCrop) {
+ setState("circularCrop", circularCrop);
+ }
+
+ /**
+ * Gets whether the crop area is shown as a circle.
+ *
+ * @return true if the crop area is a circle, false otherwise
+ */
+ public boolean isCircularCrop() {
+ return getState("circularCrop", Boolean.class);
+ }
+
+ /**
+ * Sets whether the selection can't be disabled if the user clicks outside
+ * the selection area. Defaults to false.
+ *
+ * @param keepSelection true so selection can't be disabled if the user clicks
+ * outside the selection area, false otherwise.
+ */
+ public void setKeepSelection(boolean keepSelection) {
+ setState("keepSelection", keepSelection);
+ }
+
+ /**
+ * Gets whether the selection is enabled.
+ *
+ * @return true if the selection is enabled, false otherwise
+ */
+ public boolean isKeepSelection() {
+ return getState("keepSelection", Boolean.class);
+ }
+
+ /**
+ * Sets whether the user cannot resize or draw a new crop. Defaults to false.
+ *
+ * @param disabled true to disable crop resizing and drawing, false otherwise
+ */
+ public void setDisabled(boolean disabled) {
+ setState("disabled", disabled);
+ }
+
+ /**
+ * Gets whether the crop resizing and drawing is disabled.
+ *
+ * @return true if disabled, false otherwise
+ */
+ public boolean isDisabled() {
+ return getState("disabled", Boolean.class);
+ }
+
+ /**
+ * Sets whether the user cannot create or resize a crop, but can still drag the
+ * existing crop around. Defaults to false.
+ *
+ * @param locked true to lock the crop, false otherwise
+ */
+ public void setLocked(boolean locked) {
+ setState("locked", locked);
+ }
+
+ /**
+ * Gets whether the crop is locked.
+ *
+ * @return true if the crop is locked, false otherwise
+ */
+ public boolean isLocked() {
+ return getState("locked", Boolean.class);
+ }
+
+ /**
+ * Sets a minimum crop width, in pixels.
+ *
+ * @param minWidth the minimum crop width
+ */
+ public void setCropMinWidth(Integer minWidth) {
+ setState("minWidth", minWidth);
+ }
+
+ /**
+ * Gets the minimum crop width, in pixels.
+ *
+ * @return the minimum crop width
+ */
+ public Integer getCropMinWidth() {
+ return getState("minWidth", Integer.class);
+ }
+
+ /**
+ * Sets a minimum crop height, in pixels.
+ *
+ * @param minHeight the minimum crop height
+ */
+ public void setCropMinHeight(Integer minHeight) {
+ setState("minHeight", minHeight);
+ }
+
+ /**
+ * Gets the minimum crop height, in pixels.
+ *
+ * @return the minimum crop height
+ */
+ public Integer getCropMinHeight() {
+ return getState("minHeight", Integer.class);
+ }
+
+ /**
+ * Sets a maximum crop width, in pixels.
+ *
+ * @param maxWidth the maximum crop width
+ */
+ public void setCropMaxWidth(Integer maxWidth) {
+ setState("maxWidth", maxWidth);
+ }
+
+ /**
+ * Gets the maximum crop width, in pixels.
+ *
+ * @return the maximum crop width
+ */
+ public Integer getCropMaxWidth() {
+ return getState("maxWidth", Integer.class);
+ }
+
+ /**
+ * Sets a maximum crop height, in pixels.
+ *
+ * @param maxHeight the maximum crop height
+ */
+ public void setCropMaxHeight(Integer maxHeight) {
+ setState("maxHeight", maxHeight);
+ }
+
+ /**
+ * Gets the maximum crop height, in pixels.
+ *
+ * @return the maximum crop height
+ */
+ public Integer getCropMaxHeight() {
+ return getState("maxHeight", Integer.class);
+ }
+
+ /**
+ * Sets whether to show rule of thirds lines in the cropped area. Defaults to
+ * false.
+ *
+ * @param ruleOfThirds true to show rule of thirds lines, false otherwise
+ */
+ public void setRuleOfThirds(boolean ruleOfThirds) {
+ setState("ruleOfThirds", ruleOfThirds);
+ }
+
+ /**
+ * Gets whether rule of thirds lines are shown in the cropped area.
+ *
+ * @return true if rule of thirds lines are shown, false otherwise
+ */
+ public boolean isRuleOfThirds() {
+ return getState("ruleOfThirds", Boolean.class);
+ }
+
+ /**
+ * Returns the cropped image data URI.
+ *
+ * @return the cropped image data URI
+ */
+ public String getCroppedImageDataUri() {
+ return this.croppedImageDataUri;
+ }
+
+ /**
+ * 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
+ * string.
+ *
+ *
+ * This method incorporates work licensed under MIT. Copyright 2021-2023 David "F0rce" Dodlek
+ * https://github.com/F0rce/cropper
+ *
+ *
+ * @return byte[] the decoded byte array of the cropped image
+ */
+ public byte[] getCroppedImageBase64() {
+ String croppedDataUri = this.getCroppedImageDataUri();
+ if (StringUtils.isBlank(croppedDataUri)) {
+ return null;
+ }
+
+ String base64Data = croppedDataUri;
+ if (croppedDataUri.contains("base64,")) {
+ base64Data = croppedDataUri.split(",")[1];
+ }
+
+ return Base64.getDecoder().decode(base64Data.getBytes(StandardCharsets.UTF_8));
+ }
+
+}
diff --git a/src/main/java/com/flowingcode/vaadin/addons/template/TemplateAddon.java b/src/main/java/com/flowingcode/vaadin/addons/template/TemplateAddon.java
deleted file mode 100644
index ab06f5a..0000000
--- a/src/main/java/com/flowingcode/vaadin/addons/template/TemplateAddon.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*-
- * #%L
- * Template Add-on
- * %%
- * Copyright (C) 2023 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.template;
-
-import com.vaadin.flow.component.Tag;
-import com.vaadin.flow.component.dependency.JsModule;
-import com.vaadin.flow.component.dependency.NpmPackage;
-import com.vaadin.flow.component.html.Div;
-
-@SuppressWarnings("serial")
-@NpmPackage(value = "@polymer/paper-input", version = "3.2.1")
-@JsModule("@polymer/paper-input/paper-input.js")
-@Tag("paper-input")
-public class TemplateAddon extends Div {}
diff --git a/src/main/resources/META-INF/resources/frontend/src/image-crop.tsx b/src/main/resources/META-INF/resources/frontend/src/image-crop.tsx
new file mode 100644
index 0000000..54f5d7c
--- /dev/null
+++ b/src/main/resources/META-INF/resources/frontend/src/image-crop.tsx
@@ -0,0 +1,172 @@
+/*-
+ * #%L
+ * Image Crop Add-on
+ * %%
+ * Copyright (C) 2024 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%
+ */
+
+import { ReactAdapterElement, RenderHooks } from 'Frontend/generated/flow/ReactAdapter';
+import { JSXElementConstructor, ReactElement, useRef } from "react";
+import React from 'react';
+import { type Crop, ReactCrop, PixelCrop, makeAspectCrop, centerCrop } from "react-image-crop";
+
+class ImageCropElement extends ReactAdapterElement {
+
+ protected render(hooks: RenderHooks): ReactElement> | null {
+
+ const [crop, setCrop] = hooks.useState("crop");
+ const [imgSrc] = hooks.useState("imgSrc");
+ const imgRef = useRef(null);
+ const [imgAlt] = hooks.useState("imgAlt");
+ const [aspect] = hooks.useState("aspect");
+ const [circularCrop] = hooks.useState("circularCrop", false);
+ const [keepSelection] = hooks.useState("keepSelection", false);
+ const [disabled] = hooks.useState("disabled", false);
+ const [locked] = hooks.useState("locked", false);
+ const [minWidth] = hooks.useState("minWidth");
+ const [minHeight] = hooks.useState("minHeight");
+ const [maxWidth] = hooks.useState("maxWidth");
+ const [maxHeight] = hooks.useState("maxHeight");
+ const [ruleOfThirds] = hooks.useState("ruleOfThirds", false);
+
+ const onImageLoad = () => {
+ if (imgRef.current && crop) {
+ const { width, height } = imgRef.current;
+ const newcrop = centerCrop(
+ makeAspectCrop(
+ {
+ unit: crop.unit,
+ width: crop.width,
+ height: crop.height,
+ x: crop.x,
+ y: crop.y
+ },
+ aspect,
+ width,
+ height
+ ),
+ width,
+ height
+ )
+ setCrop(newcrop);
+ }
+ };
+
+ const onChange = (c: Crop) => {
+ setCrop(c);
+ };
+
+ const onComplete = (c: PixelCrop) => {
+ croppedImageEncode(c);
+ };
+
+ const croppedImageEncode = (completedCrop: PixelCrop) => {
+ if (completedCrop) {
+
+ // get the image element
+ const image = imgRef.current;
+
+ // create a canvas element to draw the cropped image
+ const canvas = document.createElement("canvas");
+
+ // draw the image on the canvas
+ if (image) {
+ const ccrop = completedCrop;
+ const scaleX = image.naturalWidth / image.width;
+ const scaleY = image.naturalHeight / image.height;
+ const ctx = canvas.getContext("2d");
+ const pixelRatio = window.devicePixelRatio;
+ canvas.width = ccrop.width * pixelRatio * scaleX;
+ canvas.height = ccrop.height * pixelRatio * scaleY;
+
+ if (ctx) {
+ ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
+ ctx.imageSmoothingQuality = "high";
+
+ ctx.save();
+
+ if (circularCrop) {
+ canvas.width = ccrop.width;
+ canvas.height = ccrop.height;
+
+ ctx.beginPath();
+
+ ctx.arc(ccrop.width / 2, ccrop.height / 2, ccrop.height / 2, 0, Math.PI * 2, true);
+ ctx.closePath();
+ ctx.clip();
+ }
+
+ ctx.drawImage(
+ image,
+ ccrop.x * scaleX,
+ ccrop.y * scaleY,
+ ccrop.width * scaleX,
+ ccrop.height * scaleY,
+ 0,
+ 0,
+ ccrop.width,
+ ccrop.height
+ );
+
+ ctx.restore();
+ }
+
+ // get the cropped image
+ let croppedImageDataUri = canvas.toDataURL("image/png", 1.0);
+
+ // dispatch the event containing cropped image
+ this.fireCroppedImageEvent(croppedImageDataUri);
+ }
+ }
+ }
+
+ return (
+ onChange(c)}
+ onComplete={(c: PixelCrop) => onComplete(c)}
+ circularCrop={circularCrop}
+ aspect={aspect}
+ keepSelection={keepSelection}
+ disabled={disabled}
+ locked={locked}
+ minWidth={minWidth}
+ minHeight={minHeight}
+ maxWidth={maxWidth}
+ maxHeight={maxHeight}
+ ruleOfThirds={ruleOfThirds}
+ >
+
+
+ );
+ }
+
+ private fireCroppedImageEvent(croppedImageDataUri: string) {
+ this.dispatchEvent(
+ new CustomEvent("cropped-image", {
+ detail: {
+ croppedImageDataUri: croppedImageDataUri
+ },
+ })
+ );
+ }
+}
+
+customElements.define("image-crop", ImageCropElement);
\ No newline at end of file
diff --git a/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java b/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java
index 8d29aba..48be86d 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java
@@ -1,8 +1,8 @@
/*-
* #%L
- * Template Add-on
+ * Image Crop Add-on
* %%
- * Copyright (C) 2023 Flowing Code
+ * Copyright (C) 2024 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/BasicImageCropDemo.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/BasicImageCropDemo.java
new file mode 100644
index 0000000..c25566f
--- /dev/null
+++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/BasicImageCropDemo.java
@@ -0,0 +1,61 @@
+/*-
+ * #%L
+ * Image Crop Add-on
+ * %%
+ * Copyright (C) 2024 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;
+
+@DemoSource
+@PageTitle("Basic Image Crop")
+@SuppressWarnings("serial")
+@Route(value = "image-crop/basic", layout = ImageCropDemoView.class)
+public class BasicImageCropDemo extends VerticalLayout {
+
+ private Div croppedResultDiv = new Div();
+
+ 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);
+ 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();
+ 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/template/DemoView.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/DemoView.java
similarity index 86%
rename from src/test/java/com/flowingcode/vaadin/addons/template/DemoView.java
rename to src/test/java/com/flowingcode/vaadin/addons/imagecrop/DemoView.java
index a6a1d28..c698d23 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/template/DemoView.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/DemoView.java
@@ -1,8 +1,8 @@
/*-
* #%L
- * Template Add-on
+ * Image Crop Add-on
* %%
- * Copyright (C) 2023 Flowing Code
+ * Copyright (C) 2024 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,7 +18,7 @@
* #L%
*/
-package com.flowingcode.vaadin.addons.template;
+package com.flowingcode.vaadin.addons.imagecrop;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEnterEvent;
@@ -31,6 +31,6 @@ public class DemoView extends VerticalLayout implements BeforeEnterObserver {
@Override
public void beforeEnter(BeforeEnterEvent event) {
- event.forwardTo(TemplateDemoView.class);
+ event.forwardTo(ImageCropDemoView.class);
}
}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemoView.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/ImageCropDemoView.java
similarity index 67%
rename from src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemoView.java
rename to src/test/java/com/flowingcode/vaadin/addons/imagecrop/ImageCropDemoView.java
index 1c3aecd..53a5e3d 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemoView.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/ImageCropDemoView.java
@@ -1,8 +1,8 @@
/*-
* #%L
- * Template Add-on
+ * Image Crop Add-on
* %%
- * Copyright (C) 2023 Flowing Code
+ * Copyright (C) 2024 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,9 +17,10 @@
* limitations under the License.
* #L%
*/
-package com.flowingcode.vaadin.addons.template;
+package com.flowingcode.vaadin.addons.imagecrop;
import com.flowingcode.vaadin.addons.DemoLayout;
+import com.flowingcode.vaadin.addons.GithubBranch;
import com.flowingcode.vaadin.addons.GithubLink;
import com.flowingcode.vaadin.addons.demo.TabbedDemo;
import com.vaadin.flow.router.ParentLayout;
@@ -27,12 +28,14 @@
@SuppressWarnings("serial")
@ParentLayout(DemoLayout.class)
-@Route("template")
-@GithubLink("https://github.com/FlowingCode/AddonStarter24")
-public class TemplateDemoView extends TabbedDemo {
+@Route("image-crop")
+@GithubBranch("initial-implementation")
+@GithubLink("https://github.com/FlowingCode/ImageCrop")
+public class ImageCropDemoView extends TabbedDemo {
- public TemplateDemoView() {
- addDemo(TemplateDemo.class);
+ public ImageCropDemoView() {
+ addDemo(BasicImageCropDemo.class);
+ addDemo(UploadImageCropDemo.class);
setSizeFull();
}
}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/imagecrop/UploadImageCropDemo.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/UploadImageCropDemo.java
new file mode 100644
index 0000000..cb91d8e
--- /dev/null
+++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/UploadImageCropDemo.java
@@ -0,0 +1,131 @@
+/*-
+ * #%L
+ * Image Crop Add-on
+ * %%
+ * Copyright (C) 2024 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 java.io.ByteArrayOutputStream;
+import java.util.Base64;
+
+import com.flowingcode.vaadin.addons.demo.DemoSource;
+import com.vaadin.flow.component.avatar.Avatar;
+import com.vaadin.flow.component.avatar.AvatarVariant;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.button.ButtonVariant;
+import com.vaadin.flow.component.dialog.Dialog;
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.Span;
+import com.vaadin.flow.component.notification.Notification;
+import com.vaadin.flow.component.notification.NotificationVariant;
+import com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment;
+import com.vaadin.flow.component.orderedlayout.FlexComponent.JustifyContentMode;
+import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.component.upload.Upload;
+import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
+import com.vaadin.flow.router.PageTitle;
+import com.vaadin.flow.router.Route;
+
+@DemoSource
+@PageTitle("Image Crop with Upload")
+@SuppressWarnings("serial")
+@Route(value = "image-crop/upload", layout = ImageCropDemoView.class)
+public class UploadImageCropDemo extends Div {
+
+ private static final String[] ACCEPTED_MIME_TYPES =
+ {"image/gif", "image/png", "image/jpeg", "image/bmp", "image/webp"};
+
+ private Avatar avatar = new Avatar();
+ private ImageCrop imageCrop = null;
+ private byte[] newCroppedPicture = null;
+
+ public UploadImageCropDemo() {
+ avatar.addThemeVariants(AvatarVariant.LUMO_XLARGE);
+ avatar.setHeight("12em");
+ avatar.setWidth("12em");
+ Div avatarDiv = new Div(avatar);
+
+ MemoryBuffer buffer = new MemoryBuffer();
+ Upload uploadComponent = new Upload(buffer);
+ uploadComponent.setMaxFiles(1);
+ uploadComponent.setMaxFileSize(1024 * 1024 * 10);
+ uploadComponent.setAcceptedFileTypes(ACCEPTED_MIME_TYPES);
+
+ Span uploadCaption = new Span("Upload an image to crop and set as avatar:");
+
+ HorizontalLayout avatarLayout =
+ new HorizontalLayout(avatarDiv, new VerticalLayout(uploadCaption, uploadComponent));
+
+ avatarLayout.setAlignItems(Alignment.CENTER);
+
+ uploadComponent.addFinishedListener(e -> {
+ openCropDialog(((ByteArrayOutputStream) buffer.getFileData().getOutputBuffer()),
+ e.getMIMEType());
+ });
+
+ uploadComponent.addFileRejectedListener(event -> {
+ String errorMessage = event.getErrorMessage();
+ Notification notification =
+ Notification.show(errorMessage, 5000, Notification.Position.MIDDLE);
+ notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
+ });
+
+ add(avatarLayout);
+ }
+
+ private void openCropDialog(ByteArrayOutputStream outputStream, String mimeType) {
+ // Set up image crop dialog
+ Dialog dialog = new Dialog();
+ dialog.setCloseOnOutsideClick(false);
+ dialog.setMaxHeight("100%");
+ dialog.setMaxWidth(dialog.getHeight());
+
+ Button cropButton = new Button("Crop image");
+ Button dialogCancelButton = new Button("Cancel");
+ dialogCancelButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
+
+ String src = getImageAsBase64(outputStream.toByteArray(), mimeType);
+ imageCrop = new ImageCrop(src);
+ imageCrop.setAspect(1.0);
+ imageCrop.setCircularCrop(true);
+ imageCrop.setCrop(new Crop("%", 25, 25, 50, 50)); // centered crop
+ imageCrop.setKeepSelection(true);
+
+ cropButton.addClickListener(event -> {
+ newCroppedPicture = imageCrop.getCroppedImageBase64();
+ avatar.setImage(imageCrop.getCroppedImageDataUri());
+ dialog.close();
+ });
+ dialogCancelButton.addClickListener(c -> dialog.close());
+
+ HorizontalLayout buttonLayout = new HorizontalLayout(dialogCancelButton, cropButton);
+ Div dialogLayout = new Div(imageCrop);
+ dialogLayout.setSizeFull();
+ buttonLayout.setWidthFull();
+ buttonLayout.setJustifyContentMode(JustifyContentMode.END);
+ dialog.add(dialogLayout);
+ dialog.getFooter().add(buttonLayout);
+ dialog.open();
+ }
+
+ private String getImageAsBase64(byte[] src, String mimeType) {
+ return src != null ? "data:" + mimeType + ";base64," + Base64.getEncoder().encodeToString(src)
+ : null;
+ }
+}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/it/AbstractViewTest.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/it/AbstractViewTest.java
similarity index 96%
rename from src/test/java/com/flowingcode/vaadin/addons/template/it/AbstractViewTest.java
rename to src/test/java/com/flowingcode/vaadin/addons/imagecrop/it/AbstractViewTest.java
index c2fda15..fed403a 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/template/it/AbstractViewTest.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/it/AbstractViewTest.java
@@ -1,8 +1,8 @@
/*-
* #%L
- * Template Add-on
+ * Image Crop Add-on
* %%
- * Copyright (C) 2023 Flowing Code
+ * Copyright (C) 2024 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,7 +18,7 @@
* #L%
*/
-package com.flowingcode.vaadin.addons.template.it;
+package com.flowingcode.vaadin.addons.imagecrop.it;
import com.vaadin.testbench.ScreenshotOnFailureRule;
import com.vaadin.testbench.TestBench;
diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/it/ViewIT.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/it/ViewIT.java
similarity index 87%
rename from src/test/java/com/flowingcode/vaadin/addons/template/it/ViewIT.java
rename to src/test/java/com/flowingcode/vaadin/addons/imagecrop/it/ViewIT.java
index 16a2a12..13609cf 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/template/it/ViewIT.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/it/ViewIT.java
@@ -1,8 +1,8 @@
/*-
* #%L
- * Template Add-on
+ * Image Crop Add-on
* %%
- * Copyright (C) 2023 Flowing Code
+ * Copyright (C) 2024 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,15 +18,15 @@
* #L%
*/
-package com.flowingcode.vaadin.addons.template.it;
+package com.flowingcode.vaadin.addons.imagecrop.it;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
-import static org.junit.Assert.assertThat;
import com.vaadin.testbench.TestBenchElement;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
+import org.hamcrest.MatcherAssert;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.junit.Test;
@@ -58,7 +58,7 @@ protected boolean matchesSafely(TestBenchElement item, Description mismatchDescr
@Test
public void componentWorks() {
- TestBenchElement element = $("paper-input").first();
- assertThat(element, hasBeenUpgradedToCustomElement);
+ TestBenchElement element = $("image-crop").first();
+ MatcherAssert.assertThat(element, hasBeenUpgradedToCustomElement);
}
}
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
new file mode 100644
index 0000000..6b351d2
--- /dev/null
+++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/ImageCropTest.java
@@ -0,0 +1,67 @@
+package com.flowingcode.vaadin.addons.imagecrop.test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Base64;
+
+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;
+
+public class ImageCropTest {
+
+ private ImageCrop imageCrop;
+
+ @Before
+ public void setUp() {
+ imageCrop = new ImageCrop("dummyImageSrc");
+ }
+
+ @Test
+ public void testSetAndGetImageSrc() {
+ String expectedSrc = "newImageSrc";
+ imageCrop.setImageSrc(expectedSrc);
+ assertEquals(expectedSrc, imageCrop.getImageSrc());
+ }
+
+ @Test
+ public void testSetAndGetCrop() {
+ Crop expectedCrop = new Crop("%", 10, 10, 200, 200);
+ imageCrop.setCrop(expectedCrop);
+ assertEquals(expectedCrop, imageCrop.getCrop());
+ }
+
+ @Test
+ public void testSetAndGetAspect() {
+ Double expectedAspect = 16.0 / 9.0;
+ imageCrop.setAspect(expectedAspect);
+ assertEquals(expectedAspect, Double.valueOf(imageCrop.getAspect()));
+ }
+
+ @Test
+ public void testEncodedCroppedImageEvent() {
+ String expectedCroppedImageUri = "croppedImageUri";
+ CroppedImageEvent event = mock(CroppedImageEvent.class);
+ when(event.getCroppedImageDataUri()).thenReturn(expectedCroppedImageUri);
+ imageCrop = mock(ImageCrop.class);
+ when(imageCrop.getCroppedImageDataUri()).thenReturn(expectedCroppedImageUri);
+ assertEquals(expectedCroppedImageUri, imageCrop.getCroppedImageDataUri());
+ }
+
+ @Test
+ public void testGetCroppedImageBase64() {
+ byte[] expectedCroppedImageBytes = Base64.getDecoder().decode("SGVsbG8gV29ybGQ=");
+ imageCrop = mock(ImageCrop.class);
+ when(imageCrop.getCroppedImageBase64()).thenReturn(expectedCroppedImageBytes);
+ byte[] actualCroppedImageBytes = imageCrop.getCroppedImageBase64();
+ assertNotNull(actualCroppedImageBytes);
+ assertArrayEquals(expectedCroppedImageBytes, actualCroppedImageBytes);
+ }
+}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/test/SerializationTest.java b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/SerializationTest.java
similarity index 84%
rename from src/test/java/com/flowingcode/vaadin/addons/template/test/SerializationTest.java
rename to src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/SerializationTest.java
index dcf9b4e..37fafe2 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/template/test/SerializationTest.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/imagecrop/test/SerializationTest.java
@@ -1,8 +1,8 @@
/*-
* #%L
- * Template Add-on
+ * Image Crop Add-on
* %%
- * Copyright (C) 2023 Flowing Code
+ * Copyright (C) 2024 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,9 +17,8 @@
* limitations under the License.
* #L%
*/
-package com.flowingcode.vaadin.addons.template.test;
+package com.flowingcode.vaadin.addons.imagecrop.test;
-import com.flowingcode.vaadin.addons.template.TemplateAddon;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -28,6 +27,9 @@
import org.junit.Assert;
import org.junit.Test;
+import com.flowingcode.vaadin.addons.imagecrop.ImageCrop;
+import com.vaadin.flow.component.html.Image;
+
public class SerializationTest {
private void testSerializationOf(Object obj) throws IOException, ClassNotFoundException {
@@ -44,7 +46,7 @@ private void testSerializationOf(Object obj) throws IOException, ClassNotFoundEx
@Test
public void testSerialization() throws ClassNotFoundException, IOException {
try {
- testSerializationOf(new TemplateAddon());
+ testSerializationOf(new ImageCrop(new Image()));
} catch (Exception e) {
Assert.fail("Problem while testing serialization: " + e.getMessage());
}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemo.java b/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemo.java
deleted file mode 100644
index 5f6e6ee..0000000
--- a/src/test/java/com/flowingcode/vaadin/addons/template/TemplateDemo.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.flowingcode.vaadin.addons.template;
-
-import com.flowingcode.vaadin.addons.demo.DemoSource;
-import com.vaadin.flow.component.html.Div;
-import com.vaadin.flow.router.PageTitle;
-import com.vaadin.flow.router.Route;
-
-@DemoSource
-@PageTitle("Template Add-on Demo")
-@SuppressWarnings("serial")
-@Route(value = "demo", layout = TemplateDemoView.class)
-public class TemplateDemo extends Div {
-
- public TemplateDemo() {
- add(new TemplateAddon());
- }
-}
diff --git a/src/test/resources/META-INF/frontend/styles/shared-styles.css b/src/test/resources/META-INF/frontend/styles/shared-styles.css
deleted file mode 100644
index 6680e2d..0000000
--- a/src/test/resources/META-INF/frontend/styles/shared-styles.css
+++ /dev/null
@@ -1 +0,0 @@
-/*Demo styles*/
\ No newline at end of file
diff --git a/src/test/resources/META-INF/resources/images/empty-plant.png b/src/test/resources/META-INF/resources/images/empty-plant.png
new file mode 100644
index 0000000..9777f26
Binary files /dev/null and b/src/test/resources/META-INF/resources/images/empty-plant.png differ