javieranton-zz / PhotoCropper

Crop and resize your images in CodenameOne
2 stars 0 forks source link

RFE: add support for avatars and videos #1

Open jsfan3 opened 4 years ago

jsfan3 commented 4 years ago

I saw the screenshot you took in ReadMe and took a look at the code: you did a great job! I haven't tried it yet, but I'm very interested.

I'd like to ask you if you can add support for avatars (i.e. round cutouts instead of square ones) and especially for videos.

Actually for cropping pictures, and avatars in particular, there is already an alternative solution here, which is based on a completely different code from yours: https://www.codenameone.com/blog/photo-cropping-wizard.html You can evaluate whether an alternative solution of yours produces a different (and possibly better) user experience.

The real innovation would be the possibility to crop the videos taken from the gallery to make them square: as far as I know, for the moment there is nothing like that for Codename One.

javieranton-zz commented 4 years ago

Hey what's up Francesco, Thanks, I originally looked at that code but decided to come up with something different because I wanted to improve the usability a bit. I'll see if I can do what you are asking... a bit busy at the moment but who knows maybe I will get around to doing it soon. One thing you could do is mask the result with a round picture, but I assume you want users to crop using a circle as well?

I haven't touched videos yet but that sounds cool, is that for some app you're making? Is it on any store? Just curious 👍

jsfan3 commented 4 years ago

Thank you so much for your availability. Yeah, sure, it's for an app I'm doing, but there's nothing in the stores yet.

Yes, I want users to crop within a circle.

Video is quite complex - many apps allow you to do a few things, including make the video square and crop only the part of a video that interests you. For example, if the user has a 10-minute video in the gallery, the app could not only make the video square, but also let the user select at most one minute of the video.

I would need these features, but for the moment there is nothing like that for Codename One yet.

javieranton-zz commented 4 years ago

Interesting. One way to do this will be to add an additional darkened pane here that covers the dragger and then carve a circle out of it like here. The resulting image can then be masked with a round circle with transparent background to produce a circled image. Although my humble opinion is that it would be better if you saved the resulting square image and then masked it at display time because Avatars will need to be resized at various sizes each time (small for list, big for user settings, etc). If you mask the result to a circle you will need to use a constant-sized round mask which might affect the resulting image quality. What do you think?

Feel free to push any code btw

javieranton-zz commented 4 years ago

Francesco, add the below code here to display a circle for you. You can then mask the resulting image with a circle to make it a circle

GeneralPath p5 = new GeneralPath(); 
p5.setRect(new Rectangle(this.getAbsoluteX(), this.getAbsoluteY(), selectorSquareSide[0], selectorSquareSide[0]), null);
p5.arc(this.getAbsoluteX(), this.getAbsoluteY(),
                            selectorSquareSide[0], selectorSquareSide[0], 0, Math.PI*2);
g.fillShape(p5);

I won't add this to the library because it's too specific I think, but you should be able to copy the lib's main method into your project and mod it image

jsfan3 commented 4 years ago

It's seem very nice! Thank you very much! I'll write you again when I'll have done tests on real devices.

javieranton-zz commented 4 years ago

I just finished coding a similar one that crops rectangles instead of squares... maybe it's of your interest?

I don't have time to update the lib so I will copy it below. You might have to change a few things (the way I log etc). Also made a new img for the rect dragger, you can add it to your resources... attached draggerRect

