Tag Archives: thumbnail

Thumbnail Chooser

Whenever users can upload their own profile picture they should be able to choose a fixed sized thumbnail of it. Thinking of a news feed or protocol like list with user images in it, the images should all have the same size, they should be proportionally scaled and the user should select the section of the image to show.

Therefore, we implemented a Facebook like thumbnail chooser where the user can drag her image in a frame to adjust the visible section. We then create the thumbnail on the server (i.e., we first proportionally scale the original image and extract the selected aperture).

The chooser is initialized with the url of the previously selected thumbnail (if none exists we display a placeholder). After the user clicks on it we show a proportionally scaled version of the original image. The smaller dimension of the image (height or width) is scaled to fit the size of the frame (called thumbnailBox in the code) plus a given overlap. This is done in scaleToFit(Image, int):

private void scaleToFit(Image imageToScale, int overlap) {
    if (imageToScale.getWidth() < imageToScale.getHeight()) {
        int height = imageToScale.getHeight();
        double percent = (double) (fBoxWidth + overlap) / imageToScale.getWidth();
        imageToScale.setWidth(fBoxWidth + overlap + Unit.PX.getType());
        imageToScale.setHeight((height * percent) + Unit.PX.getType());
    } else {
        int width = imageToScale.getWidth();
        double percent = (double) (fBoxHeight + overlap) / imageToScale.getHeight();
        imageToScale.setHeight(fBoxHeight + overlap + Unit.PX.getType());
        imageToScale.setWidth((width * percent) + Unit.PX.getType());
    }
}

This method is called in a LoadHandler attached to the thumbnail

@UiHandler("thumbnail")
void onImageLoad(LoadEvent event) {
    // scheduler needed for Chrome, otherwise width/height of image is always 0 (see
    // http://code.google.com/p/google-web-toolkit/issues/detail?id=6848)
    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
 
        @Override
        public void execute() {
            scaleToFit(thumbnail, fSourceLoaded ? fOverlap : 0);
        }
    });
}

that is invoked after changing the source of thumbnail:

@UiHandler("thumbnailBox")
void onMouseDownBox(MouseDownEvent event) {
    if (!fSourceLoaded) {
        fMover = new ImageMover();
        fSourceLoaded = true;
        thumbnail.setUrl(fSourceUrl);
    }
    fMover.setStartPoints(event.getClientX(), event.getClientY());
    fMouseUpRegistration = Event.addNativePreviewHandler(fMouseUpHandler);
    fPressed = true;
}

Here we first check whether the original image is already loaded or not. If not we set the new url (which causes a LoadEvent to be fired). When the user drags the image fMover adjusts the top and left CSS properties of the image. The movement is triggered upon receiving MouseMoveEvents on the thumbnailBox. As soon as the user releases the mouse button the movement should stop. This one was a bit trickier since listening to MouseUpEvent on the thumbnailBox is not sufficient. What if the mouse pointer is outside the box? We therefore add a NativePreviewHandler (fMouseUpHandler) and listen for all native Event.ONMOUSEUP events:

private Event.NativePreviewHandler fMouseUpHandler = new NativePreviewHandler() {
 
    @Override
    public void onPreviewNativeEvent(NativePreviewEvent event) {
        if (event.getTypeInt() == Event.ONMOUSEUP) {
            fPressed = false;
            fMouseUpRegistration.removeHandler();
        }
    }
};

Here’s the complete code:
ThumbnailChooser.ui.xml

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui">
    <ui:style>
        .box {
            border: solid 1px orange;
            cursor: move;
       	    float: left;
            overflow: hidden;
        }
 
        .box img {
            position: relative;
        }
    </ui:style>
 
    <g:FocusPanel ui:field="thumbnailBox" styleName="{style.box}">
        <g:Image ui:field="thumbnail" />
    </g:FocusPanel>
</ui:UiBinder>

ThumbnailChooser.java

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Widget;
 
/**
 * This widget allows for selecting a sized area (i.e., a thumbnail) from a given picture.
 * <p>
 * It draws a box with the size of the thumbnail and displays the image of which the thumbnail is to be made in it. The
 * user can now drag the picture with the mouse and place it in the box as she likes.
 * 
 * @author zubi
 * 
 */
public class ThumbnailChooser extends Composite {
 
    private static ThumbnailChooserUiBinder sUiBinder = GWT.create(ThumbnailChooserUiBinder.class);
 
    interface ThumbnailChooserUiBinder extends UiBinder<Widget, ThumbnailChooser> { }
 
    @UiField
    FocusPanel thumbnailBox;
    @UiField
    Image thumbnail;
    private final int fBoxHeight;
    private final int fBoxWidth;
    private final int fOverlap;
    private int fLeft;
    private int fTop;
    private String fThumbnailUrl;
    private String fSourceUrl;
    private boolean fSourceLoaded;
    private ImageMover fMover;
    private boolean fPressed;
    private HandlerRegistration fMouseUpRegistration;
    private Event.NativePreviewHandler fMouseUpHandler = new NativePreviewHandler() {
 
        @Override
        public void onPreviewNativeEvent(NativePreviewEvent event) {
            if (event.getTypeInt() == Event.ONMOUSEUP) {
                fPressed = false;
                fMouseUpRegistration.removeHandler();
            }
        }
    };
 
