Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unclear how to use it with cropperjs #1

Closed
phlegx opened this issue Mar 22, 2019 · 33 comments
Closed

Unclear how to use it with cropperjs #1

phlegx opened this issue Mar 22, 2019 · 33 comments

Comments

@phlegx
Copy link

phlegx commented Mar 22, 2019

Hi,

I'm trying to get this plugin work with my cropper using the cropperjs library. I have read the documentation of this plugin but I don't understand some parts.

  1. How can filepond know the width and height of the cropper area?
  2. What is the range and unit of rotation? I have try out from -180 to 180. But don't seems to be correct.
  3. What is the unit of crop.center.x and crop.center.y? Pixel, percentage, etc.?
  4. I have found, that output will contain both file and output data. Is this right?
{
    data: {
        crop: {
            center: {
                x: .5,
                y: .5
            },
            flip: {
                horizontal: false,
                vertical: false
            },
            zoom: 1,
            rotation: 0,
            aspectRatio: null
        }
    }
}

I use getImageData(), getCanvasData() and getData() of Cropperjs. Any idea how I can calculate all things for this filepond output object?

@rikschennink
Copy link
Collaborator

The image-edit plugin currently mostly functions as a proxy between FilePond and the Doka Image Editor.

It should be possible to convert the custom crop info so it can be used with other croppers as well but I haven't tested this. I've yet to make time to describe the exact way in which the crop data is calculated and used.

All FilePond plugins have been open sourced today so feel free to snoop around in them and see how the data is used. The image preview and image transform plugins should expose the required info.

@phlegx
Copy link
Author

phlegx commented Mar 22, 2019

Hi @rikschennink! Thank you very much for the reply.

Here what I have found out about the output of onconfirm():

I use getCanvasData() and getData() methods of Cropperjs.

  • getCanvasData(): Output the canvas (image wrapper) position and size data.
  • getData(): Output the final cropped area position and size data (base on the natural size of the original image).

The Cropperjs data:

myeditor.data = this.$ref.imageEditor.getData()
myeditor.canvasData = this.$ref.imageEditor.getCanvasData()
  1. rotation value should be radians. My editor range is set to -180 to 180 degrees. So, I calculate the radians with (Math.PI / 180) * myeditor.range.value.
  2. Flip horizontal and vertical is boolean. So, I calculate horizontal with myeditor.data.scaleX < 0 and vertical with myeditor.data.scaleY < 0. Both calculations return true of false.
  3. zoom is calculated with myeditor.canvasData.width / myeditor.canvasData.naturalWidth. Also this works fine.
  4. aspectRatio is calculated from the selected cropped area myeditor.data.height / myeditor.data.width.
  5. center is calculated with x center (myeditor.data.x + (myeditor.data.width / 2)) / myeditor.canvasData.naturalWidth and y center (myeditor.data.y + (myeditor.data.height / 2)) / myeditor.canvasData.naturalHeight.

@phlegx
Copy link
Author

phlegx commented Mar 22, 2019

Whit this calculations explained above I get nice previews in filepond, but only if one edge of the cropbox touches one border of the image. If the cropbox is completely in the middle of the image, filepond preview zooms out from the image and the preview image is not the same as the crop area selected.

Here an example of the selected crop area in the middle of the image:

crop

Here the preview created from filepond:

preview

Here a working example with left cropbox edge touching left border of the image:

crop-left

Here the preview created from filepond:

preview-left

@rikschennink can this be a bug of filepond?

@rikschennink
Copy link
Collaborator

If it's in the middle it needs to zoom the image to fit correctly. The zoom factor is calculated based on the amount of space available around the rectangle. When it's against on of the edges its 1.

@phlegx
Copy link
Author

phlegx commented Mar 25, 2019

Every cropper tool gives coordinates x and y for the top-right corner of the crop area and width and height of the crop area. With this information you can crop everything. Zooming to fit crop area to some edge is strange. Anyway, thank you very much for your reply. Are you interested to make filepond easier to handle with crop area x, y, width and height?

@rikschennink
Copy link
Collaborator

In my experience, x, y, width, height works fine, until you start rotating the image.

@phlegx
Copy link
Author

phlegx commented Mar 25, 2019

Here an example crop with rotation:

crop

Here the generated preview:

preview

Any idea how to calculate the zoom to fit the nearest edge to the image border?

Is this the calculation filepond-plugin-image-preview.esm.js#L82?

@rikschennink
Copy link
Collaborator