image

 public static void cropRectImage(final Image img, OnComplete<Image> s) 
    {
        Form previous = getCurrentForm();
        try
        {
            CN.lockOrientation(true);
            int imgHeight = img.getHeight();
            int imgWidth = img.getWidth();
            if(imgHeight < 192 || imgWidth < 192)
            {
                Utils.getInstance().showDialog("Info", "MinImg192");
                return;
            }

            int toolbarHeight = previous.getToolbar().getHeight();
            ExtendedForm cropForm = new ExtendedForm("CropAvatar", new BorderLayout());
            DataBridge.getInstance().currentFormName = "cropImage";

            int pointerPressedX[]={0};
            int pointerPressedY[]={0};
            boolean[] pressedRight = {false};
            boolean[] pressedLeft = {false};
            boolean[] pressedTop = {false};
            boolean[] pressedBottom = {false};

            FlowLayout centerLayout = new FlowLayout(CENTER);
            centerLayout.setValign(CENTER);
            //this makes the image not overlap out of device vertically
            int[] contentPaneMargin = {0};
            int[] draggerFinalMarginY = {0};
            int[] draggerFinalMarginX = {0};
            int[] draggerFinalMarginDraggingY = {0};
            int[] draggerFinalMarginDraggingX = {0};
            Container[] boxCenter = {new Container(centerLayout)};
            boxCenter[0].getAllStyles().setMarginUnit(UNIT_TYPE_PIXELS);
            boxCenter[0].getAllStyles().setMargin(contentPaneMargin[0],contentPaneMargin[0],0,0);

            int[] newWidth = {cropForm.getContentPane().getWidth()};

            int[] newHeight = {imgHeight*newWidth[0]/imgWidth};
            //if new height is bigger than available, resize from widht
            if(newHeight[0] > Display.getInstance().getDisplayHeight() - toolbarHeight)
            {
                newHeight[0] = Display.getInstance().getDisplayHeight() - toolbarHeight;
                newWidth[0] = imgWidth*newHeight[0]/imgHeight;
            }

            boolean[] isTall = {newHeight[0] > newWidth[0]};
            int[] selectorSquareWidth = {newWidth[0]};
            int[] selectorSquareHeight = {newHeight[0]};
            int[] selectorSquareSideDraggingWidth = {0};
            int[] selectorSquareSideDraggingHeight = {0};
            Container[] dragSelectorContainerOverlay = {new Container(new FlowLayout())};
            Container dragSelectorContainer = new Container(){
                @Override 
                public Dimension calcPreferredSize()
                {
                    cropForm.setGlassPane((Graphics g, Rectangle rect) -> {
                        g.setColor(0x000000);
                        g.setAlpha(150); 
                        GeneralPath p = new GeneralPath(); 
                        p.setRect(new Rectangle(0, toolbarHeight, Display.getInstance().getDisplayWidth(), this.getAbsoluteY() - toolbarHeight), null);
                        GeneralPath p2 = new GeneralPath(); 
                        p2.setRect(new Rectangle(0, this.getAbsoluteY() + selectorSquareHeight[0], Display.getInstance().getDisplayWidth(), Display.getInstance().getDisplayHeight()), null);
                        GeneralPath p3 = new GeneralPath(); 
                        p3.setRect(new Rectangle(0, this.getAbsoluteY(), this.getAbsoluteX(), selectorSquareHeight[0]), null);
                        GeneralPath p4 = new GeneralPath(); 
                        p4.setRect(new Rectangle(this.getAbsoluteX() + selectorSquareWidth[0], this.getAbsoluteY(), Display.getInstance().getDisplayWidth() - this.getAbsoluteX() - selectorSquareWidth[0], selectorSquareHeight[0]), null);
                        g.fillShape(p);
                        g.fillShape(p2);
                        g.fillShape(p3);
                        g.fillShape(p4);
                        g.setAlpha(255);
                    });
                    return new Dimension(selectorSquareWidth[0],selectorSquareHeight[0]);
                }
            };
            dragSelectorContainer.getAllStyles().setBgImage(DataBridge.getInstance().res.getImage("draggerRect.png"));
            dragSelectorContainer.getAllStyles().setMarginUnit(UNIT_TYPE_PIXELS);
            dragSelectorContainer.getAllStyles().setPadding(0,0,0,0);
            dragSelectorContainer.getAllStyles().setMargin(0,0,0,0);
            dragSelectorContainerOverlay[0].addComponent(dragSelectorContainer);

            Label imageContainer = new Label(){
                @Override 
                public void pointerReleased(int x, int y)
                {
                        super.pointerReleased(x, y);
                        if((!pressedLeft[0] &&!pressedRight[0] && !pressedTop[0] && !pressedBottom[0]) || pressedTop[0])//dragging OR Top(any)
                        {
                            //reset vert
                            if(draggerFinalMarginY[0] + draggerFinalMarginDraggingY[0] < 0)
                                draggerFinalMarginY[0] = 0;
                            else if(draggerFinalMarginY[0] + draggerFinalMarginDraggingY[0] >= super.getHeight())
                                draggerFinalMarginY[0] = super.getHeight() - selectorSquareHeight[0];
                            else
                                draggerFinalMarginY[0] = draggerFinalMarginY[0] + draggerFinalMarginDraggingY[0];
                            //reset horiz
                            if(draggerFinalMarginX[0] + draggerFinalMarginDraggingX[0] < 0)//if dragged out on left or reached left
                                draggerFinalMarginX[0] = 0;
                            else if(draggerFinalMarginX[0] + draggerFinalMarginDraggingX[0] >= newWidth[0])//if dragged out on right or reached right
                                draggerFinalMarginX[0] = newWidth[0] - selectorSquareWidth[0];
                            else
                                draggerFinalMarginX[0] = draggerFinalMarginX[0] + draggerFinalMarginDraggingX[0];
                        }
                        else if(pressedLeft[0])
                        {
                            //reset horiz
                            if(draggerFinalMarginX[0] + draggerFinalMarginDraggingX[0] < 0)//if dragged out on left or reached left
                                draggerFinalMarginX[0] = 0;
                            else if(draggerFinalMarginX[0] + draggerFinalMarginDraggingX[0] >= newWidth[0])//if dragged out on right or reached right
                                draggerFinalMarginX[0] = newWidth[0] - selectorSquareWidth[0];
                            else
                                draggerFinalMarginX[0] = draggerFinalMarginX[0] + draggerFinalMarginDraggingX[0];
                        }
                }
                @Override
                public void pointerPressed(int x, int y) {
                        super.pointerPressed(x, y); 
                        pointerPressedX[0] = x;
                        draggerFinalMarginDraggingX[0] = x;
                        pointerPressedY[0] = y;
                        draggerFinalMarginDraggingY[0] = y;
                        selectorSquareSideDraggingWidth[0] = selectorSquareWidth[0];
                        selectorSquareSideDraggingHeight[0] = selectorSquareHeight[0];
                        pressedRight[0] = false;
                        pressedLeft[0] = false;
                        pressedTop[0] = false;
                        pressedBottom[0] = false;
                        if(Math.abs(dragSelectorContainer.getAbsoluteX() - x) < selectorSquareWidth[0] / 10)//selectors occupy 10% of dragger box approx
                            pressedLeft[0] = true;
                        if(Math.abs(dragSelectorContainer.getAbsoluteY() - y) < selectorSquareHeight[0] / 10)
                            pressedTop[0] = true;
                        if(Math.abs(dragSelectorContainer.getAbsoluteY() + selectorSquareHeight[0] - y) < selectorSquareHeight[0] / 10)
                            pressedBottom[0] = true;
                        if(Math.abs(dragSelectorContainer.getAbsoluteX() + selectorSquareWidth[0] - x) < selectorSquareWidth[0] / 10)
                            pressedRight[0] = true;
                }
                @Override
                public void pointerDragged(int[] x, int[] y) {
                    super.pointerDragged(x, y);
                    if(pressedLeft[0] || pressedRight[0] || pressedTop[0] || pressedBottom[0])//resizing
                    {
                        int newSize = 0;
                        if(pressedLeft[0])
                        {
                            newSize = selectorSquareSideDraggingWidth[0] - (x[0] - pointerPressedX[0]);
                            if(newSize >= 192)
                            {
                                draggerFinalMarginDraggingX[0] = x[0] - pointerPressedX[0];//this is negative
                                if(draggerFinalMarginX[0] + draggerFinalMarginDraggingX[0] - 20 < 0)//reached left
                                {
                                    newSize = draggerFinalMarginX[0] + selectorSquareSideDraggingWidth[0];//starting point + starting size
                                    if(newSize > newWidth[0])
                                        newSize = newWidth[0];
                                    dragSelectorContainer.getAllStyles().setMarginLeft(0);
                                    selectorSquareWidth[0] = newSize;
                                    //System.out.println("7reached left while changing size");
                                }
                                else
                                {
                                    int newMargin = draggerFinalMarginX[0] + draggerFinalMarginDraggingX[0];
                                    if(newMargin + newSize > newWidth[0])
                                        newMargin = newWidth[0] - newSize;
                                    if(newMargin < 0)
                                        newMargin = 0;
                                    dragSelectorContainer.getAllStyles().setMarginLeft(newMargin);
                                    selectorSquareWidth[0] = newSize;
                                }
                                //System.out.println("9change size. new size: "+selectorSquareSide[0]);
                            }
                        }
                        else if(pressedTop[0])
                        {
                            newSize = selectorSquareSideDraggingHeight[0] - (y[0] - pointerPressedY[0]);
                            if(newSize >= 192)
                            {
                                draggerFinalMarginDraggingX[0] = x[0] - pointerPressedX[0];//this is negative
                                draggerFinalMarginDraggingY[0] = y[0] - pointerPressedY[0];// this is negative
                                if(draggerFinalMarginY[0] + draggerFinalMarginDraggingY[0] - 20 < 0)//reached the top(add a bit to attach to side)
                                {
                                    newSize = draggerFinalMarginY[0] + selectorSquareSideDraggingHeight[0];//starting point + starting size
                                    if(newSize > newHeight[0])
                                        newSize = newHeight[0];
                                    dragSelectorContainer.getAllStyles().setMarginTop(0);
                                    selectorSquareHeight[0] = newSize;
                                    //System.out.println("16reached top while changing size");
                                }
                                else
                                {
                                    if(newSize > newHeight[0])
                                        newSize = newHeight[0];
                                    int newMargin = draggerFinalMarginY[0] + (y[0] - pointerPressedY[0]);
                                    if(newMargin < 0)
                                        newMargin = 0;
                                    if(newMargin + newSize > newHeight[0])
                                        newMargin = newHeight[0] - newSize;
                                    dragSelectorContainer.getAllStyles().setMarginTop(newMargin);
                                    selectorSquareHeight[0] = newSize;
                                }
                                //System.out.println("17change size. new size: "+selectorSquareSide[0]);
                            }
                        }
                        else if(pressedRight[0])
                        {
                            newSize = selectorSquareSideDraggingWidth[0] - (pointerPressedX[0] - x[0]);
                            if(newSize >= 192)
                                if(draggerFinalMarginX[0] + newSize + 20 >= newWidth[0])//reached the right side (add a bit to attach to side)
                                {
                                    selectorSquareWidth[0] = newWidth[0] - draggerFinalMarginX[0];
                                    //System.out.println("12reached right while changing size ");
                                }else
                                    selectorSquareWidth[0] = newSize;
                        }
                        else if(pressedBottom[0])
                        {
                            newSize = selectorSquareSideDraggingHeight[0] - (pointerPressedY[0] - y[0]);
                            if(newSize >= 192)
                                if(draggerFinalMarginY[0] + newSize + 20 >= newHeight[0])//reached the bottom(add a bit to attach to side)
                                {
                                    selectorSquareHeight[0] = newHeight[0] - draggerFinalMarginY[0];
                                    //System.out.println("13reached bottom while changing size");
                                }else
                                    selectorSquareHeight[0] = newSize;
                        }

                        //System.out.println("10newSize: "+newSize);
                        //System.out.println("11draggerFinalMarginY[0]: "+draggerFinalMarginY[0]);
                        dragSelectorContainerOverlay[0].forceRevalidate();
                    }
                    else if(!pressedLeft[0] && !pressedRight[0] && !pressedTop[0] && !pressedBottom[0])//dragging
                    {
                        //HORIZONTAL DRAGGING
                        draggerFinalMarginDraggingX[0] = x[0] - pointerPressedX[0];
                        if(draggerFinalMarginX[0] + draggerFinalMarginDraggingX[0] + selectorSquareWidth[0] >= newWidth[0])
                        {
                            draggerFinalMarginX[0] = newWidth[0] - selectorSquareWidth[0];
                            pointerPressedX[0] = x[0];
                            int safeMargin = newWidth[0] - selectorSquareWidth[0];//guard against negative margin
                            if(safeMargin < 0)
                                safeMargin = 0;
                            dragSelectorContainer.getAllStyles().setMarginLeft(safeMargin);
                            //System.out.println("reached right");
                        }
                        else if(draggerFinalMarginX[0] + draggerFinalMarginDraggingX[0] < 0)
                        {
                            draggerFinalMarginX[0] = 0;
                            pointerPressedX[0] = x[0];
                            dragSelectorContainer.getAllStyles().setMarginLeft(0);
                            //System.out.println("reached left");
                        }
                        else
                        {
                            dragSelectorContainer.getAllStyles().setMarginLeft(draggerFinalMarginX[0] + draggerFinalMarginDraggingX[0]);
                            //System.out.println("horiz. new margin: "+(x[0] - selectorSquareSide[0]/2));
                        }
                        //VERTICAL DRAGGING
                        draggerFinalMarginDraggingY[0] = y[0] - pointerPressedY[0];
                        if(draggerFinalMarginY[0] + draggerFinalMarginDraggingY[0] < 0)//reached top
                        {
                            draggerFinalMarginY[0] = 0;
                            pointerPressedY[0] = y[0];
                            dragSelectorContainer.getAllStyles().setMarginTop(0);
                            //System.out.println("reached top");
                        }else if(draggerFinalMarginY[0] + draggerFinalMarginDraggingY[0] + selectorSquareHeight[0] >= newHeight[0])//reached bottom
                        {
                            draggerFinalMarginY[0] = newHeight[0] - selectorSquareHeight[0];
                            pointerPressedY[0] = y[0];
                            int newMargin = newHeight[0] - selectorSquareHeight[0];
                            if(newMargin < 0)
                                newMargin = 0;
                            dragSelectorContainer.getAllStyles().setMarginTop(newMargin);
                            //System.out.println("reached bottom");
                        }
                        else //if(isTall[0] || selectorSquareSide[0] < super.getHeight())//drag vertically
                        {
                            int newMargin = draggerFinalMarginY[0] + draggerFinalMarginDraggingY[0];
                            if(newMargin < 0)
                                newMargin = 0;
                            dragSelectorContainer.getAllStyles().setMarginTop(newMargin);
                            //System.out.println("verti. new margin: "+(draggerFinalMarginY[0] + draggerFinalMarginDraggingY[0]));
                        }
                        dragSelectorContainerOverlay[0].forceRevalidate();
                    }
                }
            };
            imageContainer.getAllStyles().setBgImage(img);
            imageContainer.setPreferredW(newWidth[0]);
            imageContainer.setPreferredH(newHeight[0]);
            imageContainer.getAllStyles().setMarginUnit(UNIT_TYPE_PIXELS);
            imageContainer.getAllStyles().setMargin(0,0,0,0);
            imageContainer.getAllStyles().setPadding(0,0,0,0);

            Container[] actualContent = {LayeredLayout.encloseIn(imageContainer, dragSelectorContainerOverlay[0])};
            boxCenter[0].add(actualContent[0]);

            cropForm.add(BorderLayout.CENTER,boxCenter[0]);
            Style style = UIManager.getInstance().getComponentStyle("BackButton");
            Utils.getInstance().setFormUsersTitle(cropForm,"CropAvatar",null,null, ()->{
                CN.unlockOrientation();
                previous.showBack();
            }, null, null, null,null);
            cropForm.getToolbar().addCommandToRightBar("", FontImage.createMaterial(FontImage.MATERIAL_CROP, style).toImage(), e -> {
                CN.unlockOrientation();
                previous.showBack();
                try
                {
                    int originalHeight = img.getHeight();
                    int originalWidth = img.getWidth();
                    double ratio = Double.valueOf(originalHeight)/Double.valueOf(newHeight[0]);
                    int xStart = (int)Math.floor(dragSelectorContainer.getUnselectedStyle().getMarginLeft(false)*ratio);
                    int xWidth = (int)Math.floor(selectorSquareWidth[0]*ratio);
                    if(xStart + xWidth > originalWidth)
                        xWidth = originalWidth - xStart;
                    int yStart = (int)Math.floor(dragSelectorContainer.getUnselectedStyle().getMarginTop()*ratio);
                    int yHeight = (int)Math.floor(selectorSquareHeight[0]*ratio);
                    if(yStart + yHeight > originalHeight)
                        yHeight = originalHeight - yStart;
                    Image subImage = img.subImage(xStart, yStart, xWidth, yHeight, true);
                    s.completed(subImage);
                }catch(Exception ex)
                {
                    Utils.getInstance().traceDebug("2Error cropping image: "+ex.toString()+". URL Encoded Native logs: "+Util.encodeUrl(NativeLogs.getNativeLogs()));
                    s.completed(img);
                }
            });

            cropForm.show();
        }catch(Exception e)
        {
            try{
            Utils.getInstance().traceDebug("Error cropping image: "+e.toString()+". URL Encoded Native logs: "+Util.encodeUrl(NativeLogs.getNativeLogs()));
            CN.unlockOrientation();
            previous.showBack();
            s.completed(img);
            }catch(Exception ex)
            {
                Utils.getInstance().traceDebug("2Error cropping image: "+ex.toString()+". URL Encoded Native logs: "+Util.encodeUrl(NativeLogs.getNativeLogs()));
                Style redIconStyle = new Style();
                redIconStyle.setBgTransparency(0);
                redIconStyle.setFgColor(Utils.getInstance().getNightColor(0xff3333));
                EncodedImage errorImg = com.codename1.ui.EncodedImage.createFromImage(FontImage.createMaterial(FontImage.MATERIAL_ERROR, redIconStyle, 8.0f), true);
                s.completed(errorImg);
            }
        }
}
jsfan3 commented 4 years ago