    /**
     * Creates new thumbnail chooser.
     * 
     * @param thumbnailHeight
     *            height of thumbnail
     * @param thumbnailWidth
     *            width of thumbnail
     * @param thumbnailUrl
     *            url of existing thumbnail
     * @param sourceUrl
     *            url of the image of which a thumbnail is to be selected
     * @param overlap
     *            the amount in pixels the source image overlaps the thumbnail box in its shorter dimension (the image
     *            gets scaled to just fit the thumbnail box plus the given overlap)
     */
    public ThumbnailChooser(int thumbnailHeight, int thumbnailWidth, String thumbnailUrl, String sourceUrl, int overlap) {
        initWidget(sUiBinder.createAndBindUi(this));
        fBoxHeight = thumbnailHeight;
        fBoxWidth = thumbnailWidth;
        fThumbnailUrl = thumbnailUrl;
        fSourceUrl = sourceUrl;
        fOverlap = overlap;
        init();
    }
 
    private void init() {
        thumbnailBox.setPixelSize(fBoxWidth, fBoxHeight);
        Image.prefetch(fSourceUrl);
        thumbnail.setUrl(fThumbnailUrl);
    }
 
    private void scaleToFit(Image imageToScale, int overlap) {
        if (imageToScale.getWidth() < imageToScale.getHeight()) {
            int height = imageToScale.getHeight();
            double percent = (double) (fBoxWidth + overlap) / imageToScale.getWidth();
            imageToScale.setWidth(fBoxWidth + overlap + Unit.PX.getType());
            imageToScale.setHeight((height * percent) + Unit.PX.getType());
        } else {
            int width = imageToScale.getWidth();
            double percent = (double) (fBoxHeight + overlap) / imageToScale.getHeight();
            imageToScale.setHeight(fBoxHeight + overlap + Unit.PX.getType());
            imageToScale.setWidth((width * percent) + Unit.PX.getType());
        }
    }
 
    @UiHandler("thumbnail")
    void onImageLoad(LoadEvent event) {
        // scheduler needed for Chrome, otherwise width/height of image is always 0 (see
        // http://code.google.com/p/google-web-toolkit/issues/detail?id=6848)
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
 
            @Override
            public void execute() {
                scaleToFit(thumbnail, fSourceLoaded ? fOverlap : 0);
            }
        });
    }
 
    @UiHandler("thumbnail")
    void onMouseDown(MouseDownEvent event) {
        // prevent dragging of image
        event.preventDefault();
    }
 
    @UiHandler("thumbnailBox")
    void onMouseDownBox(MouseDownEvent event) {
        if (!fSourceLoaded) {
            fMover = new ImageMover();
            fSourceLoaded = true;
            thumbnail.setUrl(fSourceUrl);
        }
        fMover.setStartPoints(event.getClientX(), event.getClientY());
        fMouseUpRegistration = Event.addNativePreviewHandler(fMouseUpHandler);
        fPressed = true;
    }
 
    @UiHandler("thumbnailBox")
    void onMouseMoveBox(MouseMoveEvent event) {
        if (fSourceLoaded && fPressed) {
            fMover.move(event.getClientX(), event.getClientY());
        }
    }
 
    @UiHandler("thumbnailBox")
    void onMouseUpBox(MouseUpEvent event) {
        fPressed = false;
    }
 
    /**
     * Returns width of the thumbnail box.
     * 
     * @return thumbnail width
     */
    public int getThumbnailWidth() {
        return fBoxWidth;
    }
 
    /**
     * Returns height of thumbnail box.
     * 
     * @return thumbnail height
     */
    public int getThumbnailHeight() {
        return fBoxHeight;
    }
 
    /**
     * Returns the new x coordinate of the upper left corner of the selected thumbnail. Add {@link #getThumbnailWidth()}
     * to get the new horizontal viewport.
     * 
     * @return x coordinate of the upper left corner of the viewport
     */
    public int getX() {
        return fLeft * -1;
    }
 
    /**
     * Returns the new y coordinate of the upper left corner of the selected thumbnail. Add
     * {@link #getThumbnailHeight()} to get the new vertical viewport.
     * 
     * @return y coordinate of the upper left corner of the viewport
     */
    public int getY() {
        return fTop * -1;
    }
 
    private class ImageMover {
 
        private int startX;
        private int startY;
 
        public void setStartPoints(int clientX, int clientY) {
            startX = clientX;
            startY = clientY;
        }
 
        public void move(int clientX, int clientY) {
            // horizontal movement
            int hDiff = startX - clientX;
            int left = getLeft();
            startX = clientX;
            if (hDiff >= 0) {
                fLeft = Math.max(left - hDiff, fBoxWidth - thumbnail.getWidth());
            } else {
                fLeft = Math.min(left + Math.abs(hDiff), 0);
            }
 
            // vertical movement
            int vDiff = startY - clientY;
            startY = clientY;
            int top = getTop();
            if (vDiff >= 0) {
                fTop = Math.max(top - vDiff, fBoxHeight - thumbnail.getHeight());
            } else {
                fTop = Math.min(top + Math.abs(vDiff), 0);
            }
            thumbnail.getElement().getStyle().setLeft(fLeft, Unit.PX);
            thumbnail.getElement().getStyle().setTop(fTop, Unit.PX);
        }
 
        private int getTop() {
            String top = thumbnail.getElement().getStyle().getTop();
            if (top.isEmpty()) {
                return 0;
            }
            return Integer.parseInt(top.replaceAll(Unit.PX.getType(), ""));
        }
 
        private int getLeft() {
            String left = thumbnail.getElement().getStyle().getLeft();
            if (left.isEmpty()) {
                return 0;
            }
            return Integer.parseInt(left.replaceAll(Unit.PX.getType(), ""));
        }
    }
}