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

How to reproduce CroppableImageData transformations on the server? #54

Open
maRci002 opened this issue Dec 4, 2024 · 3 comments
Open

Comments

@maRci002
Copy link

maRci002 commented Dec 4, 2024

I plan to handle image cropping on the server side by exporting the CroppableImageData to the server. Could you clarify the order in which transformations are applied to produce the final image?

For example, is it:

  • Crop -> Flip -> Rotate (including x, y)
    or
  • Rotate (including x, y) -> Flip -> Crop?

This information would be helpful for replicating the transformation pipeline accurately.

@kekland
Copy link
Owner

kekland commented Dec 4, 2024

Hi!

You can see the cropping implementation via Flutter's Canvas in https://github.com/kekland/croppy/blob/06e84b3597571fa1f8f6818f604d80f376bf44fc/lib/src/image/crop_image.dart#L31C1-L32C1.

In short (data is CroppableImageData):

  • Cropping is applied via appliying a Path-based clipping mask (data.cropShape.vgPath). No transformations applied to the clipping mask
  • Canvas is translated by -data.cropRect.left, -data.cropRect.top
  • Canvas is transformed by data.totalImageTransform
  • Image is drawn with the above transformations applied (image's origin is set to 0,0)

As for the transformation matrix itself:

Matrix4 get totalImageTransform =>

Here, data.totalImageTransform is computed as translatedBaseTransformations * currentImageTransform * imageTransform. During the actual image cropping process, currentImageTransform is equal to identity, as there's no in-progress transformations being applied (rotation, scale, etc - based on gesture). So:

data.totalImageTransform = translatedBaseTransformations * imageTransform

translatedBaseTransformations is the baseTransformations matrix, but applied to the center of the image:

Matrix4 translateTransformation(Matrix4 transformation) {
return Matrix4.copy(transformation)
..leftTranslate(
imageSize.width / 2,
imageSize.height / 2,
)
..translate(
-imageSize.width / 2,
-imageSize.height / 2,
);
}
/// Returns the base transformation matrix, translated to the center of the
/// image.
Matrix4 get translatedBaseTransformations =>
translateTransformation(baseTransformations.matrix);

baseTransformations is a struct that stores the raw values for rotation in 3 axes, and scale in 2 axes. This is done so that we can interpolate between the values without causing some weird behavior that happens when we interpolate a matrix directly. https://github.com/kekland/croppy/blob/master/lib/src/model/base_transformations.dart. The actual matrix is computed as: scaleMatrix * rotationMatrix, so the rotation is applied first, and then scale.

So, the imageTransform is applied first (scale-zoom). Then, the base transformations are applied in order of: rotation -> scale (both at the center of the image). Then, a translation is applied (-data.cropRect.topLeft). Applying a cropping mask at origin will then produce the final result.

@maRci002
Copy link
Author

maRci002 commented Dec 5, 2024

Thank you for the detailed explanation! It seems a bit complex but very informative. I tried implementing a method to generate the ImageMagick command based on the transformations, but the result is still not perfect.

Here’s my current implementation:

class CroppableImageData extends Equatable {
   // ...

  String generateImageMagickCommand(String inputFile, String outputFile) {
    final cropLeft = cropRect.left.round();
    final cropTop = cropRect.top.round();
    final cropWidth = cropRect.width.round();
    final cropHeight = cropRect.height.round();

    final scaleX = totalImageTransform.getRow(0)[0];
    final scaleY = totalImageTransform.getRow(1)[1];
    final shearX = totalImageTransform.getRow(0)[1]; // Horizontal shear
    final shearY = totalImageTransform.getRow(1)[0]; // Vertical shear

    final rotationZDegrees = math.atan2(shearY, scaleY) * 180 / math.pi;

    // ImageMagick cropping
    final cropCommand = "-crop ${cropWidth}x$cropHeight+$cropLeft+$cropTop";

    // ImageMagick scaling
    final resizeCommand = "-resize ${scaleX * 100}x${scaleY * 100}";

    // ImageMagick rotation
    final rotateCommand = "-rotate ${rotationZDegrees.toStringAsFixed(2)}";

    // ImageMagick shear
    final shearCommand =
        "-shear ${shearX.toStringAsFixed(2)}x${shearY.toStringAsFixed(2)}";

    final command =
        "magick convert $inputFile $cropCommand $resizeCommand $shearCommand $rotateCommand $outputFile";
    return command;
  }
  
   // ...
}   

@kekland
Copy link
Owner

kekland commented Dec 5, 2024

Hm, from what I can see the difference could be that in croppy the crop mask isn't translated (it's at the origin), and the image itself is moved by cropLeft and cropTop. This can produce a difference depending on the transform matrix, as the image's transform origin will be shifted. From what I can see in the imagemagick code, the crop is moved by cropLeft and cropTop, but the image is left at the origin.

It'd also help if you could share the image (as shown in Croppy), and what the ImageMagick result is

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

2 participants