Here I am. I just finished testing your code on real Android devices and iPhones. I tried the square and circular cutout, I didn't try the rectangular one (I don't care at the moment).

At first I had problems because I had incorrectly modified your code. I finally realized what I had to do, here is a copy of the modified code: https://gist.github.com/jsfan3/c50909f8a76ca97915ec3fc1a90fce33 As you can see, I simply added a static method to choose a square or rectangular cropping. Moreover, I removed all the orientation locking and unlocking, since currently they are broken APIs on iOS, ad documented here: https://stackoverflow.com/questions/63256117/changing-the-screen-orientation-lock-on-ios I also think that the use of unlocking inside an app that is supposed locked is wrong, this is something that should be documented. Anyway, my app is already locked in the portrait orientation.

This is the code I used to test your code: https://gist.github.com/jsfan3/e0d356d9f43529c4f9d48df66c898939 (I used a separated thread because exifRotation is slow on Android devices when it has to rotate photos at the maximum resolution, and I don't want to block the EDT).

The result is good, it is what I expected by your screenshots: congratulations, thank you Javier Anton!

javieranton-zz commented 4 years ago

Nice... great job.. so good to see both methods put together. I guess we can put your code in the lib's next version? I blocked the orientation to simplify the use cases... but it's great to know that it doesn't blow up in pieces when you don't block it

