Menu

quinta-feira, 3 de dezembro de 2015

Primefaces ImageCropper is rendered as a static image in Google Chrome

An issue I had this week was related to PrimefacesImageCropper component.

We have been using it for almost 2 years in our application that receives the photo of the university students and employees.

However, last week I found out a bug that was happening only on Google Chrome.

Sometimes when the user uploads his photo and clicks to the next step to load the image cropper, the cropper behavior just doesn't load. The users are presented to static image they can't crop and consequently they can't progress to the next step to send their photos.

If they go to the previous step and advance again or refresh the page, sometimes the cropper would load correctly.

I spent a few days trying to solve the issue and I considered the following possibilities:

  • Scripts loaded out of order (for example, imagecropper.js loaded before jquery.js or primefaces.js)
  • A resource that could not be loaded (like the crop.gif that is used to display the dotted line of the cropped area)
  • Something related to the fact that PrimeFaces.widget.ImageCropper extends from PrimeFaces.widget.DeferredWidget instead of PrimeFaces.widget.BaseWidget)
  • Our customized theme and template

I digged deep into Primefaces components and JavaScripts source code and I was able to check the possibilities above, but I wasn't able to track the issue.

The only thing I found out is that every time that the component worked, the JS code defined in the encodeScript method couldn't be found in the generated HTML code, while every time it didn't work I could find the JS code right below the image cropper markup.

In the end, I had to rewrite ImageCropperRenderer encodeScript method to render our own javascript to enable the crop feature. In our case we did that directly in our Primefaces fork, but you can do it in your project by creating the CustomImageCropperRenderer and declaring it in faces-config.xml.

Faces-config

1
2
3
4
5
6
7
    <render-kit>
        <renderer>
            <component-family>org.primefaces.component</component-family>
            <renderer-type>org.primefaces.component.ImageCropperRenderer</renderer-type>
            <renderer-class>your.package.CustomImageCropperRenderer</renderer-class>
        </renderer>
    </render-kit>

CustomImageCropperRenderer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package your.package;

import java.io.IOException;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import org.primefaces.component.imagecropper.ImageCropper;
import org.primefaces.component.imagecropper.ImageCropperRenderer;
import org.primefaces.model.CroppedImage;

/**
 *
 * @author hfluz
 */
public class CustomImageCropperRenderer extends ImageCropperRenderer {

    @Override
    protected void encodeScript(FacesContext context, ImageCropper cropper) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String clientId = cropper.getClientId(context);
        writer.startElement("script", null);
        writer.writeAttribute("language", "Javascript", null);
        StringBuilder script = new StringBuilder();
        script.append("jQuery(function($){$('#")
                .append(clientId)
                .append("_image').Jcrop({");
        if (cropper.getMinSize() != null) {
            script.append("minSize:[").append(cropper.getMinSize()).append("]");
        }
        if (cropper.getMaxSize() != null) {
            script.append("maxSize:").append(cropper.getMaxSize());
        }
        script.append(",aspectRatio:").append(cropper.getAspectRatio());
        Object value = cropper.getValue();
        String select = null;
        if (value != null) {
            CroppedImage croppedImage = (CroppedImage) value;

            int x = croppedImage.getLeft();
            int y = croppedImage.getTop();
            int x2 = x + croppedImage.getWidth();
            int y2 = y + croppedImage.getHeight();

            select = "[" + x + "," + y + "," + x2 + "," + y2 + "]";
        } else if (cropper.getInitialCoords() != null) {
            select = "[" + cropper.getInitialCoords() + "]";
        }
        script.append(",setSelect:")
                .append(select)
                .append(",onChange: updateCoords")
                .append(",onSelect: updateCoords")
                .append(",onRelease: updateCoords")
                .append("});")
                .append("function updateCoords(c){$('#")
                .append(clientId)
                .append("_coords').val(c.x + \"_\" + c.y + \"_\" + c.w + \"_\" + c.h);};});");

        writer.write(script.toString());
        writer.endElement("script");
    }
}

Generated JavaScript Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
jQuery(function($) {
    $('#cropper_image').Jcrop({
        minSize: [102, 136],
        aspectRatio: 0.75,
        setSelect: null,
        onChange: updateCoords,
        onSelect: updateCoords,
        onRelease: updateCoords
    });

    function updateCoords(c) {
        $('#cropper_coords').val(c.x + "_" + c.y + "_" + c.w + "_" + c.h);
    };
});
I didn't add support to all the imageCropper properties, since we only needed minSize and aspectRatio, but they can be easily added based on the code above.

Nenhum comentário :