Yes, that's it.

@phlegx
Copy link
Author

phlegx commented Mar 25, 2019

@rikschennink I need help please. I'm not able to find the right calculation to zoom image to crop area. :( Any idea how I can calculate it?

@phlegx
Copy link
Author

phlegx commented Mar 26, 2019

If it's in the middle it needs to zoom the image to fit correctly.

How can I zoom the image to fit correctly? I have tried many things, but no one works correctly.

@rikschennink
Copy link
Collaborator

I can't help out right now, but I advise to enter some different zoom levels and see what happens to the preview.

@phlegx
Copy link
Author

phlegx commented Mar 26, 2019

Here my solution with Cropperjs. It works without rotation but with flip horizontal and vertical the image:

/* Constants. */
const canvasData = this.$refs.imageEditor.getCanvasData() // Cropperjs method getCanvasData()
const cropData = this.$refs.imageEditor.getData() // Cropperjs method getData()

/* Ratio of selected crop area. */
const cropAreaRatio = cropData.height / cropData.width

/* Center point of crop area in percent. */
const percentX = (cropData.x + cropData.width / 2) / canvasData.naturalWidth
const percentY = (cropData.y + cropData.height / 2) / canvasData.naturalHeight

/* Calculate available space round image center position. */
const cx = percentX > 0.5 ? 1 - percentX : percentX
const cy = percentY > 0.5 ? 1 - percentY : percentY

/* Calculate image rectangle respecting space round image from crop area. */
let width = canvasData.naturalWidth
let height = width * cropAreaRatio
if (height > canvasData.naturalHeight) {
  height = canvasData.naturalHeight
  width = height / cropAreaRatio
}
const rectWidth = cx * 2 * width
const rectHeight = cy * 2 * height

/* Calculate zoom. */
const zoom = Math.max(rectWidth / cropData.width, rectHeight / cropData.height)

/* Callback filepond. */
imageEditEditor.events.onconfirm({
  data: {
    crop: {
      center: {
        x: percentX,
        y: percentY
      },
      flip: {
        horizontal: cropData.scaleX < 0,
        vertical: cropData.scaleY < 0
      },
      zoom: zoom,
      rotation: (Math.PI / 180) * cropData.rotate,
      aspectRatio: cropAreaRatio
    }
  }
})

So, rotation is not working for now. Any idea is very appreciated!

@oscarlocatelli
Copy link

Hi, I tried to do some things with these calculations but no results..

Filepond is beautiful but I need to integrate with my simple in-place crop editor.

@rikschennink Could you help us?

@rikschennink
Copy link
Collaborator

@oscarlocatelli I've written Doka.js specifically for this use case. The Image Editor plugin is mostly a proxy between FilePond and Doka, while it's possible to integrate your own image cropper/editor it's probably not as cost-effective (of course this depends on your use case) as purchasing a Doka license.

If Doka does not fit your needs or you don't want to purchase a license I'm available for consulting work, please contact me on Twitter.

@oscarlocatelli
Copy link

oscarlocatelli commented Apr 3, 2019

@rikschennink Doka.js is great and maybe I can use it into future projects.
But I already have a simpler cropper plugin and I would to use the Image Editor plugin to integrate it like I have read on your documentation.
Maybe I've not understand the docs and the plugin is usable only with Doka.js if the needed calculations are not clear.

@rikschennink
Copy link
Collaborator

The docs are currently not very detailed, in theory other plugins can be made to connect with the image-edit plugin but I haven't had a lot of time to describe this clearly.

@pqina pqina deleted a comment from hmz22 Jun 25, 2019
@rikschennink
Copy link
Collaborator

@hmz22 I've deleted your earlier comment, and will delete this one as well, because you already opened an issue with the exact same content, again, please don't double post.

@pqina pqina deleted a comment from hmz22 Jun 26, 2019
@hmz22
Copy link

hmz22 commented Jun 26, 2019

Anyone can’t tell me how can access imageEditeditor onconfirm event in custom editor submit button onClick function in react?

@rikschennink
Copy link
Collaborator

Linking onconfirm and FilePond in React 👉 https://codesandbox.io/s/serene-wiles-p0j7j

@rikschennink
Copy link
Collaborator

Will close as is inactive.

@aqueelaboobacker
Copy link

aqueelaboobacker commented Nov 26, 2019

const zoom = Math.max(rectWidth / cropData.width, rectHeight / cropData.height)