jsfan3 commented 4 years ago

I prefer to wait before sending you a pull request for any changes to your cn1lib. Of course you can insert my code in the lib's next version if you want. Precisely because you have chosen the cn1lib format to distribute your code, any changes should not alternate the current behavior.

Specifically, as far as orientation locking is concerned, maybe I didn't explain it well: what you've done is fine, but the current APIs that do locking and unlocking are broken and should be fixed (I'm referring to https://stackoverflow.com/questions/63256117/changing-the-screen-orientation-lock-on-ios), and before using lock and unlock you must first check that the screen orientation hasn't already been locked. I didn't try your code in landscape orientation, because my app is already locked in portrait (so I removed your locks and unlocks).

Also, in my opinion, in addition to the public static void cropImage(final Image img, int minSize, int destWidth, int destHeight, OnComplete<Image> s, String formTitle, String minimumImageSizeWarningMsg) method, you should add a method to invoke a callback in case of error instead of the current default Dialog, for example: public static void cropImage(final Image img, int minSize, int destWidth, int destHeight, OnComplete<Image> s, String formTitle, Runnable minimumImageSizeWarningCallback). You should also add javadoc to better document the parameters.

I'll put these issues on hold for the moment. I prefer to focus on this problem: https://stackoverflow.com/questions/63256117/changing-the-screen-orientation-lock-on-ios