From 250402e9b92a3fa84ae2bf8565a6b60cf7ff3af0 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 1 Sep 2020 19:13:35 -0400 Subject: [PATCH] Add support for rendering APNGs. --- app/src/main/java/org/signal/glide/Log.java | 58 ++ .../org/signal/glide/SignalGlideCodecs.java | 18 + .../org/signal/glide/apng/APNGDrawable.java | 52 ++ .../signal/glide/apng/decode/ACTLChunk.java | 27 + .../signal/glide/apng/decode/APNGDecoder.java | 211 +++++++ .../signal/glide/apng/decode/APNGFrame.java | 147 +++++ .../signal/glide/apng/decode/APNGParser.java | 145 +++++ .../org/signal/glide/apng/decode/Chunk.java | 53 ++ .../signal/glide/apng/decode/FCTLChunk.java | 121 ++++ .../signal/glide/apng/decode/FDATChunk.java | 25 + .../signal/glide/apng/decode/IDATChunk.java | 15 + .../signal/glide/apng/decode/IENDChunk.java | 15 + .../signal/glide/apng/decode/IHDRChunk.java | 45 ++ .../signal/glide/apng/decode/StillFrame.java | 49 ++ .../org/signal/glide/apng/io/APNGReader.java | 74 +++ .../org/signal/glide/apng/io/APNGWriter.java | 41 ++ .../glide/common/FrameAnimationDrawable.java | 253 ++++++++ .../org/signal/glide/common/decode/Frame.java | 33 ++ .../glide/common/decode/FrameSeqDecoder.java | 539 ++++++++++++++++++ .../common/executor/FrameDecoderExecutor.java | 70 +++ .../glide/common/io/ByteBufferReader.java | 67 +++ .../glide/common/io/ByteBufferWriter.java | 61 ++ .../signal/glide/common/io/FileReader.java | 30 + .../signal/glide/common/io/FilterReader.java | 63 ++ .../org/signal/glide/common/io/Reader.java | 35 ++ .../signal/glide/common/io/StreamReader.java | 64 +++ .../org/signal/glide/common/io/Writer.java | 29 + .../common/loader/AssetStreamLoader.java | 32 ++ .../glide/common/loader/ByteBufferLoader.java | 26 + .../glide/common/loader/FileLoader.java | 32 ++ .../signal/glide/common/loader/Loader.java | 19 + .../common/loader/ResourceStreamLoader.java | 32 ++ .../glide/common/loader/StreamLoader.java | 25 + .../securesms/ApplicationContext.java | 32 ++ .../glide/cache/ApngBufferCacheDecoder.java | 73 +++ .../cache/ApngFrameDrawableTranscoder.java | 48 ++ .../glide/cache/ApngStreamCacheDecoder.java | 44 ++ .../cache/EncryptedApngCacheEncoder.java | 51 ++ .../cache/EncryptedBitmapCacheDecoder.java | 50 -- .../cache/EncryptedBitmapResourceEncoder.java | 2 - .../glide/cache/EncryptedCacheDecoder.java | 44 ++ .../glide/cache/EncryptedCacheEncoder.java | 2 - .../glide/cache/EncryptedGifCacheDecoder.java | 48 -- .../securesms/mms/SignalGlideModule.java | 30 +- 44 files changed, 2822 insertions(+), 108 deletions(-) create mode 100644 app/src/main/java/org/signal/glide/Log.java create mode 100644 app/src/main/java/org/signal/glide/SignalGlideCodecs.java create mode 100644 app/src/main/java/org/signal/glide/apng/APNGDrawable.java create mode 100644 app/src/main/java/org/signal/glide/apng/decode/ACTLChunk.java create mode 100644 app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java create mode 100644 app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java create mode 100644 app/src/main/java/org/signal/glide/apng/decode/APNGParser.java create mode 100644 app/src/main/java/org/signal/glide/apng/decode/Chunk.java create mode 100644 app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java create mode 100644 app/src/main/java/org/signal/glide/apng/decode/FDATChunk.java create mode 100644 app/src/main/java/org/signal/glide/apng/decode/IDATChunk.java create mode 100644 app/src/main/java/org/signal/glide/apng/decode/IENDChunk.java create mode 100644 app/src/main/java/org/signal/glide/apng/decode/IHDRChunk.java create mode 100644 app/src/main/java/org/signal/glide/apng/decode/StillFrame.java create mode 100644 app/src/main/java/org/signal/glide/apng/io/APNGReader.java create mode 100644 app/src/main/java/org/signal/glide/apng/io/APNGWriter.java create mode 100644 app/src/main/java/org/signal/glide/common/FrameAnimationDrawable.java create mode 100644 app/src/main/java/org/signal/glide/common/decode/Frame.java create mode 100644 app/src/main/java/org/signal/glide/common/decode/FrameSeqDecoder.java create mode 100644 app/src/main/java/org/signal/glide/common/executor/FrameDecoderExecutor.java create mode 100644 app/src/main/java/org/signal/glide/common/io/ByteBufferReader.java create mode 100644 app/src/main/java/org/signal/glide/common/io/ByteBufferWriter.java create mode 100644 app/src/main/java/org/signal/glide/common/io/FileReader.java create mode 100644 app/src/main/java/org/signal/glide/common/io/FilterReader.java create mode 100644 app/src/main/java/org/signal/glide/common/io/Reader.java create mode 100644 app/src/main/java/org/signal/glide/common/io/StreamReader.java create mode 100644 app/src/main/java/org/signal/glide/common/io/Writer.java create mode 100644 app/src/main/java/org/signal/glide/common/loader/AssetStreamLoader.java create mode 100644 app/src/main/java/org/signal/glide/common/loader/ByteBufferLoader.java create mode 100644 app/src/main/java/org/signal/glide/common/loader/FileLoader.java create mode 100644 app/src/main/java/org/signal/glide/common/loader/Loader.java create mode 100644 app/src/main/java/org/signal/glide/common/loader/ResourceStreamLoader.java create mode 100644 app/src/main/java/org/signal/glide/common/loader/StreamLoader.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngFrameDrawableTranscoder.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheEncoder.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapCacheDecoder.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheDecoder.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedGifCacheDecoder.java diff --git a/app/src/main/java/org/signal/glide/Log.java b/app/src/main/java/org/signal/glide/Log.java new file mode 100644 index 000000000..aa41086a3 --- /dev/null +++ b/app/src/main/java/org/signal/glide/Log.java @@ -0,0 +1,58 @@ +package org.signal.glide; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public final class Log { + + private Log() {} + + public static void v(@NonNull String tag, @NonNull String message) { + SignalGlideCodecs.getLogProvider().v(tag, message); + } + + public static void d(@NonNull String tag, @NonNull String message) { + SignalGlideCodecs.getLogProvider().d(tag, message); + } + + public static void i(@NonNull String tag, @NonNull String message) { + SignalGlideCodecs.getLogProvider().i(tag, message); + } + + public static void w(@NonNull String tag, @NonNull String message) { + SignalGlideCodecs.getLogProvider().w(tag, message); + } + + public static void e(@NonNull String tag, @NonNull String message) { + e(tag, message, null); + } + + public static void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) { + SignalGlideCodecs.getLogProvider().e(tag, message, throwable); + } + + public interface Provider { + void v(@NonNull String tag, @NonNull String message); + void d(@NonNull String tag, @NonNull String message); + void i(@NonNull String tag, @NonNull String message); + void w(@NonNull String tag, @NonNull String message); + void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable); + + Provider EMPTY = new Provider() { + @Override + public void v(@NonNull String tag, @NonNull String message) { } + + @Override + public void d(@NonNull String tag, @NonNull String message) { } + + @Override + public void i(@NonNull String tag, @NonNull String message) { } + + @Override + public void w(@NonNull String tag, @NonNull String message) { } + + @Override + public void e(@NonNull String tag, @NonNull String message, @NonNull Throwable throwable) { } + }; + } +} diff --git a/app/src/main/java/org/signal/glide/SignalGlideCodecs.java b/app/src/main/java/org/signal/glide/SignalGlideCodecs.java new file mode 100644 index 000000000..014148a88 --- /dev/null +++ b/app/src/main/java/org/signal/glide/SignalGlideCodecs.java @@ -0,0 +1,18 @@ +package org.signal.glide; + +import androidx.annotation.NonNull; + +public final class SignalGlideCodecs { + + private static Log.Provider logProvider = Log.Provider.EMPTY; + + private SignalGlideCodecs() {} + + public static void setLogProvider(@NonNull Log.Provider provider) { + logProvider = provider; + } + + public static @NonNull Log.Provider getLogProvider() { + return logProvider; + } +} diff --git a/app/src/main/java/org/signal/glide/apng/APNGDrawable.java b/app/src/main/java/org/signal/glide/apng/APNGDrawable.java new file mode 100644 index 000000000..824ef1236 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/APNGDrawable.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng; + +import android.content.Context; + +import org.signal.glide.common.FrameAnimationDrawable; +import org.signal.glide.apng.decode.APNGDecoder; +import org.signal.glide.common.decode.FrameSeqDecoder; +import org.signal.glide.common.loader.AssetStreamLoader; +import org.signal.glide.common.loader.FileLoader; +import org.signal.glide.common.loader.Loader; +import org.signal.glide.common.loader.ResourceStreamLoader; + +/** + * @Description: APNGDrawable + * @Author: pengfei.zhou + * @CreateDate: 2019/3/27 + */ +public class APNGDrawable extends FrameAnimationDrawable { + public APNGDrawable(Loader provider) { + super(provider); + } + + public APNGDrawable(APNGDecoder decoder) { + super(decoder); + } + + @Override + protected APNGDecoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener) { + return new APNGDecoder(streamLoader, listener); + } + + + public static APNGDrawable fromAsset(Context context, String assetPath) { + AssetStreamLoader assetStreamLoader = new AssetStreamLoader(context, assetPath); + return new APNGDrawable(assetStreamLoader); + } + + public static APNGDrawable fromFile(String filePath) { + FileLoader fileLoader = new FileLoader(filePath); + return new APNGDrawable(fileLoader); + } + + public static APNGDrawable fromResource(Context context, int resId) { + ResourceStreamLoader resourceStreamLoader = new ResourceStreamLoader(context, resId); + return new APNGDrawable(resourceStreamLoader); + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/ACTLChunk.java b/app/src/main/java/org/signal/glide/apng/decode/ACTLChunk.java new file mode 100644 index 000000000..37f60d90c --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/ACTLChunk.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.decode; + +import org.signal.glide.apng.io.APNGReader; + +import java.io.IOException; + +/** + * @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27acTL.27:_The_Animation_Control_Chunk + * @Author: pengfei.zhou + * @CreateDate: 2019/3/27 + */ +class ACTLChunk extends Chunk { + static final int ID = fourCCToInt("acTL"); + int num_frames; + int num_plays; + + @Override + void innerParse(APNGReader apngReader) throws IOException { + num_frames = apngReader.readInt(); + num_plays = apngReader.readInt(); + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java b/app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java new file mode 100644 index 000000000..cf1040320 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java @@ -0,0 +1,211 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.decode; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; + +import org.signal.glide.Log; +import org.signal.glide.apng.io.APNGReader; +import org.signal.glide.apng.io.APNGWriter; +import org.signal.glide.common.decode.Frame; +import org.signal.glide.common.decode.FrameSeqDecoder; +import org.signal.glide.common.io.Reader; +import org.signal.glide.common.loader.Loader; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * @Description: APNG4Android + * @Author: pengfei.zhou + * @CreateDate: 2019-05-13 + */ +public class APNGDecoder extends FrameSeqDecoder { + + private static final String TAG = APNGDecoder.class.getSimpleName(); + + private APNGWriter apngWriter; + private int mLoopCount; + private final Paint paint = new Paint(); + + + private class SnapShot { + byte dispose_op; + Rect dstRect = new Rect(); + ByteBuffer byteBuffer; + } + + private SnapShot snapShot = new SnapShot(); + + /** + * @param loader webp的reader + * @param renderListener 渲染的回调 + */ + public APNGDecoder(Loader loader, FrameSeqDecoder.RenderListener renderListener) { + super(loader, renderListener); + paint.setAntiAlias(true); + } + + @Override + protected APNGWriter getWriter() { + if (apngWriter == null) { + apngWriter = new APNGWriter(); + } + return apngWriter; + } + + @Override + protected APNGReader getReader(Reader reader) { + return new APNGReader(reader); + } + + @Override + protected int getLoopCount() { + return mLoopCount; + } + + @Override + protected void release() { + snapShot.byteBuffer = null; + apngWriter = null; + } + + + @Override + protected Rect read(APNGReader reader) throws IOException { + List chunks = APNGParser.parse(reader); + List otherChunks = new ArrayList<>(); + + boolean actl = false; + APNGFrame lastFrame = null; + byte[] ihdrData = new byte[0]; + int canvasWidth = 0, canvasHeight = 0; + for (Chunk chunk : chunks) { + if (chunk instanceof ACTLChunk) { + mLoopCount = ((ACTLChunk) chunk).num_plays; + actl = true; + } else if (chunk instanceof FCTLChunk) { + APNGFrame frame = new APNGFrame(reader, (FCTLChunk) chunk); + frame.prefixChunks = otherChunks; + frame.ihdrData = ihdrData; + frames.add(frame); + lastFrame = frame; + } else if (chunk instanceof FDATChunk) { + if (lastFrame != null) { + lastFrame.imageChunks.add(chunk); + } + } else if (chunk instanceof IDATChunk) { + if (!actl) { + //如果为非APNG图片,则只解码PNG + Frame frame = new StillFrame(reader); + frame.frameWidth = canvasWidth; + frame.frameHeight = canvasHeight; + frames.add(frame); + mLoopCount = 1; + break; + } + if (lastFrame != null) { + lastFrame.imageChunks.add(chunk); + } + + } else if (chunk instanceof IHDRChunk) { + canvasWidth = ((IHDRChunk) chunk).width; + canvasHeight = ((IHDRChunk) chunk).height; + ihdrData = ((IHDRChunk) chunk).data; + } else if (!(chunk instanceof IENDChunk)) { + otherChunks.add(chunk); + } + } + frameBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4); + snapShot.byteBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4); + return new Rect(0, 0, canvasWidth, canvasHeight); + } + + @Override + protected void renderFrame(Frame frame) { + if (frame == null || fullRect == null) { + return; + } + try { + Bitmap bitmap = obtainBitmap(fullRect.width() / sampleSize, fullRect.height() / sampleSize); + Canvas canvas = cachedCanvas.get(bitmap); + if (canvas == null) { + canvas = new Canvas(bitmap); + cachedCanvas.put(bitmap, canvas); + } + if (frame instanceof APNGFrame) { + // 从缓存中恢复当前帧 + frameBuffer.rewind(); + bitmap.copyPixelsFromBuffer(frameBuffer); + // 开始绘制前,处理快照中的设定 + if (this.frameIndex == 0) { + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + } else { + canvas.save(); + canvas.clipRect(snapShot.dstRect); + switch (snapShot.dispose_op) { + // 从快照中恢复上一帧之前的显示内容 + case FCTLChunk.APNG_DISPOSE_OP_PREVIOUS: + snapShot.byteBuffer.rewind(); + bitmap.copyPixelsFromBuffer(snapShot.byteBuffer); + break; + // 清空上一帧所画区域 + case FCTLChunk.APNG_DISPOSE_OP_BACKGROUND: + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + break; + // 什么都不做 + case FCTLChunk.APNG_DISPOSE_OP_NON: + default: + break; + } + canvas.restore(); + } + + // 然后根据dispose设定传递到快照信息中 + if (((APNGFrame) frame).dispose_op == FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) { + if (snapShot.dispose_op != FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) { + snapShot.byteBuffer.rewind(); + bitmap.copyPixelsToBuffer(snapShot.byteBuffer); + } + } + + snapShot.dispose_op = ((APNGFrame) frame).dispose_op; + canvas.save(); + if (((APNGFrame) frame).blend_op == FCTLChunk.APNG_BLEND_OP_SOURCE) { + canvas.clipRect( + frame.frameX / sampleSize, + frame.frameY / sampleSize, + (frame.frameX + frame.frameWidth) / sampleSize, + (frame.frameY + frame.frameHeight) / sampleSize); + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + } + + + snapShot.dstRect.set(frame.frameX / sampleSize, + frame.frameY / sampleSize, + (frame.frameX + frame.frameWidth) / sampleSize, + (frame.frameY + frame.frameHeight) / sampleSize); + canvas.restore(); + } + //开始真正绘制当前帧的内容 + Bitmap inBitmap = obtainBitmap(frame.frameWidth, frame.frameHeight); + recycleBitmap(frame.draw(canvas, paint, sampleSize, inBitmap, getWriter())); + recycleBitmap(inBitmap); + frameBuffer.rewind(); + bitmap.copyPixelsToBuffer(frameBuffer); + recycleBitmap(bitmap); + } catch (Throwable t) { + Log.e(TAG, "Failed to render!", t); + } + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java b/app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java new file mode 100644 index 000000000..fd1ca2706 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java @@ -0,0 +1,147 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.decode; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; + +import org.signal.glide.apng.io.APNGReader; +import org.signal.glide.apng.io.APNGWriter; +import org.signal.glide.common.decode.Frame; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.CRC32; + +/** + * @Description: APNG4Android + * @Author: pengfei.zhou + * @CreateDate: 2019-05-13 + */ +public class APNGFrame extends Frame { + public final byte blend_op; + public final byte dispose_op; + byte[] ihdrData; + List imageChunks = new ArrayList<>(); + List prefixChunks = new ArrayList<>(); + private static final byte[] sPNGSignatures = {(byte) 137, 80, 78, 71, 13, 10, 26, 10}; + private static final byte[] sPNGEndChunk = {0, 0, 0, 0, 0x49, 0x45, 0x4E, 0x44, (byte) 0xAE, 0x42, 0x60, (byte) 0x82}; + + private static ThreadLocal sCRC32 = new ThreadLocal<>(); + + private CRC32 getCRC32() { + CRC32 crc32 = sCRC32.get(); + if (crc32 == null) { + crc32 = new CRC32(); + sCRC32.set(crc32); + } + return crc32; + } + + public APNGFrame(APNGReader reader, FCTLChunk fctlChunk) { + super(reader); + blend_op = fctlChunk.blend_op; + dispose_op = fctlChunk.dispose_op; + frameDuration = fctlChunk.delay_num * 1000 / (fctlChunk.delay_den == 0 ? 100 : fctlChunk.delay_den); + frameWidth = fctlChunk.width; + frameHeight = fctlChunk.height; + frameX = fctlChunk.x_offset; + frameY = fctlChunk.y_offset; + } + + private int encode(APNGWriter apngWriter) throws IOException { + int fileSize = 8 + 13 + 12; + + //prefixChunks + for (Chunk chunk : prefixChunks) { + fileSize += chunk.length + 12; + } + + //imageChunks + for (Chunk chunk : imageChunks) { + if (chunk instanceof IDATChunk) { + fileSize += chunk.length + 12; + } else if (chunk instanceof FDATChunk) { + fileSize += chunk.length + 8; + } + } + fileSize += sPNGEndChunk.length; + apngWriter.reset(fileSize); + apngWriter.putBytes(sPNGSignatures); + //IHDR Chunk + apngWriter.writeInt(13); + int start = apngWriter.position(); + apngWriter.writeFourCC(IHDRChunk.ID); + apngWriter.writeInt(frameWidth); + apngWriter.writeInt(frameHeight); + apngWriter.putBytes(ihdrData); + CRC32 crc32 = getCRC32(); + crc32.reset(); + crc32.update(apngWriter.toByteArray(), start, 17); + apngWriter.writeInt((int) crc32.getValue()); + + //prefixChunks + for (Chunk chunk : prefixChunks) { + if (chunk instanceof IENDChunk) { + continue; + } + reader.reset(); + reader.skip(chunk.offset); + reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12); + apngWriter.skip(chunk.length + 12); + } + //imageChunks + for (Chunk chunk : imageChunks) { + if (chunk instanceof IDATChunk) { + reader.reset(); + reader.skip(chunk.offset); + reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12); + apngWriter.skip(chunk.length + 12); + } else if (chunk instanceof FDATChunk) { + apngWriter.writeInt(chunk.length - 4); + start = apngWriter.position(); + apngWriter.writeFourCC(IDATChunk.ID); + + reader.reset(); + // skip to fdat data position + reader.skip(chunk.offset + 4 + 4 + 4); + reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length - 4); + + apngWriter.skip(chunk.length - 4); + crc32.reset(); + crc32.update(apngWriter.toByteArray(), start, chunk.length); + apngWriter.writeInt((int) crc32.getValue()); + } + } + //endChunk + apngWriter.putBytes(sPNGEndChunk); + return fileSize; + } + + + @Override + public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) { + try { + int length = encode(writer); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = false; + options.inSampleSize = sampleSize; + options.inMutable = true; + options.inBitmap = reusedBitmap; + byte[] bytes = writer.toByteArray(); + Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, length, options); + assert bitmap != null; + canvas.drawBitmap(bitmap, (float) frameX / sampleSize, (float) frameY / sampleSize, paint); + return bitmap; + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java b/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java new file mode 100644 index 000000000..79dff419d --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java @@ -0,0 +1,145 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.decode; + +import android.content.Context; + +import org.signal.glide.apng.io.APNGReader; +import org.signal.glide.common.io.Reader; +import org.signal.glide.common.io.StreamReader; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * @link {https://www.w3.org/TR/PNG/#5PNG-file-signature} + * @Author: pengfei.zhou + * @CreateDate: 2019-05-13 + */ +public class APNGParser { + static class FormatException extends IOException { + FormatException() { + super("APNG Format error"); + } + } + + public static boolean isAPNG(String filePath) { + InputStream inputStream = null; + try { + inputStream = new FileInputStream(filePath); + return isAPNG(new StreamReader(inputStream)); + } catch (Exception e) { + return false; + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + public static boolean isAPNG(Context context, String assetPath) { + InputStream inputStream = null; + try { + inputStream = context.getAssets().open(assetPath); + return isAPNG(new StreamReader(inputStream)); + } catch (Exception e) { + return false; + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + public static boolean isAPNG(Context context, int resId) { + InputStream inputStream = null; + try { + inputStream = context.getResources().openRawResource(resId); + return isAPNG(new StreamReader(inputStream)); + } catch (Exception e) { + return false; + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + public static boolean isAPNG(Reader in) { + APNGReader reader = (in instanceof APNGReader) ? (APNGReader) in : new APNGReader(in); + try { + if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) { + throw new FormatException(); + } + while (reader.available() > 0) { + Chunk chunk = parseChunk(reader); + if (chunk instanceof ACTLChunk) { + return true; + } + } + } catch (IOException e) { + if (!(e instanceof FormatException)) { + e.printStackTrace(); + } + } + return false; + } + + public static List parse(APNGReader reader) throws IOException { + if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) { + throw new FormatException(); + } + + List chunks = new ArrayList<>(); + while (reader.available() > 0) { + chunks.add(parseChunk(reader)); + } + return chunks; + } + + private static Chunk parseChunk(APNGReader reader) throws IOException { + int offset = reader.position(); + int size = reader.readInt(); + int fourCC = reader.readFourCC(); + Chunk chunk; + if (fourCC == ACTLChunk.ID) { + chunk = new ACTLChunk(); + } else if (fourCC == FCTLChunk.ID) { + chunk = new FCTLChunk(); + } else if (fourCC == FDATChunk.ID) { + chunk = new FDATChunk(); + } else if (fourCC == IDATChunk.ID) { + chunk = new IDATChunk(); + } else if (fourCC == IENDChunk.ID) { + chunk = new IENDChunk(); + } else if (fourCC == IHDRChunk.ID) { + chunk = new IHDRChunk(); + } else { + chunk = new Chunk(); + } + chunk.offset = offset; + chunk.fourcc = fourCC; + chunk.length = size; + chunk.parse(reader); + chunk.crc = reader.readInt(); + return chunk; + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/Chunk.java b/app/src/main/java/org/signal/glide/apng/decode/Chunk.java new file mode 100644 index 000000000..192cc0fd6 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/Chunk.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.decode; + +import android.text.TextUtils; + +import org.signal.glide.apng.io.APNGReader; + +import java.io.IOException; + +/** + * @Description: Length (长度) 4字节 指定数据块中数据域的长度,其长度不超过(231-1)字节 + * Chunk Type Code (数据块类型码) 4字节 数据块类型码由ASCII字母(A-Z和a-z)组成 + * Chunk Data (数据块数据) 可变长度 存储按照Chunk Type Code指定的数据 + * CRC (循环冗余检测) 4字节 存储用来检测是否有错误的循环冗余码 + * @Link https://www.w3.org/TR/PNG + * @Author: pengfei.zhou + * @CreateDate: 2019/3/27 + */ +class Chunk { + int length; + int fourcc; + int crc; + int offset; + + static int fourCCToInt(String fourCC) { + if (TextUtils.isEmpty(fourCC) || fourCC.length() != 4) { + return 0xbadeffff; + } + return (fourCC.charAt(0) & 0xff) + | (fourCC.charAt(1) & 0xff) << 8 + | (fourCC.charAt(2) & 0xff) << 16 + | (fourCC.charAt(3) & 0xff) << 24 + ; + } + + void parse(APNGReader reader) throws IOException { + int available = reader.available(); + innerParse(reader); + int offset = available - reader.available(); + if (offset > length) { + throw new IOException("Out of chunk area"); + } else if (offset < length) { + reader.skip(length - offset); + } + } + + void innerParse(APNGReader reader) throws IOException { + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java b/app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java new file mode 100644 index 000000000..9e74d806f --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java @@ -0,0 +1,121 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.decode; + +import org.signal.glide.apng.io.APNGReader; + +import java.io.IOException; + +/** + * @Author: pengfei.zhou + * @CreateDate: 2019/3/27 + * @see {link=https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fcTL.27:_The_Frame_Control_Chunk} + */ +class FCTLChunk extends Chunk { + static final int ID = fourCCToInt("fcTL"); + int sequence_number; + /** + * x_offset >= 0 + * y_offset >= 0 + * width > 0 + * height > 0 + * x_offset + width <= 'IHDR' width + * y_offset + height <= 'IHDR' height + */ + /** + * Width of the following frame. + */ + int width; + /** + * Height of the following frame. + */ + int height; + /** + * X position at which to render the following frame. + */ + int x_offset; + /** + * Y position at which to render the following frame. + */ + int y_offset; + + /** + * The delay_num and delay_den parameters together specify a fraction indicating the time to + * display the current frame, in seconds. If the denominator is 0, it is to be treated as if it + * were 100 (that is, delay_num then specifies 1/100ths of a second). + * If the the value of the numerator is 0 the decoder should render the next frame as quickly as + * possible, though viewers may impose a reasonable lower bound. + *

+ * Frame timings should be independent of the time required for decoding and display of each frame, + * so that animations will run at the same rate regardless of the performance of the decoder implementation. + */ + + /** + * Frame delay fraction numerator. + */ + short delay_num; + + /** + * Frame delay fraction denominator. + */ + short delay_den; + + /** + * Type of frame area disposal to be done after rendering this frame. + * dispose_op specifies how the output buffer should be changed at the end of the delay (before rendering the next frame). + * If the first 'fcTL' chunk uses a dispose_op of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND. + */ + byte dispose_op; + + /** + * Type of frame area rendering for this frame. + */ + byte blend_op; + + /** + * No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. + */ + static final int APNG_DISPOSE_OP_NON = 0; + + /** + * The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. + */ + static final int APNG_DISPOSE_OP_BACKGROUND = 1; + + /** + * The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame. + */ + static final int APNG_DISPOSE_OP_PREVIOUS = 2; + + /** + * blend_op specifies whether the frame is to be alpha blended into the current output buffer content, + * or whether it should completely replace its region in the output buffer. + */ + /** + * All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region. + */ + static final int APNG_BLEND_OP_SOURCE = 0; + + /** + * The frame should be composited onto the output buffer based on its alpha, + * using a simple OVER operation as described in the Alpha Channel Processing section of the Extensions + * to the PNG Specification, Version 1.2.0. Note that the second variation of the sample code is applicable. + */ + static final int APNG_BLEND_OP_OVER = 1; + + @Override + void innerParse(APNGReader reader) throws IOException { + sequence_number = reader.readInt(); + width = reader.readInt(); + height = reader.readInt(); + x_offset = reader.readInt(); + y_offset = reader.readInt(); + delay_num = reader.readShort(); + delay_den = reader.readShort(); + dispose_op = reader.peek(); + blend_op = reader.peek(); + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/FDATChunk.java b/app/src/main/java/org/signal/glide/apng/decode/FDATChunk.java new file mode 100644 index 000000000..1618c59df --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/FDATChunk.java @@ -0,0 +1,25 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.decode; + +import org.signal.glide.apng.io.APNGReader; + +import java.io.IOException; + +/** + * @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fdAT.27:_The_Frame_Data_Chunk + * @Author: pengfei.zhou + * @CreateDate: 2019/3/27 + */ +class FDATChunk extends Chunk { + static final int ID = fourCCToInt("fdAT"); + int sequence_number; + + @Override + void innerParse(APNGReader reader) throws IOException { + sequence_number = reader.readInt(); + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/IDATChunk.java b/app/src/main/java/org/signal/glide/apng/decode/IDATChunk.java new file mode 100644 index 000000000..bd7a60fee --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/IDATChunk.java @@ -0,0 +1,15 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.decode; + +/** + * @Description: 作用描述 + * @Author: pengfei.zhou + * @CreateDate: 2019/3/27 + */ +class IDATChunk extends Chunk { + static final int ID = fourCCToInt("IDAT"); +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/IENDChunk.java b/app/src/main/java/org/signal/glide/apng/decode/IENDChunk.java new file mode 100644 index 000000000..f0cbd8008 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/IENDChunk.java @@ -0,0 +1,15 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.decode; + +/** + * @Description: 作用描述 + * @Author: pengfei.zhou + * @CreateDate: 2019/3/27 + */ +class IENDChunk extends Chunk { + static final int ID = Chunk.fourCCToInt("IEND"); +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/IHDRChunk.java b/app/src/main/java/org/signal/glide/apng/decode/IHDRChunk.java new file mode 100644 index 000000000..eebd9d278 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/IHDRChunk.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.decode; + +import org.signal.glide.apng.io.APNGReader; + +import java.io.IOException; + +/** + * The IHDR chunk shall be the first chunk in the PNG datastream. It contains: + *

+ * Width 4 bytes + * Height 4 bytes + * Bit depth 1 byte + * Colour type 1 byte + * Compression method 1 byte + * Filter method 1 byte + * Interlace method 1 byte + * + * @Author: pengfei.zhou + * @CreateDate: 2019/3/27 + */ +class IHDRChunk extends Chunk { + static final int ID = fourCCToInt("IHDR"); + /** + * 图像宽度,以像素为单位 + */ + int width; + /** + * 图像高度,以像素为单位 + */ + int height; + + byte[] data = new byte[5]; + + @Override + void innerParse(APNGReader reader) throws IOException { + width = reader.readInt(); + height = reader.readInt(); + reader.read(data, 0, data.length); + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/StillFrame.java b/app/src/main/java/org/signal/glide/apng/decode/StillFrame.java new file mode 100644 index 000000000..65715f1e4 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/StillFrame.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.decode; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; + +import org.signal.glide.apng.io.APNGReader; +import org.signal.glide.apng.io.APNGWriter; +import org.signal.glide.common.decode.Frame; + +import java.io.IOException; + +/** + * @Description: APNG4Android + * @Author: pengfei.zhou + * @CreateDate: 2019-05-13 + */ +public class StillFrame extends Frame { + + public StillFrame(APNGReader reader) { + super(reader); + } + + @Override + public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = false; + options.inSampleSize = sampleSize; + options.inMutable = true; + options.inBitmap = reusedBitmap; + Bitmap bitmap = null; + try { + reader.reset(); + bitmap = BitmapFactory.decodeStream(reader.toInputStream(), null, options); + assert bitmap != null; + paint.setXfermode(null); + canvas.drawBitmap(bitmap, 0, 0, paint); + } catch (IOException e) { + e.printStackTrace(); + } + return bitmap; + } +} diff --git a/app/src/main/java/org/signal/glide/apng/io/APNGReader.java b/app/src/main/java/org/signal/glide/apng/io/APNGReader.java new file mode 100644 index 000000000..293ed2461 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/io/APNGReader.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.io; + +import android.text.TextUtils; + +import org.signal.glide.common.io.FilterReader; +import org.signal.glide.common.io.Reader; + +import java.io.IOException; + +/** + * @Description: APNGReader + * @Author: pengfei.zhou + * @CreateDate: 2019-05-13 + */ +public class APNGReader extends FilterReader { + private static ThreadLocal __intBytes = new ThreadLocal<>(); + + + protected static byte[] ensureBytes() { + byte[] bytes = __intBytes.get(); + if (bytes == null) { + bytes = new byte[4]; + __intBytes.set(bytes); + } + return bytes; + } + + public APNGReader(Reader in) { + super(in); + } + + public int readInt() throws IOException { + byte[] buf = ensureBytes(); + read(buf, 0, 4); + return buf[3] & 0xFF | + (buf[2] & 0xFF) << 8 | + (buf[1] & 0xFF) << 16 | + (buf[0] & 0xFF) << 24; + } + + public short readShort() throws IOException { + byte[] buf = ensureBytes(); + read(buf, 0, 2); + return (short) (buf[1] & 0xFF | + (buf[0] & 0xFF) << 8); + } + + /** + * @return read FourCC and match chars + */ + public boolean matchFourCC(String chars) throws IOException { + if (TextUtils.isEmpty(chars) || chars.length() != 4) { + return false; + } + int fourCC = readFourCC(); + for (int i = 0; i < 4; i++) { + if (((fourCC >> (i * 8)) & 0xff) != chars.charAt(i)) { + return false; + } + } + return true; + } + + public int readFourCC() throws IOException { + byte[] buf = ensureBytes(); + read(buf, 0, 4); + return buf[0] & 0xff | (buf[1] & 0xff) << 8 | (buf[2] & 0xff) << 16 | (buf[3] & 0xff) << 24; + } +} diff --git a/app/src/main/java/org/signal/glide/apng/io/APNGWriter.java b/app/src/main/java/org/signal/glide/apng/io/APNGWriter.java new file mode 100644 index 000000000..25a7c2764 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/io/APNGWriter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.apng.io; + +import org.signal.glide.common.io.ByteBufferWriter; + +import java.nio.ByteOrder; + +/** + * @Description: APNGWriter + * @Author: pengfei.zhou + * @CreateDate: 2019-05-13 + */ +public class APNGWriter extends ByteBufferWriter { + public APNGWriter() { + super(); + } + + public void writeFourCC(int val) { + putByte((byte) (val & 0xff)); + putByte((byte) ((val >> 8) & 0xff)); + putByte((byte) ((val >> 16) & 0xff)); + putByte((byte) ((val >> 24) & 0xff)); + } + + public void writeInt(int val) { + putByte((byte) ((val >> 24) & 0xff)); + putByte((byte) ((val >> 16) & 0xff)); + putByte((byte) ((val >> 8) & 0xff)); + putByte((byte) (val & 0xff)); + } + + @Override + public void reset(int size) { + super.reset(size); + this.byteBuffer.order(ByteOrder.BIG_ENDIAN); + } +} diff --git a/app/src/main/java/org/signal/glide/common/FrameAnimationDrawable.java b/app/src/main/java/org/signal/glide/common/FrameAnimationDrawable.java new file mode 100644 index 000000000..57ba5a68b --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/FrameAnimationDrawable.java @@ -0,0 +1,253 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.DrawFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import androidx.annotation.NonNull; +import androidx.vectordrawable.graphics.drawable.Animatable2Compat; + +import org.signal.glide.Log; +import org.signal.glide.common.decode.FrameSeqDecoder; +import org.signal.glide.common.loader.Loader; + +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Set; + +/** + * @Description: Frame animation drawable + * @Author: pengfei.zhou + * @CreateDate: 2019/3/27 + */ +public abstract class FrameAnimationDrawable extends Drawable implements Animatable2Compat, FrameSeqDecoder.RenderListener { + private static final String TAG = FrameAnimationDrawable.class.getSimpleName(); + private final Paint paint = new Paint(); + private final Decoder frameSeqDecoder; + private DrawFilter drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + private Matrix matrix = new Matrix(); + private Set animationCallbacks = new HashSet<>(); + private Bitmap bitmap; + private static final int MSG_ANIMATION_START = 1; + private static final int MSG_ANIMATION_END = 2; + private Handler uiHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_ANIMATION_START: + for (AnimationCallback animationCallback : animationCallbacks) { + animationCallback.onAnimationStart(FrameAnimationDrawable.this); + } + break; + case MSG_ANIMATION_END: + for (AnimationCallback animationCallback : animationCallbacks) { + animationCallback.onAnimationEnd(FrameAnimationDrawable.this); + } + break; + } + } + }; + private Runnable invalidateRunnable = new Runnable() { + @Override + public void run() { + invalidateSelf(); + } + }; + private boolean autoPlay = true; + + public FrameAnimationDrawable(Decoder frameSeqDecoder) { + paint.setAntiAlias(true); + this.frameSeqDecoder = frameSeqDecoder; + } + + public FrameAnimationDrawable(Loader provider) { + paint.setAntiAlias(true); + this.frameSeqDecoder = createFrameSeqDecoder(provider, this); + } + + public void setAutoPlay(boolean autoPlay) { + this.autoPlay = autoPlay; + } + + protected abstract Decoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener); + + /** + * @param loopLimit <=0为无限播放,>0为实际播放次数 + */ + public void setLoopLimit(int loopLimit) { + frameSeqDecoder.setLoopLimit(loopLimit); + } + + public void reset() { + frameSeqDecoder.reset(); + } + + public void pause() { + frameSeqDecoder.pause(); + } + + public void resume() { + frameSeqDecoder.resume(); + } + + public boolean isPaused() { + return frameSeqDecoder.isPaused(); + } + + @Override + public void start() { + if (autoPlay) { + frameSeqDecoder.start(); + } else { + this.frameSeqDecoder.addRenderListener(this); + if (!this.frameSeqDecoder.isRunning()) { + this.frameSeqDecoder.start(); + } + } + } + + @Override + public void stop() { + if (autoPlay) { + frameSeqDecoder.stop(); + } else { + this.frameSeqDecoder.removeRenderListener(this); + this.frameSeqDecoder.stopIfNeeded(); + } + } + + @Override + public boolean isRunning() { + return frameSeqDecoder.isRunning(); + } + + @Override + public void draw(Canvas canvas) { + if (bitmap == null || bitmap.isRecycled()) { + return; + } + canvas.setDrawFilter(drawFilter); + canvas.drawBitmap(bitmap, matrix, paint); + } + + @Override + public void setBounds(int left, int top, int right, int bottom) { + super.setBounds(left, top, right, bottom); + boolean sampleSizeChanged = frameSeqDecoder.setDesiredSize(getBounds().width(), getBounds().height()); + matrix.setScale( + 1.0f * getBounds().width() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().width(), + 1.0f * getBounds().height() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().height()); + + if (sampleSizeChanged) + this.bitmap = Bitmap.createBitmap( + frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(), + frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(), + Bitmap.Config.ARGB_8888); + } + + @Override + public void setAlpha(int alpha) { + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + paint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void onStart() { + Message.obtain(uiHandler, MSG_ANIMATION_START).sendToTarget(); + } + + @Override + public void onRender(ByteBuffer byteBuffer) { + if (!isRunning()) { + return; + } + if (this.bitmap == null || this.bitmap.isRecycled()) { + this.bitmap = Bitmap.createBitmap( + frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(), + frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(), + Bitmap.Config.ARGB_8888); + } + byteBuffer.rewind(); + if (byteBuffer.remaining() < this.bitmap.getByteCount()) { + Log.e(TAG, "onRender:Buffer not large enough for pixels"); + return; + } + this.bitmap.copyPixelsFromBuffer(byteBuffer); + uiHandler.post(invalidateRunnable); + } + + @Override + public void onEnd() { + Message.obtain(uiHandler, MSG_ANIMATION_END).sendToTarget(); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + if (this.autoPlay) { + if (visible) { + if (!isRunning()) { + start(); + } + } else if (isRunning()) { + stop(); + } + } + return super.setVisible(visible, restart); + } + + @Override + public int getIntrinsicWidth() { + try { + return frameSeqDecoder.getBounds().width(); + } catch (Exception exception) { + return 0; + } + } + + @Override + public int getIntrinsicHeight() { + try { + return frameSeqDecoder.getBounds().height(); + } catch (Exception exception) { + return 0; + } + } + + @Override + public void registerAnimationCallback(@NonNull AnimationCallback animationCallback) { + this.animationCallbacks.add(animationCallback); + } + + @Override + public boolean unregisterAnimationCallback(@NonNull AnimationCallback animationCallback) { + return this.animationCallbacks.remove(animationCallback); + } + + @Override + public void clearAnimationCallbacks() { + this.animationCallbacks.clear(); + } +} diff --git a/app/src/main/java/org/signal/glide/common/decode/Frame.java b/app/src/main/java/org/signal/glide/common/decode/Frame.java new file mode 100644 index 000000000..e7fd5e963 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/decode/Frame.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.decode; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; + +import org.signal.glide.common.io.Reader; +import org.signal.glide.common.io.Writer; + +/** + * @Description: One frame in an animation + * @Author: pengfei.zhou + * @CreateDate: 2019-05-13 + */ +public abstract class Frame { + protected final R reader; + public int frameWidth; + public int frameHeight; + public int frameX; + public int frameY; + public int frameDuration; + + public Frame(R reader) { + this.reader = reader; + } + + public abstract Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, W writer); +} diff --git a/app/src/main/java/org/signal/glide/common/decode/FrameSeqDecoder.java b/app/src/main/java/org/signal/glide/common/decode/FrameSeqDecoder.java new file mode 100644 index 000000000..2834ee618 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/decode/FrameSeqDecoder.java @@ -0,0 +1,539 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.decode; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import org.signal.glide.Log; +import org.signal.glide.common.executor.FrameDecoderExecutor; +import org.signal.glide.common.io.Reader; +import org.signal.glide.common.io.Writer; +import org.signal.glide.common.loader.Loader; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; + +/** + * @Description: Abstract Frame Animation Decoder + * @Author: pengfei.zhou + * @CreateDate: 2019/3/27 + */ +public abstract class FrameSeqDecoder { + private static final String TAG = FrameSeqDecoder.class.getSimpleName(); + private final int taskId; + + private final Loader mLoader; + private final Handler workerHandler; + protected List frames = new ArrayList<>(); + protected int frameIndex = -1; + private int playCount; + private Integer loopLimit = null; + private Set renderListeners = new HashSet<>(); + private AtomicBoolean paused = new AtomicBoolean(true); + private static final Rect RECT_EMPTY = new Rect(); + private Runnable renderTask = new Runnable() { + @Override + public void run() { + if (paused.get()) { + return; + } + if (canStep()) { + long start = System.currentTimeMillis(); + long delay = step(); + long cost = System.currentTimeMillis() - start; + workerHandler.postDelayed(this, Math.max(0, delay - cost)); + for (RenderListener renderListener : renderListeners) { + renderListener.onRender(frameBuffer); + } + } else { + stop(); + } + } + }; + protected int sampleSize = 1; + + private Set cacheBitmaps = new HashSet<>(); + protected Map cachedCanvas = new WeakHashMap<>(); + protected ByteBuffer frameBuffer; + protected volatile Rect fullRect; + private W mWriter = getWriter(); + private R mReader = null; + + /** + * If played all the needed + */ + private boolean finished = false; + + private enum State { + IDLE, + RUNNING, + INITIALIZING, + FINISHING, + } + + private volatile State mState = State.IDLE; + + public Loader getLoader() { + return mLoader; + } + + protected abstract W getWriter(); + + protected abstract R getReader(Reader reader); + + protected Bitmap obtainBitmap(int width, int height) { + Bitmap ret = null; + Iterator iterator = cacheBitmaps.iterator(); + while (iterator.hasNext()) { + int reuseSize = width * height * 4; + ret = iterator.next(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (ret != null && ret.getAllocationByteCount() >= reuseSize) { + iterator.remove(); + if (ret.getWidth() != width || ret.getHeight() != height) { + ret.reconfigure(width, height, Bitmap.Config.ARGB_8888); + } + ret.eraseColor(0); + return ret; + } + } else { + if (ret != null && ret.getByteCount() >= reuseSize) { + if (ret.getWidth() == width && ret.getHeight() == height) { + iterator.remove(); + ret.eraseColor(0); + } + return ret; + } + } + } + + try { + Bitmap.Config config = Bitmap.Config.ARGB_8888; + ret = Bitmap.createBitmap(width, height, config); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + } + return ret; + } + + protected void recycleBitmap(Bitmap bitmap) { + if (bitmap != null && !cacheBitmaps.contains(bitmap)) { + cacheBitmaps.add(bitmap); + } + } + + /** + * 解码器的渲染回调 + */ + public interface RenderListener { + /** + * 播放开始 + */ + void onStart(); + + /** + * 帧播放 + */ + void onRender(ByteBuffer byteBuffer); + + /** + * 播放结束 + */ + void onEnd(); + } + + + /** + * @param loader webp的reader + * @param renderListener 渲染的回调 + */ + public FrameSeqDecoder(Loader loader, @Nullable RenderListener renderListener) { + this.mLoader = loader; + if (renderListener != null) { + this.renderListeners.add(renderListener); + } + this.taskId = FrameDecoderExecutor.getInstance().generateTaskId(); + this.workerHandler = new Handler(FrameDecoderExecutor.getInstance().getLooper(taskId)); + } + + + public void addRenderListener(final RenderListener renderListener) { + this.workerHandler.post(new Runnable() { + @Override + public void run() { + renderListeners.add(renderListener); + } + }); + } + + public void removeRenderListener(final RenderListener renderListener) { + this.workerHandler.post(new Runnable() { + @Override + public void run() { + renderListeners.remove(renderListener); + } + }); + } + + public void stopIfNeeded() { + this.workerHandler.post(new Runnable() { + @Override + public void run() { + if (renderListeners.size() == 0) { + stop(); + } + } + }); + } + + public Rect getBounds() { + if (fullRect == null) { + if (mState == State.FINISHING) { + Log.e(TAG, "In finishing,do not interrupt"); + } + final Thread thread = Thread.currentThread(); + workerHandler.post(new Runnable() { + @Override + public void run() { + try { + if (fullRect == null) { + if (mReader == null) { + mReader = getReader(mLoader.obtain()); + } else { + mReader.reset(); + } + initCanvasBounds(read(mReader)); + } + } catch (Exception e) { + e.printStackTrace(); + fullRect = RECT_EMPTY; + } finally { + LockSupport.unpark(thread); + } + } + }); + LockSupport.park(thread); + } + return fullRect; + } + + private void initCanvasBounds(Rect rect) { + fullRect = rect; + frameBuffer = ByteBuffer.allocate((rect.width() * rect.height() / (sampleSize * sampleSize) + 1) * 4); + if (mWriter == null) { + mWriter = getWriter(); + } + } + + + private int getFrameCount() { + return this.frames.size(); + } + + /** + * @return Loop Count defined in file + */ + protected abstract int getLoopCount(); + + public void start() { + if (fullRect == RECT_EMPTY) { + return; + } + if (mState == State.RUNNING || mState == State.INITIALIZING) { + Log.i(TAG, debugInfo() + " Already started"); + return; + } + if (mState == State.FINISHING) { + Log.e(TAG, debugInfo() + " Processing,wait for finish at " + mState); + } + mState = State.INITIALIZING; + if (Looper.myLooper() == workerHandler.getLooper()) { + innerStart(); + } else { + workerHandler.post(new Runnable() { + @Override + public void run() { + innerStart(); + } + }); + } + } + + @WorkerThread + private void innerStart() { + paused.compareAndSet(true, false); + + final long start = System.currentTimeMillis(); + try { + if (frames.size() == 0) { + try { + if (mReader == null) { + mReader = getReader(mLoader.obtain()); + } else { + mReader.reset(); + } + initCanvasBounds(read(mReader)); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } finally { + Log.i(TAG, debugInfo() + " Set state to RUNNING,cost " + (System.currentTimeMillis() - start)); + mState = State.RUNNING; + } + if (getNumPlays() == 0 || !finished) { + this.frameIndex = -1; + renderTask.run(); + for (RenderListener renderListener : renderListeners) { + renderListener.onStart(); + } + } else { + Log.i(TAG, debugInfo() + " No need to started"); + } + } + + @WorkerThread + private void innerStop() { + workerHandler.removeCallbacks(renderTask); + frames.clear(); + for (Bitmap bitmap : cacheBitmaps) { + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + } + cacheBitmaps.clear(); + if (frameBuffer != null) { + frameBuffer = null; + } + cachedCanvas.clear(); + try { + if (mReader != null) { + mReader.close(); + mReader = null; + } + if (mWriter != null) { + mWriter.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + release(); + mState = State.IDLE; + for (RenderListener renderListener : renderListeners) { + renderListener.onEnd(); + } + } + + public void stop() { + if (fullRect == RECT_EMPTY) { + return; + } + if (mState == State.FINISHING || mState == State.IDLE) { + Log.i(TAG, debugInfo() + "No need to stop"); + return; + } + if (mState == State.INITIALIZING) { + Log.e(TAG, debugInfo() + "Processing,wait for finish at " + mState); + } + mState = State.FINISHING; + if (Looper.myLooper() == workerHandler.getLooper()) { + innerStop(); + } else { + workerHandler.post(new Runnable() { + @Override + public void run() { + innerStop(); + } + }); + } + } + + private String debugInfo() { + return ""; + } + + protected abstract void release(); + + public boolean isRunning() { + return mState == State.RUNNING || mState == State.INITIALIZING; + } + + public boolean isPaused() { + return paused.get(); + } + + public void setLoopLimit(int limit) { + this.loopLimit = limit; + } + + public void reset() { + this.playCount = 0; + this.frameIndex = -1; + this.finished = false; + } + + public void pause() { + workerHandler.removeCallbacks(renderTask); + paused.compareAndSet(false, true); + } + + public void resume() { + paused.compareAndSet(true, false); + workerHandler.removeCallbacks(renderTask); + workerHandler.post(renderTask); + } + + + public int getSampleSize() { + return sampleSize; + } + + public boolean setDesiredSize(int width, int height) { + boolean sampleSizeChanged = false; + int sample = getDesiredSample(width, height); + if (sample != this.sampleSize) { + this.sampleSize = sample; + sampleSizeChanged = true; + final boolean tempRunning = isRunning(); + workerHandler.removeCallbacks(renderTask); + workerHandler.post(new Runnable() { + @Override + public void run() { + innerStop(); + try { + initCanvasBounds(read(getReader(mLoader.obtain()))); + if (tempRunning) { + innerStart(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + return sampleSizeChanged; + } + + protected int getDesiredSample(int desiredWidth, int desiredHeight) { + if (desiredWidth == 0 || desiredHeight == 0) { + return 1; + } + int radio = Math.min(getBounds().width() / desiredWidth, getBounds().height() / desiredHeight); + int sample = 1; + while ((sample * 2) <= radio) { + sample *= 2; + } + return sample; + } + + protected abstract Rect read(R reader) throws IOException; + + private int getNumPlays() { + return this.loopLimit != null ? this.loopLimit : this.getLoopCount(); + } + + private boolean canStep() { + if (!isRunning()) { + return false; + } + if (frames.size() == 0) { + return false; + } + if (getNumPlays() <= 0) { + return true; + } + if (this.playCount < getNumPlays() - 1) { + return true; + } else if (this.playCount == getNumPlays() - 1 && this.frameIndex < this.getFrameCount() - 1) { + return true; + } + finished = true; + return false; + } + + @WorkerThread + private long step() { + this.frameIndex++; + if (this.frameIndex >= this.getFrameCount()) { + this.frameIndex = 0; + this.playCount++; + } + Frame frame = getFrame(this.frameIndex); + if (frame == null) { + return 0; + } + renderFrame(frame); + return frame.frameDuration; + } + + protected abstract void renderFrame(Frame frame); + + private Frame getFrame(int index) { + if (index < 0 || index >= frames.size()) { + return null; + } + return frames.get(index); + } + + /** + * Get Indexed frame + * + * @param index <0 means reverse from last index + */ + public Bitmap getFrameBitmap(int index) throws IOException { + if (mState != State.IDLE) { + Log.e(TAG, debugInfo() + ",stop first"); + return null; + } + mState = State.RUNNING; + paused.compareAndSet(true, false); + if (frames.size() == 0) { + if (mReader == null) { + mReader = getReader(mLoader.obtain()); + } else { + mReader.reset(); + } + initCanvasBounds(read(mReader)); + } + if (index < 0) { + index += this.frames.size(); + } + if (index < 0) { + index = 0; + } + frameIndex = -1; + while (frameIndex < index) { + if (canStep()) { + step(); + } else { + break; + } + } + frameBuffer.rewind(); + Bitmap bitmap = Bitmap.createBitmap(getBounds().width() / getSampleSize(), getBounds().height() / getSampleSize(), Bitmap.Config.ARGB_8888); + bitmap.copyPixelsFromBuffer(frameBuffer); + innerStop(); + return bitmap; + } +} diff --git a/app/src/main/java/org/signal/glide/common/executor/FrameDecoderExecutor.java b/app/src/main/java/org/signal/glide/common/executor/FrameDecoderExecutor.java new file mode 100644 index 000000000..6a7aae089 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/executor/FrameDecoderExecutor.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.executor; + +import android.os.HandlerThread; +import android.os.Looper; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @Description: com.github.penfeizhou.animation.executor + * @Author: pengfei.zhou + * @CreateDate: 2019-11-21 + */ +public class FrameDecoderExecutor { + private static int sPoolNumber = 4; + private ArrayList mHandlerThreadGroup = new ArrayList<>(); + private AtomicInteger counter = new AtomicInteger(0); + + private FrameDecoderExecutor() { + } + + static class Inner { + static final FrameDecoderExecutor sInstance = new FrameDecoderExecutor(); + } + + public void setPoolSize(int size) { + sPoolNumber = size; + } + + public static FrameDecoderExecutor getInstance() { + return Inner.sInstance; + } + + public Looper getLooper(int taskId) { + int idx = taskId % sPoolNumber; + if (idx >= mHandlerThreadGroup.size()) { + HandlerThread handlerThread = new HandlerThread("FrameDecoderExecutor-" + idx); + handlerThread.start(); + + mHandlerThreadGroup.add(handlerThread); + Looper looper = handlerThread.getLooper(); + if (looper != null) { + return looper; + } else { + return Looper.getMainLooper(); + } + } else { + if (mHandlerThreadGroup.get(idx) != null) { + Looper looper = mHandlerThreadGroup.get(idx).getLooper(); + if (looper != null) { + return looper; + } else { + return Looper.getMainLooper(); + } + } else { + return Looper.getMainLooper(); + } + } + } + + public int generateTaskId() { + return counter.getAndIncrement(); + } +} + diff --git a/app/src/main/java/org/signal/glide/common/io/ByteBufferReader.java b/app/src/main/java/org/signal/glide/common/io/ByteBufferReader.java new file mode 100644 index 000000000..7ed9cfa10 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/ByteBufferReader.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.io; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * @Description: APNG4Android + * @Author: pengfei.zhou + * @CreateDate: 2019-05-14 + */ +public class ByteBufferReader implements Reader { + + private final ByteBuffer byteBuffer; + + public ByteBufferReader(ByteBuffer byteBuffer) { + this.byteBuffer = byteBuffer; + byteBuffer.position(0); + } + + @Override + public long skip(long total) throws IOException { + byteBuffer.position((int) (byteBuffer.position() + total)); + return total; + } + + @Override + public byte peek() throws IOException { + return byteBuffer.get(); + } + + @Override + public void reset() throws IOException { + byteBuffer.position(0); + } + + @Override + public int position() { + return byteBuffer.position(); + } + + @Override + public int read(byte[] buffer, int start, int byteCount) throws IOException { + byteBuffer.get(buffer, start, byteCount); + return byteCount; + } + + @Override + public int available() throws IOException { + return byteBuffer.limit() - byteBuffer.position(); + } + + @Override + public void close() throws IOException { + } + + @Override + public InputStream toInputStream() throws IOException { + return new ByteArrayInputStream(byteBuffer.array()); + } +} diff --git a/app/src/main/java/org/signal/glide/common/io/ByteBufferWriter.java b/app/src/main/java/org/signal/glide/common/io/ByteBufferWriter.java new file mode 100644 index 000000000..f60688fb8 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/ByteBufferWriter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.io; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * @Description: ByteBufferWriter + * @Author: pengfei.zhou + * @CreateDate: 2019-05-12 + */ +public class ByteBufferWriter implements Writer { + + protected ByteBuffer byteBuffer; + + public ByteBufferWriter() { + reset(10 * 1024); + } + + @Override + public void putByte(byte b) { + byteBuffer.put(b); + } + + @Override + public void putBytes(byte[] b) { + byteBuffer.put(b); + } + + @Override + public int position() { + return byteBuffer.position(); + } + + @Override + public void skip(int length) { + byteBuffer.position(length + position()); + } + + @Override + public byte[] toByteArray() { + return byteBuffer.array(); + } + + @Override + public void close() { + } + + @Override + public void reset(int size) { + if (byteBuffer == null || size > byteBuffer.capacity()) { + byteBuffer = ByteBuffer.allocate(size); + this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + } + byteBuffer.clear(); + } +} diff --git a/app/src/main/java/org/signal/glide/common/io/FileReader.java b/app/src/main/java/org/signal/glide/common/io/FileReader.java new file mode 100644 index 000000000..1f21184d4 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/FileReader.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.io; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * @Description: FileReader + * @Author: pengfei.zhou + * @CreateDate: 2019-05-23 + */ +public class FileReader extends FilterReader { + private final File mFile; + + public FileReader(File file) throws IOException { + super(new StreamReader(new FileInputStream(file))); + mFile = file; + } + + @Override + public void reset() throws IOException { + reader.close(); + reader = new StreamReader(new FileInputStream(mFile)); + } +} diff --git a/app/src/main/java/org/signal/glide/common/io/FilterReader.java b/app/src/main/java/org/signal/glide/common/io/FilterReader.java new file mode 100644 index 000000000..08abbc912 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/FilterReader.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.io; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @Description: FilterReader + * @Author: pengfei.zhou + * @CreateDate: 2019-05-23 + */ +public class FilterReader implements Reader { + protected Reader reader; + + public FilterReader(Reader in) { + this.reader = in; + } + + @Override + public long skip(long total) throws IOException { + return reader.skip(total); + } + + @Override + public byte peek() throws IOException { + return reader.peek(); + } + + @Override + public void reset() throws IOException { + reader.reset(); + } + + @Override + public int position() { + return reader.position(); + } + + @Override + public int read(byte[] buffer, int start, int byteCount) throws IOException { + return reader.read(buffer, start, byteCount); + } + + @Override + public int available() throws IOException { + return reader.available(); + } + + @Override + public void close() throws IOException { + reader.close(); + } + + @Override + public InputStream toInputStream() throws IOException { + reset(); + return reader.toInputStream(); + } +} diff --git a/app/src/main/java/org/signal/glide/common/io/Reader.java b/app/src/main/java/org/signal/glide/common/io/Reader.java new file mode 100644 index 000000000..6be530b2b --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/Reader.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.io; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @link {https://developers.google.com/speed/webp/docs/riff_container#terminology_basics} + * @Author: pengfei.zhou + * @CreateDate: 2019-05-11 + */ +public interface Reader { + long skip(long total) throws IOException; + + byte peek() throws IOException; + + void reset() throws IOException; + + int position(); + + int read(byte[] buffer, int start, int byteCount) throws IOException; + + int available() throws IOException; + + /** + * close io + */ + void close() throws IOException; + + InputStream toInputStream() throws IOException; +} diff --git a/app/src/main/java/org/signal/glide/common/io/StreamReader.java b/app/src/main/java/org/signal/glide/common/io/StreamReader.java new file mode 100644 index 000000000..7eb08f51f --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/StreamReader.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * @Author: pengfei.zhou + * @CreateDate: 2019-05-11 + */ +public class StreamReader extends FilterInputStream implements Reader { + private int position; + + public StreamReader(InputStream in) { + super(in); + try { + in.reset(); + } catch (IOException e) { + // e.printStackTrace(); + } + } + + @Override + public byte peek() throws IOException { + byte ret = (byte) read(); + position++; + return ret; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int ret = super.read(b, off, len); + position += Math.max(0, ret); + return ret; + } + + @Override + public synchronized void reset() throws IOException { + super.reset(); + position = 0; + } + + @Override + public long skip(long n) throws IOException { + long ret = super.skip(n); + position += ret; + return ret; + } + + @Override + public int position() { + return position; + } + + @Override + public InputStream toInputStream() throws IOException { + return this; + } +} diff --git a/app/src/main/java/org/signal/glide/common/io/Writer.java b/app/src/main/java/org/signal/glide/common/io/Writer.java new file mode 100644 index 000000000..84600a08f --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/Writer.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.io; + +import java.io.IOException; + +/** + * @Description: APNG4Android + * @Author: pengfei.zhou + * @CreateDate: 2019-05-12 + */ +public interface Writer { + void reset(int size); + + void putByte(byte b); + + void putBytes(byte[] b); + + int position(); + + void skip(int length); + + byte[] toByteArray(); + + void close() throws IOException; +} diff --git a/app/src/main/java/org/signal/glide/common/loader/AssetStreamLoader.java b/app/src/main/java/org/signal/glide/common/loader/AssetStreamLoader.java new file mode 100644 index 000000000..d62ac7200 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/AssetStreamLoader.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.loader; + +import android.content.Context; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @Description: 从Asset中读取流 + * @Author: pengfei.zhou + * @CreateDate: 2019/3/28 + */ +public class AssetStreamLoader extends StreamLoader { + + private final Context mContext; + private final String mAssetName; + + public AssetStreamLoader(Context context, String assetName) { + mContext = context.getApplicationContext(); + mAssetName = assetName; + } + + @Override + protected InputStream getInputStream() throws IOException { + return mContext.getAssets().open(mAssetName); + } +} diff --git a/app/src/main/java/org/signal/glide/common/loader/ByteBufferLoader.java b/app/src/main/java/org/signal/glide/common/loader/ByteBufferLoader.java new file mode 100644 index 000000000..05bf8d36e --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/ByteBufferLoader.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.loader; + +import org.signal.glide.common.io.ByteBufferReader; +import org.signal.glide.common.io.Reader; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * @Description: ByteBufferLoader + * @Author: pengfei.zhou + * @CreateDate: 2019-05-15 + */ +public abstract class ByteBufferLoader implements Loader { + public abstract ByteBuffer getByteBuffer(); + + @Override + public Reader obtain() throws IOException { + return new ByteBufferReader(getByteBuffer()); + } +} diff --git a/app/src/main/java/org/signal/glide/common/loader/FileLoader.java b/app/src/main/java/org/signal/glide/common/loader/FileLoader.java new file mode 100644 index 000000000..e861aa7ed --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/FileLoader.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.loader; + +import org.signal.glide.common.io.FileReader; +import org.signal.glide.common.io.Reader; + +import java.io.File; +import java.io.IOException; + +/** + * @Description: 从文件加载流 + * @Author: pengfei.zhou + * @CreateDate: 2019/3/28 + */ +public class FileLoader implements Loader { + + private final File mFile; + private Reader mReader; + + public FileLoader(String path) { + mFile = new File(path); + } + + @Override + public synchronized Reader obtain() throws IOException { + return new FileReader(mFile); + } +} diff --git a/app/src/main/java/org/signal/glide/common/loader/Loader.java b/app/src/main/java/org/signal/glide/common/loader/Loader.java new file mode 100644 index 000000000..9a38babb2 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/Loader.java @@ -0,0 +1,19 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.loader; + +import org.signal.glide.common.io.Reader; + +import java.io.IOException; + +/** + * @Description: Loader + * @Author: pengfei.zhou + * @CreateDate: 2019-05-14 + */ +public interface Loader { + Reader obtain() throws IOException; +} diff --git a/app/src/main/java/org/signal/glide/common/loader/ResourceStreamLoader.java b/app/src/main/java/org/signal/glide/common/loader/ResourceStreamLoader.java new file mode 100644 index 000000000..5d6db5a84 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/ResourceStreamLoader.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.loader; + +import android.content.Context; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @Description: 从资源加载流 + * @Author: pengfei.zhou + * @CreateDate: 2019/3/28 + */ +public class ResourceStreamLoader extends StreamLoader { + private final Context mContext; + private final int mResId; + + + public ResourceStreamLoader(Context context, int resId) { + mContext = context.getApplicationContext(); + mResId = resId; + } + + @Override + protected InputStream getInputStream() throws IOException { + return mContext.getResources().openRawResource(mResId); + } +} diff --git a/app/src/main/java/org/signal/glide/common/loader/StreamLoader.java b/app/src/main/java/org/signal/glide/common/loader/StreamLoader.java new file mode 100644 index 000000000..0103ca80f --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/StreamLoader.java @@ -0,0 +1,25 @@ +/* + * Copyright 2019 Zhou Pengfei + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.signal.glide.common.loader; + +import org.signal.glide.common.io.Reader; +import org.signal.glide.common.io.StreamReader; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @Author: pengfei.zhou + * @CreateDate: 2019/3/28 + */ +public abstract class StreamLoader implements Loader { + protected abstract InputStream getInputStream() throws IOException; + + + public final synchronized Reader obtain() throws IOException { + return new StreamReader(getInputStream()); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 176b6472d..b6b7ffa05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -22,6 +22,7 @@ import android.os.AsyncTask; import android.os.Build; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDelegate; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; @@ -32,6 +33,7 @@ import com.google.android.gms.security.ProviderInstaller; import org.conscrypt.Conscrypt; import org.signal.aesgcmprovider.AesGcmProvider; +import org.signal.glide.SignalGlideCodecs; import org.signal.ringrtc.CallManager; import org.thoughtcrime.securesms.components.TypingStatusRepository; import org.thoughtcrime.securesms.components.TypingStatusSender; @@ -127,6 +129,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi initializePendingMessages(); initializeBlobProvider(); initializeCleanup(); + initializeGlideCodecs(); FeatureFlags.init(); NotificationChannels.create(this); @@ -378,6 +381,35 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi }); } + private void initializeGlideCodecs() { + SignalGlideCodecs.setLogProvider(new org.signal.glide.Log.Provider() { + @Override + public void v(@NonNull String tag, @NonNull String message) { + Log.v(tag, message); + } + + @Override + public void d(@NonNull String tag, @NonNull String message) { + Log.d(tag, message); + } + + @Override + public void i(@NonNull String tag, @NonNull String message) { + Log.i(tag, message); + } + + @Override + public void w(@NonNull String tag, @NonNull String message) { + Log.w(tag, message); + } + + @Override + public void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) { + Log.e(tag, message, throwable); + } + }); + } + @Override protected void attachBaseContext(Context base) { super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base))); diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java new file mode 100644 index 000000000..90ad63825 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java @@ -0,0 +1,73 @@ +package org.thoughtcrime.securesms.glide.cache; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.engine.Resource; + +import org.signal.glide.apng.decode.APNGDecoder; +import org.signal.glide.apng.decode.APNGParser; +import org.signal.glide.common.io.ByteBufferReader; +import org.signal.glide.common.loader.ByteBufferLoader; +import org.signal.glide.common.loader.Loader; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class ApngBufferCacheDecoder implements ResourceDecoder { + + @Override + public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) { + return APNGParser.isAPNG(new ByteBufferReader(source)); + } + + @Override + public @Nullable Resource decode(@NonNull final ByteBuffer source, int width, int height, @NonNull Options options) throws IOException { + if (!APNGParser.isAPNG(new ByteBufferReader(source))) { + return null; + } + + Loader loader = new ByteBufferLoader() { + @Override + public ByteBuffer getByteBuffer() { + source.position(0); + return source; + } + }; + + return new FrameSeqDecoderResource(new APNGDecoder(loader, null), source.limit()); + } + + private static class FrameSeqDecoderResource implements Resource { + private final APNGDecoder decoder; + private final int size; + + FrameSeqDecoderResource(@NonNull APNGDecoder decoder, int size) { + this.decoder = decoder; + this.size = size; + } + + @Override + public @NonNull Class getResourceClass() { + return APNGDecoder.class; + } + + @Override + public @NonNull APNGDecoder get() { + return this.decoder; + } + + @Override + public int getSize() { + return this.size; + } + + @Override + public void recycle() { + this.decoder.stop(); + } + } +} + diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngFrameDrawableTranscoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngFrameDrawableTranscoder.java new file mode 100644 index 000000000..f12af6a81 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngFrameDrawableTranscoder.java @@ -0,0 +1,48 @@ +package org.thoughtcrime.securesms.glide.cache; + +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.resource.drawable.DrawableResource; +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; + +import org.signal.glide.apng.APNGDrawable; +import org.signal.glide.apng.decode.APNGDecoder; + +public class ApngFrameDrawableTranscoder implements ResourceTranscoder { + + @Override + public @Nullable Resource transcode(@NonNull Resource toTranscode, @NonNull Options options) { + APNGDecoder decoder = toTranscode.get(); + APNGDrawable drawable = new APNGDrawable(decoder); + + drawable.setAutoPlay(false); + drawable.setLoopLimit(0); + + return new DrawableResource(drawable) { + @Override + public @NonNull Class getResourceClass() { + return Drawable.class; + } + + @Override + public int getSize() { + return 0; + } + + @Override + public void recycle() { + } + + @Override + public void initialize() { + super.initialize(); + } + }; + } +} + diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java new file mode 100644 index 000000000..0cb7cd021 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java @@ -0,0 +1,44 @@ +package org.thoughtcrime.securesms.glide.cache; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.engine.Resource; + +import org.signal.glide.apng.decode.APNGDecoder; +import org.signal.glide.apng.decode.APNGParser; +import org.signal.glide.common.io.StreamReader; +import org.thoughtcrime.securesms.util.Util; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +public class ApngStreamCacheDecoder implements ResourceDecoder { + + private final ResourceDecoder byteBufferDecoder; + + public ApngStreamCacheDecoder(ResourceDecoder byteBufferDecoder) { + this.byteBufferDecoder = byteBufferDecoder; + } + + @Override + public boolean handles(@NonNull InputStream source, @NonNull Options options) { + return APNGParser.isAPNG(new StreamReader(source)); + } + + @Override + public @Nullable Resource decode(@NonNull final InputStream source, int width, int height, @NonNull Options options) throws IOException { + byte[] data = Util.readFully(source); + + if (data == null) { + return null; + } + + ByteBuffer byteBuffer = ByteBuffer.wrap(data); + return byteBufferDecoder.decode(byteBuffer, width, height, options); + } +} + diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheEncoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheEncoder.java new file mode 100644 index 000000000..852cc8912 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheEncoder.java @@ -0,0 +1,51 @@ +package org.thoughtcrime.securesms.glide.cache; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.load.EncodeStrategy; +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.ResourceEncoder; +import com.bumptech.glide.load.engine.Resource; + +import org.signal.glide.apng.decode.APNGDecoder; +import org.signal.glide.common.loader.Loader; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.util.Util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class EncryptedApngCacheEncoder extends EncryptedCoder implements ResourceEncoder { + + private static final String TAG = Log.tag(EncryptedApngCacheEncoder.class); + + private final byte[] secret; + + public EncryptedApngCacheEncoder(@NonNull byte[] secret) { + this.secret = secret; + } + + @Override + public @NonNull EncodeStrategy getEncodeStrategy(@NonNull Options options) { + return EncodeStrategy.SOURCE; + } + + @Override + public boolean encode(@NonNull Resource data, @NonNull File file, @NonNull Options options) { + try { + Loader loader = data.get().getLoader(); + InputStream input = loader.obtain().toInputStream(); + OutputStream output = createEncryptedOutputStream(secret, file); + + Util.copy(input, output); + return true; + } catch (IOException e) { + Log.w(TAG, e); + } + + return false; + } +} + diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapCacheDecoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapCacheDecoder.java deleted file mode 100644 index 6f99927a1..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapCacheDecoder.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.thoughtcrime.securesms.glide.cache; - - -import android.graphics.Bitmap; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import org.thoughtcrime.securesms.logging.Log; - -import com.bumptech.glide.load.Options; -import com.bumptech.glide.load.ResourceDecoder; -import com.bumptech.glide.load.engine.Resource; -import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; - -public class EncryptedBitmapCacheDecoder extends EncryptedCoder implements ResourceDecoder { - - private static final String TAG = EncryptedBitmapCacheDecoder.class.getSimpleName(); - - private final StreamBitmapDecoder streamBitmapDecoder; - private final byte[] secret; - - public EncryptedBitmapCacheDecoder(@NonNull byte[] secret, @NonNull StreamBitmapDecoder streamBitmapDecoder) { - this.secret = secret; - this.streamBitmapDecoder = streamBitmapDecoder; - } - - @Override - public boolean handles(@NonNull File source, @NonNull Options options) - throws IOException - { - try (InputStream inputStream = createEncryptedInputStream(secret, source)) { - return streamBitmapDecoder.handles(inputStream, options); - } catch (IOException e) { - Log.w(TAG, e); - return false; - } - } - - @Override - public @Nullable Resource decode(@NonNull File source, int width, int height, @NonNull Options options) - throws IOException - { - try (InputStream inputStream = createEncryptedInputStream(secret, source)) { - return streamBitmapDecoder.decode(inputStream, width, height, options); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapResourceEncoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapResourceEncoder.java index 7541e73d9..38864e70a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapResourceEncoder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapResourceEncoder.java @@ -33,8 +33,6 @@ public class EncryptedBitmapResourceEncoder extends EncryptedCoder implements Re @SuppressWarnings("EmptyCatchBlock") @Override public boolean encode(@NonNull Resource data, @NonNull File file, @NonNull Options options) { - Log.i(TAG, "Encrypted resource encoder running: " + file.toString()); - Bitmap bitmap = data.get(); Bitmap.CompressFormat format = getFormat(bitmap, options); int quality = options.get(BitmapEncoder.COMPRESSION_QUALITY); diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheDecoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheDecoder.java new file mode 100644 index 000000000..92315a720 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheDecoder.java @@ -0,0 +1,44 @@ +package org.thoughtcrime.securesms.glide.cache; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.engine.Resource; + +import org.thoughtcrime.securesms.logging.Log; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +public class EncryptedCacheDecoder extends EncryptedCoder implements ResourceDecoder { + + private static final String TAG = Log.tag(EncryptedCacheDecoder.class); + + private final byte[] secret; + private final ResourceDecoder decoder; + + public EncryptedCacheDecoder(byte[] secret, ResourceDecoder decoder) { + this.secret = secret; + this.decoder = decoder; + } + + @Override + public boolean handles(@NonNull File source, @NonNull Options options) throws IOException { + try (InputStream inputStream = createEncryptedInputStream(secret, source)) { + return decoder.handles(inputStream, options); + } catch (IOException e) { + Log.w(TAG, e); + return false; + } + } + + @Override + public @Nullable Resource decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException { + try (InputStream inputStream = createEncryptedInputStream(secret, source)) { + return decoder.decode(inputStream, width, height, options); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheEncoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheEncoder.java index 1d546ccf5..1744efcae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheEncoder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheEncoder.java @@ -30,8 +30,6 @@ public class EncryptedCacheEncoder extends EncryptedCoder implements Encoder { - - private static final String TAG = EncryptedGifCacheDecoder.class.getSimpleName(); - - private final byte[] secret; - private final StreamGifDecoder gifDecoder; - - public EncryptedGifCacheDecoder(@NonNull byte[] secret, @NonNull StreamGifDecoder gifDecoder) { - this.secret = secret; - this.gifDecoder = gifDecoder; - } - - @Override - public boolean handles(@NonNull File source, @NonNull Options options) { - try (InputStream inputStream = createEncryptedInputStream(secret, source)) { - return gifDecoder.handles(inputStream, options); - } catch (IOException e) { - Log.w(TAG, e); - return false; - } - } - - @Override - public @Nullable Resource decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException { - Log.i(TAG, "Encrypted GIF cache decoder running..."); - try (InputStream inputStream = createEncryptedInputStream(secret, source)) { - return gifDecoder.decode(inputStream, width, height, options); - } - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java index bcca70879..8fa4e41a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java @@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; import android.graphics.Bitmap; import androidx.annotation.NonNull; + +import android.graphics.drawable.Drawable; import android.util.Log; import com.bumptech.glide.Glide; @@ -27,14 +29,18 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl; +import org.thoughtcrime.securesms.glide.cache.ApngBufferCacheDecoder; +import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheEncoder; import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader; import org.thoughtcrime.securesms.glide.ContactPhotoLoader; +import org.thoughtcrime.securesms.glide.cache.ApngFrameDrawableTranscoder; import org.thoughtcrime.securesms.glide.OkHttpUrlLoader; -import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapCacheDecoder; +import org.thoughtcrime.securesms.glide.cache.ApngStreamCacheDecoder; import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder; +import org.thoughtcrime.securesms.glide.cache.EncryptedCacheDecoder; import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder; -import org.thoughtcrime.securesms.glide.cache.EncryptedGifCacheDecoder; import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder; +import org.signal.glide.apng.decode.APNGDecoder; import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.stickers.StickerRemoteUri; @@ -42,6 +48,7 @@ import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader; import java.io.File; import java.io.InputStream; +import java.nio.ByteBuffer; @GlideModule public class SignalGlideModule extends AppGlideModule { @@ -63,14 +70,25 @@ public class SignalGlideModule extends AppGlideModule { byte[] secret = attachmentSecret.getModernKey(); registry.prepend(File.class, File.class, UnitModelLoader.Factory.getInstance()); - registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool())); - registry.prepend(File.class, Bitmap.class, new EncryptedBitmapCacheDecoder(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); - registry.prepend(File.class, GifDrawable.class, new EncryptedGifCacheDecoder(secret, new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); - registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder()); + registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool())); registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret)); + registry.prepend(File.class, Bitmap.class, new EncryptedCacheDecoder<>(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); + registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret)); + registry.prepend(File.class, GifDrawable.class, new EncryptedCacheDecoder<>(secret, new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); + + ApngBufferCacheDecoder apngBufferCacheDecoder = new ApngBufferCacheDecoder(); + ApngStreamCacheDecoder apngStreamCacheDecoder = new ApngStreamCacheDecoder(apngBufferCacheDecoder); + + registry.prepend(InputStream.class, APNGDecoder.class, apngStreamCacheDecoder); + registry.prepend(ByteBuffer.class, APNGDecoder.class, apngBufferCacheDecoder); + registry.prepend(APNGDecoder.class, new EncryptedApngCacheEncoder(secret)); + registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, apngStreamCacheDecoder)); + registry.register(APNGDecoder.class, Drawable.class, new ApngFrameDrawableTranscoder()); + + registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder()); registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context)); registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));