I don't know whether this is the right solution, but works for me. I tested with few images. Only works when cropBoxMovable: false.

  let x = percentX;
  let y = percentY;

  if (cropData.rotate == 90) {
     x = 0.5 - percentY + 0.5;
     y = 0.5 - percentX + 0.5;
  } else if (cropData.rotate == -90) {
     x = percentY;
     y = percentX;
  } else if (cropData.rotate == 180) {
      x = 0.5 - percentX + 0.5;
      y = 0.5 - percentY + 0.5;
  }


/* Callback filepond. */
imageEditEditor.events.onconfirm({
  data: {
    crop: {
      center: {
        x: x,
        y: y
      },
      flip: {
        horizontal: cropData.scaleX < 0,
        vertical: cropData.scaleY < 0
      },
      zoom: zoom,
      rotation: (Math.PI / 180) * cropData.rotate,
      aspectRatio: cropAreaRatio
    }
  }
})

@TechMky
Copy link

TechMky commented Feb 17, 2020

Hi @rikschennink ,
Thanks for this great plugin.

Also, thanks to @phlegx for his effort in doing all the math.

I just wanted to know when the onconfirm is called, does, FilePond, subsequently also calls the processFile?

What is happening is, when I look into the uploaded directory, the same image is being uploaded without any changes that the cropper has made?

Any idea what am I mising?

@rikschennink
Copy link
Collaborator

rikschennink commented Feb 17, 2020

@TechMky Hi, yes, it will reprocess the image.

Plan to add a diff here in the future, to quickly compare previous and new file metadata.

@TechMky
Copy link

TechMky commented Feb 17, 2020

@rikschennink I am checking the server logs, FilePond sends the file, the thing I am not able to understand is the server is saving the image but there is no change in the image whatsoever.

@rikschennink
Copy link
Collaborator

Have you installed the image-transform plugin?

@TechMky
Copy link

TechMky commented Feb 17, 2020

@rikschennink I was just going through the code and it clicked. Indeed the image-transform plugin was missing. I came here to ask, and your answer was already here.

Thanks a lot.

P.S. Really like FilePond

@willypoon
Copy link

@rikschennink, after a few days trying, I am still struggling from the zooming issue same as @phlegx did.

In my case I do not need any rotation modification, with the same zoom 'value', when the cropping box not touching the edge of the container, it zooms out.

Knowing that there are some calculation to affect the zooming factors, could you suggest the correct way to calculate the zoom value if there are different cx and cy values? the way suggested by @phlegx is not fixing the issue yet.

Thank you very much

@rikschennink
Copy link
Collaborator

@willypoon #11 (comment)

@willypoon
Copy link

@rikschennink , OK, look forward to seeing the new updates, thank you. Btw, Your works are awesome.

@davideconte-3
Copy link

Hello to all,
could anyone please help me integrating cropper.js with filepond?
@phlegx @Aqueeel

I don't know where to put code lines provided by @phlegx

Thanks in advance

@phlegx
Copy link
Author

phlegx commented Nov 17, 2020

Hi @davideconte!

Put the code in a Vue method (e.g. onConfirm). This method is called, when the user confirms the changes made on cropper.js. After that, I emit the data object to the filepond editor.

@davideconte-3
Copy link

Put the code in a Vue method (e.g. onConfirm). This method is called, when the user confirms the changes made on cropper.js. After that, I emit the data object to the filepond editor.

Thanks, but now the problem is building the cropper :/

I already tried to create as this way:
this.$refs.pond.imageTransformBeforeCreateBlob = (canvas) => new Promise(resolve => { this.cropper = new Cropper(canvas, { aspectRatio: 16 / 9, viewMode: 1 }); resolve(canvas) })

But it doesn't work :/

cropper.js:3413 Uncaught (in promise) TypeError: Cannot read property 'insertBefore' of null at Cropper.clone (cropper.js:3413) because canvas passed from imageTransformBeforeCreateBlob doesn't have parentNode

@tpra7
Copy link

tpra7 commented May 15, 2023

If someone's looking for it after all those years, here's a way to convert Cropper.js crop informations to Filepond's (weird) standard.
I don't use flip or scale, but it works with any image rotation, even if the crop zone goes outside of the rotated image.
It's based on what @phlegx started to do (though there was a problem with comparing ratios on X and Y), plus i added zoom computing based on half-crop-diagonal length compared to the shortest distance between crop center point and the image borders lined on diagonals.
Not that easy to explain with words, i can post a drawing if someone is interested.

