diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03255e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target/ +.project +.classpath +.settings/ diff --git a/README.md b/README.md index e3c1a76..da9fd12 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,82 @@ -# primefaces-imageeditor -Image Editor Component for Primefaces +ImageEditor Component for PrimeFaces +==================================== + +About +----- + +This package provides a JSF (Java Server Faces) component *ImageEditor* as addition to the +commonly used [PrimeFaces](http://www.primefaces.org) widget library. + +**Author**: Christian Simon +**Copyright**: illucIT Software GmbH +**Website**: [illucit.com](http://www.illucit.com) +**License**: Apache License 2.0 (see LICENSE file) + +Compatibility: +-------------- + +*ImageEditor* is written for and tested with **PrimeFaces 5.2** and **JSF 2.2**. +Due to changes in the PrimeFaces API for streamed data, the library is not compatible with earlies PrimeFaces versions without modifications. + +Setup +----- + +The *ImageEditor* component can either be downloaded directly on Github or included via Maven. + +If you want to use Maven to add the library to your web project, you first need to add the public illucIT Maven Repository, as the library is not published on Maven Central, yet. + + + + illucit + illucIT Maven Repository + http://repository.illucit.com + + true + + + + +Then just add the Maven artifact to your dependencies: + + + + com.illucit + primefaces-imageeditor + ${version.imageeditor} + + + +Usage in JSF +------------ + +The library provides a taglib including the `imageEditor` component. +In order to use the component, first declare a namespace for the taglib in your JSF source file (where your also would include the namespace for PrimeFaces): + + + +Then you can use the `imageEditor` tag in your facelet file. + + + + + +The following parameters are required for the `imageEditor` component to work correctly: +* `value`: Expression of a method returing a `StreamedContent` object containing the image data. +Every parameter child element of type `` will be attached to the image request. +Note: As the image is requested by a normal GET request, no view scope can be used for this as the view id will not be transmitted. +* `fileUploadListener`: Bean method accepting an `ImageEditedEvent` object. +The method is called when the save button is pressed. The given object contains the binary image data. + + +Disclaimer: +----------- + +ImageEditor is free software and comes with NO WARRANTY! diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0a3ab60 --- /dev/null +++ b/pom.xml @@ -0,0 +1,109 @@ + + + + 4.0.0 + + + com.illucit + illucit-parent + 2 + + primefaces-imageeditor + 1.0.0 + jar + + Primefaces ImageEditor Component + Component for Primefaces which provides an image editor for painting into images + + http://www.illucit.com + + + Apache License, Version 2.0 + repo + http://www.apache.org/licenses/LICENSE-2.0.html + + + + + scm:git:${scm.connection} + scm:git:${scm.connection} + ${scm.url} + + + + + csimon + Christian Simon + simon@illucit.com + https://github.com/metaxmx + + + dwieth + Daniel Wieth + wieth@illucit.com + https://github.com/danielwieth + + + + + + git@github.com:illucIT/primefaces-imageeditor.git + https://github.com/illucIT/primefaces-imageeditor + + + 5.2 + + + 2.2.0 + 2.2 + + + 1.4.12 + + + 1.7 + 1.7 + + + + + org.primefaces + primefaces + ${version.primefaces} + + + org.webjars + fabric.js + ${version.fabricjs} + + + org.glassfish + javax.faces + ${version.faces} + provided + + + javax.el + el-api + ${version.el} + provided + + + + + ${project.artifactId} + + + + + illucit + illucIT Maven Repository + http://repository.illucit.com + + true + + + + + diff --git a/src/main/java/com/illucit/faces/component/imageeditor/ImageEditedEvent.java b/src/main/java/com/illucit/faces/component/imageeditor/ImageEditedEvent.java new file mode 100644 index 0000000..a5f411a --- /dev/null +++ b/src/main/java/com/illucit/faces/component/imageeditor/ImageEditedEvent.java @@ -0,0 +1,30 @@ +package com.illucit.faces.component.imageeditor; + +import javax.faces.component.UIComponent; + +import org.primefaces.event.FileUploadEvent; +import org.primefaces.model.UploadedFile; + +/** + * Event to indicate that the image was stored by {@link ImageEditor} action. + * + * @author Christian Simon + * + */ +public class ImageEditedEvent extends FileUploadEvent { + + private static final long serialVersionUID = -2624800646668578507L; + + /** + * Create event. + * + * @param component + * component that fired the event + * @param file + * uploaded file data + */ + public ImageEditedEvent(UIComponent component, UploadedFile file) { + super(component, file); + } + +} diff --git a/src/main/java/com/illucit/faces/component/imageeditor/ImageEditor.java b/src/main/java/com/illucit/faces/component/imageeditor/ImageEditor.java new file mode 100644 index 0000000..38206c1 --- /dev/null +++ b/src/main/java/com/illucit/faces/component/imageeditor/ImageEditor.java @@ -0,0 +1,187 @@ +package com.illucit.faces.component.imageeditor; + +import javax.el.MethodExpression; +import javax.faces.application.ResourceDependencies; +import javax.faces.application.ResourceDependency; +import javax.faces.component.UINamingContainer; +import javax.faces.component.UIPanel; +import javax.faces.context.FacesContext; +import javax.faces.event.AbortProcessingException; +import javax.faces.event.FacesEvent; + +import org.primefaces.component.api.Widget; + +/** + * Image Editor component. + * + * @author Christian Simon + * + */ +//@formatter:off +@ResourceDependencies({ + @ResourceDependency(library="primefaces", name="jquery/jquery.js"), + @ResourceDependency(library="primefaces", name="primefaces.js"), + @ResourceDependency(library="webjars/fabric.js/1.4.12", name="fabric.js"), + @ResourceDependency(library="illufaces", name="imageeditor.js"), + @ResourceDependency(library="illufaces", name="imageeditor.css") +}) +//@formatter:on +public class ImageEditor extends UIPanel implements Widget { + + public static final String COMPONENT_TYPE = "com.illucit.faces.component.ImageEditor"; + public static final String COMPONENT_FAMILY = "com.illucit.faces.component"; + private static final String DEFAULT_RENDERER = "com.illucit.faces.component.ImageEditorRenderer"; + + protected enum PropertyKeys { + style, styleClass, value, widgetVar, initialColor, initialShape, fileUploadListener, disabled, labelSave, labelDownload, onsuccess, onerror + } + + public ImageEditor() { + setRendererType(DEFAULT_RENDERER); + } + + public String getFamily() { + return COMPONENT_FAMILY; + } + + public String getStyle() { + return (String) getStateHelper().eval(PropertyKeys.style, null); + } + + public void setStyle(String _style) { + getStateHelper().put(PropertyKeys.style, _style); + } + + public String getStyleClass() { + return (String) getStateHelper().eval(PropertyKeys.styleClass, null); + } + + public void setStyleClass(String _styleClass) { + getStateHelper().put(PropertyKeys.styleClass, _styleClass); + } + + public Object getValue() { + return getStateHelper().eval(PropertyKeys.value); + } + + public void setValue(Object value) { + getStateHelper().put(PropertyKeys.value, value); + } + + public String getWidgetVar() { + return (String) getStateHelper().eval(PropertyKeys.widgetVar, null); + } + + public void setWidgetVar(String _widgetVar) { + getStateHelper().put(PropertyKeys.widgetVar, _widgetVar); + } + + public String getInitialColor() { + return (String) getStateHelper().eval(PropertyKeys.initialColor, "000000"); + } + + public void setInitialColor(String _initialColor) { + getStateHelper().put(PropertyKeys.initialColor, _initialColor); + } + + public String getInitialShape() { + return (String) getStateHelper().eval(PropertyKeys.initialShape, "rect"); + } + + public void setInitialShape(String _initialShape) { + getStateHelper().put(PropertyKeys.initialShape, _initialShape); + } + + public javax.el.MethodExpression getFileUploadListener() { + return (MethodExpression) getStateHelper().eval(PropertyKeys.fileUploadListener, null); + } + + public void setFileUploadListener(MethodExpression _fileUploadListener) { + getStateHelper().put(PropertyKeys.fileUploadListener, _fileUploadListener); + } + + public boolean isDisabled() { + return (Boolean) getStateHelper().eval(PropertyKeys.disabled, false); + } + + public void setDisabled(boolean _disabled) { + getStateHelper().put(PropertyKeys.disabled, _disabled); + } + + public String getLabelSave() { + return (String) getStateHelper().eval(PropertyKeys.labelSave, "Save"); + } + + public void setLabelSave(String _labelSave) { + getStateHelper().put(PropertyKeys.labelSave, _labelSave); + } + + public String getLabelDownload() { + return (String) getStateHelper().eval(PropertyKeys.labelDownload, "Download"); + } + + public void setLabelDownload(String _labelDownload) { + getStateHelper().put(PropertyKeys.labelDownload, _labelDownload); + } + + public String getOnsuccess() { + return (String) getStateHelper().eval(PropertyKeys.onsuccess, null); + } + + public void setOnsuccess(String _onsuccess) { + getStateHelper().put(PropertyKeys.onsuccess, _onsuccess); + } + + public String getOnerror() { + return (String) getStateHelper().eval(PropertyKeys.onerror, null); + } + + public void setOnerror(String _onerror) { + getStateHelper().put(PropertyKeys.onerror, _onerror); + } + + /* + * Utility functions + */ + + public String resolveStyleClass() { + String styleClass = "ui-panel ui-widget ui-widget-content ui-corner-all ui-image-editor"; + String userClass = getStyleClass(); + if (userClass != null && !userClass.isEmpty()) { + styleClass += " " + userClass; + } + return styleClass; + } + + /* + * Events + */ + + @Override + public void broadcast(FacesEvent event) throws AbortProcessingException { + super.broadcast(event); + + FacesContext facesContext = getFacesContext(); + MethodExpression me = getFileUploadListener(); + + if (me != null && event instanceof ImageEditedEvent) { + me.invoke(facesContext.getELContext(), new Object[] { event }); + } + } + + /* + * Widget + */ + + @Override + public String resolveWidgetVar() { + FacesContext context = getFacesContext(); + String userWidgetVar = (String) getAttributes().get("widgetVar"); + + if (userWidgetVar != null) + return userWidgetVar; + else + return "widget_" + getClientId(context).replaceAll("-|" + UINamingContainer.getSeparatorChar(context), "_"); + } + +} diff --git a/src/main/java/com/illucit/faces/component/imageeditor/ImageEditorComponentHandler.java b/src/main/java/com/illucit/faces/component/imageeditor/ImageEditorComponentHandler.java new file mode 100644 index 0000000..116a3ce --- /dev/null +++ b/src/main/java/com/illucit/faces/component/imageeditor/ImageEditorComponentHandler.java @@ -0,0 +1,145 @@ +package com.illucit.faces.component.imageeditor; + +import javax.faces.application.Application; +import javax.faces.component.UIComponent; +import javax.faces.component.UIPanel; +import javax.faces.component.UISelectItem; +import javax.faces.view.facelets.ComponentConfig; +import javax.faces.view.facelets.ComponentHandler; +import javax.faces.view.facelets.FaceletContext; +import javax.faces.view.facelets.MetaRule; +import javax.faces.view.facelets.MetaRuleset; + +import org.primefaces.component.button.Button; +import org.primefaces.component.selectonebutton.SelectOneButton; +import org.primefaces.component.toolbar.Toolbar; +import org.primefaces.facelets.MethodRule; + +import com.sun.faces.facelets.compiler.UILiteralText; + +/** + * Component Handler for Image Editor component. + * + * @author Christian Simon + * + */ +public class ImageEditorComponentHandler extends ComponentHandler { + + private final static String IMPLICIT_PANEL = "com.sun.faces.facelets.IMPLICIT_PANEL"; + + private static final MetaRule FILE_UPLOAD_LISTENER = new MethodRule("fileUploadListener", null, + new Class[] { ImageEditedEvent.class }); + + public ImageEditorComponentHandler(ComponentConfig config) { + super(config); + } + + @SuppressWarnings("rawtypes") + @Override + protected MetaRuleset createMetaRuleset(Class type) { + MetaRuleset metaRuleset = super.createMetaRuleset(type); + metaRuleset.addRule(FILE_UPLOAD_LISTENER); + return metaRuleset; + } + + @Override + public void onComponentCreated(FaceletContext ctx, UIComponent c, UIComponent parent) { + + ImageEditor ed = (ImageEditor) c; + + Application app = ctx.getFacesContext().getApplication(); + + String baseId = c.getId() + "_"; + + Toolbar toolbar = (Toolbar) app.createComponent(Toolbar.COMPONENT_TYPE); + toolbar.setId(baseId + "image-editor-toolbar"); + c.getChildren().add(toolbar); + + // "left" facet + String leftFacetName = "left"; + UIComponent leftPanelGroup = app.createComponent(UIPanel.COMPONENT_TYPE); + toolbar.getFacets().put(leftFacetName, leftPanelGroup); + leftPanelGroup.getAttributes().put(IMPLICIT_PANEL, true); + + // Clear + addButton(app, c, leftPanelGroup, "clear-button", "ui-icon-trash", null); + + // || + leftPanelGroup.getChildren().add(createSeparator()); + + // Line + + SelectOneButton drawSelection = (SelectOneButton) app.createComponent(SelectOneButton.COMPONENT_TYPE); + drawSelection.setId(baseId + "draw-selection"); + drawSelection.setWidgetVar(ed.getWidgetVar() + "DrawSelection"); + drawSelection.getChildren().add(getSelectItem("rect", "Square")); + drawSelection.getChildren().add(getSelectItem("ellipse", "Ellipsis")); + drawSelection.getChildren().add(getSelectItem("line", "Line")); + drawSelection.setValue(ed.getInitialShape()); + drawSelection.setOnchange("PrimeFaces.widgets." + ed.resolveWidgetVar() + ".onSelectedShapeChanged()"); + leftPanelGroup.getChildren().add(drawSelection); + + // || + leftPanelGroup.getChildren().add(createSeparator()); + + // Color Picker + UILiteralText colorChooser = new UILiteralText(""); + leftPanelGroup.getChildren().add(colorChooser); + + // || + leftPanelGroup.getChildren().add(createSeparator()); + + // Rotate + addButton(app, c, leftPanelGroup, "rotate-ccw-button", "ui-icon-arrowreturnthick-1-w", null); + addButton(app, c, leftPanelGroup, "rotate-cw-button", "ui-icon-arrowreturnthick-1-w ui-icon-mirror-horizontal", + null); + + // || + leftPanelGroup.getChildren().add(createSeparator()); + + // Undo Button + addButton(app, c, leftPanelGroup, "undo-button", "ui-icon-arrowrefresh-1-s ui-icon-mirror-horizontal", null); + + // "right" facet + String rightFacetName = "right"; + UIComponent rightPanelGroup = app.createComponent(UIPanel.COMPONENT_TYPE); + toolbar.getFacets().put(rightFacetName, rightPanelGroup); + rightPanelGroup.getAttributes().put(IMPLICIT_PANEL, true); + + // Save + addButton(app, c, rightPanelGroup, "save-button", "ui-icon-disk", ed.getLabelSave()); + + // Download + addButton(app, c, rightPanelGroup, "download-button", "ui-icon-arrowthick-1-s", ed.getLabelDownload()); + + } + + private UISelectItem getSelectItem(String value, String label) { + UISelectItem item = new UISelectItem(); + item.setItemValue(value); + item.setItemLabel(label); + return item; + } + + private void addButton(Application app, UIComponent parent, UIComponent panel, String idSuffix, String icon, + String value) { + Button btn = (Button) app.createComponent(Button.COMPONENT_TYPE); + btn.setId(parent.getId() + "_" + idSuffix); + if (value != null) { + btn.setValue(value); + } + if (icon != null) { + btn.setIcon(icon); + } + btn.setOnclick("return false"); + panel.getChildren().add(btn); + } + + private UIComponent createSeparator() { + return new UILiteralText("" + + ""); + } + +} diff --git a/src/main/java/com/illucit/faces/component/imageeditor/ImageEditorRenderer.java b/src/main/java/com/illucit/faces/component/imageeditor/ImageEditorRenderer.java new file mode 100644 index 0000000..10ff6b9 --- /dev/null +++ b/src/main/java/com/illucit/faces/component/imageeditor/ImageEditorRenderer.java @@ -0,0 +1,106 @@ +package com.illucit.faces.component.imageeditor; + +import static org.primefaces.application.resource.DynamicContentType.STREAMED_CONTENT; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import javax.faces.component.UIComponent; +import javax.faces.context.FacesContext; +import javax.faces.context.ResponseWriter; + +import org.primefaces.renderkit.CoreRenderer; +import org.primefaces.util.Base64; +import org.primefaces.util.DynamicResourceBuilder; +import org.primefaces.util.WidgetBuilder; + +/** + * Renderer for {@link ImageEditor}. + * + * @author Christian Simon + * + */ +public class ImageEditorRenderer extends CoreRenderer { + + @Override + public void encodeEnd(FacesContext context, UIComponent component) throws IOException { + ImageEditor editor = (ImageEditor) component; + + encodeMarkup(context, editor); + encodeScript(context, editor); + } + + @Override + public void encodeChildren(FacesContext context, UIComponent component) throws IOException { + // Do nothing + } + + @Override + public boolean getRendersChildren() { + return true; + } + + protected void encodeMarkup(FacesContext context, ImageEditor editor) throws IOException { + ResponseWriter writer = context.getResponseWriter(); + String clientId = editor.getClientId(context); + + writer.startElement("div", editor); + writer.writeAttribute("id", clientId, "id"); + writer.writeAttribute("name", clientId, "name"); + writer.writeAttribute("class", editor.resolveStyleClass(), "styleClass"); + if (editor.getStyle() != null) { + writer.writeAttribute("style", editor.getStyle(), "style"); + } + + renderChildren(context, editor); + + writer.startElement("div", null); + writer.writeAttribute("class", "ui-image-editor-canvas-container", null); + + writer.startElement("canvas", null); + writer.writeAttribute("id", clientId + "_canvas", null); + writer.writeAttribute("class", "imageEditor", null); + writer.endElement("canvas"); + + writer.endElement("div"); + + writer.endElement("div"); + } + + protected void encodeScript(FacesContext context, ImageEditor editor) throws IOException { + String clientId = editor.getClientId(context); + WidgetBuilder wb = getWidgetBuilder(context); + wb.init("ImageEditor", editor.resolveWidgetVar(), clientId); + wb.attr("imageSource", getImageSrc(context, editor)); + wb.attr("initialShape", editor.getInitialShape()); + wb.callback("onsuccess", "function()", editor.getOnsuccess()); + wb.callback("onerror", "function()", editor.getOnerror()); + wb.finish(); + } + + protected String getImageSrc(FacesContext context, ImageEditor editor) { + try { + return DynamicResourceBuilder.build(context, editor.getValue(), editor, false, STREAMED_CONTENT); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + @Override + public void decode(FacesContext context, UIComponent component) { + ImageEditor imageEditor = (ImageEditor) component; + if (!imageEditor.isDisabled()) { + String dataUrl = context.getExternalContext().getRequestParameterMap() + .get(imageEditor.getClientId(context) + "_save"); + if (dataUrl != null) { + String encodingPrefix = "base64,"; + int contentStartIndex = dataUrl.indexOf(encodingPrefix) + encodingPrefix.length(); + byte[] imageData = Base64.decode(dataUrl.substring(contentStartIndex)); + imageEditor.setTransient(true); + imageEditor.queueEvent(new ImageEditedEvent(imageEditor, new InMemoryUploadedFile(imageData, + "image.jpg", "image/jpg"))); + } + } + } + +} diff --git a/src/main/java/com/illucit/faces/component/imageeditor/InMemoryUploadedFile.java b/src/main/java/com/illucit/faces/component/imageeditor/InMemoryUploadedFile.java new file mode 100644 index 0000000..9310787 --- /dev/null +++ b/src/main/java/com/illucit/faces/component/imageeditor/InMemoryUploadedFile.java @@ -0,0 +1,82 @@ +package com.illucit.faces.component.imageeditor; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.primefaces.model.UploadedFile; + +/** + * {@link UploadedFile} implementation for in-memory data (small image data). + * + * @author Christian Simon + * + */ +public class InMemoryUploadedFile implements UploadedFile { + + private final byte[] data; + + private final String filename; + + private final String contentType; + + /** + * Create instance with fixed data. + * + * @param data + * byte data + * @param filename + * filename of the virtual file + * @param contentType + * content type of the virtual file + */ + public InMemoryUploadedFile(byte[] data, String filename, String contentType) { + this.data = data; + this.filename = filename; + this.contentType = contentType; + } + + @Override + public String getFileName() { + return filename; + } + + @Override + public InputStream getInputstream() throws IOException { + return new ByteArrayInputStream(data); + } + + @Override + public long getSize() { + return data.length; + } + + @Override + public byte[] getContents() { + return data; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public void write(String fileName) throws Exception { + File target = new File(fileName); + + FileOutputStream fout = null; + try { + fout = new FileOutputStream(target); + fout.write(data); + } finally { + if (fout != null) { + fout.close(); + } + } + + } + +} diff --git a/src/main/resources/META-INF/faces-config.xml b/src/main/resources/META-INF/faces-config.xml new file mode 100644 index 0000000..8eff51e --- /dev/null +++ b/src/main/resources/META-INF/faces-config.xml @@ -0,0 +1,18 @@ + + + + + com.illucit.faces.component.ImageEditor + com.illucit.faces.component.imageeditor.ImageEditor + + + + com.illucit.faces.component + com.illucit.faces.component.ImageEditorRenderer + com.illucit.faces.component.imageeditor.ImageEditorRenderer + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/imageeditor.taglib.xml b/src/main/resources/META-INF/imageeditor.taglib.xml new file mode 100644 index 0000000..0018361 --- /dev/null +++ b/src/main/resources/META-INF/imageeditor.taglib.xml @@ -0,0 +1,96 @@ + + + http://www.illucit.com/jsf/imageeditor + + + + imageEditor + + com.illucit.faces.component.ImageEditor + com.illucit.faces.component.ImageEditorRenderer + com.illucit.faces.component.imageeditor.ImageEditorComponentHandler + + + + id + false + java.lang.String + + + + rendered + false + java.lang.Boolean + + + + style + false + java.lang.String + + + + styleClass + false + java.lang.String + + + + value + false + java.lang.Object + + + + widgetVar + false + java.lang.String + + + + onsuccess + false + java.lang.String + + + + onerror + false + java.lang.String + + + + initialColor + false + java.lang.String + + + + initialShape + false + java.lang.String + + + + fileUploadListener + false + javax.el.MethodExpression + + + + labelSave + false + java.lang.String + + + + labelDownload + false + java.lang.String + + + + diff --git a/src/main/resources/META-INF/resources/illufaces/imageeditor.css b/src/main/resources/META-INF/resources/illufaces/imageeditor.css new file mode 100644 index 0000000..75a1126 --- /dev/null +++ b/src/main/resources/META-INF/resources/illufaces/imageeditor.css @@ -0,0 +1,47 @@ +.ui-image-editor-canvas-container { + margin-top: 2px; + padding: 6px; + text-align: center; + overflow: auto; +} + +.ui-image-editor-canvas-container .canvas-container { + margin-left: auto; + margin-right: auto; +} + +canvas.imageEditor { + border: 1px solid #aaaaaa; +} + +.ui-icon-mirror-horizontal { + -moz-transform: scaleX(-1); + -o-transform: scaleX(-1); + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + filter: FlipH; + -ms-filter: "FlipH"; +} + +.ui-icon.ui-icon-imageeditor { + background-image:url("#{resource['illufaces:images/imageeditor-icons.png']}") !important; + border-radius: 0; +} + +.ui-icon.ui-icon-rect { + background-position: -0px -0px; +} + +.ui-icon.ui-icon-ellipse { + background-position: -16px -0px; +} + +.ui-icon.ui-icon-line { + background-position: -32px -0px; +} + +.ui-image-editor .ui-color-chooser { + vertical-align: top; + height: 2.3em; + padding: 6px; +} \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/illufaces/imageeditor.js b/src/main/resources/META-INF/resources/illufaces/imageeditor.js new file mode 100644 index 0000000..b227716 --- /dev/null +++ b/src/main/resources/META-INF/resources/illufaces/imageeditor.js @@ -0,0 +1,441 @@ +/* + * IlluFaces ImageEditor Widget + */ +PrimeFaces.widget.ImageEditor = PrimeFaces.widget.BaseWidget.extend({ + + init : function(cfg, refreshOnly) { + this._super(cfg); + + this.jqCanvas = $(this.jqId + '_canvas'); + this.clearButton = $(this.jqId + '_clear-button'); + this.saveButton = $(this.jqId + '_save-button'); + this.downloadButton = $(this.jqId + '_download-button'); + this.rotateCWButton = $(this.jqId + '_rotate-cw-button'); + this.rotateCCWButton = $(this.jqId + '_rotate-ccw-button'); + this.undoButton = $(this.jqId + '_undo-button'); + this.colorChooser = this.jq.find('input[type=color]'); + + this.form = this.jq.closest('form'); + this.canvasContainer = this.jq.find('.ui-image-editor-canvas-container'); + + if (refreshOnly !== true) { + this.initState(); + } else { + this.refreshState(); + } + this.initializeCanvas(); + this.bindEvents(); + this.skinButtons(); + + console.log("Current State: ", this.state); + }, + + refresh : function(cfg) { + this.init(cfg, true); + }, + + getShapeSelectionWidget: function() { + return PrimeFaces.widgets[this.widgetVar + 'DrawSelection']; + }, + + initState : function() { + this.state = { + // Visisble shapes + shapes: new Array(), + + // Settings for new shapes + shapeType: 'rect', + strokeSize: 5, + color: '#000000', + + // Drawing + down: false, + shape: null, + startX: 0, + startY: 0 + }; + + if (this.cfg.initialShape) { + this.state.shapeType = this.cfg.initialShape; + } + }, + + refreshState : function() { + this.state.shapes = new Array(); + + // Mark currently selected shape in refreshed UI + this.selectShape(this.state.shapeType) + + // Restore current color + this.colorChooser.val(this.state.color); + }, + + initializeCanvas : function() { + this.canvas = new fabric.Canvas(this.jqCanvas[0], { + selection : false + }); + var $this = this; + fabric.Image.fromURL(this.cfg.imageSource, function(img) { + var size = img.getOriginalSize(); + img.selectable = false; + $this.canvas.setWidth(size.width); + $this.canvas.setHeight(size.height); + $this.canvas.setBackgroundImage(img); + $this.backgroundImage = img; + $this.canvas.renderAll(); + }); + }, + + bindEvents : function() { + var $this = this; + this.clearButton.on('click.imageed', function() { + $this.clear(); + return false; + }); + this.downloadButton.on('click.imageed', function(event) { + $this.download(event); + }); + this.saveButton.on('click.imageed', function() { + $this.save(); + return false; + }); + this.rotateCWButton.on('click.imageed', function() { + $this.rotateClockwise(); + return false; + }); + this.rotateCCWButton.on('click.imageed', function() { + $this.rotateCounterClockwise(); + return false; + }); + this.undoButton.on('click.imageed', function() { + $this.undo(); + return false; + }); + this.canvas.on('mouse:down', function(event) { + $this.onMouseDown(event); + }); + this.canvas.on('mouse:move', function(event) { + $this.onMouseMove(event); + }); + this.canvas.on('mouse:up', function(event) { + $this.onMouseUp(event); + }); + this.colorChooser.on('change', function(event) { + $this.state.color = $this.colorChooser.val(); + }); + }, + + skinButtons : function() { + var rectButton = this.jq.find('.ui-button input[value=rect]').closest('div.ui-button'); + rectButton.removeClass('ui-button-text-only').addClass('ui-button-icon-only'); + rectButton.append(''); + + var ellipseButton = this.jq.find('.ui-button input[value=ellipse]').closest('div.ui-button'); + ellipseButton.removeClass('ui-button-text-only').addClass('ui-button-icon-only'); + ellipseButton.append(''); + + var lineButton = this.jq.find('.ui-button input[value=line]').closest('div.ui-button'); + lineButton.removeClass('ui-button-text-only').addClass('ui-button-icon-only'); + lineButton.append(''); + }, + + onSelectedShapeChanged : function() { + var selectedShape = this.getShapeSelectionWidget().inputs.filter(':checked').val(); + this._selectShape(selectedShape); + }, + + selectShape : function(shape) { + var button = this.getShapeSelectionWidget().inputs.filter('input[value=' + shape + ']').closest('div.ui-button'); + if (button.length) { + this.getShapeSelectionWidget().select(button); + } + }, + + _selectShape : function(shape) { + switch (shape) { + case 'rect': + case 'line': + case 'ellipse': + this.state.shapeType = shape; + break; + + default: + this._selectShape('rect'); // Default for unknown shapes + break; + } + }, + + clear : function() { + this.canvas.clear(); + this.state.shapes = new Array(); + }, + + onMouseDown : function(event) { + var color = this.state.color; + var shapeType = this.state.shapeType; + var strokeSize = this.state.strokeSize; + var pointer = this.canvas.getPointer(event.e); + + this.state.down = true; + this.state.startX = pointer.x; + this.state.startY = pointer.y; + + switch (shapeType) { + case 'line': + var points = [pointer.x, pointer.y, pointer.x, pointer.y]; + this.state.shape = new fabric.Line(points, { + strokeWidth : this.state.strokeSize, + fill : color, + stroke : color, + selectable : false, + originX : 'center', + originY : 'center' + }); + break; + + case 'rect': + this.state.shape = new fabric.Rect({ + left : pointer.x, + top : pointer.y, + width : 0, + height : 0, + strokeWidth : this.state.strokeSize, + stroke : color, + selectable : false, + fill : 'transparent', + hasBorders : true + }); + break; + + case 'ellipse': + this.state.shape = new fabric.Ellipse({ + left : pointer.x, + top : pointer.y, + rx : 0, + ry : 0, + strokeWidth : this.state.strokeSize, + stroke : color, + selectable : false, + fill : 'transparent', + hasBorders : true + }); + break; + } + + if (!this.state.shape) { + return; + } + + this.canvas.add(this.state.shape); + this.state.shapes.push(this.state.shape); + }, + + onMouseMove : function(event) { + var shape = this.state.shape; + var shapeType = this.state.shapeType; + var pointer = this.canvas.getPointer(event.e); + + if (!this.state.down || !shape) { + return; + } + + switch (shapeType) { + case 'line': + shape.set({ + x2 : pointer.x, + y2 : pointer.y + }); + break; + + case 'rect': + var width = Math.abs(pointer.x - this.state.startX); + var height = Math.abs(pointer.y - this.state.startY); + + var left = Math.min(pointer.x, this.state.startX); + var top = Math.min(pointer.y, this.state.startY); + + shape.set({ + left : left, + top : top, + width : width, + height : height + }); + break; + + case 'ellipse': + var rx = Math.abs(pointer.x - this.state.startX) / 2; + var ry = Math.abs(pointer.y - this.state.startY) / 2; + + var left = Math.min(pointer.x, this.state.startX); + var top = Math.min(pointer.y, this.state.startY); + + shape.set({ + left : left, + top : top, + rx : rx, + ry : ry, + }); + break; + } + + this.canvas.renderAll(); + }, + + onMouseUp : function(event) { + this.state.down = false; + this.state.shape = null; + }, + + rotateClockwise : function() { + this._rotateCanvas(90); + $.each(this.state.shapes, function (index, shape) { + var height = this.canvas.getWidth(); // Canvas is already rotated + switch(shape.type) { + case 'ellipse': + shape.set({ + left : height - shape.getTop() - (2 * shape.getRy()), + top : shape.getLeft(), + rx : shape.getRy(), + ry : shape.getRx() + }); + break; + + case 'rect': + shape.set({ + left : height - shape.getTop() - shape.getHeight() - shape.getStrokeWidth(), + top : shape.getLeft(), + width : shape.getHeight(), + height : shape.getWidth() + }); + break; + + case 'line': + shape.set({ + x1 : height - shape.y1, + y1 : shape.x1, + x2 : height - shape.y2, + y2 : shape.x2 + }); + break; + } + }); + this.canvas.renderAll(); + }, + + rotateCounterClockwise : function() { + this._rotateCanvas(-90); + $.each(this.state.shapes, function (index, shape) { + var width = this.canvas.getHeight(); // Canvas is already rotated + switch(shape.type) { + case 'ellipse': + shape.set({ + left : shape.getTop(), + top : width - shape.getLeft() - (2 * shape.getRx()), + rx : shape.getRy(), + ry : shape.getRx() + }); + break; + + case 'rect': + shape.set({ + left : shape.getTop(), + top : width - shape.getLeft() - shape.getWidth() - shape.getStrokeWidth(), + width : shape.getHeight(), + height : shape.getWidth() + }); + break; + + case 'line': + shape.set({ + x1 : shape.y1, + y1 : width - shape.x1, + x2 : shape.y2, + y2 : width - shape.x2 + }); + break; + } + }); + this.canvas.renderAll(); + }, + + undo : function() { + var shape = this.state.shapes.pop(); + if (shape) { + this.canvas.remove(shape); + } + }, + + _rotateCanvas : function(angle) { + var height = this.canvas.height; + var width = this.canvas.width; + this.backgroundImage.setAngle(this.backgroundImage.angle + angle); + this.canvas.setHeight(width); + this.canvas.setWidth(height); + this.canvas.centerObject(this.backgroundImage); + }, + + download : function(event) { + var data = this.getDataUrl(); + window.open(data); + }, + + save : function() { + + var options = { + source : this.id, + process : this.id, + }; + + var $this = this; + + options.onerror = function() { + if ($this.cfg.onerror) { + $this.cfg.onerror.call(this); + } else { + alert('Error'); + } + }; + + options.onsuccess = function() { + if ($this.cfg.onsuccess) { + $this.cfg.onsuccess.call(this); + } + }; + + options.oncomplete = function() { + // Re-Enable Save Button + $this.saveButton + .removeClass('ui-state-disabled') + .removeAttr('disabled'); + var label = $this.saveButton.find('span.ui-button-text'); + label.text(label.data('originalLabel')); + } + + options.onstart = function() { + // Disable Save Button + $this.saveButton + .removeClass('ui-state-hover ui-state-focus ui-state-active') + .addClass('ui-state-disabled') + .attr('disabled', 'disabled'); + var label = $this.saveButton.find('span.ui-button-text'); + label.data('originalLabel', label.text()); + label.text(label.text() + " ..."); + } + + options.params = [ { + name : this.id + '_save', + value : this.getDataUrl() + } ]; + + PrimeFaces.ajax.AjaxRequest(options); + }, + + getDataUrl : function() { + var options = { + format: 'jpeg', + quality: 1 + } + return this.canvas.toDataURL(options); + } + +}); \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/illufaces/images/imageeditor-icons.png b/src/main/resources/META-INF/resources/illufaces/images/imageeditor-icons.png new file mode 100644 index 0000000..8b09d9e Binary files /dev/null and b/src/main/resources/META-INF/resources/illufaces/images/imageeditor-icons.png differ