That fuck shit the fascists are using
1package org.tm.archive.scribbles;
2
3import android.content.Context;
4import android.graphics.Bitmap;
5import android.graphics.Matrix;
6import android.graphics.Paint;
7import android.graphics.Point;
8import android.graphics.PorterDuff;
9import android.graphics.PorterDuffXfermode;
10import android.graphics.RectF;
11import android.graphics.drawable.Drawable;
12import android.net.Uri;
13import android.os.Parcel;
14import android.renderscript.Allocation;
15import android.renderscript.Element;
16import android.renderscript.RenderScript;
17import android.renderscript.ScriptIntrinsicBlur;
18
19import androidx.annotation.NonNull;
20import androidx.annotation.Nullable;
21
22import com.bumptech.glide.Glide;
23import com.bumptech.glide.RequestBuilder;
24import com.bumptech.glide.request.RequestListener;
25import com.bumptech.glide.request.target.CustomTarget;
26import com.bumptech.glide.request.transition.Transition;
27
28import org.signal.core.util.logging.Log;
29import org.signal.imageeditor.core.Bounds;
30import org.signal.imageeditor.core.Renderer;
31import org.signal.imageeditor.core.RendererContext;
32import org.signal.imageeditor.core.SelectableRenderer;
33import org.signal.imageeditor.core.model.EditorElement;
34import org.signal.imageeditor.core.model.EditorModel;
35import org.tm.archive.mms.DecryptableStreamUriLoader;
36import org.tm.archive.util.BitmapUtil;
37
38import java.util.concurrent.ExecutionException;
39
40/**
41 * Uses Glide to load an image and implements a {@link Renderer}.
42 *
43 * The image can be encrypted.
44 */
45public final class UriGlideRenderer implements SelectableRenderer {
46
47 private static final String TAG = Log.tag(UriGlideRenderer.class);
48
49 private static final int PREVIEW_DIMENSION_LIMIT = 2048;
50 private static final int MAX_BLUR_DIMENSION = 300;
51
52 public static final float WEAK_BLUR = 3f;
53 public static final float STRONG_BLUR = 25f;
54
55 private final Uri imageUri;
56 private final Paint paint = new Paint();
57 private final Matrix imageProjectionMatrix = new Matrix();
58 private final Matrix temp = new Matrix();
59 private final Matrix blurScaleMatrix = new Matrix();
60 private final boolean decryptable;
61 private final int maxWidth;
62 private final int maxHeight;
63 private final float blurRadius;
64 private final RequestListener<Bitmap> bitmapRequestListener;
65
66 private boolean selected;
67
68 @Nullable private Bitmap bitmap;
69 @Nullable private Bitmap blurredBitmap;
70 @Nullable private Paint blurPaint;
71
72 public UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight) {
73 this(imageUri, decryptable, maxWidth, maxHeight, STRONG_BLUR);
74 }
75
76 public UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight, float blurRadius) {
77 this(imageUri, decryptable, maxWidth, maxHeight, blurRadius, null);
78 }
79
80 public UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight, float blurRadius, @Nullable RequestListener<Bitmap> bitmapRequestListener) {
81 this.imageUri = imageUri;
82 this.decryptable = decryptable;
83 this.maxWidth = maxWidth;
84 this.maxHeight = maxHeight;
85 this.blurRadius = blurRadius;
86 this.bitmapRequestListener = bitmapRequestListener;
87 paint.setAntiAlias(true);
88 paint.setFilterBitmap(true);
89 paint.setDither(true);
90 }
91
92 @Override
93 public void render(@NonNull RendererContext rendererContext) {
94 if (getBitmap() == null) {
95 if (rendererContext.isBlockingLoad()) {
96 try {
97 Bitmap bitmap = getGlideRequestBuilder(rendererContext.context, false).submit().get();
98 setBitmap(rendererContext, bitmap);
99 } catch (ExecutionException | InterruptedException e) {
100 throw new RuntimeException(e);
101 }
102 } else {
103 getGlideRequestBuilder(rendererContext.context, true).into(new CustomTarget<Bitmap>() {
104 @Override
105 public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
106 setBitmap(rendererContext, resource);
107
108 rendererContext.invalidate.onInvalidate(UriGlideRenderer.this);
109 }
110
111 @Override
112 public void onLoadCleared(@Nullable Drawable placeholder) {
113 bitmap = null;
114 }
115 });
116 }
117 }
118
119 final Bitmap bitmap = getBitmap();
120 if (bitmap != null) {
121 rendererContext.save();
122
123 rendererContext.canvasMatrix.concat(imageProjectionMatrix);
124
125 // Units are image level pixels at this point.
126
127 int alpha = paint.getAlpha();
128 paint.setAlpha(rendererContext.getAlpha(alpha));
129
130 rendererContext.canvas.drawBitmap(bitmap, 0, 0, rendererContext.getMaskPaint() != null ? rendererContext.getMaskPaint() : paint);
131
132 paint.setAlpha(alpha);
133
134 rendererContext.restore();
135
136 renderBlurOverlay(rendererContext);
137 } else if (rendererContext.isBlockingLoad()) {
138 // If failed to load, we draw a black out, in case image was sticker positioned to cover private info.
139 rendererContext.canvas.drawRect(Bounds.FULL_BOUNDS, paint);
140 }
141 }
142
143 private void renderBlurOverlay(RendererContext rendererContext) {
144 boolean renderMask = false;
145
146 for (EditorElement child : rendererContext.getChildren()) {
147 if (child.getZOrder() == EditorModel.Z_MASK) {
148 renderMask = true;
149 if (blurPaint == null) {
150 blurPaint = new Paint();
151 blurPaint.setAntiAlias(true);
152 blurPaint.setFilterBitmap(true);
153 blurPaint.setDither(true);
154 }
155 blurPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
156 rendererContext.setMaskPaint(blurPaint);
157 child.draw(rendererContext);
158 }
159 }
160
161 if (renderMask) {
162 rendererContext.save();
163 rendererContext.canvasMatrix.concat(imageProjectionMatrix);
164
165 blurPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));
166 blurPaint.setMaskFilter(null);
167
168 if (blurredBitmap == null) {
169 blurredBitmap = blur(bitmap, rendererContext.context, blurRadius);
170
171 blurScaleMatrix.setRectToRect(new RectF(0, 0, blurredBitmap.getWidth(), blurredBitmap.getHeight()),
172 new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()),
173 Matrix.ScaleToFit.FILL);
174 }
175
176 rendererContext.canvas.concat(blurScaleMatrix);
177 rendererContext.canvas.drawBitmap(blurredBitmap, 0, 0, blurPaint);
178 blurPaint.setXfermode(null);
179
180 rendererContext.restore();
181 }
182 }
183
184 private RequestBuilder<Bitmap> getGlideRequestBuilder(@NonNull Context context, boolean preview) {
185 int width = this.maxWidth;
186 int height = this.maxHeight;
187
188 if (preview) {
189 width = Math.min(width, PREVIEW_DIMENSION_LIMIT);
190 height = Math.min(height, PREVIEW_DIMENSION_LIMIT);
191 }
192
193 return Glide.with(context)
194 .asBitmap()
195 .override(width, height)
196 .centerInside()
197 .addListener(bitmapRequestListener)
198 .load(decryptable ? new DecryptableStreamUriLoader.DecryptableUri(imageUri) : imageUri);
199 }
200
201 @Override
202 public boolean hitTest(float x, float y) {
203 return selected ? Bounds.contains(x, y) : pixelAlphaNotZero(x, y);
204 }
205
206 private boolean pixelAlphaNotZero(float x, float y) {
207 Bitmap bitmap = getBitmap();
208
209 if (bitmap == null) return false;
210
211 imageProjectionMatrix.invert(temp);
212
213 float[] onBmp = new float[2];
214 temp.mapPoints(onBmp, new float[]{ x, y });
215
216 int xInt = (int) onBmp[0];
217 int yInt = (int) onBmp[1];
218
219 if (xInt >= 0 && xInt < bitmap.getWidth() && yInt >= 0 && yInt < bitmap.getHeight()) {
220 return (bitmap.getPixel(xInt, yInt) & 0xff000000) != 0;
221 } else {
222 return false;
223 }
224 }
225
226 /**
227 * Always use this getter, as Bitmap is kept in Glide's LRUCache, so it could have been recycled
228 * by Glide. If it has, or was never set, this method returns null.
229 */
230 public @Nullable Bitmap getBitmap() {
231 if (bitmap != null && bitmap.isRecycled()) {
232 bitmap = null;
233 }
234 return bitmap;
235 }
236
237 private void setBitmap(@NonNull RendererContext rendererContext, @Nullable Bitmap bitmap) {
238 this.bitmap = bitmap;
239 if (bitmap != null) {
240 RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
241 imageProjectionMatrix.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
242 rendererContext.rendererReady.onReady(UriGlideRenderer.this, cropMatrix(bitmap), new Point(bitmap.getWidth(), bitmap.getHeight()));
243 }
244 }
245
246 private static Matrix cropMatrix(Bitmap bitmap) {
247 Matrix matrix = new Matrix();
248 if (bitmap.getWidth() > bitmap.getHeight()) {
249 matrix.preScale(1, ((float) bitmap.getHeight()) / bitmap.getWidth());
250 } else {
251 matrix.preScale(((float) bitmap.getWidth()) / bitmap.getHeight(), 1);
252 }
253 return matrix;
254 }
255
256 private static @NonNull Bitmap blur(Bitmap bitmap, Context context, float blurRadius) {
257 Point previewSize = scaleKeepingAspectRatio(new Point(bitmap.getWidth(), bitmap.getHeight()), PREVIEW_DIMENSION_LIMIT);
258 Point blurSize = scaleKeepingAspectRatio(new Point(previewSize.x / 2, previewSize.y / 2 ), MAX_BLUR_DIMENSION);
259 Bitmap small = BitmapUtil.createScaledBitmap(bitmap, blurSize.x, blurSize.y);
260
261 Log.d(TAG, "Bitmap: " + bitmap.getWidth() + "x" + bitmap.getHeight() + ", Blur: " + blurSize.x + "x" + blurSize.y);
262
263 RenderScript rs = RenderScript.create(context);
264 Allocation input = Allocation.createFromBitmap(rs, small);
265 Allocation output = Allocation.createTyped(rs, input.getType());
266 ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
267
268 script.setRadius(blurRadius);
269 script.setInput(input);
270 script.forEach(output);
271
272 Bitmap blurred = Bitmap.createBitmap(small.getWidth(), small.getHeight(), small.getConfig());
273 output.copyTo(blurred);
274 return blurred;
275 }
276
277 private static @NonNull Point scaleKeepingAspectRatio(@NonNull Point dimens, int maxDimen) {
278 int outX = dimens.x;
279 int outY = dimens.y;
280
281 if (dimens.x > maxDimen || dimens.y > maxDimen) {
282 outX = maxDimen;
283 outY = maxDimen;
284
285 float widthRatio = dimens.x / (float) maxDimen;
286 float heightRatio = dimens.y / (float) maxDimen;
287
288 if (widthRatio > heightRatio) {
289 outY = (int) (dimens.y / widthRatio);
290 } else {
291 outX = (int) (dimens.x / heightRatio);
292 }
293 }
294
295 return new Point(outX, outY);
296 }
297
298 public static final Creator<UriGlideRenderer> CREATOR = new Creator<UriGlideRenderer>() {
299 @Override
300 public UriGlideRenderer createFromParcel(Parcel in) {
301 return new UriGlideRenderer(Uri.parse(in.readString()),
302 in.readInt() == 1,
303 in.readInt(),
304 in.readInt(),
305 in.readFloat()
306 );
307 }
308
309 @Override
310 public UriGlideRenderer[] newArray(int size) {
311 return new UriGlideRenderer[size];
312 }
313 };
314
315 @Override
316 public int describeContents() {
317 return 0;
318 }
319
320 @Override
321 public void writeToParcel(Parcel dest, int flags) {
322 dest.writeString(imageUri.toString());
323 dest.writeInt(decryptable ? 1 : 0);
324 dest.writeInt(maxWidth);
325 dest.writeInt(maxHeight);
326 dest.writeFloat(blurRadius);
327 }
328
329 @Override
330 public void onSelected(boolean selected) {
331 if (this.selected != selected) {
332 this.selected = selected;
333 }
334 }
335
336 @Override
337 public void getSelectionBounds(@NonNull RectF bounds) {
338 bounds.set(Bounds.FULL_BOUNDS);
339 }
340}