Code's not optimized, mainly because it's "easier" to understand and add your own logic for flip scale if needed.
It's all the result of how filepond works : when you have the center point of the crop area, filpond consider zoom:1 as a rectangle that has the croparea ratio and that touches the closest image edge.

Capture d’écran 2023-05-15 à 11 08 59

Hope this helps.

const cropperToFilePondEditorOutput = (cropData, canvasData, imageData) => {

    /* coordinates of each corner of the original image with the origin at the center of the canvas (rotation point) */
    const offsetTopLeftX =  -imageData.naturalWidth/2;
    const offsetTopLeftY =  -imageData.naturalHeight/2;
    const offsetTopRightX =  imageData.naturalWidth/2;
    const offsetTopRightY =  -imageData.naturalHeight/2;
    const offsetBottomLeftX =  -imageData.naturalWidth/2;
    const offsetBottomLeftY =  imageData.naturalHeight/2;
    const offsetBottomRightX = imageData.naturalWidth/2;
    const offsetBottomRightY = imageData.naturalHeight/2;

    /* apply rotation to each corner */
    const rotatedTopLeftX = offsetTopLeftX * Math.cos(cropData.rotate * Math.PI / 180) - offsetTopLeftY * Math.sin(cropData.rotate * Math.PI / 180);
    const rotatedTopLeftY = offsetTopLeftX * Math.sin(cropData.rotate * Math.PI / 180) + offsetTopLeftY * Math.cos(cropData.rotate * Math.PI / 180);
    const rotatedTopRightX = offsetTopRightX * Math.cos(cropData.rotate * Math.PI / 180) - offsetTopRightY * Math.sin(cropData.rotate * Math.PI / 180);
    const rotatedTopRightY = offsetTopRightX * Math.sin(cropData.rotate * Math.PI / 180) + offsetTopRightY * Math.cos(cropData.rotate * Math.PI / 180);
    const rotatedBottomLeftX = offsetBottomLeftX * Math.cos(cropData.rotate * Math.PI / 180) - offsetBottomLeftY * Math.sin(cropData.rotate * Math.PI / 180);
    const rotatedBottomLeftY = offsetBottomLeftX * Math.sin(cropData.rotate * Math.PI / 180) + offsetBottomLeftY * Math.cos(cropData.rotate * Math.PI / 180);
    const rotatedBottomRightX = offsetBottomRightX * Math.cos(cropData.rotate * Math.PI / 180) - offsetBottomRightY * Math.sin(cropData.rotate * Math.PI / 180);
    const rotatedBottomRightY = offsetBottomRightX * Math.sin(cropData.rotate * Math.PI / 180) + offsetBottomRightY * Math.cos(cropData.rotate * Math.PI / 180);

    /* offset coordinates so origin is the top left corner of the rotated canvas (ie use canvasData width and height) */
    const translatedTopLeftX = rotatedTopLeftX + canvasData.naturalWidth/2;
    const translatedTopLeftY = rotatedTopLeftY + canvasData.naturalHeight/2;
    const translatedTopRightX = rotatedTopRightX + canvasData.naturalWidth/2;
    const translatedTopRightY = rotatedTopRightY + canvasData.naturalHeight/2;
    const translatedBottomLeftX = rotatedBottomLeftX + canvasData.naturalWidth/2;
    const translatedBottomLeftY = rotatedBottomLeftY + canvasData.naturalHeight/2;
    const translatedBottomRightX = rotatedBottomRightX + canvasData.naturalWidth/2;
    const translatedBottomRightY = rotatedBottomRightY + canvasData.naturalHeight/2;

    /* Center point of crop area in rotated coordinates */
    let centerX = cropData.x + cropData.width / 2
    let centerY = cropData.y + cropData.height / 2


    let distances = [];

    distances.push(getCenterPointToIntersectionDistance(centerX, centerY, centerX + cropData.width/2, centerY + cropData.height/2, translatedTopLeftX, translatedTopLeftY, translatedTopRightX, translatedTopRightY ));
    distances.push(getCenterPointToIntersectionDistance(centerX, centerY, centerX + cropData.width/2, centerY + cropData.height/2, translatedTopLeftX, translatedTopLeftY, translatedBottomLeftX, translatedBottomLeftY ));
    distances.push(getCenterPointToIntersectionDistance(centerX, centerY, centerX + cropData.width/2, centerY + cropData.height/2, translatedBottomLeftX, translatedBottomLeftY, translatedBottomRightX, translatedBottomRightY ));
    distances.push(getCenterPointToIntersectionDistance(centerX, centerY, centerX + cropData.width/2, centerY + cropData.height/2, translatedTopRightX, translatedTopRightY, translatedBottomRightX, translatedBottomRightY ));

    distances.push(getCenterPointToIntersectionDistance(centerX, centerY, centerX + cropData.width/2, centerY - cropData.height/2, translatedTopLeftX, translatedTopLeftY, translatedTopRightX, translatedTopRightY ));
    distances.push(getCenterPointToIntersectionDistance(centerX, centerY, centerX + cropData.width/2, centerY - cropData.height/2, translatedTopLeftX, translatedTopLeftY, translatedBottomLeftX, translatedBottomLeftY ));
    distances.push(getCenterPointToIntersectionDistance(centerX, centerY, centerX + cropData.width/2, centerY - cropData.height/2, translatedBottomLeftX, translatedBottomLeftY, translatedBottomRightX, translatedBottomRightY ));
    distances.push(getCenterPointToIntersectionDistance(centerX, centerY, centerX + cropData.width/2, centerY - cropData.height/2, translatedTopRightX, translatedTopRightY, translatedBottomRightX, translatedBottomRightY ));

    // remove false values from array
    distances = distances.filter(function (el) { return el !== false; });

    let shortestDistance = Math.min(...distances);
    // gets the zoom from shortest distance and half diagonal of crop area
    let zoom = shortestDistance / Math.sqrt(Math.pow(cropData.width/2, 2) + Math.pow(cropData.height/2, 2));
    
    /* Center point in the non-rotated image coordinates (for filepond) */

    /* offset coordinates so origin is the center of the canvas (rotation point) */
    const offsetX = centerX - canvasData.naturalWidth/2;
    const offsetY = centerY - canvasData.naturalHeight/2;
    
    /* apply reverse rotation to the point */
    const rotatedX = offsetX * Math.cos(-cropData.rotate * Math.PI / 180) - offsetY * Math.sin(-cropData.rotate * Math.PI / 180);
    const rotatedY = offsetX * Math.sin(-cropData.rotate * Math.PI / 180) + offsetY * Math.cos(-cropData.rotate * Math.PI / 180);
    
    /* offset coordinates so origin is the top left corner of the unrotated canvas (ie use imageData width and height) */
    const translatedX = rotatedX + imageData.naturalWidth/2;
    const translatedY = rotatedY + imageData.naturalHeight/2;
    
    centerX = translatedX;
    centerY = translatedY;

    /* Ratio of selected crop area. */
    const cropAreaRatio = cropData.height / cropData.width

    /* Center point mapped to a [0,1] range (that's what filepond waits for) */
    const mappedCenterX = centerX / imageData.naturalWidth;
    const mappedCenterY = centerY / imageData.naturalHeight;
    
    const filepondCropData = {
        data: {
            crop: {
                center: {
                    x: mappedCenterX,
                    y: mappedCenterY
                },
                flip: {
                    horizontal: cropData.scaleX < 0,
                    vertical: cropData.scaleY < 0
                },
                zoom: zoom,
                rotation: (Math.PI / 180) * cropData.rotate,
                aspectRatio: cropAreaRatio
            }
        }
    }
    
    return filepondCropData;

}


const getCenterPointToIntersectionDistance = (cropCenterX, cropCenterY, cropDirectionPointX, cropDirectionPointY, linePointX1, linePointY1, linePointX2, linePointY2) => {
    const intersectionPoint = intersect(cropCenterX, cropCenterY, cropDirectionPointX, cropDirectionPointY, linePointX1, linePointY1, linePointX2, linePointY2);
    if (intersectionPoint) {
        const distance = Math.sqrt(Math.pow(intersectionPoint.x - cropCenterX, 2) + Math.pow(intersectionPoint.y - cropCenterY, 2));
        return distance;
    }
    return false;
}



/* line intercept math by Paul Bourke http://paulbourke.net/geometry/pointlineplane/
/ Determine the intersection point of two line segments
/ Return FALSE if the lines don't intersect */
function intersect(x1, y1, x2, y2, x3, y3, x4, y4) {
    if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) return false ; // Zero length line
    const denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
    if (denominator === 0) return false; // Lines are parallel
    let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator
    let x = x1 + ua * (x2 - x1)
    let y = y1 + ua * (y2 - y1)
    return {x, y}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants