From b2db64022fe6b6fa474c1e8524909bd38beb238b Mon Sep 17 00:00:00 2001 From: Grishka Date: Mon, 6 Jun 2022 16:45:56 +0300 Subject: [PATCH] Add pre-upload avatar and header resizing --- .../api/AvatarResizedImageRequestBody.java | 39 +++++ .../android/api/ResizedImageRequestBody.java | 136 +++++++++++++----- .../accounts/UpdateAccountCredentials.java | 13 +- 3 files changed, 148 insertions(+), 40 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/api/AvatarResizedImageRequestBody.java diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/AvatarResizedImageRequestBody.java b/mastodon/src/main/java/org/joinmastodon/android/api/AvatarResizedImageRequestBody.java new file mode 100644 index 00000000..905f31be --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/AvatarResizedImageRequestBody.java @@ -0,0 +1,39 @@ +package org.joinmastodon.android.api; + +import android.graphics.Rect; +import android.net.Uri; + +import java.io.IOException; + +public class AvatarResizedImageRequestBody extends ResizedImageRequestBody{ + public AvatarResizedImageRequestBody(Uri uri, ProgressListener progressListener) throws IOException{ + super(uri, 0, progressListener); + } + + @Override + protected int[] getTargetSize(int srcWidth, int srcHeight){ + float factor=400f/Math.min(srcWidth, srcHeight); + return new int[]{Math.round(srcWidth*factor), Math.round(srcHeight*factor)}; + } + + @Override + protected boolean needResize(int srcWidth, int srcHeight){ + return srcHeight>400 || srcWidth!=srcHeight; + } + + @Override + protected boolean needCrop(int srcWidth, int srcHeight){ + return srcWidth!=srcHeight; + } + + @Override + protected Rect getCropBounds(int srcWidth, int srcHeight){ + Rect rect=new Rect(); + if(srcWidth>srcHeight){ + rect.set(srcWidth/2-srcHeight/2, 0, srcWidth/2-srcHeight/2+srcHeight, srcHeight); + }else{ + rect.set(0, srcHeight/2-srcWidth/2, srcWidth, srcHeight/2-srcWidth/2+srcWidth); + } + return rect; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/ResizedImageRequestBody.java b/mastodon/src/main/java/org/joinmastodon/android/api/ResizedImageRequestBody.java index ee837681..1bc67178 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/ResizedImageRequestBody.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/ResizedImageRequestBody.java @@ -14,6 +14,7 @@ import android.os.Build; import android.provider.OpenableColumns; import org.joinmastodon.android.MastodonApp; +import org.joinmastodon.android.ui.utils.UiUtils; import java.io.File; import java.io.FileOutputStream; @@ -30,62 +31,105 @@ public class ResizedImageRequestBody extends CountingRequestBody{ private File tempFile; private Uri uri; private String contentType; + private int maxSize; public ResizedImageRequestBody(Uri uri, int maxSize, ProgressListener progressListener) throws IOException{ super(progressListener); this.uri=uri; - contentType=MastodonApp.context.getContentResolver().getType(uri); + this.maxSize=maxSize; BitmapFactory.Options opts=new BitmapFactory.Options(); opts.inJustDecodeBounds=true; - try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){ - BitmapFactory.decodeStream(in, null, opts); + if("file".equals(uri.getScheme())){ + BitmapFactory.decodeFile(uri.getPath(), opts); + contentType=UiUtils.getFileMediaType(new File(uri.getPath())).type(); + }else{ + try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){ + BitmapFactory.decodeStream(in, null, opts); + } + contentType=MastodonApp.context.getContentResolver().getType(uri); } - if(opts.outWidth*opts.outHeight>maxSize){ + if(needResize(opts.outWidth, opts.outHeight) || needCrop(opts.outWidth, opts.outHeight)){ Bitmap bitmap; - if(Build.VERSION.SDK_INT>=29){ - bitmap=ImageDecoder.decodeBitmap(ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri), (decoder, info, source)->{ - int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getWidth()/info.getSize().getHeight()))); - int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getHeight()/info.getSize().getWidth()))); + if(Build.VERSION.SDK_INT>=28){ + ImageDecoder.Source source; + if("file".equals(uri.getScheme())){ + source=ImageDecoder.createSource(new File(uri.getPath())); + }else{ + source=ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri); + } + bitmap=ImageDecoder.decodeBitmap(source, (decoder, info, _source)->{ + int[] size=getTargetSize(info.getSize().getWidth(), info.getSize().getHeight()); decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); - decoder.setTargetSize(targetWidth, targetHeight); + decoder.setTargetSize(size[0], size[1]); + // Breaks images in mysterious ways +// if(needCrop(size[0], size[1])) +// decoder.setCrop(getCropBounds(size[0], size[1])); }); + if(needCrop(bitmap.getWidth(), bitmap.getHeight())){ + Rect crop=getCropBounds(bitmap.getWidth(), bitmap.getHeight()); + bitmap=Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), crop.height()); + } }else{ - int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outWidth/opts.outHeight))); - int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outHeight/opts.outWidth))); + int[] size=getTargetSize(opts.outWidth, opts.outHeight); + int targetWidth=size[0]; + int targetHeight=size[1]; float factor=opts.outWidth/(float)targetWidth; opts=new BitmapFactory.Options(); opts.inSampleSize=(int)factor; - try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){ - bitmap=BitmapFactory.decodeStream(in, null, opts); + if("file".equals(uri.getScheme())){ + bitmap=BitmapFactory.decodeFile(uri.getPath(), opts); + }else{ + try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){ + bitmap=BitmapFactory.decodeStream(in, null, opts); + } } - if(factor%1f!=0f){ - Bitmap scaled=Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888); - new Canvas(scaled).drawBitmap(bitmap, null, new Rect(0, 0, targetWidth, targetHeight), new Paint(Paint.FILTER_BITMAP_FLAG)); + boolean needCrop=needCrop(targetWidth, targetHeight); + if(factor%1f!=0f || needCrop){ + Rect srcBounds=null; + Rect dstBounds; + if(needCrop){ + Rect crop=getCropBounds(targetWidth, targetHeight); + dstBounds=new Rect(0, 0, crop.width(), crop.height()); + srcBounds=new Rect( + Math.round(crop.left/(float)targetWidth*bitmap.getWidth()), + Math.round(crop.top/(float)targetHeight*bitmap.getHeight()), + Math.round(crop.right/(float)targetWidth*bitmap.getWidth()), + Math.round(crop.bottom/(float)targetHeight*bitmap.getHeight()) + ); + }else{ + dstBounds=new Rect(0, 0, targetWidth, targetHeight); + } + Bitmap scaled=Bitmap.createBitmap(dstBounds.width(), dstBounds.height(), Bitmap.Config.ARGB_8888); + new Canvas(scaled).drawBitmap(bitmap, srcBounds, dstBounds, new Paint(Paint.FILTER_BITMAP_FLAG)); bitmap=scaled; } - if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){ - int rotation; + int orientation=0; + if("file".equals(uri.getScheme())){ + ExifInterface exif=new ExifInterface(uri.getPath()); + orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + }else if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){ try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){ ExifInterface exif=new ExifInterface(in); - int orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - rotation=switch(orientation){ - case ExifInterface.ORIENTATION_ROTATE_90 -> 90; - case ExifInterface.ORIENTATION_ROTATE_180 -> 180; - case ExifInterface.ORIENTATION_ROTATE_270 -> 270; - default -> 0; - }; - } - if(rotation!=0){ - Matrix matrix=new Matrix(); - matrix.setRotate(rotation); - bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false); + orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); } } + int rotation=switch(orientation){ + case ExifInterface.ORIENTATION_ROTATE_90 -> 90; + case ExifInterface.ORIENTATION_ROTATE_180 -> 180; + case ExifInterface.ORIENTATION_ROTATE_270 -> 270; + default -> 0; + }; + if(rotation!=0){ + Matrix matrix=new Matrix(); + matrix.setRotate(rotation); + bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false); + } } - tempFile=new File(MastodonApp.context.getCacheDir(), "tmp_upload_image"); + boolean isPNG="image/png".equals(contentType); + tempFile=File.createTempFile("mastodon_tmp_resized", null); try(FileOutputStream out=new FileOutputStream(tempFile)){ - if("image/png".equals(contentType)){ + if(isPNG){ bitmap.compress(Bitmap.CompressFormat.PNG, 0, out); }else{ bitmap.compress(Bitmap.CompressFormat.JPEG, 97, out); @@ -94,9 +138,13 @@ public class ResizedImageRequestBody extends CountingRequestBody{ } length=tempFile.length(); }else{ - try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){ - cursor.moveToFirst(); - length=cursor.getInt(0); + if("file".equals(uri.getScheme())){ + length=new File(uri.getPath()).length(); + }else{ + try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){ + cursor.moveToFirst(); + length=cursor.getInt(0); + } } } } @@ -125,4 +173,22 @@ public class ResizedImageRequestBody extends CountingRequestBody{ } } } + + protected int[] getTargetSize(int srcWidth, int srcHeight){ + int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)srcWidth/srcHeight))); + int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)srcHeight/srcWidth))); + return new int[]{targetWidth, targetHeight}; + } + + protected boolean needResize(int srcWidth, int srcHeight){ + return srcWidth*srcHeight>maxSize; + } + + protected boolean needCrop(int srcWidth, int srcHeight){ + return false; + } + + protected Rect getCropBounds(int srcWidth, int srcHeight){ + return null; + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentials.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentials.java index 0bd821d7..5724a933 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentials.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentials.java @@ -2,13 +2,16 @@ package org.joinmastodon.android.api.requests.accounts; import android.net.Uri; +import org.joinmastodon.android.api.AvatarResizedImageRequestBody; import org.joinmastodon.android.api.ContentUriRequestBody; import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.api.ResizedImageRequestBody; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.AccountField; import org.joinmastodon.android.ui.utils.UiUtils; import java.io.File; +import java.io.IOException; import java.util.List; import okhttp3.MultipartBody; @@ -39,21 +42,21 @@ public class UpdateAccountCredentials extends MastodonAPIRequest{ } @Override - public RequestBody getRequestBody(){ + public RequestBody getRequestBody() throws IOException{ MultipartBody.Builder bldr=new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("display_name", displayName) .addFormDataPart("note", bio); if(avatar!=null){ - bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new ContentUriRequestBody(avatar, null)); + bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new AvatarResizedImageRequestBody(avatar, null)); }else if(avatarFile!=null){ - bldr.addFormDataPart("avatar", avatarFile.getName(), RequestBody.create(UiUtils.getFileMediaType(avatarFile), avatarFile)); + bldr.addFormDataPart("avatar", avatarFile.getName(), new AvatarResizedImageRequestBody(Uri.fromFile(avatarFile), null)); } if(cover!=null){ - bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ContentUriRequestBody(cover, null)); + bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ResizedImageRequestBody(cover, 1500*500, null)); }else if(coverFile!=null){ - bldr.addFormDataPart("header", coverFile.getName(), RequestBody.create(UiUtils.getFileMediaType(coverFile), coverFile)); + bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null)); } if(fields.isEmpty()){ bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");