Skip to content

Commit

Permalink
Feat[cropper]: DO NOT USE. Begin implementing support for running wit…
Browse files Browse the repository at this point in the history
…h BitmapRegionDecoder

TODO:
- actually add cropping to the BitmapRegionDecoder code
- implement out-of-bounds region decoding
  • Loading branch information
artdeell committed Dec 27, 2023
1 parent 5a1f9e9 commit 85a60da
Show file tree
Hide file tree
Showing 7 changed files with 636 additions and 380 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package net.kdt.pojavlaunch.imgcropper;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;

public class BitmapCropBehaviour implements CropperBehaviour{
private final Matrix mTranslateInverse = new Matrix();
protected final Matrix mTranslateMatrix = new Matrix();
private final Matrix mPrescaleMatrix = new Matrix();
private final Matrix mImageMatrix = new Matrix();
protected final Matrix mZoomMatrix = new Matrix();
private boolean mTranslateInverseOutdated = true;
protected Bitmap mOriginalBitmap;
protected CropperView mHostView;
public BitmapCropBehaviour(CropperView hostView) {
mHostView = hostView;
}
@Override
public void pan(float panX, float panY) {
if(mHostView.horizontalLock) panX = 0;
if(mHostView.verticalLock) panY = 0;
mTranslateMatrix.postTranslate(panX, panY);
if(panX != 0 || panY != 0) {
// Only mark the matrix as outdated if any amount of panning has occured
mTranslateInverseOutdated = true;
}
refresh();
}

public void zoom(float zoomLevel, float midpointX, float midpointY) {
// Do this to avoid constantly inverting the same matrix on each touch event.
if(mTranslateInverseOutdated) {
inverse(mTranslateMatrix, mTranslateInverse);
mTranslateInverseOutdated = false;
}
float[] zoomCenter = new float[] {
midpointX,
midpointY
};
float[] realZoomCenter = new float[2];
mTranslateInverse.mapPoints(realZoomCenter, 0, zoomCenter, 0, 1);
mZoomMatrix.postScale(zoomLevel, zoomLevel, realZoomCenter[0], realZoomCenter[1]);
refresh();
}

public int getLargestImageSide() {
if(mOriginalBitmap == null) return 0;
return Math.max(mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight());
}

@Override
public void drawPreHighlight(Canvas canvas) {
canvas.drawBitmap(mOriginalBitmap, mImageMatrix, null);
}

@Override
public void onSelectionRectUpdated() {
computeLocalPrescaleMatrix();
}

public void reset() {
mHostView.reset();
computeLocalPrescaleMatrix();
resetTransforms();
refresh();
}

public void loadBitmap(Bitmap bitmap) {
mOriginalBitmap = bitmap;
reset();
}

protected void refresh() {
mImageMatrix.set(mPrescaleMatrix);
mImageMatrix.postConcat(mZoomMatrix);
mImageMatrix.postConcat(mTranslateMatrix);
mHostView.invalidate();
}

public Bitmap crop(int targetMaxSide) {
Matrix imageInverse = new Matrix();
inverse(mImageMatrix, imageInverse);
// By inverting the matrix we will effectively "divide" our rectangle by it, thus getting
// its two points on the bitmap's surface. Math be cool indeed.
float[] src = new float[] {
mHostView.mSelectionRect.left,
mHostView.mSelectionRect.top,
mHostView.mSelectionRect.right,
mHostView.mSelectionRect.bottom
};
float[] dst = new float[4];
imageInverse.mapPoints(dst, 0, src, 0, 2);
Rect originalBitmapRect = new Rect(
(int)dst[0], (int)dst[1],
(int)dst[2], (int)dst[3]
);
// Pick the best dimensions for the crop result, shrinking the target if necessary.
int targetWidth, targetHeight;
int targetMinDimension = Math.min(originalBitmapRect.width(), originalBitmapRect.height());
if(targetMinDimension > targetMaxSide) {
float ratio = (float) targetMinDimension / targetMaxSide;
targetWidth = (int) (originalBitmapRect.width() * ratio);
targetHeight = (int) (originalBitmapRect.height() * ratio);
}else {
targetWidth = originalBitmapRect.width();
targetHeight = originalBitmapRect.height();
}
Bitmap croppedBitmap = Bitmap.createBitmap(
targetWidth, targetHeight,
mOriginalBitmap.getConfig()
);
// Draw the bitmap on the target. Doing this allows us to not bother with making sure
// that originalBitmapRect is fully contained within image bounds.
Canvas drawCanvas = new Canvas(croppedBitmap);
drawCanvas.drawBitmap(
mOriginalBitmap,
originalBitmapRect,
new Rect(0, 0, targetWidth, targetHeight),
null
);

return croppedBitmap;
}

/**
* Computes a prescale matrix.
* This matrix basically centers the source image in the selection rect.
* Mainly intended for convenience of implementing a "Reset" button.
*/
protected void computePrescaleMatrix(Matrix inMatrix, int imageWidth, int imageHeight) {
if(mOriginalBitmap == null) return;
int selectionRectWidth = mHostView.mSelectionRect.width();
int selectionRectHeight = mHostView.mSelectionRect.height();
// A basic "scale to fit while preserving aspect ratio" I have taken from
// https://stackoverflow.com/a/23105310
float hRatio = (float)selectionRectWidth / imageWidth ;
float vRatio = (float)selectionRectHeight / imageHeight;
float ratio = Math.min (hRatio, vRatio);
float centerShift_x = (selectionRectWidth - imageWidth*ratio) / 2;
float centerShift_y = (selectionRectWidth - imageHeight*ratio) / 2;
centerShift_x += mHostView.mSelectionRect.left;
centerShift_y += mHostView.mSelectionRect.top;
// By doing setScale() we don't have to reset() the matrix beforehand saving us a
// JNI transition
inMatrix.setScale(ratio, ratio);
inMatrix.postTranslate(centerShift_x, centerShift_y);
refresh();
}

private void computeLocalPrescaleMatrix() {
computePrescaleMatrix(
mPrescaleMatrix,
mOriginalBitmap.getWidth(),
mOriginalBitmap.getHeight()
);
}

public void resetTransforms() {
// Don't set the mTranslateInverseOutdated flag to true here as
// the inverse of an identity matrix (aka the matrix we're setting ours to on reset())
// is an identity matrix, which technically means that mTranslateInverse gets up-to-date there
mTranslateMatrix.reset();
mTranslateInverse.reset();
mZoomMatrix.reset();
refresh();
}

/**
* Android's conditions for matrix inversion are wacky, and sometimes it just stops working out
* of the blue. So, when Android's accelerated matrix inverse dies, just invert by hand.
* @param source Source matrix
* @param destination The inverse of the source matrix
*/
protected void inverse(Matrix source, Matrix destination) {
if(source.invert(destination)) return;
float[] matrix = new float[9];
source.getValues(matrix);
inverseMatrix(matrix);
destination.setValues(matrix);
}

// This was made by ChatGPT and i have no clue what's happening here, but it works so eh
private static void inverseMatrix(float[] matrix) {
float determinant = matrix[0] * (matrix[4] * matrix[8] - matrix[5] * matrix[7])
- matrix[1] * (matrix[3] * matrix[8] - matrix[5] * matrix[6])
+ matrix[2] * (matrix[3] * matrix[7] - matrix[4] * matrix[6]);

if (determinant == 0) {
throw new IllegalArgumentException("Matrix is not invertible");
}

float invDet = 1 / determinant;

float temp0 = (matrix[4] * matrix[8] - matrix[5] * matrix[7]);
float temp1 = (matrix[2] * matrix[7] - matrix[1] * matrix[8]);
float temp2 = (matrix[1] * matrix[5] - matrix[2] * matrix[4]);
float temp3 = (matrix[5] * matrix[6] - matrix[3] * matrix[8]);
float temp4 = (matrix[0] * matrix[8] - matrix[2] * matrix[6]);
float temp5 = (matrix[2] * matrix[3] - matrix[0] * matrix[5]);
float temp6 = (matrix[3] * matrix[7] - matrix[4] * matrix[6]);
float temp7 = (matrix[1] * matrix[6] - matrix[0] * matrix[7]);
float temp8 = (matrix[0] * matrix[4] - matrix[1] * matrix[3]);
matrix[0] = temp0 * invDet;
matrix[1] = temp1 * invDet;
matrix[2] = temp2 * invDet;
matrix[3] = temp3 * invDet;
matrix[4] = temp4 * invDet;
matrix[5] = temp5 * invDet;
matrix[6] = temp6 * invDet;
matrix[7] = temp7 * invDet;
matrix[8] = temp8 * invDet;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package net.kdt.pojavlaunch.imgcropper;

import android.graphics.Bitmap;
import android.graphics.Canvas;

public interface CropperBehaviour {
int getLargestImageSide();
void drawPreHighlight(Canvas canvas);
void onSelectionRectUpdated();
void resetTransforms();
void pan(float dx, float dy);
void zoom(float dz, float originX, float originY);
Bitmap crop(int targetMaxSide);
CropperBehaviour DUMMY = new CropperBehaviour() {
@Override
public int getLargestImageSide() {
return 0;
}

@Override
public void drawPreHighlight(Canvas canvas) {

}

@Override
public void onSelectionRectUpdated() {

}

@Override
public void resetTransforms() {

}

@Override
public void pan(float dx, float dy) {

}

@Override
public void zoom(float dz, float originX, float originY) {

}

@Override
public Bitmap crop(int targetMaxSide) {
return null;
}
};
}
Loading

0 comments on commit 85a60da

Please sign in to comment.